chore: simplify honeycomb alerts (#26142)
This commit is contained in:
parent
293bb422fa
commit
f8aa4a3be0
9
bun.lock
9
bun.lock
@ -107,7 +107,6 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"solid-stripe": "0.8.1",
|
"solid-stripe": "0.8.1",
|
||||||
"svix": "1.92.2",
|
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@ -2168,8 +2167,6 @@
|
|||||||
|
|
||||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="],
|
"@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="],
|
||||||
|
|
||||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
|
||||||
|
|
||||||
"@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="],
|
"@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="],
|
||||||
|
|
||||||
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="],
|
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="],
|
||||||
@ -3180,8 +3177,6 @@
|
|||||||
|
|
||||||
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
||||||
|
|
||||||
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
|
||||||
|
|
||||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||||
@ -4656,8 +4651,6 @@
|
|||||||
|
|
||||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||||
|
|
||||||
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
|
||||||
|
|
||||||
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
@ -4726,8 +4719,6 @@
|
|||||||
|
|
||||||
"sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="],
|
"sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="],
|
||||||
|
|
||||||
"svix": ["svix@1.92.2", "", { "dependencies": { "standardwebhooks": "1.0.0" } }, "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ=="],
|
|
||||||
|
|
||||||
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
|
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { domain } from "./stage"
|
import { domain } from "./stage"
|
||||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||||
|
import { SECRET } from "./secret"
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// DATABASE
|
// DATABASE
|
||||||
@ -221,8 +222,6 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
|||||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||||
properties: { value: stripeWebhook.secret },
|
properties: { value: stripeWebhook.secret },
|
||||||
})
|
})
|
||||||
const INCIDENT_WEBHOOK_SIGNING_SECRET = new sst.Secret("INCIDENT_WEBHOOK_SIGNING_SECRET")
|
|
||||||
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
|
|
||||||
|
|
||||||
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||||
|
|
||||||
@ -233,6 +232,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
|||||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||||
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||||
|
|
||||||
|
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
|
||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
@ -254,8 +254,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
INCIDENT_WEBHOOK_SIGNING_SECRET,
|
|
||||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||||
|
SECRET.HoneycombWebhookSecret,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
|
|||||||
@ -1,318 +1,91 @@
|
|||||||
const displayName = (s: string) =>
|
import { SECRET } from "./secret"
|
||||||
s
|
import { domain } from "./stage"
|
||||||
.split("-")
|
|
||||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
||||||
.join(" ")
|
|
||||||
.replace(/(?<=\d) (?=\d)/g, ".")
|
|
||||||
|
|
||||||
const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "")
|
const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", {
|
||||||
|
name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`,
|
||||||
const varSpec = (label: string, name: string) =>
|
url: `https://${domain}/honeycomb/webhook`,
|
||||||
$jsonStringify({
|
secret: SECRET.HoneycombWebhookSecret.result,
|
||||||
content: [
|
|
||||||
{
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
attrs: {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
missing: false,
|
|
||||||
},
|
|
||||||
type: "varSpec",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: "paragraph",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: "doc",
|
|
||||||
})
|
|
||||||
|
|
||||||
const fields = {
|
|
||||||
model: incident.getAlertAttributeOutput({ name: "Model" }),
|
|
||||||
product: incident.getAlertAttributeOutput({ name: "Product" }),
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertSource = new incident.AlertSource("HoneycombAlertSource", {
|
|
||||||
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
|
|
||||||
sourceType: "honeycomb",
|
|
||||||
template: {
|
|
||||||
title: {
|
|
||||||
literal: varSpec("Payload -> Title", "title"),
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
literal: varSpec("Payload -> Description", "description"),
|
|
||||||
},
|
|
||||||
attributes: [
|
|
||||||
{
|
|
||||||
alertAttributeId: fields.model.id,
|
|
||||||
binding: {
|
|
||||||
value: {
|
|
||||||
reference: 'expressions["model"]',
|
|
||||||
},
|
|
||||||
mergeStrategy: "first_wins",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alertAttributeId: fields.product.id,
|
|
||||||
binding: {
|
|
||||||
value: {
|
|
||||||
reference: 'expressions["product"]',
|
|
||||||
},
|
|
||||||
mergeStrategy: "first_wins",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
expressions: [
|
|
||||||
{
|
|
||||||
label: "Model",
|
|
||||||
operations: [
|
|
||||||
{
|
|
||||||
operationType: "parse",
|
|
||||||
parse: {
|
|
||||||
returns: {
|
|
||||||
array: false,
|
|
||||||
type: fields.model.type,
|
|
||||||
},
|
|
||||||
source: "$['model']",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reference: "model",
|
|
||||||
rootReference: "payload",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Product",
|
|
||||||
operations: [
|
|
||||||
{
|
|
||||||
operationType: "parse",
|
|
||||||
parse: {
|
|
||||||
returns: {
|
|
||||||
array: false,
|
|
||||||
type: fields.product.type,
|
|
||||||
},
|
|
||||||
source: "$['product']",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reference: "product",
|
|
||||||
rootReference: "payload",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, {
|
|
||||||
name: $app.stage === "production" ? "Incident.io" : `Incident.io (${$app.stage})`,
|
|
||||||
url: alertSource.alertEventsUrl,
|
|
||||||
secret: alertSource.secretToken,
|
|
||||||
templates: [
|
templates: [
|
||||||
{
|
{
|
||||||
type: "trigger",
|
type: "trigger",
|
||||||
body: $jsonStringify({
|
body: `{
|
||||||
title: "{{ .Name }}",
|
"url": {{ .Result.URL | quote }},
|
||||||
description: "{{ .Description }}",
|
"type": {{ .Vars.type | quote }},
|
||||||
status: "{{ .Alert.Status }}",
|
"name": {{ .Name | quote }},
|
||||||
deduplication_key: "{{ .Alert.InstanceID }}",
|
"status": {{ .Alert.Status | quote }},
|
||||||
source_url: "{{ .Result.URL }}",
|
"isTest": {{ .Alert.IsTest }},
|
||||||
model: "{{ .Vars.model }}",
|
"groups": {{ .Result.GroupsTriggered | toJson }}
|
||||||
product: "{{ .Vars.product }}",
|
}`,
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
variables: [
|
variables: [
|
||||||
{
|
{
|
||||||
name: "model",
|
name: "type",
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "product",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
new incident.AlertRoute("HoneycombAlertRoute", {
|
const modelHttpErrorsQuery = (product: "go" | "zen") => {
|
||||||
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
|
const filters = [
|
||||||
enabled: true,
|
{ column: "model", op: "exists" },
|
||||||
isPrivate: false,
|
{ column: "event_type", op: "=", value: "completions" },
|
||||||
alertSources: [
|
{ column: "user_agent", op: "contains", value: "opencode" },
|
||||||
{
|
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
||||||
alertSourceId: alertSource.id,
|
]
|
||||||
conditionGroups: [
|
|
||||||
{
|
return honeycomb.getQuerySpecificationOutput({
|
||||||
conditions: [
|
breakdowns: ["model"],
|
||||||
{
|
calculatedFields: [
|
||||||
subject: "alert.title",
|
|
||||||
operation: "is_set",
|
|
||||||
paramBindings: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
conditionGroups: [
|
|
||||||
{
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
subject: "alert.title",
|
|
||||||
operation: "is_set",
|
|
||||||
paramBindings: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
expressions: [],
|
|
||||||
escalationConfig: {
|
|
||||||
autoCancelEscalations: true,
|
|
||||||
escalationTargets: [],
|
|
||||||
},
|
|
||||||
incidentConfig: {
|
|
||||||
autoDeclineEnabled: true,
|
|
||||||
enabled: true,
|
|
||||||
conditionGroups: [],
|
|
||||||
deferTimeSeconds: 0,
|
|
||||||
groupingKeys: [
|
|
||||||
{
|
{
|
||||||
reference: $interpolate`alert.attributes.${fields.model.id}`,
|
name: "is_failed_http_status",
|
||||||
},
|
expression: `IF(AND(GTE($status, "400"), NOT(EQUALS($status, "401"))), 1, 0)`,
|
||||||
{
|
|
||||||
reference: $interpolate`alert.attributes.${fields.product.id}`,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
groupingWindowSeconds: 3600,
|
|
||||||
},
|
|
||||||
incidentTemplate: {
|
|
||||||
name: {
|
|
||||||
value: {
|
|
||||||
literal: varSpec("Alert -> Title", "alert.title"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
value: {
|
|
||||||
literal: varSpec("Alert -> Description", "alert.description"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
startInTriage: {
|
|
||||||
value: {
|
|
||||||
literal: "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
mergeStrategy: "first-wins",
|
|
||||||
},
|
|
||||||
incidentMode: {
|
|
||||||
value: {
|
|
||||||
literal: $app.stage === "production" ? "standard" : "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type Product = "go" | "zen"
|
|
||||||
|
|
||||||
type Trigger = (opts: { model: string; product: Product }) => {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
json: honeycomb.GetQuerySpecificationOutputArgs
|
|
||||||
threshold: { op: ">=" | "<="; value: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
type Model = { id: string; products: Product[]; triggers: Trigger[] }
|
|
||||||
|
|
||||||
const httpErrors: Trigger = ({ model, product }) => ({
|
|
||||||
id: "increased-http-errors",
|
|
||||||
title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`,
|
|
||||||
description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`,
|
|
||||||
json: {
|
|
||||||
calculations: [
|
calculations: [
|
||||||
{
|
{ op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
|
||||||
op: "COUNT",
|
{ op: "SUM", name: "FAILED", column: "is_failed_http_status", filterCombination: "AND", filters },
|
||||||
name: "TOTAL",
|
|
||||||
filterCombination: "AND",
|
|
||||||
filters: [
|
|
||||||
{ column: "model", op: "=", value: model },
|
|
||||||
{ column: "event_type", op: "=", value: "completions" },
|
|
||||||
{ column: "user_agent", op: "contains", value: "opencode" },
|
|
||||||
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: "COUNT",
|
|
||||||
name: "FAILED",
|
|
||||||
filterCombination: "AND",
|
|
||||||
filters: [
|
|
||||||
{ column: "model", op: "=", value: model },
|
|
||||||
{ column: "event_type", op: "=", value: "completions" },
|
|
||||||
{ column: "user_agent", op: "contains", value: "opencode" },
|
|
||||||
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
|
|
||||||
{ column: "status", op: ">=", value: "400" },
|
|
||||||
{ column: "status", op: "!=", value: "401" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }],
|
formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 2500), DIV($FAILED, $TOTAL), 0)" }],
|
||||||
timeRange: 900,
|
timeRange: 900,
|
||||||
},
|
}).json
|
||||||
threshold: { op: ">=", value: 0.8 },
|
}
|
||||||
|
|
||||||
|
const description = "Managed by SST (Don't edit in Honeycomb UI)"
|
||||||
|
|
||||||
|
new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
|
||||||
|
name: "Increased Model HTTP Errors [Go]",
|
||||||
|
description,
|
||||||
|
queryJson: modelHttpErrorsQuery("go"),
|
||||||
|
alertType: "on_change",
|
||||||
|
frequency: 300,
|
||||||
|
thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }],
|
||||||
|
recipients: [
|
||||||
|
// {
|
||||||
|
// id: webhookRecipient.id,
|
||||||
|
// notificationDetails: [
|
||||||
|
// {
|
||||||
|
// variables: [{ name: "type", value: "model_http_errors" }],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const models: Model[] = [
|
new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
|
||||||
{ id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] },
|
name: "Increased Model HTTP Errors [Zen]",
|
||||||
{ id: "kimi-k2.5", products: ["go", "zen"], triggers: [httpErrors] },
|
description,
|
||||||
{ id: "deepseek-v4-flash", products: ["go", "zen"], triggers: [httpErrors] },
|
queryJson: modelHttpErrorsQuery("zen"),
|
||||||
{ id: "deepseek-v4-pro", products: ["go", "zen"], triggers: [httpErrors] },
|
alertType: "on_change",
|
||||||
{ id: "glm-5.1", products: ["go", "zen"], triggers: [httpErrors] },
|
frequency: 300,
|
||||||
// { id: "glm-5", products: ["go"], triggers: [httpErrors] },
|
thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }],
|
||||||
{ id: "qwen3.6-plus", products: ["go", "zen"], triggers: [httpErrors] },
|
recipients: [
|
||||||
{ id: "qwen3.5-plus", products: ["go"], triggers: [httpErrors] },
|
// {
|
||||||
{ id: "minimax-m2.7", products: ["go", "zen"], triggers: [httpErrors] },
|
// id: webhookRecipient.id,
|
||||||
// { id: "minimax-m2.5", products: ["go", "zen"], triggers: [httpErrors] },
|
// notificationDetails: [
|
||||||
{ id: "mimo-v2.5-pro", products: ["go"], triggers: [httpErrors] },
|
// {
|
||||||
// { id: "mimo-v2.5", products: ["go"], triggers: [httpErrors] },
|
// variables: [{ name: "type", value: "model_http_errors" }],
|
||||||
// { id: "mimo-v2-omni", products: ["go"], triggers: [httpErrors] },
|
// },
|
||||||
// { id: "mimo-v2-pro", products: ["go"], triggers: [httpErrors] },
|
// ],
|
||||||
{ id: "claude-opus-4-7", products: ["zen"], triggers: [httpErrors] },
|
// },
|
||||||
// { id: "claude-opus-4-6", products: ["zen"], triggers: [httpErrors] },
|
],
|
||||||
// { id: "claude-sonnet-4-6", products: ["zen"], triggers: [httpErrors] },
|
})
|
||||||
{ id: "gpt-5.5", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
{ id: "big-pickle", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "minimax-m2.5-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "hy3-preview-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "nemotron-3-super-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "trinity-large-preview-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
// { id: "ling-2.6-flash-free", products: ["zen"], triggers: [httpErrors] },
|
|
||||||
]
|
|
||||||
|
|
||||||
if ($app.stage !== "production") {
|
|
||||||
models.splice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
for (const product of model.products) {
|
|
||||||
for (const trigger of model.triggers) {
|
|
||||||
const spec = trigger({ model: model.id, product })
|
|
||||||
|
|
||||||
new honeycomb.Trigger(resourceName(`${spec.id}-${product}-${model.id}`), {
|
|
||||||
name: spec.title,
|
|
||||||
description: spec.description,
|
|
||||||
queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json,
|
|
||||||
alertType: "on_change",
|
|
||||||
frequency: 300,
|
|
||||||
thresholds: [{ ...spec.threshold, exceededLimit: 1 }],
|
|
||||||
recipients: [
|
|
||||||
{
|
|
||||||
id: webhookRecipient.id,
|
|
||||||
notificationDetails: [
|
|
||||||
{
|
|
||||||
variables: [
|
|
||||||
{ name: "model", value: model.id },
|
|
||||||
{ name: "product", value: product },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
|
sst.Linkable.wrap(random.RandomPassword, (resource) => ({
|
||||||
|
properties: {
|
||||||
|
value: resource.result,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
export const SECRET = {
|
export const SECRET = {
|
||||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||||
|
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,6 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"solid-stripe": "0.8.1",
|
"solid-stripe": "0.8.1",
|
||||||
"svix": "1.92.2",
|
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
81
packages/console/app/src/routes/honeycomb/webhook.ts
Normal file
81
packages/console/app/src/routes/honeycomb/webhook.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
|
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||||
|
|
||||||
|
const DISCORD_ALERT_ROLE_ID = "1501447160175136838"
|
||||||
|
|
||||||
|
const basePayload = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
status: z.string().optional(),
|
||||||
|
isTest: z.boolean().optional(),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const groups = z.object({ group: z.object({ key: z.string(), value: z.string() }).array() }).array()
|
||||||
|
|
||||||
|
const honeycombWebhookPayload = z.discriminatedUnion("type", [
|
||||||
|
basePayload.extend({
|
||||||
|
type: z.literal("model_http_errors"),
|
||||||
|
groups,
|
||||||
|
}),
|
||||||
|
basePayload.extend({
|
||||||
|
type: z.literal("provider_http_errors"),
|
||||||
|
groups,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const postDiscordMessage = async (payload: z.infer<typeof honeycombWebhookPayload>) => {
|
||||||
|
const group = payload.type === "model_http_errors" ? "model" : "provider"
|
||||||
|
const names = (payload.groups ?? []).flatMap((item) => item.group.map((g) => g.value))
|
||||||
|
|
||||||
|
const content = [
|
||||||
|
`[**${payload.isTest ? "[TEST] " : ""}${payload.name ?? "Honeycomb alert"}**](${payload.url})`,
|
||||||
|
names.length > 0 ? `Affected ${group}s:` : undefined,
|
||||||
|
...names.map((name) => `- ${name}`),
|
||||||
|
"",
|
||||||
|
`<@&${DISCORD_ALERT_ROLE_ID}>`,
|
||||||
|
]
|
||||||
|
.filter((line) => line !== undefined)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
content,
|
||||||
|
allowed_mentions: { roles: [DISCORD_ALERT_ROLE_ID] },
|
||||||
|
flags: 4,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(input: APIEvent) {
|
||||||
|
const token = input.request.headers.get("X-Honeycomb-Webhook-Token")
|
||||||
|
if (!safeEqual(token ?? "", Resource.HoneycombWebhookSecret.value)) {
|
||||||
|
console.debug("Invalid Honeycomb webhook token")
|
||||||
|
return Response.json({ message: "invalid token" }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await input.request.json()
|
||||||
|
console.log(body, JSON.stringify(body, null, 2))
|
||||||
|
|
||||||
|
const parsed = honeycombWebhookPayload.safeParse(body)
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error(parsed.error)
|
||||||
|
return Response.json({ message: "invalid payload" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.data.status !== "TRIGGERED") {
|
||||||
|
console.debug("Skipping resolved alert Honeycomb webhook")
|
||||||
|
return Response.json({ message: "ignored" }, { status: 200 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await postDiscordMessage(parsed.data)
|
||||||
|
if (!response.ok) {
|
||||||
|
return Response.json({ message: "discord webhook failed" }, { status: 502 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ message: "sent" }, { status: 200 })
|
||||||
|
}
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import type { APIEvent } from "@solidjs/start/server"
|
|
||||||
import { Resource } from "@opencode-ai/console-resource"
|
|
||||||
import { Webhook } from "svix"
|
|
||||||
|
|
||||||
const DISCORD_INCIDENT_ROLE_ID = "1501447160175136838"
|
|
||||||
|
|
||||||
type Incident = {
|
|
||||||
mode?: "test" | "standard"
|
|
||||||
name?: string
|
|
||||||
permalink?: string
|
|
||||||
summary?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncidentWebhookPayload = {
|
|
||||||
event_type?: string
|
|
||||||
"public_incident.incident_created_v2"?: Incident
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyWebhook = async (request: Request) => {
|
|
||||||
const body = await request.text()
|
|
||||||
try {
|
|
||||||
return new Webhook(Resource.INCIDENT_WEBHOOK_SIGNING_SECRET.value).verify(
|
|
||||||
body,
|
|
||||||
Object.fromEntries(request.headers.entries()),
|
|
||||||
) as IncidentWebhookPayload
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const postDiscordMessage = async (incident: Incident) => {
|
|
||||||
return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
content: [
|
|
||||||
`**${incident.mode === "test" ? "[TEST] " : ""}${incident.name ?? "Incident has been created"}**`,
|
|
||||||
incident.summary,
|
|
||||||
"",
|
|
||||||
`<@&${DISCORD_INCIDENT_ROLE_ID}>`,
|
|
||||||
"",
|
|
||||||
incident.permalink,
|
|
||||||
]
|
|
||||||
.filter((line) => line !== undefined)
|
|
||||||
.join("\n"),
|
|
||||||
allowed_mentions: {
|
|
||||||
roles: [DISCORD_INCIDENT_ROLE_ID],
|
|
||||||
},
|
|
||||||
flags: 4,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(input: APIEvent) {
|
|
||||||
const payload = await verifyWebhook(input.request)
|
|
||||||
if (!payload) {
|
|
||||||
return Response.json({ message: "invalid signature" }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.event_type !== "public_incident.incident_created_v2") {
|
|
||||||
return Response.json({ message: "ignored event" }, { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const incident = payload["public_incident.incident_created_v2"]
|
|
||||||
if (!incident) {
|
|
||||||
return Response.json({ message: "missing incident" }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await postDiscordMessage(incident)
|
|
||||||
if (!response.ok) {
|
|
||||||
return Response.json({ message: "discord webhook failed" }, { status: 502 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ message: "sent" }, { status: 200 })
|
|
||||||
}
|
|
||||||
8
packages/console/core/src/util/crypto.ts
Normal file
8
packages/console/core/src/util/crypto.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { timingSafeEqual } from "node:crypto"
|
||||||
|
|
||||||
|
export function safeEqual(a: string, b: string): boolean {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const aBytes = encoder.encode(a)
|
||||||
|
const bBytes = encoder.encode(b)
|
||||||
|
return aBytes.length === bBytes.length && timingSafeEqual(aBytes, bBytes)
|
||||||
|
}
|
||||||
4
packages/console/core/sst-env.d.ts
vendored
4
packages/console/core/sst-env.d.ts
vendored
@ -91,8 +91,8 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"INCIDENT_WEBHOOK_SIGNING_SECRET": {
|
"HoneycombWebhookSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"R2AccessKey": {
|
"R2AccessKey": {
|
||||||
|
|||||||
4
packages/console/function/sst-env.d.ts
vendored
4
packages/console/function/sst-env.d.ts
vendored
@ -91,8 +91,8 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"INCIDENT_WEBHOOK_SIGNING_SECRET": {
|
"HoneycombWebhookSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"R2AccessKey": {
|
"R2AccessKey": {
|
||||||
|
|||||||
4
packages/console/resource/sst-env.d.ts
vendored
4
packages/console/resource/sst-env.d.ts
vendored
@ -91,8 +91,8 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"INCIDENT_WEBHOOK_SIGNING_SECRET": {
|
"HoneycombWebhookSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"R2AccessKey": {
|
"R2AccessKey": {
|
||||||
|
|||||||
4
packages/enterprise/sst-env.d.ts
vendored
4
packages/enterprise/sst-env.d.ts
vendored
@ -91,8 +91,8 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"INCIDENT_WEBHOOK_SIGNING_SECRET": {
|
"HoneycombWebhookSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"R2AccessKey": {
|
"R2AccessKey": {
|
||||||
|
|||||||
4
packages/function/sst-env.d.ts
vendored
4
packages/function/sst-env.d.ts
vendored
@ -91,8 +91,8 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"INCIDENT_WEBHOOK_SIGNING_SECRET": {
|
"HoneycombWebhookSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"R2AccessKey": {
|
"R2AccessKey": {
|
||||||
|
|||||||
4
sst-env.d.ts
vendored
4
sst-env.d.ts
vendored
@ -114,8 +114,8 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"INCIDENT_WEBHOOK_SIGNING_SECRET": {
|
"HoneycombWebhookSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"LogProcessor": {
|
"LogProcessor": {
|
||||||
|
|||||||
@ -11,15 +11,10 @@ export default $config({
|
|||||||
stripe: {
|
stripe: {
|
||||||
apiKey: process.env.STRIPE_SECRET_KEY!,
|
apiKey: process.env.STRIPE_SECRET_KEY!,
|
||||||
},
|
},
|
||||||
|
random: "4.19.2",
|
||||||
planetscale: "0.4.1",
|
planetscale: "0.4.1",
|
||||||
honeycomb: {
|
honeycomb: "0.49.0",
|
||||||
version: "0.49.0",
|
incident: "5.35.0",
|
||||||
apiKey: process.env.HONEYCOMB_API_KEY!,
|
|
||||||
},
|
|
||||||
incident: {
|
|
||||||
version: "5.35.0",
|
|
||||||
apiKey: process.env.INCIDENT_API_KEY!,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -27,7 +22,7 @@ export default $config({
|
|||||||
await import("./infra/app.js")
|
await import("./infra/app.js")
|
||||||
await import("./infra/console.js")
|
await import("./infra/console.js")
|
||||||
await import("./infra/enterprise.js")
|
await import("./infra/enterprise.js")
|
||||||
if ($app.stage === "production") {
|
if ($app.stage === "production" || $app.stage === "vimtor") {
|
||||||
await import("./infra/monitoring.js")
|
await import("./infra/monitoring.js")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user