Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/ai/src/mcp/tools/__tests__/alert-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,29 @@ const layer = (seen: Seen) =>
evaluationDurationMs: 12,
errorMessage: "column Foo not found",
errorCategory: "validation",
skipReason: null,
},
{
timestamp: NOW,
groupKey: "checkout",
status: "skipped",
skipReason: "no_data",
signalType: "error_rate",
comparator: "gt",
threshold: 0.05,
thresholdUpper: null,
observedValue: null,
sampleCount: 0,
windowMinutes: 5,
windowStart: NOW,
windowEnd: NOW,
consecutiveBreaches: 0,
consecutiveHealthy: 0,
incidentId: null,
incidentTransition: "none",
evaluationDurationMs: 9,
errorMessage: null,
errorCategory: null,
},
],
}),
Expand Down Expand Up @@ -323,6 +346,10 @@ describe("alert tools", () => {
})
const output = Schema.decodeUnknownSync(ListAlertChecksOutput)(result.structuredContent)
expect(output.errored).toBe(1)
expect(output.noData).toBe(1)
expect(output.checks[1]?.skipReason).toBe("no_data")
expect(text(result)).toContain("1 skipped [1 no data]")
expect(text(result)).toContain("| no data |")
expect(output.timeRange).toEqual({ start: "2026-09-24 00:00:00", end: "2026-09-24 12:00:00" })
expect(text(result)).toContain("Time range: 2026-09-24 00:00:00 to 2026-09-24 12:00:00")
expect(text(result)).toContain(`\`get_alert_rule rule_id="${RULE_ID}"\``)
Expand Down
57 changes: 53 additions & 4 deletions apps/ai/src/mcp/tools/list-alert-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@ const WINDOW = P.timeWindow({ defaultHours: 7 * 24 })
/** Rows the text shows; the rest stay in the structured output. */
const SHOWN_ROWS = 100

/** What the Value column shows for a skipped check, in place of its missing value. */
const skipReasonLabel = (reason: string): string => {
switch (reason) {
case "no_data":
return "no data"
case "below_min_samples":
return "below min samples"
case "no_value":
return "no value"
default:
return reason
}
}

/** Skipped checks are not healthy ones, so the summary says why they skipped. */
const formatSkipped = (output: {
readonly skipped: number
readonly noData: number
readonly belowMinSamples: number
}): string => {
const parts = [
output.noData > 0 ? `${output.noData} no data` : undefined,
output.belowMinSamples > 0 ? `${output.belowMinSamples} below min samples` : undefined,
].filter((part) => part !== undefined)
return parts.length > 0 ? `${output.skipped} skipped [${parts.join(", ")}]` : `${output.skipped} skipped`
}

/** Window bounds are `YYYY-MM-DD HH:mm:ss` UTC; the read model parses ISO 8601. */
const toIso = (value: string): string => `${value.replace(" ", "T")}Z`

Expand Down Expand Up @@ -79,12 +106,15 @@ export function registerListAlertChecksTool(server: McpToolRegistrar) {
breached: count("breached"),
healthy: count("healthy"),
skipped: count("skipped"),
noData: checks.filter((c) => c.skipReason === "no_data").length,
belowMinSamples: checks.filter((c) => c.skipReason === "below_min_samples").length,
errored: count("error"),
transitions: checks.filter((c) => c.incidentTransition !== "none").length,
checks: checks.map((c) => ({
timestamp: c.timestamp,
groupKey: c.groupKey,
status: c.status,
skipReason: c.skipReason,
observedValue: c.observedValue,
threshold: c.threshold,
comparator: c.comparator,
Expand Down Expand Up @@ -132,6 +162,23 @@ export function registerListAlertChecksTool(server: McpToolRegistrar) {
),
)
}
if (output.noData > 0 && output.noData === output.total) {
next.push(
doc.next(
"get_alert_rule",
{ rule_id: output.ruleId },
"every check had no data (the query matched nothing), so the rule is blind, not healthy: check its filters",
),
)
} else if (output.belowMinSamples > 0) {
next.push(
doc.next(
"get_alert_rule",
{ rule_id: output.ruleId },
"checks skipped below the minimum sample count: compare minimumSampleCount with the observed samples",
),
)
}
if (next.length === 0) {
next.push(
doc.next(
Expand Down Expand Up @@ -169,7 +216,7 @@ export function registerListAlertChecksTool(server: McpToolRegistrar) {
? []
: [
doc.text(
`Total: ${output.total} (${output.breached} breached, ${output.healthy} healthy, ${output.skipped} skipped, ${output.errored} errored, ${output.transitions} incident transitions)`,
`Total: ${output.total} (${output.breached} breached, ${output.healthy} healthy, ${formatSkipped(output)}, ${output.errored} errored, ${output.transitions} incident transitions)`,
),
doc.table(
[
Expand All @@ -187,9 +234,11 @@ export function registerListAlertChecksTool(server: McpToolRegistrar) {
c.status,
c.status === "error" && c.errorMessage != null
? truncate(c.errorMessage, 40)
: c.observedValue != null
? String(c.observedValue)
: "-",
: c.status === "skipped" && c.skipReason != null
? skipReasonLabel(c.skipReason)
: c.observedValue != null
? String(c.observedValue)
: "-",
String(c.threshold),
String(c.sampleCount),
truncate(c.groupKey || "all", 20),
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v2/alert-rules.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const toV2Check = (check: AlertCheckDocument): V2AlertCheck => ({
timestamp: check.timestamp,
group_key: check.groupKey,
status: check.status,
skip_reason: check.skipReason,
signal_type: check.signalType,
comparator: check.comparator,
threshold: check.threshold,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/v2/alerts.http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ describe("v2 alerts over HTTP", () => {
evaluationDurationMs: 8,
errorMessage: null,
errorCategory: "",
skipReason: "",
}
})
const pagedWarehouse: WarehouseQueryServiceApi = {
Expand Down
13 changes: 13 additions & 0 deletions apps/cli/src/server/local-schema-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,19 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray<LocalSchemaHistoryEntry> = Obje
manifestDigest: "a3f69dea62db6610d63a6b2442f86d9071c91c286b5935e764700efc01fa7d7a",
projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a",
}),
Object.freeze({
// v27 appends SkipReason to alert_checks. No row is rewritten: checks
// recorded before the edge keep an empty reason.
//
// projectRevision is carried forward deliberately: it is a hardcoded
// constant that no longer tracks the generator's header, and the identity
// this gate compares is the fingerprint/digest pair.
version: 27,
fingerprint: "64a7d92899d7bdb7",
digest: "64a7d92899d7bdb7380c150b0c15301396572d8d573bf4db7b890426e729c43c",
manifestDigest: "2f52ca5a525a85621729e885a028bc9bfe73fa94ef62be7790efec0283d792d5",
projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a",
}),
] as const)

/** Immutable SQLite control DDL identities, checked by clickhouse:schema:check. */
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/server/local-schema-version.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Increment this value for every structural change to the generated local
// schema. The compatibility manifest and migration registry must be updated in
// the same change before a new value can ship.
export const LOCAL_SCHEMA_VERSION = 26 as const
export const LOCAL_SCHEMA_VERSION = 27 as const

/** SQLite eventing state has its own independent version sequence. */
export const LOCAL_CONTROL_SCHEMA_VERSION = 1 as const
19 changes: 19 additions & 0 deletions apps/cli/src/server/local-store-migrations/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1140,5 +1140,24 @@ export const LOCAL_STORE_STEPS: ReadonlyArray<StepSpec> = [
},
],
},
{
id: "local-0026-to-0027-alert-checks-skip-reason",
from: 26,
to: 27,
description: "Add SkipReason to alert_checks",
clonedBefore: "any DDL runs",
beforeBootstrap: [addColumns("alert_checks", [["SkipReason", "LowCardinality(String) DEFAULT ''"]])],
plan: [["widen-alert-checks", "Add SkipReason to alert_checks so skipped checks record why"]],
verifies: "Verify the v27 physical schema and the retained raw telemetry counts",
dispositions: [
{
name: "alert_checks",
classification: "authoritative",
disposition: "preserve-exact",
guarantee:
"Existing check rows are kept as written with an empty SkipReason; only checks recorded after the migration carry one.",
},
],
},
// local-schema:bump appends the next step above this line.
]
3 changes: 3 additions & 0 deletions apps/cli/src/server/schema-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import schemaV23Sql from "./schema/local-schema-v23.sql" with { type: "text" }
import schemaV24Sql from "./schema/local-schema-v24.sql" with { type: "text" }
import schemaV25Sql from "./schema/local-schema-v25.sql" with { type: "text" }
import schemaV26Sql from "./schema/local-schema-v26.sql" with { type: "text" }
import schemaV27Sql from "./schema/local-schema-v27.sql" with { type: "text" }
import { schemaDigest as digestSchema, schemaFingerprint as fingerprintSchema } from "./store-version"
import { buildLocalSchemaManifest, type LocalSchemaManifest } from "./schema-manifest"
import { LOCAL_SCHEMA_VERSION } from "./local-schema-version"
Expand Down Expand Up @@ -95,6 +96,7 @@ const SNAPSHOT_SQL: ReadonlyArray<string> = [
schemaV24Sql,
schemaV25Sql,
schemaV26Sql,
schemaV27Sql,
]

export interface LocalSchemaSnapshot {
Expand Down Expand Up @@ -178,6 +180,7 @@ export const LOCAL_SCHEMA_V23 = localSchemaIdentity(23)
export const LOCAL_SCHEMA_V24 = localSchemaIdentity(24)
export const LOCAL_SCHEMA_V25 = localSchemaIdentity(25)
export const LOCAL_SCHEMA_V26 = localSchemaIdentity(26)
export const LOCAL_SCHEMA_V27 = localSchemaIdentity(27)

export const CURRENT_LOCAL_SCHEMA: LocalSchemaIdentity = Object.freeze({
version: LOCAL_SCHEMA_VERSION,
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/server/schema/local-inserts.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"projectRevision": "6f17e5994398bfddfebe6a99f13f097bbe822a95820cf6e0cb575082823d4f32",
"projectRevision": "4305027b76678ea156f64ad487cad6557295c8e138c7fc4fb0e076d81c5e5247",
"orgPlaceholder": "__ORG__",
"datasources": {
"traces": {
Expand Down
Loading
Loading