diff --git a/.github/workflows/deploy-pr-preview.yml b/.github/workflows/deploy-pr-preview.yml index 73810fcce9..3e45e92053 100644 --- a/.github/workflows/deploy-pr-preview.yml +++ b/.github/workflows/deploy-pr-preview.yml @@ -124,6 +124,31 @@ jobs: WEB_URL: ${{ steps.deploy.outputs.web_url }} run: bun scripts/preview-seed-smoke.ts + # Browser e2e against the fresh preview (apps/web/e2e/README.md). Non-blocking while + # it beds in; `agent.*` tests register only once ANTHROPIC_API_KEY is in Infisical. + - name: End-to-end tests against the preview + id: e2e + if: ${{ env.TEARDOWN != 'true' && steps.smoke.outcome == 'success' }} + continue-on-error: true + working-directory: apps/web/e2e + env: + MAPLE_E2E_APP_URL: ${{ steps.deploy.outputs.web_url }} + MAPLE_E2E_API_URL: ${{ steps.deploy.outputs.api_url }} + E2E_TELEMETRY_DISABLED: "1" + run: | + bunx e2e-web install chromium --with-deps + bunx e2e run --reporter list,junit + + - name: Upload e2e results + if: ${{ !cancelled() && steps.e2e.outcome != 'skipped' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: e2e-results + path: apps/web/e2e/.e2e/ + if-no-files-found: warn + include-hidden-files: true + retention-days: 7 + - name: Destroy PR preview stack with Alchemy id: destroy if: ${{ env.TEARDOWN == 'true' }} diff --git a/apps/ai/src/mcp/lib/alert-rules.ts b/apps/ai/src/mcp/lib/alert-rules.ts index 2afbf25427..dac60fb4f5 100644 --- a/apps/ai/src/mcp/lib/alert-rules.ts +++ b/apps/ai/src/mcp/lib/alert-rules.ts @@ -107,14 +107,26 @@ export const parseRuleId = (ruleId: string) => onSome: (id) => Effect.succeed(id), }) +/** + * An invalid rule as the model reads it: the message and each detail once. A + * message that already folds in every detail is dropped as a repeat; any other + * message names the failure the details only qualify, so it stays. + */ +export const invalidRuleInput = (error: AlertValidationError) => { + const repeatsDetails = + error.details.length > 0 && error.details.every((detail) => error.message.includes(detail)) + return new McpInvalidInputError({ + message: [ + repeatsDetails ? "Invalid alert rule:" : `Invalid alert rule: ${error.message}`, + ...error.details.map((detail) => `- ${detail}`), + ].join("\n"), + }) +} + /** The write failures that are the caller's to fix, as input errors naming the parameter. */ export const ruleWriteInputErrors = { "@maple/http/errors/AlertValidationError": (error: AlertValidationError) => - Effect.fail( - new McpInvalidInputError({ - message: [`Invalid alert rule: ${error.message}`, ...error.details].join("\n"), - }), - ), + Effect.fail(invalidRuleInput(error)), "@maple/http/errors/AlertRuleDestinationNotFoundError": (error: AlertRuleDestinationNotFoundError) => Effect.fail( new McpInvalidInputError({ diff --git a/apps/ai/src/mcp/tools/__tests__/alert-tools.test.ts b/apps/ai/src/mcp/tools/__tests__/alert-tools.test.ts index 9cc0dcdbe5..f802260451 100644 --- a/apps/ai/src/mcp/tools/__tests__/alert-tools.test.ts +++ b/apps/ai/src/mcp/tools/__tests__/alert-tools.test.ts @@ -14,6 +14,7 @@ import { AlertRulePreviewResponse, AlertRulesListResponse, AlertRuleUpsertRequest, + AlertValidationError, OrgId, UserId, } from "@maple/domain/http" @@ -33,6 +34,7 @@ import { AlertReadModelsService } from "@maple/backend/services/alerts/AlertRead import { CurrentMcpTenant } from "../../lib/query-warehouse" import { executeRegisteredMcpToolUnscoped } from "../registry" import { buildUpdatedRequest } from "../update-alert-rule" +import { invalidRuleInput } from "../../lib/alert-rules" import type { McpToolResult } from "../types" const RULE_ID = "7f1c2d3e-4b5a-4c6d-8e9f-0a1b2c3d4e5f" @@ -59,6 +61,10 @@ const rule = Schema.decodeUnknownSync(AlertRuleDocument)({ windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 1, + breachesOutOf: null, + thresholds: [ + { severity: "critical", value: 0.05, upper: null, recoveryValue: null, destinationIds: [DEST_ID] }, + ], consecutiveHealthyRequired: 1, renotifyIntervalMinutes: 60, apdexThresholdMs: null, @@ -111,6 +117,7 @@ const incident = (overrides: Record) => ({ comparator: "gt", threshold: 0.05, thresholdUpper: null, + peakSeverity: null, firstTriggeredAt: NOW, lastTriggeredAt: NOW, resolvedAt: null, @@ -181,6 +188,15 @@ const layer = (seen: Seen) => windowMinutes: 5, threshold: 0.05, thresholdUpper: null, + thresholds: [ + { + severity: "warning", + value: 0.05, + upper: null, + recoveryValue: null, + destinationIds: [], + }, + ], comparator: "gt", truncatedToStart: null, series: [ @@ -208,7 +224,14 @@ const layer = (seen: Seen) => return Effect.succeed( id === RULE_ID ? rule - : { ...rule, id, name: "Latency", destinationIds: request.destinationIds }, + : { + ...rule, + id, + name: "Latency", + destinationIds: (request.thresholds ?? []).flatMap( + (tier) => tier.destinationIds, + ), + }, ) }, listDestinations: () => @@ -270,6 +293,7 @@ const layer = (seen: Seen) => consecutiveHealthy: 0, incidentId: null, incidentTransition: "none", + level: null, evaluationDurationMs: 12, errorMessage: "column Foo not found", errorCategory: "validation", @@ -293,6 +317,7 @@ const layer = (seen: Seen) => consecutiveHealthy: 0, incidentId: null, incidentTransition: "none", + level: null, evaluationDurationMs: 9, errorMessage: null, errorCategory: null, @@ -564,7 +589,7 @@ describe("alert tools", () => { await ok("preview_alert_rule", { rule_id: RULE_ID, threshold: 0.2 }, seen) expect(seen.previewed?.rule).toMatchObject({ name: "Checkout errors", - threshold: 0.2, + thresholds: [{ value: 0.2 }], signalType: "error_rate", }) @@ -580,7 +605,7 @@ describe("alert tools", () => { seen, ) expect(seen.updated).toMatchObject({ - threshold: 0.1, + thresholds: [{ severity: "critical", value: 0.1 }], name: "Checkout errors", serviceNames: ["checkout"], groupBy: ["service.name", "attr.http.route"], @@ -606,9 +631,96 @@ describe("alert tools", () => { ...rule, comparator: "between", thresholdUpper: 10, + thresholds: [ + { severity: "critical", value: 0.05, upper: 10, recoveryValue: null, destinationIds: [] }, + ], + }) + const upperAfter = (params: Parameters[1]) => + Effect.runSync(buildUpdatedRequest(range, params)).thresholds[0].upper + expect(upperAfter({ comparator: "gt" })).toBeNull() + expect(upperAfter({ threshold: 2 })).toBe(10) + }) + + it("update_alert_rule drops a recovery value the new threshold invalidates", () => { + const recovering = Schema.decodeUnknownSync(AlertRuleDocument)({ + ...rule, + comparator: "gt", + thresholds: [ + { + severity: "critical", + value: 100, + upper: null, + recoveryValue: 80, + destinationIds: [DEST_ID], + }, + ], + }) + const recoveryAfter = (params: Parameters[1]) => + Effect.runSync(buildUpdatedRequest(recovering, params)).thresholds[0].recoveryValue + expect(recoveryAfter({ threshold: 120 })).toBe(80) + expect(recoveryAfter({ threshold: 70 })).toBeNull() + expect(recoveryAfter({ comparator: "lt" })).toBeNull() + }) + + it("update_alert_rule keeps a two-tier rule's tiers and refuses to merge them", () => { + const tiered = Schema.decodeUnknownSync(AlertRuleDocument)({ + ...rule, + breachesOutOf: 5, + thresholds: [ + { + severity: "warning", + value: 0.02, + upper: null, + recoveryValue: 0.01, + destinationIds: ["00000000-0000-4000-8000-00000000d001"], + }, + { + severity: "critical", + value: 0.05, + upper: null, + recoveryValue: null, + destinationIds: ["00000000-0000-4000-8000-00000000d002"], + }, + ], }) - expect(buildUpdatedRequest(range, { comparator: "gt" }).thresholdUpper).toBeNull() - expect(buildUpdatedRequest(range, { threshold: 2 }).thresholdUpper).toBe(10) + const renamed = Effect.runSync(buildUpdatedRequest(tiered, { name: "Renamed" })) + expect(renamed.thresholds).toEqual(tiered.thresholds) + expect(renamed.breachesOutOf).toBe(5) + + const added = Effect.runSync( + buildUpdatedRequest(tiered, { + add_destination_ids: ["00000000-0000-4000-8000-00000000d003"], + remove_destination_ids: ["00000000-0000-4000-8000-00000000d002"], + }), + ) + expect(added.thresholds.map((tier) => tier.destinationIds)).toEqual([ + ["00000000-0000-4000-8000-00000000d001", "00000000-0000-4000-8000-00000000d003"], + [], + ]) + + const collapsing = Effect.runSync(Effect.flip(buildUpdatedRequest(tiered, { threshold: 0.1 }))) + expect(collapsing.parameter).toBe("threshold") + }) + + it("names each invalid-rule detail once, though the domain message already carries them", () => { + const details = ["threshold must be a finite number", "at least one destination must be selected"] + const message = invalidRuleInput( + new AlertValidationError({ message: `Invalid alert rule: ${details.join("; ")}`, details }), + ).message + for (const detail of details) { + expect(message.split(detail)).toHaveLength(2) + } + }) + + it("keeps an invalid-rule message that names a failure its details only qualify", () => { + const message = invalidRuleInput( + new AlertValidationError({ + message: "Unsupported traces metric: p95", + details: ["Traces filter http.status_code supports only =; ignoring !="], + }), + ).message + expect(message).toContain("Unsupported traces metric: p95") + expect(message).toContain("ignoring !=") }) it("delete_alert_rule requires confirm and maps not-found to rule_id", async () => { @@ -693,7 +805,7 @@ describe("alert tools", () => { seen, ) expect(seen.updatedIds).toEqual([RULE_ID, ALL_ID, RAW_ID, OTHER_RAW_ID]) - expect(seen.updated?.destinationIds).toEqual([NEW_DEST]) + expect(seen.updated?.thresholds?.[0]?.destinationIds).toEqual([NEW_DEST]) const output = Schema.decodeUnknownSync(UpdateAlertRuleOutput)(result.structuredContent) expect(output.rules).toHaveLength(4) expect(text(result)).toContain("Updated 4 Alert Rules") diff --git a/apps/ai/src/mcp/tools/preview-alert-rule.ts b/apps/ai/src/mcp/tools/preview-alert-rule.ts index 8360b9064d..351da829d4 100644 --- a/apps/ai/src/mcp/tools/preview-alert-rule.ts +++ b/apps/ai/src/mcp/tools/preview-alert-rule.ts @@ -9,7 +9,7 @@ import { AlertRulesService } from "@maple/backend/services/alerts/AlertRulesServ import { toMcpHttpError } from "../lib/map-http-error" import { warehouseReadToMcpHandlers } from "../lib/map-warehouse-error" import { CurrentMcpTenant } from "../lib/query-warehouse" -import { formatCondition, ruleNotFound } from "../lib/alert-rules" +import { formatCondition, invalidRuleInput, ruleNotFound } from "../lib/alert-rules" import { minSampleCountWarning } from "../lib/alert-rule-evaluation" import { truncate } from "../lib/format" import * as P from "../lib/params" @@ -135,7 +135,7 @@ export function registerPreviewAlertRuleTool(server: McpToolRegistrar) { .pipe(Effect.mapError(toMcpHttpError("preview_alert_rule"))) const current = list.rules.find((r) => r.id === params.rule_id) if (!current) return yield* ruleNotFound(params.rule_id) - rule = buildUpdatedRequest(current, { ...params, rule_id: current.id }) + rule = yield* buildUpdatedRequest(current, { ...params, rule_id: current.id }) } else { rule = yield* buildAlertRuleRequest({ ...params, name: "Preview", destination_ids: [] }) } @@ -154,13 +154,7 @@ export function registerPreviewAlertRuleTool(server: McpToolRegistrar) { const preview = yield* alerts.previewRule(tenant.orgId, tenant.roles, request).pipe( Effect.catchTags({ "@maple/http/errors/AlertValidationError": (error) => - Effect.fail( - new McpInvalidInputError({ - message: [`Invalid alert rule: ${error.message}`, ...error.details].join( - "\n", - ), - }), - ), + Effect.fail(invalidRuleInput(error)), "@maple/http/errors/AlertForbiddenError": (error) => Effect.fail(toMcpHttpError("preview_alert_rule")(error)), "@maple/http/errors/QueryEngineValidationError": (error) => diff --git a/apps/ai/src/mcp/tools/update-alert-rule.ts b/apps/ai/src/mcp/tools/update-alert-rule.ts index 47db4ece5d..c84843b28e 100644 --- a/apps/ai/src/mcp/tools/update-alert-rule.ts +++ b/apps/ai/src/mcp/tools/update-alert-rule.ts @@ -1,15 +1,18 @@ import { McpInvalidInputError, type McpToolRegistrar } from "./types" -import { Effect, Schema } from "effect" +import { Array as Arr, Effect, Schema } from "effect" import { UpdateAlertRuleOutput } from "@maple/domain/mcp-outputs" import { toMcpHttpError } from "../lib/map-http-error" import { CurrentMcpTenant } from "../lib/query-warehouse" import { AlertsService } from "@maple/backend/services/alerts/AlertsService" import { AlertRulesService } from "@maple/backend/services/alerts/AlertRulesService" +import { recoveryValueFits } from "@maple/backend/services/alerts/alert-thresholds" import { AlertRuleUpsertRequest, isRangeComparator, QueryBuilderQueryDraftSchema, + type AlertComparator, type AlertRuleDocument, + type AlertThreshold, } from "@maple/domain/http" import { ALERT_COMPARATORS, @@ -92,7 +95,7 @@ export const UpdateAlertRuleParameters = Schema.Struct({ * signal type or comparator does not use is dropped rather than carried over, as v2 PATCH does, * because `normalizeRule` rejects it. */ -export function buildUpdatedRequest( +export const buildUpdatedRequest = Effect.fnUntraced(function* ( current: AlertRuleDocument, params: typeof UpdateAlertRuleParameters.Type, ) { @@ -111,13 +114,13 @@ export function buildUpdatedRequest( const signalType = params.signal_type ?? current.signalType const comparator = params.comparator ?? current.comparator const keepsRawQuery = signalType === "raw_query" + const thresholds = yield* mergeThresholds(current, params, comparator) return { name: params.name ?? current.name, notes: current.notes, notificationTemplate, enabled: params.enabled ?? current.enabled, - severity: params.severity ?? current.severity, serviceNames: [...(params.services ?? current.serviceNames)], excludeServiceNames: [...current.excludeServiceNames], environments: [...(params.environments ?? current.environments)], @@ -132,8 +135,8 @@ export function buildUpdatedRequest( : null, signalType, comparator, - threshold: params.threshold ?? current.threshold, - thresholdUpper: isRangeComparator(comparator) ? current.thresholdUpper : null, + thresholds, + breachesOutOf: current.breachesOutOf, windowMinutes: params.window_minutes ?? current.windowMinutes, minimumSampleCount: params.minimum_sample_count ?? current.minimumSampleCount, consecutiveBreachesRequired: params.consecutive_breaches ?? current.consecutiveBreachesRequired, @@ -145,8 +148,58 @@ export function buildUpdatedRequest( rawQuerySql: keepsRawQuery ? (params.raw_query_sql ?? current.rawQuerySql) : null, rawQueryReducer: keepsRawQuery ? (params.raw_query_reducer ?? current.rawQueryReducer) : null, alertOnNoData: params.alert_on_no_data ?? current.noDataBehavior === "alert", - destinationIds: mergeDestinations(current.destinationIds, params), } +}) + +/** + * Single-threshold params edit a one-tier rule's tier in place. On a two-tier rule they would + * collapse it, so they are rejected; added destinations join the warning tier, which every incident reaches. + */ +const mergeThresholds = ( + current: AlertRuleDocument, + params: typeof UpdateAlertRuleParameters.Type, + comparator: AlertComparator, +) => { + const fitBound = (tier: AlertThreshold) => + isRangeComparator(comparator) ? tier : { ...tier, upper: null } + const [only, ...rest] = current.thresholds + if (rest.length === 0) { + const value = params.threshold ?? only.value + // MCP cannot set a recovery value yet, so one the edit invalidates is dropped, not a dead end. + const recoveryValue = recoveryValueFits(comparator, { value, recoveryValue: only.recoveryValue }) + ? only.recoveryValue + : null + return Effect.succeed( + Arr.of({ + ...fitBound(only), + severity: params.severity ?? only.severity, + value, + recoveryValue, + destinationIds: mergeDestinations(only.destinationIds, params), + }), + ) + } + const collapsing = (["severity", "threshold", "destination_ids"] as const).find( + (key) => params[key] !== undefined, + ) + if (collapsing !== undefined) { + return Effect.fail( + new McpInvalidInputError({ + message: `"${current.name}" has warning and critical thresholds; ${collapsing} would merge them into one.`, + parameter: collapsing, + }), + ) + } + return Effect.succeed( + Arr.map(current.thresholds, (tier, index) => ({ + ...fitBound(tier), + recoveryValue: recoveryValueFits(comparator, tier) ? tier.recoveryValue : null, + destinationIds: mergeDestinations( + tier.destinationIds, + index === 0 ? params : { remove_destination_ids: params.remove_destination_ids }, + ), + })), + ) } /** `destination_ids` replaces; add/remove edit the current (or replaced) list in place. */ @@ -213,14 +266,18 @@ export function registerUpdateAlertRuleTool(server: McpToolRegistrar) { // Decode every merged request before writing any, so a bad overlay saves nothing. const requests = yield* Effect.forEach(targets, (current) => - decodeAlertRuleRequest(buildUpdatedRequest(current, params)).pipe( - Effect.map((request) => ({ current, request })), - Effect.mapError( - (error) => - new McpInvalidInputError({ - message: `Invalid alert rule${targets.length > 1 ? ` "${current.name}"` : ""}: ${String(error)}`, - }), + buildUpdatedRequest(current, params).pipe( + Effect.flatMap((merged) => + decodeAlertRuleRequest(merged).pipe( + Effect.mapError( + (error) => + new McpInvalidInputError({ + message: `Invalid alert rule${targets.length > 1 ? ` "${current.name}"` : ""}: ${String(error)}`, + }), + ), + ), ), + Effect.map((request) => ({ current, request })), ), ) diff --git a/apps/api/src/routes/v2/alert-incidents.http.ts b/apps/api/src/routes/v2/alert-incidents.http.ts index 41d2462ef5..256dd85d60 100644 --- a/apps/api/src/routes/v2/alert-incidents.http.ts +++ b/apps/api/src/routes/v2/alert-incidents.http.ts @@ -14,6 +14,7 @@ const toV2Incident = (doc: AlertIncidentDocument): V2AlertIncident => ({ group_key: doc.groupKey, signal_type: doc.signalType, severity: doc.severity, + peak_severity: doc.peakSeverity, status: doc.status, comparator: doc.comparator, threshold: doc.threshold, diff --git a/apps/api/src/routes/v2/alert-rules.http.ts b/apps/api/src/routes/v2/alert-rules.http.ts index 7887262828..0ab8b75723 100644 --- a/apps/api/src/routes/v2/alert-rules.http.ts +++ b/apps/api/src/routes/v2/alert-rules.http.ts @@ -1,5 +1,11 @@ import { HttpApiBuilder } from "effect/http-api" -import type { AlertCheckDocument, AlertRuleDocument, AlertRulePreviewResponse } from "@maple/domain/http" +import type { + AlertCheckDocument, + AlertRuleDocument, + AlertRulePreviewResponse, + AlertThreshold, + AlertThresholds, +} from "@maple/domain/http" import { AlertRulePreviewRequest, AlertRuleUpsertRequest, @@ -17,9 +23,17 @@ import type { V2AlertRulePreviewResult, V2AlertRuleUpdateParams, } from "@maple/domain/http/v2" -import { MapleApiV2, paginateArray, scopeAllows, timestamp, V2ParameterInvalid } from "@maple/domain/http/v2" +import { + fromV2Threshold, + MapleApiV2, + paginateArray, + scopeAllows, + timestamp, + toV2Threshold, + V2ParameterInvalid, +} from "@maple/domain/http/v2" import { AlertForbiddenError } from "@maple/domain/http" -import { DateTime, Effect, Option, Result, Schema } from "effect" +import { Array as Arr, DateTime, Effect, Option, Result, Schema } from "effect" import { Base64Url } from "effect/encoding" import { auditDiff } from "@/routes/v2/audit-changes" import { recordHttpAudit } from "@maple/backend/services/audit/AuditLogService" @@ -65,6 +79,7 @@ const toV2Rule = (doc: AlertRuleDocument): V2AlertRule => ({ notification_template: doc.notificationTemplate, enabled: doc.enabled, severity: doc.severity, + thresholds: Arr.map(doc.thresholds, toV2Threshold), service_names: doc.serviceNames, exclude_service_names: doc.excludeServiceNames, environments: doc.environments, @@ -77,6 +92,7 @@ const toV2Rule = (doc: AlertRuleDocument): V2AlertRule => ({ window_minutes: doc.windowMinutes, minimum_sample_count: doc.minimumSampleCount, consecutive_breaches_required: doc.consecutiveBreachesRequired, + breaches_out_of: doc.breachesOutOf, consecutive_healthy_required: doc.consecutiveHealthyRequired, renotify_interval_minutes: doc.renotifyIntervalMinutes, apdex_threshold_ms: doc.apdexThresholdMs, @@ -102,6 +118,7 @@ const ruleAuditDiff = auditDiff ({ consecutive_healthy: check.consecutiveHealthy, incident_id: check.incidentId, incident_transition: check.incidentTransition, + level: check.level, evaluation_duration_ms: check.evaluationDurationMs, error_message: check.errorMessage, error_category: check.errorCategory, @@ -182,12 +201,18 @@ const toUpsertRequest = ( : { queryBuilderDraft: yield* decodeDraft(params.query_builder_draft) } return new AlertRuleUpsertRequest({ name: params.name, - severity: params.severity, signalType: params.signal_type, comparator: params.comparator, - threshold: params.threshold, windowMinutes: params.window_minutes, - destinationIds: params.destination_ids, + ...(params.thresholds !== undefined + ? { thresholds: Arr.map(params.thresholds, fromV2Threshold) } + : undefined), + ...(params.severity !== undefined ? { severity: params.severity } : undefined), + ...(params.threshold !== undefined ? { threshold: params.threshold } : undefined), + ...(params.destination_ids !== undefined + ? { destinationIds: params.destination_ids } + : undefined), + ...(params.breaches_out_of !== undefined ? { breachesOutOf: params.breaches_out_of } : undefined), ...(params.notes !== undefined ? { notes: params.notes } : undefined), ...(params.notification_template !== undefined ? { @@ -241,6 +266,54 @@ const toUpsertRequest = ( }) }) +/** + * `thresholds` replaces the tiers; single-threshold fields edit a one-tier rule in place and + * are rejected on two tiers. A stored range bound is dropped when the comparator stops being a range. + */ +const mergeThresholds = ( + doc: AlertRuleDocument, + patch: V2AlertRuleUpdateParams, + comparator: AlertRuleDocument["comparator"], +): Effect.Effect> => { + const fitBound = (tier: AlertThreshold): AlertThreshold => + isRangeComparator(comparator) ? tier : { ...tier, upper: null } + if (patch.thresholds !== undefined) { + return Effect.succeed(Arr.map(patch.thresholds, fromV2Threshold)) + } + // Legacy fields echoed back as read (a client PATCHing the document it fetched) are not edits. + const echoed = { + severity: patch.severity === doc.severity, + threshold: patch.threshold === doc.threshold, + threshold_upper: patch.threshold_upper === doc.thresholdUpper, + destination_ids: + patch.destination_ids !== undefined && + patch.destination_ids.length === doc.destinationIds.length && + patch.destination_ids.every((id) => doc.destinationIds.includes(id)), + } + const legacyKeys = (["severity", "threshold", "threshold_upper", "destination_ids"] as const).filter( + (key) => patch[key] !== undefined && !echoed[key], + ) + if (legacyKeys.length === 0) return Effect.succeed(Arr.map(doc.thresholds, fitBound)) + const [only, ...rest] = doc.thresholds + if (rest.length > 0) { + return Effect.fail( + V2ParameterInvalid.make( + `This rule has severity tiers; update \`thresholds\` instead of \`${legacyKeys[0]}\`.`, + { param: legacyKeys[0] }, + ), + ) + } + return Effect.succeed( + Arr.of({ + ...fitBound(only), + severity: patch.severity ?? only.severity, + value: patch.threshold ?? only.value, + ...(patch.threshold_upper !== undefined ? { upper: patch.threshold_upper } : undefined), + destinationIds: patch.destination_ids ?? only.destinationIds, + }), + ) +} + /** * PATCH semantics over the domain full-upsert `updateRule`: overlay the fields * present in the patch onto the rule's current state. Read-merge-write — no @@ -253,6 +326,7 @@ const mergeUpsertRequest = ( Effect.gen(function* () { const signalType = patch.signal_type ?? doc.signalType const comparator = patch.comparator ?? doc.comparator + const thresholds = yield* mergeThresholds(doc, patch, comparator) const queryBuilderDraft = signalType !== "builder_query" ? null @@ -287,7 +361,7 @@ const mergeUpsertRequest = ( ? patch.notification_template : doc.notificationTemplate, enabled: patch.enabled ?? doc.enabled, - severity: patch.severity ?? doc.severity, + thresholds, serviceNames: patch.service_names ?? doc.serviceNames, excludeServiceNames: patch.exclude_service_names ?? doc.excludeServiceNames, environments: patch.environments ?? doc.environments, @@ -295,18 +369,11 @@ const mergeUpsertRequest = ( groupBy: patch.group_by !== undefined ? patch.group_by : doc.groupBy, signalType, comparator, - threshold: patch.threshold ?? doc.threshold, - // Dropped, not carried over, when the comparator stops being a range. - thresholdUpper: - patch.threshold_upper !== undefined - ? patch.threshold_upper - : isRangeComparator(comparator) - ? doc.thresholdUpper - : null, windowMinutes: patch.window_minutes ?? doc.windowMinutes, minimumSampleCount: patch.minimum_sample_count ?? doc.minimumSampleCount, consecutiveBreachesRequired: patch.consecutive_breaches_required ?? doc.consecutiveBreachesRequired, + breachesOutOf: patch.breaches_out_of !== undefined ? patch.breaches_out_of : doc.breachesOutOf, consecutiveHealthyRequired: patch.consecutive_healthy_required ?? doc.consecutiveHealthyRequired, renotifyIntervalMinutes: patch.renotify_interval_minutes ?? doc.renotifyIntervalMinutes, apdexThresholdMs: @@ -315,7 +382,6 @@ const mergeUpsertRequest = ( rawQuerySql, rawQueryReducer, alertOnNoData: patch.alert_on_no_data ?? doc.noDataBehavior === "alert", - destinationIds: patch.destination_ids ?? doc.destinationIds, }) }) @@ -325,6 +391,7 @@ const toV2PreviewResult = (preview: AlertRulePreviewResponse): V2AlertRulePrevie window_minutes: preview.windowMinutes, threshold: preview.threshold, threshold_upper: preview.thresholdUpper, + thresholds: Arr.map(preview.thresholds, toV2Threshold), comparator: preview.comparator, truncated_to_start: preview.truncatedToStart, series: preview.series.map((series) => ({ @@ -336,12 +403,14 @@ const toV2PreviewResult = (preview: AlertRulePreviewResponse): V2AlertRulePrevie status: point.status, ...(point.skipReason !== undefined ? { skip_reason: point.skipReason } : undefined), ...(point.provisional !== undefined ? { provisional: point.provisional } : undefined), + ...(point.level !== undefined ? { level: point.level } : undefined), })), })), would_fire: preview.wouldFire.map((span) => ({ group_key: span.groupKey, start: span.start, end: span.end, + severity: span.severity, })), }) diff --git a/apps/api/src/routes/v2/alerts.http.test.ts b/apps/api/src/routes/v2/alerts.http.test.ts index 210ed44bbb..71af4833fb 100644 --- a/apps/api/src/routes/v2/alerts.http.test.ts +++ b/apps/api/src/routes/v2/alerts.http.test.ts @@ -489,6 +489,12 @@ describe("v2 alerts over HTTP", () => { expect(patched.status, JSON.stringify(patched.body)).toBe(200) expect(patched.body.comparator).toBe("gt") expect(patched.body.threshold_upper).toBeNull() + + // A bound sent explicitly is validated, not silently dropped. + const explicit = await harness.request("PATCH", `/v2/alerts/rules/${created.body.id}`, key.secret, { + threshold_upper: 5, + }) + expect(explicit.status, JSON.stringify(explicit.body)).toBe(400) await harness.dispose() }) @@ -545,6 +551,129 @@ describe("v2 alerts over HTTP", () => { await harness.dispose() }) + it("keeps a rule's severity tiers through PATCH and rejects single-threshold edits on them", async () => { + const harness = makeHarness() + const key = await harness.bootstrapKey(["alerts:write"]) + const destination = await harness.request("POST", "/v2/alerts/destinations", key.secret, { + type: "webhook", + name: "Ops hook", + url: "https://example.com/hooks/maple", + }) + const pager = await harness.request("POST", "/v2/alerts/destinations", key.secret, { + type: "webhook", + name: "Pager hook", + url: "https://example.com/hooks/pager", + }) + const tiers = [ + { + severity: "warning", + value: 0.02, + upper: null, + recovery_value: 0.015, + destination_ids: [destination.body.id], + }, + { + severity: "critical", + value: 0.05, + upper: null, + recovery_value: null, + destination_ids: [pager.body.id], + }, + ] + const created = await harness.request("POST", "/v2/alerts/rules", key.secret, { + name: "Checkout error rate tiers", + signal_type: "error_rate", + comparator: "gt", + window_minutes: 5, + thresholds: tiers, + consecutive_breaches_required: 3, + breaches_out_of: 5, + }) + expect(created.status).toBe(200) + expect(created.body.thresholds).toEqual(tiers) + expect(created.body.severity).toBe("critical") + expect(created.body.threshold).toBe(0.02) + expect(created.body.breaches_out_of).toBe(5) + + const toggled = await harness.request("PATCH", `/v2/alerts/rules/${created.body.id}`, key.secret, { + enabled: false, + }) + expect(toggled.status).toBe(200) + expect(toggled.body.thresholds).toEqual(tiers) + expect(toggled.body.breaches_out_of).toBe(5) + + const ambiguous = await harness.request("PATCH", `/v2/alerts/rules/${created.body.id}`, key.secret, { + threshold: 0.1, + }) + expect(ambiguous.status).toBe(400) + expect(JSON.stringify(ambiguous.body)).toContain("thresholds") + + // Echoing the derived single-threshold fields back unchanged is not an edit. + const echoed = await harness.request("PATCH", `/v2/alerts/rules/${created.body.id}`, key.secret, { + name: "Echoed", + severity: created.body.severity, + threshold: created.body.threshold, + threshold_upper: created.body.threshold_upper, + destination_ids: [...created.body.destination_ids].reverse(), + }) + expect(echoed.status, JSON.stringify(echoed.body)).toBe(200) + expect(echoed.body.name).toBe("Echoed") + expect(echoed.body.thresholds).toEqual(tiers) + expect(echoed.body).toMatchObject({ + severity: "critical", + threshold: 0.02, + threshold_upper: null, + breaches_out_of: 5, + }) + expect([...echoed.body.destination_ids].sort()).toEqual([destination.body.id, pager.body.id].sort()) + expect(echoed.body.severity).toBe(created.body.severity) + expect(echoed.body.threshold).toBe(created.body.threshold) + expect(echoed.body.threshold_upper).toBe(created.body.threshold_upper) + expect(echoed.body.breaches_out_of).toBe(5) + expect(echoed.body.destination_ids).toEqual(created.body.destination_ids) + + await harness.dispose() + }) + + it("reads omitted tier bounds as null and names what an invalid rule got wrong", async () => { + const harness = makeHarness() + const key = await harness.bootstrapKey(["alerts:write"]) + const destination = await harness.request("POST", "/v2/alerts/destinations", key.secret, { + type: "webhook", + name: "Ops hook", + url: "https://example.com/hooks/maple", + }) + const created = await harness.request("POST", "/v2/alerts/rules", key.secret, { + name: "Bare tier", + signal_type: "error_rate", + comparator: "gt", + window_minutes: 5, + thresholds: [{ severity: "warning", value: 0.02, destination_ids: [destination.body.id] }], + }) + expect(created.status, JSON.stringify(created.body)).toBe(200) + expect(created.body.thresholds).toEqual([ + { + severity: "warning", + value: 0.02, + upper: null, + recovery_value: null, + destination_ids: [destination.body.id], + }, + ]) + + const missingUpper = await harness.request("POST", "/v2/alerts/rules", key.secret, { + name: "Open range", + signal_type: "error_rate", + comparator: "between", + window_minutes: 5, + thresholds: [{ severity: "warning", value: 0.02, destination_ids: [destination.body.id] }], + }) + expect(missingUpper.status).toBe(400) + expect(missingUpper.body.error.message).toContain("is required for between / not_between comparators") + + await harness.dispose() + }) + it("reports malformed stored rules as a redacted storage failure", async () => { const harness = makeHarness() const key = await harness.bootstrapKey(["alerts:write"]) @@ -552,7 +681,7 @@ describe("v2 alerts over HTTP", () => { expect(created.rule.status).toBe(200) await executeSql( harness.testDb, - "update alert_rules set severity = 'not-a-severity' where org_id = $1", + `update alert_rules set thresholds_json = '[{"severity":"not-a-severity","value":1,"upper":null,"recoveryValue":null,"destinationIds":[]}]'::jsonb where org_id = $1`, ["org_alerts_e2e"], ) @@ -712,6 +841,7 @@ describe("v2 alerts over HTTP", () => { errorMessage: null, errorCategory: "", skipReason: "", + level: "", } }) const pagedWarehouse: WarehouseQueryServiceApi = { diff --git a/apps/cli/src/server/local-schema-history.ts b/apps/cli/src/server/local-schema-history.ts index 3c2059ad52..2b52eb8f03 100644 --- a/apps/cli/src/server/local-schema-history.ts +++ b/apps/cli/src/server/local-schema-history.ts @@ -394,6 +394,19 @@ export const LOCAL_SCHEMA_HISTORY: ReadonlyArray = Obje manifestDigest: "020f17fa907a00c3c44ab7a05cc4306933a4d8f4542ea99d441728b47b7eaca6", projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a", }), + Object.freeze({ + // alert_checks gains Level (metadata-only ADD COLUMN with a '' default): + // no part is rewritten, no row moves, and existing checks are not backfilled. + // + // 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: 32, + fingerprint: "7bc1c04157f9f51f", + digest: "7bc1c04157f9f51f68efd2668e1447385491a3589d384844810e929df83becc7", + manifestDigest: "4b47147a87fbdda6a9efa8c96bc45306e356339f7b664ed40f3058d802dc2d1e", + projectRevision: "ed74788ef292834069e0ea6ee3b22d68fc604fb66cb54d2d551db67ce8d20b3a", + }), ] as const) /** Immutable SQLite control DDL identities, checked by clickhouse:schema:check. */ diff --git a/apps/cli/src/server/local-schema-version.ts b/apps/cli/src/server/local-schema-version.ts index 14ecab83b0..af4996f211 100644 --- a/apps/cli/src/server/local-schema-version.ts +++ b/apps/cli/src/server/local-schema-version.ts @@ -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 = 31 as const +export const LOCAL_SCHEMA_VERSION = 32 as const /** SQLite eventing state has its own independent version sequence. */ export const LOCAL_CONTROL_SCHEMA_VERSION = 1 as const diff --git a/apps/cli/src/server/local-store-migrations/steps.ts b/apps/cli/src/server/local-store-migrations/steps.ts index a0b55b86e2..d3395d04fb 100644 --- a/apps/cli/src/server/local-store-migrations/steps.ts +++ b/apps/cli/src/server/local-store-migrations/steps.ts @@ -1280,5 +1280,29 @@ export const LOCAL_STORE_STEPS: ReadonlyArray = [ { ...ERROR_EVENTS_BY_TIME_FORWARD_ONLY, disposition: "preserve-exact" }, ], }, + { + id: "local-0031-to-0032-alert-checks-level", + from: 31, + to: 32, + description: "Add Level to alert_checks", + clonedBefore: "any DDL runs", + beforeBootstrap: [addColumns("alert_checks", [["Level", "LowCardinality(String) DEFAULT ''"]])], + plan: [ + [ + "widen-alert-checks", + "Add Level to alert_checks so checks record the severity tier they breached", + ], + ], + verifies: "Verify the v32 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 Level; only checks recorded after the migration carry one.", + }, + ], + }, // local-schema:bump appends the next step above this line. ] diff --git a/apps/cli/src/server/schema-identity.ts b/apps/cli/src/server/schema-identity.ts index 9facf5745e..337c112bfe 100644 --- a/apps/cli/src/server/schema-identity.ts +++ b/apps/cli/src/server/schema-identity.ts @@ -30,6 +30,7 @@ import schemaV28Sql from "./schema/local-schema-v28.sql" with { type: "text" } import schemaV29Sql from "./schema/local-schema-v29.sql" with { type: "text" } import schemaV30Sql from "./schema/local-schema-v30.sql" with { type: "text" } import schemaV31Sql from "./schema/local-schema-v31.sql" with { type: "text" } +import schemaV32Sql from "./schema/local-schema-v32.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" @@ -105,6 +106,7 @@ const SNAPSHOT_SQL: ReadonlyArray = [ schemaV29Sql, schemaV30Sql, schemaV31Sql, + schemaV32Sql, ] export interface LocalSchemaSnapshot { @@ -193,6 +195,7 @@ export const LOCAL_SCHEMA_V28 = localSchemaIdentity(28) export const LOCAL_SCHEMA_V29 = localSchemaIdentity(29) export const LOCAL_SCHEMA_V30 = localSchemaIdentity(30) export const LOCAL_SCHEMA_V31 = localSchemaIdentity(31) +export const LOCAL_SCHEMA_V32 = localSchemaIdentity(32) export const CURRENT_LOCAL_SCHEMA: LocalSchemaIdentity = Object.freeze({ version: LOCAL_SCHEMA_VERSION, diff --git a/apps/cli/src/server/schema/local-inserts.json b/apps/cli/src/server/schema/local-inserts.json index 28a5ad1ff8..74bcd23cec 100644 --- a/apps/cli/src/server/schema/local-inserts.json +++ b/apps/cli/src/server/schema/local-inserts.json @@ -1,5 +1,5 @@ { - "projectRevision": "64b7ae1279269efadefe2cb5773eaf28967c16da7af3d0659c297770532bbc97", + "projectRevision": "9c4759bea56d9544000d0326a35b6224fa56ec76c373ce34bf16869430700c0a", "orgPlaceholder": "__ORG__", "datasources": { "traces": { diff --git a/apps/cli/src/server/schema/local-schema-v32.sql b/apps/cli/src/server/schema/local-schema-v32.sql new file mode 100644 index 0000000000..f2ed09d98e --- /dev/null +++ b/apps/cli/src/server/schema/local-schema-v32.sql @@ -0,0 +1,2151 @@ +-- This file is generated by scripts/generate-clickhouse-schema-sql.ts +-- Do not edit manually. Run `bun run clickhouse:schema` to regenerate. +-- projectRevision: 9c4759bea56d9544000d0326a35b6224fa56ec76c373ce34bf16869430700c0a +-- localSchemaVersion: 31 + +CREATE TABLE IF NOT EXISTS ai_crawler_requests ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + ServiceName LowCardinality(String), + Crawler LowCardinality(String), + Host LowCardinality(String), + Path String, + HttpStatus UInt16 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, TraceId) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS ai_trace_index ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + SessionId String, + VendorId LowCardinality(String), + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + Model LowCardinality(String), + AgentName LowCardinality(String), + ToolName LowCardinality(String), + SpanId String, + ParentSpanId String, + Duration UInt64, + IsError UInt8, + IsLlmCall UInt8, + IsToolCall UInt8, + Tokens Float64, + Cost Float64, + ResponseId String, + VendorVersion LowCardinality(String), + InputTokens Float64, + CacheReadTokens Float64, + CacheWriteTokens Float64, + OutputTokens Float64, + ReasoningTokens Float64, + ErrorType LowCardinality(String), + StatusMessage String, + ToolDescription String, + FailedToolCallResult String, + ErrorFingerprint UInt64 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, TraceId) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS alert_checks ( + OrgId LowCardinality(String), + RuleId String, + GroupKey String, + Timestamp DateTime64(3), + Status LowCardinality(String), + SignalType LowCardinality(String), + Comparator LowCardinality(String), + Threshold Float64, + ObservedValue Nullable(Float64), + SampleCount UInt32, + WindowMinutes UInt16, + WindowStart DateTime64(3), + WindowEnd DateTime64(3), + ConsecutiveBreaches UInt16, + ConsecutiveHealthy UInt16, + IncidentId Nullable(String), + IncidentTransition LowCardinality(String), + EvaluationDurationMs UInt32, + ErrorMessage Nullable(String), + ErrorCategory LowCardinality(String), + SkipReason LowCardinality(String) DEFAULT '', + Level LowCardinality(String) DEFAULT '' +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, RuleId, GroupKey, Timestamp) +TTL toDate(Timestamp) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS attribute_keys_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + AttributeKey LowCardinality(String), + AttributeScope LowCardinality(String), + UsageCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, AttributeScope, Hour, AttributeKey) +TTL Hour + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS attribute_values_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + AttributeKey LowCardinality(String), + AttributeValue String, + AttributeScope LowCardinality(String), + UsageCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, AttributeScope, AttributeKey, Hour, AttributeValue) +TTL Hour + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS audit_log ( + OrgId LowCardinality(String), + Id String, + OccurredAt DateTime64(3), + RecordedAt DateTime64(3), + ActorType LowCardinality(String), + UserId String, + ApiKeyId String, + ActorId String, + ActorLabel String, + AffectedUserId String, + Source LowCardinality(String), + Action LowCardinality(String), + Outcome LowCardinality(String), + DenialReason String, + ResourceType LowCardinality(String), + ResourceId String, + ChangedFields Array(String), + Changes String, + Metadata String, + RequestId String, + OriginIp String, + OriginCountry LowCardinality(String) +) +ENGINE = ReplacingMergeTree +PARTITION BY toYYYYMM(OccurredAt) +ORDER BY (OrgId, OccurredAt, Id) +TTL toDate(OccurredAt) + INTERVAL 2190 DAY; + +CREATE TABLE IF NOT EXISTS error_events ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + SpanId String, + ParentSpanId String DEFAULT '__unset__', + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ExceptionType LowCardinality(String), + ExceptionMessage String, + ExceptionStacktrace String, + TopFrame String, + FingerprintHash UInt64, + StatusMessage String, + Duration UInt64, + ErrorLabel String, + ServiceVersion LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, FingerprintHash, Timestamp) +TTL Timestamp + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS error_events_by_time ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + SpanId String, + ParentSpanId String DEFAULT '__unset__', + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ExceptionType LowCardinality(String), + ExceptionMessage String, + ExceptionStacktrace String, + TopFrame String, + FingerprintHash UInt64, + StatusMessage String, + Duration UInt64, + ErrorLabel String, + ServiceVersion LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, FingerprintHash) +TTL Timestamp + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS error_fingerprints_minutely ( + OrgId LowCardinality(String), + Minute DateTime, + FingerprintHash UInt64, + ServiceName SimpleAggregateFunction(anyLast, String), + ExceptionType SimpleAggregateFunction(anyLast, String), + ExceptionMessage SimpleAggregateFunction(anyLast, String), + ErrorLabel SimpleAggregateFunction(anyLast, String), + TopFrame SimpleAggregateFunction(anyLast, String), + OccurrenceCount SimpleAggregateFunction(sum, UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + LastSeen SimpleAggregateFunction(max, DateTime), + ServiceVersions SimpleAggregateFunction(groupUniqArrayArray, Array(String)) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Minute) +ORDER BY (OrgId, Minute, FingerprintHash) +TTL Minute + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS identity_links ( + OrgId LowCardinality(String), + VisitorId String, + UserId String, + FirstSeen SimpleAggregateFunction(min, DateTime64(9)) +) +ENGINE = AggregatingMergeTree +PARTITION BY tuple() +ORDER BY (OrgId, VisitorId, UserId) +TTL toDate(FirstSeen) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS logs ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TimestampTime DateTime, + TraceId String, + SpanId String, + TraceFlags UInt8, + SeverityText LowCardinality(String), + SeverityNumber UInt8, + ServiceName LowCardinality(String), + Body String, + ResourceSchemaUrl String, + ResourceAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + LogAttributes Map(LowCardinality(String), String), + ResourceAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ResourceAttributes), mapValues(ResourceAttributes)), + ScopeAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ScopeAttributes), mapValues(ScopeAttributes)), + LogAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(LogAttributes), mapValues(LogAttributes)), + INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_keys mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_vals mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_keys mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_vals mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_log_attr_keys mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_log_attr_vals mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_lower_body lower(Body) TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 8 +) +ENGINE = MergeTree +PARTITION BY toDate(TimestampTime) +ORDER BY (OrgId, toStartOfFiveMinutes(Timestamp), ServiceName, Timestamp) +TTL toDate(TimestampTime) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS logs_aggregates_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SeverityText LowCardinality(String), + DeploymentEnv LowCardinality(String), + Count SimpleAggregateFunction(sum, UInt64), + SizeBytes SimpleAggregateFunction(sum, UInt64), + ServiceNamespace LowCardinality(String), + INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4 +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, SeverityText, DeploymentEnv, ServiceNamespace) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS metric_catalog ( + OrgId LowCardinality(String), + Hour DateTime, + MetricType LowCardinality(String), + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription SimpleAggregateFunction(anyLast, String), + MetricUnit SimpleAggregateFunction(anyLast, String), + IsMonotonic SimpleAggregateFunction(anyLast, UInt8), + DataPointCount SimpleAggregateFunction(sum, UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + LastSeen SimpleAggregateFunction(max, DateTime) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, MetricType, ServiceName, MetricName, Hour) +TTL Hour + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_exponential_histogram ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Count UInt64, + Sum Float64, + Scale Int32, + ZeroCount UInt64, + PositiveOffset Int32, + PositiveBucketCounts Array(UInt64), + NegativeOffset Int32, + NegativeBucketCounts Array(UInt64), + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)), + Flags UInt32, + Min Nullable(Float64), + Max Nullable(Float64), + AggregationTemporality Int32 +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_gauge ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Value Float64, + Flags UInt32, + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)) +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_histogram ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Count UInt64, + Sum Float64, + BucketCounts Array(UInt64), + ExplicitBounds Array(Float64), + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)), + Flags UInt32, + Min Nullable(Float64), + Max Nullable(Float64), + AggregationTemporality Int32 +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS metrics_sum ( + OrgId LowCardinality(String), + ResourceAttributes Map(LowCardinality(String), String), + ResourceSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + MetricDescription LowCardinality(String), + MetricUnit LowCardinality(String), + Attributes Map(LowCardinality(String), String), + StartTimeUnix DateTime64(9), + TimeUnix DateTime64(9), + Value Float64, + Flags UInt32, + ExemplarsTraceId Array(String), + ExemplarsSpanId Array(String), + ExemplarsTimestamp Array(DateTime64(9)), + ExemplarsValue Array(Float64), + ExemplarsFilteredAttributes Array(Map(LowCardinality(String), String)), + AggregationTemporality Int32, + IsMonotonic Bool +) +ENGINE = MergeTree +PARTITION BY toDate(TimeUnix) +ORDER BY (OrgId, ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDate(TimeUnix) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS product_events ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + Source LowCardinality(String) DEFAULT 'browser', + SessionId String DEFAULT '', + Seq UInt32 DEFAULT 0, + VisitorId String DEFAULT '', + UserId String DEFAULT '', + GroupId String DEFAULT '', + Kind LowCardinality(String), + EventName String, + Host LowCardinality(String) DEFAULT '', + PagePath String DEFAULT '', + Url String DEFAULT '', + ServiceName LowCardinality(String) DEFAULT '', + Attributes Map(String, String) DEFAULT map(), + TraceId String DEFAULT '', + SpanId String DEFAULT '', + INDEX idx_event_name EventName TYPE set(64) GRANULARITY 4, + INDEX idx_user_id UserId TYPE bloom_filter GRANULARITY 4, + INDEX idx_trace_id TraceId TYPE bloom_filter GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, VisitorId, SessionId, Seq) +TTL toDate(Timestamp) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_address_resolutions_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + SourceService LowCardinality(String), + ParentServerAddress String, + ResolvedTargetService LowCardinality(String), + DeploymentEnv LowCardinality(String) +) +ENGINE = ReplacingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, SourceService, ParentServerAddress, ResolvedTargetService) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_external_edges_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + TargetType LowCardinality(String), + TargetSystem LowCardinality(String), + TargetName String, + DeploymentEnv LowCardinality(String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + DurationSumMs SimpleAggregateFunction(sum, Float64), + MaxDurationMs SimpleAggregateFunction(max, Float64), + SampleRateSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95), UInt64, UInt32) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, ServiceName, TargetType, TargetSystem, TargetName) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_children ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + ParentSpanId String, + ServiceName LowCardinality(String), + SpanKind LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + TraceState String, + DeploymentEnv LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, TraceId, ParentSpanId, Timestamp) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS service_map_db_edges_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DbSystem LowCardinality(String), + DeploymentEnv LowCardinality(String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + DurationSumMs SimpleAggregateFunction(sum, Float64), + MaxDurationMs SimpleAggregateFunction(max, Float64), + SampledSpanCount SimpleAggregateFunction(sum, UInt64), + UnsampledSpanCount SimpleAggregateFunction(sum, UInt64), + SampleRateSum SimpleAggregateFunction(sum, Float64), + DbNamespace LowCardinality(String), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95), UInt64, UInt32) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, ServiceName, DbSystem, DbNamespace) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_db_query_shapes_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DbSystem LowCardinality(String), + DeploymentEnv LowCardinality(String), + QueryKey String, + QueryLabel SimpleAggregateFunction(any, String), + SampleStatement SimpleAggregateFunction(any, String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedCount SimpleAggregateFunction(sum, Float64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + WeightedDurationSumMs SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95), UInt64, UInt32), + DbNamespace LowCardinality(String) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, ServiceName, DbSystem, DbNamespace, QueryKey) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_edges_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + SourceService LowCardinality(String), + TargetService String, + DeploymentEnv LowCardinality(String), + CallCount SimpleAggregateFunction(sum, UInt64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + DurationSumMs SimpleAggregateFunction(sum, Float64), + MaxDurationMs SimpleAggregateFunction(max, Float64), + SampledSpanCount SimpleAggregateFunction(sum, UInt64), + UnsampledSpanCount SimpleAggregateFunction(sum, UInt64), + SampleRateSum SimpleAggregateFunction(sum, Float64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, DeploymentEnv, SourceService, TargetService) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_map_edges_hourly_ingest ( + OrgId LowCardinality(String), + Hour DateTime, + SourceService LowCardinality(String), + TargetService String, + DeploymentEnv LowCardinality(String), + CallCount UInt64, + ErrorCount UInt64, + DurationSumMs Float64, + MaxDurationMs Float64, + SampledSpanCount UInt64, + UnsampledSpanCount UInt64, + SampleRateSum Float64 +) +ENGINE = Null; + +CREATE TABLE IF NOT EXISTS service_map_spans ( + OrgId LowCardinality(String), + Timestamp DateTime, + TraceId String, + SpanId String, + ParentSpanId String, + ServiceName LowCardinality(String), + SpanKind LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + TraceState String, + DeploymentEnv LowCardinality(String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, TraceId, SpanId, Timestamp) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS service_operations_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + SpanName String, + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64), + ClassifiedSpanCount SimpleAggregateFunction(sum, UInt64), + ServerSpanCount SimpleAggregateFunction(sum, UInt64), + RoutedSpanCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Hour) +ORDER BY (OrgId, ServiceName, DeploymentEnv, Hour, SpanName) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_operations_minutely ( + OrgId LowCardinality(String), + Minute DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + SpanName String, + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64), + ClassifiedSpanCount SimpleAggregateFunction(sum, UInt64), + ServerSpanCount SimpleAggregateFunction(sum, UInt64), + RoutedSpanCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Minute) +ORDER BY (OrgId, ServiceName, DeploymentEnv, Minute, SpanName) +TTL toDate(Minute) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS service_overview_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + CommitSha LowCardinality(String), + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95, 0.99), UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + ApdexSatisfiedCount SimpleAggregateFunction(sum, UInt64), + ApdexToleratingCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toYYYYMM(Hour) +ORDER BY (OrgId, ServiceName, Hour, DeploymentEnv, ServiceNamespace, CommitSha) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_overview_minutely ( + OrgId LowCardinality(String), + Minute DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + CommitSha LowCardinality(String), + SpanCount SimpleAggregateFunction(sum, UInt64), + EstimatedSpanCount SimpleAggregateFunction(sum, Float64), + ErrorCount SimpleAggregateFunction(sum, UInt64), + EstimatedErrorCount SimpleAggregateFunction(sum, Float64), + DurationSum SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95, 0.99), UInt64), + FirstSeen SimpleAggregateFunction(min, DateTime), + ApdexSatisfiedCount SimpleAggregateFunction(sum, UInt64), + ApdexToleratingCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Minute) +ORDER BY (OrgId, ServiceName, Minute, DeploymentEnv, ServiceNamespace, CommitSha) +TTL toDate(Minute) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS service_overview_spans ( + OrgId LowCardinality(String), + Timestamp DateTime, + ServiceName LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + TraceState String, + DeploymentEnv LowCardinality(String), + CommitSha LowCardinality(String), + SampleRate Float64 DEFAULT 1, + ServiceNamespace LowCardinality(String), + SpanKind LowCardinality(String) DEFAULT '', + IsRoot UInt8 DEFAULT 0, + INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, ServiceName, Timestamp) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS service_platforms_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + DeploymentEnv LowCardinality(String), + K8sCluster SimpleAggregateFunction(max, String), + K8sPodName SimpleAggregateFunction(max, String), + K8sDeploymentName SimpleAggregateFunction(max, String), + K8sStatefulSetName SimpleAggregateFunction(max, String), + K8sDaemonSetName SimpleAggregateFunction(max, String), + K8sNamespaceName SimpleAggregateFunction(max, String), + CloudPlatform SimpleAggregateFunction(max, String), + CloudProvider SimpleAggregateFunction(max, String), + FaasName SimpleAggregateFunction(max, String), + MapleSdkType SimpleAggregateFunction(max, String), + ProcessRuntimeName SimpleAggregateFunction(max, String), + SpanCount SimpleAggregateFunction(sum, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, DeploymentEnv) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS service_usage ( + OrgId LowCardinality(String), + ServiceName LowCardinality(String), + Hour DateTime, + LogCount UInt64, + LogSizeBytes UInt64, + TraceCount UInt64, + TraceSizeBytes UInt64, + SumMetricCount UInt64, + SumMetricSizeBytes UInt64, + GaugeMetricCount UInt64, + GaugeMetricSizeBytes UInt64, + HistogramMetricCount UInt64, + HistogramMetricSizeBytes UInt64, + ExpHistogramMetricCount UInt64, + ExpHistogramMetricSizeBytes UInt64 +) +ENGINE = SummingMergeTree +ORDER BY (OrgId, ServiceName, Hour) +TTL Hour + INTERVAL 365 DAY; + +CREATE TABLE IF NOT EXISTS session_events ( + OrgId LowCardinality(String), + SessionId String, + Timestamp DateTime64(9), + Seq UInt32 DEFAULT 0, + Type LowCardinality(String), + Url String DEFAULT '', + TraceId String DEFAULT '', + Level LowCardinality(String) DEFAULT '', + Message String DEFAULT '', + TargetSelector String DEFAULT '', + TargetText String DEFAULT '', + NetMethod LowCardinality(String) DEFAULT '', + NetUrl String DEFAULT '', + NetStatus UInt16 DEFAULT 0, + NetDurationMs UInt32 DEFAULT 0, + ErrorStack String DEFAULT '', + Attributes Map(String, String), + VisitorId String DEFAULT '', + UserId String DEFAULT '', + GroupId String DEFAULT '', + INDEX idx_type Type TYPE set(16) GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, SessionId, Timestamp, Seq) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS session_replay_events ( + OrgId LowCardinality(String), + SessionId String, + ChunkSeq UInt32, + Timestamp DateTime64(9), + DurationMs UInt32 DEFAULT 0, + EventCount UInt32 DEFAULT 0, + ByteSize UInt32 DEFAULT 0, + Events String, + IsCheckpoint UInt8 DEFAULT 0 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, SessionId, ChunkSeq) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS session_replays ( + OrgId LowCardinality(String), + SessionId String, + StartTime DateTime64(9), + EndTime Nullable(DateTime64(9)), + DurationMs Nullable(UInt32), + Status LowCardinality(String), + UserId String, + UrlInitial String, + UserAgent String, + BrowserName LowCardinality(String), + OsName LowCardinality(String), + DeviceType LowCardinality(String), + Country LowCardinality(String) DEFAULT '', + ServiceName LowCardinality(String), + PageViews UInt32 DEFAULT 0, + ClickCount UInt32 DEFAULT 0, + ErrorCount UInt32 DEFAULT 0, + TraceIds Array(String) DEFAULT [], + ResourceAttributes Map(LowCardinality(String), String), + Version UInt32, + VisitorId String DEFAULT '', + VisitorIsNew UInt8 DEFAULT 0, + UserEmail String DEFAULT '', + UserName String DEFAULT '', + GroupId String DEFAULT '', + GroupName String DEFAULT '', + UserTraits Map(String, String) DEFAULT map(), + Referrer String DEFAULT '', + ReferrerHost LowCardinality(String) DEFAULT '', + UtmSource LowCardinality(String) DEFAULT '', + UtmMedium LowCardinality(String) DEFAULT '', + UtmCampaign LowCardinality(String) DEFAULT '', + UtmTerm String DEFAULT '', + UtmContent String DEFAULT '', + Host LowCardinality(String) DEFAULT '', + EntryPath String DEFAULT '', + ExitPath String DEFAULT '', + Language LowCardinality(String) DEFAULT '', + LastActivityAt Nullable(DateTime64(9)) +) +ENGINE = ReplacingMergeTree +PARTITION BY toDate(StartTime) +ORDER BY (OrgId, SessionId) +TTL toDate(StartTime) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS span_metrics_calls_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + MetricName LowCardinality(String), + SpanKind LowCardinality(String), + AttrFingerprint UInt64, + ResourceFingerprint UInt64, + StartTimeUnix DateTime64(9), + LastValue AggregateFunction(argMax, Float64, DateTime64(9)) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix) +TTL toDate(Hour) + INTERVAL 90 DAY; + +CREATE TABLE IF NOT EXISTS trace_detail_spans ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + SpanId String, + ParentSpanId String, + SpanName LowCardinality(String), + SpanKind LowCardinality(String), + ServiceName LowCardinality(String), + Duration UInt64 DEFAULT 0, + StatusCode LowCardinality(String), + StatusMessage String, + SpanAttributes Map(LowCardinality(String), String), + ResourceAttributes Map(LowCardinality(String), String) +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, TraceId, SpanId) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS trace_facets_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SpanName String, + HttpMethod LowCardinality(String), + HttpStatusCode LowCardinality(String), + DeploymentEnv LowCardinality(String), + ServiceNamespace LowCardinality(String), + HasError UInt8, + TraceCount SimpleAggregateFunction(sum, UInt64), + DurationMin SimpleAggregateFunction(min, UInt64), + DurationMax SimpleAggregateFunction(max, UInt64), + DurationQuantiles AggregateFunction(quantilesTDigest(0.5, 0.95), UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError) +TTL Hour + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS trace_list_mv ( + OrgId LowCardinality(String), + TraceId String, + Timestamp DateTime, + ServiceName LowCardinality(String), + SpanName String, + SpanKind LowCardinality(String), + Duration UInt64, + StatusCode LowCardinality(String), + HttpMethod LowCardinality(String), + HttpRoute String, + HttpStatusCode LowCardinality(String), + DeploymentEnv LowCardinality(String), + HasError UInt8, + TraceState String, + ServiceNamespace LowCardinality(String), + INDEX idx_service_namespace ServiceNamespace TYPE set(1000) GRANULARITY 4 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, Timestamp, TraceId) +TTL Timestamp + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS traces ( + OrgId LowCardinality(String), + Timestamp DateTime64(9), + TraceId String, + SpanId String, + ParentSpanId String, + TraceState String, + SpanName LowCardinality(String), + SpanKind LowCardinality(String), + ServiceName LowCardinality(String), + ResourceSchemaUrl String, + ResourceAttributes Map(LowCardinality(String), String), + ScopeSchemaUrl String, + ScopeName String, + ScopeVersion String, + ScopeAttributes Map(LowCardinality(String), String), + Duration UInt64 DEFAULT 0, + StatusCode LowCardinality(String), + StatusMessage String, + SpanAttributes Map(LowCardinality(String), String), + EventsTimestamp Array(DateTime64(9)), + EventsName Array(LowCardinality(String)), + EventsAttributes Array(Map(LowCardinality(String), String)), + LinksTraceId Array(String), + LinksSpanId Array(String), + LinksTraceState Array(String), + LinksAttributes Array(Map(LowCardinality(String), String)), + SampleRate Float64 DEFAULT multiIf(SpanAttributes['SampleRate'] != '' AND toFloat64OrZero(SpanAttributes['SampleRate']) >= 1.0, toFloat64OrZero(SpanAttributes['SampleRate']), match(TraceState, 'th:[0-9a-f]+'), 1.0 / greatest(1.0 - reinterpretAsUInt64(reverse(unhex(rightPad(extract(TraceState, 'th:([0-9a-f]+)'), 16, '0')))) / pow(2.0, 64), 0.0001), 1.0), + IsEntryPoint UInt8 DEFAULT if(SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '', 1, 0), + ResourceAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ResourceAttributes), mapValues(ResourceAttributes)), + ScopeAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(ScopeAttributes), mapValues(ScopeAttributes)), + SpanAttributeItems Array(String) DEFAULT arrayMap((k, v) -> concat(k, char(31), v), mapKeys(SpanAttributes), mapValues(SpanAttributes)), + INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_span_attr_keys mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_span_attr_vals mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_keys mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_resource_attr_vals mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_keys mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, + INDEX idx_scope_attr_vals mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1 +) +ENGINE = MergeTree +PARTITION BY toDate(Timestamp) +ORDER BY (OrgId, ServiceName, SpanName, toDateTime(Timestamp)) +TTL toDate(Timestamp) + INTERVAL 30 DAY; + +CREATE TABLE IF NOT EXISTS traces_aggregates_hourly ( + OrgId LowCardinality(String), + Hour DateTime, + ServiceName LowCardinality(String), + SpanName LowCardinality(String), + SpanKind LowCardinality(String), + StatusCode LowCardinality(String), + IsEntryPoint UInt8, + DeploymentEnv LowCardinality(String), + WeightedCount SimpleAggregateFunction(sum, Float64), + WeightedDurationSum SimpleAggregateFunction(sum, Float64), + WeightedErrorCount SimpleAggregateFunction(sum, Float64), + DurationQuantiles AggregateFunction(quantilesTDigestWeighted(0.5, 0.95, 0.99), UInt64, UInt32), + DurationMin SimpleAggregateFunction(min, UInt64), + DurationMax SimpleAggregateFunction(max, UInt64) +) +ENGINE = AggregatingMergeTree +PARTITION BY toDate(Hour) +ORDER BY (OrgId, Hour, ServiceName, SpanName, SpanKind, StatusCode, IsEntryPoint, DeploymentEnv) +TTL toDate(Hour) + INTERVAL 365 DAY; + +CREATE MATERIALIZED VIEW IF NOT EXISTS ai_crawler_requests_mv TO ai_crawler_requests AS +SELECT + OrgId, + Timestamp, + TraceId, + ServiceName, + arrayElement(['GPTBot', 'OAI-SearchBot', 'ChatGPT-User', 'ClaudeBot', 'anthropic-ai', 'Claude-SearchBot', 'Claude-User', 'PerplexityBot', 'Perplexity-User', 'Meta-ExternalAgent', 'Meta-WebIndexer', 'Meta-ExternalFetcher', 'Bytespider', 'MistralAI-User', 'CCBot', 'Amazonbot', 'DuckAssistBot', 'cohere-training-data-crawler', 'cohere-ai'], multiSearchFirstIndexCaseInsensitive(coalesce(nullIf(SpanAttributes['user_agent.original'], ''), nullIf(SpanAttributes['http.user_agent'], ''), ''), ['GPTBot', 'OAI-SearchBot', 'ChatGPT-User', 'ClaudeBot', 'anthropic-ai', 'Claude-SearchBot', 'Claude-User', 'PerplexityBot', 'Perplexity-User', 'meta-externalagent', 'meta-webindexer', 'meta-externalfetcher', 'Bytespider', 'MistralAI-User', 'CCBot', 'Amazonbot', 'DuckAssistBot', 'cohere-training-data-crawler', 'cohere-ai'])) AS Crawler, + lower(replaceRegexpOne(coalesce(nullIf(SpanAttributes['server.address'], ''), nullIf(SpanAttributes['http.host'], ''), nullIf(SpanAttributes['net.host.name'], ''), nullIf(domain(SpanAttributes['url.full']), ''), nullIf(domain(SpanAttributes['http.url']), ''), ''), ':[0-9]+$', '')) AS Host, + leftUTF8(coalesce(nullIf(SpanAttributes['url.path'], ''), nullIf(replaceRegexpOne(SpanAttributes['http.target'], '[?#].*$', ''), ''), nullIf(path(SpanAttributes['url.full']), ''), nullIf(path(SpanAttributes['http.url']), ''), ''), 512) AS Path, + toUInt16OrZero(coalesce(nullIf(SpanAttributes['http.response.status_code'], ''), nullIf(SpanAttributes['http.status_code'], ''), '')) AS HttpStatus + FROM traces + WHERE SpanKind = 'Server' + AND multiSearchFirstIndexCaseInsensitive(coalesce(nullIf(SpanAttributes['user_agent.original'], ''), nullIf(SpanAttributes['http.user_agent'], ''), ''), ['GPTBot', 'OAI-SearchBot', 'ChatGPT-User', 'ClaudeBot', 'anthropic-ai', 'Claude-SearchBot', 'Claude-User', 'PerplexityBot', 'Perplexity-User', 'meta-externalagent', 'meta-webindexer', 'meta-externalfetcher', 'Bytespider', 'MistralAI-User', 'CCBot', 'Amazonbot', 'DuckAssistBot', 'cohere-training-data-crawler', 'cohere-ai']) > 0 + AND leftUTF8(coalesce(nullIf(SpanAttributes['url.path'], ''), nullIf(replaceRegexpOne(SpanAttributes['http.target'], '[?#].*$', ''), ''), nullIf(path(SpanAttributes['url.full']), ''), nullIf(path(SpanAttributes['http.url']), ''), ''), 512) != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS ai_trace_index_mv TO ai_trace_index AS +SELECT + OrgId, + Timestamp, + TraceId, + SpanAttributes['maple_ai.session.id'] AS SessionId, + SpanAttributes['maple_ai.vendor.id'] AS VendorId, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + SpanAttributes['maple_ai.model'] AS Model, + SpanAttributes['maple_ai.agent.name'] AS AgentName, + SpanAttributes['maple_ai.tool.name'] AS ToolName, + SpanId, + ParentSpanId, + Duration, + toUInt8(SpanAttributes['maple_ai.error'] = '1') AS IsError, + toUInt8(SpanAttributes['maple_ai.llm_call'] = '1') AS IsLlmCall, + toUInt8(SpanAttributes['maple_ai.tool_call'] = '1') AS IsToolCall, + toFloat64OrZero(SpanAttributes['maple_ai.usage.input_tokens']) + toFloat64OrZero(SpanAttributes['maple_ai.usage.cache_read_tokens']) + toFloat64OrZero(SpanAttributes['maple_ai.usage.cache_write_tokens']) + toFloat64OrZero(SpanAttributes['maple_ai.usage.output_tokens']) + toFloat64OrZero(SpanAttributes['maple_ai.usage.reasoning_tokens']) AS Tokens, + toFloat64OrZero(SpanAttributes['maple_ai.usage.cost']) AS Cost, + SpanAttributes['maple_ai.response.id'] AS ResponseId, + SpanAttributes['maple_ai.vendor.version'] AS VendorVersion, + toFloat64OrZero(SpanAttributes['maple_ai.usage.input_tokens']) AS InputTokens, + toFloat64OrZero(SpanAttributes['maple_ai.usage.cache_read_tokens']) AS CacheReadTokens, + toFloat64OrZero(SpanAttributes['maple_ai.usage.cache_write_tokens']) AS CacheWriteTokens, + toFloat64OrZero(SpanAttributes['maple_ai.usage.output_tokens']) AS OutputTokens, + toFloat64OrZero(SpanAttributes['maple_ai.usage.reasoning_tokens']) AS ReasoningTokens, + SpanAttributes['error.type'] AS ErrorType, + leftUTF8(StatusMessage, 400) AS StatusMessage, + SpanAttributes['maple_ai.tool.description'] AS ToolDescription, + SpanAttributes['maple_ai.tool.error_result'] AS FailedToolCallResult, + if(SpanAttributes['maple_ai.error'] = '1', cityHash64(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(leftUTF8(coalesce(nullIf(SpanAttributes['maple_ai.tool.error_result'], ''), leftUTF8(StatusMessage, 400)), 400), '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'EMAIL'), 'https?://[^/ )"]+', ''), '/(Users|home)/[^/ ]+', '/~'), '[?][A-Za-z0-9_]+=[^ )"]*', '?#'), '\'[^\' ]*/[^\' ]*\'|\'[^\' ]{25,}\'', '\'#\''), '"[^" ]*/[^" ]*"|"[^" ]{25,}"', '"#"'), '`[^` ]*/[^` ]*`|`[^` ]{25,}`', '`#`'), '[0-9a-fA-F-]{6,}|[0-9]+', '#')), 0) AS ErrorFingerprint + FROM traces + WHERE SpanAttributes['maple_ai.vendor.id'] != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS error_events_by_time_mv TO error_events_by_time AS +WITH + arrayFirstIndex(n -> n = 'exception', EventsName) AS _ei, + -- Only fill the old Unknown Error bucket. Event values (including + -- empty fields) and spans with StatusMessage keep every hash input. + _ei = 0 AND StatusMessage = '' AS _useAttrs, + if( + _ei > 0, EventsAttributes[_ei]['exception.type'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.type'], ''), SpanAttributes['error.type']), '') + ) AS _exType, + if( + _ei > 0, EventsAttributes[_ei]['exception.message'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.message'], ''), SpanAttributes['error.message']), StatusMessage) + ) AS _exMsg, + if( + _ei > 0, EventsAttributes[_ei]['exception.stacktrace'], + if(_useAttrs, SpanAttributes['exception.stacktrace'], '') + ) AS _exStack, + if(_useAttrs, _exMsg, StatusMessage) AS _msgText, + -- JSON detection for the message signature below. + isValidJSON(_msgText) AS _isJson, + _isJson AND JSONType(_msgText) = 'Object' AS _isJsonObj, + -- General, KEY-NAME-AGNOSTIC canonical signature: iterate ALL top-level + -- keys, redact volatile tokens (long hex / numbers) in each raw value, then + -- sort by "key=value" so key order & whitespace don't matter. No assumption + -- about which keys exist — works for any producer's JSON shape. (Nested + -- objects are hashed as their raw substring; only top-level is canonicalized.) + arrayStringConcat( + arraySort( + arrayMap( + kv -> concat(kv.1, '=', replaceRegexpAll(kv.2, '[0-9a-fA-F-]{6,}|[0-9]+', '#')), + JSONExtractKeysAndValuesRaw(_msgText) + ) + ), + '|' + ) AS _jsonSig, + -- The message signature is folded in ALWAYS, not only when there are no + -- frames. Bundled runtimes minify every module into one file, so the top + -- three frames of a Worker error are `toDatabaseError (worker.js)` for + -- every failing query alike: on frames alone, 25 distinct DatabaseError + -- bugs (316k occurrences) collapse into a single issue. The signature + -- restores that discrimination, and it cannot reinflate cardinality the + -- way a raw prefix would because everything variable is redacted first: + -- emails, URL origins, home directories, query strings, quoted values, + -- then ids and every digit run. See ERROR_MSG_REDACTIONS for the order, + -- what is deliberately kept, and the one residual it cannot reach. + multiIf( + _isJsonObj, _jsonSig, + substringUTF8( + replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(substringUTF8(_msgText, 1, 400), '\\x1b\\[[0-9;]*m', ''), '^((\\[Error\\]|[\\s\\S]*?DB::Exception:)[\\s\\S]*?)( [Ii]n scope | [Ww]hile processing )[\\s\\S]*', '\\1'), '(^|[ \\t\\r\\n\'"(`=])~/[^ \'"`:,)]*', '\\1PATH'), '(^|[ \\t\\r\\n\'"(`=])/(tmp|var|srv|private|opt|mnt|data|Volumes|System|app|work|run|Users/[^/ \\t\\r\\n\'"`:,)]+|home/[^/ \\t\\r\\n\'"`:,)]+)([/.][^ \\t\\r\\n\'"`:,)]*)?([ \\t\\r\\n\'"`:,)]|$)', '\\1PATH\\4'), '(^|[ \\t\\r\\n\'"(`=])/(tmp|var|srv|private|opt|mnt|data|Volumes|System|app|work|run|Users/[^/ \\t\\r\\n\'"`:,)]+|home/[^/ \\t\\r\\n\'"`:,)]+)([/.][^ \\t\\r\\n\'"`:,)]*)?([ \\t\\r\\n\'"`:,)]|$)', '\\1PATH\\4'), '\\b[a-z]{2,8}_[a-z]*[A-Z0-9][A-Za-z0-9]{8,}\\b', 'ID'), '\\b[0-9A-HJKMNP-TV-Z]{26}\\b', 'ID'), '\\b(HTTP|[Ss]tatus|returned|code|failed|failed with)(:? ?)([1-4][0-9][0-9]|500)\\b', '\\1\\2§\\3'), '\\b(HTTP|[Ss]tatus|returned|code|failed|failed with)(:? ?)5(0[1-9]|[1-9][0-9])\\b', '\\1\\2§5xx'), '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'EMAIL'), 'https?://[^/ )"]+', ''), '/(Users|home)/[^/ ]+', '/~'), '[?][A-Za-z0-9_]+=[^ )"]*', '?#'), '\'[^\' ]*/[^\' ]*\'|\'[^\' ]{25,}\'', '\'#\''), '"[^" ]*/[^" ]*"|"[^" ]{25,}"', '"#"'), '`[^` ]*/[^` ]*`|`[^` ]{25,}`', '`#`'), '(§[1-4][0-9][0-9]|§500|§5xx)|[0-9a-fA-F-]{6,}|[0-9]+', '\\1#'), '§([1-4][0-9][0-9]|500|5xx)#', '\\1'), + 1, 120 + ) + ) AS _msgSig, + -- Frame lines are matched by SHAPE, not by "contains :NUMBER". The old + -- rule accepted any line with a colon-digit, which let non-frame lines + -- in: Drizzle's `params: ` line, and the `Type: message` + -- header (`Code: 62`, `position 1628`, embedded timestamps). Row values + -- and message text then entered the hash and split one bug into + -- thousands of issues — 23,035 fingerprints for six real + -- AnomalyPersistenceError call sites, 15,051 for thirteen DatabaseError + -- ones. + -- + -- The pattern is rendered from FRAME_LINE_PATTERN in fingerprint.ts, + -- as is every redaction below. They used to be hand-copied here, which + -- let the reference implementation the tests exercise drift away from + -- the SQL that actually runs, silently. + arraySlice( + arrayFilter( + line -> match(line, '^[ \\t]*at |^[ \\t]*File "|^[ \\t]+from [^ ]+:[0-9]+|^[^ \\t@]+@[^ \\t]*:[0-9]+|^[ \\t]+[^ \\t]+\\.(go|rs):[0-9]+|^[0-9]+ +\\S.* +0x[0-9a-fA-F]+') + AND position(line, '(definition)') = 0 + AND NOT match(line, '^[ \\t]*at \\[') + AND NOT match(line, '^[ \\t]*at ([^(:]|:[^0-9])*:?$'), + splitByChar('\n', _exStack) + ), + -- A namespaced type plus its message names one failure: keep only the + -- throw site. With no message the frames are all that is left; keep 3. + 1, if(position(_exType, '/') > 0 AND _msgSig != '', 1, 3) + ) AS _rawFrames, + -- Redact every volatile token a frame line can carry: the URL origin + -- (so preview hosts share one fingerprint), Vite's 8-char bundle + -- content hash (so a deploy does not re-split every triaged browser and + -- Worker issue), then line numbers, hex pointers and long id runs. See + -- FRAME_REDACTIONS for the order and the reasoning. + arrayMap( + line -> replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(line, '@[0-9]+\\.[0-9]+\\.[0-9]+[^/ )]*', '@#'), '\\(index [0-9]+\\)', '(index #)'), 'file:///bundle/', ''), 'https?://[^/ )]+', ''), '(-[A-Za-z0-9_-]{8})+\\.js', '.js'), '(-[A-Za-z0-9_-]{8})+\\.css', '.css'), '(/_astro/[^/ ).]+)\\.[A-Za-z0-9_-]{8}\\.js', '\\1.js'), ':[0-9]+|line [0-9]+|0x[0-9a-fA-F]+|[0-9a-fA-F]{8,}|[0-9]{6,}', ''), + _rawFrames + ) AS _topFrames, + if(length(_topFrames) > 0, _topFrames[1], '') AS _topFrame, + arrayStringConcat(_topFrames, '\n') AS _fpFrames, + -- Display-only, best-effort human label (decoupled from the fingerprint: + -- many labels may map to one hash). The broad key list here is a DISPLAY + -- heuristic only; the fingerprint above makes no key-name assumption. + multiIf( + JSONExtractString(_msgText, 'title') != '', JSONExtractString(_msgText, 'title'), + JSONExtractString(_msgText, 'message') != '', JSONExtractString(_msgText, 'message'), + JSONExtractString(_msgText, 'error') != '', JSONExtractString(_msgText, 'error'), + JSONExtractString(_msgText, '_tag') != '', JSONExtractString(_msgText, '_tag'), + JSONExtractString(_msgText, 'reason') != '', JSONExtractString(_msgText, 'reason'), + JSONExtractString(_msgText, 'name') != '', JSONExtractString(_msgText, 'name'), + JSONExtractString(_msgText, 'type') != '', extract(JSONExtractString(_msgText, 'type'), '([^/]+)$'), + 'JSON error' + ) AS _jsonLabel, + multiIf( + _msgText = '', 'Unknown Error', + position(_msgText, '{ readonly') = 1 OR position(_msgText, '└─') > 0, + if( + extract(_msgText, 'readonly (\\w+)') != '', + concat('Schema parse error: ', extract(_msgText, 'readonly (\\w+)')), + 'Schema parse error' + ), + _isJsonObj OR position(_msgText, '[') = 1, _jsonLabel, + left(_msgText, multiIf( + position(_msgText, ': ') > 3, toInt64(position(_msgText, ': ')) - 1, + position(_msgText, ' (') > 3, toInt64(position(_msgText, ' (')) - 1, + position(_msgText, '\n') > 3, toInt64(position(_msgText, '\n')) - 1, + least(toInt64(length(_msgText)), 150) + )) + ) AS _statusLabel, + if(_exType != '', _exType, _statusLabel) AS _errorLabel, + -- Both semconv spellings; the current key wins when both are present. + toUInt16OrZero( + if( + SpanAttributes['http.response.status_code'] != '', + SpanAttributes['http.response.status_code'], + SpanAttributes['http.status_code'] + ) + ) AS _httpStatus + SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + SpanId, + ParentSpanId, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + _exType AS ExceptionType, + _exMsg AS ExceptionMessage, + _exStack AS ExceptionStacktrace, + _topFrame AS TopFrame, + cityHash64(OrgId, ServiceName, _exType, _fpFrames, _msgSig, 'fp3') AS FingerprintHash, + StatusMessage, + Duration, + _errorLabel AS ErrorLabel, + ResourceAttributes['service.version'] AS ServiceVersion + FROM traces + WHERE StatusCode = 'Error' + -- Client-side runtimes (notably the native Cloudflare Workers + -- observability) mark ANY non-2xx fetch span as Error, so 404s from bot + -- traffic arrived here as unlabelled "Unknown Error" issues. Drop a + -- span only when all hold: 4xx, no exception event, no exception.type + -- attribute, and no error.type beyond the status code itself (HTTP + -- semconv sets error.type to the bare status on a non-2xx response, + -- which carries no exception). 5xx and anything carrying a real + -- exception still count, and SpanKind is deliberately not consulted — + -- these are Client spans. + AND NOT ( + _httpStatus >= 400 AND _httpStatus < 500 + AND _ei = 0 + AND SpanAttributes['exception.type'] = '' + AND (SpanAttributes['error.type'] = '' OR SpanAttributes['error.type'] = toString(_httpStatus)) + ); + +CREATE MATERIALIZED VIEW IF NOT EXISTS error_events_mv TO error_events AS +WITH + arrayFirstIndex(n -> n = 'exception', EventsName) AS _ei, + -- Only fill the old Unknown Error bucket. Event values (including + -- empty fields) and spans with StatusMessage keep every hash input. + _ei = 0 AND StatusMessage = '' AS _useAttrs, + if( + _ei > 0, EventsAttributes[_ei]['exception.type'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.type'], ''), SpanAttributes['error.type']), '') + ) AS _exType, + if( + _ei > 0, EventsAttributes[_ei]['exception.message'], + if(_useAttrs, coalesce(nullIf(SpanAttributes['exception.message'], ''), SpanAttributes['error.message']), StatusMessage) + ) AS _exMsg, + if( + _ei > 0, EventsAttributes[_ei]['exception.stacktrace'], + if(_useAttrs, SpanAttributes['exception.stacktrace'], '') + ) AS _exStack, + if(_useAttrs, _exMsg, StatusMessage) AS _msgText, + -- JSON detection for the message signature below. + isValidJSON(_msgText) AS _isJson, + _isJson AND JSONType(_msgText) = 'Object' AS _isJsonObj, + -- General, KEY-NAME-AGNOSTIC canonical signature: iterate ALL top-level + -- keys, redact volatile tokens (long hex / numbers) in each raw value, then + -- sort by "key=value" so key order & whitespace don't matter. No assumption + -- about which keys exist — works for any producer's JSON shape. (Nested + -- objects are hashed as their raw substring; only top-level is canonicalized.) + arrayStringConcat( + arraySort( + arrayMap( + kv -> concat(kv.1, '=', replaceRegexpAll(kv.2, '[0-9a-fA-F-]{6,}|[0-9]+', '#')), + JSONExtractKeysAndValuesRaw(_msgText) + ) + ), + '|' + ) AS _jsonSig, + -- The message signature is folded in ALWAYS, not only when there are no + -- frames. Bundled runtimes minify every module into one file, so the top + -- three frames of a Worker error are `toDatabaseError (worker.js)` for + -- every failing query alike: on frames alone, 25 distinct DatabaseError + -- bugs (316k occurrences) collapse into a single issue. The signature + -- restores that discrimination, and it cannot reinflate cardinality the + -- way a raw prefix would because everything variable is redacted first: + -- emails, URL origins, home directories, query strings, quoted values, + -- then ids and every digit run. See ERROR_MSG_REDACTIONS for the order, + -- what is deliberately kept, and the one residual it cannot reach. + multiIf( + _isJsonObj, _jsonSig, + substringUTF8( + replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(substringUTF8(_msgText, 1, 400), '\\x1b\\[[0-9;]*m', ''), '^((\\[Error\\]|[\\s\\S]*?DB::Exception:)[\\s\\S]*?)( [Ii]n scope | [Ww]hile processing )[\\s\\S]*', '\\1'), '(^|[ \\t\\r\\n\'"(`=])~/[^ \'"`:,)]*', '\\1PATH'), '(^|[ \\t\\r\\n\'"(`=])/(tmp|var|srv|private|opt|mnt|data|Volumes|System|app|work|run|Users/[^/ \\t\\r\\n\'"`:,)]+|home/[^/ \\t\\r\\n\'"`:,)]+)([/.][^ \\t\\r\\n\'"`:,)]*)?([ \\t\\r\\n\'"`:,)]|$)', '\\1PATH\\4'), '(^|[ \\t\\r\\n\'"(`=])/(tmp|var|srv|private|opt|mnt|data|Volumes|System|app|work|run|Users/[^/ \\t\\r\\n\'"`:,)]+|home/[^/ \\t\\r\\n\'"`:,)]+)([/.][^ \\t\\r\\n\'"`:,)]*)?([ \\t\\r\\n\'"`:,)]|$)', '\\1PATH\\4'), '\\b[a-z]{2,8}_[a-z]*[A-Z0-9][A-Za-z0-9]{8,}\\b', 'ID'), '\\b[0-9A-HJKMNP-TV-Z]{26}\\b', 'ID'), '\\b(HTTP|[Ss]tatus|returned|code|failed|failed with)(:? ?)([1-4][0-9][0-9]|500)\\b', '\\1\\2§\\3'), '\\b(HTTP|[Ss]tatus|returned|code|failed|failed with)(:? ?)5(0[1-9]|[1-9][0-9])\\b', '\\1\\2§5xx'), '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'EMAIL'), 'https?://[^/ )"]+', ''), '/(Users|home)/[^/ ]+', '/~'), '[?][A-Za-z0-9_]+=[^ )"]*', '?#'), '\'[^\' ]*/[^\' ]*\'|\'[^\' ]{25,}\'', '\'#\''), '"[^" ]*/[^" ]*"|"[^" ]{25,}"', '"#"'), '`[^` ]*/[^` ]*`|`[^` ]{25,}`', '`#`'), '(§[1-4][0-9][0-9]|§500|§5xx)|[0-9a-fA-F-]{6,}|[0-9]+', '\\1#'), '§([1-4][0-9][0-9]|500|5xx)#', '\\1'), + 1, 120 + ) + ) AS _msgSig, + -- Frame lines are matched by SHAPE, not by "contains :NUMBER". The old + -- rule accepted any line with a colon-digit, which let non-frame lines + -- in: Drizzle's `params: ` line, and the `Type: message` + -- header (`Code: 62`, `position 1628`, embedded timestamps). Row values + -- and message text then entered the hash and split one bug into + -- thousands of issues — 23,035 fingerprints for six real + -- AnomalyPersistenceError call sites, 15,051 for thirteen DatabaseError + -- ones. + -- + -- The pattern is rendered from FRAME_LINE_PATTERN in fingerprint.ts, + -- as is every redaction below. They used to be hand-copied here, which + -- let the reference implementation the tests exercise drift away from + -- the SQL that actually runs, silently. + arraySlice( + arrayFilter( + line -> match(line, '^[ \\t]*at |^[ \\t]*File "|^[ \\t]+from [^ ]+:[0-9]+|^[^ \\t@]+@[^ \\t]*:[0-9]+|^[ \\t]+[^ \\t]+\\.(go|rs):[0-9]+|^[0-9]+ +\\S.* +0x[0-9a-fA-F]+') + AND position(line, '(definition)') = 0 + AND NOT match(line, '^[ \\t]*at \\[') + AND NOT match(line, '^[ \\t]*at ([^(:]|:[^0-9])*:?$'), + splitByChar('\n', _exStack) + ), + -- A namespaced type plus its message names one failure: keep only the + -- throw site. With no message the frames are all that is left; keep 3. + 1, if(position(_exType, '/') > 0 AND _msgSig != '', 1, 3) + ) AS _rawFrames, + -- Redact every volatile token a frame line can carry: the URL origin + -- (so preview hosts share one fingerprint), Vite's 8-char bundle + -- content hash (so a deploy does not re-split every triaged browser and + -- Worker issue), then line numbers, hex pointers and long id runs. See + -- FRAME_REDACTIONS for the order and the reasoning. + arrayMap( + line -> replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(line, '@[0-9]+\\.[0-9]+\\.[0-9]+[^/ )]*', '@#'), '\\(index [0-9]+\\)', '(index #)'), 'file:///bundle/', ''), 'https?://[^/ )]+', ''), '(-[A-Za-z0-9_-]{8})+\\.js', '.js'), '(-[A-Za-z0-9_-]{8})+\\.css', '.css'), '(/_astro/[^/ ).]+)\\.[A-Za-z0-9_-]{8}\\.js', '\\1.js'), ':[0-9]+|line [0-9]+|0x[0-9a-fA-F]+|[0-9a-fA-F]{8,}|[0-9]{6,}', ''), + _rawFrames + ) AS _topFrames, + if(length(_topFrames) > 0, _topFrames[1], '') AS _topFrame, + arrayStringConcat(_topFrames, '\n') AS _fpFrames, + -- Display-only, best-effort human label (decoupled from the fingerprint: + -- many labels may map to one hash). The broad key list here is a DISPLAY + -- heuristic only; the fingerprint above makes no key-name assumption. + multiIf( + JSONExtractString(_msgText, 'title') != '', JSONExtractString(_msgText, 'title'), + JSONExtractString(_msgText, 'message') != '', JSONExtractString(_msgText, 'message'), + JSONExtractString(_msgText, 'error') != '', JSONExtractString(_msgText, 'error'), + JSONExtractString(_msgText, '_tag') != '', JSONExtractString(_msgText, '_tag'), + JSONExtractString(_msgText, 'reason') != '', JSONExtractString(_msgText, 'reason'), + JSONExtractString(_msgText, 'name') != '', JSONExtractString(_msgText, 'name'), + JSONExtractString(_msgText, 'type') != '', extract(JSONExtractString(_msgText, 'type'), '([^/]+)$'), + 'JSON error' + ) AS _jsonLabel, + multiIf( + _msgText = '', 'Unknown Error', + position(_msgText, '{ readonly') = 1 OR position(_msgText, '└─') > 0, + if( + extract(_msgText, 'readonly (\\w+)') != '', + concat('Schema parse error: ', extract(_msgText, 'readonly (\\w+)')), + 'Schema parse error' + ), + _isJsonObj OR position(_msgText, '[') = 1, _jsonLabel, + left(_msgText, multiIf( + position(_msgText, ': ') > 3, toInt64(position(_msgText, ': ')) - 1, + position(_msgText, ' (') > 3, toInt64(position(_msgText, ' (')) - 1, + position(_msgText, '\n') > 3, toInt64(position(_msgText, '\n')) - 1, + least(toInt64(length(_msgText)), 150) + )) + ) AS _statusLabel, + if(_exType != '', _exType, _statusLabel) AS _errorLabel, + -- Both semconv spellings; the current key wins when both are present. + toUInt16OrZero( + if( + SpanAttributes['http.response.status_code'] != '', + SpanAttributes['http.response.status_code'], + SpanAttributes['http.status_code'] + ) + ) AS _httpStatus + SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + SpanId, + ParentSpanId, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + _exType AS ExceptionType, + _exMsg AS ExceptionMessage, + _exStack AS ExceptionStacktrace, + _topFrame AS TopFrame, + cityHash64(OrgId, ServiceName, _exType, _fpFrames, _msgSig, 'fp3') AS FingerprintHash, + StatusMessage, + Duration, + _errorLabel AS ErrorLabel, + ResourceAttributes['service.version'] AS ServiceVersion + FROM traces + WHERE StatusCode = 'Error' + -- Client-side runtimes (notably the native Cloudflare Workers + -- observability) mark ANY non-2xx fetch span as Error, so 404s from bot + -- traffic arrived here as unlabelled "Unknown Error" issues. Drop a + -- span only when all hold: 4xx, no exception event, no exception.type + -- attribute, and no error.type beyond the status code itself (HTTP + -- semconv sets error.type to the bare status on a non-2xx response, + -- which carries no exception). 5xx and anything carrying a real + -- exception still count, and SpanKind is deliberately not consulted — + -- these are Client spans. + AND NOT ( + _httpStatus >= 400 AND _httpStatus < 500 + AND _ei = 0 + AND SpanAttributes['exception.type'] = '' + AND (SpanAttributes['error.type'] = '' OR SpanAttributes['error.type'] = toString(_httpStatus)) + ); + +CREATE MATERIALIZED VIEW IF NOT EXISTS error_fingerprints_minutely_mv TO error_fingerprints_minutely AS +SELECT + OrgId, + toStartOfMinute(Timestamp) AS Minute, + FingerprintHash, + anyLast(ServiceName) AS ServiceName, + anyLast(ExceptionType) AS ExceptionType, + anyLast(ExceptionMessage) AS ExceptionMessage, + anyLast(ErrorLabel) AS ErrorLabel, + anyLast(TopFrame) AS TopFrame, + count() AS OccurrenceCount, + min(Timestamp) AS FirstSeen, + max(Timestamp) AS LastSeen, + -- Distinct builds, not a sample: see ServiceVersions on the datasource. + groupUniqArray(ServiceVersion) AS ServiceVersions + FROM error_events + GROUP BY OrgId, Minute, FingerprintHash; + +CREATE MATERIALIZED VIEW IF NOT EXISTS identity_links_mv TO identity_links AS +SELECT + OrgId, + VisitorId, + UserId, + StartTime AS FirstSeen + FROM session_replays + WHERE VisitorId != '' AND UserId != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS log_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + arrayJoin(mapKeys(LogAttributes)) AS AttributeKey, + 'log' AS AttributeScope, + count() AS UsageCount + FROM logs + WHERE LogAttributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS log_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + AttributeKey, + AttributeValue, + 'log' AS AttributeScope, + count() AS UsageCount + FROM logs + ARRAY JOIN + mapKeys(LogAttributes) AS AttributeKey, + mapValues(LogAttributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS logs_aggregates_hourly_mv TO logs_aggregates_hourly AS +SELECT + OrgId, + toStartOfHour(TimestampTime) AS Hour, + ServiceName, + SeverityText, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + count() AS Count, + sum(length(Body) + 200) AS SizeBytes, + ResourceAttributes['service.namespace'] AS ServiceNamespace + FROM logs + GROUP BY OrgId, Hour, ServiceName, SeverityText, DeploymentEnv, ServiceNamespace; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + arrayJoin(mapKeys(Attributes)) AS AttributeKey, + 'metric' AS AttributeScope, + count() AS UsageCount + FROM metrics_sum + WHERE Attributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + AttributeKey, + AttributeValue, + 'metric' AS AttributeScope, + count() AS UsageCount + FROM metrics_sum + ARRAY JOIN + mapKeys(Attributes) AS AttributeKey, + mapValues(Attributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_exp_histogram_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'exponential_histogram' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + toUInt8(0) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_exponential_histogram + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_gauge_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'gauge' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + toUInt8(0) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_gauge + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_histogram_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'histogram' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + toUInt8(0) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_histogram + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS metric_catalog_sum_mv TO metric_catalog AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 'sum' AS MetricType, + ServiceName, + MetricName, + anyLast(MetricDescription) AS MetricDescription, + anyLast(MetricUnit) AS MetricUnit, + anyLast(toUInt8(IsMonotonic)) AS IsMonotonic, + count() AS DataPointCount, + min(toDateTime(TimeUnix)) AS FirstSeen, + max(toDateTime(TimeUnix)) AS LastSeen + FROM metrics_sum + GROUP BY OrgId, Hour, MetricType, ServiceName, MetricName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS product_events_mv TO product_events AS +SELECT + OrgId, + Timestamp, + 'browser' AS Source, + SessionId, + Seq, + VisitorId, + UserId, + GroupId, + Type AS Kind, + if(Type = 'navigation', '$pageview', Message) AS EventName, + domain(Url) AS Host, + path(Url) AS PagePath, + Url, + '' AS ServiceName, + Attributes, + '' AS TraceId, + '' AS SpanId + FROM session_events + WHERE Type IN ('navigation', 'custom'); + +CREATE MATERIALIZED VIEW IF NOT EXISTS product_events_traces_mv TO product_events AS +SELECT + OrgId, + Timestamp, + 'trace' AS Source, + SpanAttributes['session.id'] AS SessionId, + 0 AS Seq, + SpanAttributes['maple.product_event.visitor_id'] AS VisitorId, + SpanAttributes['maple.product_event.user_id'] AS UserId, + SpanAttributes['maple.product_event.group_id'] AS GroupId, + 'custom' AS Kind, + SpanAttributes['maple.product_event.name'] AS EventName, + domain(SpanAttributes['maple.product_event.url']) AS Host, + path(SpanAttributes['maple.product_event.url']) AS PagePath, + SpanAttributes['maple.product_event.url'] AS Url, + ServiceName, + mapUpdate( + CAST( + mapFilter( + (k, v) -> NOT startsWith(k, 'maple.product_event.') + AND ( + NOT has(mapKeys(SpanAttributes), 'maple.product_event.include') + OR has( + arrayMap( + key -> trimBoth(key), + splitByChar(',', SpanAttributes['maple.product_event.include']) + ), + k + ) + ), + SpanAttributes + ), + 'Map(String, String)' + ), + mapApply( + (k, v) -> (substring(k, 26), v), + mapFilter((k, v) -> startsWith(k, 'maple.product_event.prop.'), SpanAttributes) + ) + ) AS Attributes, + TraceId, + SpanId + FROM traces + WHERE SpanAttributes['maple.product_event.name'] != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_external_edges_hourly_mv TO service_external_edges_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + multiIf( + coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' OR SpanAttributes['messaging.system'] != '', 'messaging', + SpanAttributes['rpc.service'] != '' OR SpanAttributes['rpc.system'] != '', 'rpc', + 'http' + ) AS TargetType, + multiIf( + coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' OR SpanAttributes['messaging.system'] != '', SpanAttributes['messaging.system'], + SpanAttributes['rpc.service'] != '' OR SpanAttributes['rpc.system'] != '', SpanAttributes['rpc.system'], + '' + ) AS TargetSystem, + multiIf( + coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' OR SpanAttributes['messaging.system'] != '', + if(coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '', coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']), SpanAttributes['messaging.system']), + SpanAttributes['rpc.service'] != '' OR SpanAttributes['rpc.system'] != '', + if(SpanAttributes['rpc.service'] != '', SpanAttributes['rpc.service'], SpanAttributes['rpc.system']), + if(SpanAttributes['server.address'] != '', + SpanAttributes['server.address'], + if(SpanAttributes['http.host'] != '', + SpanAttributes['http.host'], + SpanAttributes['url.authority'])) + ) AS TargetName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + count() AS CallCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sum(Duration / 1000000) AS DurationSumMs, + max(Duration / 1000000) AS MaxDurationMs, + sum(SampleRate) AS SampleRateSum, + quantilesTDigestWeightedState(0.5, 0.95)(Duration, toUInt32(greatest(SampleRate, 1.0))) AS DurationQuantiles + FROM traces + WHERE SpanKind IN ('Client', 'Producer') + AND coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) = '' + AND ServiceName != '' + AND ( + SpanAttributes['server.address'] != '' + OR SpanAttributes['http.host'] != '' + OR SpanAttributes['url.authority'] != '' + OR coalesce(nullIf(SpanAttributes['messaging.destination.name'], ''), SpanAttributes['messaging.destination']) != '' + OR SpanAttributes['messaging.system'] != '' + OR SpanAttributes['rpc.service'] != '' + OR SpanAttributes['rpc.system'] != '' + ) + GROUP BY OrgId, Hour, ServiceName, TargetType, TargetSystem, TargetName, DeploymentEnv + HAVING TargetName != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_children_mv TO service_map_children AS +SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + ParentSpanId, + ServiceName, + SpanKind, + Duration, + StatusCode, + TraceState, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') + AND ParentSpanId != ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_db_edges_hourly_mv TO service_map_db_edges_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) AS DbSystem, + if(match(coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name']), '^([0-9a-fA-F]{32}|.*[.]hyperdrive[.]local)$'), 'hyperdrive', coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name'])) AS DbNamespace, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + count() AS CallCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sum(Duration / 1000000) AS DurationSumMs, + max(Duration / 1000000) AS MaxDurationMs, + countIf(TraceState LIKE '%th:%') AS SampledSpanCount, + countIf(TraceState = '' OR TraceState NOT LIKE '%th:%') AS UnsampledSpanCount, + sum(SampleRate) AS SampleRateSum, + quantilesTDigestWeightedState(0.5, 0.95)(Duration, toUInt32(greatest(SampleRate, 1.0))) AS DurationQuantiles + FROM traces + WHERE SpanKind IN ('Client', 'Producer') + AND coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) != '' + AND ServiceName != '' + GROUP BY OrgId, Hour, ServiceName, DbSystem, DbNamespace, DeploymentEnv; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_db_query_shapes_hourly_mv TO service_map_db_query_shapes_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) AS DbSystem, + if(match(coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name']), '^([0-9a-fA-F]{32}|.*[.]hyperdrive[.]local)$'), 'hyperdrive', coalesce(nullIf(SpanAttributes['db.namespace'], ''), nullIf(SpanAttributes['db.name'], ''), nullIf(SpanAttributes['server.address'], ''), SpanAttributes['net.peer.name'])) AS DbNamespace, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + coalesce( + nullIf(SpanAttributes['db.query.fingerprint'], ''), + nullIf(SpanAttributes['db.statement.fingerprint'], ''), + nullIf(if(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']) != '', toString(cityHash64(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(replaceRegexpAll(lower(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement'])), '\'[^\']*\'', '?'), '\\bin\\s*\\([^)]*\\)', 'in (?)'), '[0-9]+(\\.[0-9]+)?', '?'), '\\s+', ' '), '^\\s+|\\s+$', ''))), ''), ''), + toString(cityHash64(coalesce( + nullIf(SpanAttributes['db.query.summary'], ''), + nullIf(if(SpanAttributes['db.operation.name'] != '', trimBoth(concat(SpanAttributes['db.operation.name'], if(coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace']) != '', concat(' ', coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace'])), ''))), ''), ''), + nullIf(if(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']) != '', trimBoth(concat(upper(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '^\\s*(\\w+)')), if(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)') != '', concat(' ', extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)')), ''))), ''), ''), + nullIf(SpanAttributes['query.context'], ''), + nullIf(SpanAttributes['db.operation.name'], ''), + nullIf(SpanAttributes['db.operation'], ''), + SpanName +))) +) AS QueryKey, + any(substring(coalesce( + nullIf(SpanAttributes['db.query.summary'], ''), + nullIf(if(SpanAttributes['db.operation.name'] != '', trimBoth(concat(SpanAttributes['db.operation.name'], if(coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace']) != '', concat(' ', coalesce(nullIf(SpanAttributes['db.collection.name'], ''), SpanAttributes['db.namespace'])), ''))), ''), ''), + nullIf(if(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']) != '', trimBoth(concat(upper(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '^\\s*(\\w+)')), if(extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)') != '', concat(' ', extract(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), '(?i)(?:from|into|update|join|table)\\s+\\W?([\\w.]+)')), ''))), ''), ''), + nullIf(SpanAttributes['query.context'], ''), + nullIf(SpanAttributes['db.operation.name'], ''), + nullIf(SpanAttributes['db.operation'], ''), + SpanName +), 1, 220)) AS QueryLabel, + any(substring(coalesce(nullIf(SpanAttributes['db.query.text'], ''), SpanAttributes['db.statement']), 1, 1000)) AS SampleStatement, + count() AS CallCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sum(SampleRate) AS EstimatedCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration) * SampleRate / 1000000) AS WeightedDurationSumMs, + quantilesTDigestWeightedState(0.5, 0.95)(Duration, toUInt32(greatest(SampleRate, 1.0))) AS DurationQuantiles + FROM traces + WHERE SpanKind IN ('Client', 'Producer') + AND coalesce(nullIf(SpanAttributes['db.system.name'], ''), SpanAttributes['db.system']) != '' + AND ServiceName != '' + GROUP BY OrgId, Hour, ServiceName, DbSystem, DbNamespace, DeploymentEnv, QueryKey; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_edges_hourly_ingest_mv TO service_map_edges_hourly AS +SELECT + OrgId, + Hour, + SourceService, + TargetService, + DeploymentEnv, + CallCount, + ErrorCount, + DurationSumMs, + MaxDurationMs, + SampledSpanCount, + UnsampledSpanCount, + SampleRateSum + FROM service_map_edges_hourly_ingest; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_map_spans_mv TO service_map_spans AS +SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + TraceId, + SpanId, + ParentSpanId, + ServiceName, + SpanKind, + Duration, + StatusCode, + TraceState, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv + FROM traces + WHERE SpanKind IN ('Client', 'Producer', 'Server', 'Consumer'); + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_operations_hourly_mv TO service_operations_hourly AS +SELECT + OrgId, + toStartOfHour(Minute) AS Hour, + ServiceName, + DeploymentEnv, + SpanName, + sum(SpanCount) AS SpanCount, + sum(EstimatedSpanCount) AS EstimatedSpanCount, + sum(ErrorCount) AS ErrorCount, + sum(EstimatedErrorCount) AS EstimatedErrorCount, + sum(DurationSum) AS DurationSum, + quantilesTDigestMergeState(0.5, 0.95)(DurationQuantiles) AS DurationQuantiles, + sum(ClassifiedSpanCount) AS ClassifiedSpanCount, + sum(ServerSpanCount) AS ServerSpanCount, + sum(RoutedSpanCount) AS RoutedSpanCount + FROM service_operations_minutely + GROUP BY OrgId, Hour, ServiceName, DeploymentEnv, SpanName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_operations_minutely_mv TO service_operations_minutely AS +SELECT + OrgId, + toStartOfMinute(toDateTime(Timestamp)) AS Minute, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + if(((SpanName LIKE 'http.server %' OR SpanName IN ('GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS')) AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != '')), concat(if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), ' ', if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path'])), SpanName) AS SpanName, + count() AS SpanCount, + sum(SampleRate) AS EstimatedSpanCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration)) AS DurationSum, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles, + count() AS ClassifiedSpanCount, + countIf(SpanKind IN ('Server', 'Consumer')) AS ServerSpanCount, + countIf(SpanAttributes['http.route'] != '') AS RoutedSpanCount + FROM traces + GROUP BY OrgId, Minute, ServiceName, DeploymentEnv, SpanName; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_overview_hourly_mv TO service_overview_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + ResourceAttributes['service.namespace'] AS ServiceNamespace, + ResourceAttributes['vcs.ref.head.revision'] AS CommitSha, + count() AS SpanCount, + sum(SampleRate) AS EstimatedSpanCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration)) AS DurationSum, + quantilesTDigestState(0.5, 0.95, 0.99)(Duration) AS DurationQuantiles, + min(toDateTime(Timestamp)) AS FirstSeen, + countIf(StatusCode != 'Error' AND Duration < 500000000) AS ApdexSatisfiedCount, + countIf(StatusCode != 'Error' AND Duration >= 500000000 AND Duration < 2000000000) AS ApdexToleratingCount + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '' + GROUP BY OrgId, Hour, ServiceName, DeploymentEnv, ServiceNamespace, CommitSha; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_overview_minutely_mv TO service_overview_minutely AS +SELECT + OrgId, + toStartOfMinute(toDateTime(Timestamp)) AS Minute, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + ResourceAttributes['service.namespace'] AS ServiceNamespace, + ResourceAttributes['vcs.ref.head.revision'] AS CommitSha, + count() AS SpanCount, + sum(SampleRate) AS EstimatedSpanCount, + countIf(StatusCode = 'Error') AS ErrorCount, + sumIf(SampleRate, StatusCode = 'Error') AS EstimatedErrorCount, + sum(toFloat64(Duration)) AS DurationSum, + quantilesTDigestState(0.5, 0.95, 0.99)(Duration) AS DurationQuantiles, + min(toDateTime(Timestamp)) AS FirstSeen, + countIf(StatusCode != 'Error' AND Duration < 500000000) AS ApdexSatisfiedCount, + countIf(StatusCode != 'Error' AND Duration >= 500000000 AND Duration < 2000000000) AS ApdexToleratingCount + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') OR ParentSpanId = '' + GROUP BY OrgId, Minute, ServiceName, DeploymentEnv, ServiceNamespace, CommitSha; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_overview_spans_mv TO service_overview_spans AS +SELECT + OrgId, + toDateTime(Timestamp) AS Timestamp, + ServiceName, + Duration, + StatusCode, + TraceState, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + ResourceAttributes['vcs.ref.head.revision'] AS CommitSha, + SampleRate, + ResourceAttributes['service.namespace'] AS ServiceNamespace, + SpanKind, + toUInt8(ParentSpanId = '') AS IsRoot + FROM traces + WHERE SpanKind IN ('Server', 'Consumer') OR ParentSpanId = ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_platforms_hourly_mv TO service_platforms_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + max(ResourceAttributes['k8s.cluster.name']) AS K8sCluster, + max(ResourceAttributes['k8s.pod.name']) AS K8sPodName, + max(ResourceAttributes['k8s.deployment.name']) AS K8sDeploymentName, + max(ResourceAttributes['k8s.statefulset.name']) AS K8sStatefulSetName, + max(ResourceAttributes['k8s.daemonset.name']) AS K8sDaemonSetName, + max(ResourceAttributes['k8s.namespace.name']) AS K8sNamespaceName, + max(ResourceAttributes['cloud.platform']) AS CloudPlatform, + max(ResourceAttributes['cloud.provider']) AS CloudProvider, + max(ResourceAttributes['faas.name']) AS FaasName, + max(ResourceAttributes['maple.sdk.type']) AS MapleSdkType, + max(ResourceAttributes['process.runtime.name']) AS ProcessRuntimeName, + count() AS SpanCount + FROM traces + WHERE ServiceName != '' + GROUP BY OrgId, Hour, ServiceName, DeploymentEnv; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_logs_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(TimestampTime) AS Hour, + count() AS LogCount, + sum(length(Body) + 200) AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM logs + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_exp_histogram_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + count() AS ExpHistogramMetricCount, + count() * 300 AS ExpHistogramMetricSizeBytes + FROM metrics_exponential_histogram + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_gauge_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + count() AS GaugeMetricCount, + count() * 150 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM metrics_gauge + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_histogram_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + count() AS HistogramMetricCount, + count() * 250 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM metrics_histogram + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_metrics_sum_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + 0 AS TraceCount, + 0 AS TraceSizeBytes, + count() AS SumMetricCount, + count() * 150 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM metrics_sum + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS service_usage_traces_mv TO service_usage AS +SELECT + OrgId, + ServiceName, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + 0 AS LogCount, + 0 AS LogSizeBytes, + count() AS TraceCount, + sum(length(SpanName) + 300) AS TraceSizeBytes, + 0 AS SumMetricCount, + 0 AS SumMetricSizeBytes, + 0 AS GaugeMetricCount, + 0 AS GaugeMetricSizeBytes, + 0 AS HistogramMetricCount, + 0 AS HistogramMetricSizeBytes, + 0 AS ExpHistogramMetricCount, + 0 AS ExpHistogramMetricSizeBytes + FROM traces + GROUP BY OrgId, ServiceName, Hour; + +CREATE MATERIALIZED VIEW IF NOT EXISTS span_metrics_calls_hourly_mv TO span_metrics_calls_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(TimeUnix)) AS Hour, + ServiceName, + MetricName, + Attributes['span.kind'] AS SpanKind, + cityHash64(mapKeys(Attributes), mapValues(Attributes)) AS AttrFingerprint, + cityHash64(mapKeys(ResourceAttributes), mapValues(ResourceAttributes)) AS ResourceFingerprint, + StartTimeUnix, + argMaxState(Value, TimeUnix) AS LastValue + FROM metrics_sum + -- 'traces.span.metrics.calls' is the name the collector actually emits: + -- spanmetricsconnector output is namespaced by the pipeline it is attached + -- to. Without it this MV matched nothing and the target sat at 0 rows since + -- it was created, while ~880k rows / 2 days of the real counter flowed past + -- into metrics_sum and every read fell back to the raw window-function scan + -- (~7s p95 -- see queries/metrics.ts). Keep this list in sync with + -- SPAN_METRICS_CALLS_NAMES on the read side. + WHERE MetricName IN ('span.metrics.calls', 'calls', 'traces.span.metrics.calls') AND IsMonotonic + GROUP BY OrgId, Hour, ServiceName, MetricName, SpanKind, AttrFingerprint, ResourceFingerprint, StartTimeUnix; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_detail_spans_mv TO trace_detail_spans AS +SELECT + OrgId, + Timestamp, + TraceId, + SpanId, + ParentSpanId, + SpanName, + SpanKind, + ServiceName, + Duration, + StatusCode, + StatusMessage, + SpanAttributes, + ResourceAttributes + FROM traces; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_facets_hourly_mv TO trace_facets_hourly AS +SELECT + OrgId, + toStartOfHour(Timestamp) AS Hour, + ServiceName, + SpanName, + HttpMethod, + HttpStatusCode, + DeploymentEnv, + ServiceNamespace, + HasError, + count() AS TraceCount, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax, + quantilesTDigestState(0.5, 0.95)(Duration) AS DurationQuantiles + FROM trace_list_mv + GROUP BY OrgId, Hour, ServiceName, SpanName, HttpMethod, HttpStatusCode, DeploymentEnv, ServiceNamespace, HasError; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_list_mv_mv TO trace_list_mv AS +SELECT + OrgId, + TraceId, + toDateTime(Timestamp) AS Timestamp, + ServiceName, + if( + (SpanName LIKE 'http.server %' OR SpanName IN ('GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS')) + AND (SpanAttributes['http.route'] != '' OR SpanAttributes['url.path'] != ''), + concat( + if(SpanName LIKE 'http.server %', replaceOne(SpanName, 'http.server ', ''), SpanName), + ' ', + if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], SpanAttributes['url.path']) + ), + SpanName + ) AS SpanName, + SpanKind, + Duration, + StatusCode, + if(SpanAttributes['http.method'] != '', SpanAttributes['http.method'], SpanAttributes['http.request.method']) AS HttpMethod, + if(SpanAttributes['http.route'] != '', SpanAttributes['http.route'], if(SpanAttributes['url.path'] != '', SpanAttributes['url.path'], SpanAttributes['http.target'])) AS HttpRoute, + if(SpanAttributes['http.status_code'] != '', SpanAttributes['http.status_code'], SpanAttributes['http.response.status_code']) AS HttpStatusCode, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + toUInt8( + StatusCode = 'Error' + OR (SpanAttributes['http.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.status_code']) >= 500) + OR (SpanAttributes['http.response.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.response.status_code']) >= 500) + ) AS HasError, + TraceState, + ResourceAttributes['service.namespace'] AS ServiceNamespace + FROM traces + WHERE ParentSpanId = ''; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_resource_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + arrayJoin(mapKeys(ResourceAttributes)) AS AttributeKey, + 'resource' AS AttributeScope, + count() AS UsageCount + FROM traces + WHERE ResourceAttributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_resource_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + AttributeKey, + AttributeValue, + 'resource' AS AttributeScope, + count() AS UsageCount + FROM traces + ARRAY JOIN + mapKeys(ResourceAttributes) AS AttributeKey, + mapValues(ResourceAttributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_span_attribute_keys_mv TO attribute_keys_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + arrayJoin(mapKeys(SpanAttributes)) AS AttributeKey, + 'span' AS AttributeScope, + count() AS UsageCount + FROM traces + WHERE SpanAttributes != map() + GROUP BY OrgId, Hour, AttributeKey, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS trace_span_attribute_values_mv TO attribute_values_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + AttributeKey, + AttributeValue, + 'span' AS AttributeScope, + count() AS UsageCount + FROM traces + ARRAY JOIN + mapKeys(SpanAttributes) AS AttributeKey, + mapValues(SpanAttributes) AS AttributeValue + WHERE AttributeValue != '' + AND length(AttributeValue) <= 128 + AND NOT (length(AttributeValue) > 4 AND match(AttributeValue, '^[0-9]+([.][0-9]+)?$')) + AND NOT match(AttributeKey, '(_id|[.]id|Id|_ns)$') + AND AttributeKey NOT LIKE 'http.request.header.%' + AND AttributeKey NOT LIKE 'http.response.header.%' + GROUP BY OrgId, Hour, AttributeKey, AttributeValue, AttributeScope; + +CREATE MATERIALIZED VIEW IF NOT EXISTS traces_aggregates_hourly_mv TO traces_aggregates_hourly AS +SELECT + OrgId, + toStartOfHour(toDateTime(Timestamp)) AS Hour, + ServiceName, + SpanName, + SpanKind, + StatusCode, + IsEntryPoint, + coalesce(nullIf(ResourceAttributes['deployment.environment.name'], ''), ResourceAttributes['deployment.environment']) AS DeploymentEnv, + sum(SampleRate) AS WeightedCount, + sum(toFloat64(Duration) * SampleRate) AS WeightedDurationSum, + sumIf(SampleRate, StatusCode = 'Error') AS WeightedErrorCount, + quantilesTDigestWeightedState(0.5, 0.95, 0.99)(Duration, toUInt32(SampleRate)) AS DurationQuantiles, + min(Duration) AS DurationMin, + max(Duration) AS DurationMax + FROM traces + GROUP BY OrgId, Hour, ServiceName, SpanName, SpanKind, StatusCode, IsEntryPoint, DeploymentEnv; diff --git a/apps/cli/src/server/schema/local-schema.sql b/apps/cli/src/server/schema/local-schema.sql index 8440967639..cfa3889a39 100644 --- a/apps/cli/src/server/schema/local-schema.sql +++ b/apps/cli/src/server/schema/local-schema.sql @@ -1,7 +1,7 @@ -- This file is generated by scripts/generate-clickhouse-schema-sql.ts -- Do not edit manually. Run `bun run clickhouse:schema` to regenerate. --- projectRevision: 64b7ae1279269efadefe2cb5773eaf28967c16da7af3d0659c297770532bbc97 --- localSchemaVersion: 31 +-- projectRevision: 9c4759bea56d9544000d0326a35b6224fa56ec76c373ce34bf16869430700c0a +-- localSchemaVersion: 32 CREATE TABLE IF NOT EXISTS ai_crawler_requests ( OrgId LowCardinality(String), @@ -76,7 +76,8 @@ CREATE TABLE IF NOT EXISTS alert_checks ( EvaluationDurationMs UInt32, ErrorMessage Nullable(String), ErrorCategory LowCardinality(String), - SkipReason LowCardinality(String) DEFAULT '' + SkipReason LowCardinality(String) DEFAULT '', + Level LowCardinality(String) DEFAULT '' ) ENGINE = MergeTree PARTITION BY toDate(Timestamp) diff --git a/apps/cli/test/local-store-migrations.test.ts b/apps/cli/test/local-store-migrations.test.ts index 8c07a85d51..bc2d73ae72 100644 --- a/apps/cli/test/local-store-migrations.test.ts +++ b/apps/cli/test/local-store-migrations.test.ts @@ -35,6 +35,7 @@ import { LOCAL_SCHEMA_V29, LOCAL_SCHEMA_V30, LOCAL_SCHEMA_V31, + LOCAL_SCHEMA_V32, SCHEMA_DIGEST, SCHEMA_FINGERPRINT, } from "../src/server/schema-identity" @@ -99,11 +100,11 @@ const [v10ToV11ProductEventsModule] = localStoreMigrations.filter( ) describe("current local schema identity", () => { - it("matches the generated v31 revision and keeps the issue-297 identity frozen", () => { + it("matches the generated v32 revision and keeps the issue-297 identity frozen", () => { // oxlint-disable-next-line test-slop/no-constant-pin -- rewritten by local-schema:bump; catches an unbumped schema edit - expect(SCHEMA_FINGERPRINT).toBe("ea8d1656ca6f4b4f") + expect(SCHEMA_FINGERPRINT).toBe("7bc1c04157f9f51f") // oxlint-disable-next-line test-slop/no-constant-pin -- rewritten by local-schema:bump; catches an unbumped schema edit - expect(SCHEMA_DIGEST).toBe("ea8d1656ca6f4b4f7cfda050291fb26fca73cc7d4ce21d50fff04cff5276b079") + expect(SCHEMA_DIGEST).toBe("7bc1c04157f9f51f68efd2668e1447385491a3589d384844810e929df83becc7") // oxlint-disable-next-line test-slop/no-constant-pin -- frozen identity that stores already on disk carry expect(ISSUE_297_TARGET_SCHEMA_PROJECT_REVISION).toBe( "506bc745f7a7eca202ec905a6403a6815e86413faf0cd3cbbf73881023edce91", @@ -111,8 +112,8 @@ describe("current local schema identity", () => { expect(CURRENT_SCHEMA_PROJECT_REVISION).toMatch(/^[0-9a-f]{64}$/) expect(LOCAL_SCHEMA_MANIFEST.objects.length).toBeGreaterThan(60) // oxlint-disable-next-line test-slop/no-constant-pin -- anchor that local-schema:bump rewrites on every bump - expect(CURRENT_LOCAL_SCHEMA.version).toBe(31) - expect(CURRENT_LOCAL_SCHEMA).toEqual(LOCAL_SCHEMA_V31) + expect(CURRENT_LOCAL_SCHEMA.version).toBe(32) + expect(CURRENT_LOCAL_SCHEMA).toEqual(LOCAL_SCHEMA_V32) const logs = LOCAL_SCHEMA_MANIFEST.objects.find((object) => object.name === "logs") expect(logs?.columns.some((column) => column.name.startsWith("idx_"))).toBe(false) expect(logs?.indexes).toContain("idx_lower_body") @@ -384,7 +385,7 @@ describe("local migration registry", () => { // One past the current tip — bump alongside LOCAL_SCHEMA_VERSION, or this // stops testing the future-store guard and starts testing the // unknown-fingerprint one. - { ...CURRENT_LOCAL_SCHEMA, version: 32, fingerprint: "future", digest: SCHEMA_DIGEST }, + { ...CURRENT_LOCAL_SCHEMA, version: 33, fingerprint: "future", digest: SCHEMA_DIGEST }, CURRENT_LOCAL_SCHEMA, ), ).toThrow(/newer than this build/) @@ -534,6 +535,7 @@ describe("local migration registry", () => { "local-0028-to-0029-external-edges-legacy-db-system", "local-0029-to-0030-error-fingerprint-v3", "local-0030-to-0031-error-fingerprint-span-name-frames", + "local-0031-to-0032-alert-checks-level", ] const versions = (id: string) => { const match = /^local-(\d{4})-to-(\d{4})-[a-z0-9]+(-[a-z0-9]+)*$/.exec(id) @@ -1516,6 +1518,7 @@ describe("v10 -> v11 product events module", () => { "local-0028-to-0029-external-edges-legacy-db-system", "local-0029-to-0030-error-fingerprint-v3", "local-0030-to-0031-error-fingerprint-span-name-frames", + "local-0031-to-0032-alert-checks-level", ]) expect(chain[0]?.to).toEqual(LOCAL_SCHEMA_V11) // The dropped table is declared, and the backfilled ones say what they diff --git a/apps/cli/test/native-local-store-migration.sh b/apps/cli/test/native-local-store-migration.sh index 9843261dbb..7cd4395f80 100755 --- a/apps/cli/test/native-local-store-migration.sh +++ b/apps/cli/test/native-local-store-migration.sh @@ -142,7 +142,7 @@ grep -q "local store migrated" "$ROOT/migrate.out" || fail "native migration did # must be bumped in lockstep with LOCAL_SCHEMA_VERSION and the matching # LOCAL_SCHEMA_V.fingerprint in apps/cli/src/server/schema-identity.ts; # leaving it on the previous version is what makes this step fail after a bump. -jq -e '.formatVersion == 2 and .activation == "active" and .schemaVersion == 31 and .schema == "ea8d1656ca6f4b4f"' \ +jq -e '.formatVersion == 2 and .activation == "active" and .schemaVersion == 32 and .schema == "7bc1c04157f9f51f"' \ "$ROOT/maple-store-version.json" >/dev/null || fail "native migration wrote the wrong active identity" step "reopening promoted store in a fresh server" diff --git a/apps/ingest/src/clickhouse_insert_mappings.rs b/apps/ingest/src/clickhouse_insert_mappings.rs index 757156ea15..8652b37fc0 100644 --- a/apps/ingest/src/clickhouse_insert_mappings.rs +++ b/apps/ingest/src/clickhouse_insert_mappings.rs @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-clickhouse-insert-mappings.ts // Do not edit manually. -pub const PROJECT_REVISION: &str = "64b7ae1279269efadefe2cb5773eaf28967c16da7af3d0659c297770532bbc97"; +pub const PROJECT_REVISION: &str = "9c4759bea56d9544000d0326a35b6224fa56ec76c373ce34bf16869430700c0a"; // Gate for BYO-ClickHouse ingest readiness — the migration version, NOT the // Tinybird-coupled PROJECT_REVISION. Compared against // org_clickhouse_settings.schema_version. See @maple/domain/clickhouse diff --git a/apps/ios/Maple/Fixtures/FixtureAPI.swift b/apps/ios/Maple/Fixtures/FixtureAPI.swift index 574b258a69..daa224d6ef 100644 --- a/apps/ios/Maple/Fixtures/FixtureAPI.swift +++ b/apps/ios/Maple/Fixtures/FixtureAPI.swift @@ -330,6 +330,7 @@ struct FixtureAPI: MapleAPI { signalType: signal, tags: [], threshold: threshold, + thresholds: [.init(destinationIds: ["dest_chat_oncall"], severity: severity, value: threshold)], updatedAt: stamp(-2 * 86_400), updatedBy: "user_fixture", windowMinutes: window diff --git a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json index bd94c37c62..045f57f484 100644 --- a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json +++ b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json @@ -15,6 +15,7 @@ "group_key": "__total__", "incident_id": "inc_tC4d9V79DCDzgbGKhAnff9", "incident_transition": "opened", + "level": "critical", "object": "alert_check", "observed_value": 0.09, "sample_count": 132, @@ -64,6 +65,13 @@ "incident_transition": { "$ref": "#/components/schemas/_maple_AlertIncidentTransition" }, + "level": { + "$ref": "#/components/schemas/_maple_AlertSeverity", + "description": "The highest severity tier this check breached, or `null` when none did (and on checks recorded before tiers).", + "examples": [ + "critical" + ] + }, "object": { "description": "The object type — always `\"alert_check\"`.", "enum": [ @@ -310,6 +318,7 @@ "last_sample_count": 132, "last_triggered_at": "2026-07-15T09:40:00.000Z", "object": "alert_incident", + "peak_severity": "critical", "resolved_at": null, "rule_id": "alrt_gU26thvJECdQvu54Ad9jiz", "rule_name": "Checkout error rate", @@ -382,6 +391,13 @@ ], "type": "string" }, + "peak_severity": { + "$ref": "#/components/schemas/_maple_AlertSeverity", + "description": "Highest severity the incident reached, or `null` on incidents opened before tiers.", + "examples": [ + "critical" + ] + }, "resolved_at": { "description": "When the incident resolved, or `null` while it is still open.", "type": "string" @@ -485,6 +501,7 @@ { "alert_on_no_data": false, "apdex_threshold_ms": null, + "breaches_out_of": null, "comparator": "gt", "consecutive_breaches_required": 2, "consecutive_healthy_required": 3, @@ -523,6 +540,17 @@ ], "threshold": 0.05, "threshold_upper": null, + "thresholds": [ + { + "destination_ids": [ + "dest_oybbpTBhtSFGShMjjLiCrh" + ], + "recovery_value": null, + "severity": "critical", + "upper": null, + "value": 0.05 + } + ], "updated_at": "2026-07-14T08:30:00.000Z", "updated_by": "user_2Nk8mXqPfR3yZ1aB4cD5eF6g", "window_minutes": 5 @@ -541,11 +569,20 @@ "exclusiveMinimum": 0, "type": "number" }, + "breaches_out_of": { + "description": "Count the required breaches over the last M checks instead of in a row (\"N of M\"), or `null`.", + "examples": [ + null + ], + "exclusiveMinimum": 0, + "maximum": 30, + "type": "integer" + }, "comparator": { "$ref": "#/components/schemas/_maple_AlertComparator" }, "consecutive_breaches_required": { - "description": "Consecutive breached checks required before an incident opens.", + "description": "Breached checks required before an incident opens or escalates (N). In a row unless `breaches_out_of` is set.", "examples": [ 2 ], @@ -569,7 +606,7 @@ "$ref": "#/components/schemas/_maple_UserId" }, "destination_ids": { - "description": "The alert destinations (`dest_…`) this rule notifies.", + "description": "Every destination (`dest_…`) any tier notifies. Derived from `thresholds`.", "items": { "$ref": "#/components/schemas/_maple_AlertDestinationId" }, @@ -717,9 +754,18 @@ "type": "number" }, "threshold_upper": { - "description": "Upper bound for range comparators (`between` / `not_between`), otherwise `null`.", + "description": "The lowest tier's upper bound for range comparators, otherwise `null`.", "type": "number" }, + "thresholds": { + "description": "Severity tiers, lowest first. An incident opens at the tier the value breaches and moves between tiers as it changes.", + "items": { + "$ref": "#/components/schemas/AlertThreshold" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, "updated_at": { "type": "string" }, @@ -742,6 +788,7 @@ "name", "enabled", "severity", + "thresholds", "service_names", "exclude_service_names", "environments", @@ -809,6 +856,43 @@ "title": "Alert rule list", "type": "object" }, + "AlertThreshold": { + "additionalProperties": false, + "description": "One severity tier of an alert rule.", + "properties": { + "destination_ids": { + "description": "Destinations (`dest_…`) this severity notifies. A `critical` incident also notifies every `warning` destination.", + "items": { + "$ref": "#/components/schemas/_maple_AlertDestinationId" + }, + "type": "array" + }, + "recovery_value": { + "description": "The value the signal must cross back over before this tier clears, or `null` (the default) to clear as soon as it stops breaching. Must sit on the healthy side of `value`.", + "examples": [ + 0.03 + ], + "type": "number" + }, + "severity": { + "$ref": "#/components/schemas/_maple_AlertSeverity" + }, + "upper": { + "description": "Upper bound for range comparators (`between` / `not_between`), otherwise `null`. Defaults to `null`.", + "type": "number" + }, + "value": { + "type": "number" + } + }, + "required": [ + "severity", + "value", + "destination_ids" + ], + "title": "Alert threshold", + "type": "object" + }, "AnomalyIncident": { "additionalProperties": false, "description": "A detected anomaly on a monitored signal (error rate, latency, throughput, error spike, or log volume) for a service and environment, opened by Maple's baseline detector.", @@ -3378,7 +3462,7 @@ "type": "string" }, "_maple_AlertIncidentTransition": { - "description": "How this check moved the incident state machine: `none`, `opened`, `continued`, or `resolved`.", + "description": "How this check moved the incident state machine: `none`, `opened`, `continued`, or `resolved`. A change of severity is `continued`; see `level`.", "enum": [ "none", "opened", @@ -3435,7 +3519,7 @@ "type": "string" }, "_maple_AlertSeverity": { - "description": "Severity attached to incidents this rule opens: `warning` or `critical`.", + "description": "The highest severity this rule can open an incident at: `warning` or `critical`. Derived from `thresholds`.", "enum": [ "warning", "critical" diff --git a/apps/web/e2e/.gitignore b/apps/web/e2e/.gitignore new file mode 100644 index 0000000000..2e73892f5d --- /dev/null +++ b/apps/web/e2e/.gitignore @@ -0,0 +1,3 @@ +# Run output; `cache/` holds replay recordings and is committed once agent steps run in CI. +.e2e/* +!.e2e/cache/ diff --git a/apps/web/e2e/README.md b/apps/web/e2e/README.md new file mode 100644 index 0000000000..c827a08b44 --- /dev/null +++ b/apps/web/e2e/README.md @@ -0,0 +1,24 @@ +# Web e2e + +Browser tests against a deployed Maple stack, written with [e2e](https://github.com/tester-army/e2e). +They catch what unit and component tests can't: a rule made by the API and edited in the UI, +a preview query that only fails on the deployed warehouse. + +Run against a PR preview (signs in as the shared preview reviewer, see `docs/pr-previews.md`): + +```bash +cd apps/web/e2e +E2E_TELEMETRY_DISABLED=1 MAPLE_E2E_APP_URL=https://app-pr-.maple.dev \ + infisical run --env dev -- bunx e2e run +``` + +- `tests/auth.setup.e2e.ts` trades `PREVIEW_SIGNIN_KEY` for a single-use Clerk ticket in the + runner, so the key never reaches the page, a trace, or a model. +- `tests/support.ts` `api()` calls the v2 API from the page with its Clerk session, for setup + and for asserting what was saved. +- Prefer locators and `expect`. `agent.assert` is for what selectors can't state (a chart's + lines); those tests register only when `ANTHROPIC_API_KEY` is set. +- Tests create their own rules and destinations with a run-unique suffix and delete them after. + +CI runs this after the preview's seed and smoke test (`deploy-pr-preview.yml`), non-blocking; +traces and screenshots land in the `e2e-results` artifact. diff --git a/apps/web/e2e/e2e.config.ts b/apps/web/e2e/e2e.config.ts new file mode 100644 index 0000000000..ef6efe4db5 --- /dev/null +++ b/apps/web/e2e/e2e.config.ts @@ -0,0 +1,29 @@ +import { anthropic } from "@ai-sdk/anthropic" +import { web } from "@e2e-dev/web" +import type { E2EConfig } from "e2e" + +// Runs against a deployed stack: a PR preview in CI, any signed-in-able URL locally. +// `agent.*` steps need ANTHROPIC_API_KEY; without it those tests skip themselves. +const config: E2EConfig = { + projectId: "maple-web", + targets: [ + { + name: "web", + engine: web(), + app: { + url: process.env.MAPLE_E2E_APP_URL ?? "http://localhost:3471", + // Every PR preview is its own origin; one identity lets them share replay recordings. + identity: "maple-web", + environment: "staging", + }, + }, + ], + timeout: 180_000, + assertionTimeout: 15_000, +} + +if (process.env.ANTHROPIC_API_KEY) { + config.agents = { default: { model: anthropic("claude-sonnet-5-5") } } +} + +export default config diff --git a/apps/web/e2e/tests/alert-rules.e2e.ts b/apps/web/e2e/tests/alert-rules.e2e.ts new file mode 100644 index 0000000000..141a8b7354 --- /dev/null +++ b/apps/web/e2e/tests/alert-rules.e2e.ts @@ -0,0 +1,141 @@ +import { AlertRulePublicId } from "@maple/domain/http/v2" +import { test } from "@e2e-dev/web" +import { Schema } from "effect" +import { expect } from "e2e" + +import { api, runTag } from "./support" + +interface Tier { + severity: "warning" | "critical" + value: number + upper: number | null + recovery_value: number | null + destination_ids: string[] +} +interface Rule { + id: string + name: string + thresholds: Tier[] + consecutive_breaches_required: number + breaches_out_of: number | null +} + +const toRuleUuid = Schema.decodeSync(AlertRulePublicId) + +test.describe("alert rules", () => { + // A rule written by the API or MCP, then edited in the UI: the path that dropped tiers. + test( + "keeps an API-made tiered rule intact through UI edits", + { session: "reviewer" }, + async ({ app, browser, screen }) => { + const appUrl = app.baseUrl ?? "" + await app.open("/alerts") + const tag = runTag() + const destination = (name: string) => + api<{ id: string }>(browser, appUrl, "/v2/alerts/destinations", { + method: "POST", + body: { + type: "webhook", + name: `e2e ${name} ${tag}`, + url: "https://example.com/maple-e2e", + }, + }) + const warn = await destination("warn") + const page = await destination("page") + const tiers: Tier[] = [ + { + severity: "warning", + value: 0.007, + upper: null, + recovery_value: 0.005, + destination_ids: [warn.body.id], + }, + { + severity: "critical", + value: 0.03, + upper: null, + recovery_value: null, + destination_ids: [page.body.id], + }, + ] + const created = await api(browser, appUrl, "/v2/alerts/rules", { + method: "POST", + body: { + name: `e2e tiers ${tag}`, + signal_type: "error_rate", + comparator: "gt", + window_minutes: 5, + consecutive_breaches_required: 2, + breaches_out_of: 4, + thresholds: tiers, + }, + }) + expect(created.status).toBe(200) + const editUrl = `/alerts/create?ruleId=${toRuleUuid(created.body.id)}` + const read = () => api(browser, appUrl, `/v2/alerts/rules/${created.body.id}`) + + try { + await app.open(editUrl) + await expect(browser.locator("#rule-threshold")).toHaveValue("0.7") + await expect(screen.getByLabel("Critical threshold")).toHaveValue("3") + await expect(screen.getByLabel("Out of the last")).toHaveValue("4") + + await screen.getByLabel("Rule name").fill(`e2e tiers renamed ${tag}`) + await screen.getByRole("button", "Save changes").tap() + await expect(browser).toHaveURL(/\/alerts$/) + const renamed = await read() + expect(renamed.body.name).toBe(`e2e tiers renamed ${tag}`) + expect(renamed.body.thresholds).toEqual(tiers) + expect(renamed.body.breaches_out_of).toBe(4) + + await app.open(editUrl) + await screen.getByLabel("Critical threshold").fill("4") + await screen.getByRole("button", "Save changes").tap() + await expect(browser).toHaveURL(/\/alerts$/) + const retuned = await read() + expect(retuned.body.thresholds.map((tier) => tier.value)).toEqual([0.007, 0.04]) + } finally { + await api(browser, appUrl, `/v2/alerts/rules/${created.body.id}`, { method: "DELETE" }) + await api(browser, appUrl, `/v2/alerts/destinations/${warn.body.id}`, { method: "DELETE" }) + await api(browser, appUrl, `/v2/alerts/destinations/${page.body.id}`, { method: "DELETE" }) + } + }, + ) + + test("names an empty range bound before save", { session: "reviewer" }, async ({ app, screen }) => { + await app.open("/alerts/create") + await screen.getByRole("button", /^Skip/).tap() + await screen.getByLabel("Rule name").fill(`e2e range ${runTag()}`) + await screen.getByRole("combobox", "Alert when the value").tap() + await screen.getByRole("option", "is between").tap() + + await expect(screen.getByText(/^Missing:/)).toContainText("Upper threshold") + }) + + test("previews a SQL rule", { session: "reviewer" }, async ({ app, screen }) => { + await app.open("/alerts/create") + await screen.getByRole("button", /^Skip/).tap() + await screen.getByRole("button", "SQL").tap() + + await expect(screen.getByText(/^Breach above/)).toBeVisible({ timeout: 45_000 }) + await expect(screen.getByText("Preview query failed")).toBeHidden() + }) + + // The agent fixture needs a model, so this one only registers when there is one. + if (process.env.ANTHROPIC_API_KEY) { + test( + "draws both tier lines on the chart", + { session: "reviewer" }, + async ({ agent, app, screen }) => { + await app.open("/alerts/create") + await screen.getByRole("button", /^Skip/).tap() + await screen.getByRole("button", "Add critical tier").tap() + + await expect(screen.getByText(/^Critical above/)).toBeVisible({ timeout: 45_000 }) + await agent.assert( + "The preview chart shows two dashed horizontal threshold lines: an amber one and a red one higher up", + ) + }, + ) + } +}) diff --git a/apps/web/e2e/tests/auth.setup.e2e.ts b/apps/web/e2e/tests/auth.setup.e2e.ts new file mode 100644 index 0000000000..06b2c997c1 --- /dev/null +++ b/apps/web/e2e/tests/auth.setup.e2e.ts @@ -0,0 +1,27 @@ +import { test } from "@e2e-dev/web" +import { expect } from "e2e" + +import { apiUrl } from "./support" + +// The shared preview reviewer (docs/pr-previews.md). The key is traded for a single-use, +// two-minute Clerk ticket here in the runner, so it never reaches the page or the model. +test.setup( + "sign in as the preview reviewer", + { sessions: ["reviewer"] }, + async ({ app, browser, session }) => { + const key = process.env.PREVIEW_SIGNIN_KEY?.trim() + if (!key) throw new Error("PREVIEW_SIGNIN_KEY is not set; run under `infisical run --env dev`.") + const response = await fetch(`${apiUrl(app.baseUrl ?? "")}/api/auth/preview/sign-in`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ key }), + }) + if (!response.ok) throw new Error(`Preview sign-in failed: HTTP ${response.status}`) + const { ticket }: { ticket: string } = await response.json() + + await app.open(`/sign-in?__clerk_ticket=${encodeURIComponent(ticket)}`) + await browser.waitForURL(/^(?!.*\/sign-in)/, { timeout: 30_000 }) + await expect(browser).toHaveURL(/^(?!.*\/sign-in)/) + await session.save("reviewer") + }, +) diff --git a/apps/web/e2e/tests/support.ts b/apps/web/e2e/tests/support.ts new file mode 100644 index 0000000000..37408f7eca --- /dev/null +++ b/apps/web/e2e/tests/support.ts @@ -0,0 +1,52 @@ +import type { Browser } from "@e2e-dev/web" + +declare global { + interface Window { + Clerk?: { session?: { getToken: () => Promise } | null } + } +} + +/** The preview's API origin: `app-pr-` serves its API at `api-pr-`. */ +export const apiUrl = (appUrl: string): string => + process.env.MAPLE_E2E_API_URL ?? appUrl.replace("://app-", "://api-") + +/** + * A v2 API call made from the page, so it carries the signed-in Clerk session. The body + * crosses back as JSON text: `evaluate` only returns JSON values. + */ +export const api = async ( + browser: Browser, + appUrl: string, + path: string, + init: { method?: string; body?: object } = {}, +): Promise<{ status: number; body: T }> => { + const result = await browser.evaluate( + async ({ url, method, body }) => { + // Clerk attaches its session a moment after the app shell renders. + const session = async (deadline: number): Promise<{ getToken: () => Promise }> => { + const current = window.Clerk?.session + if (current) return current + if (Date.now() > deadline) throw new Error("No Clerk session on the page") + await new Promise((resolve) => setTimeout(resolve, 200)) + return session(deadline) + } + const token = await (await session(Date.now() + 20_000)).getToken() + const request: RequestInit = { + method, + headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, + } + if (body !== null) request.body = body + const response = await fetch(url, request) + return { status: response.status, text: await response.text() } + }, + { + url: `${apiUrl(appUrl)}${path}`, + method: init.method ?? "GET", + body: init.body === undefined ? null : JSON.stringify(init.body), + }, + ) + return { status: result.status, body: JSON.parse(result.text) } +} + +/** A run-unique suffix, so rules and destinations from earlier runs never collide. */ +export const runTag = (): string => `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}` diff --git a/apps/web/e2e/tsconfig.json b/apps/web/e2e/tsconfig.json new file mode 100644 index 0000000000..94c7c834f9 --- /dev/null +++ b/apps/web/e2e/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { "types": ["node"] }, + "include": ["**/*.ts"] +} diff --git a/apps/web/package.json b/apps/web/package.json index 0102254acf..d485ebec28 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,6 +7,7 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run --passWithNoTests", + "test:e2e": "cd e2e && e2e run", "test:perf": "playwright test", "test:perf:bundle": "bun run build && bun run check:bundle", "check:bundle": "bun perf/check-bundle-budget.ts", @@ -78,6 +79,8 @@ "web-vitals": "^6.2.3" }, "devDependencies": { + "@ai-sdk/anthropic": "4.0.82", + "@e2e-dev/web": "0.15.2", "@effect/language-service": "catalog:effect", "@playwright/test": "^1.63.0", "@tanstack/devtools-vite": "^0.8.5", @@ -92,7 +95,9 @@ "@types/three": "^0.186.0", "@vitejs/plugin-react": "^6.1.2", "@vitest/browser-playwright": "catalog:", + "ai": "7.0.143", "dialkit": "2.0.2", + "e2e": "0.21.2", "playwright": "catalog:", "typescript": "catalog:tooling", "vite": "catalog:", diff --git a/apps/web/src/components/alerts/alert-create-form-surface.tsx b/apps/web/src/components/alerts/alert-create-form-surface.tsx index 9bd60117a1..2b2a8f3734 100644 --- a/apps/web/src/components/alerts/alert-create-form-surface.tsx +++ b/apps/web/src/components/alerts/alert-create-form-surface.tsx @@ -36,6 +36,8 @@ import { buildRuleCreateParamsV2, buildRuleTestParamsV2, deriveRuleQueryIssues, + deriveTierIssues, + isFilledNumber, isRangeComparator, isRulePreviewReady, pickDefaultDestination, @@ -226,7 +228,7 @@ export function AlertCreateFormSurface({ // Drop the verdict as soon as the user edits anything it depended on. const currentPreviewKey = previewIdentityKey(ruleForm) const freshPreviewResult = previewResult?.key === currentPreviewKey ? previewResult : null - const thresholdMissing = !Number.isFinite(Number(ruleForm.threshold)) || ruleForm.threshold.trim() === "" + const thresholdMissing = !isFilledNumber(ruleForm.threshold) return ( <> @@ -260,28 +262,30 @@ export function AlertCreateFormSurface({ status={thresholdMissing ? "missing" : "done"} >
- void previewRule()} - testing={previewingRule} - previewResult={freshPreviewResult} - range={previewRange} - timeRange={previewTimeRange} - onTimeRangeChange={setPreviewTimeRange} - comparison={ - savedForm !== null && changes.length > 0 - ? { - savedPreview: evaluationChanged ? savedReplay.preview : preview, - savedLoading: evaluationChanged && savedReplay.previewLoading, - changes, - clearedIncidents, - } - : null - } - /> + void previewRule()} + testing={previewingRule} + previewResult={freshPreviewResult} + range={previewRange} + timeRange={previewTimeRange} + onTimeRangeChange={setPreviewTimeRange} + comparison={ + savedForm !== null && changes.length > 0 + ? { + savedPreview: evaluationChanged + ? savedReplay.preview + : preview, + savedLoading: evaluationChanged && savedReplay.previewLoading, + changes, + clearedIncidents, + } + : null + } + />
@@ -291,7 +295,12 @@ export function AlertCreateFormSurface({ number={3} divider={false} title="Who gets notified" - status={ruleForm.destinationIds.length === 0 ? "missing" : "done"} + status={ + ruleForm.destinationIds.length === 0 || + deriveTierIssues(ruleForm).includes("Critical destinations") + ? "missing" + : "done" + } > tier) + return JSON.stringify({ ...evaluated, tiers }) } function makeSuggestedName(form: RuleFormState): string | null { diff --git a/apps/web/src/components/alerts/alert-create-page-content.test.ts b/apps/web/src/components/alerts/alert-create-page-content.test.ts index 5b4d3cc7e3..6d9eb42707 100644 --- a/apps/web/src/components/alerts/alert-create-page-content.test.ts +++ b/apps/web/src/components/alerts/alert-create-page-content.test.ts @@ -40,6 +40,9 @@ const rule = (id: string) => notificationTemplate: null, enabled: true, severity: "warning", + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], serviceNames: [`svc-${id}`], excludeServiceNames: [], environments: [], @@ -52,6 +55,7 @@ const rule = (id: string) => windowMinutes: 15, minimumSampleCount: 0, consecutiveBreachesRequired: 1, + breachesOutOf: null, consecutiveHealthyRequired: 1, renotifyIntervalMinutes: 60, apdexThresholdMs: null, diff --git a/apps/web/src/components/alerts/alert-create-remount-key.browser.test.tsx b/apps/web/src/components/alerts/alert-create-remount-key.browser.test.tsx index 235cf26210..7ff63ff988 100644 --- a/apps/web/src/components/alerts/alert-create-remount-key.browser.test.tsx +++ b/apps/web/src/components/alerts/alert-create-remount-key.browser.test.tsx @@ -27,6 +27,9 @@ const rule = (id: string) => notificationTemplate: null, enabled: true, severity: "warning", + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], serviceNames: [`svc-${id}`], excludeServiceNames: [], environments: [], @@ -39,6 +42,7 @@ const rule = (id: string) => windowMinutes: 15, minimumSampleCount: 0, consecutiveBreachesRequired: 1, + breachesOutOf: null, consecutiveHealthyRequired: 1, renotifyIntervalMinutes: 60, apdexThresholdMs: null, diff --git a/apps/web/src/components/alerts/alert-rule-chart.browser.test.tsx b/apps/web/src/components/alerts/alert-rule-chart.browser.test.tsx index 64a8ea5664..db7a0df77e 100644 --- a/apps/web/src/components/alerts/alert-rule-chart.browser.test.tsx +++ b/apps/web/src/components/alerts/alert-rule-chart.browser.test.tsx @@ -34,6 +34,7 @@ function check(offsetMinutes: number, status: AlertCheckDocument["status"], obse consecutiveHealthy: 0, incidentId: null, incidentTransition: "none", + level: null, evaluationDurationMs: 12, errorMessage: null, errorCategory: null, diff --git a/apps/web/src/components/alerts/alert-rule-chart.tsx b/apps/web/src/components/alerts/alert-rule-chart.tsx index b70aa2f67f..79c02a5dd0 100644 --- a/apps/web/src/components/alerts/alert-rule-chart.tsx +++ b/apps/web/src/components/alerts/alert-rule-chart.tsx @@ -57,11 +57,13 @@ const SIGNAL_TOKEN = "--chart-1" const PAINT_KEYS = { signal: "__signal", destructive: "__destructive", + warn: "__warn", muted: "__muted", } as const const PAINT_COLORS = [ { key: PAINT_KEYS.signal, color: SIGNAL_TOKEN }, { key: PAINT_KEYS.destructive, color: "--destructive" }, + { key: PAINT_KEYS.warn, color: "--severity-warn" }, { key: PAINT_KEYS.muted, color: "--muted-foreground" }, ] @@ -85,6 +87,8 @@ interface AlertRuleChartProps { showWouldFire?: boolean threshold: number thresholdUpper?: number | null + /** A two-tier rule's critical value: drawn as its own line, and `threshold` reads as warning. */ + criticalThreshold?: number | null comparator: AlertComparator signalType: AlertSignalType /** Page time window in epoch ms — the shared domain for the axis, bands, and rail. */ @@ -144,6 +148,7 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ showWouldFire = false, threshold, thresholdUpper, + criticalThreshold = null, comparator, signalType, window: requestedDomain, @@ -249,6 +254,7 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ const yDomain = React.useMemo<[number, number]>(() => { let maxVal = threshold if (thresholdUpper != null) maxVal = Math.max(maxVal, thresholdUpper) + if (criticalThreshold != null) maxVal = Math.max(maxVal, criticalThreshold) for (const point of chartData) { for (const key of seriesKeys) maxVal = Math.max(maxVal, num(point[key])) // The ghost shares the axis — clipping it would misrepresent the gap. @@ -256,7 +262,7 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ } const upper = Math.max(maxVal * 1.15, threshold * 1.3) return [0, upper > 0 ? upper : 1] - }, [chartData, seriesKeys, threshold, thresholdUpper]) + }, [chartData, seriesKeys, threshold, thresholdUpper, criticalThreshold]) // The breach region — the part of the fill that turns red — is the side of the // threshold the comparator flags. Range comparators have two bounds, so they @@ -448,6 +454,7 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ () => ({ signal: plot.color(PAINT_KEYS.signal), destructive: plot.color(PAINT_KEYS.destructive), + warn: plot.color(PAINT_KEYS.warn), muted: plot.color(PAINT_KEYS.muted), }), [plot], @@ -545,12 +552,23 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ : [{ value: threshold }], { y: (entry: { value: number }) => entry.value, - stroke: palette.destructive, + stroke: criticalThreshold != null ? palette.warn : palette.destructive, strokeOpacity: 1, strokeWidth: 1.5, strokeDasharray: "6 4", }, ), + ...(criticalThreshold != null + ? [ + ruleY([{ value: criticalThreshold }], { + y: (entry: { value: number }) => entry.value, + stroke: palette.destructive, + strokeOpacity: 1, + strokeWidth: 1.5, + strokeDasharray: "6 4", + }), + ] + : []), // The unselected source, behind the primary: same shape, dashed and // muted, so "the query says one thing, the evaluator recorded another" // is visible instead of inferred. @@ -646,6 +664,7 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ wouldFireBands, threshold, thresholdUpper, + criticalThreshold, hasGhost, isMultiSeries, seriesKeys, @@ -717,24 +736,48 @@ export const AlertRuleChart = React.memo(function AlertRuleChart({ {chartArea} {hasSignal && ( -
- - - {thresholdUpper != null - ? "Threshold range " - : breachBelow - ? "Breach below " - : "Breach above "} - - {formatSignalValue(signalType, threshold)} +
+ + + + {criticalThreshold != null ? "Warning " : ""} {thresholdUpper != null - ? ` – ${formatSignalValue(signalType, thresholdUpper)}` - : ""} + ? "Threshold range " + : breachBelow + ? criticalThreshold != null + ? "below " + : "Breach below " + : criticalThreshold != null + ? "above " + : "Breach above "} + + {formatSignalValue(signalType, threshold)} + {thresholdUpper != null + ? ` – ${formatSignalValue(signalType, thresholdUpper)}` + : ""} + + {criticalThreshold != null && ( + + + + Critical {breachBelow ? "below " : "above "} + + {formatSignalValue(signalType, criticalThreshold)} + + + + )}
)} diff --git a/apps/web/src/components/alerts/notifications-section.tsx b/apps/web/src/components/alerts/notifications-section.tsx index 71263aa7e9..4dfd47c73d 100644 --- a/apps/web/src/components/alerts/notifications-section.tsx +++ b/apps/web/src/components/alerts/notifications-section.tsx @@ -5,12 +5,17 @@ import { Field, FieldLabel } from "@maple/ui/components/ui/field" import { useState, type Dispatch, type SetStateAction } from "react" import { useUser } from "@clerk/clerk-react" -import { ALERT_TEMPLATE_VARIABLES, type AlertDestinationDocument } from "@maple/domain/http" +import { + ALERT_TEMPLATE_VARIABLES, + type AlertDestinationDocument, + type AlertDestinationId, +} from "@maple/domain/http" import { Badge } from "@maple/ui/components/ui/badge" import { Button } from "@maple/ui/components/ui/button" import { Card } from "@maple/ui/components/ui/card" import { IconButton } from "@maple/ui/components/ui/icon-button" import { Panel } from "@maple/ui/components/ui/panel" +import { StatusDot } from "@maple/ui/components/ui/status-dot" import { Tooltip, TooltipContent, TooltipTrigger } from "@maple/ui/components/ui/tooltip" import { DropdownMenu, @@ -32,6 +37,7 @@ import { import { useChatConnectors, useChatWorkspaceConnect } from "@/components/alerts/use-chat-workspaces" import { chatDestinationForm, + comparatorSupportsTiers, defaultDestinationForm, type DestinationFormState, type RuleFormState, @@ -101,23 +107,28 @@ export function NotificationsSection({ onAddDestination, onQuickCreate, }: NotificationsSectionProps) { - const selected = destinations.filter((d) => form.destinationIds.includes(d.id)) - const available = destinations.filter((d) => !form.destinationIds.includes(d.id)) - // Saved ids the list does not hold (deleted, or the list failed to load) still get notified on - // save, so they stay on screen and removable rather than silently riding along. - const unresolved = form.destinationIds.filter((id) => !destinations.some((d) => d.id === id)) - const canAdd = available.length > 0 || onAddDestination !== undefined - - const select = (id: AlertDestinationDocument["id"]) => - onChange((c) => ({ ...c, destinationIds: [...new Set([...c.destinationIds, id])] })) - const remove = (id: AlertDestinationDocument["id"]) => - onChange((c) => ({ ...c, destinationIds: c.destinationIds.filter((existing) => existing !== id) })) + const escalation = comparatorSupportsTiers(form.comparator) ? form.escalation : null + const anySelected = destinations.some( + (d) => form.destinationIds.includes(d.id) || (escalation?.destinationIds.includes(d.id) ?? false), + ) + const list = ( + ids: ReadonlyArray, + setIds: (update: (ids: AlertDestinationId[]) => AlertDestinationId[]) => void, + ) => ( + setIds((current) => [...new Set([...current, id])])} + onRemove={(id) => setIds((current) => current.filter((existing) => existing !== id))} + destinations={destinations} + onAddDestination={onAddDestination} + onQuickCreate={onQuickCreate} + /> + ) return ( - {selected.length > 0 && ( -
- {( + {anySelected && ( +
- )} -
+
)} - - {selected.map((destination) => ( - remove(destination.id)} - /> - ))} - {unresolved.map((id) => ( -
- - Destination unavailable - - remove(id)} - > - - + {escalation === null ? ( + list(form.destinationIds, (update) => + onChange((c) => ({ ...c, destinationIds: update(c.destinationIds) })), + ) + ) : ( +
+
+ Warning + {list(form.destinationIds, (update) => + onChange((c) => ({ ...c, destinationIds: update(c.destinationIds) })), + )}
- ))} - {selected.length === 0 && unresolved.length === 0 && ( -

- {canAdd - ? "No one is notified yet." - : "No destinations exist yet, and only an org admin can add one."} -

- )} - {canAdd && ( - - )} - +
+ + Critical, on top of the warning destinations + + {list(escalation.destinationIds, (update) => + onChange((c) => + c.escalation === null + ? c + : { + ...c, + escalation: { + ...c.escalation, + destinationIds: update(c.escalation.destinationIds), + }, + }, + ), + )} +
+
+ )} ) } +function TierHeading({ severity, children }: { severity: "warning" | "critical"; children: string }) { + return ( +

+ + {children} +

+ ) +} + +/** One tier's destinations, plus the menu that adds an existing or new one. */ +function DestinationList({ + ids, + onSelect, + onRemove, + destinations, + onAddDestination, + onQuickCreate, +}: { + ids: ReadonlyArray + onSelect: (id: AlertDestinationDocument["id"]) => void + onRemove: (id: AlertDestinationDocument["id"]) => void + destinations: AlertDestinationDocument[] + onAddDestination?: (preset?: DestinationFormState) => void + onQuickCreate?: (form: DestinationFormState) => Promise +}) { + const selected = destinations.filter((d) => ids.includes(d.id)) + const available = destinations.filter((d) => !ids.includes(d.id)) + // Saved ids the list does not hold (deleted, or the list failed to load) still get notified on + // save, so they stay on screen and removable rather than silently riding along. + const unresolved = ids.filter((id) => !destinations.some((d) => d.id === id)) + const canAdd = available.length > 0 || onAddDestination !== undefined + + return ( + + {selected.map((destination) => ( + onRemove(destination.id)} + /> + ))} + {unresolved.map((id) => ( +
+ + Destination unavailable + + onRemove(id)} + > + + +
+ ))} + {selected.length === 0 && unresolved.length === 0 && ( +

+ {canAdd + ? "No one is notified yet." + : "No destinations exist yet, and only an org admin can add one."} +

+ )} + {canAdd && ( + + )} +
+ ) +} + function SelectedDestination({ destination, onRemove, diff --git a/apps/web/src/components/alerts/rule-condition-bar.browser.test.tsx b/apps/web/src/components/alerts/rule-condition-bar.browser.test.tsx new file mode 100644 index 0000000000..c358fcad11 --- /dev/null +++ b/apps/web/src/components/alerts/rule-condition-bar.browser.test.tsx @@ -0,0 +1,39 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react" +import { useState } from "react" +import { afterEach, describe, expect, it } from "vitest" + +import { defaultRuleForm, type RuleFormState } from "@/lib/alerts/form-utils" +import { describeRuleCondition, RuleConditionFields } from "./rule-condition-bar" + +afterEach(cleanup) + +function Harness({ initial }: { initial: RuleFormState }) { + const [form, setForm] = useState(initial) + return ( + <> + + {describeRuleCondition(form)} + + ) +} + +describe("RuleConditionFields tiers", () => { + it("adds a critical tier one step past the warning threshold, and removes it again", () => { + render() + + fireEvent.click(screen.getByRole("button", { name: "Add critical tier" })) + expect(screen.getByLabelText("Critical threshold")).toHaveProperty("value", "10") + expect(screen.getByRole("status").textContent).toContain("critical above 10%") + + fireEvent.click(screen.getByRole("button", { name: "Remove" })) + expect(screen.queryByLabelText("Critical threshold")).toBeNull() + expect(screen.getByRole("button", { name: "Add critical tier" })).toBeTruthy() + }) + + it("offers no critical tier or recovery value on a range condition", () => { + render() + + expect(screen.queryByRole("button", { name: "Add critical tier" })).toBeNull() + expect(screen.queryByLabelText("Recovers below")).toBeNull() + }) +}) diff --git a/apps/web/src/components/alerts/rule-condition-bar.tsx b/apps/web/src/components/alerts/rule-condition-bar.tsx index a52de2bea3..9d229212ac 100644 --- a/apps/web/src/components/alerts/rule-condition-bar.tsx +++ b/apps/web/src/components/alerts/rule-condition-bar.tsx @@ -15,11 +15,16 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { StatusDot } from "@maple/ui/components/ui/status-dot" import { cn } from "@maple/ui/lib/utils" +import { Button } from "@maple/ui/components/ui/button" + +import { PlusIcon } from "@/components/icons" import { comparatorLabels, + comparatorSupportsTiers, isRangeComparator, signalLabels, thresholdUnitLabel, + type EscalationTierForm, type RuleFormState, } from "@/lib/alerts/form-utils" @@ -79,10 +84,38 @@ export function describeRuleCondition(form: RuleFormState): string { const threshold = isRangeComparator(form.comparator) ? `${value(form.threshold)} and ${value(form.thresholdUpper)}` : value(form.threshold) + const escalation = comparatorSupportsTiers(form.comparator) ? form.escalation : null const checks = form.consecutiveBreachesRequired.trim() || "?" + const outOf = form.breachesOutOf.trim() const windowText = `${form.windowMinutes.trim() || "?"} min` - const when = checks === "1" ? `over ${windowText}` : `in ${checks} checks in a row (${windowText} each)` - return `${form.severity === "critical" ? "Critical" : "Warning"} when ${conditionSubject(form)} ${COMPARATOR_WORDS[form.comparator]} ${threshold} ${when}` + const when = + outOf.length > 0 + ? `in ${checks} of the last ${outOf} checks (${windowText} each)` + : checks === "1" + ? `over ${windowText}` + : `in ${checks} checks in a row (${windowText} each)` + const severity = escalation === null && form.severity === "critical" ? "Critical" : "Warning" + const critical = + escalation === null + ? "" + : `, critical ${COMPARATOR_WORDS[form.comparator].replace(/^is /, "")} ${value(escalation.threshold)},` + return `${severity} when ${conditionSubject(form)} ${COMPARATOR_WORDS[form.comparator]} ${threshold}${critical} ${when}` +} + +/** What crossing back looks like for a comparator, as the recovery field's label. */ +function recoveryLabel(comparator: AlertComparator): string { + return comparator === "gt" || comparator === "gte" ? "Recovers below" : "Recovers above" +} + +/** A starting critical value one step worse than the warning one, so the new row is never blank. */ +function suggestCriticalThreshold(form: RuleFormState): string { + const warning = Number(form.threshold) + if (form.threshold.trim().length === 0 || !Number.isFinite(warning)) return "" + // One step of the warning's own size (1 at zero) in the breaching direction, + // so the suggestion is always worse than the warning, whatever its sign. + const step = Math.abs(warning) || 1 + const worse = form.comparator === "gt" || form.comparator === "gte" ? warning + step : warning - step + return String(Number(worse.toPrecision(12))) } interface RuleConditionFieldsProps { @@ -92,13 +125,17 @@ interface RuleConditionFieldsProps { saved: RuleFormState | null } -/** Threshold, severity and evaluation as labeled fields in a grid under the chart; each redraws it. */ +/** Condition, one row per severity tier and the evaluation fields under the chart; each redraws it. */ export function RuleConditionFields({ form, onChange, saved }: RuleConditionFieldsProps) { const rangeMode = isRangeComparator(form.comparator) + const tiered = comparatorSupportsTiers(form.comparator) + const escalation = tiered ? form.escalation : null const unit = thresholdUnitLabel(form) const was = (pick: (f: RuleFormState) => string) => saved !== null && pick(saved).trim() !== pick(form).trim() ? pick(saved) : null const comparatorWas = was((f) => f.comparator) + const setEscalation = (patch: Partial) => + onChange((c) => (c.escalation === null ? c : { ...c, escalation: { ...c.escalation, ...patch } })) return ( @@ -128,98 +165,230 @@ export function RuleConditionFields({ form, onChange, saved }: RuleConditionFiel {COMPARATOR_WORDS[saved?.comparator ?? form.comparator]} )} + {!tiered && form.escalation !== null && ( +

+ This condition has one tier, so the critical tier is dropped on save. +

+ )} +
-
- f.threshold)} - onChange={(value) => onChange((c) => ({ ...c, threshold: value }))} - /> - {rangeMode && ( +
+ Thresholds +
+ {escalation === null ? ( + onChange((c) => ({ ...c, severity }))} + /> + ) : ( + + )} +
f.thresholdUpper)} - onChange={(value) => onChange((c) => ({ ...c, thresholdUpper: value }))} + previous={was((f) => f.threshold)} + onChange={(value) => onChange((c) => ({ ...c, threshold: value }))} + /> + {rangeMode && ( + f.thresholdUpper)} + onChange={(value) => onChange((c) => ({ ...c, thresholdUpper: value }))} + /> + )} +
+ {tiered && ( + f.recoveryValue)} + onChange={(value) => onChange((c) => ({ ...c, recoveryValue: value }))} /> )}
- - Severity - onChange((c) => ({ ...c, severity: value }))} - aria-labelledby="rule-severity-label" - className="grid grid-cols-2 gap-1 rounded-md bg-muted/30 p-0.5" + {escalation !== null && ( +
+
+ + +
+ f.escalation?.threshold ?? "")} + onChange={(value) => setEscalation({ threshold: value })} + /> + f.escalation?.recoveryValue ?? "")} + onChange={(value) => setEscalation({ recoveryValue: value })} + /> +
+ )} + + {tiered && escalation === null && form.severity === "warning" && ( + + )}
How it is checked
- f.windowMinutes)} - onChange={(value) => onChange((c) => ({ ...c, windowMinutes: value }))} - /> - f.consecutiveBreachesRequired)} - onChange={(value) => onChange((c) => ({ ...c, consecutiveBreachesRequired: value }))} - /> - f.minimumSampleCount)} - onChange={(value) => onChange((c) => ({ ...c, minimumSampleCount: value }))} - /> + f.windowMinutes)} + onChange={(value) => onChange((c) => ({ ...c, windowMinutes: value }))} + /> +
+ f.consecutiveBreachesRequired)} + onChange={(value) => + onChange((c) => ({ ...c, consecutiveBreachesRequired: value })) + } + /> + 0 ? "checks" : null} + previous={was((f) => f.breachesOutOf)} + onChange={(value) => onChange((c) => ({ ...c, breachesOutOf: value }))} + /> +

+ Empty counts them in a row. 1 fires on the first breach. +

+
+ f.minimumSampleCount)} + onChange={(value) => onChange((c) => ({ ...c, minimumSampleCount: value }))} + />
) } +function SeverityPicker({ + value, + onChange, +}: { + value: AlertSeverity + onChange: (value: AlertSeverity) => void +}) { + return ( + + Severity + onChange(next)} + aria-labelledby="rule-severity-label" + className="grid grid-cols-2 gap-1 rounded-md bg-muted/30 p-0.5" + > + {SEVERITY_OPTIONS.map((option) => { + const selected = value === option.value + return ( + + + {option.label} + + ) + })} + + + ) +} + +/** The fixed severity of a row once a rule has two tiers. */ +function TierLabel({ severity }: { severity: AlertSeverity }) { + const option = SEVERITY_OPTIONS.find((o) => o.value === severity) ?? SEVERITY_OPTIONS[0]! + return ( + + Severity + + + {option.label} + + + ) +} + function WasNote({ children }: { children: ReactNode }) { return (

@@ -232,6 +401,8 @@ function NumberField({ id, label, description, + placeholder, + ariaLabel, value, unit, previous, @@ -240,6 +411,9 @@ function NumberField({ id: string label: string description?: string + placeholder?: string + /** Names the input apart from its visible label, where two rows share one. */ + ariaLabel?: string value: string unit: string | null /** The saved value when it differs. */ @@ -255,6 +429,8 @@ function NumberField({ type="number" inputMode="decimal" value={value} + placeholder={placeholder} + aria-label={ariaLabel} onChange={(event) => onChange(event.target.value)} controlClassName={NUMERIC_CONTROL_CLASS} /> diff --git a/apps/web/src/components/alerts/rule-live-chart-hero.tsx b/apps/web/src/components/alerts/rule-live-chart-hero.tsx index c2e7c8061c..97f0850b61 100644 --- a/apps/web/src/components/alerts/rule-live-chart-hero.tsx +++ b/apps/web/src/components/alerts/rule-live-chart-hero.tsx @@ -24,11 +24,7 @@ import { normalizeTimestampInput } from "@/lib/timezone-format" import { TimeRangePicker } from "@/components/time-range-picker/time-range-picker" import type { TimeRange } from "@/components/time-range-picker/types" import type { AlertIncidentDocument } from "@maple/domain/http" -import { - displaySignalType, - formatSignalValue, - type RuleFormState, -} from "@/lib/alerts/form-utils" +import { displaySignalType, formatSignalValue, type RuleFormState } from "@/lib/alerts/form-utils" interface RuleLiveChartHeroProps { form: RuleFormState @@ -80,7 +76,7 @@ export function RuleLiveChartHero({ }: RuleLiveChartHeroProps) { // The preview plots domain units (error rate as a 0–1 ratio); the form's // threshold goes through the same conversion the save does. - const { threshold, upper: thresholdUpper } = draftThresholds(form) + const { threshold, upper: thresholdUpper, critical: criticalThreshold } = draftThresholds(form) const displaySignal = displaySignalType(form) // The chart's domain is the exact window the preview query ran over, so the @@ -163,6 +159,7 @@ export function RuleLiveChartHero({ showWouldFire threshold={threshold} thresholdUpper={thresholdUpper} + criticalThreshold={criticalThreshold} comparator={form.comparator} signalType={displaySignal} window={window} diff --git a/apps/web/src/hooks/use-alert-rule-preview.ts b/apps/web/src/hooks/use-alert-rule-preview.ts index 66434e03a3..436958f502 100644 --- a/apps/web/src/hooks/use-alert-rule-preview.ts +++ b/apps/web/src/hooks/use-alert-rule-preview.ts @@ -7,6 +7,7 @@ import type { V2AlertRulePreviewParams } from "@maple/domain/http/v2" import { buildRuleCreateParamsV2, deriveRuleQueryIssues, + isFilledNumber, v2PreviewToResponse, type RuleFormState, } from "@/lib/alerts/form-utils" @@ -55,6 +56,11 @@ export const toPreviewForm = (form: RuleFormState): RuleFormState => ({ notes: "", tags: [], destinationIds: [], + // A critical tier still being typed would fail the whole preview; chart the warning tier until then. + escalation: + form.escalation === null || !isFilledNumber(form.escalation.threshold) + ? null + : { ...form.escalation, destinationIds: [] }, notificationTitle: "", notificationBody: "", }) diff --git a/apps/web/src/lab/alerts-fixture.ts b/apps/web/src/lab/alerts-fixture.ts index 9fcec0d4d7..dd12cf7a80 100644 --- a/apps/web/src/lab/alerts-fixture.ts +++ b/apps/web/src/lab/alerts-fixture.ts @@ -295,6 +295,16 @@ const ruleFrom = (spec: RuleSpec, nowMs: number): AlertRuleDocument => { windowMinutes: 5, minimumSampleCount: spec.minSamples ?? 0, consecutiveBreachesRequired: 2, + breachesOutOf: null, + thresholds: [ + { + severity: spec.severity, + value: spec.threshold, + upper: null, + recoveryValue: null, + destinationIds: [], + }, + ], consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 15, apdexThresholdMs: spec.signalType === "apdex" ? 500 : null, @@ -411,6 +421,7 @@ const incidentFrom = (spec: IncidentSpec, rule: AlertRuleDocument, nowMs: number comparator: rule.comparator, threshold: rule.threshold, thresholdUpper: rule.thresholdUpper, + peakSeverity: null, firstTriggeredAt: iso(nowMs - spec.startAgo), lastTriggeredAt: iso(nowMs - (spec.held ? 58 * MIN : (spec.endAgo ?? 0) + MIN)), resolvedAt: open ? null : iso(nowMs - (spec.endAgo ?? 0)), diff --git a/apps/web/src/lab/worst-case-docs.ts b/apps/web/src/lab/worst-case-docs.ts index c3ce5b039b..13dc05f57f 100644 --- a/apps/web/src/lab/worst-case-docs.ts +++ b/apps/web/src/lab/worst-case-docs.ts @@ -155,6 +155,10 @@ export function rulesFixture(mode: WorstCaseMode): RulesFixture { windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 1, + breachesOutOf: null, + thresholds: [ + { severity: "critical", value: threshold, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 1, renotifyIntervalMinutes: 60, apdexThresholdMs: null, diff --git a/apps/web/src/lib/alerts/chart-series.test.ts b/apps/web/src/lib/alerts/chart-series.test.ts index 00e0c4daf9..31a4d2a92b 100644 --- a/apps/web/src/lib/alerts/chart-series.test.ts +++ b/apps/web/src/lib/alerts/chart-series.test.ts @@ -41,6 +41,9 @@ const previewOf = ( windowMinutes: 5, threshold: 0.05, thresholdUpper: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], comparator: "gt", truncatedToStart: options?.truncatedToStart != null ? IsoDateTimeString.make(options.truncatedToStart) : null, diff --git a/apps/web/src/lib/alerts/diagnosis.test.ts b/apps/web/src/lib/alerts/diagnosis.test.ts index ec8c0a1ffe..48163d76ba 100644 --- a/apps/web/src/lib/alerts/diagnosis.test.ts +++ b/apps/web/src/lib/alerts/diagnosis.test.ts @@ -30,6 +30,10 @@ function makeRule(overrides: Record = {}): AlertRuleDocument { windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 3, + breachesOutOf: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 30, apdexThresholdMs: null, diff --git a/apps/web/src/lib/alerts/form-tiers.test.ts b/apps/web/src/lib/alerts/form-tiers.test.ts new file mode 100644 index 0000000000..1b82765080 --- /dev/null +++ b/apps/web/src/lib/alerts/form-tiers.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest" +import { Schema } from "effect" +import { AlertDestinationId, AlertRuleDocument } from "@maple/domain/http" +import { describeRuleCondition } from "@/components/alerts/rule-condition-bar" +import { buildRuleCreateParamsV2, defaultRuleForm, deriveTierIssues, ruleToFormState } from "./form-utils" +import { diffRuleForms } from "./preview-insights" + +const slack = AlertDestinationId.make("4f6c6b1e-7a11-4c2e-9f40-1c3b2d9a0001") +const pager = AlertDestinationId.make("4f6c6b1e-7a11-4c2e-9f40-1c3b2d9a0002") + +const tieredRule = (): AlertRuleDocument => + Schema.decodeUnknownSync(AlertRuleDocument)({ + id: "6f1d1c9e-2b9a-4d1e-8c3f-5a7b9c0d0001", + name: "Checkout errors", + notes: null, + notificationTemplate: null, + enabled: true, + severity: "critical", + serviceNames: ["checkout"], + excludeServiceNames: [], + environments: [], + tags: [], + groupBy: null, + signalType: "error_rate", + comparator: "gt", + threshold: 0.007, + thresholdUpper: null, + windowMinutes: 5, + minimumSampleCount: 0, + consecutiveBreachesRequired: 2, + breachesOutOf: 4, + thresholds: [ + { severity: "warning", value: 0.007, upper: null, recoveryValue: 0.005, destinationIds: [slack] }, + { severity: "critical", value: 0.03, upper: null, recoveryValue: null, destinationIds: [pager] }, + ], + consecutiveHealthyRequired: 2, + renotifyIntervalMinutes: 30, + apdexThresholdMs: null, + queryBuilderDraft: null, + rawQuerySql: null, + rawQueryReducer: null, + destinationIds: [slack, pager], + noDataBehavior: "skip", + lastEvaluationError: null, + lastEvaluatedAt: null, + lastScheduledAt: null, + createdAt: "2026-10-01T00:00:00.000Z", + updatedAt: "2026-10-01T00:00:00.000Z", + createdBy: "user_tiers_test", + updatedBy: "user_tiers_test", + }) + +describe("severity tiers on the rule form", () => { + it("saves a loaded tiered rule back with every tier, recovery value and breach window", () => { + const params = buildRuleCreateParamsV2(ruleToFormState(tieredRule())) + + expect(params.thresholds).toEqual([ + { + severity: "warning", + value: 0.007, + upper: null, + recovery_value: 0.005, + destination_ids: [slack], + }, + { + severity: "critical", + value: 0.03, + upper: null, + recovery_value: null, + destination_ids: [pager], + }, + ]) + expect(params.breaches_out_of).toBe(4) + expect(params.consecutive_breaches_required).toBe(2) + expect(params).toMatchObject({ severity: "critical", threshold: 0.007 }) + }) + + it("reports no change for a tiered rule opened and saved untouched", () => { + const form = ruleToFormState(tieredRule()) + + expect(diffRuleForms(form, form)).toEqual([]) + expect(diffRuleForms(form, { ...form, breachesOutOf: "" })).toMatchObject([{ field: "Out of last" }]) + }) + + it("describes both tiers and the breach window in one sentence", () => { + expect(describeRuleCondition(ruleToFormState(tieredRule()))).toBe( + "Warning when error rate is above 0.7%, critical above 3%, in 2 of the last 4 checks (5 min each)", + ) + }) + + it("drops the critical tier for comparators that only take one", () => { + const form = { ...ruleToFormState(tieredRule()), comparator: "between" as const, thresholdUpper: "2" } + const params = buildRuleCreateParamsV2(form) + + expect(params.thresholds).toHaveLength(1) + expect(params.thresholds?.[0]).toMatchObject({ + severity: "warning", + recovery_value: null, + upper: 0.02, + }) + }) + + it("names each tier problem the server would reject", () => { + const base = { ...defaultRuleForm(), name: "Errors", destinationIds: [slack] } + const withCritical = (critical: string, recovery = "") => ({ + ...base, + escalation: { threshold: critical, recoveryValue: recovery, destinationIds: [pager] }, + }) + + expect(deriveTierIssues(withCritical("10"))).toEqual([]) + expect(deriveTierIssues(withCritical(""))).toEqual(["Critical threshold"]) + expect(deriveTierIssues(withCritical("5"))).toEqual(["Critical threshold past warning"]) + expect(deriveTierIssues(withCritical("10", "4"))).toEqual(["Critical recovery past warning"]) + expect(deriveTierIssues(withCritical("10", "12"))).toEqual(["Critical recovery value"]) + expect(deriveTierIssues({ ...base, recoveryValue: "6" })).toEqual(["Recovery value"]) + expect(deriveTierIssues({ ...withCritical("10"), escalation: null })).toEqual([]) + expect( + deriveTierIssues({ + ...base, + escalation: { threshold: "10", recoveryValue: "", destinationIds: [] }, + }), + ).toEqual(["Critical destinations"]) + expect(deriveTierIssues({ ...base, breachesOutOf: "1" })).toEqual(["Breach window"]) + }) +}) diff --git a/apps/web/src/lib/alerts/form-utils.test.ts b/apps/web/src/lib/alerts/form-utils.test.ts index 42d0a18044..d8f7786999 100644 --- a/apps/web/src/lib/alerts/form-utils.test.ts +++ b/apps/web/src/lib/alerts/form-utils.test.ts @@ -62,6 +62,13 @@ describe("threshold unit conversion", () => { expect(domainThresholdToForm({ signalType: "error_rate" }, 0.29)).toBe("29") }) + it("round-trips a percent the form read back to the stored ratio", () => { + for (const ratio of [0.007, 0.011, 0.0029, 0.333]) { + const form = domainThresholdToForm({ signalType: "error_rate" }, ratio) + expect(formThresholdToDomain({ signalType: "error_rate" }, form)).toBe(ratio) + } + }) + it("keeps precision for error rates below 0.01%", () => { expect(formatSignalValue("error_rate", 0.00005)).toBe("0.0050%") expect(formatSignalValue("error_rate", 0.05)).toBe(formatErrorRate(0.05)) @@ -279,6 +286,9 @@ describe("v2 response mappers", () => { window_minutes: 5, threshold: 0.05, threshold_upper: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recovery_value: 0.03, destination_ids: [] }, + ], comparator: "gt", truncated_to_start: "2026-07-15T00:00:00.000Z", series: [ @@ -300,10 +310,14 @@ describe("v2 response mappers", () => { group_key: "__total__", start: "2026-07-15T09:10:00.000Z", end: "2026-07-15T09:40:00.000Z", + severity: "warning", }, ], }) + expect(response.thresholds).toEqual([ + { severity: "warning", value: 0.05, upper: null, recoveryValue: 0.03, destinationIds: [] }, + ]) expect(response.bucketSeconds).toBe(300) expect(response.windowMinutes).toBe(5) expect(response.truncatedToStart).toBe("2026-07-15T00:00:00.000Z") @@ -319,6 +333,7 @@ describe("v2 response mappers", () => { groupKey: "__total__", start: "2026-07-15T09:10:00.000Z", end: "2026-07-15T09:40:00.000Z", + severity: "warning", }) }) @@ -342,6 +357,7 @@ describe("v2 response mappers", () => { consecutive_healthy: 0, incident_id: null, incident_transition: "opened", + level: "warning", evaluation_duration_ms: 412, error_message: null, error_category: null, diff --git a/apps/web/src/lib/alerts/form-utils.ts b/apps/web/src/lib/alerts/form-utils.ts index 87974d5394..9aa44b0211 100644 --- a/apps/web/src/lib/alerts/form-utils.ts +++ b/apps/web/src/lib/alerts/form-utils.ts @@ -23,6 +23,7 @@ import { type AlertSignalType, type ChatWorkspaceId, type QueryBuilderQueryDraftPayload, + MAX_ALERT_BREACH_WINDOW, } from "@maple/domain/http" import type { V2AlertCheck, @@ -32,9 +33,11 @@ import type { V2AlertRuleCreateParams, V2AlertRulePreviewResult, V2AlertRuleTestParams, + V2AlertThreshold, } from "@maple/domain/http/v2" +import { fromV2Threshold } from "@maple/domain/http/v2" import type { QueryEngineAlertReducer } from "@maple/query-engine" -import { Schema } from "effect" +import { Array as Arr, Schema } from "effect" import { buildTimeseriesQuerySpec, createQueryDraft, @@ -79,11 +82,17 @@ export type RuleFormState = { comparator: AlertComparator threshold: string thresholdUpper: string + /** Where the signal must get back to before the incident clears; "" clears once it stops breaching. */ + recoveryValue: string + /** The critical tier over a warning one; null on a one-tier rule. */ + escalation: EscalationTierForm | null windowMinutes: string minimumSampleCount: string /** Count an empty window as a breach instead of skipping it. */ alertOnNoData: boolean consecutiveBreachesRequired: string + /** Fire on N breaches out of the last M checks; "" counts N in a row. */ + breachesOutOf: string consecutiveHealthyRequired: string renotifyIntervalMinutes: string apdexThresholdMs: string @@ -105,6 +114,13 @@ export type RuleFormState = { notificationBody: string } +/** The critical tier of a two-tier rule. Range and equality comparators have one tier only. */ +export interface EscalationTierForm { + threshold: string + recoveryValue: string + destinationIds: AlertDestinationId[] +} + export const signalLabels: Record = { error_rate: "Error rate", p95_latency: "P95 latency", @@ -211,7 +227,9 @@ export function thresholdUnitLabel(subject: ThresholdSubject): string | null { */ export function formThresholdToDomain(subject: ThresholdSubject, value: string): number { const n = Number(value) - return isPercentSignal(subject) ? n / 100 : n + // Same rounding as `domainThresholdToForm`: 0.7 / 100 is 0.006999999999999999, which + // the API reads as an edit of the stored 0.007 and rejects on a tiered rule. + return isPercentSignal(subject) ? Number((n / 100).toPrecision(12)) : n } export function domainThresholdToForm(subject: ThresholdSubject, value: number): string { @@ -282,10 +300,13 @@ export function defaultRuleForm(serviceName?: string): RuleFormState { comparator: "gt", threshold: "5", thresholdUpper: "", + recoveryValue: "", + escalation: null, windowMinutes: "5", minimumSampleCount: "50", alertOnNoData: false, consecutiveBreachesRequired: "2", + breachesOutOf: "", consecutiveHealthyRequired: "2", renotifyIntervalMinutes: "30", apdexThresholdMs: "500", @@ -310,11 +331,14 @@ export function ruleFormIsGrouped(form: RuleFormState): boolean { export function ruleToFormState(rule: AlertRuleDocument): RuleFormState { const queryBuilderDraft = normalizeRuleQueryDraft(rule.queryBuilderDraft) const unitSubject = { signalType: rule.signalType, queryBuilderDraft } + const toForm = (value: number | null) => (value === null ? "" : domainThresholdToForm(unitSubject, value)) + // Tiers arrive lowest severity first: the first is the form's main row, a second is the escalation. + const [primary, escalation] = rule.thresholds return { name: rule.name, notes: rule.notes ?? "", enabled: rule.enabled, - severity: rule.severity, + severity: primary.severity, serviceNames: rule.serviceNames?.length > 0 ? [...rule.serviceNames] : [], excludeServiceNames: rule.excludeServiceNames?.length > 0 ? [...rule.excludeServiceNames] : [], environments: rule.environments?.length > 0 ? [...rule.environments] : [], @@ -322,20 +346,29 @@ export function ruleToFormState(rule: AlertRuleDocument): RuleFormState { groupBy: rule.groupBy ? [...rule.groupBy] : [], signalType: rule.signalType, comparator: rule.comparator, - threshold: domainThresholdToForm(unitSubject, rule.threshold), - thresholdUpper: - rule.thresholdUpper == null ? "" : domainThresholdToForm(unitSubject, rule.thresholdUpper), + threshold: toForm(primary.value), + thresholdUpper: toForm(primary.upper), + recoveryValue: toForm(primary.recoveryValue), + escalation: + escalation === undefined + ? null + : { + threshold: toForm(escalation.value), + recoveryValue: toForm(escalation.recoveryValue), + destinationIds: [...escalation.destinationIds], + }, windowMinutes: String(rule.windowMinutes), minimumSampleCount: String(rule.minimumSampleCount), alertOnNoData: rule.noDataBehavior === "alert", consecutiveBreachesRequired: String(rule.consecutiveBreachesRequired), + breachesOutOf: rule.breachesOutOf === null ? "" : String(rule.breachesOutOf), consecutiveHealthyRequired: String(rule.consecutiveHealthyRequired), renotifyIntervalMinutes: String(rule.renotifyIntervalMinutes), apdexThresholdMs: rule.apdexThresholdMs == null ? "500" : String(rule.apdexThresholdMs), queryBuilderDraft, rawQuerySql: rule.rawQuerySql ?? DEFAULT_RAW_QUERY_SQL, rawQueryReducer: rule.rawQueryReducer ?? "identity", - destinationIds: [...rule.destinationIds], + destinationIds: [...primary.destinationIds], notificationTitle: rule.notificationTemplate?.title ?? "", notificationBody: rule.notificationTemplate?.body ?? "", } @@ -369,6 +402,104 @@ export function deriveRuleQueryIssues(form: RuleFormState): string[] { return issues } +/** Whether the comparator takes a critical tier and recovery values: range and equality ones do not. */ +export function comparatorSupportsTiers(comparator: AlertComparator): boolean { + return !isRangeComparator(comparator) && comparator !== "eq" && comparator !== "neq" +} + +/** A form value that may be left empty, in domain units; null when empty or not a number. */ +export function optionalFormThreshold(subject: ThresholdSubject, value: string): number | null { + return value.trim().length > 0 && Number.isFinite(Number(value)) + ? formThresholdToDomain(subject, value) + : null +} + +/** Whether `value` breaches `threshold`, for the one-sided comparators that carry tiers. */ +function breachesOneSided(value: number, comparator: AlertComparator, threshold: number): boolean { + return comparator === "gt" + ? value > threshold + : comparator === "gte" + ? value >= threshold + : comparator === "lt" + ? value < threshold + : value <= threshold +} + +/** `Number("")` is 0, so an empty field would otherwise pass as a value of zero. */ +export const isFilledNumber = (value: string): boolean => + value.trim().length > 0 && Number.isFinite(Number(value)) + +/** + * The tier and breach-window problems the server would reject, named for the form. Mirrors + * `validateThresholds` / `validateBreachWindow` in the backend. + */ +export function deriveTierIssues(form: RuleFormState): string[] { + const issues: string[] = [] + const tiered = comparatorSupportsTiers(form.comparator) + // Tier ordering is strict even for inclusive comparators: equal tiers would always fire together. + const strict = form.comparator === "gte" ? "gt" : form.comparator === "lte" ? "lt" : form.comparator + const recoveryIssue = (label: string, threshold: string, recovery: string) => { + if (recovery.trim().length === 0 || !isFilledNumber(threshold)) return + if ( + !isFilledNumber(recovery) || + breachesOneSided(Number(recovery), form.comparator, Number(threshold)) + ) { + issues.push(label) + } + } + if (tiered) recoveryIssue("Recovery value", form.threshold, form.recoveryValue) + const escalation = tiered ? form.escalation : null + if (escalation !== null) { + if (!isFilledNumber(escalation.threshold)) { + issues.push("Critical threshold") + } else if ( + isFilledNumber(form.threshold) && + !breachesOneSided(Number(escalation.threshold), strict, Number(form.threshold)) + ) { + issues.push("Critical threshold past warning") + } else if ( + isFilledNumber(escalation.recoveryValue) && + breachesOneSided(Number(form.threshold), strict, Number(escalation.recoveryValue)) + ) { + issues.push("Critical recovery past warning") + } + recoveryIssue("Critical recovery value", escalation.threshold, escalation.recoveryValue) + if (escalation.destinationIds.length === 0) issues.push("Critical destinations") + } + if (form.breachesOutOf.trim().length > 0) { + const outOf = Number(form.breachesOutOf) + const required = Number(form.consecutiveBreachesRequired) + if (!Number.isInteger(outOf) || outOf < required || outOf > MAX_ALERT_BREACH_WINDOW) { + issues.push("Breach window") + } + } + return issues +} + +/** The tiers the form saves, lowest severity first, in domain units. */ +export function formThresholds(form: RuleFormState): Arr.NonEmptyReadonlyArray { + const tiered = comparatorSupportsTiers(form.comparator) + const escalation = tiered ? form.escalation : null + const primary: V2AlertThreshold = { + severity: escalation === null ? form.severity : "warning", + value: formThresholdToDomain(form, form.threshold), + upper: isRangeComparator(form.comparator) ? optionalFormThreshold(form, form.thresholdUpper) : null, + recovery_value: tiered ? optionalFormThreshold(form, form.recoveryValue) : null, + destination_ids: [...new Set(form.destinationIds)], + } + if (escalation === null) return [primary] + return [ + primary, + { + severity: "critical", + value: formThresholdToDomain(form, escalation.threshold), + upper: null, + recovery_value: optionalFormThreshold(form, escalation.recoveryValue), + destination_ids: [...new Set(escalation.destinationIds)], + }, + ] +} + /** * Assemble the v2 `POST /v2/alerts/rules` body from the form. Field names are * the v2 snake_case wire spelling; IDs stay the internal branded values — the @@ -388,11 +519,16 @@ export function buildRuleCreateParamsV2(form: RuleFormState): V2AlertRuleCreateP ...(notificationBody.length > 0 ? { body: notificationBody } : undefined), } : null + // `thresholds` is what the server stores; the single-threshold fields mirror it the way the + // server reports them, for the change list and preview identity that compare payloads. + const thresholds = formThresholds(form) + const primary = Arr.headNonEmpty(thresholds) return { name: form.name.trim(), notes: form.notes.trim() || null, enabled: form.enabled, - severity: form.severity, + thresholds, + severity: Arr.lastNonEmpty(thresholds).severity, tags: form.tags, service_names: queryOwnsScope ? [] : form.serviceNames.filter((s) => s.trim().length > 0), exclude_service_names: queryOwnsScope @@ -402,16 +538,13 @@ export function buildRuleCreateParamsV2(form: RuleFormState): V2AlertRuleCreateP group_by: queryOwnsScope ? null : form.groupBy.length > 0 ? form.groupBy : null, signal_type: signalType, comparator: form.comparator, - threshold: formThresholdToDomain(form, form.threshold), - threshold_upper: isRangeComparator(form.comparator) - ? Number.isFinite(Number(form.thresholdUpper)) - ? formThresholdToDomain(form, form.thresholdUpper) - : null - : null, + threshold: primary.value, + threshold_upper: primary.upper, window_minutes: parsePositiveNumber(form.windowMinutes, 5), minimum_sample_count: parseNonNegativeNumber(form.minimumSampleCount, 0), alert_on_no_data: form.alertOnNoData && !ruleFormIsGrouped(form), consecutive_breaches_required: parsePositiveNumber(form.consecutiveBreachesRequired, 2), + breaches_out_of: form.breachesOutOf.trim().length === 0 ? null : Number(form.breachesOutOf), consecutive_healthy_required: parsePositiveNumber(form.consecutiveHealthyRequired, 2), renotify_interval_minutes: parsePositiveNumber(form.renotifyIntervalMinutes, 30), apdex_threshold_ms: signalType === "apdex" ? parsePositiveNumber(form.apdexThresholdMs, 500) : null, @@ -421,10 +554,7 @@ export function buildRuleCreateParamsV2(form: RuleFormState): V2AlertRuleCreateP : null, raw_query_sql: signalType === "raw_query" ? form.rawQuerySql.trim() || null : null, raw_query_reducer: signalType === "raw_query" ? form.rawQueryReducer : null, - // Dedupe so the same destination is never persisted twice (e.g. when editing a - // rule that already had duplicates). The server is authoritative (normalizeRule), - // but keeping the request clean avoids a needless write-then-normalize round trip. - destination_ids: [...new Set(form.destinationIds)], + destination_ids: Arr.dedupe(thresholds.flatMap((tier) => tier.destination_ids)), notification_template: notificationTemplate, } } @@ -438,10 +568,9 @@ export function buildRuleTestParamsV2(form: RuleFormState, sendNotification: boo export function isRulePreviewReady(form: RuleFormState): boolean { if (form.name.trim().length === 0) return false - if (!Number.isFinite(Number(form.threshold))) return false - if (isRangeComparator(form.comparator) && !Number.isFinite(Number(form.thresholdUpper))) { - return false - } + if (!isFilledNumber(form.threshold)) return false + if (isRangeComparator(form.comparator) && !isFilledNumber(form.thresholdUpper)) return false + if (deriveTierIssues(form).some((issue) => issue !== "Critical destinations")) return false if (form.signalType === "builder_query") return deriveRuleQueryIssues(form).length === 0 if (form.signalType === "raw_query") { // The alert workload additionally requires $__timeFilter, and the shared @@ -765,6 +894,7 @@ export function v2PreviewToResponse(result: V2AlertRulePreviewResult): AlertRule windowMinutes: result.window_minutes, threshold: result.threshold, thresholdUpper: result.threshold_upper, + thresholds: Arr.map(result.thresholds, fromV2Threshold), comparator: result.comparator, truncatedToStart: asIsoOrNull(result.truncated_to_start), series: result.series.map( @@ -784,6 +914,7 @@ export function v2PreviewToResponse(result: V2AlertRulePreviewResult): AlertRule ...(point.provisional !== undefined ? { provisional: point.provisional } : undefined), + ...(point.level !== undefined ? { level: point.level } : undefined), }), ), }), @@ -794,6 +925,7 @@ export function v2PreviewToResponse(result: V2AlertRulePreviewResult): AlertRule groupKey: span.group_key, start: asIso(span.start), end: asIso(span.end), + severity: span.severity, }), ), }) @@ -818,6 +950,7 @@ export function v2CheckToDocument(check: V2AlertCheck): AlertCheckDocument { consecutiveHealthy: check.consecutive_healthy, incidentId: check.incident_id, incidentTransition: check.incident_transition, + level: check.level, evaluationDurationMs: check.evaluation_duration_ms, errorMessage: check.error_message, errorCategory: check.error_category, diff --git a/apps/web/src/lib/alerts/headroom.test.ts b/apps/web/src/lib/alerts/headroom.test.ts index b1b15ed640..1769c6e988 100644 --- a/apps/web/src/lib/alerts/headroom.test.ts +++ b/apps/web/src/lib/alerts/headroom.test.ts @@ -33,6 +33,10 @@ function rule(id: string, overrides: Record = {}): AlertRuleDoc minimumSampleCount: 0, noDataBehavior: "skip", consecutiveBreachesRequired: 2, + breachesOutOf: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 30, apdexThresholdMs: null, @@ -110,6 +114,7 @@ describe("quietForMs", () => { comparator: "gt", threshold: 0.05, thresholdUpper: null, + peakSeverity: null, firstTriggeredAt: "2026-10-15T20:00:00.000Z", lastTriggeredAt: "2026-10-15T20:10:00.000Z", resolvedAt, diff --git a/apps/web/src/lib/alerts/incident-timeline.test.ts b/apps/web/src/lib/alerts/incident-timeline.test.ts index 71a3823541..bc5e5d1815 100644 --- a/apps/web/src/lib/alerts/incident-timeline.test.ts +++ b/apps/web/src/lib/alerts/incident-timeline.test.ts @@ -21,6 +21,7 @@ function makeIncident(overrides: Record = {}): AlertIncidentDoc comparator: "gt", threshold: 0.05, thresholdUpper: null, + peakSeverity: null, firstTriggeredAt: "2026-10-15T14:14:00.000Z", lastTriggeredAt: "2026-10-15T14:30:00.000Z", resolvedAt: null, diff --git a/apps/web/src/lib/alerts/overview-derive.test.ts b/apps/web/src/lib/alerts/overview-derive.test.ts index 61a5a6da27..98e9662c58 100644 --- a/apps/web/src/lib/alerts/overview-derive.test.ts +++ b/apps/web/src/lib/alerts/overview-derive.test.ts @@ -3,7 +3,14 @@ import { Schema } from "effect" import { describe, expect, it } from "vitest" import { defaultRuleForm } from "./form-utils" -import { blindSpots, fireLanes, ruleBucket, ruleConditionSummary, ruleHeadroom, ruleRibbon } from "./overview-derive" +import { + blindSpots, + fireLanes, + ruleBucket, + ruleConditionSummary, + ruleHeadroom, + ruleRibbon, +} from "./overview-derive" import { builtinToQueryDraft } from "./rule-modes" import type { DerivedRuleStatus } from "./rule-status" @@ -35,6 +42,10 @@ function makeRule(overrides: Record = {}): AlertRuleDocument { windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 1, + breachesOutOf: null, + thresholds: [ + { severity: "critical", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 1, renotifyIntervalMinutes: 30, apdexThresholdMs: null, @@ -66,6 +77,7 @@ function makeIncident(overrides: Record = {}): AlertIncidentDoc comparator: "gt", threshold: 0.05, thresholdUpper: null, + peakSeverity: null, firstTriggeredAt: iso(6 * HOUR), lastTriggeredAt: iso(5 * HOUR), resolvedAt: iso(4 * HOUR), diff --git a/apps/web/src/lib/alerts/preview-insights.test.ts b/apps/web/src/lib/alerts/preview-insights.test.ts index 3844c5145b..7f7f53f510 100644 --- a/apps/web/src/lib/alerts/preview-insights.test.ts +++ b/apps/web/src/lib/alerts/preview-insights.test.ts @@ -28,6 +28,9 @@ function preview( windowMinutes: 5, threshold: 0.05, thresholdUpper: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], comparator: "gt", truncatedToStart: null, series: Object.entries(series).map(([groupKey, points]) => ({ @@ -44,6 +47,7 @@ function preview( groupKey: span.groupKey, start: at(span.start), end: at(span.end), + severity: "warning", })), }) } @@ -60,6 +64,7 @@ function incident(groupKey: string | null, status = "open"): AlertIncidentDocume comparator: "gt", threshold: 0.05, thresholdUpper: null, + peakSeverity: null, firstTriggeredAt: at(0), lastTriggeredAt: at(10), resolvedAt: null, diff --git a/apps/web/src/lib/alerts/preview-insights.ts b/apps/web/src/lib/alerts/preview-insights.ts index 11415c6c97..843c378e83 100644 --- a/apps/web/src/lib/alerts/preview-insights.ts +++ b/apps/web/src/lib/alerts/preview-insights.ts @@ -9,10 +9,12 @@ import { import { buildRuleCreateParamsV2, comparatorLabels, + comparatorSupportsTiers, displaySignalType, formThresholdToDomain, formatSignalValue, isRangeComparator, + optionalFormThreshold, RAW_QUERY_REDUCER_LABELS, signalLabels, type RuleFormState, @@ -144,6 +146,8 @@ export function diffRuleForms(saved: RuleFormState, draft: RuleFormState): RuleF const b = buildRuleCreateParamsV2(draft) const threshold = (form: RuleFormState, value: number | null | undefined) => value == null ? "none" : formatSignalValue(displaySignalType(form), value) + // The escalation tier only: a one-tier critical rule reports through the plain threshold rows. + const critical = (params: typeof a) => params.thresholds?.[1] const list = (values: readonly string[] | null | undefined) => values == null || values.length === 0 ? "all" : values.join(", ") const query = (form: RuleFormState) => @@ -199,6 +203,24 @@ export function diffRuleForms(saved: RuleFormState, draft: RuleFormState): RuleF after: threshold(draft, b.threshold), same: a.threshold === b.threshold, }, + { + field: "Critical threshold", + before: threshold(saved, critical(a)?.value), + after: threshold(draft, critical(b)?.value), + same: critical(a)?.value === critical(b)?.value, + }, + { + field: "Recovery", + before: threshold(saved, a.thresholds?.[0]?.recovery_value), + after: threshold(draft, b.thresholds?.[0]?.recovery_value), + same: a.thresholds?.[0]?.recovery_value === b.thresholds?.[0]?.recovery_value, + }, + { + field: "Critical recovery", + before: threshold(saved, critical(a)?.recovery_value), + after: threshold(draft, critical(b)?.recovery_value), + same: critical(a)?.recovery_value === critical(b)?.recovery_value, + }, { field: "Upper threshold", before: threshold(saved, a.threshold_upper), @@ -219,6 +241,12 @@ export function diffRuleForms(saved: RuleFormState, draft: RuleFormState): RuleF after: String(b.consecutive_breaches_required), same: a.consecutive_breaches_required === b.consecutive_breaches_required, }, + { + field: "Out of last", + before: a.breaches_out_of == null ? "in a row" : `${a.breaches_out_of} checks`, + after: b.breaches_out_of == null ? "in a row" : `${b.breaches_out_of} checks`, + same: a.breaches_out_of === b.breaches_out_of, + }, { field: "Checks to resolve", before: String(a.consecutive_healthy_required), @@ -243,7 +271,12 @@ export function diffRuleForms(saved: RuleFormState, draft: RuleFormState): RuleF after: `${b.apdex_threshold_ms ?? "none"}`, same: a.apdex_threshold_ms === b.apdex_threshold_ms, }, - { field: "Severity", before: a.severity, after: b.severity, same: a.severity === b.severity }, + { + field: "Severity", + before: a.severity ?? "", + after: b.severity ?? "", + same: a.severity === b.severity, + }, { field: "Renotify", before: `${a.renotify_interval_minutes}m`, @@ -252,9 +285,18 @@ export function diffRuleForms(saved: RuleFormState, draft: RuleFormState): RuleF }, { field: "Destinations", - before: String(a.destination_ids.length), - after: String(b.destination_ids.length), - same: [...a.destination_ids].sort().join() === [...b.destination_ids].sort().join(), + before: String(a.destination_ids?.length ?? 0), + after: String(b.destination_ids?.length ?? 0), + same: + [...(a.destination_ids ?? [])].sort().join() === [...(b.destination_ids ?? [])].sort().join(), + }, + { + field: "Critical destinations", + before: String(critical(a)?.destination_ids.length ?? 0), + after: String(critical(b)?.destination_ids.length ?? 0), + same: + [...(critical(a)?.destination_ids ?? [])].sort().join() === + [...(critical(b)?.destination_ids ?? [])].sort().join(), }, { field: "Name", before: a.name, after: b.name, same: a.name === b.name }, { @@ -286,11 +328,17 @@ export function diffRuleForms(saved: RuleFormState, draft: RuleFormState): RuleF } /** The draft's threshold as a domain value, for the chart and the verdict. */ -export function draftThresholds(form: RuleFormState): { threshold: number; upper: number | null } { +export function draftThresholds(form: RuleFormState): { + threshold: number + upper: number | null + critical: number | null +} { const threshold = formThresholdToDomain(form, form.threshold) const upper = form.thresholdUpper.trim().length > 0 && Number.isFinite(Number(form.thresholdUpper)) ? formThresholdToDomain(form, form.thresholdUpper) : null - return { threshold: Number.isFinite(threshold) ? threshold : 0, upper } + const escalation = comparatorSupportsTiers(form.comparator) ? form.escalation : null + const critical = escalation === null ? null : optionalFormThreshold(form, escalation.threshold) + return { threshold: Number.isFinite(threshold) ? threshold : 0, upper, critical } } diff --git a/apps/web/src/lib/alerts/rule-status.test.ts b/apps/web/src/lib/alerts/rule-status.test.ts index 96acfbb969..8695a0cc9a 100644 --- a/apps/web/src/lib/alerts/rule-status.test.ts +++ b/apps/web/src/lib/alerts/rule-status.test.ts @@ -29,6 +29,10 @@ function makeRule(overrides: Record = {}): AlertRuleDocument { windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 2, + breachesOutOf: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 30, apdexThresholdMs: null, diff --git a/apps/web/src/lib/alerts/tag-grouping.ts b/apps/web/src/lib/alerts/tag-grouping.ts index d86a33b047..b79c233231 100644 --- a/apps/web/src/lib/alerts/tag-grouping.ts +++ b/apps/web/src/lib/alerts/tag-grouping.ts @@ -1,8 +1,6 @@ /** - * Tag filtering + grouping for the alerts list. Shared by the Rules tab (over - * alert rules) and the Monitor tab (over incidents joined to their rule's tags), - * so the two surfaces stay consistent. All functions are generic over the item - * type via a `getTags` accessor. + * Tag grouping for the alerts Rules tab, generic over the item type via a + * `getTags` accessor. */ /** Stable key for the bucket holding items with no tags. */ diff --git a/apps/web/src/lib/collections/alerts.test.ts b/apps/web/src/lib/collections/alerts.test.ts index 95dce9221e..2c1e46d756 100644 --- a/apps/web/src/lib/collections/alerts.test.ts +++ b/apps/web/src/lib/collections/alerts.test.ts @@ -27,6 +27,7 @@ import { const RULE_ID = "99999999-8888-4777-8666-555544443333" const INCIDENT_ID = "aaaa1111-2222-4333-8444-555566667777" const DEST_ID = "abababab-cdcd-4efe-8aba-121212121212" +const PAGER_ID = "cdcdcdcd-abab-4efe-8aba-343434343434" const ruleRow: AlertRuleRow = { id: RULE_ID, @@ -146,6 +147,51 @@ describe("rowToAlertRuleDocument", () => { assert.strictEqual(doc.signalType, "raw_query") assert.strictEqual(doc.rawQueryReducer, "sum") }) + + it("reads a row written before severity tiers as one tier from its legacy columns", () => { + const doc = rowToAlertRuleDocument(ruleRow, new Map()) + assert.deepStrictEqual(doc.thresholds, [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [DEST_ID] }, + ]) + assert.strictEqual(doc.breachesOutOf, null) + }) + + it("reads stored tiers lowest first and derives the single-threshold fields from them", () => { + const doc = rowToAlertRuleDocument( + { + ...ruleRow, + thresholds_json: [ + { + severity: "critical", + value: 0.2, + upper: null, + recoveryValue: 0.1, + destinationIds: [PAGER_ID], + }, + { + severity: "warning", + value: 0.05, + upper: null, + recoveryValue: null, + destinationIds: [DEST_ID], + }, + ], + breaches_out_of: 5, + }, + new Map(), + ) + assert.deepStrictEqual( + doc.thresholds.map((tier) => [tier.severity, tier.value, tier.recoveryValue]), + [ + ["warning", 0.05, null], + ["critical", 0.2, 0.1], + ], + ) + assert.strictEqual(doc.severity, "critical") + assert.strictEqual(doc.threshold, 0.05) + assert.deepStrictEqual([...doc.destinationIds], [DEST_ID, PAGER_ID]) + assert.strictEqual(doc.breachesOutOf, 5) + }) }) describe("rowToAlertIncidentDocument", () => { diff --git a/apps/web/src/lib/collections/alerts.ts b/apps/web/src/lib/collections/alerts.ts index 9f980ed784..4552f4485f 100644 --- a/apps/web/src/lib/collections/alerts.ts +++ b/apps/web/src/lib/collections/alerts.ts @@ -13,15 +13,19 @@ import { AlertRuleId, AlertSeverity, AlertSignalType, + AlertThresholds, ChatConnectorId, ChatWorkspaceId, ErrorIssueId, IsoDateTimeString, QueryBuilderQueryDraftSchema, UserId, + alertThresholdLegacyFields, + legacyAlertThreshold, + sortAlertThresholds, } from "@maple/domain/http" import { QueryEngineAlertReducer, QueryEngineNoDataBehavior } from "@maple/domain/query-engine" -import { Option, Schema } from "effect" +import { Array as Arr, Option, Schema } from "effect" import { createSyncedCollection, timestamptzParser } from "./shape-fetch" const decodeIso = Schema.decodeUnknownSync(IsoDateTimeString) @@ -45,6 +49,7 @@ const decodeNoDataBehavior = Schema.decodeUnknownOption(QueryEngineNoDataBehavio const decodeNotificationTemplate = Schema.decodeUnknownOption(AlertNotificationTemplate) const decodeQueryBuilderDraft = Schema.decodeUnknownOption(QueryBuilderQueryDraftSchema) +const decodeThresholds = Schema.decodeUnknownOption(AlertThresholds) const decodeStringArray = Schema.decodeUnknownOption(Schema.Array(Schema.String)) // The stored `group_by` column is a JSON-encoded string array (mirrors the // server's `Schema.fromJsonString(AlertGroupBy)`). @@ -82,9 +87,12 @@ export const AlertRuleRowSchema = Schema.Struct({ comparator: Schema.String, threshold: Schema.Number, threshold_upper: Schema.NullOr(Schema.Number), + // Optional: rows synced before the column existed omit it. + thresholds_json: Schema.optionalKey(Schema.NullOr(Schema.Unknown)), window_minutes: Schema.Number, minimum_sample_count: Schema.Number, consecutive_breaches_required: Schema.Number, + breaches_out_of: Schema.optionalKey(Schema.NullOr(Schema.Number)), consecutive_healthy_required: Schema.Number, renotify_interval_minutes: Schema.Number, apdex_threshold_ms: Schema.NullOr(Schema.Number), @@ -112,6 +120,7 @@ export const AlertRuleStateRowSchema = Schema.Struct({ group_key: Schema.String, consecutive_breaches: Schema.Number, consecutive_healthy: Schema.Number, + history_json: Schema.optionalKey(Schema.NullOr(Schema.Unknown)), last_status: Schema.NullOr(Schema.String), last_value: Schema.NullOr(Schema.Number), last_sample_count: Schema.NullOr(Schema.Number), @@ -132,6 +141,7 @@ export const AlertIncidentRowSchema = Schema.Struct({ group_key: Schema.NullOr(Schema.String), signal_type: Schema.String, severity: Schema.String, + peak_severity: Schema.optionalKey(Schema.NullOr(Schema.String)), status: Schema.String, comparator: Schema.String, threshold: Schema.Number, @@ -185,6 +195,18 @@ export const rowToAlertRuleDocument = ( statesByRuleId: Map, ): AlertRuleDocument => { const state = statesByRuleId.get(row.id) + const destinationIds = safeParseStringArray(row.destination_ids_json).map((id) => decodeDestinationId(id)) + // Rows written before tiers carry none: they are one tier from the legacy columns. + const legacyTier = legacyAlertThreshold({ + severity: asSeverity(row.severity), + threshold: row.threshold, + thresholdUpper: row.threshold_upper, + destinationIds, + }) + const thresholds = sortAlertThresholds( + Option.getOrElse(decodeThresholds(row.thresholds_json), () => Arr.of(legacyTier)), + ) + const legacy = alertThresholdLegacyFields(thresholds) return new AlertRuleDocument({ id: asAlertRuleId(row.id), name: row.name, @@ -194,7 +216,8 @@ export const rowToAlertRuleDocument = ( ? null : Option.getOrElse(decodeNotificationTemplate(row.notification_template_json), () => null), enabled: row.enabled, - severity: asSeverity(row.severity), + severity: legacy.severity, + thresholds, serviceNames: [...safeParseStringArray(row.service_names_json)], excludeServiceNames: [...safeParseStringArray(row.exclude_service_names_json)], environments: [...safeParseStringArray(row.environments_json)], @@ -202,11 +225,12 @@ export const rowToAlertRuleDocument = ( groupBy: row.group_by == null ? null : decodeGroupByFromJson(row.group_by), signalType: asSignalType(row.signal_type), comparator: asComparator(row.comparator), - threshold: row.threshold, - thresholdUpper: row.threshold_upper, + threshold: legacy.threshold, + thresholdUpper: legacy.thresholdUpper, windowMinutes: row.window_minutes, minimumSampleCount: row.minimum_sample_count, consecutiveBreachesRequired: row.consecutive_breaches_required, + breachesOutOf: row.breaches_out_of ?? null, consecutiveHealthyRequired: row.consecutive_healthy_required, renotifyIntervalMinutes: row.renotify_interval_minutes, apdexThresholdMs: row.apdex_threshold_ms, @@ -216,7 +240,7 @@ export const rowToAlertRuleDocument = ( : Option.getOrElse(decodeQueryBuilderDraft(row.query_builder_draft_json), () => null), rawQuerySql: row.raw_query_sql ?? null, rawQueryReducer: row.signal_type === "raw_query" ? asReducer(row.reducer) : null, - destinationIds: safeParseStringArray(row.destination_ids_json).map((id) => decodeDestinationId(id)), + destinationIds: legacy.destinationIds, noDataBehavior: Option.getOrElse(decodeNoDataBehavior(row.no_data_behavior), () => "skip" as const), lastEvaluationError: state?.last_error ?? null, lastEvaluatedAt: state?.last_evaluated_at != null ? decodeIso(state.last_evaluated_at) : null, @@ -240,6 +264,7 @@ export const rowToAlertIncidentDocument = (row: AlertIncidentRow): AlertIncident groupKey: row.group_key, signalType: asSignalType(row.signal_type), severity: asSeverity(row.severity), + peakSeverity: row.peak_severity == null ? null : asSeverity(row.peak_severity), status: asIncidentStatus(row.status), comparator: asComparator(row.comparator), threshold: row.threshold, diff --git a/apps/web/src/lib/models/alerts-overview-model.registry.test.ts b/apps/web/src/lib/models/alerts-overview-model.registry.test.ts index 0f1ba3f4e2..01c77b5af4 100644 --- a/apps/web/src/lib/models/alerts-overview-model.registry.test.ts +++ b/apps/web/src/lib/models/alerts-overview-model.registry.test.ts @@ -55,6 +55,10 @@ const makeRule = (overrides: Record = {}): AlertRuleDocument => windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 2, + breachesOutOf: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 30, apdexThresholdMs: null, diff --git a/apps/web/src/lib/models/alerts-overview-model.test.ts b/apps/web/src/lib/models/alerts-overview-model.test.ts index be3f34691b..0c1f32190f 100644 --- a/apps/web/src/lib/models/alerts-overview-model.test.ts +++ b/apps/web/src/lib/models/alerts-overview-model.test.ts @@ -34,6 +34,10 @@ function makeRule(overrides: Record = {}): AlertRuleDocument { windowMinutes: 5, minimumSampleCount: 0, consecutiveBreachesRequired: 2, + breachesOutOf: null, + thresholds: [ + { severity: "warning", value: 0.05, upper: null, recoveryValue: null, destinationIds: [] }, + ], consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 30, apdexThresholdMs: null, @@ -65,6 +69,7 @@ function makeIncident(overrides: Record = {}): AlertIncidentDoc comparator: "gt", threshold: 0.05, thresholdUpper: null, + peakSeverity: null, firstTriggeredAt: iso(10 * 60_000), lastTriggeredAt: iso(60_000), resolvedAt: null, @@ -161,6 +166,7 @@ describe("deriveOverview", () => { makeIncident({ id: "00000000-0000-4000-8000-0000000000f2", lastTriggeredAt: iso(45 * DAY_MS), + peakSeverity: null, firstTriggeredAt: iso(46 * DAY_MS), }), // Ancient and resolved — dropped before any sorting/grouping. @@ -168,6 +174,7 @@ describe("deriveOverview", () => { id: "00000000-0000-4000-8000-0000000000f3", status: "resolved", lastTriggeredAt: iso(31 * DAY_MS), + peakSeverity: null, firstTriggeredAt: iso(32 * DAY_MS), resolvedAt: iso(31 * DAY_MS), }), diff --git a/apps/web/src/routes/alerts/$ruleId.tsx b/apps/web/src/routes/alerts/$ruleId.tsx index dd5f85c7a6..beb7cb2adc 100644 --- a/apps/web/src/routes/alerts/$ruleId.tsx +++ b/apps/web/src/routes/alerts/$ruleId.tsx @@ -264,6 +264,7 @@ function RuleDetailContent() { consecutiveHealthy: 0, incidentId: null, incidentTransition: point.transitionCount > 0 ? "continued" : "none", + level: null, evaluationDurationMs: 0, errorMessage: null, errorCategory: null, @@ -514,7 +515,9 @@ function RuleDetailContent() { title={rule.name} meta={ <> - + {rule.thresholds.map((tier) => ( + + ))} {isFiring ? ( )} - - - {comparatorLabels[rule.comparator]}{" "} - {formatSignalValue(rule.signalType, rule.threshold)} /{" "} - {rule.windowMinutes}min - - - - - - + {rule.thresholds.map((tier) => ( + + + {tierCondition(rule, tier)} / {rule.windowMinutes}min + + + ))} + - {rule.consecutiveBreachesRequired} + {rule.breachesOutOf === null + ? `${rule.consecutiveBreachesRequired} in a row` + : `${rule.consecutiveBreachesRequired} of last ${rule.breachesOutOf}`} @@ -1071,8 +1079,7 @@ function RuleDetailContent() { ) } > - + Open investigation 2.00% (recovers < 1.50%)". */ +function tierCondition(rule: AlertRuleDocument, tier: AlertRuleDocument["thresholds"][number]): string { + const value = (n: number) => formatSignalValue(rule.signalType, n) + const bound = tier.upper === null ? value(tier.value) : `${value(tier.value)} – ${value(tier.upper)}` + const healthy = rule.comparator === "gt" || rule.comparator === "gte" ? "<" : ">" + const recovery = tier.recoveryValue === null ? "" : ` (recovers ${healthy} ${value(tier.recoveryValue)})` + return `${comparatorLabels[rule.comparator]} ${bound}${recovery}` +} + function ruleSubtitle(rule: AlertRuleDocument): string { - return `${signalLabels[rule.signalType]} ${comparatorLabels[rule.comparator]} ${formatSignalValue(rule.signalType, rule.threshold)} over ${rule.windowMinutes}min${rule.serviceNames?.length > 0 ? ` on ${rule.serviceNames.join(", ")}` : ""}${rule.environments?.length > 0 ? ` in ${rule.environments.join(", ")}` : ""}${rule.excludeServiceNames?.length > 0 ? ` (excl. ${rule.excludeServiceNames.join(", ")})` : ""}` + const tiers = rule.thresholds + .map((tier) => + rule.thresholds.length > 1 + ? `${tier.severity} ${formatSignalValue(rule.signalType, tier.value)}` + : formatSignalValue(rule.signalType, tier.value), + ) + .join(", ") + return `${signalLabels[rule.signalType]} ${comparatorLabels[rule.comparator]} ${tiers} over ${rule.windowMinutes}min${rule.serviceNames?.length > 0 ? ` on ${rule.serviceNames.join(", ")}` : ""}${rule.environments?.length > 0 ? ` in ${rule.environments.join(", ")}` : ""}${rule.excludeServiceNames?.length > 0 ? ` (excl. ${rule.excludeServiceNames.join(", ")})` : ""}` } function ConfigRow({ label, children, wide }: { label: string; children: React.ReactNode; wide?: boolean }) { diff --git a/bun.lock b/bun.lock index c0d1698281..30f4284944 100644 --- a/bun.lock +++ b/bun.lock @@ -342,6 +342,8 @@ "web-vitals": "^6.2.3", }, "devDependencies": { + "@ai-sdk/anthropic": "4.0.82", + "@e2e-dev/web": "0.15.2", "@effect/language-service": "catalog:effect", "@playwright/test": "^1.63.0", "@tanstack/devtools-vite": "^0.8.5", @@ -356,7 +358,9 @@ "@types/three": "^0.186.0", "@vitejs/plugin-react": "^6.1.2", "@vitest/browser-playwright": "catalog:", + "ai": "7.0.143", "dialkit": "2.0.2", + "e2e": "0.21.2", "playwright": "catalog:", "typescript": "catalog:tooling", "vite": "catalog:", @@ -1016,6 +1020,14 @@ }, }, "packages": { + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.82", "", { "dependencies": { "@ai-sdk/provider": "4.0.29", "@ai-sdk/provider-utils": "5.0.62" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1HvCBHdBsfG/OTDOmo3R+rwKtVofnPCWKyMKqOa75yieQEC8V7leB40EaG6uQ1wKnqeXdc31s7tNyq+OnKV/mg=="], + + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.116", "", { "dependencies": { "@ai-sdk/provider": "4.0.29", "@ai-sdk/provider-utils": "5.0.62", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YK2As2GpxmyHi5w6R4FLepiLcs6MbIMxh6nyTMgHaoHKoM5aWEXvKDe/JqdMQ1ivOy3EPJiEBgRI677YTGxfHQ=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@4.0.29", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-nV7UpLADtKn9f0xfnxVZAeFEKuNkScJv5rOpHL7qeWe2EwEEBa3pd6P0QVK6D6coCJ0E8FyCgoM50AffTYrzDQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.62", "", { "dependencies": { "@ai-sdk/provider": "4.0.29", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-//6sHa6QNQdM2KHOg5pxp1UMFQB4U8cqdw/kSm9BMr0DLeaFgG3TXZfxhpwRZmxgBpOrzMVZ/gwLXH309+Ortw=="], + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], "@alchemy.run/cloudflare-runtime": ["@alchemy.run/cloudflare-runtime@2.0.0-beta.83", "", { "dependencies": { "@alchemy.run/node-utils": "2.0.0-beta.83", "@cloudflare/unenv-preset": "^2.16.1", "@puppeteer/browsers": "^3.2.0", "capnp-es": "^0.0.16", "magic-string": "^0.30.21", "mime": "^4.0.7", "sharp": "^0.35.3", "unenv": "^2.0.0-rc.24", "workerd": "1.20261002.1", "yauzl": "^3.4.0" }, "peerDependencies": { "@distilled.cloud/cloudflare": "1.0.0-rc.15", "@effect/platform-bun": "^4.0.0", "@effect/platform-node": "^4.0.0", "effect": "^4.0.0", "rolldown": "1.2.5", "vite": "^7.0.0 || ^8.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node", "rolldown", "vite"] }, "sha512-tchwc16KccR3Q8fmEZSqJcXRPJ0mV0Y3LEu7VbdE6R3e0VqKFNnz5xYiCfQWUAahB+QtJDtTu3/nkjOajE3ggQ=="], @@ -1272,6 +1284,8 @@ "@distilled.cloud/zerossl": ["@distilled.cloud/zerossl@1.0.0-rc.15", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.15" }, "peerDependencies": { "effect": "^4.0.0" } }, "sha512-lu69h33Ib18kATq0W1c1zw6CH3YkUvKWGj1QUi8lbuINXPXHb2/88ATzehDQr5GM/qxtO+azgglhs/NVOf4UVw=="], + "@e2e-dev/web": ["@e2e-dev/web@0.15.2", "", { "dependencies": { "playwright-core": "1.64.0" }, "peerDependencies": { "e2e": ">=0.18.0 <1" }, "bin": { "e2e-web": "./dist/bin.js" } }, "sha512-XidBXbpiEeJQqPsXcEwMOon5KDk8PEaaXR65rDu5P1u115bW+2KK7DlWqYvLg/RpjK/pnOa5iNuD9JthqODLVQ=="], + "@effect-router/core": ["@effect-router/core@workspace:lib/effect-router"], "@effect/ai-openai-compat": ["@effect/ai-openai-compat@4.0.4", "", { "peerDependencies": { "effect": "^4.0.4" } }, "sha512-7ujq5yF/9xSvPvUdxqcdMfrAb1c9IeN6JZtfU/MXrLVSpEKl0pSmFizd+v7fWeUQMWAp4q5Gah0TU5ErGsJkJQ=="], @@ -1662,6 +1676,10 @@ "@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="], + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.3.1", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g=="], + + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.3.1", "", { "dependencies": { "@modelcontextprotocol/core": "2.3.1", "zod": "^4.2.0" } }, "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], @@ -1820,6 +1838,8 @@ "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.150.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.153.0", "", {}, "sha512-rXJvtlsVG4xJNq64hiaUTIatE7jbBgpa6l4QgmEa2jLgT3ULH9scUBkpsC9N1DfmqixLEyT6DuK6OSXozziIlw=="], + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], @@ -1860,6 +1880,44 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], + "@oxc-transform/binding-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.153.0", "", { "os": "android", "cpu": "arm" }, "sha512-EUYebwgzuoDXutZJYbMdL6vzwnQoJyO3fRWGuo5AXSeKRO2YBySW1u2gcVwAA2lTcFXMPJh2Fw1OM23ZfFQF1A=="], + + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.153.0", "", { "os": "android", "cpu": "arm64" }, "sha512-UwwX/zjbEFbLuUdV1+9+SKWhjqwO9tnHqKNQYN4dR3ypK61MFa7s0C2z+vASSSZt7CmcAbl5ArGPcykFiga38A=="], + + "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.153.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J6r1FDN0eqhrddJ8Fxe01DMABxsRTGzrn0d14eDXT2a6gqj5sdtEOiEksN7FimuqDPrMsX1QGjEacSNAuzboGw=="], + + "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.153.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6ad61F1FuyozXDGkPl9VHi/aSOCNUS4ebqXhUqc2JsGXG2S1CFVOsN11u5haNWCLdctbognhzKexK4zpFAsLXg=="], + + "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.153.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-eVcvny0FJzAV7Rr49JkWupfQxoaghJ189nwiwQBv//wI52GtDj+IWkrpPEd7x5bCf1GRX8hoBJmHk/QvKS5S/g=="], + + "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.153.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8DiPMKx18GaMvt4KvHxHb7kHbOnZL9D0Xjxofc+N67RgeWNDdXiqLaUXVmptpOXmIK+sG4+RtQtHGrAt7fnyDQ=="], + + "@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.153.0", "", { "os": "linux", "cpu": "arm" }, "sha512-k1MDve56Sml/O8f9EeeRecZtMdzAQZYtq8mqShtzyJ/LvLQ5KKXnyzlZxQYjoMsRJwzsdkWNHE2svNQxtpyBaQ=="], + + "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.153.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-N8ZvBN7uhlqRgxiqwhgpF7KAaIj5qT/4cGMscxleipj90tbtYOO3rAqxiBuABCrvoosrLX4HbJ39gQX9verUiw=="], + + "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.153.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-OIRZqffxi0yam6aMzIV0lAV5JsMeGs1cQRTwco0qzeAFTKkVaFhpUWXdw1j4BXDDTqbJ3PWi+oQaYzLcGNiaFg=="], + + "@oxc-transform/binding-linux-ppc64-gnu": ["@oxc-transform/binding-linux-ppc64-gnu@0.153.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-KLf7dDN7zU6MFhka9zUHmJw8fvNRSxxGgLDelZwKh0iuaRjUSibKFQ4ZQd0/Rk5ybZbJgqDNFH5xeVU4Cd6ZfQ=="], + + "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.153.0", "", { "os": "linux", "cpu": "none" }, "sha512-WByPPuG7ndXq0tb12P6eHzzolS9Q+ioBmUF99x1FfOiya5nkuc6bjjixq7ZgYezNiSxkw7UsfCgcBZwtNYajkQ=="], + + "@oxc-transform/binding-linux-riscv64-musl": ["@oxc-transform/binding-linux-riscv64-musl@0.153.0", "", { "os": "linux", "cpu": "none" }, "sha512-yXM0cc0mtvIH9keD/9oULanLhVXN8Rf8cvSO5OO2vVrGYs5b0DwkTStztvwn1JQc1eQxPru9iKpZSJq6R4kURA=="], + + "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.153.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-fqwaM2IMmOI0L7DEUWxWtJzGPyvr9ciIOXhB1vp8gkQ97j6xUCddJrEeUJTOAyJyYvHeT0rf03rWciASK4FqUw=="], + + "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.153.0", "", { "os": "linux", "cpu": "x64" }, "sha512-q0yrehYszYqbJfBW0LeUeYvI+NUMwD5p/6skX05EHVQsOJCUTdYKI2wEN2b3nBRUVYxf030pFdOtXJgpZatf/A=="], + + "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.153.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+e0dMTEE7azbGjnJSG97BIQ/CtHM2FbudIlaMmq0Dx5BMaeZaynvXfOg4X2uAYWHEIJva9pC+o6JByuIhUrMOw=="], + + "@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.153.0", "", { "os": "none", "cpu": "arm64" }, "sha512-yQu5Ykk76Nu0g82B6q5nMgyuqgntdmv16LXQPBOVIl2CvwbSwx2jrigfTztNDHk708/hCjiPp6/J7AnNpxO/kg=="], + + "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.153.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6hXONV9UIT0wqfSOtuXlRZSZofHF8orlhyYCNwZJY+xuu/HjFI6D/+T6BbuTnCrhbutynMT5ZUIeVScqsELxDw=="], + + "@oxc-transform/binding-win32-ia32-msvc": ["@oxc-transform/binding-win32-ia32-msvc@0.153.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-i0tleWz872L9t0SAYs6bcY2RLfN/3XE3i0h0UDuTCxMT7wYj22Nd3IFF+4CFZQgF9Eb/Jh92OFrXQHFlwfhA0Q=="], + + "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.153.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DC1juC+WQbUmGf3qJ7x+w4K0U9SUAANizzgGU3J+Y0WrMNZZryXWq0zzHAKTfvFpY2fORgz+J1IXlZ/wf6mRGg=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.72.0", "", { "os": "android", "cpu": "arm" }, "sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.72.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA=="], @@ -2374,6 +2432,8 @@ "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.9", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA=="], @@ -2382,6 +2442,8 @@ "@vitest/browser-playwright": ["@vitest/browser-playwright@5.0.3", "", { "dependencies": { "@vitest/browser": "5.0.3", "@vitest/mocker": "5.0.3", "tinyrainbow": "^3.1.1" }, "peerDependencies": { "playwright": "*", "vitest": "5.0.3" } }, "sha512-rYBHwIFL/ZtI6dl0rbrAatScXCrvX+TL2BX2AGOigkC1Y6u3rIWw8KMQJJMS4drSa1ksMCmZU2wpgwdE7kqu3g=="], + "@vitest/expect": ["@vitest/expect@5.0.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "5.0.3", "@vitest/utils": "5.0.3", "chai": "^6.2.2", "tinyrainbow": "^3.1.1" } }, "sha512-N/VGY122AqBx25OB4EatqlVu8PBayvm9dQzpJ8kVq/pOvqAhXTE5MAmoSMzdB8klDU9wcgSEUliwgMNMtxPa1w=="], + "@vitest/mocker": ["@vitest/mocker@5.0.3", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.3", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg=="], "@vitest/pretty-format": ["@vitest/pretty-format@5.0.3", "", { "dependencies": { "tinyrainbow": "^3.1.1" } }, "sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw=="], @@ -2424,6 +2486,8 @@ "@vueuse/shared": ["@vueuse/shared@14.4.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g=="], + "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="], + "@xstate/fsm": ["@xstate/fsm@1.6.5", "", {}, "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw=="], "@xyflow/react": ["@xyflow/react@12.12.0", "", { "dependencies": { "@xyflow/system": "0.0.83", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-74oXI0Rgm1Eu33cHQAd74J6oVgCH9mbFyTVQbsGgp2kmWwh0HdtBr2VP04z8mbPD+M/xO69nqG5dDEt7/EN2rQ=="], @@ -2488,6 +2552,8 @@ "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "ai": ["ai@7.0.143", "", { "dependencies": { "@ai-sdk/gateway": "4.0.116", "@ai-sdk/provider": "4.0.29", "@ai-sdk/provider-utils": "5.0.62" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y9QNgCK9QRHRCcHEUNoU2vemJ+q2zFX5M9T7xHCMZutfcWCqZPckkAfDbp+KHwqZDiJs5AqoKazj64cb20m5PA=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -2844,6 +2910,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "e2e": ["e2e@0.21.2", "", { "dependencies": { "@ai-sdk/provider": "4.0.26", "@clack/prompts": "1.8.1", "@jridgewell/remapping": "2.3.5", "@modelcontextprotocol/server": "2.3.1", "@oxc-project/runtime": "0.153.0", "@vitest/expect": "5.0.3", "commander": "15.0.0", "fast-png": "8.0.0", "get-tsconfig": "4.14.3", "oxc-parser": "0.153.0", "oxc-transform": "0.153.0", "picocolors": "1.1.1", "pixelmatch": "8.0.0", "zod": "4.6.1" }, "peerDependencies": { "@ai-sdk/anthropic": "^4.0.0", "@ai-sdk/google": "^4.0.0", "@ai-sdk/openai": "^4.0.0", "@ai-sdk/openai-compatible": "^3.0.0", "@ai-sdk/xai": "^5.0.0", "ai": "^7.0.0" }, "optionalPeers": ["@ai-sdk/anthropic", "@ai-sdk/google", "@ai-sdk/openai", "@ai-sdk/openai-compatible", "@ai-sdk/xai", "ai"], "bin": { "e2e": "./dist/cli/bin.js" } }, "sha512-w3NNJLbu0wkUhGy44O3elTXhDNdwetrvEhVvqA2ZjUx+l+ccchIVADodrOXaP2FaB15Y/lnZMbQrOdfVOheJIA=="], + "effect": ["effect@4.0.4", "", {}, "sha512-Sr50OeVSC3M5UkoUYUhFf3EULryhGaZZUf6OzDHTvaznOw7HRlxOKdF3/4nvQXHL22StOyMG3Cvo8Bt7xUp+Ng=="], "electron-to-chromium": ["electron-to-chromium@1.5.439", "", {}, "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q=="], @@ -2912,6 +2980,8 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], @@ -2926,6 +2996,8 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-png": ["fast-png@8.0.0", "", { "dependencies": { "fflate": "^0.8.2", "iobuffer": "^6.0.1" } }, "sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], @@ -3118,6 +3190,8 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "iobuffer": ["iobuffer@6.0.1", "", {}, "sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q=="], + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], "is-accessor-descriptor": ["is-accessor-descriptor@1.0.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g=="], @@ -3186,6 +3260,8 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -3502,6 +3578,8 @@ "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], + "oxc-transform": ["oxc-transform@0.153.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm-eabi": "0.153.0", "@oxc-transform/binding-android-arm64": "0.153.0", "@oxc-transform/binding-darwin-arm64": "0.153.0", "@oxc-transform/binding-darwin-x64": "0.153.0", "@oxc-transform/binding-freebsd-x64": "0.153.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.153.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.153.0", "@oxc-transform/binding-linux-arm64-gnu": "0.153.0", "@oxc-transform/binding-linux-arm64-musl": "0.153.0", "@oxc-transform/binding-linux-ppc64-gnu": "0.153.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.153.0", "@oxc-transform/binding-linux-riscv64-musl": "0.153.0", "@oxc-transform/binding-linux-s390x-gnu": "0.153.0", "@oxc-transform/binding-linux-x64-gnu": "0.153.0", "@oxc-transform/binding-linux-x64-musl": "0.153.0", "@oxc-transform/binding-openharmony-arm64": "0.153.0", "@oxc-transform/binding-win32-arm64-msvc": "0.153.0", "@oxc-transform/binding-win32-ia32-msvc": "0.153.0", "@oxc-transform/binding-win32-x64-msvc": "0.153.0" } }, "sha512-QNLqPkK0IeFF8QWB5KwieTLOZAo6v2hEHrXtmgPEINznmNy/nRi/fce/rm51RPKHPBkrEsxhePwYjGKp6wvfHQ=="], + "oxc-walker": ["oxc-walker@1.1.1", "", { "peerDependencies": { "@oxc-project/types": ">=0.98.0", "oxc-parser": ">=0.98.0", "rolldown": ">=1.0.0" }, "optionalPeers": ["@oxc-project/types", "oxc-parser", "rolldown"] }, "sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ=="], "oxfmt": ["oxfmt@0.72.0", "", { "dependencies": { "tinypool": "2.2.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.72.0", "@oxfmt/binding-android-arm64": "0.72.0", "@oxfmt/binding-darwin-arm64": "0.72.0", "@oxfmt/binding-darwin-x64": "0.72.0", "@oxfmt/binding-freebsd-x64": "0.72.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.72.0", "@oxfmt/binding-linux-arm-musleabihf": "0.72.0", "@oxfmt/binding-linux-arm64-gnu": "0.72.0", "@oxfmt/binding-linux-arm64-musl": "0.72.0", "@oxfmt/binding-linux-ppc64-gnu": "0.72.0", "@oxfmt/binding-linux-riscv64-gnu": "0.72.0", "@oxfmt/binding-linux-riscv64-musl": "0.72.0", "@oxfmt/binding-linux-s390x-gnu": "0.72.0", "@oxfmt/binding-linux-x64-gnu": "0.72.0", "@oxfmt/binding-linux-x64-musl": "0.72.0", "@oxfmt/binding-openharmony-arm64": "0.72.0", "@oxfmt/binding-win32-arm64-msvc": "0.72.0", "@oxfmt/binding-win32-ia32-msvc": "0.72.0", "@oxfmt/binding-win32-x64-msvc": "0.72.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g=="], @@ -3568,6 +3646,8 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "pixelmatch": ["pixelmatch@8.0.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-cIqOLfh5k52us715qf3B3+kTOxZv+WDv77HN7xokLScYQGYQHi4m/ynoDIPpvF2LvCyUJGyvzY64i0QCXtzu1g=="], + "pkg-types": ["pkg-types@2.3.3", "", { "dependencies": { "confbox": "^0.3.1", "exsolve": "^1.1.1", "pathe": "^2.0.3" } }, "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w=="], "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], @@ -4130,6 +4210,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/provider-utils/undici": ["undici@7.30.0", "", {}, "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ=="], + "@alchemy.run/cloudflare-runtime/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "@astrojs/compiler-binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], @@ -4160,6 +4242,8 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@e2e-dev/web/playwright-core": ["playwright-core@1.64.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg=="], + "@effect/sql-d1/@cloudflare/workers-types": ["@cloudflare/workers-types@5.20261013.1", "", {}, "sha512-0SfkM/MA+oSIAzFFmjr0wZLH54oeCtugaOn8GpJDBnerQiML+rOPq2W6j6bBGE51o+N4DOd0tvMruLmkAQzlpw=="], "@electric-sql/pglite-socket/@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], @@ -4336,6 +4420,14 @@ "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "e2e/@ai-sdk/provider": ["@ai-sdk/provider@4.0.26", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-BZ443uBEq6HuMA+Z4b7Hpm65GpZSo7pWqnYqpf/7tAO3C48JbtvAhOE2JnODFj+jodsZzfRHkAyQb64xuD2tZQ=="], + + "e2e/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + + "e2e/oxc-parser": ["oxc-parser@0.153.0", "", { "dependencies": { "@oxc-project/types": "^0.153.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.153.0", "@oxc-parser/binding-android-arm64": "0.153.0", "@oxc-parser/binding-darwin-arm64": "0.153.0", "@oxc-parser/binding-darwin-x64": "0.153.0", "@oxc-parser/binding-freebsd-x64": "0.153.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.153.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.153.0", "@oxc-parser/binding-linux-arm64-gnu": "0.153.0", "@oxc-parser/binding-linux-arm64-musl": "0.153.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.153.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.153.0", "@oxc-parser/binding-linux-riscv64-musl": "0.153.0", "@oxc-parser/binding-linux-s390x-gnu": "0.153.0", "@oxc-parser/binding-linux-x64-gnu": "0.153.0", "@oxc-parser/binding-linux-x64-musl": "0.153.0", "@oxc-parser/binding-openharmony-arm64": "0.153.0", "@oxc-parser/binding-win32-arm64-msvc": "0.153.0", "@oxc-parser/binding-win32-ia32-msvc": "0.153.0", "@oxc-parser/binding-win32-x64-msvc": "0.153.0" } }, "sha512-cEnpyyV1OJRXv++SFNw0z8i+2o/cr88rVLaOYJafELDoHM5yHvCAbwGRLvxuany6SNLcbblSMUyZaGzH+quSYg=="], + + "e2e/zod": ["zod@4.6.1", "", {}, "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q=="], + "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -4678,6 +4770,46 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "e2e/oxc-parser/@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.153.0", "", { "os": "android", "cpu": "arm" }, "sha512-BcWu25dMwMjHpaDCEidmpAi7rGg1qZoihJeUUH9bD55pVZ0z4Wa5CQgqSTo0R41H81oozisQKW3WqvAN2m77IQ=="], + + "e2e/oxc-parser/@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.153.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9l5rdp2DhRqVriBHsNpu5cEf+hizZ4B58rt93HzyQEBUgZiC6qe2oFpaAAnXGCxHGnQExI1s7S2G0QhFacUZ6w=="], + + "e2e/oxc-parser/@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.153.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EcpsyRXHhENDpekpG4QNaSZj2F/BAz4Gh4o89zZwNss86R8KyRSQ6sXbAj74Xyeb4z3j/yBZZ6joQZhraIWyfw=="], + + "e2e/oxc-parser/@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.153.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hlj80HUQy07/2lYp6rmxH+3u+H2ik3rjLR52CUdPApxmZCNxASDUf/+ykTzAHS/fehftmVMATKvPaWmuLHuasg=="], + + "e2e/oxc-parser/@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.153.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fWRcv2pMSh1qtez46MOu8vuUJ5fiaQZPHSphbWulbL7SHBMKfRgZl2PzjXZYQJbq7NEXyXbgIuIuUm+/Rmkv8w=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.153.0", "", { "os": "linux", "cpu": "arm" }, "sha512-paZeUK1GXShl/bOJQET1/8AQxayFeuN4SIWAkgTRxE7ZQwv9uYSB2n0648smygP22N7jcI3zxAm0Z+ALNiIKxg=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.153.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qFPp5NwMf4K1C0O+DCVGmcbOaYyYAq98Rvv9p4VSfb777iUwlJCqMUxf6QG9N5f5ls4TEX6LitOZHi+6GL6rYQ=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.153.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oufEhZCX7saeo27Logra8cvBR1jQ/yw2zUI2hHmWw3qA/XYqEW31FOvLQBv7DWN32NjVdlcaP11WBBZTJo4k9g=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.153.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zXYzuAqng85h6+LI3iRm0zeMLX1/K2z41ZAMWxVRgt0D10VvZUaV5q+TEB46KDhoKBpdexoTLC6QoB/EiP2oqQ=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.153.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-J7+NLwLEzdkKM9cKyPHH/AjXXstWOs98KYgLrPPnO4N9MO8EZRBfPNnBcS3S67uMOBkRpUmCl2FhRnWdQ5CbBA=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.153.0", "", { "os": "linux", "cpu": "none" }, "sha512-sD0K11GQTLWylln54nPMMTtgmBbQrR5It/vMpZpy/fogiw8O2AvIE+9LuWUXddrWk9zvFIHGRcSgFn4IHOfoYg=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.153.0", "", { "os": "linux", "cpu": "none" }, "sha512-7L66uql2BhvSz51LwfExjOdn6et/ydTiQxl23VApdKzJhTc3TczA1Qt6VR/UcvlQrL2ZbytIJb447vh7p5wmUw=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.153.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hXV3T7mNyrPR19rbPYkglkQJtoM/UNo/xwbZeeqdAxEg8PXgb4LXXuxPpGgI4bzRRCWwLUxdDSS58xIyC2u1TA=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.153.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Da8Vqs1xdtoDYtEjmozD7wGgJeLBQkMbR0WUBECOSxodyX5DXy8ySlvJWbukyO2Q0+TM9EK+4LT6tbq0jCg4hQ=="], + + "e2e/oxc-parser/@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.153.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zGAPxNTIIw6X5WyTRscIFE/oDjpdNtrrl32ing6NOXdGjzRucadnxBilexLdeYQ9YxixF93GujpXyQ0jw7ddSg=="], + + "e2e/oxc-parser/@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.153.0", "", { "os": "none", "cpu": "arm64" }, "sha512-GoHzpqP5XyPIp9DHokoEdCp+K4UPl/h1neFj1uXnUB3pJYyOQ7NOhFVOttNUgCGcj2eywAvu5cRivTOSq4S10g=="], + + "e2e/oxc-parser/@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.153.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xGcy12tO9k7zFbeyQzEuilfWfQpyRq23/fLPfN3wjSWuWv58k46fQT1hsO1KFNykZuVGW9xSJE/z592TmAoSDA=="], + + "e2e/oxc-parser/@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.153.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-o1ykUjWncvXX6N5a4KebgLjtD8zRui+f1llMhSGoSC9oP2KOqapRpdwmZ3NcNG0axFouwVKmhGBjd+IhA8BksA=="], + + "e2e/oxc-parser/@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.153.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gxQz7G96Up136fd+2zWrP+o6FYGs9NO4Dyv55IoeJFJUDwLFHcLYY11hhA5wQKs0HoM57Q8esQ3nEVhnCdsjAQ=="], + + "e2e/oxc-parser/@oxc-project/types": ["@oxc-project/types@0.153.0", "", {}, "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw=="], + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "ink-confirm-input/ink-text-input/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], diff --git a/docs/alerting-tier1-spec.md b/docs/alerting-tier1-spec.md new file mode 100644 index 0000000000..319bab0b4e --- /dev/null +++ b/docs/alerting-tier1-spec.md @@ -0,0 +1,498 @@ +# Alerting tier 1: severity tiers, recovery, N-of-M, change conditions, silences, incident actions + +Status: proposal, 2026-10-16; PR 1 built (see "PR 1 as built"). Follows a survey of how other alerting +tools model thresholds, silences and incidents, and the Paper "Alerting rethink" designs, +which already assume warn + page thresholds, silences and incident ack/snooze. + +## Goals + +1. One rule carries a **warning** and a **critical** threshold. The incident's severity moves between them. +2. Each threshold can have its own **recovery value** (hysteresis by value, not only by tick count). +3. A rule opens after **N of the last M** checks breach, not only N in a row. +4. A rule can watch the **change** of a value against the same window 1h, 1d or 7d earlier. +5. **Silences**: one-off and recurring, scoped by rule, tag, group and severity. They mute notifications only. +6. **Incident actions**: acknowledge, snooze, resolve with a note. Assignee and notes reuse the linked issue. + +Non-goals for tier 1: routing policies, notification grouping, per-group no-data, SLO burn rate, anomaly as a +rule condition, heartbeat monitors, per-rule check interval. They build on this model and come next. + +## Current model (what changes) + +- `alert_rules` has one `severity`, `comparator`, `threshold`, `threshold_upper` and `destination_ids_json` + (`packages/db/src/tables/alerts.ts`). +- Evaluation is `evaluateAlertObservation` → breached / healthy / skipped (`packages/alerting-core/src/index.ts`). +- `planAlertLifecycle` folds that into consecutive counters through `IncidentHysteresis` + (`packages/alerting-core/src/hysteresis.ts`) and decides opened / continued / resolved, renotify and flap + suppression. The anomaly detector shares the same machine. +- `alert_incidents` is read-only to users: `open | resolved` plus the hold sub-state. + +## 1. Rule model + +### Schema (domain) + +New struct in `packages/domain/src/http/alerts.ts`: + +```ts +export const AlertThreshold = Schema.Struct({ + severity: AlertSeverity, // "warning" | "critical" + value: Schema.Number, + upper: Schema.optionalKey(Schema.NullOr(Schema.Number)), // range comparators only + recoveryValue: Schema.optionalKey(Schema.NullOr(Schema.Number)), // null = recover when no longer breaching + destinationIds: Schema.Array(AlertDestinationId), +}) + +export const AlertThresholds = Schema.Array(AlertThreshold).check(Schema.isMinLength(1), Schema.isMaxLength(2)) + +export const AlertCompareTo = Schema.Struct({ + offsetMinutes: Schema.Literals([60, 1440, 10080]), // 1h, 1d, 7d + mode: Schema.Literals(["percent", "absolute"]), +}) +``` + +Rule document and upsert request: + +- Add `thresholds: AlertThresholds`, `compareTo: NullOr(AlertCompareTo)`, `breachesOutOf: NullOr(PositiveInt)`. +- `comparator` stays rule-level. Both tiers share one direction, so "warn above 2%, page above 5%" reads + naturally and the chart has one shaded side. +- `consecutiveBreachesRequired` keeps its column and meaning **N**. `breachesOutOf` is **M**: null means M = N, + which is exactly today's "N in a row". +- Remove `severity`, `threshold`, `thresholdUpper` and `destinationIds` from the document. See Compatibility for + how writes that still send them are handled. + +Validation (upsert boundary, as `AlertValidationError` so bad input is a 400): + +- At most one threshold per severity. +- With two tiers, critical must be strictly worse than warning in the comparator's direction + (`gt`/`gte`: critical > warning; `lt`/`lte`: critical < warning). +- `recoveryValue` must sit on the healthy side of `value` (for `gt`: recovery < value). For `gte`/`lte` + the boundary is inclusive the same way. +- `eq`, `neq`, `between`, `not_between`: one tier only, no `recoveryValue` in tier 1. Range tiers would need + nested bands and a two-sided recovery. That is rare and can come later. +- `1 <= N <= M <= 30`. +- `compareTo` is rejected for `signalType = "raw_query"` in tier 1 (no stable way to shift the user's + `$__timeFilter`, see 4). It is allowed with group-by. +- Every threshold needs at least one destination unless the rule is disabled. This keeps today's "unrouted" + blind spot meaningful per tier. + +### Postgres + +`alert_rules` (additive migration first, see Rollout): + +| column | type | notes | +|---|---|---| +| `thresholds_json` | `jsonb not null` | `AlertThresholds`, schema-typed via `PG.jsonb(AlertThresholds)` | +| `breaches_out_of` | `int4 null` | M; null = N | +| `compare_to_json` | `jsonb null` | `AlertCompareTo` | + +Backfill in the same migration: + +```sql +UPDATE alert_rules SET thresholds_json = jsonb_build_array(jsonb_build_object( + 'severity', severity, 'value', threshold, 'upper', threshold_upper, + 'recoveryValue', NULL, 'destinationIds', destination_ids_json)); +``` + +A follow-up migration drops `severity`, `threshold`, `threshold_upper` and `destination_ids_json` once no +deployed code reads them. To verify before writing it: whether `db:generate` keeps a hand-added `UPDATE` in +`migration.sql`, and whether `parity.test.ts` accepts it. + +`alert_rule_states` replaces the two counters with a short history: + +| column | type | notes | +|---|---|---| +| `active_severity` | `text null` | the tier currently active for this group, null when clear | +| `history` | `int2[] not null default '{}'` | last M ticks, newest last, one code per tick (below) | + +Each tick code packs one 2-bit status per tier: `0 = clear`, `1 = hold` (between recovery and trigger), +`2 = breach`. So `code = warning | critical << 2`. Skipped ticks are **not** appended: as today, they count +as evidence of nothing. + +`consecutive_breaches` / `consecutive_healthy` stay populated (derived from `history`) until the drop +migration, because `alert_checks` and the web read them. + +Electric write-amplification: `alert_rule_states` is synced, and today's upsert is skipped when nothing +changed. A ring buffer shifts every tick, which would defeat that gate. Saturate it: when the last M codes are +all identical and the new code matches, the array is unchanged and the upsert is skipped. Steady healthy and +steady breaching rules (almost all of them) stay write-free. Only mixed histories write each tick, and those +are exactly the groups worth watching. + +### Notification template variables + +Add to `ALERT_TEMPLATE_VARIABLES`: + +- `severity.previous` (empty unless the event is a severity change) +- `threshold.warning`, `threshold.critical`, `recovery` (formatted) +- `baseline` and `change.summary` (for example `+64% vs 1d ago`) when `compareTo` is set +- `ack.by` and `snooze.until` + +`event.type` gains `escalate`, `deescalate` and `acknowledge` (see 2 and 6). + +## 2. Evaluator + +All decision logic stays pure in `@maple/alerting-core`. `AlertsService` stays the I/O adapter. + +### Per-tick evaluation + +`evaluateAlertObservation` becomes `evaluateTieredObservation(policy, observation)`. `policy` carries +`comparator`, the tiers, `minimumSampleCount` and `noDataBehavior`. The skip rules are unchanged: no_data, +below_min_samples and no_value still skip the whole tick. For a non-skipped value, each tier gets: + +- `breach` when `compareAlertThreshold(value, comparator, tier.value, tier.upper)` holds. +- `clear` when it does not hold **and** (no `recoveryValue`, or the value is past `recoveryValue` on the + healthy side). +- `hold` otherwise: inside the dead band. + +No-data with `noDataBehavior = "alert"` marks **every** tier `breach`, so an absence rule pages at critical +when it has a critical tier. `"zero"` evaluates 0 as a value, as today. + +The result keeps today's `status` for consumers: `breached` if any tier breaches, `healthy` if all are clear, +and `healthy` otherwise (a tick in the dead band is a healthy check row). The per-tier codes travel +alongside as `levels`. + +### Lifecycle: `planTieredLifecycle` + +Replaces `planAlertLifecycle` for rules. Input adds `history`, `activeSeverity` and the incident's current +severity, ack and snooze state. Per tier, after appending the tick code and trimming to M: + +- **Activate** tier t when `count(code_t == breach over the last M) >= N`. +- **Deactivate** an active tier t when the last R codes are all `clear_t` + (R = `consecutiveHealthyRequired`). `hold` neither counts toward recovery nor toward a new breach. + +`activeSeverity` is the highest active tier. Transitions: + +| from | to | transition | event | +|---|---|---|---| +| none | warning/critical | `opened` | `trigger` to that tier's destinations | +| warning | critical | `escalated` | `escalate` to critical destinations; also clears an ack (new information) | +| critical | warning | `deescalated` | `deescalate` to the destinations that received critical | +| any | none | `resolved` | `resolve` to every destination notified during the incident | + +Everything else carries over unchanged: renotify cadence (sent to the current tier's destinations), flap +suppression on re-open, the held-incident liveness gate for no-data recovery, and `allowNoDataResolution`. + +The anomaly detector keeps using `foldObservation` untouched. Moving it onto the tiered machine is a later +cleanup, not part of tier 1. + +### Delivery fan-out + +The destinations for an event are the union of every tier at or below the current severity. Example: +critical tier → PagerDuty, warning tier → Slack. A critical incident goes to both, and a warning incident +goes only to Slack. This matches what people mean by "warn in Slack, page on critical" and avoids configuring +Slack twice. The upsert stores tier lists as given; the union is computed at send time. + +Delivery keys stay `incidentId:destinationId:eventType:scheduledAt`, so a destination in both tiers is +notified once per event. + +PagerDuty: open at warning sends `severity: warning`. Escalation re-sends `trigger` with the same `dedup_key` +and `severity: critical`. **To verify**: whether Events v2 updates severity on an existing alert or ignores +it. If it ignores it, escalation needs a second dedup key per severity. + +### Incident row + +`alert_incidents.severity` becomes the **current** severity (it is mutable from now on). Add `peak_severity` +(never decreases) for lists and stats. The open-incident unique index on `(org, rule, group)` is unchanged: +escalation updates the row and does not open a second incident. + +### `alert_checks` (ClickHouse) + +Add, appended at the end so the ADD COLUMN migration lines up: + +- `Level LowCardinality(String) DEFAULT ''`: `ok | warning | critical | hold-warning | hold-critical` +- `BaselineValue Nullable(Float64)`: set when `compareTo` is on +- `SilenceId String DEFAULT ''`: set when a delivery from this tick was suppressed + +`Threshold` keeps the warning tier's value (or the only tier's) so existing readers stay correct. Run +`bun run tinybird:manifest` and `bun run local-schema:bump alert-checks-tiers`. + +### Preview / backtest + +`AlertRulePreviewResponse` gains `thresholds` (in place of `threshold`/`thresholdUpper`/`comparator` +top-level fields, which become derived for one release). `AlertRulePreviewPoint` gains `level` and, with +`compareTo`, `baseline`. `AlertRulePreviewFiringSpan` gains `severity`, and a span splits whenever severity +changes. The replay in `rule-edit-impact.tsx` then shows "pages 3 times instead of 11" per severity. + +## 3. N-of-M + +This falls out of the history column above; there is no separate mechanism. Defaults keep today's behaviour +exactly: M = null = N, R = 2. + +The live scheduler slides one minute per tick. With a 5-minute window, consecutive ticks overlap, so "3 of 5" +means 3 of the last 5 one-minute steps of a sliding 5-minute window. The editor copy must say "checks", not +"minutes of breach", to keep this honest. + +## 4. Change conditions + +With `compareTo`, the rule's value becomes the change: + +- `percent`: `(current - baseline) / |baseline| * 100` +- `absolute`: `current - baseline` + +Thresholds, recovery and tiers then apply to that number. "Changes by more than 50% either way" is +`not_between [-50, 50]`. + +Evaluator: + +- `evaluateRule` runs the same compiled plan twice: once for `[end - window, end]`, once for the same window + shifted back by `offsetMinutes`. +- The baseline window is in the past and immutable. Cache it in the query-engine cache keyed by plan hash and + window end with a TTL of the offset, so a 1-minute tick pays for at most one cold baseline query per + window. +- Grouped rules join baseline and current by `groupKey`. +- Skip reasons gain `no_baseline`. It applies when a group has no baseline (a new service) or the percent + baseline is 0. It is a skip, so it never opens or resolves anything. +- `minimumSampleCount` applies to both windows. + +`raw_query` is excluded in tier 1: the user's SQL owns its time filter through macros, and shifting it means +rewriting `$__timeFilter` expansion in the compiler. The builder and built-in signals go through +`planEvaluateSource`, which already takes the window explicitly. + +Preview: query the range a second time shifted by the offset and align the two series per bucket. + +## 5. Silences + +### Table `alert_silences` (Electric-published, low write rate) + +| column | type | notes | +|---|---|---| +| `id` | branded `AlertSilenceId` | | +| `org_id` | `OrgId` | tenant column | +| `name` | `text` | shown in UI and on suppressed deliveries | +| `matchers_json` | `jsonb` | `{ ruleIds?, tags?, groupKeys?, severities? }` | +| `schedule_json` | `jsonb` | tagged union, below | +| `mute_resolve` | `bool default false` | also suppress resolve notifications | +| `created_by`, `created_at`, `updated_at` | | | +| `cancelled_at` | `timestamptzMillis null` | soft end; cancelled silences stay for audit | + +Matching: the fields inside a matcher are ANDed, and the values inside one field are ORed. An empty matcher +is rejected, so nobody silences the whole org by accident. A whole-org silence must say so explicitly with +`{ all: true }`. + +Schedule: + +```ts +Schema.TaggedUnion({ + Once: { startsAt: timestamptzMillis, endsAt: timestamptzMillis }, + Weekly: { + weekdays: Array(Literals(["mon","tue","wed","thu","fri","sat","sun"])), + startTime: "HH:MM", durationMinutes: PositiveInt (<= 7 days), + timezone: IANA string, until: NullOr(timestamptzMillis), + }, +}) +``` + +Daily is `Weekly` with all seven days. Monthly and full RRULE wait until someone asks. + +### Semantics + +- Evaluation, state, incidents and `alert_checks` all continue. Only **outbound notifications** are + suppressed: deliveries, chat posts and mobile push. This is the common default elsewhere, and it keeps + backtests and incident history truthful. +- One gate, `resolveSilence(orgSilences, rule, groupKey, severity, nowMs)`, a pure function in alerting-core. + It is called where `AlertsService` queues delivery events and where push is enqueued. Active silences are + loaded once per org per tick. +- A suppressed event writes the incident's `suppressed_by_silence_id` and a timeline entry. No delivery row + is created. +- When a silence ends and a matched incident is still open with a suppressed trigger, the next tick sends + `trigger`. Without this, a page silenced during a deploy would never arrive. +- Escalation during a silence is suppressed too, unless the matcher's `severities` excludes critical. That is + how "mute warnings during the deploy, still page on critical" is expressed. + +## 6. Incident actions + +### Ownership split + +The linked error issue (`error_issues`, kind `alert`) already has an assignee, claim, comments and workflow +state, and it outlives individual incidents for the same rule and group. Duplicating them on the incident +would fork the source of truth. + +- **On the incident** (episode-scoped): acknowledge, snooze, manual resolve. +- **On the issue** (persistent): assignee, notes and comments. The incident page reads and writes them + through the existing issue endpoints. + +### Columns on `alert_incidents` + +`acknowledged_at`, `acknowledged_by`, `snoozed_until`, `snoozed_by`, `resolved_by` (`null` = system), +`resolution_note`, `peak_severity`, `suppressed_by_silence_id`. + +### Table `alert_incident_events` (not synced, read via API) + +`id, org_id, incident_id, type, actor_id null, at, data_json` + +`type` is one of `acknowledged | unacknowledged | snoozed | unsnoozed | resolved_manually | escalated | +deescalated | silenced`. The incident timeline merges this with deliveries and checks, as it already merges +deliveries and holds. Each human action also goes to the org audit log. + +### Behaviour + +- **Acknowledge**: stops renotify. Sends `acknowledge` only to providers that understand it (PagerDuty Events + v2 `acknowledge`, a reply in the chat thread). Escalation to critical clears the ack and notifies. +- **Snooze** `{ until }` (presets 30m, 1h, 4h, tomorrow 09:00 in the user's timezone): suppresses renotify and + escalation notifications until `until`. If the incident is still open when the snooze expires, renotify + fires on the next tick. Resolve during a snooze still notifies. +- **Resolve** `{ note? }`: sets `status = resolved`, `resolved_by` and `resolution_note`, resets the group's + `history`, and sends `resolve`. If the signal is still bad, a new incident opens after N-of-M as usual. + Flap suppression does **not** apply to a re-open after a manual resolve, because the human asked for a clean + slate. + +### API, MCP, web + +- v2: + - `POST /alert-incidents/{id}/acknowledge` + - `/unacknowledge` + - `/snooze` `{ until }` + - `/unsnooze` + - `/resolve` `{ note? }` + - Typed errors for "already resolved" and "snooze in the past". +- MCP: + - one `act_on_alert_incident` tool with `action: P.oneOf([...])`. + - `create_alert_silence`, `list_alert_silences`, `cancel_alert_silence`. + - `create_alert_rule` / `update_alert_rule` take `thresholds`, `compare_to`, `breaches_out_of`. + - Output schemas in `packages/domain/src/mcp-outputs/`. + - `registry.contract.test.ts` covers the new tools. + +## 7. Editor and UI + +Keep the dashboard query panel and the Built-in / Query builder / SQL switch exactly as they are (decided +2026-10-15). Changes sit below the query. + +**Condition bar** (`rule-condition-bar.tsx`). Replace the severity radio and single threshold with tier rows: + +``` +When [error rate] [is above ▾] + ● Warning 2 % recover below 1.5 % → #alerts-api + ● Critical 5 % recover below 4 % → PagerDuty: api-oncall [remove] + + Add critical threshold +Compare to [the value ▾ | change vs 1h / 1d / 7d ago] [% | absolute] +``` + +- The destination picker moves from `notifications-section.tsx` into each tier row. The message template + stays in the notifications section. +- `describeRuleCondition` produces one sentence across both tiers for the action bar, for example + "Warn above 2%, page above 5% of error rate". + +**Chart hero** (`rule-live-chart-hero.tsx`): + +- Two threshold lines, warning amber and critical red, each draggable and writing back into the form. +- A recovery value shows as a dashed line with the dead band lightly shaded. +- Preview firing spans are coloured by severity. +- With `compareTo`, a toggle shows either value and baseline overlaid, or the change series against the + thresholds. + +**Noise section** (`rule-noise-section.tsx`): + +- "Open after [N] of the last [M] checks". It reads "N checks in a row" when M = N. +- "Resolve after [R] healthy checks". +- Remind interval and no-data stay. + +**Edit impact**: the replay splits counts per severity. + +**Incident page**: + +- Action bar: Acknowledge, Snooze menu, Resolve with an optional note. +- The rail shows the issue assignee with a reassign control. +- The timeline renders the new event types and the silence that muted a notification. + +**Alerts hub**: + +- Inbox: acknowledged and snoozed incidents drop below the unacknowledged ones and get badges. +- Routing tab: gains a Silences section for active, scheduled and recently ended silences, plus a create + dialog with scope and schedule. +- Rule and incident pages get a quick "Silence this rule for 1h". + +## 8. Compatibility + +- The v2 public API is the contract that matters (`docs/api-v2.md`). For one release, a write without + `thresholds` but with `severity` + `threshold` (+ `thresholdUpper`, `destinationIds`) is converted to a + single tier, and responses also carry those legacy fields derived from the highest tier. Both are marked + deprecated in the OpenAPI document. Then remove them in the same change as the column drop. +- The web and MCP move to `thresholds` immediately. Nothing internal reads the legacy fields after PR 2. +- Mobile reads alert incidents; `severity` keeps its meaning (current severity) and values, so no app change is + required. `peak_severity` is optional to consume. + +## 9. Rollout (PR sequence) + +1. **Core + schema.** + - alerting-core: `evaluateTieredObservation`, `planTieredLifecycle`, `resolveSilence` (pure), with + table-driven tests. + - The additive Postgres migration with backfill. + - Domain schemas, the evaluator wired to tiers, history, recovery and N-of-M. + - `alert_checks` columns, and preview with levels. + - The API accepts both shapes. Behaviour for existing rules is identical, and a test pins that: one tier, + M = N, no recovery. +2. **Editor**: tier rows, draggable lines, N-of-M controls, per-tier destinations, MCP params. +3. **Change conditions**: the second query with its cache, the `no_baseline` skip, preview baseline, the + compare-to UI. +4. **Incident actions**: columns, the events table, the v2 endpoints and MCP tool, the incident page, + PagerDuty ack. +5. **Silences**: the table, published to Electric, the gate in delivery and push, the "still firing after + silence" trigger, the Routing tab UI and MCP tools. +6. **Cleanup**: drop the legacy columns and API fields, and derive nothing from them. + +PRs 3, 4 and 5 are independent of each other once PR 1 lands. + +## 10. Tests (behaviour, per test-slop rules) + +- alerting-core: + - N-of-M opens on the Nth breach inside M, and not on N spread over more than M. + - Hold neither resolves nor reopens. + - Escalation, de-escalation and resolve orders. + - Skipped ticks are not appended. + - Saturated history is byte-identical across steady ticks, which keeps the Electric gate. +- `AlertsService` with `createTestDb()`: + - Fan-out union per severity, sending once to a destination present in both tiers. + - Ack stops renotify, and escalation clears ack. + - Snooze expiry renotifies. + - Manual resolve then re-open is not flap-suppressed. + - A silence suppresses delivery and push, the incident is still created, and a trigger is sent when the + silence ends. +- Change conditions: + - Grouped join. + - A new group skips with `no_baseline`. + - A zero baseline in percent mode skips. + - A baseline cache hit within a window. + +## PR 1 as built (where it differs from the sections above) + +- **No new event types.** The iOS client decodes `AlertEventType` and `AlertIncidentTransition` as + closed enums, so a new value would break it. + - Escalation is delivered as `trigger` at the new severity, and de-escalation as `renotify`, both + restarting the reminder clock. + - Check rows record a severity change as `continued`, and the new `Level` column says which tier + breached. + - Planner transitions (`escalated`, `deescalated`) stay internal. +- **No `active_severity` column.** The current severity is the open incident's `severity`, so the state + row only adds `history_json` (jsonb `number[]`, not `int2[]`). + - A state row without a history is replayed from its counters inside `planAlertLifecycle`. A + property test proves the result matches the old consecutive-count machine. +- **Legacy columns stay authoritative for pre-tier rows only.** `thresholds_json` is nullable and not + backfilled: a null reads as one tier from `severity`/`threshold`/`destination_ids_json`, so an old + worker editing a rule mid-deploy cannot leave a stale tier list behind. Writes keep those columns in + step, and the drop migration backfills first. + - `destination_ids_json` holds every tier's destinations, so the "destination in use" check is + unchanged. +- **`peak_severity` is not backfilled.** `alert_incidents` is synced with full-row WAL, so rewriting + history is churn. Null reads as the current severity. +- **v2 PATCH carries tiers forward.** Single-threshold fields edit a one-tier rule's tier in place, + keeping its recovery value. On a two-tier rule they are rejected with a 400, never silently collapsed. +- **Notification routing.** Trigger and escalation reach every tier at or below the new severity. + De-escalation and resolve also reach every tier up to the incident's peak; reminders only reach the + current tier's. +- **Rule edits.** An edit that changes the comparator or a tier value resets the group's history and + counters in the same transaction, so ticks judged against the old condition never count. + An incident whose severity the rule no longer has reads as the tier the rule kept; the move is + announced (escalation or de-escalation) on its first breaching tick, and resolves reach that tier. +- **Linked issue.** Opening at or escalating to critical raises a detector-set issue to critical. +- **Validation.** Tiers are strictly ordered even for `gte`/`lte`, and a critical recovery value may + not sit past the warning threshold. +- **Preview spans start at the first breach** inside the last M checks, not `(N-1)` windows before the + tripping one. The two only differ when a skipped window sits inside the run. +- **Template variables** (`severity.previous`, `threshold.warning`, ...) are not added yet; they land + with the editor in PR 2. + +## Open questions + +1. Is an `info` tier worth adding now (some tools add `info`)? Proposal: no, two tiers cover warn/page. +2. Should ack auto-expire (PagerDuty's ack timeout re-triggers after N minutes)? Proposal: not in tier 1. +3. Should a silence be able to suppress incident creation as well ("detect but do not open")? Proposal: + no; truthful history wins. +4. Does PagerDuty Events v2 update severity on a deduped trigger? This decides the escalation dedup-key shape. diff --git a/knip.json b/knip.json index 25bc4c41d6..37b5ef7f98 100644 --- a/knip.json +++ b/knip.json @@ -22,7 +22,10 @@ "src/worker-entry.ts", "src/components/ai-elements/**", "src/components/icons/**", - "src/lib/effect-atom.ts" + "src/lib/effect-atom.ts", + // The browser e2e suite, loaded by path by `e2e run` (`test:e2e`). + "e2e/e2e.config.ts", + "e2e/tests/**/*.e2e.ts" ] }, "apps/landing": { diff --git a/packages/alerting-core/src/index.test.ts b/packages/alerting-core/src/index.test.ts index 110065c277..e72b30b151 100644 --- a/packages/alerting-core/src/index.test.ts +++ b/packages/alerting-core/src/index.test.ts @@ -24,14 +24,22 @@ const breached: AlertEvaluation = { comparator: "gt", reason: "above threshold", derivedFromNoData: false, + levels: [{ severity: "warning", status: "breach" }], } -const healthy: AlertEvaluation = { ...breached, status: "healthy", value: 9 } +const healthy: AlertEvaluation = { + ...breached, + status: "healthy", + value: 9, + levels: [{ severity: "warning", status: "clear" }], +} const policy = { consecutiveBreachesRequired: 2, + breachWindow: 2, consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 10, + severities: ["warning" as const], } const projectAlertLifecycleEvent = (input: Parameters[0]) => @@ -45,6 +53,7 @@ describe("evaluateAlertObservation", () => { comparator: "between", threshold: 10, thresholdUpper: 20, + tiers: [{ severity: "warning", value: 10, upper: 20, recoveryValue: null }], minimumSampleCount: 2, noDataBehavior: "skip", }, @@ -61,6 +70,7 @@ describe("evaluateAlertObservation", () => { comparator: "gt", threshold: 10, thresholdUpper: null, + tiers: [{ severity: "warning", value: 10, upper: null, recoveryValue: null }], minimumSampleCount: 0, noDataBehavior: "zero", }, @@ -113,6 +123,7 @@ describe("planAlertLifecycle", () => { evaluation: healthy, state: { consecutiveBreaches: 0, consecutiveHealthy: 1 }, openIncident: { + severity: "warning" as const, firstTriggeredAtMs: 600_000, lastNotifiedAtMs: opened.inheritedNotificationAtMs, lastDeliveredEventType: null, @@ -132,6 +143,7 @@ describe("planAlertLifecycle", () => { evaluation: breached, state: { consecutiveBreaches: 2, consecutiveHealthy: 0 }, openIncident: { + severity: "warning" as const, firstTriggeredAtMs: 0, lastNotifiedAtMs: 1_000, lastDeliveredEventType: "trigger", @@ -152,6 +164,7 @@ describe("planAlertLifecycle", () => { evaluation: noDataHealthy, state: { consecutiveBreaches: 0, consecutiveHealthy: 1 }, openIncident: { + severity: "warning" as const, firstTriggeredAtMs: 0, lastNotifiedAtMs: 0, lastDeliveredEventType: "trigger" as const, @@ -170,6 +183,7 @@ describe("planAlertLifecycle", () => { comparator: "gt", threshold: 10, thresholdUpper: null, + tiers: [{ severity: "warning", value: 10, upper: null, recoveryValue: null }], minimumSampleCount: 0, noDataBehavior: "skip", }, @@ -182,6 +196,7 @@ describe("planAlertLifecycle", () => { evaluation, state: { consecutiveBreaches: 2, consecutiveHealthy: 0 }, openIncident: { + severity: "warning" as const, firstTriggeredAtMs: 0, lastNotifiedAtMs: 0, lastDeliveredEventType: "trigger" as const, @@ -223,6 +238,7 @@ describe("planAlertLifecycle", () => { comparator: "gt", threshold: 10, thresholdUpper: null, + tiers: [{ severity: "warning", value: 10, upper: null, recoveryValue: null }], minimumSampleCount: 50, noDataBehavior: "alert", }, @@ -247,6 +263,7 @@ describe("planAlertLifecycle", () => { comparator: "gt", threshold: 10, thresholdUpper: null, + tiers: [{ severity: "warning", value: 10, upper: null, recoveryValue: null }], minimumSampleCount: 50, noDataBehavior: "alert", }, @@ -267,6 +284,7 @@ describe("planAlertLifecycle", () => { comparator: "gt", threshold: 10, thresholdUpper: null, + tiers: [{ severity: "warning", value: 10, upper: null, recoveryValue: null }], minimumSampleCount: 2, noDataBehavior: "skip", }, @@ -282,6 +300,7 @@ describe("planAlertLifecycle", () => { evaluation, state, openIncident: { + severity: "warning" as const, firstTriggeredAtMs: 0, lastNotifiedAtMs: 0, lastDeliveredEventType: "trigger", @@ -328,6 +347,7 @@ describe("delivery policy", () => { comparator: "gt" as const, threshold: 5, thresholdUpper: null, + tiers: [{ severity: "warning", value: 5, upper: null, recoveryValue: null }], windowMinutes: 5, value: 7.2, sampleCount: 12, diff --git a/packages/alerting-core/src/index.ts b/packages/alerting-core/src/index.ts index 4076a09598..4be80dc17f 100644 --- a/packages/alerting-core/src/index.ts +++ b/packages/alerting-core/src/index.ts @@ -1,5 +1,17 @@ import { Effect, Result, Schema } from "effect" -import { foldObservation } from "./hysteresis" +import { + appendTick, + classifyTier, + countersFromHistory, + encodeTick, + maxSeverity, + nearestSeverity, + resolveActiveSeverity, + seedHistory, + type AlertThresholdTier, + type AlertTierLevel, + type TieredHysteresisPolicy, +} from "./tiers" import { makeCloudEvent, CloudEventInvalid, type MapleCloudEvent } from "@maple/eventing-core" import type { @@ -7,8 +19,10 @@ import type { AlertEventType, AlertEvaluationResult, AlertEvaluationStatus, + AlertSeverity, } from "@maple/domain/http" export type { AlertComparator, AlertEventType, AlertEvaluationStatus } from "@maple/domain/http" +export * from "./tiers" export interface AlertObservation { readonly value: number | null @@ -18,8 +32,11 @@ export interface AlertObservation { export interface AlertEvaluationPolicy { readonly comparator: AlertComparator + /** The primary (lowest) tier, reported on single-threshold surfaces. */ readonly threshold: number readonly thresholdUpper: number | null + /** Every tier the value is classified against. */ + readonly tiers: ReadonlyArray readonly minimumSampleCount: number readonly noDataBehavior: "skip" | "zero" | "alert" } @@ -37,32 +54,8 @@ export interface AlertEvaluation extends Pick< > { /** Derived from an empty window: healthy when read as zero, breached when the rule alerts on no data. */ readonly derivedFromNoData: boolean -} - -export const compareAlertThreshold = ( - value: number, - comparator: AlertComparator, - threshold: number, - thresholdUpper: number | null = null, -): boolean => { - switch (comparator) { - case "gt": - return value > threshold - case "gte": - return value >= threshold - case "lt": - return value < threshold - case "lte": - return value <= threshold - case "eq": - return value === threshold - case "neq": - return value !== threshold - case "between": - return thresholdUpper != null && value >= threshold && value <= thresholdUpper - case "not_between": - return thresholdUpper != null && (value < threshold || value > thresholdUpper) - } + /** Per-tier verdict; empty on a skipped tick. */ + readonly levels: ReadonlyArray } export const evaluateAlertObservation = ( @@ -92,6 +85,7 @@ export const evaluateAlertObservation = ( reason: "No data in the selected window", skipReason: "no_data", derivedFromNoData: false, + levels: [], } } @@ -106,6 +100,7 @@ export const evaluateAlertObservation = ( comparator: policy.comparator, reason: "No data in the selected window", derivedFromNoData: true, + levels: policy.tiers.map((tier) => ({ severity: tier.severity, status: "breach" as const })), } } @@ -120,6 +115,7 @@ export const evaluateAlertObservation = ( reason: `Sample count ${sampleCount} is below minimum ${policy.minimumSampleCount}`, skipReason: "below_min_samples", derivedFromNoData: false, + levels: [], } } @@ -134,13 +130,17 @@ export const evaluateAlertObservation = ( reason: "Alert evaluation did not return a scalar value", skipReason: "no_value", derivedFromNoData: false, + levels: [], } } + const levels = policy.tiers.map((tier) => ({ + severity: tier.severity, + status: classifyTier(value, policy.comparator, tier), + })) return { - status: compareAlertThreshold(value, policy.comparator, policy.threshold, policy.thresholdUpper) - ? "breached" - : "healthy", + status: levels.some((level) => level.status === "breach") ? "breached" : "healthy", + levels, value, sampleCount, threshold: policy.threshold, @@ -153,22 +153,36 @@ export const evaluateAlertObservation = ( export interface AlertLifecyclePolicy { readonly consecutiveBreachesRequired: number + /** M in "N of the last M checks"; equal to N for "N in a row". */ + readonly breachWindow: number readonly consecutiveHealthyRequired: number readonly renotifyIntervalMinutes: number + /** The rule's tier severities. */ + readonly severities: ReadonlyArray } export interface AlertLifecycleState { readonly consecutiveBreaches: number readonly consecutiveHealthy: number + /** Recent non-skipped ticks, oldest first; see `encodeTick`. */ + readonly history: ReadonlyArray } export interface AlertLifecycleIncident { + /** The incident's current severity. */ + readonly severity: AlertSeverity readonly firstTriggeredAtMs: number readonly lastNotifiedAtMs: number | null readonly lastDeliveredEventType: AlertEventType | null } -export type AlertIncidentTransition = "none" | "opened" | "continued" | "resolved" +export type AlertIncidentTransition = + | "none" + | "opened" + | "continued" + | "escalated" + | "deescalated" + | "resolved" export type AlertNotificationSuppression = "flapping" | "flap_resolution" | null export type AlertLifecycleHold = "missing_telemetry" | null @@ -269,7 +283,10 @@ export const projectAlertLifecycleEvent = ( export interface AlertLifecycleInput { readonly policy: AlertLifecyclePolicy readonly evaluation: AlertEvaluation - readonly state: AlertLifecycleState | null + /** Null for a group never evaluated; a null `history` (pre-tier row) is replayed from its counters. */ + readonly state: + | (Omit & { readonly history?: ReadonlyArray | null }) + | null readonly openIncident: AlertLifecycleIncident | null readonly nowMs: number /** Most recent notification for a resolved incident with the same rule and group. */ @@ -281,6 +298,8 @@ export interface AlertLifecycleInput { export interface AlertLifecyclePlan { readonly state: AlertLifecycleState readonly transition: AlertIncidentTransition + /** Severity of the open incident after this tick; null when none stands. */ + readonly severity: AlertSeverity | null readonly eventType: AlertEventType | null readonly notificationSuppression: AlertNotificationSuppression readonly hold: AlertLifecycleHold @@ -327,9 +346,14 @@ export const alertDeliveryRetryDelayMs = ( return base + Math.max(0, Math.floor(jitterMs)) } -const noTransition = (state: AlertLifecycleState, hold: AlertLifecycleHold = null): AlertLifecyclePlan => ({ +const noTransition = ( + state: AlertLifecycleState, + severity: AlertSeverity | null, + hold: AlertLifecycleHold = null, +): AlertLifecyclePlan => ({ state, transition: "none", + severity, eventType: null, notificationSuppression: null, hold, @@ -346,6 +370,7 @@ const resolutionPlan = ( return { state, transition: "resolved", + severity: null, eventType: flapResolutionSuppressed ? null : "resolve", notificationSuppression: flapResolutionSuppressed ? "flap_resolution" : null, hold: null, @@ -362,9 +387,24 @@ const resolutionPlan = ( * the hosted PostgreSQL/Tinybird adapter and a future Maple Local adapter. */ export const planAlertLifecycle = (input: AlertLifecycleInput): Effect.Effect => - Effect.gen(function* () { + Effect.sync(() => { const { evaluation, policy, openIncident, nowMs } = input - const previous = input.state ?? { consecutiveBreaches: 0, consecutiveHealthy: 0 } + const current = + openIncident === null ? null : nearestSeverity(openIncident.severity, policy.severities) + const hysteresis: TieredHysteresisPolicy = { + breachesToOpen: policy.consecutiveBreachesRequired, + breachWindow: policy.breachWindow, + healthyToResolve: policy.consecutiveHealthyRequired, + } + const counters = input.state ?? { consecutiveBreaches: 0, consecutiveHealthy: 0, history: [] } + const seedSeverity = + current ?? + policy.severities.reduce((highest, s) => maxSeverity(highest, s) ?? s, "warning") + const previous: AlertLifecycleState = { + consecutiveBreaches: counters.consecutiveBreaches, + consecutiveHealthy: counters.consecutiveHealthy, + history: counters.history ?? seedHistory(counters, seedSeverity, hysteresis), + } if (evaluation.status === "skipped") { // Empty skipped windows use the same host liveness gate as missing @@ -372,31 +412,17 @@ export const planAlertLifecycle = (input: AlertLifecycleInput): Effect.Effect= policy.consecutiveBreachesRequired - ) { + if (openIncident === null) { + if (next === null) return noTransition(state, null) const previousNotificationAtMs = input.previousNotificationAtMs ?? null const flapSuppressed = previousNotificationAtMs != null && @@ -404,6 +430,7 @@ export const planAlertLifecycle = (input: AlertLifecycleInput): Effect.Effect= policy.consecutiveHealthyRequired - ) { - if (evaluation.derivedFromNoData && input.allowNoDataResolution !== true) { - return noTransition(state, "missing_telemetry") - } - - return resolutionPlan(state, openIncident) - } - - return noTransition(state) + return noTransition(state, next) }) /** Preserve per-tenant order while preventing one tenant from monopolizing a tick. */ diff --git a/packages/alerting-core/src/tiers.test.ts b/packages/alerting-core/src/tiers.test.ts new file mode 100644 index 0000000000..245526106a --- /dev/null +++ b/packages/alerting-core/src/tiers.test.ts @@ -0,0 +1,286 @@ +import { Effect } from "effect" +import { describe, expect, it } from "vitest" +import { foldObservation, type HysteresisRow } from "./hysteresis" +import { + evaluateAlertObservation, + planAlertLifecycle, + type AlertEvaluation, + type AlertLifecycleIncident, + type AlertLifecycleInput, + type AlertLifecyclePlan, + type AlertThresholdTier, +} from "./index" + +const plan = (input: AlertLifecycleInput) => Effect.runSync(planAlertLifecycle(input)) + +const warnAt = (value: number, recoveryValue: number | null = null): AlertThresholdTier => ({ + severity: "warning", + value, + upper: null, + recoveryValue, +}) +const critAt = (value: number, recoveryValue: number | null = null): AlertThresholdTier => ({ + severity: "critical", + value, + upper: null, + recoveryValue, +}) + +const evaluate = (tiers: ReadonlyArray, value: number | null): AlertEvaluation => + evaluateAlertObservation( + { + comparator: "gt", + threshold: tiers[0]!.value, + thresholdUpper: null, + tiers, + minimumSampleCount: 0, + noDataBehavior: "skip", + }, + { value, sampleCount: value === null ? 0 : 10, hasData: value !== null }, + "test", + ) + +interface Replay { + readonly plans: ReadonlyArray + readonly severities: ReadonlyArray +} + +/** Feed values through evaluation + lifecycle the way the scheduler does, one tick per value. */ +const replay = ( + tiers: ReadonlyArray, + values: ReadonlyArray, + policy: { readonly n: number; readonly m?: number; readonly r: number }, +): Replay => { + const initial = { + state: null as AlertLifecycleInput["state"], + incident: null as AlertLifecycleIncident | null, + plans: [] as ReadonlyArray, + } + const final = values.reduce((acc, value, tick) => { + const next = plan({ + policy: { + consecutiveBreachesRequired: policy.n, + breachWindow: policy.m ?? policy.n, + consecutiveHealthyRequired: policy.r, + renotifyIntervalMinutes: 60, + severities: tiers.map((tier) => tier.severity), + }, + evaluation: evaluate(tiers, value), + state: acc.state, + openIncident: acc.incident, + nowMs: tick * 60_000, + }) + const incident: AlertLifecycleIncident | null = + next.transition === "resolved" || next.severity === null + ? null + : { + severity: next.severity, + firstTriggeredAtMs: acc.incident?.firstTriggeredAtMs ?? tick * 60_000, + lastNotifiedAtMs: tick * 60_000, + lastDeliveredEventType: "trigger", + } + return { state: next.state, incident, plans: [...acc.plans, next] } + }, initial) + return { plans: final.plans, severities: final.plans.map((p) => p.severity) } +} + +describe("severity tiers", () => { + it("opens at warning, escalates to critical, de-escalates, then resolves", () => { + const { plans, severities } = replay([warnAt(10), critAt(20)], [15, 15, 25, 25, 15, 15, 5, 5], { + n: 2, + r: 2, + }) + expect(plans.map((p) => p.transition)).toEqual([ + "none", + "opened", + // One critical breach is not N = 2 of them yet: still a warning incident. + "continued", + "escalated", + // Back under critical, but critical has not had R = 2 clear checks. + "continued", + "deescalated", + "none", + "resolved", + ]) + expect(severities).toEqual([ + null, + "warning", + "warning", + "critical", + "critical", + "warning", + "warning", + null, + ]) + // Escalation pages like a trigger; de-escalation reads as a reminder. + expect(plans[3]).toMatchObject({ eventType: "trigger", advanceNotificationAnchor: true }) + expect(plans[5]).toMatchObject({ eventType: "renotify", advanceNotificationAnchor: true }) + }) + + it("opens straight at critical when the value jumps past both tiers", () => { + const { plans } = replay([warnAt(10), critAt(20)], [30, 30], { n: 2, r: 2 }) + expect(plans[1]).toMatchObject({ transition: "opened", severity: "critical", eventType: "trigger" }) + }) + + it("holds an incident open inside the recovery band", () => { + // Trigger above 10, clear only at or below 8: 9 is the dead band. + const { plans } = replay([warnAt(10, 8)], [12, 12, 9, 9, 9, 7, 7], { n: 2, r: 2 }) + expect(plans.map((p) => p.transition)).toEqual([ + "none", + "opened", + "none", + "none", + "none", + "none", + "resolved", + ]) + }) + + it("does not count a recovery-band check as healthy in the audit counters", () => { + const { plans } = replay([warnAt(10, 8)], [12, 12, 9, 7], { n: 2, r: 2 }) + expect(plans.map((p) => p.state.consecutiveHealthy)).toEqual([0, 0, 0, 1]) + }) + + it("does not re-open from inside the recovery band", () => { + const { plans } = replay([warnAt(10, 8)], [9, 9, 9], { n: 1, r: 1 }) + expect(plans.every((p) => p.transition === "none")).toBe(true) + }) + + it("opens on N breaches within the last M checks, not when they are spread wider", () => { + const within = replay([warnAt(10)], [15, 5, 15, 5, 15], { n: 3, m: 5, r: 2 }) + expect(within.plans.map((p) => p.transition)).toEqual(["none", "none", "none", "none", "opened"]) + + const spread = replay([warnAt(10)], [15, 5, 5, 5, 15, 5, 5, 15], { n: 3, m: 5, r: 2 }) + expect(spread.plans.some((p) => p.transition === "opened")).toBe(false) + }) + + it("never opens on a tick that is not itself breaching", () => { + // 2 of the last 3 are breaches once the third tick lands, but that tick is clear. + const { plans } = replay([warnAt(10)], [15, 15, 5], { n: 3, m: 3, r: 1 }) + expect(plans.map((p) => p.transition)).toEqual(["none", "none", "none"]) + }) + + it("ignores skipped checks: they neither count toward nor reset a run", () => { + const { plans } = replay([warnAt(10)], [15, null, 15], { n: 2, r: 2 }) + expect(plans.map((p) => p.transition)).toEqual(["none", "none", "opened"]) + }) + + it("escalates, not resolves, when its rule's only tier changes severity", () => { + // A warning incident whose rule is now critical-only; the state row predates the edit. + const kept = plan({ + policy: { + consecutiveBreachesRequired: 2, + breachWindow: 2, + consecutiveHealthyRequired: 2, + renotifyIntervalMinutes: 60, + severities: ["critical"], + }, + evaluation: evaluate([critAt(10)], 15), + state: { consecutiveBreaches: 2, consecutiveHealthy: 0, history: null }, + openIncident: { + severity: "warning", + firstTriggeredAtMs: 0, + lastNotifiedAtMs: 60_000, + lastDeliveredEventType: "trigger", + }, + nowMs: 120_000, + }) + expect(kept).toMatchObject({ transition: "escalated", severity: "critical", eventType: "trigger" }) + }) + + it("leaves a steady group's history byte-identical, so the state upsert can be skipped", () => { + const healthy = replay([warnAt(10), critAt(20)], [1, 1, 1, 1, 1, 1], { n: 2, m: 4, r: 3 }) + const histories = healthy.plans.map((p) => JSON.stringify(p.state.history)) + expect(histories.at(-1)).toEqual(histories.at(-2)) + + const firing = replay([warnAt(10)], [15, 15, 15, 15, 15, 15], { n: 2, m: 4, r: 3 }) + const firingHistories = firing.plans.map((p) => JSON.stringify(p.state.history)) + expect(firingHistories.at(-1)).toEqual(firingHistories.at(-2)) + }) +}) + +describe("single-tier rules behave exactly as the consecutive-count machine did", () => { + // Deterministic pseudo-random so a failure reproduces. + const lcg = (seed: number) => { + const next = (state: number) => (state * 1_103_515_245 + 12_345) % 2_147_483_648 + return (count: number) => + Array.from({ length: count }).reduce<{ seed: number; out: number[] }>( + (acc) => { + const seedNext = next(acc.seed) + return { seed: seedNext, out: [...acc.out, seedNext / 2_147_483_648] } + }, + { seed, out: [] }, + ).out + } + + it.each([1, 2, 3, 7, 42, 1234])("seed %i", (seed) => { + const rolls = lcg(seed)(300) + const n = 1 + Math.floor(rolls[0]! * 3) + const r = 1 + Math.floor(rolls[1]! * 3) + const values = rolls.slice(2).map((roll) => (roll < 0.1 ? null : roll < 0.55 ? 15 : 5)) + + const tiered = replay([warnAt(10)], values, { n, r }) + + const config = { breachesToOpen: n, healthyToResolve: r, cooldownMs: 0 } + const legacy = values.reduce( + (acc, value, tick) => { + const status = value === null ? "skipped" : value > 10 ? "breached" : "healthy" + const outcome = Effect.runSync(foldObservation(acc.row, status, config, tick * 60_000)) + const row: HysteresisRow = { + consecutiveBreaches: outcome.consecutiveBreaches, + consecutiveHealthy: outcome.consecutiveHealthy, + incidentOpen: + outcome.transition === "open" + ? true + : outcome.transition === "resolve" + ? false + : acc.row.incidentOpen, + lastResolvedAtMs: null, + } + return { row, outcomes: [...acc.outcomes, outcome] } + }, + { + row: { + consecutiveBreaches: 0, + consecutiveHealthy: 0, + incidentOpen: false, + lastResolvedAtMs: null, + } as HysteresisRow, + outcomes: [] as ReadonlyArray<{ + transition: string + consecutiveBreaches: number + consecutiveHealthy: number + }>, + }, + ).outcomes + + const asLegacy = (transition: string) => + transition === "opened" ? "open" : transition === "resolved" ? "resolve" : "other" + expect(tiered.plans.map((p) => asLegacy(p.transition))).toEqual( + legacy.map((o) => + o.transition === "open" || o.transition === "resolve" ? o.transition : "other", + ), + ) + expect(tiered.plans.map((p) => [p.state.consecutiveBreaches, p.state.consecutiveHealthy])).toEqual( + legacy.map((o) => [o.consecutiveBreaches, o.consecutiveHealthy]), + ) + }) + + it("replays a pre-history state row from its counters", () => { + // One breach recorded by the old machine; the next breach must open (N = 2). + const next = plan({ + policy: { + consecutiveBreachesRequired: 2, + breachWindow: 2, + consecutiveHealthyRequired: 2, + renotifyIntervalMinutes: 60, + severities: ["critical"], + }, + evaluation: evaluate([critAt(10)], 15), + state: { consecutiveBreaches: 1, consecutiveHealthy: 0, history: null }, + openIncident: null, + nowMs: 0, + }) + expect(next).toMatchObject({ transition: "opened", severity: "critical" }) + }) +}) diff --git a/packages/alerting-core/src/tiers.ts b/packages/alerting-core/src/tiers.ts new file mode 100644 index 0000000000..799610514f --- /dev/null +++ b/packages/alerting-core/src/tiers.ts @@ -0,0 +1,194 @@ +import { + ALERT_SEVERITY_RANK, + alertSeverityOrder, + type AlertComparator, + type AlertSeverity, +} from "@maple/domain/http" +import { Array as Arr, Option } from "effect" + +/** + * Severity tiers: one rule, a warning and a critical threshold, each with an + * optional recovery value. Everything here is pure; the scheduler persists the + * tick history and the open incident carries the current severity. + */ + +/** Where one tick leaves one tier: past its trigger, inside the recovery band, or clear of it. */ +export type AlertTierStatus = "clear" | "hold" | "breach" + +export interface AlertThresholdTier { + readonly severity: AlertSeverity + readonly value: number + readonly upper: number | null + /** Null recovers as soon as the trigger stops holding. */ + readonly recoveryValue: number | null +} + +export interface AlertTierLevel { + readonly severity: AlertSeverity + readonly status: AlertTierStatus +} + +export interface TieredHysteresisPolicy { + /** N: breaching ticks needed to activate a tier. */ + readonly breachesToOpen: number + /** M: how many recent ticks the N are counted over. M = N is "N in a row". */ + readonly breachWindow: number + /** R: trailing clear ticks needed to deactivate a tier. */ + readonly healthyToResolve: number +} + +export const compareAlertThreshold = ( + value: number, + comparator: AlertComparator, + threshold: number, + thresholdUpper: number | null = null, +): boolean => { + switch (comparator) { + case "gt": + return value > threshold + case "gte": + return value >= threshold + case "lt": + return value < threshold + case "lte": + return value <= threshold + case "eq": + return value === threshold + case "neq": + return value !== threshold + case "between": + return thresholdUpper != null && value >= threshold && value <= thresholdUpper + case "not_between": + return thresholdUpper != null && (value < threshold || value > thresholdUpper) + } +} + +export const classifyTier = ( + value: number, + comparator: AlertComparator, + tier: AlertThresholdTier, +): AlertTierStatus => { + if (compareAlertThreshold(value, comparator, tier.value, tier.upper)) return "breach" + if (tier.recoveryValue === null) return "clear" + // Still on the breaching side of the recovery value: the dead band. + return compareAlertThreshold(value, comparator, tier.recoveryValue) ? "hold" : "clear" +} + +const STATUS_CODE = { clear: 0, hold: 1, breach: 2 } as const satisfies Record +const CODE_STATUS: ReadonlyArray = ["clear", "hold", "breach"] + +/** One tick as a small int: two bits per tier, warning low, critical high. */ +export const encodeTick = (levels: ReadonlyArray): number => + levels.reduce((code, level) => code | (STATUS_CODE[level.status] << shiftOf(level.severity)), 0) + +const tierStatusAt = (code: number, severity: AlertSeverity): AlertTierStatus => + CODE_STATUS[(code >> shiftOf(severity)) & 0b11] ?? "clear" + +const shiftOf = (severity: AlertSeverity) => (ALERT_SEVERITY_RANK[severity] - 1) * 2 + +const historyLength = (policy: TieredHysteresisPolicy) => + Math.max(policy.breachWindow, policy.breachesToOpen, policy.healthyToResolve) + +/** + * Append a tick, keeping only what the policy can still read. A steady rule + * produces the same array tick after tick, which is what lets the scheduler + * skip the state upsert. + */ +export const appendTick = ( + history: ReadonlyArray, + code: number, + policy: TieredHysteresisPolicy, +): ReadonlyArray => [...history, code].slice(-historyLength(policy)) + +const trailingRun = (history: ReadonlyArray, matches: (code: number) => boolean): number => + Option.match( + Arr.findLastIndex(history, (code) => !matches(code)), + { onNone: () => history.length, onSome: (index) => history.length - 1 - index }, + ) + +const anyBreach = (code: number) => + tierStatusAt(code, "warning") === "breach" || tierStatusAt(code, "critical") === "breach" + +/** + * The group's severity after the newest tick. A tier activates when that tick and N of the last M + * breach it; an active tier stays until its last R ticks are clear (`hold` counts as neither). + */ +export const resolveActiveSeverity = ( + history: ReadonlyArray, + severities: ReadonlyArray, + current: AlertSeverity | null, + policy: TieredHysteresisPolicy, +): AlertSeverity | null => { + const newest = history.at(-1) + if (newest === undefined) return current + const recent = history.slice(-policy.breachWindow) + const currentRank = current === null ? 0 : ALERT_SEVERITY_RANK[current] + const ordered = [...severities].sort((a, b) => ALERT_SEVERITY_RANK[b] - ALERT_SEVERITY_RANK[a]) + for (const severity of ordered) { + const breaches = recent.filter((code) => tierStatusAt(code, severity) === "breach").length + if (tierStatusAt(newest, severity) === "breach" && breaches >= policy.breachesToOpen) return severity + if (currentRank >= ALERT_SEVERITY_RANK[severity]) { + const cleared = trailingRun(history, (code) => tierStatusAt(code, severity) === "clear") + if (cleared < policy.healthyToResolve) return severity + } + } + return null +} + +/** Every tier past its recovery value: a tick in the recovery band is not healthy yet. */ +const allClear = (code: number) => + tierStatusAt(code, "warning") === "clear" && tierStatusAt(code, "critical") === "clear" + +/** The breach / healthy run lengths the audit trail and old readers expect. */ +export const countersFromHistory = ( + history: ReadonlyArray, + policy: TieredHysteresisPolicy, +): { readonly consecutiveBreaches: number; readonly consecutiveHealthy: number } => ({ + consecutiveBreaches: Math.min(trailingRun(history, anyBreach), policy.breachesToOpen), + consecutiveHealthy: Math.min(trailingRun(history, allClear), policy.healthyToResolve), +}) + +/** + * A history for a state row written before histories existed. The counters + * are trailing runs, at most one of them non-zero, so they replay exactly. + */ +export const seedHistory = ( + counters: { readonly consecutiveBreaches: number; readonly consecutiveHealthy: number }, + severity: AlertSeverity, + policy: TieredHysteresisPolicy, +): ReadonlyArray => { + const breach = encodeTick([{ severity, status: "breach" }]) + const ticks = [ + ...Array.from({ length: counters.consecutiveHealthy }, () => 0), + ...Array.from({ length: counters.consecutiveBreaches }, () => breach), + ] + return ticks.slice(-historyLength(policy)) +} + +/** + * The tier a severity reads from: its own, else the lowest one (a rule edited + * to drop the tier an open incident sits at). + */ +export const tierFor = ( + tiers: Arr.NonEmptyReadonlyArray, + severity: AlertSeverity, +): AlertThresholdTier => + tiers.find((tier) => tier.severity === severity) ?? Arr.headNonEmpty(Arr.sort(tiers, alertSeverityOrder)) + +/** + * The rule tier an incident at `severity` reads as: its own, else the one the rule kept + * (with two severities there is only one other). + */ +export const nearestSeverity = ( + severity: AlertSeverity, + severities: ReadonlyArray, +): AlertSeverity => (severities.includes(severity) ? severity : (severities[0] ?? severity)) + +export const maxSeverity = (a: AlertSeverity | null, b: AlertSeverity | null): AlertSeverity | null => + a === null ? b : b === null ? a : ALERT_SEVERITY_RANK[a] >= ALERT_SEVERITY_RANK[b] ? a : b + +/** The highest tier a tick breached, for the audit trail; null when none did. */ +export const highestBreachedLevel = (levels: ReadonlyArray): AlertSeverity | null => + levels + .filter((level) => level.status === "breach") + .reduce((highest, level) => maxSeverity(highest, level.severity), null) diff --git a/packages/backend/src/services/alerts/AlertDestinationDelivery.ts b/packages/backend/src/services/alerts/AlertDestinationDelivery.ts index 830d4e2452..e0f31c8471 100644 --- a/packages/backend/src/services/alerts/AlertDestinationDelivery.ts +++ b/packages/backend/src/services/alerts/AlertDestinationDelivery.ts @@ -170,7 +170,11 @@ export const makeAlertDestinationDelivery = (options: { threshold: context.threshold, thresholdUpper: context.thresholdUpper, windowMinutes: context.windowMinutes, + ...(context.ruleSeverities === undefined + ? undefined + : { severities: context.ruleSeverities }), }, + peakSeverity: context.incidentPeakSeverity ?? null, observed: { value: context.value, sampleCount: context.sampleCount, @@ -195,6 +199,7 @@ export const makeAlertDestinationDelivery = (options: { readonly incidentStatus: AlertDeliveryPayloadContext["incidentStatus"] readonly dedupeKey: string readonly rule: Record + readonly peakSeverity: AlertDeliveryPayloadContext["incidentPeakSeverity"] | null readonly observed: Record readonly template: AlertNotificationTemplate | null readonly chart: { diff --git a/packages/backend/src/services/alerts/AlertReadModelsService.ts b/packages/backend/src/services/alerts/AlertReadModelsService.ts index 3f6784e670..76671c8e41 100644 --- a/packages/backend/src/services/alerts/AlertReadModelsService.ts +++ b/packages/backend/src/services/alerts/AlertReadModelsService.ts @@ -161,6 +161,7 @@ const rowToIncidentDocument = (row: AlertIncidentRow) => groupKey: row.groupKey, signalType: decodeAlertSignalTypeSync(row.signalType), severity: decodeAlertSeveritySync(row.severity), + peakSeverity: row.peakSeverity === null ? null : decodeAlertSeveritySync(row.peakSeverity), status: decodeAlertIncidentStatusSync(row.status), comparator: decodeAlertComparatorSync(row.comparator), threshold: row.threshold, @@ -354,6 +355,7 @@ export class AlertReadModelsService extends Context.Service< ? null : decodeAlertIncidentIdSync(r.incidentId), incidentTransition: decodeAlertIncidentTransitionSync(rawTransition), + level: r.level === "" ? null : decodeAlertSeveritySync(r.level), evaluationDurationMs: r.evaluationDurationMs, errorMessage: r.errorMessage == null || r.errorMessage === "" ? null : r.errorMessage, diff --git a/packages/backend/src/services/alerts/AlertRuleModel.ts b/packages/backend/src/services/alerts/AlertRuleModel.ts index 03acef3a77..cb2d5b4a35 100644 --- a/packages/backend/src/services/alerts/AlertRuleModel.ts +++ b/packages/backend/src/services/alerts/AlertRuleModel.ts @@ -17,7 +17,11 @@ import { AlertRuleStoredConfigInvalidError, AlertSeverity as AlertSeveritySchema, AlertSignalType as AlertSignalTypeSchema, + AlertThresholds as AlertThresholdsSchema, AlertValidationError, + alertThresholdLegacyFields, + legacyAlertThreshold, + sortAlertThresholds, QueryBuilderQueryDraftSchema, UNGROUPED_GROUP_KEY, UserId, @@ -28,6 +32,7 @@ import { type AlertRuleUpsertRequest, type AlertSeverity, type AlertSignalType, + type AlertThresholds, type QueryBuilderQueryDraftPayload, type OrgId, } from "@maple/domain/http" @@ -36,6 +41,7 @@ import { AlertRules, type AlertRuleRow } from "@maple/db/tables" import { Array as Arr, Effect, Match, Result, Schema } from "effect" import type { AlertRuntimeApi } from "./AlertRuntime" import type { QueryBuilderDataSource } from "@maple/query-model" +import { validateBreachWindow, validateThresholds } from "./alert-thresholds" const StringArraySchema = Schema.Array(Schema.String) @@ -45,6 +51,7 @@ type StoredJsonColumn = | "environmentsJson" | "tagsJson" | "destinationIdsJson" + | "thresholdsJson" /** * An `alert_rules` row with its schema-typed jsonb columns read as `unknown`. The model @@ -63,6 +70,7 @@ export const selectStoredAlertRules = () => environmentsJson: PG.undecoded($.environmentsJson), tagsJson: PG.undecoded($.tagsJson), destinationIdsJson: PG.undecoded($.destinationIdsJson), + thresholdsJson: PG.undecoded($.thresholdsJson), })) const DestinationIdArraySchema = Schema.Array(AlertDestinationDocument.fields.id) const AlertGroupByFromJson = Schema.fromJsonString(AlertGroupBySchema) @@ -70,7 +78,6 @@ const AlertGroupByFromJson = Schema.fromJsonString(AlertGroupBySchema) const decodeAlertRuleIdSync = Schema.decodeUnknownSync(AlertRuleDocument.fields.id) const decodeQuerySpecSync = Schema.decodeUnknownSync(QuerySpec) const decodeIsoDateTimeStringSync = Schema.decodeUnknownSync(AlertDestinationDocument.fields.createdAt) -const decodeAlertSeveritySync = Schema.decodeUnknownSync(AlertSeveritySchema) const decodeAlertSignalTypeSync = Schema.decodeUnknownSync(AlertSignalTypeSchema) const decodeAlertComparatorSync = Schema.decodeUnknownSync(AlertComparatorSchema) const decodeQueryEngineAlertReducerSync = Schema.decodeUnknownSync(QueryEngineAlertReducer) @@ -82,6 +89,9 @@ export interface NormalizedRule { readonly name: string readonly notificationTemplate: AlertNotificationTemplate | null readonly enabled: boolean + /** Severity tiers, lowest first. The four single-threshold fields below derive from them. */ + readonly thresholds: AlertThresholds + /** Highest tier's severity. */ readonly severity: AlertSeverity readonly serviceName: string | null readonly serviceNames: ReadonlyArray @@ -96,6 +106,8 @@ export interface NormalizedRule { readonly windowMinutes: number readonly minimumSampleCount: number readonly consecutiveBreachesRequired: number + /** M in "N of the last M checks"; null is N in a row. */ + readonly breachesOutOf: number | null readonly consecutiveHealthyRequired: number readonly renotifyIntervalMinutes: number readonly apdexThresholdMs: number | null @@ -131,6 +143,7 @@ export const normalizedRuleToDocument = ( notificationTemplate: rule.notificationTemplate, enabled: rule.enabled, severity: rule.severity, + thresholds: rule.thresholds, serviceNames: [...rule.serviceNames], excludeServiceNames: [...rule.excludeServiceNames], environments: [...rule.environments], @@ -143,6 +156,7 @@ export const normalizedRuleToDocument = ( windowMinutes: rule.windowMinutes, minimumSampleCount: rule.minimumSampleCount, consecutiveBreachesRequired: rule.consecutiveBreachesRequired, + breachesOutOf: rule.breachesOutOf, consecutiveHealthyRequired: rule.consecutiveHealthyRequired, renotifyIntervalMinutes: rule.renotifyIntervalMinutes, apdexThresholdMs: rule.apdexThresholdMs, @@ -178,6 +192,10 @@ export const makeAlertValidationError = ( ...(!(cause === undefined) ? { cause } : undefined), }) +// The details ride in the message too: public HTTP errors expose `message`, not `details`. +const invalidRuleError = (details: ReadonlyArray) => + makeAlertValidationError(`Invalid alert rule: ${details.join("; ")}`, details) + const normalizeTags = (tags: ReadonlyArray | undefined): ReadonlyArray => { if (!tags || tags.length === 0) return [] return Arr.dedupe( @@ -565,6 +583,36 @@ export const decodeStoredAlertRuleDestinationIds = ( ), ) +/** The rule's tiers; a pre-tier row (`thresholds_json` null) is one tier from its legacy columns. */ +const decodeStoredAlertRuleThresholds = ( + row: StoredAlertRuleRow, + destinationIds: ReadonlyArray, +): Effect.Effect => + row.thresholdsJson === null + ? decodeStoredRuleComponent(row.id, "thresholds", AlertSeveritySchema, row.severity).pipe( + Effect.map((severity) => + Arr.of( + legacyAlertThreshold({ + severity, + threshold: row.threshold, + thresholdUpper: row.thresholdUpper, + destinationIds, + }), + ), + ), + ) + : decodeStoredRuleComponent(row.id, "thresholds", AlertThresholdsSchema, row.thresholdsJson).pipe( + Effect.map(sortAlertThresholds), + ) + +/** The stored condition ticks are judged against; none when the row no longer decodes. */ +export const storedRuleCondition = (row: StoredAlertRuleRow) => + decodeStoredAlertRuleDestinationIds(row.id, row.destinationIdsJson).pipe( + Effect.flatMap((destinationIds) => decodeStoredAlertRuleThresholds(row, destinationIds)), + Effect.map((thresholds) => ({ comparator: row.comparator, thresholds })), + Effect.option, + ) + export const rowToRuleDocument = ( row: StoredAlertRuleRow, evaluationState?: RuleEvaluationState, @@ -572,6 +620,8 @@ export const rowToRuleDocument = ( Effect.gen(function* () { const stored = yield* decodeStoredAlertRuleMetadata(row) const destinationIds = yield* decodeStoredAlertRuleDestinationIds(row.id, row.destinationIdsJson) + const thresholds = yield* decodeStoredAlertRuleThresholds(row, destinationIds) + const legacy = alertThresholdLegacyFields(thresholds) return yield* Effect.try({ try: () => { return new AlertRuleDocument({ @@ -580,7 +630,8 @@ export const rowToRuleDocument = ( notes: row.notes ?? null, notificationTemplate: stored.notificationTemplate, enabled: row.enabled, - severity: decodeAlertSeveritySync(row.severity), + severity: legacy.severity, + thresholds, serviceNames: [...stored.serviceNames], excludeServiceNames: [...stored.excludeServiceNames], environments: [...stored.environments], @@ -588,11 +639,12 @@ export const rowToRuleDocument = ( groupBy: stored.groupBy, signalType: decodeAlertSignalTypeSync(row.signalType), comparator: decodeAlertComparatorSync(row.comparator), - threshold: row.threshold, - thresholdUpper: row.thresholdUpper, + threshold: legacy.threshold, + thresholdUpper: legacy.thresholdUpper, windowMinutes: row.windowMinutes, minimumSampleCount: row.minimumSampleCount, consecutiveBreachesRequired: row.consecutiveBreachesRequired, + breachesOutOf: row.breachesOutOf, consecutiveHealthyRequired: row.consecutiveHealthyRequired, renotifyIntervalMinutes: row.renotifyIntervalMinutes, apdexThresholdMs: row.apdexThresholdMs, @@ -602,7 +654,7 @@ export const rowToRuleDocument = ( row.signalType === "raw_query" ? decodeQueryEngineAlertReducerSync(row.reducer) : null, - destinationIds, + destinationIds: legacy.destinationIds, noDataBehavior: decodeNoDataBehaviorSync(row.noDataBehavior), lastEvaluationError: evaluationState?.error ?? null, lastEvaluatedAt: @@ -641,7 +693,6 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { id: decodeAlertRuleIdSync(row.id), serviceName: stored.serviceNames.length === 1 ? (stored.serviceNames[0] ?? null) : null, signalType: decodeAlertSignalTypeSync(row.signalType), - severity: decodeAlertSeveritySync(row.severity), comparator: decodeAlertComparatorSync(row.comparator), groupBy: stored.groupBy, rawQueryReducer: @@ -658,12 +709,18 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { cause, }), }) + const thresholds = yield* decodeStoredAlertRuleThresholds( + row, + yield* decodeStoredAlertRuleDestinationIds(row.id, row.destinationIdsJson), + ) + const legacy = alertThresholdLegacyFields(thresholds) return { id: decoded.id, name: row.name, notificationTemplate: stored.notificationTemplate, enabled: row.enabled, - severity: decoded.severity, + thresholds, + severity: legacy.severity, serviceName: decoded.serviceName, serviceNames: stored.serviceNames, excludeServiceNames: stored.excludeServiceNames, @@ -672,11 +729,12 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { groupBy: decoded.groupBy, signalType: decoded.signalType, comparator: decoded.comparator, - threshold: row.threshold, - thresholdUpper: row.thresholdUpper, + threshold: legacy.threshold, + thresholdUpper: legacy.thresholdUpper, windowMinutes: row.windowMinutes, minimumSampleCount: row.minimumSampleCount, consecutiveBreachesRequired: row.consecutiveBreachesRequired, + breachesOutOf: row.breachesOutOf, consecutiveHealthyRequired: row.consecutiveHealthyRequired, renotifyIntervalMinutes: row.renotifyIntervalMinutes, apdexThresholdMs: row.apdexThresholdMs, @@ -684,7 +742,7 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { rawQuerySql: row.rawQuerySql ?? null, rawQueryReducer: decoded.rawQueryReducer, alertOnNoData: row.noDataBehavior === "alert", - destinationIds: yield* decodeStoredAlertRuleDestinationIds(row.id, row.destinationIdsJson), + destinationIds: legacy.destinationIds, compiledPlan: yield* parseCompiledPlan(row), createdAt: row.createdAt, updatedAt: row.updatedAt, @@ -725,25 +783,44 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { : [] const tags = normalizeTags(request.tags) const groupBy = request.groupBy ?? null - const destinationIds = Arr.dedupe(request.destinationIds) const details: string[] = [] if (name.length === 0 && !options?.forPreview) details.push("name is required") - if (destinationIds.length === 0 && !options?.forPreview) { - details.push("at least one destination must be selected") - } - if (request.threshold == null || !Number.isFinite(request.threshold)) { + // `thresholds` wins; the single-threshold shape is still one tier. + const tiered = request.thresholds !== undefined + const requestedThresholds: AlertThresholds | null = + request.thresholds !== undefined + ? request.thresholds + : request.threshold === undefined + ? null + : Arr.of( + legacyAlertThreshold({ + severity: request.severity ?? "warning", + threshold: request.threshold, + thresholdUpper: request.thresholdUpper ?? null, + destinationIds: request.destinationIds ?? [], + }), + ) + if (requestedThresholds === null) { details.push("threshold must be a finite number") + } else { + if (!tiered && request.severity === undefined) details.push("severity is required") + details.push( + ...validateThresholds(request.comparator, requestedThresholds, { + label: (index, field) => + tiered + ? `thresholds[${index}].${field}` + : field === "value" + ? "threshold" + : field === "upper" + ? "thresholdUpper" + : "recoveryValue", + requireDestinations: !options?.forPreview, + }), + ) } - const isRange = request.comparator === "between" || request.comparator === "not_between" - if (isRange) { - if (request.thresholdUpper == null || !Number.isFinite(request.thresholdUpper)) { - details.push("thresholdUpper is required for between / not_between comparators") - } else if (request.threshold != null && request.thresholdUpper < request.threshold) { - details.push("thresholdUpper must be greater than or equal to threshold") - } - } else if (request.thresholdUpper != null) { - details.push("thresholdUpper is only supported for between / not_between comparators") - } + const consecutiveBreachesRequired = request.consecutiveBreachesRequired ?? 2 + const breachesOutOf = request.breachesOutOf ?? null + details.push(...validateBreachWindow(consecutiveBreachesRequired, breachesOutOf)) if (request.signalType === "builder_query" && !request.queryBuilderDraft) { details.push("queryBuilderDraft is required for builder_query alerts") } @@ -774,9 +851,11 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { ) { details.push('excludeServiceNames requires groupBy=["service.name"]') } - if (details.length > 0) { - return yield* Effect.fail(makeAlertValidationError("Invalid alert rule", details)) + // A null tier list always left a detail behind; the check is for the narrowing. + if (details.length > 0 || requestedThresholds === null) { + return yield* Effect.fail(invalidRuleError(details)) } + const thresholds = sortAlertThresholds(requestedThresholds) if (request.signalType === "raw_query") { yield* prepareRawSql({ sql: request.rawQuerySql ?? "", @@ -793,12 +872,14 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { } const nowMs = yield* runtime.now + const legacy = alertThresholdLegacyFields(thresholds) const normalizedBase = { id: decodeAlertRuleIdSync(runtime.makeUuid()), name, notificationTemplate: request.notificationTemplate ?? null, enabled: request.enabled ?? true, - severity: request.severity, + thresholds, + severity: legacy.severity, serviceName, serviceNames, excludeServiceNames, @@ -807,11 +888,12 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { groupBy, signalType: request.signalType, comparator: request.comparator, - threshold: request.threshold, - thresholdUpper: request.thresholdUpper ?? null, + threshold: legacy.threshold, + thresholdUpper: legacy.thresholdUpper, windowMinutes: request.windowMinutes, minimumSampleCount: request.minimumSampleCount ?? 0, - consecutiveBreachesRequired: request.consecutiveBreachesRequired ?? 2, + consecutiveBreachesRequired, + breachesOutOf, consecutiveHealthyRequired: request.consecutiveHealthyRequired ?? 2, renotifyIntervalMinutes: request.renotifyIntervalMinutes ?? 30, apdexThresholdMs: request.apdexThresholdMs ?? (request.signalType === "apdex" ? 500 : null), @@ -820,7 +902,7 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { request.signalType === "raw_query" ? normalizeOptionalString(request.rawQuerySql) : null, rawQueryReducer: request.signalType === "raw_query" ? (request.rawQueryReducer ?? null) : null, alertOnNoData: request.alertOnNoData ?? false, - destinationIds, + destinationIds: legacy.destinationIds, createdAt: nowMs, updatedAt: nowMs, } @@ -829,7 +911,7 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { // liveness check; an empty result would breach under no real group. if (normalizedBase.alertOnNoData && planGroupingTokens(compiledPlan) != null) { return yield* Effect.fail( - makeAlertValidationError("Invalid alert rule", [ + invalidRuleError([ "alertOnNoData is not supported on grouped rules: a group that stops reporting is already held open by its incident's telemetry check", ]), ) diff --git a/packages/backend/src/services/alerts/AlertRulesService.ts b/packages/backend/src/services/alerts/AlertRulesService.ts index 37a93698a8..3b626c9d48 100644 --- a/packages/backend/src/services/alerts/AlertRulesService.ts +++ b/packages/backend/src/services/alerts/AlertRulesService.ts @@ -25,7 +25,7 @@ import { AlertRules, AlertRuleStates, } from "@maple/db/tables" -import { Array as Arr, Context, Effect, HashSet, Layer, Schema } from "effect" +import { Array as Arr, Context, Effect, HashSet, Layer, Option, Schema } from "effect" import { Database, type DatabaseApi } from "@maple/backend/platform/DatabaseLive" import { makeDbExecute } from "@maple/backend/platform/db-execute" import { currentTxid, readTxid } from "@maple/backend/platform/electric-txid" @@ -36,8 +36,10 @@ import { normalizeOptionalString, rowToRuleDocument, selectStoredAlertRules, + storedRuleCondition, type RuleEvaluationState, } from "./AlertRuleModel" +import { tierConditionChanged } from "./alert-thresholds" import { makePersistenceError } from "./alert-persistence" import { AlertRuntime, type AlertRuntimeApi } from "./AlertRuntime" @@ -169,9 +171,11 @@ export const makeAlertRulePersistence = (options: { comparator: normalized.comparator, threshold: normalized.threshold, thresholdUpper: normalized.thresholdUpper, + thresholdsJson: normalized.thresholds, windowMinutes: normalized.windowMinutes, minimumSampleCount: normalized.minimumSampleCount, consecutiveBreachesRequired: normalized.consecutiveBreachesRequired, + breachesOutOf: normalized.breachesOutOf, consecutiveHealthyRequired: normalized.consecutiveHealthyRequired, renotifyIntervalMinutes: normalized.renotifyIntervalMinutes, apdexThresholdMs: normalized.apdexThresholdMs, @@ -230,6 +234,22 @@ export const makeAlertRulePersistence = (options: { } } + const [previousRow] = + existingId == null + ? [] + : yield* db.run( + selectStoredAlertRules().where(($) => [ + $.orgId.eq(orgId), + $.id.eq(existingId), + ]), + ) + const previous = + previousRow === undefined ? Option.none() : yield* storedRuleCondition(previousRow) + const conditionChanged = Option.match(previous, { + onNone: () => true, + onSome: (condition) => tierConditionChanged(condition, normalized), + }) + return existingId == null ? yield* db.run( PG.insertInto(AlertRules) @@ -248,12 +268,34 @@ export const makeAlertRulePersistence = (options: { db.run( PG.deleteFrom(AlertRuleClaims).where(($) => [$.ruleId.eq(existingId)]), ), - db.run( - PG.update(AlertRules) - .set(ruleFields) - .where(($) => [$.orgId.eq(orgId), $.id.eq(existingId)]) - .returning(() => ({ txid: currentTxid })), - ), + db + .run( + PG.update(AlertRules) + .set(ruleFields) + .where(($) => [$.orgId.eq(orgId), $.id.eq(existingId)]) + .returning(() => ({ txid: currentTxid })), + ) + .pipe( + // Ticks judged against the old condition must not count toward the new one. + // Read under the lock, so a concurrent edit cannot restore it unnoticed. + Effect.tap(() => + conditionChanged + ? db.run( + PG.update(AlertRuleStates) + .set({ + historyJson: [], + consecutiveBreaches: 0, + consecutiveHealthy: 0, + updatedAt: timestamp, + }) + .where(($) => [ + $.orgId.eq(orgId), + $.ruleId.eq(existingId), + ]), + ) + : Effect.void, + ), + ), ) }), ), diff --git a/packages/backend/src/services/alerts/AlertsService.test.ts b/packages/backend/src/services/alerts/AlertsService.test.ts index 82bc0cf267..836f63dcda 100644 --- a/packages/backend/src/services/alerts/AlertsService.test.ts +++ b/packages/backend/src/services/alerts/AlertsService.test.ts @@ -1212,6 +1212,526 @@ describe("AlertsService", () => { }).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub(state), { fetch: okFetch }))) }) + describe("severity tiers", () => { + const errorRateRows = (errorRate: number) => [ + tracesRow({ + count: 200, + avgDuration: 40, + p50Duration: 20, + p95Duration: 120, + p99Duration: 240, + errorRate, + satisfiedCount: 180, + toleratingCount: 10, + apdexScore: 0.925, + }), + ] + + const createTieredSetup = (alerts: AlertsServiceApi, orgId: OrgId, userId: UserId) => + Effect.gen(function* () { + const warnHook = yield* alerts.createDestination(orgId, userId, adminRoles, { + type: "webhook", + name: "Team channel", + enabled: true, + url: "https://example.com/warn", + signingSecret: "warn-secret", + }) + const pageHook = yield* alerts.createDestination(orgId, userId, adminRoles, { + type: "webhook", + name: "Pager", + enabled: true, + url: "https://example.com/page", + signingSecret: "page-secret", + }) + const rule = yield* alerts.createRule( + orgId, + userId, + adminRoles, + new AlertRuleUpsertRequest({ + name: "Checkout error rate tiers", + enabled: true, + serviceNames: ["checkout"], + signalType: "error_rate", + comparator: "gt", + thresholds: [ + { + severity: "critical", + value: 20, + upper: null, + recoveryValue: null, + destinationIds: [pageHook.id], + }, + { + severity: "warning", + value: 5, + upper: null, + recoveryValue: null, + destinationIds: [warnHook.id], + }, + ], + windowMinutes: 5, + minimumSampleCount: 10, + consecutiveBreachesRequired: 2, + consecutiveHealthyRequired: 2, + renotifyIntervalMinutes: 30, + }), + ) + return { warnHook, pageHook, rule } + }) + + it.effect("opens at warning, pages on escalation, and resolves to everyone it notified", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(10) as ReadonlyArray> } + const requests: Array<{ url: string; eventType: string | null }> = [] + const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ + url: String(input), + eventType: new Headers(init?.headers).get("x-maple-event-type"), + }) + return new Response("ok", { status: 200 }) + }) as typeof fetch + + const tick = (alerts: AlertsServiceApi) => + Effect.gen(function* () { + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + }) + + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers") + const userId = asUserId("user_tiers") + const { rule } = yield* createTieredSetup(alerts, orgId, userId) + + // Stored lowest tier first; the single-threshold fields mirror it. + assert.deepStrictEqual( + rule.thresholds.map((tier) => tier.severity), + ["warning", "critical"], + ) + assert.strictEqual(rule.severity, "critical") + assert.strictEqual(rule.threshold, 5) + + yield* alerts.runSchedulerTick() + yield* tick(alerts) + const opened = (yield* alerts.listIncidents(orgId)).incidents[0] + assert.strictEqual(opened?.severity, "warning") + assert.deepStrictEqual(requests, [{ url: "https://example.com/warn", eventType: "trigger" }]) + + state.tracesAggregateRows = errorRateRows(30) + yield* tick(alerts) + yield* tick(alerts) + const escalated = (yield* alerts.listIncidents(orgId)).incidents[0] + assert.strictEqual(escalated?.id, opened?.id) + assert.strictEqual(escalated?.severity, "critical") + assert.strictEqual(escalated?.peakSeverity, "critical") + assert.strictEqual(escalated?.threshold, 20) + // The linked issue follows the escalation while the detector owns its severity. + const issues = yield* Effect.promise(() => + testDb.pglite.query<{ severity: string }>( + "select severity from error_issues where org_id = $1 and kind = 'alert'", + [orgId], + ), + ) + assert.deepStrictEqual( + issues.rows.map((issue) => issue.severity), + ["critical"], + ) + assert.sameMembers( + requests.slice(1).map((request) => `${request.eventType} ${request.url}`), + ["trigger https://example.com/warn", "trigger https://example.com/page"], + ) + + state.tracesAggregateRows = errorRateRows(0.5) + yield* tick(alerts) + yield* tick(alerts) + const resolved = (yield* alerts.listIncidents(orgId)).incidents[0] + assert.strictEqual(resolved?.status, "resolved") + assert.sameMembers( + requests.slice(3).map((request) => `${request.eventType} ${request.url}`), + ["resolve https://example.com/warn", "resolve https://example.com/page"], + ) + }).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub(state), { fetch: fetchImpl }))) + }) + + const recordingFetch = (requests: Array) => + (async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push(`${new Headers(init?.headers).get("x-maple-event-type")} ${String(input)}`) + return new Response("ok", { status: 200 }) + }) as typeof fetch + + it.effect("reminds only the current tier after a de-escalation", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(30) as ReadonlyArray> } + const requests: Array = [] + const tick = (alerts: AlertsServiceApi, minutes = 1) => + Effect.gen(function* () { + yield* TestClock.adjust(Duration.minutes(minutes)) + yield* alerts.runSchedulerTick() + }) + + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_remind") + yield* createTieredSetup(alerts, orgId, asUserId("user_tiers_remind")) + + yield* alerts.runSchedulerTick() + yield* tick(alerts) + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.severity, "critical") + + state.tracesAggregateRows = errorRateRows(10) + yield* tick(alerts) + yield* tick(alerts) + const deescalated = (yield* alerts.listIncidents(orgId)).incidents[0] + assert.strictEqual(deescalated?.severity, "warning") + assert.strictEqual(deescalated?.peakSeverity, "critical") + // The pager that heard the critical hears it calm down. + assert.sameMembers(requests.slice(2), [ + "renotify https://example.com/warn", + "renotify https://example.com/page", + ]) + + yield* tick(alerts, 31) + assert.deepStrictEqual(requests.slice(4), ["renotify https://example.com/warn"]) + }).pipe( + Effect.provide( + makeLayer(testDb, makeWarehouseStub(state), { fetch: recordingFetch(requests) }), + ), + ) + }) + + it.effect("resolves a warning incident ended by a rule edit without paging", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(10) as ReadonlyArray> } + const requests: Array = [] + + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_edit") + const userId = asUserId("user_tiers_edit") + const { rule } = yield* createTieredSetup(alerts, orgId, userId) + + yield* alerts.runSchedulerTick() + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + assert.deepStrictEqual(requests, ["trigger https://example.com/warn"]) + + yield* alerts.updateRule( + orgId, + userId, + adminRoles, + rule.id, + new AlertRuleUpsertRequest({ + name: rule.name, + enabled: false, + serviceNames: [...rule.serviceNames], + signalType: rule.signalType, + comparator: rule.comparator, + thresholds: rule.thresholds, + windowMinutes: rule.windowMinutes, + minimumSampleCount: rule.minimumSampleCount, + consecutiveBreachesRequired: rule.consecutiveBreachesRequired, + consecutiveHealthyRequired: rule.consecutiveHealthyRequired, + renotifyIntervalMinutes: rule.renotifyIntervalMinutes, + }), + ) + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "resolved") + assert.deepStrictEqual(requests.slice(1), ["resolve https://example.com/warn"]) + }).pipe( + Effect.provide( + makeLayer(testDb, makeWarehouseStub(state), { fetch: recordingFetch(requests) }), + ), + ) + }) + + it.effect("resolves to the remaining tier after the incident's tier is removed", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(10) as ReadonlyArray> } + const requests: Array = [] + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_removed") + const userId = asUserId("user_tiers_removed") + const { rule, pageHook } = yield* createTieredSetup(alerts, orgId, userId) + const tick = Effect.gen(function* () { + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + }) + + yield* alerts.runSchedulerTick() + yield* tick + assert.deepStrictEqual(requests, ["trigger https://example.com/warn"]) + + yield* alerts.updateRule( + orgId, + userId, + adminRoles, + rule.id, + new AlertRuleUpsertRequest({ + name: rule.name, + serviceNames: [...rule.serviceNames], + signalType: rule.signalType, + comparator: rule.comparator, + thresholds: [ + { + severity: "critical", + value: 20, + upper: null, + recoveryValue: null, + destinationIds: [pageHook.id], + }, + ], + windowMinutes: rule.windowMinutes, + minimumSampleCount: rule.minimumSampleCount, + consecutiveBreachesRequired: rule.consecutiveBreachesRequired, + consecutiveHealthyRequired: rule.consecutiveHealthyRequired, + renotifyIntervalMinutes: rule.renotifyIntervalMinutes, + }), + ) + state.tracesAggregateRows = errorRateRows(0.5) + yield* tick + yield* tick + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "resolved") + // The remaining tier's pager, and the webhook that heard the trigger. + assert.sameMembers(requests.slice(1), [ + "resolve https://example.com/page", + "resolve https://example.com/warn", + ]) + }).pipe( + Effect.provide( + makeLayer(testDb, makeWarehouseStub(state), { fetch: recordingFetch(requests) }), + ), + ) + }) + + it.effect("resolves to a destination an edit removed after it was paged", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(30) as ReadonlyArray> } + const requests: Array = [] + + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_removed_dest") + const userId = asUserId("user_tiers_removed_dest") + const { rule, warnHook } = yield* createTieredSetup(alerts, orgId, userId) + + yield* alerts.runSchedulerTick() + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + assert.sameMembers(requests, [ + "trigger https://example.com/warn", + "trigger https://example.com/page", + ]) + + // Same condition, but the critical tier no longer pages the pager. + yield* alerts.updateRule( + orgId, + userId, + adminRoles, + rule.id, + new AlertRuleUpsertRequest({ + name: rule.name, + enabled: true, + serviceNames: [...rule.serviceNames], + signalType: rule.signalType, + comparator: rule.comparator, + thresholds: [ + rule.thresholds[0], + { ...rule.thresholds[1]!, destinationIds: [warnHook.id] }, + ], + windowMinutes: rule.windowMinutes, + minimumSampleCount: rule.minimumSampleCount, + consecutiveBreachesRequired: rule.consecutiveBreachesRequired, + consecutiveHealthyRequired: rule.consecutiveHealthyRequired, + renotifyIntervalMinutes: rule.renotifyIntervalMinutes, + }), + ) + + state.tracesAggregateRows = errorRateRows(0.5) + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "resolved") + assert.include(requests, "resolve https://example.com/page") + }).pipe( + Effect.provide( + makeLayer(testDb, makeWarehouseStub(state), { fetch: recordingFetch(requests) }), + ), + ) + }) + + it.effect("keeps counting ticks across an edit that leaves the condition alone", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(10) as ReadonlyArray> } + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_rename") + const userId = asUserId("user_tiers_rename") + const { rule } = yield* createTieredSetup(alerts, orgId, userId) + + yield* alerts.runSchedulerTick() + yield* alerts.updateRule( + orgId, + userId, + adminRoles, + rule.id, + new AlertRuleUpsertRequest({ + name: "Renamed", + serviceNames: [...rule.serviceNames], + signalType: rule.signalType, + comparator: rule.comparator, + thresholds: rule.thresholds, + windowMinutes: rule.windowMinutes, + minimumSampleCount: rule.minimumSampleCount, + consecutiveBreachesRequired: rule.consecutiveBreachesRequired, + consecutiveHealthyRequired: rule.consecutiveHealthyRequired, + renotifyIntervalMinutes: rule.renotifyIntervalMinutes, + }), + ) + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + // The breach before the rename still counts: two in a row open the incident. + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.severity, "warning") + }).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub(state), { fetch: okFetch }))) + }) + + it.effect("does not count ticks judged against an edited threshold", () => { + const testDb = createTestDb(trackedDbs) + const state = { tracesAggregateRows: errorRateRows(10) as ReadonlyArray> } + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_edit_history") + const userId = asUserId("user_tiers_edit_history") + const destination = yield* createWebhookDestination(alerts, orgId, userId) + const request = (value: number) => + new AlertRuleUpsertRequest({ + name: "Two of five", + serviceNames: ["checkout"], + signalType: "error_rate", + comparator: "gt", + thresholds: [ + { + severity: "warning", + value, + upper: null, + recoveryValue: null, + destinationIds: [destination.id], + }, + ], + windowMinutes: 5, + minimumSampleCount: 10, + consecutiveBreachesRequired: 2, + breachesOutOf: 5, + }) + const rule = yield* alerts.createRule(orgId, userId, adminRoles, request(5)) + const tick = Effect.gen(function* () { + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + }) + + // One breach, then a healthy tick, both against the old threshold of 5. + yield* alerts.runSchedulerTick() + state.tracesAggregateRows = errorRateRows(1) + yield* tick + yield* alerts.updateRule(orgId, userId, adminRoles, rule.id, request(15)) + + // A single breach of the new threshold is one of five, not two. + state.tracesAggregateRows = errorRateRows(30) + yield* tick + assert.lengthOf((yield* alerts.listIncidents(orgId)).incidents, 0) + yield* tick + assert.lengthOf((yield* alerts.listIncidents(orgId)).incidents, 1) + }).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub(state), { fetch: okFetch }))) + }) + + it.effect( + "records the critical tier's threshold on the issue of an incident that opens at critical", + () => { + const testDb = createTestDb(trackedDbs) + const state = { + tracesAggregateRows: errorRateRows(30) as ReadonlyArray>, + } + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_issue") + yield* createTieredSetup(alerts, orgId, asUserId("user_tiers_issue")) + + yield* alerts.runSchedulerTick() + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + + const incident = (yield* alerts.listIncidents(orgId)).incidents[0] + assert.strictEqual(incident?.severity, "critical") + assert.strictEqual(incident?.threshold, 20) + const issue = yield* Effect.promise(() => + queryFirstRow<{ exception_message: string }>( + testDb, + "select exception_message from error_issues where org_id = $1 and kind = 'alert'", + [orgId], + ), + ) + assert.include(issue?.exception_message ?? "", "gt 20") + }).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub(state), { fetch: okFetch }))) + }, + ) + + it.effect("rejects a critical threshold that is not worse than the warning one", () => { + const testDb = createTestDb(trackedDbs) + return Effect.gen(function* () { + const alerts = yield* AlertsService + const orgId = asOrgId("org_tiers_invalid") + const userId = asUserId("user_tiers_invalid") + const destination = yield* createWebhookDestination(alerts, orgId, userId) + const exit = yield* alerts + .createRule( + orgId, + userId, + adminRoles, + new AlertRuleUpsertRequest({ + name: "Inverted tiers", + signalType: "error_rate", + comparator: "gt", + thresholds: [ + { + severity: "warning", + value: 20, + upper: null, + recoveryValue: 25, + destinationIds: [destination.id], + }, + { + severity: "critical", + value: 5, + upper: null, + recoveryValue: null, + destinationIds: [destination.id], + }, + ], + windowMinutes: 5, + }), + ) + .pipe(Effect.exit) + assert.isTrue(Exit.isFailure(exit)) + const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : null + assert.instanceOf(error, AlertValidationError) + assert.includeMembers(error instanceof AlertValidationError ? [...error.details] : [], [ + "the critical threshold must be worse than the warning threshold", + "thresholds[0].recoveryValue must be on the healthy side of thresholds[0].value", + ]) + }).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub({ tracesAggregateRows: [] })))) + }) + }) + it.effect("suppresses trigger and resolve notifications while an incident flaps", () => { const testDb = createTestDb(trackedDbs) const breachedRows = [ diff --git a/packages/backend/src/services/alerts/AlertsService.ts b/packages/backend/src/services/alerts/AlertsService.ts index 2efc04f9b8..8855b47699 100644 --- a/packages/backend/src/services/alerts/AlertsService.ts +++ b/packages/backend/src/services/alerts/AlertsService.ts @@ -2,8 +2,11 @@ import { alertDeliveryRetryDelayMs, canRetryAlertDelivery, evaluateAlertObservation, + highestBreachedLevel, interleaveAlertRulesByTenant, makeAlertDeliveryKey, + maxSeverity, + nearestSeverity, planAlertLifecycle, type AlertLifecycleInput, } from "@maple/alerting-core" @@ -43,6 +46,7 @@ import { AlertRulePreviewSeries, type AlertRulePreviewRequest, AlertSeverity as AlertSeveritySchema, + type AlertSeverity, AlertSignalType as AlertSignalTypeSchema, AlertValidationError, AlertNotificationTemplate, @@ -100,7 +104,7 @@ import { } from "effect" import { HttpClient } from "effect/http" import * as AlertingMetrics from "@maple/backend/observability/AlertingMetrics" -import { upsertAlertIssue } from "@maple/backend/services/errors/issue-hub" +import { escalateAlertIssue, upsertAlertIssue } from "@maple/backend/services/errors/issue-hub" import { holdCeilingMs, type LivenessReason, @@ -111,6 +115,7 @@ import { simulateFiringSpans } from "./alert-firing-spans" import { ChatSessions } from "@maple/backend/platform/bindings" import { Database } from "@maple/backend/platform/DatabaseLive" import { formatComparator } from "./alert-formatting" +import { ruleAtSeverity } from "./alert-thresholds" import { buildChatDigestBlocks, CHAT_DIGEST_THRESHOLD, type ChatDigestItem } from "./delivery/chat-digest" import { makeIncidentPushBudget, type IncidentPushBudget } from "./alert-push-budget" import { EmailService } from "@maple/backend/platform/EmailService" @@ -134,6 +139,7 @@ import { AlertReadModelsService, type AlertReadModelsServiceApi } from "./AlertR import { AlertRulesService, makeAlertRulePersistence, type AlertRulesServiceApi } from "./AlertRulesService" import { decodeStoredAlertRuleMetadata, + storedRuleCondition, isGroupedPlan, toStorageGroupKey, makeAlertValidationError as makeValidationError, @@ -218,6 +224,56 @@ const afterHold = ( ? null : { reason: decodeAlertIncidentHoldReasonSync(incident.holdReason), heldForMs } +const decodeAlertSeverityOption = Schema.decodeUnknownOption(AlertSeveritySchema) + +/** + * The tier fields a delivery dispatches with. The rule's tiers come from the + * queued event when it carries them, so the key an event pages under cannot + * change between queueing and a retry; older payloads fall back to the rule as + * it is now. The peak is the higher of the event's and the incident's, so an + * alert opened later still closes. + */ +const dispatchTierFields = ( + queuedSeverities: ReadonlyArray | undefined, + queuedPeak: string | null, + incidentPeak: string | null, + currentRule: Option.Option<{ readonly thresholds: ReadonlyArray<{ readonly severity: AlertSeverity }> }>, +) => { + const ruleSeverities = + queuedSeverities === undefined + ? Option.map(currentRule, (condition) => condition.thresholds.map((tier) => tier.severity)) + : Option.some( + queuedSeverities.flatMap((severity) => + Option.toArray(decodeAlertSeverityOption(severity)), + ), + ) + const peak = maxSeverity( + Option.getOrNull(decodeAlertSeverityOption(queuedPeak)), + Option.getOrNull(decodeAlertSeverityOption(incidentPeak)), + ) + return { + ...Option.match(ruleSeverities, { + onNone: () => undefined, + onSome: (value) => ({ ruleSeverities: value }), + }), + ...(peak === null ? undefined : { incidentPeakSeverity: peak }), + } +} + +/** The rule as an incident's resolve sees it: its tier (or the one the rule kept), up to its peak. */ +const ruleForResolve = ( + rule: NormalizedRule, + incident: Pick, +) => + ruleAtSeverity( + rule, + nearestSeverity( + decodeAlertSeveritySync(incident.severity), + rule.thresholds.map((tier) => tier.severity), + ), + incident.peakSeverity === null ? null : decodeAlertSeveritySync(incident.peakSeverity), + ) + const describeHold = (reason: Exclude): string => { switch (reason) { case "volume_collapsed": @@ -291,8 +347,10 @@ const StoredDeliveryPayloadSchema = Schema.StructWithRest( threshold: Schema.optionalKey(Schema.Number), thresholdUpper: Schema.optionalKey(Schema.NullOr(Schema.Number)), windowMinutes: Schema.optionalKey(Schema.Number), + severities: Schema.optionalKey(Schema.Array(Schema.String)), }), ), + peakSeverity: Schema.optionalKey(Schema.NullOr(Schema.String)), observed: Schema.optionalKey( Schema.Struct({ value: Schema.optionalKey(Schema.NullOr(Schema.Number)), @@ -814,6 +872,7 @@ export class AlertsService extends Context.Service + db.run( + PG.from(AlertDeliveryEvents) + .select("destinationId") + .where(($) => [$.orgId.eq(orgId), $.incidentId.eq(incident.id)]), + ), + )).map((row) => row.destinationId) + : [] + const destinationIds = Arr.dedupe([...rule.destinationIds, ...priorDestinationIds]) + if (destinationIds.length === 0) return [] const rows = yield* dbExecute((db) => db.run( PG.from(AlertDestinations) .select("id", "enabled") - .where(($) => [$.orgId.eq(orgId), $.id.in_(...rule.destinationIds)]), + .where(($) => [$.orgId.eq(orgId), $.id.in_(...destinationIds)]), ), ) @@ -1093,6 +1165,12 @@ export class AlertsService extends Context.Service tier.severity), + ...(incident.peakSeverity === null + ? undefined + : { incidentPeakSeverity: decodeAlertSeveritySync(incident.peakSeverity) }), comparator: rule.comparator, threshold: rule.threshold, thresholdUpper: rule.thresholdUpper, @@ -1108,23 +1186,21 @@ export class AlertsService extends Context.Service makeValidationError(error.message, [], error))) - return rule.destinationIds.flatMap( - (destinationId): ReadonlyArray => { - const destination = destinations.get(destinationId) - if (!destination || !destination.enabled) return [] - return [ - { - orgId, - incidentId: incident.id, - ruleId: rule.id, - destinationId, - eventType, - payload, - scheduledAt, - }, - ] - }, - ) + return destinationIds.flatMap((destinationId): ReadonlyArray => { + const destination = destinations.get(destinationId) + if (!destination || !destination.enabled) return [] + return [ + { + orgId, + incidentId: incident.id, + ruleId: rule.id, + destinationId, + eventType, + payload, + scheduledAt, + }, + ] + }) }) const insertIncidentDeliveries = ( @@ -1549,9 +1625,7 @@ export class AlertsService extends Context.Service - new AlertRulePreviewPoint({ - bucket: iso(bucketMs), - value, - sampleCount, - status, - ...(skipReason === undefined ? undefined : { skipReason }), - ...(provisional ? { provisional } : undefined), - }), - ), + points: evaluations.map(({ bucketMs, evaluation, sampleCount, provisional }) => { + const level = highestBreachedLevel(evaluation.levels) + return new AlertRulePreviewPoint({ + bucket: iso(bucketMs), + value: evaluation.value, + sampleCount, + status: evaluation.status, + ...(level === null ? undefined : { level }), + ...(evaluation.skipReason === undefined + ? undefined + : { skipReason: evaluation.skipReason }), + ...(provisional ? { provisional } : undefined), + }) + }), }) // The would-fire shading is the scheduler's own state machine replayed @@ -1583,6 +1660,7 @@ export class AlertsService extends Context.Service lastStatus: string lastValue: number | null lastSampleCount: number @@ -2397,6 +2488,9 @@ export class AlertsService extends Context.Service code === fields.historyJson[index]) && state.lastStatus === fields.lastStatus && state.lastError == null && state.lastEvaluatedAt != null && @@ -2430,8 +2524,10 @@ export class AlertsService extends Context.Service tier.severity), }, evaluation, state: @@ -2440,11 +2536,13 @@ export class AlertsService extends Context.Service 0 ? [true, remaining - 1] : [false, remaining], ) if (allowed) { + // The tier the incident opened at, so severity and threshold agree. + const openedRule = ruleAtSeverity(normalized, outcome.severity ?? normalized.severity) yield* upsertAlertIssue({ orgId: row.orgId, ruleId: row.id, ruleName: row.name, groupKey, signalType: normalized.signalType, - severity: normalized.severity, + severity: openedRule.severity, comparator: normalized.comparator, - threshold: normalized.threshold, - thresholdUpper: normalized.thresholdUpper, + threshold: openedRule.threshold, + thresholdUpper: openedRule.thresholdUpper, windowMinutes: normalized.windowMinutes, observedValue: evaluation.value, sampleCount: evaluation.sampleCount, @@ -2862,7 +3009,13 @@ export class AlertsService extends Context.Service ({ severity: tier.severity, status: "clear" })), reason, // This "healthy" is fabricated for config-driven resolves (rule changed, // group vanished), not derived from an empty query window. Callers that @@ -3026,7 +3180,7 @@ export class AlertsService extends Context.Service, - thresholds: AlertCounterThresholds, + policy: AlertReplayPolicy, windowMs: number, -) => Effect.runSync(simulateFiringSpans(evaluations, thresholds, windowMs)) +) => Effect.runSync(simulateFiringSpans(evaluations, policy, windowMs)) -const thresholds = (breaches: number, healthy: number): AlertCounterThresholds => ({ +const thresholds = (breaches: number, healthy: number): AlertReplayPolicy => ({ consecutiveBreachesRequired: breaches, + breachesOutOf: null, consecutiveHealthyRequired: healthy, + thresholds: [{ severity: "warning" }], }) const MINUTE = 60_000 describe("simulateFiringSpans", () => { const evaluations = ( - statuses: ReadonlyArray, + statuses: ReadonlyArray, options?: { readonly provisionalLast?: boolean }, ): ReadonlyArray => statuses.map((status, i) => ({ bucketMs: i * MINUTE, - status, + evaluation: { + status, + value: status === "skipped" ? null : 1, + sampleCount: 1, + threshold: 1, + thresholdUpper: null, + comparator: "gte", + reason: "test", + derivedFromNoData: false, + ...(status === "skipped" ? { skipReason: "below_min_samples" as const } : undefined), + levels: + status === "skipped" + ? [] + : [{ severity: "warning", status: status === "breached" ? "breach" : "clear" }], + }, provisional: options?.provisionalLast === true && i === statuses.length - 1, })) @@ -41,7 +54,7 @@ describe("simulateFiringSpans", () => { ) // The second breach trips the counter at t=2m, but the incident began with // the first breach at t=1m; it resolves at the end of the second healthy. - expect(spans).toEqual([{ startMs: 1 * MINUTE, endMs: 5 * MINUTE }]) + expect(spans).toEqual([{ startMs: 1 * MINUTE, endMs: 5 * MINUTE, severity: "warning" }]) }) it("does not resolve on a single healthy window when two are required", () => { @@ -52,7 +65,7 @@ describe("simulateFiringSpans", () => { ) // One healthy resets the breach counter but never reaches the resolve bar, // so the whole stretch is one continuous span left open at the end. - expect(spans).toEqual([{ startMs: 0, endMs: 5 * MINUTE }]) + expect(spans).toEqual([{ startMs: 0, endMs: 5 * MINUTE, severity: "warning" }]) }) // Skipped windows freeze the machine, so a breach run survives a gap in data @@ -63,7 +76,8 @@ describe("simulateFiringSpans", () => { thresholds(2, 2), MINUTE, ) - expect(spans).toEqual([{ startMs: 1 * MINUTE, endMs: 5 * MINUTE }]) + // The span starts at the first breach, not one window before the tripping one. + expect(spans).toEqual([{ startMs: 0, endMs: 5 * MINUTE, severity: "warning" }]) }) // The trailing in-progress window charts, but the scheduler has not evaluated @@ -79,7 +93,7 @@ describe("simulateFiringSpans", () => { it("closes a still-open run at the last complete window's boundary", () => { const spans = spansOf(evaluations(["breached", "breached", "breached"]), thresholds(2, 2), MINUTE) - expect(spans).toEqual([{ startMs: 0, endMs: 3 * MINUTE }]) + expect(spans).toEqual([{ startMs: 0, endMs: 3 * MINUTE, severity: "warning" }]) }) it("returns nothing when the breach requirement is never met", () => { @@ -90,4 +104,51 @@ describe("simulateFiringSpans", () => { ) expect(spans).toEqual([]) }) + + it("starts a re-opened span after the previous one, even with breaches left in the window", () => { + const spans = spansOf( + evaluations(["breached", "breached", "healthy", "breached", "healthy"]), + { ...thresholds(2, 1), breachesOutOf: 5 }, + MINUTE, + ) + expect(spans).toEqual([ + { startMs: 0, endMs: 3 * MINUTE, severity: "warning" }, + { startMs: 3 * MINUTE, endMs: 5 * MINUTE, severity: "warning" }, + ]) + }) + + it("splits a span where the incident changes severity", () => { + const tiered: AlertReplayPolicy = { + ...thresholds(2, 2), + thresholds: [{ severity: "warning" }, { severity: "critical" }], + } + // 0 = clear, w = warning only, c = warning and critical. + const ticks = ["w", "w", "c", "c", "w", "w", "0", "0"] as const + const spans = spansOf( + ticks.map((tick, i) => ({ + bucketMs: i * MINUTE, + evaluation: { + status: tick === "0" ? "healthy" : "breached", + value: 1, + sampleCount: 1, + threshold: 1, + thresholdUpper: null, + comparator: "gte", + reason: "test", + derivedFromNoData: false, + levels: [ + { severity: "warning", status: tick === "0" ? "clear" : "breach" }, + { severity: "critical", status: tick === "c" ? "breach" : "clear" }, + ], + }, + })), + tiered, + MINUTE, + ) + expect(spans).toEqual([ + { startMs: 0, endMs: 3 * MINUTE, severity: "warning" }, + { startMs: 3 * MINUTE, endMs: 5 * MINUTE, severity: "critical" }, + { startMs: 5 * MINUTE, endMs: 8 * MINUTE, severity: "warning" }, + ]) + }) }) diff --git a/packages/backend/src/services/alerts/alert-firing-spans.ts b/packages/backend/src/services/alerts/alert-firing-spans.ts index fff8c9dc76..d83f9553b2 100644 --- a/packages/backend/src/services/alerts/alert-firing-spans.ts +++ b/packages/backend/src/services/alerts/alert-firing-spans.ts @@ -1,19 +1,25 @@ import { Effect } from "effect" -import type { AlertEvaluationStatus } from "@maple/domain/http" +import { + planAlertLifecycle, + type AlertEvaluation, + type AlertLifecycleIncident, + type AlertLifecycleInput, +} from "@maple/alerting-core" +import type { AlertSeverity, AlertThreshold } from "@maple/domain/http" -import { foldObservation, type HysteresisConfig, type HysteresisRow } from "./incident-hysteresis" - -/** The rule fields the hysteresis reads — nothing else about a rule matters here. */ -export interface AlertCounterThresholds { +/** The rule fields the lifecycle reads; nothing else about a rule matters here. */ +export interface AlertReplayPolicy { readonly consecutiveBreachesRequired: number + readonly breachesOutOf: number | null readonly consecutiveHealthyRequired: number + readonly thresholds: ReadonlyArray> } /** One observation the preview replays, in the order the scheduler would see it. */ export interface SimulatedEvaluation { readonly bucketMs: number - readonly status: AlertEvaluationStatus + readonly evaluation: AlertEvaluation /** * The trailing in-progress window. It charts, but the scheduler has not * evaluated it yet, so it stays out of the simulation entirely. @@ -21,82 +27,122 @@ export interface SimulatedEvaluation { readonly provisional?: boolean } -/** A stretch of wall time the rule would have held an incident open. */ +/** A stretch of wall time the rule would have held an incident open at one severity. */ export interface SimulatedFiringSpan { readonly startMs: number readonly endMs: number + readonly severity: AlertSeverity +} + +interface ReplayState { + readonly state: AlertLifecycleInput["state"] + readonly incident: AlertLifecycleIncident | null + readonly open: { readonly startMs: number; readonly severity: AlertSeverity } | null + /** Buckets of the recent non-skipped ticks that breached, to date a span's first breach. */ + readonly recentBreaches: ReadonlyArray + readonly lastCompleteBucketMs: number | null + readonly spans: ReadonlyArray } /** - * Replay a whole series through the shared incident hysteresis and return the - * spans during which the rule would have held an incident open. - * - * The preview promises to show what the scheduler will do, so it must not be a - * second implementation of the scheduler: both run the same `foldObservation`, - * and a drift here would only ever show up as a rule firing differently than - * its chart said it would. - * - * A span opens at the start of the run's *first* breached window, not at the - * window that tripped the counter — the incident is understood to have begun - * when the breach did. A run still open at the end of the series is closed at - * the last complete window's boundary rather than left dangling. + * Replay a series through the scheduler's `planAlertLifecycle` (no liveness probe, so empty windows + * are evidence of nothing). Spans start at the first breach inside the last M checks, split at each + * severity change, and an open run closes at the last complete window. */ export const simulateFiringSpans = ( evaluations: ReadonlyArray, - thresholds: AlertCounterThresholds, + rule: AlertReplayPolicy, windowMs: number, ): Effect.Effect> => Effect.gen(function* () { - const config: HysteresisConfig = { - breachesToOpen: thresholds.consecutiveBreachesRequired, - healthyToResolve: thresholds.consecutiveHealthyRequired, - cooldownMs: 0, + const breachWindow = rule.breachesOutOf ?? rule.consecutiveBreachesRequired + const policy: AlertLifecycleInput["policy"] = { + consecutiveBreachesRequired: rule.consecutiveBreachesRequired, + breachWindow, + consecutiveHealthyRequired: rule.consecutiveHealthyRequired, + renotifyIntervalMinutes: Number.MAX_SAFE_INTEGER, + severities: rule.thresholds.map((tier) => tier.severity), } const initial: ReplayState = { - row: { - consecutiveBreaches: 0, - consecutiveHealthy: 0, - incidentOpen: false, - lastResolvedAtMs: null, - }, - openStartMs: null, + state: null, + incident: null, + open: null, + recentBreaches: [], lastCompleteBucketMs: null, spans: [], } const final = yield* Effect.reduce( - evaluations.filter((evaluation) => evaluation.provisional !== true), + evaluations.filter((tick) => tick.provisional !== true), () => initial, - (state, evaluation) => - foldObservation(state.row, evaluation.status, config, evaluation.bucketMs).pipe( - Effect.map((outcome): ReplayState => { - const row: HysteresisRow = { - consecutiveBreaches: outcome.consecutiveBreaches, - consecutiveHealthy: outcome.consecutiveHealthy, - incidentOpen: - outcome.transition === "open" - ? true - : outcome.transition === "resolve" - ? false - : state.row.incidentOpen, - lastResolvedAtMs: state.row.lastResolvedAtMs, + (acc, { bucketMs, evaluation }) => + planAlertLifecycle({ + policy, + evaluation, + state: acc.state, + openIncident: acc.incident, + nowMs: bucketMs, + allowNoDataResolution: evaluation.status !== "skipped", + }).pipe( + Effect.map((plan): ReplayState => { + const recentBreaches = + evaluation.status === "skipped" + ? acc.recentBreaches + : [ + ...acc.recentBreaches, + evaluation.status === "breached" ? bucketMs : null, + ].slice(-breachWindow) + const incident: AlertLifecycleIncident | null = + plan.transition === "resolved" || plan.severity === null + ? null + : { + severity: plan.severity, + firstTriggeredAtMs: acc.incident?.firstTriggeredAtMs ?? bucketMs, + lastNotifiedAtMs: bucketMs, + lastDeliveredEventType: "trigger", + } + const base = { + ...acc, + state: plan.state, + incident, + recentBreaches, + lastCompleteBucketMs: bucketMs, + } + if (plan.transition === "opened" && plan.severity !== null) { + const firstBreach = recentBreaches.find((ms) => ms !== null) ?? bucketMs + return { ...base, open: { startMs: firstBreach, severity: plan.severity } } } - const base = { ...state, row, lastCompleteBucketMs: evaluation.bucketMs } - if (outcome.transition === "open") { + if (plan.transition === "resolved" && acc.open !== null) { return { ...base, - openStartMs: - evaluation.bucketMs - - (thresholds.consecutiveBreachesRequired - 1) * windowMs, + open: null, + // A re-open within M must not date back past this resolve. + recentBreaches: [], + spans: [ + ...acc.spans, + { + startMs: acc.open.startMs, + endMs: bucketMs + windowMs, + severity: acc.open.severity, + }, + ], } } - if (outcome.transition === "resolve" && state.openStartMs != null) { + if ( + acc.open !== null && + plan.severity !== null && + plan.severity !== acc.open.severity + ) { return { ...base, - openStartMs: null, + open: { startMs: bucketMs, severity: plan.severity }, spans: [ - ...state.spans, - { startMs: state.openStartMs, endMs: evaluation.bucketMs + windowMs }, + ...acc.spans, + { + startMs: acc.open.startMs, + endMs: bucketMs, + severity: acc.open.severity, + }, ], } } @@ -105,14 +151,14 @@ export const simulateFiringSpans = ( ), ) - return final.openStartMs != null && final.lastCompleteBucketMs != null - ? [...final.spans, { startMs: final.openStartMs, endMs: final.lastCompleteBucketMs + windowMs }] + return final.open !== null && final.lastCompleteBucketMs !== null + ? [ + ...final.spans, + { + startMs: final.open.startMs, + endMs: final.lastCompleteBucketMs + windowMs, + severity: final.open.severity, + }, + ] : final.spans }) - -interface ReplayState { - readonly row: HysteresisRow - readonly openStartMs: number | null - readonly lastCompleteBucketMs: number | null - readonly spans: ReadonlyArray -} diff --git a/packages/backend/src/services/alerts/alert-thresholds.test.ts b/packages/backend/src/services/alerts/alert-thresholds.test.ts new file mode 100644 index 0000000000..6f9c5a7274 --- /dev/null +++ b/packages/backend/src/services/alerts/alert-thresholds.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest" + +import type { AlertComparator, AlertThresholds } from "@maple/domain/http" +import { validateThresholds } from "./alert-thresholds" + +const tiers = ( + warning: { readonly value: number; readonly recoveryValue?: number }, + critical: { readonly value: number; readonly recoveryValue?: number }, +): AlertThresholds => [ + { severity: "warning", upper: null, recoveryValue: null, destinationIds: [], ...warning }, + { severity: "critical", upper: null, recoveryValue: null, destinationIds: [], ...critical }, +] + +const problems = (comparator: AlertComparator, thresholds: AlertThresholds) => + validateThresholds(comparator, thresholds, { + label: (index, field) => `thresholds[${index}].${field}`, + requireDestinations: false, + }) + +describe("validateThresholds", () => { + it("accepts a critical tier past the warning one, with recovery values on the healthy side", () => { + expect( + problems("gt", tiers({ value: 10, recoveryValue: 8 }, { value: 20, recoveryValue: 15 })), + ).toEqual([]) + expect(problems("lte", tiers({ value: 50 }, { value: 10, recoveryValue: 50 }))).toEqual([]) + }) + + it("rejects equal tiers even for an inclusive comparator", () => { + expect(problems("gte", tiers({ value: 10 }, { value: 10 }))).toContain( + "the critical threshold must be worse than the warning threshold", + ) + }) + + it("rejects a critical recovery value past the warning threshold", () => { + // At 8 the warning tier is clear, so a critical incident could not still be held open. + expect(problems("gt", tiers({ value: 10 }, { value: 20, recoveryValue: 5 }))).toContain( + "the critical recovery value must not be past the warning threshold", + ) + expect(problems("lt", tiers({ value: 50 }, { value: 10, recoveryValue: 60 }))).toContain( + "the critical recovery value must not be past the warning threshold", + ) + }) +}) diff --git a/packages/backend/src/services/alerts/alert-thresholds.ts b/packages/backend/src/services/alerts/alert-thresholds.ts new file mode 100644 index 0000000000..6d565ff6d0 --- /dev/null +++ b/packages/backend/src/services/alerts/alert-thresholds.ts @@ -0,0 +1,165 @@ +import { compareAlertThreshold, maxSeverity, tierFor } from "@maple/alerting-core" +import { + ALERT_SEVERITY_RANK, + isRangeComparator, + MAX_ALERT_BREACH_WINDOW, + sortAlertThresholds, + type AlertComparator, + type AlertDestinationId, + type AlertSeverity, + type AlertThreshold, + type AlertThresholds, +} from "@maple/domain/http" +import { Array as Arr } from "effect" + +/** Destinations an incident at `severity` notifies: its tier and every tier under it. */ +const destinationsUpTo = ( + thresholds: ReadonlyArray, + severity: AlertSeverity, +): ReadonlyArray => + Arr.dedupe( + thresholds + .filter((tier) => ALERT_SEVERITY_RANK[tier.severity] <= ALERT_SEVERITY_RANK[severity]) + .flatMap((tier) => tier.destinationIds), + ) + +interface TieredRuleFields { + readonly thresholds: AlertThresholds + readonly severity: AlertSeverity + readonly threshold: number + readonly thresholdUpper: number | null + readonly destinationIds: ReadonlyArray +} + +/** + * The rule as one notification sees it: that tier's threshold and destinations. `reachedSeverity` + * (the peak) widens them so a de-escalation or resolve reaches the pager that heard the critical. + */ +export const ruleAtSeverity = ( + rule: R, + severity: AlertSeverity, + reachedSeverity: AlertSeverity | null = null, +): R => { + const tier = tierFor(rule.thresholds, severity) + return { + ...rule, + severity, + threshold: tier.value, + thresholdUpper: tier.upper, + destinationIds: destinationsUpTo(rule.thresholds, maxSeverity(severity, reachedSeverity) ?? severity), + } +} + +/** Whether a recovery value still fits its tier after an edit to the value or comparator. */ +export const recoveryValueFits = ( + comparator: AlertComparator, + tier: Pick, +): boolean => + tier.recoveryValue === null || + (!isRangeComparator(comparator) && + comparator !== "eq" && + comparator !== "neq" && + !compareAlertThreshold(tier.recoveryValue, comparator, tier.value)) + +/** Whether an edit changes what a tick is classified against; destinations aside. */ +export const tierConditionChanged = ( + a: { readonly comparator: string; readonly thresholds: AlertThresholds }, + b: { readonly comparator: string; readonly thresholds: AlertThresholds }, +): boolean => { + const key = (rule: typeof a) => + JSON.stringify([ + rule.comparator, + rule.thresholds.map((t) => [t.severity, t.value, t.upper, t.recoveryValue]), + ]) + return key(a) !== key(b) +} + +/** + * Validation messages for a tier list. `label` names a field for the message, + * so a request in the single-threshold shape keeps its old wording. + */ +export const validateThresholds = ( + comparator: AlertComparator, + thresholds: AlertThresholds, + options: { + readonly label: (index: number, field: "value" | "upper" | "recoveryValue") => string + readonly requireDestinations: boolean + }, +): ReadonlyArray => { + const details: string[] = [] + const { label } = options + const range = isRangeComparator(comparator) + const singleTierOnly = range || comparator === "eq" || comparator === "neq" + if (new Set(thresholds.map((tier) => tier.severity)).size !== thresholds.length) { + details.push("thresholds must have at most one tier per severity") + } + if (thresholds.length > 1 && singleTierOnly) { + details.push(`only one threshold tier is supported with the ${comparator} comparator`) + } + thresholds.forEach((tier, index) => { + if (!Number.isFinite(tier.value)) details.push(`${label(index, "value")} must be a finite number`) + if (range) { + if (tier.upper == null || !Number.isFinite(tier.upper)) { + details.push(`${label(index, "upper")} is required for between / not_between comparators`) + } else if (tier.upper < tier.value) { + details.push( + `${label(index, "upper")} must be greater than or equal to ${label(index, "value")}`, + ) + } + } else if (tier.upper != null) { + details.push(`${label(index, "upper")} is only supported for between / not_between comparators`) + } + if (tier.recoveryValue != null) { + if (singleTierOnly) { + details.push( + `${label(index, "recoveryValue")} is not supported with the ${comparator} comparator`, + ) + } else if (!Number.isFinite(tier.recoveryValue)) { + details.push(`${label(index, "recoveryValue")} must be a finite number`) + } else if (compareAlertThreshold(tier.recoveryValue, comparator, tier.value)) { + details.push( + `${label(index, "recoveryValue")} must be on the healthy side of ${label(index, "value")}`, + ) + } + } + if (options.requireDestinations && tier.destinationIds.length === 0) { + details.push( + thresholds.length === 1 + ? "at least one destination must be selected" + : `the ${tier.severity} threshold needs at least one destination`, + ) + } + }) + // Tier ordering is strict even for inclusive comparators: equal tiers would always fire together. + const strict = comparator === "gte" ? "gt" : comparator === "lte" ? "lt" : comparator + const [warning, critical] = sortAlertThresholds(thresholds) + if ( + warning !== undefined && + critical !== undefined && + Number.isFinite(warning.value) && + Number.isFinite(critical.value) + ) { + if (!compareAlertThreshold(critical.value, strict, warning.value)) { + details.push("the critical threshold must be worse than the warning threshold") + } + if ( + critical.recoveryValue != null && + compareAlertThreshold(warning.value, strict, critical.recoveryValue) + ) { + details.push("the critical recovery value must not be past the warning threshold") + } + } + return details +} + +export const validateBreachWindow = ( + breachesRequired: number, + breachesOutOf: number | null, +): ReadonlyArray => + breachesOutOf === null + ? [] + : breachesOutOf < breachesRequired + ? ["breachesOutOf must be at least consecutiveBreachesRequired"] + : breachesOutOf > MAX_ALERT_BREACH_WINDOW + ? [`breachesOutOf must be at most ${MAX_ALERT_BREACH_WINDOW}`] + : [] diff --git a/packages/backend/src/services/alerts/delivery/Transport.ts b/packages/backend/src/services/alerts/delivery/Transport.ts index 14268cb0a9..f588e95436 100644 --- a/packages/backend/src/services/alerts/delivery/Transport.ts +++ b/packages/backend/src/services/alerts/delivery/Transport.ts @@ -85,6 +85,13 @@ export interface HttpTransport { readonly providerLabel: string /** PURE. The unit-testable heart of each provider. */ readonly render: (input: RenderInput) => HttpRequestSpec + /** + * PURE. Requests sent after `render`'s succeeds, in order; any non-2xx fails + * the delivery like the primary would. Only `pagerduty` implements this, to + * close the alert a rule's other severity opened. They must be idempotent: a + * retry sends the primary and every follow-up again. + */ + readonly followUps?: (input: RenderInput) => ReadonlyArray /** * PURE. Give a non-2xx a better message than the generic one. Return null to * fall through to the runner's default. Only `hazel-oauth` implements this. diff --git a/packages/backend/src/services/alerts/delivery/context.ts b/packages/backend/src/services/alerts/delivery/context.ts index 4f35597b59..d80315d292 100644 --- a/packages/backend/src/services/alerts/delivery/context.ts +++ b/packages/backend/src/services/alerts/delivery/context.ts @@ -38,6 +38,19 @@ export interface DispatchContext { */ readonly signalDisplay?: SignalDisplay | null readonly severity: AlertSeverity + /** + * The rule's tier severities. A rule with both a warning and a critical tier + * pages critical under its own PagerDuty dedup key. Absent on the + * escalation/error paths, which dispatch without an alert rule. + */ + readonly ruleSeverities?: ReadonlyArray + /** + * The open incident's severity at dispatch, which can differ from the + * queued event's `severity`; absent when no incident is open. + */ + readonly incidentSeverity?: AlertSeverity + /** Highest severity the incident reached; absent when the delivery has no incident. */ + readonly incidentPeakSeverity?: AlertSeverity readonly comparator: AlertComparator readonly threshold: number readonly thresholdUpper: number | null diff --git a/packages/backend/src/services/alerts/delivery/runTransport.ts b/packages/backend/src/services/alerts/delivery/runTransport.ts index a47a0ec8e7..f55547e6d4 100644 --- a/packages/backend/src/services/alerts/delivery/runTransport.ts +++ b/packages/backend/src/services/alerts/delivery/runTransport.ts @@ -159,6 +159,35 @@ const sendHttp = Effect.fn("AlertDelivery.http", { kind: "client" })(function* ( return response }) +/** Fail the delivery on a non-2xx, in the transport's own words when it has them. */ +const failOnStatus = ( + response: HttpClientResponse.HttpClientResponse, + transport: HttpTransport, + input: RenderInput, +): Effect.Effect => + Effect.gen(function* () { + if (response.status >= 200 && response.status < 300) return + const detail = yield* readErrorBody(response) + // A transport may supply a better sentence for a status it knows well; + // it does not get to change whether the failure is retryable. + const described = transport.describeStatus?.(response.status) ?? null + const failure = failureForStatus(response.status, { + message: described ?? statusFailureMessage(transport.providerLabel, response.status, detail), + destinationType: transport.type, + providerStatus: response.status, + }) + yield* Effect.annotateCurrentSpan({ + "maple.delivery.failure_tag": failure._tag, + "maple.delivery.retryable": failure.error.retryable, + // Which destination, not just which provider: a flat rate of + // identical `retry: "never"` failures is one broken row, and the + // error issue is only triageable if the span names it. + "maple.delivery.destination_id": input.context.destination.id, + "maple.delivery.destination_type": transport.type, + }) + return yield* Effect.fail(failure) + }) + export const runHttpTransport = ( transport: HttpTransport, input: RenderInput, @@ -167,28 +196,15 @@ export const runHttpTransport = ( Effect.gen(function* () { const spec = transport.render(input) const response = yield* sendHttp(spec, transport, runtime) - - if (response.status < 200 || response.status >= 300) { - const detail = yield* readErrorBody(response) - // A transport may supply a better sentence for a status it knows well; - // it does not get to change whether the failure is retryable. - const described = transport.describeStatus?.(response.status) ?? null - const failure = failureForStatus(response.status, { - message: described ?? statusFailureMessage(transport.providerLabel, response.status, detail), - destinationType: transport.type, - providerStatus: response.status, - }) - yield* Effect.annotateCurrentSpan({ - "maple.delivery.failure_tag": failure._tag, - "maple.delivery.retryable": failure.error.retryable, - // Which destination, not just which provider: a flat rate of - // identical `retry: "never"` failures is one broken row, and the - // error issue is only triageable if the span names it. - "maple.delivery.destination_id": input.context.destination.id, - "maple.delivery.destination_type": transport.type, - }) - return yield* Effect.fail(failure) - } + yield* failOnStatus(response, transport, input) + yield* Effect.forEach( + transport.followUps?.(input) ?? [], + (followUp) => + sendHttp(followUp, transport, runtime).pipe( + Effect.flatMap((followUpResponse) => failOnStatus(followUpResponse, transport, input)), + ), + { discard: true }, + ) // A 2xx is not proof of delivery for every provider — Telegram answers 200 // with `{ ok: false }` — so a transport may claim the body. diff --git a/packages/backend/src/services/alerts/delivery/transports/pagerduty.ts b/packages/backend/src/services/alerts/delivery/transports/pagerduty.ts index 990d59b19d..125b9dd814 100644 --- a/packages/backend/src/services/alerts/delivery/transports/pagerduty.ts +++ b/packages/backend/src/services/alerts/delivery/transports/pagerduty.ts @@ -1,10 +1,52 @@ import { Duration, Effect } from "effect" import { HttpClient, HttpClientRequest } from "effect/http" +import type { AlertSeverity } from "@maple/domain/http" import { displayGroupKey, truncate } from "../../alert-formatting" -import type { HttpTransport, RenderInput, SecretConfigOf } from "../Transport" +import type { DispatchContext } from "../context" +import type { HttpRequestSpec, HttpTransport, RenderInput, SecretConfigOf } from "../Transport" type Config = SecretConfigOf<"pagerduty"> +const EVENTS_URL = "https://events.pagerduty.com/v2/enqueue" + +/** + * A rule with both a warning and a critical tier pages critical under its own + * dedup key: re-triggering the warning alert's key would only update that alert, + * and an acknowledged alert does not page again, so an escalation could go + * unheard. Every other rule keeps the incident's single key, which is what its + * already-open alerts were raised under. + */ +const criticalHasOwnKey = (context: Pick): boolean => + context.ruleSeverities !== undefined && + context.ruleSeverities.includes("warning") && + context.ruleSeverities.includes("critical") + +const criticalDedupKey = (dedupeKey: string): string => `${dedupeKey}:critical` + +/** + * The tier a delivery's key is chosen from: the queued event's, unless the + * incident has since fallen below it. A critical trigger retried after a + * de-escalation must not re-open (and re-page) the alert it closed. + */ +const keySeverity = (context: Pick): AlertSeverity => + context.severity === "critical" && context.incidentSeverity === "warning" ? "warning" : context.severity + +export const pagerDutyDedupKey = ( + context: Pick, + severity: AlertSeverity, +): string => + severity === "critical" && criticalHasOwnKey(context) + ? criticalDedupKey(context.dedupeKey) + : context.dedupeKey + +const resolveRequest = (integrationKey: string, dedupKey: string): HttpRequestSpec => ({ + url: EVENTS_URL, + headers: { "content-type": "application/json" }, + guarded: false, + sensitivePath: false, + body: JSON.stringify({ routing_key: integrationKey, event_action: "resolve", dedup_key: dedupKey }), +}) + /** * PagerDuty Events API v2. The body is machine-facing, so `custom_details` * carries raw numbers rather than the formatted strings the chat providers @@ -21,18 +63,24 @@ export const pagerDutyTransport: HttpTransport = { return { // Fixed vendor host: nothing user-supplied to guard against, and the // path carries no credential (the routing key rides in the body). - url: "https://events.pagerduty.com/v2/enqueue", + url: EVENTS_URL, headers: { "content-type": "application/json" }, guarded: false, sensitivePath: false, body: JSON.stringify({ routing_key: input.config.integrationKey, event_action: context.eventType === "resolve" ? "resolve" : "trigger", - dedup_key: context.dedupeKey, + // A resolve closes the warning-keyed alert here and the critical one + // in `followUps`. + dedup_key: pagerDutyDedupKey( + context, + context.eventType === "resolve" ? "warning" : keySeverity(context), + ), payload: { summary: truncate(templated?.title ?? `${context.ruleName} ${context.eventType}`, 1024), source: displayGroupKey(context.groupKey) ?? "maple-alerts", - severity: context.severity === "critical" ? "critical" : "warning", + // The tier the key was chosen for, so a stale critical event cannot raise the warning alert. + severity: keySeverity(context) === "critical" ? "critical" : "warning", custom_details: { ...(templated ? { message: templated.body } : undefined), ruleName: context.ruleName, @@ -53,9 +101,25 @@ export const pagerDutyTransport: HttpTransport = { }), } }, + // The critical alert closes when the incident resolves or falls back to + // warning. The rule may have dropped its critical tier since the key was + // minted, so an incident that peaked at critical closes it too. Resolving a + // key PagerDuty has never seen is a no-op. + followUps: (input) => { + const { context } = input + const mayHoldCriticalKey = criticalHasOwnKey(context) || context.incidentPeakSeverity === "critical" + // Judged by where the incident is now, not by the queued event: a warning + // reminder delivered after an escalation must leave the critical page open. + const closesCritical = + context.eventType === "resolve" || + (context.eventType !== "test" && (context.incidentSeverity ?? context.severity) === "warning") + return mayHoldCriticalKey && closesCritical + ? [resolveRequest(input.config.integrationKey, criticalDedupKey(context.dedupeKey))] + : [] + }, ack: (input) => ({ providerMessage: "Delivered to PagerDuty", - providerReference: input.context.dedupeKey, + providerReference: pagerDutyDedupKey(input.context, keySeverity(input.context)), }), } diff --git a/packages/backend/src/services/alerts/delivery/transports/render.test.ts b/packages/backend/src/services/alerts/delivery/transports/render.test.ts index d036b8bbc0..09f39a6eba 100644 --- a/packages/backend/src/services/alerts/delivery/transports/render.test.ts +++ b/packages/backend/src/services/alerts/delivery/transports/render.test.ts @@ -272,3 +272,84 @@ describe("transport render: sparkline", () => { } }) }) + +describe("pagerduty dedup keys across severity tiers", () => { + const pd = (overrides: Partial) => { + const input = inputFor( + { type: "pagerduty" as const, integrationKey: "k" }, + { context: { ...context, ...overrides } }, + ) + const sent = [pagerDutyTransport.render(input), ...(pagerDutyTransport.followUps?.(input) ?? [])] + return sent.map((spec) => { + const body = JSON.parse(spec.body) + return `${body.event_action} ${body.dedup_key}` + }) + } + const tiered = { ruleSeverities: ["warning", "critical"] as const } + + it("keeps a single-tier rule on the incident's one key, with no extra requests", () => { + assert.deepStrictEqual(pd({ ruleSeverities: ["critical"] }), ["trigger org_1:rule_1:checkout"]) + assert.deepStrictEqual(pd({ ruleSeverities: ["critical"], eventType: "resolve" }), [ + "resolve org_1:rule_1:checkout", + ]) + }) + + it("pages a tiered rule's critical on its own key, so an acknowledged warning cannot swallow it", () => { + assert.deepStrictEqual(pd({ ...tiered, severity: "warning" }), [ + "trigger org_1:rule_1:checkout", + "resolve org_1:rule_1:checkout:critical", + ]) + assert.deepStrictEqual(pd({ ...tiered, severity: "critical" }), [ + "trigger org_1:rule_1:checkout:critical", + ]) + }) + + it("closes the critical alert of an incident that peaked at critical after the rule dropped that tier", () => { + assert.deepStrictEqual( + pd({ + ruleSeverities: ["warning"], + incidentPeakSeverity: "critical", + severity: "warning", + eventType: "renotify", + }), + ["trigger org_1:rule_1:checkout", "resolve org_1:rule_1:checkout:critical"], + ) + assert.deepStrictEqual( + pd({ ruleSeverities: ["warning"], incidentPeakSeverity: "critical", eventType: "resolve" }), + ["resolve org_1:rule_1:checkout", "resolve org_1:rule_1:checkout:critical"], + ) + }) + + it("leaves the critical page open when a warning event is delivered after an escalation", () => { + assert.deepStrictEqual( + pd({ ...tiered, severity: "warning", eventType: "renotify", incidentSeverity: "critical" }), + ["trigger org_1:rule_1:checkout"], + ) + }) + + it("does not re-open the critical alert when a critical event is retried after a de-escalation", () => { + assert.deepStrictEqual(pd({ ...tiered, severity: "critical", incidentSeverity: "warning" }), [ + "trigger org_1:rule_1:checkout", + "resolve org_1:rule_1:checkout:critical", + ]) + // The warning alert it lands on is not raised to critical either. + const spec = pagerDutyTransport.render( + inputFor( + { type: "pagerduty", integrationKey: "k" }, + { context: { ...context, ...tiered, severity: "critical", incidentSeverity: "warning" } }, + ), + ) + assert.strictEqual(JSON.parse(spec.body).payload.severity, "warning") + }) + + it("closes the critical alert on de-escalation and both alerts on resolve", () => { + assert.deepStrictEqual(pd({ ...tiered, severity: "warning", eventType: "renotify" }), [ + "trigger org_1:rule_1:checkout", + "resolve org_1:rule_1:checkout:critical", + ]) + assert.deepStrictEqual(pd({ ...tiered, severity: "critical", eventType: "resolve" }), [ + "resolve org_1:rule_1:checkout", + "resolve org_1:rule_1:checkout:critical", + ]) + }) +}) diff --git a/packages/backend/src/services/alerts/incident-hysteresis.test.ts b/packages/backend/src/services/alerts/incident-hysteresis.test.ts index 775111fff1..3615c1342c 100644 --- a/packages/backend/src/services/alerts/incident-hysteresis.test.ts +++ b/packages/backend/src/services/alerts/incident-hysteresis.test.ts @@ -310,8 +310,10 @@ describe("host-neutral alert lifecycle parity", () => { const actual = planAlertLifecycle({ policy: { consecutiveBreachesRequired: 3, + breachWindow: 3, consecutiveHealthyRequired: 2, renotifyIntervalMinutes: 60, + severities: ["warning"], }, evaluation: { status, @@ -322,6 +324,15 @@ describe("host-neutral alert lifecycle parity", () => { comparator: "gte", reason: "parity", derivedFromNoData: false, + levels: + status === "skipped" + ? [] + : [ + { + severity: "warning", + status: status === "breached" ? "breach" : "clear", + }, + ], }, state: { consecutiveBreaches: row.consecutiveBreaches, @@ -329,6 +340,7 @@ describe("host-neutral alert lifecycle parity", () => { }, openIncident: row.incidentOpen ? { + severity: "warning", firstTriggeredAtMs: START_MS, lastNotifiedAtMs: START_MS, lastDeliveredEventType: "trigger", @@ -336,7 +348,9 @@ describe("host-neutral alert lifecycle parity", () => { : null, nowMs, }) - expect(actual.state).toEqual({ + // The state is rebuilt from counters alone each tick, so this also + // proves a pre-history row replays exactly. + expect(actual.state).toMatchObject({ consecutiveBreaches: expected.consecutiveBreaches, consecutiveHealthy: expected.consecutiveHealthy, }) @@ -347,7 +361,8 @@ describe("host-neutral alert lifecycle parity", () => { ) row = { ...row, - ...actual.state, + consecutiveBreaches: actual.state.consecutiveBreaches, + consecutiveHealthy: actual.state.consecutiveHealthy, incidentOpen: actual.transition === "opened" || (row.incidentOpen && actual.transition !== "resolved"), diff --git a/packages/backend/src/services/errors/issue-hub.test.ts b/packages/backend/src/services/errors/issue-hub.test.ts index 1cea5c4832..faf309fc86 100644 --- a/packages/backend/src/services/errors/issue-hub.test.ts +++ b/packages/backend/src/services/errors/issue-hub.test.ts @@ -333,6 +333,48 @@ describe("upsertAlertIssue", () => { }).pipe(Effect.provide(makeLayer())), ) + it.effect("raises a detector severity when a critical incident reopens a wontfix issue", () => + Effect.gen(function* () { + yield* TestClock.setTime(T0) + const database = yield* Database + const first = yield* upsertAlertIssue(baseInput()) + yield* database.execute((db) => + db.run( + PG.update(ErrorIssues) + .set({ workflowState: "wontfix", snoozeUntil: T0 + 50_000 }) + .where(($) => [$.id.eq(first.issueId!)]), + ), + ) + + yield* upsertAlertIssue( + baseInput({ incidentId: INCIDENT_2, severity: "critical", timestamp: T0 + 100_000 }), + ) + const issues = yield* loadIssue + assert.strictEqual(issues[0]?.workflowState, "triage") + assert.strictEqual(issues[0]?.severity, "critical") + }).pipe(Effect.provide(makeLayer())), + ) + + it.effect("keeps a manually set severity when a critical incident re-fires", () => + Effect.gen(function* () { + yield* TestClock.setTime(T0) + const database = yield* Database + const first = yield* upsertAlertIssue(baseInput()) + yield* database.execute((db) => + db.run( + PG.update(ErrorIssues) + .set({ severity: "low", severitySource: "manual" }) + .where(($) => [$.id.eq(first.issueId!)]), + ), + ) + + yield* upsertAlertIssue( + baseInput({ incidentId: INCIDENT_2, severity: "critical", timestamp: T0 + 100_000 }), + ) + assert.strictEqual((yield* loadIssue)[0]?.severity, "low") + }).pipe(Effect.provide(makeLayer())), + ) + it.effect("reports { issueId: null, action: 'error' } instead of failing when the DB breaks", () => Effect.gen(function* () { yield* TestClock.setTime(T0) diff --git a/packages/backend/src/services/errors/issue-hub.ts b/packages/backend/src/services/errors/issue-hub.ts index 30be07d8f6..7b39998c23 100644 --- a/packages/backend/src/services/errors/issue-hub.ts +++ b/packages/backend/src/services/errors/issue-hub.ts @@ -278,7 +278,6 @@ export const upsertAlertIssue: ( .where(($) => [$.orgId.eq(input.orgId), $.id.eq(prior.id), $.severity.isNull()]), ), ) - const reopenFrom: WorkflowState | null = prior.workflowState === "done" || prior.workflowState === "wontfix" ? prior.workflowState @@ -312,6 +311,9 @@ export const upsertAlertIssue: ( } else { action = "refreshed" } + // After the reopen, which takes a wontfix issue out of the raise's exclusion. + if (input.severity === "critical") + yield* raiseDetectorSeverity(input.orgId, fingerprintHash, input.timestamp) } yield* database.execute((db) => @@ -368,6 +370,58 @@ export const upsertAlertIssue: ( ), ) +/** + * Raise an alert issue's severity when its incident escalates, while the severity is still the + * detector's (manual > ai > detector). Never fails, like `upsertAlertIssue`. + */ +export const escalateAlertIssue = Effect.fn("IssueHub.escalateAlertIssue")( + function* (input: { + readonly orgId: OrgId + readonly ruleId: AlertRuleId + readonly groupKey: string + readonly timestamp: number + }) { + yield* raiseDetectorSeverity( + input.orgId, + alertIssueFingerprint(input.ruleId, input.groupKey), + input.timestamp, + ) + }, + (effect, input) => + Effect.catchCause(effect, (cause) => + Effect.logError("Alert issue escalation failed").pipe( + Effect.annotateLogs({ + orgId: input.orgId, + ruleId: input.ruleId, + error: summarizeCause(cause), + }), + ), + ), +) + +/** Raise a detector-set alert issue to critical; manual and AI severities and wontfix issues stay. */ +const raiseDetectorSeverity = Effect.fn("IssueHub.raiseDetectorSeverity")(function* ( + orgId: OrgId, + fingerprintHash: string, + timestamp: number, +) { + const database = yield* Database + const critical = detectorSeverityFor("critical") + yield* database.execute((db) => + db.run( + PG.update(ErrorIssues) + .set({ severity: critical, updatedAt: timestamp }) + .where(($) => [ + $.orgId.eq(orgId), + $.fingerprintHash.eq(fingerprintHash), + $.severitySource.eq("detector"), + $.severity.neq(critical), + $.workflowState.neq("wontfix"), + ]), + ), + ) +}) + const recordIssueEvent = Effect.fn("IssueHub.recordIssueEvent")(function* ( orgId: OrgId, issueId: ErrorIssueId, diff --git a/packages/db/drizzle/20261015225936_alert_severity_tiers/migration.sql b/packages/db/drizzle/20261015225936_alert_severity_tiers/migration.sql new file mode 100644 index 0000000000..ba8a2de2c2 --- /dev/null +++ b/packages/db/drizzle/20261015225936_alert_severity_tiers/migration.sql @@ -0,0 +1,11 @@ +-- metadata +ALTER TABLE "alert_incidents" ADD COLUMN IF NOT EXISTS "peak_severity" text; +--> statement-breakpoint +-- metadata +ALTER TABLE "alert_rule_states" ADD COLUMN IF NOT EXISTS "history_json" jsonb; +--> statement-breakpoint +-- metadata +ALTER TABLE "alert_rules" ADD COLUMN IF NOT EXISTS "thresholds_json" jsonb; +--> statement-breakpoint +-- metadata +ALTER TABLE "alert_rules" ADD COLUMN IF NOT EXISTS "breaches_out_of" integer; diff --git a/packages/db/drizzle/20261015225936_alert_severity_tiers/snapshot.json b/packages/db/drizzle/20261015225936_alert_severity_tiers/snapshot.json new file mode 100644 index 0000000000..90e664aac4 --- /dev/null +++ b/packages/db/drizzle/20261015225936_alert_severity_tiers/snapshot.json @@ -0,0 +1,13131 @@ +{ + "version": "1", + "dialect": "postgres", + "id": "e711d5dde160d7c4c99464419fd2f843ff61414db882e429136e4ed15160462e", + "prevIds": [ + "48d3838043af2317670b4d331517fd04bd0799b1c95720a38eda958dac3f91dd" + ], + "entities": [ + { + "kind": "table", + "name": "actors", + "primaryKey": { + "name": "actors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "agent_feedback", + "primaryKey": { + "name": "agent_feedback_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "ai_triage_settings", + "primaryKey": { + "name": "ai_triage_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "alert_delivery_events", + "primaryKey": { + "name": "alert_delivery_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_destinations", + "primaryKey": { + "name": "alert_destinations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_incidents", + "primaryKey": { + "name": "alert_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_rule_claims", + "primaryKey": { + "name": "alert_rule_claims_pkey", + "columns": [ + "rule_id" + ] + } + }, + { + "kind": "table", + "name": "alert_rule_states", + "primaryKey": { + "name": "alert_rule_states_org_id_rule_id_group_key_pk", + "columns": [ + "org_id", + "rule_id", + "group_key" + ] + } + }, + { + "kind": "table", + "name": "alert_rules", + "primaryKey": { + "name": "alert_rules_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "anomaly_detector_settings", + "primaryKey": { + "name": "anomaly_detector_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "anomaly_detector_states", + "primaryKey": { + "name": "anomaly_detector_states_org_id_detector_key_pk", + "columns": [ + "org_id", + "detector_key" + ] + } + }, + { + "kind": "table", + "name": "anomaly_incidents", + "primaryKey": { + "name": "anomaly_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "api_keys", + "primaryKey": { + "name": "api_keys_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cancellation_reviews", + "primaryKey": { + "name": "cancellation_reviews_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "chat_identities", + "primaryKey": { + "name": "chat_identities_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "chat_workspaces", + "primaryKey": { + "name": "chat_workspaces_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cli_device_authorizations", + "primaryKey": { + "name": "cli_device_authorizations_pkey", + "columns": [ + "device_code_hash" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_analytics_state", + "primaryKey": { + "name": "cloudflare_analytics_state_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_hyperdrive_configs", + "primaryKey": { + "name": "cloudflare_hyperdrive_configs_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_logpush_connectors", + "primaryKey": { + "name": "cloudflare_logpush_connectors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboard_shares", + "primaryKey": { + "name": "dashboard_shares_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboard_versions", + "primaryKey": { + "name": "dashboard_versions_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboards", + "primaryKey": { + "name": "dashboards_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "digest_subscriptions", + "primaryKey": { + "name": "digest_subscriptions_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_fingerprint_candidates", + "primaryKey": { + "name": "error_fingerprint_candidates_org_id_fingerprint_hash_pk", + "columns": [ + "org_id", + "fingerprint_hash" + ] + } + }, + { + "kind": "table", + "name": "error_incidents", + "primaryKey": { + "name": "error_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_events", + "primaryKey": { + "name": "error_issue_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_pull_requests", + "primaryKey": { + "name": "error_issue_pull_requests_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_states", + "primaryKey": { + "name": "error_issue_states_org_id_issue_id_pk", + "columns": [ + "org_id", + "issue_id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_verifications", + "primaryKey": { + "name": "error_issue_verifications_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issues", + "primaryKey": { + "name": "error_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_notification_deliveries", + "primaryKey": { + "name": "error_notification_deliveries_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_notification_policies", + "primaryKey": { + "name": "error_notification_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "error_tick_states", + "primaryKey": { + "name": "error_tick_states_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "gcp_connectors", + "primaryKey": { + "name": "gcp_connectors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "gcp_resources", + "primaryKey": { + "name": "gcp_resources_pkey", + "columns": [ + "connector_id", + "name" + ] + } + }, + { + "kind": "table", + "name": "investigations", + "primaryKey": { + "name": "investigations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "issue_escalation_policies", + "primaryKey": { + "name": "issue_escalation_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "issue_escalations", + "primaryKey": { + "name": "issue_escalations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "live_activities", + "primaryKey": { + "name": "live_activities_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_authorizations", + "primaryKey": { + "name": "mcp_oauth_authorizations_pkey", + "columns": [ + "request_id_hash" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_clients", + "primaryKey": { + "name": "mcp_oauth_clients_pkey", + "columns": [ + "client_id" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_refresh_tokens", + "primaryKey": { + "name": "mcp_oauth_refresh_tokens_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "mobile_devices", + "primaryKey": { + "name": "mobile_devices_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "oauth_auth_states", + "primaryKey": { + "name": "oauth_auth_states_pkey", + "columns": [ + "state" + ] + } + }, + { + "kind": "table", + "name": "oauth_connections", + "primaryKey": { + "name": "oauth_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_clickhouse_schema_apply_runs", + "primaryKey": { + "name": "org_clickhouse_schema_apply_runs_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_clickhouse_settings", + "primaryKey": { + "name": "org_clickhouse_settings_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_attribute_mappings", + "primaryKey": { + "name": "org_ingest_attribute_mappings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_keys", + "primaryKey": { + "name": "org_ingest_keys_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_sampling_policies", + "primaryKey": { + "name": "org_ingest_sampling_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_onboarding_state", + "primaryKey": { + "name": "org_onboarding_state_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_openrouter_keys", + "primaryKey": { + "name": "org_openrouter_keys_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_recommendation_issues", + "primaryKey": { + "name": "org_recommendation_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_support_channels", + "primaryKey": { + "name": "org_support_channels_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_connections", + "primaryKey": { + "name": "planetscale_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_databases", + "primaryKey": { + "name": "planetscale_databases_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_events", + "primaryKey": { + "name": "planetscale_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_issue_receipts", + "primaryKey": { + "name": "planetscale_issue_receipts_org_id_event_id_pk", + "columns": [ + "org_id", + "event_id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_poll_state", + "primaryKey": { + "name": "planetscale_poll_state_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_edits", + "primaryKey": { + "name": "pr_review_edits_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_finding_embeddings", + "primaryKey": { + "name": "pr_review_finding_embeddings_pkey", + "columns": [ + "finding_id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_findings", + "primaryKey": { + "name": "pr_review_findings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_merge_steps", + "primaryKey": { + "name": "pr_review_merge_steps_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_replies", + "primaryKey": { + "name": "pr_review_replies_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_settings", + "primaryKey": { + "name": "pr_review_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "pr_reviews", + "primaryKey": { + "name": "pr_reviews_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "railway_connections", + "primaryKey": { + "name": "railway_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "railway_environments", + "primaryKey": { + "name": "railway_environments_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "scrape_target_checks", + "primaryKey": { + "name": "scrape_target_checks_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "scrape_targets", + "primaryKey": { + "name": "scrape_targets_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_commits", + "primaryKey": { + "name": "vcs_commits_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_installations", + "primaryKey": { + "name": "vcs_installations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_repositories", + "primaryKey": { + "name": "vcs_repositories_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_repository_branches", + "primaryKey": { + "name": "vcs_repository_branches_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "column", + "table": "actors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "user_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "agent_name", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "model", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "capabilities_json", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "created_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "last_active_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "impact", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "summary", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "reason", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "details", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "related_to", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_type", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_name", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_model", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_version", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "source", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "max_runs_per_day", + "position": 2, + "type": "integer", + "notNull": true, + "default": "20", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "max_passes_per_day", + "position": 3, + "type": "integer", + "notNull": true, + "default": "90", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "updated_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "updated_by", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "rule_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "destination_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "delivery_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "event_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "attempt_number", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "scheduled_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claimed_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claim_expires_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claimed_by", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "attempted_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "provider_message", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "provider_reference", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "response_code", + "position": 16, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "error_message", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "payload_json", + "position": 18, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "config_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_tested_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_test_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "consecutive_failures", + "position": 11, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_failure_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "disabled_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "disabled_reason", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "created_by", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "updated_by", + "position": 18, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "rule_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "incident_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "rule_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "group_key", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "signal_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "severity", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "peak_severity", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "comparator", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "threshold", + "position": 11, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "threshold_upper", + "position": 12, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "first_triggered_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_triggered_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "resolved_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_observed_value", + "position": 16, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_sample_count", + "position": 17, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_evaluated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "dedupe_key", + "position": 19, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_delivered_event_type", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_notified_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "hold_reason", + "position": 22, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "held_since", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "error_issue_id", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "created_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "updated_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "rule_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "last_scheduled_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "rule_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "group_key", + "position": 2, + "type": "text", + "notNull": true, + "default": "'__total__'", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "consecutive_breaches", + "position": 3, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "consecutive_healthy", + "position": 4, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "history_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_status", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_value", + "position": 7, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_sample_count", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_evaluated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "notes", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "notification_template_json", + "position": 4, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "severity", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "service_names_json", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "exclude_service_names_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "environments_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "tags_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "signal_type", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "comparator", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold", + "position": 13, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold_upper", + "position": 14, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "thresholds_json", + "position": 15, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "window_minutes", + "position": 16, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "minimum_sample_count", + "position": 17, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_breaches_required", + "position": 18, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "breaches_out_of", + "position": 19, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_healthy_required", + "position": 20, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "renotify_interval_minutes", + "position": 21, + "type": "integer", + "notNull": true, + "default": "30", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "apdex_threshold_ms", + "position": 22, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_builder_draft_json", + "position": 23, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "raw_query_sql", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "group_by", + "position": 25, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "destination_ids_json", + "position": 26, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_spec_json", + "position": 27, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "reducer", + "position": 28, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "sample_count_strategy", + "position": 29, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "no_data_behavior", + "position": 30, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "last_scheduled_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_by", + "position": 34, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_by", + "position": 35, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "sensitivity", + "position": 2, + "type": "text", + "notNull": true, + "default": "'normal'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "muted_signals_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "last_tick_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "detector_key", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "signal_type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "service_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "deployment_env", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "fingerprint_hash", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "consecutive_breaches", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "consecutive_healthy", + "position": 7, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_status", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_value", + "position": 9, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "baseline_median", + "position": 10, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_sample_count", + "position": 11, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_evaluated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "open_incident_id", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_resolved_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_incident_id", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "detector_key", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "signal_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "deployment_env", + "position": 5, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "fingerprint_hash", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "error_issue_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "opened_value", + "position": 10, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "baseline_median", + "position": 11, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "baseline_sigma", + "position": 12, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "threshold_value", + "position": 13, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_observed_value", + "position": 14, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_sample_count", + "position": 15, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "first_triggered_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_triggered_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "resolved_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "resolve_reason", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "triage_status", + "position": 20, + "type": "text", + "notNull": true, + "default": "'none'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "dedupe_key", + "position": 21, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "fingerprints_json", + "position": 22, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "reopen_count", + "position": 23, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_reopened_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "created_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "updated_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "key_hash", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "key_prefix", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "revoked", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "revoked_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "last_used_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "metadata_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "scopes", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "kind", + "position": 12, + "type": "text", + "notNull": true, + "default": "'standard'", + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_by_email", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "plan_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "subscription_started_at", + "position": 3, + "type": "bigint", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "canceled_at", + "position": 4, + "type": "bigint", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "snapshot_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "rule_reason", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "posted_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "connector", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "external_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "user_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "display_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "connector", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "external_workspace_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "settings", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_ciphertext", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_iv", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_tag", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "device_code_hash", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "user_code_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "device_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_org_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_user_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_roles", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_user_email", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "api_key_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_ciphertext", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_iv", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_tag", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "denied_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "expires_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "account_id", + "position": 2, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "dataset", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "zone_id", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "zone_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "enabled", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "watermark_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "backfill_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "settings_json", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "settings_fetched_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "quantiles_available", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "discovered_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "live_scripts_json", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "grant_call_cap", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_success_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_error_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "lease_until", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "account_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "config_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_host", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_port", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_scheme", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_database", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_user", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "deleted_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "zone_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "dataset", + "position": 5, + "type": "text", + "notNull": true, + "default": "'http_requests'", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_hash", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "enabled", + "position": 10, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "last_received_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "last_error", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_rotated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "created_by", + "position": 16, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "updated_by", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "dashboard_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "widget_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "mode", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_hash", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_suffix", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "created_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "updated_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "revoked_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "dashboard_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "version_number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "snapshot_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "change_kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "change_summary", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "source_version_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "created_by", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "payload_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "created_by", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "version", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "email", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "opted_out_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "day_of_week", + "position": 6, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "timezone", + "position": 7, + "type": "text", + "notNull": true, + "default": "'UTC'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "namespaces_json", + "position": 8, + "type": "text", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "environments_json", + "position": 9, + "type": "text", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "last_sent_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "last_attempted_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_enabled", + "position": 12, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_opted_out_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_last_sent_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_last_attempted_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "updated_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "fingerprint_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "service_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "exception_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "exception_message", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "error_label", + "position": 5, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "top_frame", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "service_versions_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "first_seen_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "last_seen_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "status", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "reason", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "first_triggered_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "last_triggered_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "resolved_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "actor_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "type", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "from_state", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "to_state", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "payload_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "provider", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "external_repo_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "repo_full_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "number", + "position": 6, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "url", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "title", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "author_login", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "state", + "position": 10, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "merged_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "merge_commit_sha", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "link_source", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "linked_by_actor_id", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "issue_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "last_observed_occurrence_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "last_evaluated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "open_incident_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "pull_request_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "status", + "position": 4, + "type": "text", + "notNull": true, + "default": "'waiting'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "merged_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verify_after", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_versions_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_rate_per_hour", + "position": 9, + "type": "double precision", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "investigation_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verdict", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verdict_note", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "post_merge_occurrence_count", + "position": 13, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "attempt", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "kind", + "position": 2, + "type": "text", + "notNull": true, + "default": "'error'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "source_ref_json", + "position": 3, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "fingerprint_hash", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "fingerprint_version", + "position": 5, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "service_name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "exception_type", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "exception_message", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "error_label", + "position": 9, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "top_frame", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "workflow_state", + "position": 11, + "type": "text", + "notNull": true, + "default": "'triage'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "priority", + "position": 12, + "type": "integer", + "notNull": true, + "default": "3", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "severity", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "severity_source", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "assigned_actor_id", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "lease_holder_actor_id", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "lease_expires_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "claimed_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "notes", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "first_seen_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_seen_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "occurrence_count", + "position": 22, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_by_actor_id", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_resolved_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_regressed_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "regression_count", + "position": 27, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "seen_versions_json", + "position": 28, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_versions_json", + "position": 29, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "snooze_until", + "position": 30, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "archived_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "created_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "updated_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "destination_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "delivery_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "payload_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "status", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "attempt_count", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "scheduled_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claimed_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claim_expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claimed_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "attempted_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "error_message", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "destination_ids_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_first_seen", + "position": 3, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_regression", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_resolve", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_transition_in_review", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_transition_done", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_claim", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "min_occurrence_count", + "position": 9, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "severity", + "position": 10, + "type": "text", + "notNull": true, + "default": "'warning'", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "updated_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "processed_through", + "position": 1, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "bootstrap_completed", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "claim_token", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "claim_expires_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "scope_type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "scope_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "project_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "logs_enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_enabled", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_ciphertext", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_iv", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_tag", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_hash", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_received_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_error", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "applied_logs_enabled", + "position": 13, + "type": "boolean", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "applied_metrics_enabled", + "position": 14, + "type": "boolean", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "setup_reported_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_watermark_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_metrics_received_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_metrics_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_lease_until", + "position": 19, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "resources_synced_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_resources_error", + "position": 21, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "created_by", + "position": 22, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "created_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "updated_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "connector_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "asset_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "project_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "location", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "display_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "state", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "labels", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "details", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "resource_created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "resource_updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "last_seen_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": "'investigating'", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "seeded_by", + "position": 3, + "type": "text", + "notNull": true, + "default": "'user'", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "subject_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "snapshot_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "incident_kind", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "incident_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "issue_id", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "report_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "progress_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "severity", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "confidence", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "model", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "input_tokens", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "output_tokens", + "position": 15, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "started_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "autonomous_turns", + "position": 18, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "created_by", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "created_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "diagnosed_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "updated_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "rules_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "updated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "severity", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "source", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "reason", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "run_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "investigation_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "payload_json", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "delivery_results_json", + "position": 9, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "attempts", + "position": 11, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "dedupe_key", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "error", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "processed_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "device_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "incident_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "activity_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "push_token", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "ended_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "ended_reason", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "request_id_hash", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "client_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "client_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "redirect_uri", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "state", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "resource", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "scopes", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "code_challenge", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "authorization_code_hash", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_org_id", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_user_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_roles", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_user_email", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "denied_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "used_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "expires_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_name", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "redirect_uris", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_uri", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "token_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "family_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "client_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "resource", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "scopes", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "org_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "user_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "roles", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "user_email", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "access_key_id", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "replaced_by_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "revoked_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "expires_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "family_expires_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "platform", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "token", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "environment", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "bundle_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "app_version", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "live_activity_start_token", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "device_name", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "preferences", + "position": 10, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "disabled_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "disabled_reason", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "last_pushed_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "last_seen_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "state", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "initiated_by_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "redirect_uri", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "return_to", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "code_verifier", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "expires_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_user_email", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_account_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "granted_accounts_json", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "connected_by_user_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "scope", + "position": 8, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_ciphertext", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_iv", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_tag", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_ciphertext", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_iv", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_tag", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "expires_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "revoked_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "workflow_instance_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "phase", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "current_migration", + "position": 4, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "steps_total", + "position": 5, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "steps_done", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "applied_versions", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "skipped", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "error_message", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "started_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "finished_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "created_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "updated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_url", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_user", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_ciphertext", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_iv", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_tag", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_database", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "sync_status", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "last_sync_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "last_sync_error", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "schema_version", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "created_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "updated_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "source_context", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "source_key", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "target_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "operation", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "enabled", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_key", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_key_hash", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_ciphertext", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_iv", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_tag", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_hash", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_rotated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_rotated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "created_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "updated_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "trace_sample_ratio", + "position": 1, + "type": "double precision", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "always_keep_error_spans", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "always_keep_slow_spans_ms", + "position": 3, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "user_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "email", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "role", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "demo_data_requested", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "onboarding_completed_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "checklist_dismissed_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "first_data_received_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "welcome_email_sent_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "connect_nudge_email_sent_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "stalled_email_sent_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "activation_email_sent_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "reward_claimed_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "reward_reserved_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_ciphertext", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_iv", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_tag", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_hint", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "number", + "position": 2, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "recommendation_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "source_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "canonical_key", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "status", + "position": 7, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "usage_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "opened_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "resolved_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "slack_channel_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "slack_channel_name", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "reservation_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "reserved_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "created_by_user_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "updated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "ps_organization", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "connected_by_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "scrape_target_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_ciphertext", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_iv", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_tag", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "detected_permissions_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "last_inventory_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "last_inventory_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "database_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": "'mysql'", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "state", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "region", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "plan", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "branches_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "deleted_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "database_id", + "position": 2, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "database_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "branch_name", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "category", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "event_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "state", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "external_id", + "position": 8, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "title", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "source", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "actor_login", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "url", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "payload_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "occurred_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "event_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "processed_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "dataset", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "database_id", + "position": 3, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "watermark_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_success_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_error", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_error_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "lease_until", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "reply_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "seq", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "path", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "old_text", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "new_text", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "finding_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "model", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "embedding", + "position": 4, + "type": "real[]", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "review_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "handle", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "path", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "line", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "category", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "title", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "status", + "position": 11, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "comment_id", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "resolved_sha", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "reactions_up", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "reactions_down", + "position": 15, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "updated_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "key", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "title", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "source", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "path", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "done_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "done_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "reminded_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "first_review_id", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "last_review_id", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "comment_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "surface", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "thread_root_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "author_login", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "command", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "head_sha", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "session_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "reply_url", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "commit_sha", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "error", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "model", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "defaults", + "position": 2, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "updated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "head_sha", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "base_sha", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "url", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "title", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "author_login", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "skip_reason", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "session_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "comment_attempt", + "position": 12, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "report_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "score", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "check_run_url", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "comment_url", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "review_url", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "publish_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "error", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "model", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "input_tokens", + "position": 21, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "output_tokens", + "position": 22, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "started_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "finished_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "merged_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "telemetry_json", + "position": 26, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "merge_commit_sha", + "position": 27, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_status", + "position": 28, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_after", + "position": 29, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_json", + "position": 30, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "created_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "updated_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_ciphertext", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_iv", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_tag", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "workspace_names", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "connected_by_user_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "auth_failed_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "discovered_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "lease_until", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_success_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_error_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "project_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "project_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "environment_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "environment_name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "services_json", + "position": 7, + "type": "text", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "enabled", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "watermark_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_success_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_error_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "id", + "position": 0, + "type": "integer", + "notNull": true, + "default": null, + "identity": "by default" + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "target_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "sub_target_key", + "position": 3, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "checked_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "error", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "duration_ms", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "samples_scraped", + "position": 7, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "samples_post_relabel", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "service_name", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "url", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "target_type", + "position": 5, + "type": "text", + "notNull": true, + "default": "'prometheus'", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "discovery_config_json", + "position": 6, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "scrape_interval_seconds", + "position": 7, + "type": "integer", + "notNull": true, + "default": "15", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "labels_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_type", + "position": 9, + "type": "text", + "notNull": true, + "default": "'none'", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "managed_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_ciphertext", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_iv", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_tag", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "enabled", + "position": 14, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "last_scrape_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "last_scrape_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "repository_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "sha", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "message", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_email", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_login", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_avatar_url", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "authored_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "committed_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "html_url", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "external_installation_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_login", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_type", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "external_account_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_avatar_url", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "repository_selection", + "position": 8, + "type": "text", + "notNull": true, + "default": "'all'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'active'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "suspended_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "ci_traces_enabled", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "installed_by_user_id", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "installation_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "external_repo_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "owner", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "full_name", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "default_branch", + "position": 8, + "type": "text", + "notNull": true, + "default": "'main'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "tracked_branch", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "html_url", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "is_private", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "is_archived", + "position": 12, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "status", + "position": 13, + "type": "text", + "notNull": true, + "default": "'active'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "sync_status", + "position": 14, + "type": "text", + "notNull": true, + "default": "'pending'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "last_synced_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "last_sync_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "pr_review_enabled", + "position": 17, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "pr_review_config", + "position": 18, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "repository_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "is_default", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "head_sha", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "updated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_agent_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"agent_name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_type_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"type\"" + ], + "where": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "agent_feedback", + "name": "agent_feedback_kind_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"kind\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "agent_feedback", + "name": "agent_feedback_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_claim_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"claim_expires_at\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_delivery_attempt_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"delivery_key\"", + "\"attempt_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_org_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_incident_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"incident_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_open_group_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"rule_id\"", + "\"group_key\"" + ], + "where": "\"status\" = 'open'" + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"error_issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_rule_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"rule_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rule_claims", + "name": "alert_rule_claims_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rule_states", + "name": "alert_rule_states_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_detector_states", + "name": "anomaly_detector_states_evaluated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"last_evaluated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_detector_states", + "name": "anomaly_detector_states_open_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"open_incident_id\"" + ], + "where": "\"open_incident_id\" is not null" + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_open_detector_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"detector_key\"" + ], + "where": "\"status\" = 'open'" + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_detector_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"detector_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"error_issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_status_triggered_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"", + "\"last_triggered_at\"", + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_triggered_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_triggered_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "api_keys", + "name": "api_keys_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "api_keys", + "name": "api_keys_org_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cancellation_reviews", + "name": "cancellation_reviews_subscription_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"plan_id\"", + "\"subscription_started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_connector_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"connector\"", + "\"external_user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_connector_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"connector\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_workspaces", + "name": "chat_workspaces_connector_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"connector\"", + "\"external_workspace_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_workspaces", + "name": "chat_workspaces_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cli_device_authorizations", + "name": "cli_device_authorizations_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cli_device_authorizations", + "name": "cli_device_authorizations_user_code_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"user_code_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_analytics_state", + "name": "cf_analytics_state_org_account_dataset_zone_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"account_id\"", + "\"dataset\"", + "\"zone_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_analytics_state", + "name": "cf_analytics_state_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_hyperdrive_configs", + "name": "cloudflare_hyperdrive_configs_org_config_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"config_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_hyperdrive_configs", + "name": "cloudflare_hyperdrive_configs_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_secret_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_live_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "coalesce(widget_id, '')" + ], + "where": "revoked_at is null" + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_org_dashboard_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_token_hash_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"token_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_versions", + "name": "dashboard_versions_org_dashboard_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "\"version_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_versions", + "name": "dashboard_versions_org_dashboard_version_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "\"version_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboards", + "name": "dashboards_org_name_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboards", + "name": "dashboards_org_updated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"updated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "digest_subscriptions", + "name": "digest_subscriptions_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "digest_subscriptions", + "name": "digest_subscriptions_org_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_fingerprint_candidates", + "name": "error_fingerprint_candidates_last_seen_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_seen_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_incidents", + "name": "error_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_incidents", + "name": "error_incidents_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"", + "\"last_triggered_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_actor_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"actor_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_type_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"type\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_issue_pr_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"", + "\"provider\"", + "\"repo_full_name\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_repo_number_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"repo_full_name\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"verify_after\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_open_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": "\"status\" in ('waiting', 'running')" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_lease_expiry_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"lease_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_archived_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"archived_at\"" + ], + "where": "\"archived_at\" is not null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_assignee_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"assigned_actor_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_fp_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"fingerprint_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_fp_version_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"fingerprint_version\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_live_seen_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_seen_at\"", + "\"id\"" + ], + "where": "\"archived_at\" is null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_severity_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"severity\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_workflow_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"workflow_state\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"", + "\"claim_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_key_destination_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"delivery_key\"", + "\"destination_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_tick_states", + "name": "error_tick_states_claim_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"claim_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_org_scope_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"scope_type\"", + "\"scope_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_secret_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_incident_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_kind\"", + "\"incident_id\"" + ], + "where": "\"incident_id\" is not null" + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"dedupe_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "live_activities", + "name": "live_activities_device_incident_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"device_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "live_activities", + "name": "live_activities_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_authorizations", + "name": "mcp_oauth_authorizations_code_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"authorization_code_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_authorizations", + "name": "mcp_oauth_authorizations_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_family_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"family_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"token_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_org_platform_token_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"platform\"", + "\"token\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_auth_states", + "name": "oauth_auth_states_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_org_provider_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_attribute_mappings", + "name": "org_ingest_attribute_mappings_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_private_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"private_key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_public_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"public_key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_public_key_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"public_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_recommendation_issues", + "name": "org_recommendation_issues_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_recommendation_issues", + "name": "org_recommendation_issues_org_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"recommendation_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_connections", + "name": "planetscale_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_databases", + "name": "planetscale_databases_org_db_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_databases", + "name": "planetscale_databases_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_name\"", + "\"event_type\"", + "\"external_id\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_org_db_time_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_name\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_org_time_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_issue_receipts", + "name": "planetscale_issue_receipts_processed_at_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"processed_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_poll_state", + "name": "planetscale_poll_state_org_dataset_db_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dataset\"", + "\"database_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_poll_state", + "name": "planetscale_poll_state_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_edits", + "name": "pr_review_edits_reply_seq_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"reply_id\"", + "\"seq\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_finding_embeddings", + "name": "pr_review_finding_embeddings_org_model_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"model\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_findings", + "name": "pr_review_findings_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_findings", + "name": "pr_review_findings_pr_handle_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"handle\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_merge_steps", + "name": "pr_review_merge_steps_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_merge_steps", + "name": "pr_review_merge_steps_pr_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_replies", + "name": "pr_review_replies_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_replies", + "name": "pr_review_replies_repo_comment_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"comment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_post_merge_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"post_merge_after\"" + ], + "where": "\"post_merge_status\" = 'waiting'" + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_repo_number_head_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"head_sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_repo_number_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_connections", + "name": "railway_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_environments", + "name": "railway_environments_connection_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"connection_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_environments", + "name": "railway_environments_org_env_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"environment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_target_checks", + "name": "scrape_target_checks_target_checked_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"target_id\"", + "\"checked_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_commits", + "name": "vcs_commits_org_sha_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_commits", + "name": "vcs_commits_repo_sha_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_provider_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"provider\"", + "\"external_installation_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_installation_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"installation_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_org_repo_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"external_repo_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repository_branches", + "name": "vcs_repository_branches_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repository_branches", + "name": "vcs_repository_branches_repo_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "foreign_key", + "table": "dashboard_shares", + "name": "dashboard_shares_dashboard_fk", + "columns": [ + "org_id", + "dashboard_id" + ], + "foreignTable": "dashboards", + "foreignColumns": [ + "org_id", + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "gcp_resources", + "name": "gcp_resources_connector_id_gcp_connectors_id_fk", + "columns": [ + "connector_id" + ], + "foreignTable": "gcp_connectors", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "scrape_target_checks", + "name": "scrape_target_checks_target_id_scrape_targets_id_fk", + "columns": [ + "target_id" + ], + "foreignTable": "scrape_targets", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + } + ] +} diff --git a/packages/db/drizzle/20261017151322_merge_alert_tiers_and_digest/migration.sql b/packages/db/drizzle/20261017151322_merge_alert_tiers_and_digest/migration.sql new file mode 100644 index 0000000000..8f9e0a9059 --- /dev/null +++ b/packages/db/drizzle/20261017151322_merge_alert_tiers_and_digest/migration.sql @@ -0,0 +1,3 @@ +-- Custom SQL migration. Separate statements with a line holding only: +-- --> statement-breakpoint +-- Joins the alert severity tiers and digest send hour branches; runs nothing. diff --git a/packages/db/drizzle/20261017151322_merge_alert_tiers_and_digest/snapshot.json b/packages/db/drizzle/20261017151322_merge_alert_tiers_and_digest/snapshot.json new file mode 100644 index 0000000000..305a0678de --- /dev/null +++ b/packages/db/drizzle/20261017151322_merge_alert_tiers_and_digest/snapshot.json @@ -0,0 +1,13142 @@ +{ + "version": "1", + "dialect": "postgres", + "id": "38f536dabd1b4b041c1aecc2dbde113378874aeb9c7aef40da11c459f790c881", + "prevIds": [ + "e711d5dde160d7c4c99464419fd2f843ff61414db882e429136e4ed15160462e", + "a240a246596b6192403f5133d7dbd6f57e559f11ea408e7c6998b0049ab9c03e" + ], + "entities": [ + { + "kind": "table", + "name": "actors", + "primaryKey": { + "name": "actors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "agent_feedback", + "primaryKey": { + "name": "agent_feedback_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "ai_triage_settings", + "primaryKey": { + "name": "ai_triage_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "alert_delivery_events", + "primaryKey": { + "name": "alert_delivery_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_destinations", + "primaryKey": { + "name": "alert_destinations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_incidents", + "primaryKey": { + "name": "alert_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_rule_claims", + "primaryKey": { + "name": "alert_rule_claims_pkey", + "columns": [ + "rule_id" + ] + } + }, + { + "kind": "table", + "name": "alert_rule_states", + "primaryKey": { + "name": "alert_rule_states_org_id_rule_id_group_key_pk", + "columns": [ + "org_id", + "rule_id", + "group_key" + ] + } + }, + { + "kind": "table", + "name": "alert_rules", + "primaryKey": { + "name": "alert_rules_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "anomaly_detector_settings", + "primaryKey": { + "name": "anomaly_detector_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "anomaly_detector_states", + "primaryKey": { + "name": "anomaly_detector_states_org_id_detector_key_pk", + "columns": [ + "org_id", + "detector_key" + ] + } + }, + { + "kind": "table", + "name": "anomaly_incidents", + "primaryKey": { + "name": "anomaly_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "api_keys", + "primaryKey": { + "name": "api_keys_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cancellation_reviews", + "primaryKey": { + "name": "cancellation_reviews_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "chat_identities", + "primaryKey": { + "name": "chat_identities_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "chat_workspaces", + "primaryKey": { + "name": "chat_workspaces_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cli_device_authorizations", + "primaryKey": { + "name": "cli_device_authorizations_pkey", + "columns": [ + "device_code_hash" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_analytics_state", + "primaryKey": { + "name": "cloudflare_analytics_state_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_hyperdrive_configs", + "primaryKey": { + "name": "cloudflare_hyperdrive_configs_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_logpush_connectors", + "primaryKey": { + "name": "cloudflare_logpush_connectors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboard_shares", + "primaryKey": { + "name": "dashboard_shares_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboard_versions", + "primaryKey": { + "name": "dashboard_versions_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboards", + "primaryKey": { + "name": "dashboards_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "digest_subscriptions", + "primaryKey": { + "name": "digest_subscriptions_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_fingerprint_candidates", + "primaryKey": { + "name": "error_fingerprint_candidates_org_id_fingerprint_hash_pk", + "columns": [ + "org_id", + "fingerprint_hash" + ] + } + }, + { + "kind": "table", + "name": "error_incidents", + "primaryKey": { + "name": "error_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_events", + "primaryKey": { + "name": "error_issue_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_pull_requests", + "primaryKey": { + "name": "error_issue_pull_requests_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_states", + "primaryKey": { + "name": "error_issue_states_org_id_issue_id_pk", + "columns": [ + "org_id", + "issue_id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_verifications", + "primaryKey": { + "name": "error_issue_verifications_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issues", + "primaryKey": { + "name": "error_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_notification_deliveries", + "primaryKey": { + "name": "error_notification_deliveries_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_notification_policies", + "primaryKey": { + "name": "error_notification_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "error_tick_states", + "primaryKey": { + "name": "error_tick_states_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "gcp_connectors", + "primaryKey": { + "name": "gcp_connectors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "gcp_resources", + "primaryKey": { + "name": "gcp_resources_pkey", + "columns": [ + "connector_id", + "name" + ] + } + }, + { + "kind": "table", + "name": "investigations", + "primaryKey": { + "name": "investigations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "issue_escalation_policies", + "primaryKey": { + "name": "issue_escalation_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "issue_escalations", + "primaryKey": { + "name": "issue_escalations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "live_activities", + "primaryKey": { + "name": "live_activities_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_authorizations", + "primaryKey": { + "name": "mcp_oauth_authorizations_pkey", + "columns": [ + "request_id_hash" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_clients", + "primaryKey": { + "name": "mcp_oauth_clients_pkey", + "columns": [ + "client_id" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_refresh_tokens", + "primaryKey": { + "name": "mcp_oauth_refresh_tokens_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "mobile_devices", + "primaryKey": { + "name": "mobile_devices_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "oauth_auth_states", + "primaryKey": { + "name": "oauth_auth_states_pkey", + "columns": [ + "state" + ] + } + }, + { + "kind": "table", + "name": "oauth_connections", + "primaryKey": { + "name": "oauth_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_clickhouse_schema_apply_runs", + "primaryKey": { + "name": "org_clickhouse_schema_apply_runs_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_clickhouse_settings", + "primaryKey": { + "name": "org_clickhouse_settings_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_attribute_mappings", + "primaryKey": { + "name": "org_ingest_attribute_mappings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_keys", + "primaryKey": { + "name": "org_ingest_keys_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_sampling_policies", + "primaryKey": { + "name": "org_ingest_sampling_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_onboarding_state", + "primaryKey": { + "name": "org_onboarding_state_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_openrouter_keys", + "primaryKey": { + "name": "org_openrouter_keys_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_recommendation_issues", + "primaryKey": { + "name": "org_recommendation_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_support_channels", + "primaryKey": { + "name": "org_support_channels_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_connections", + "primaryKey": { + "name": "planetscale_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_databases", + "primaryKey": { + "name": "planetscale_databases_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_events", + "primaryKey": { + "name": "planetscale_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_issue_receipts", + "primaryKey": { + "name": "planetscale_issue_receipts_org_id_event_id_pk", + "columns": [ + "org_id", + "event_id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_poll_state", + "primaryKey": { + "name": "planetscale_poll_state_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_edits", + "primaryKey": { + "name": "pr_review_edits_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_finding_embeddings", + "primaryKey": { + "name": "pr_review_finding_embeddings_pkey", + "columns": [ + "finding_id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_findings", + "primaryKey": { + "name": "pr_review_findings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_merge_steps", + "primaryKey": { + "name": "pr_review_merge_steps_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_replies", + "primaryKey": { + "name": "pr_review_replies_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_settings", + "primaryKey": { + "name": "pr_review_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "pr_reviews", + "primaryKey": { + "name": "pr_reviews_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "railway_connections", + "primaryKey": { + "name": "railway_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "railway_environments", + "primaryKey": { + "name": "railway_environments_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "scrape_target_checks", + "primaryKey": { + "name": "scrape_target_checks_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "scrape_targets", + "primaryKey": { + "name": "scrape_targets_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_commits", + "primaryKey": { + "name": "vcs_commits_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_installations", + "primaryKey": { + "name": "vcs_installations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_repositories", + "primaryKey": { + "name": "vcs_repositories_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_repository_branches", + "primaryKey": { + "name": "vcs_repository_branches_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "column", + "table": "actors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "user_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "agent_name", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "model", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "capabilities_json", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "created_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "last_active_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "impact", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "summary", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "reason", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "details", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "related_to", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_type", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_name", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_model", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_version", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "source", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "max_runs_per_day", + "position": 2, + "type": "integer", + "notNull": true, + "default": "20", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "max_passes_per_day", + "position": 3, + "type": "integer", + "notNull": true, + "default": "90", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "updated_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "updated_by", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "rule_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "destination_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "delivery_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "event_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "attempt_number", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "scheduled_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claimed_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claim_expires_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claimed_by", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "attempted_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "provider_message", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "provider_reference", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "response_code", + "position": 16, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "error_message", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "payload_json", + "position": 18, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "config_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_tested_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_test_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "consecutive_failures", + "position": 11, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_failure_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "disabled_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "disabled_reason", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "created_by", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "updated_by", + "position": 18, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "rule_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "incident_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "rule_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "group_key", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "signal_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "severity", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "peak_severity", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "comparator", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "threshold", + "position": 11, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "threshold_upper", + "position": 12, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "first_triggered_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_triggered_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "resolved_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_observed_value", + "position": 16, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_sample_count", + "position": 17, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_evaluated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "dedupe_key", + "position": 19, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_delivered_event_type", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_notified_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "hold_reason", + "position": 22, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "held_since", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "error_issue_id", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "created_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "updated_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "rule_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "last_scheduled_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "rule_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "group_key", + "position": 2, + "type": "text", + "notNull": true, + "default": "'__total__'", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "consecutive_breaches", + "position": 3, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "consecutive_healthy", + "position": 4, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "history_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_status", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_value", + "position": 7, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_sample_count", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_evaluated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "notes", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "notification_template_json", + "position": 4, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "severity", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "service_names_json", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "exclude_service_names_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "environments_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "tags_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "signal_type", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "comparator", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold", + "position": 13, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold_upper", + "position": 14, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "thresholds_json", + "position": 15, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "window_minutes", + "position": 16, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "minimum_sample_count", + "position": 17, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_breaches_required", + "position": 18, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "breaches_out_of", + "position": 19, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_healthy_required", + "position": 20, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "renotify_interval_minutes", + "position": 21, + "type": "integer", + "notNull": true, + "default": "30", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "apdex_threshold_ms", + "position": 22, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_builder_draft_json", + "position": 23, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "raw_query_sql", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "group_by", + "position": 25, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "destination_ids_json", + "position": 26, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_spec_json", + "position": 27, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "reducer", + "position": 28, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "sample_count_strategy", + "position": 29, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "no_data_behavior", + "position": 30, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "last_scheduled_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_by", + "position": 34, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_by", + "position": 35, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "sensitivity", + "position": 2, + "type": "text", + "notNull": true, + "default": "'normal'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "muted_signals_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "last_tick_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "detector_key", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "signal_type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "service_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "deployment_env", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "fingerprint_hash", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "consecutive_breaches", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "consecutive_healthy", + "position": 7, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_status", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_value", + "position": 9, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "baseline_median", + "position": 10, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_sample_count", + "position": 11, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_evaluated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "open_incident_id", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_resolved_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_incident_id", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "detector_key", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "signal_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "deployment_env", + "position": 5, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "fingerprint_hash", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "error_issue_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "opened_value", + "position": 10, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "baseline_median", + "position": 11, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "baseline_sigma", + "position": 12, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "threshold_value", + "position": 13, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_observed_value", + "position": 14, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_sample_count", + "position": 15, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "first_triggered_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_triggered_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "resolved_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "resolve_reason", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "triage_status", + "position": 20, + "type": "text", + "notNull": true, + "default": "'none'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "dedupe_key", + "position": 21, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "fingerprints_json", + "position": 22, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "reopen_count", + "position": 23, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_reopened_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "created_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "updated_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "key_hash", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "key_prefix", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "revoked", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "revoked_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "last_used_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "metadata_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "scopes", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "kind", + "position": 12, + "type": "text", + "notNull": true, + "default": "'standard'", + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_by_email", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "plan_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "subscription_started_at", + "position": 3, + "type": "bigint", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "canceled_at", + "position": 4, + "type": "bigint", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "snapshot_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "rule_reason", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "posted_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "connector", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "external_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "user_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "display_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "connector", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "external_workspace_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "settings", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_ciphertext", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_iv", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_tag", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "device_code_hash", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "user_code_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "device_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_org_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_user_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_roles", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_user_email", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "api_key_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_ciphertext", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_iv", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_tag", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "denied_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "expires_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "account_id", + "position": 2, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "dataset", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "zone_id", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "zone_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "enabled", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "watermark_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "backfill_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "settings_json", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "settings_fetched_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "quantiles_available", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "discovered_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "live_scripts_json", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "grant_call_cap", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_success_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_error_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "lease_until", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "account_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "config_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_host", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_port", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_scheme", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_database", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_user", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "deleted_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "zone_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "dataset", + "position": 5, + "type": "text", + "notNull": true, + "default": "'http_requests'", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_hash", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "enabled", + "position": 10, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "last_received_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "last_error", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_rotated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "created_by", + "position": 16, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "updated_by", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "dashboard_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "widget_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "mode", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_hash", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_suffix", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "created_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "updated_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "revoked_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "dashboard_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "version_number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "snapshot_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "change_kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "change_summary", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "source_version_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "created_by", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "payload_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "created_by", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "version", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "email", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "opted_out_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "day_of_week", + "position": 6, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "timezone", + "position": 7, + "type": "text", + "notNull": true, + "default": "'UTC'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "send_hour", + "position": 8, + "type": "integer", + "notNull": true, + "default": "9", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "namespaces_json", + "position": 9, + "type": "text", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "environments_json", + "position": 10, + "type": "text", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "last_sent_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "last_attempted_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_enabled", + "position": 13, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_opted_out_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_last_sent_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_last_attempted_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "fingerprint_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "service_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "exception_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "exception_message", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "error_label", + "position": 5, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "top_frame", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "service_versions_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "first_seen_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "last_seen_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "status", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "reason", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "first_triggered_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "last_triggered_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "resolved_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "actor_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "type", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "from_state", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "to_state", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "payload_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "provider", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "external_repo_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "repo_full_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "number", + "position": 6, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "url", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "title", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "author_login", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "state", + "position": 10, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "merged_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "merge_commit_sha", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "link_source", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "linked_by_actor_id", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "issue_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "last_observed_occurrence_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "last_evaluated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "open_incident_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "pull_request_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "status", + "position": 4, + "type": "text", + "notNull": true, + "default": "'waiting'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "merged_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verify_after", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_versions_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_rate_per_hour", + "position": 9, + "type": "double precision", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "investigation_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verdict", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verdict_note", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "post_merge_occurrence_count", + "position": 13, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "attempt", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "kind", + "position": 2, + "type": "text", + "notNull": true, + "default": "'error'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "source_ref_json", + "position": 3, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "fingerprint_hash", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "fingerprint_version", + "position": 5, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "service_name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "exception_type", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "exception_message", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "error_label", + "position": 9, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "top_frame", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "workflow_state", + "position": 11, + "type": "text", + "notNull": true, + "default": "'triage'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "priority", + "position": 12, + "type": "integer", + "notNull": true, + "default": "3", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "severity", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "severity_source", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "assigned_actor_id", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "lease_holder_actor_id", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "lease_expires_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "claimed_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "notes", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "first_seen_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_seen_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "occurrence_count", + "position": 22, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_by_actor_id", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_resolved_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_regressed_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "regression_count", + "position": 27, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "seen_versions_json", + "position": 28, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_versions_json", + "position": 29, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "snooze_until", + "position": 30, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "archived_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "created_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "updated_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "destination_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "delivery_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "payload_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "status", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "attempt_count", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "scheduled_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claimed_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claim_expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claimed_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "attempted_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "error_message", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "destination_ids_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_first_seen", + "position": 3, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_regression", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_resolve", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_transition_in_review", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_transition_done", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_claim", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "min_occurrence_count", + "position": 9, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "severity", + "position": 10, + "type": "text", + "notNull": true, + "default": "'warning'", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "updated_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "processed_through", + "position": 1, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "bootstrap_completed", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "claim_token", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "claim_expires_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "scope_type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "scope_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "project_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "logs_enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_enabled", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_ciphertext", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_iv", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_tag", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_hash", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_received_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_error", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "applied_logs_enabled", + "position": 13, + "type": "boolean", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "applied_metrics_enabled", + "position": 14, + "type": "boolean", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "setup_reported_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_watermark_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_metrics_received_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_metrics_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_lease_until", + "position": 19, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "resources_synced_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_resources_error", + "position": 21, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "created_by", + "position": 22, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "created_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "updated_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "connector_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "asset_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "project_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "location", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "display_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "state", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "labels", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "details", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "resource_created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "resource_updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "last_seen_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": "'investigating'", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "seeded_by", + "position": 3, + "type": "text", + "notNull": true, + "default": "'user'", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "subject_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "snapshot_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "incident_kind", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "incident_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "issue_id", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "report_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "progress_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "severity", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "confidence", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "model", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "input_tokens", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "output_tokens", + "position": 15, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "started_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "autonomous_turns", + "position": 18, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "created_by", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "created_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "diagnosed_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "updated_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "rules_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "updated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "severity", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "source", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "reason", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "run_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "investigation_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "payload_json", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "delivery_results_json", + "position": 9, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "attempts", + "position": 11, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "dedupe_key", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "error", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "processed_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "device_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "incident_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "activity_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "push_token", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "ended_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "ended_reason", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "request_id_hash", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "client_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "client_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "redirect_uri", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "state", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "resource", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "scopes", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "code_challenge", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "authorization_code_hash", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_org_id", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_user_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_roles", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_user_email", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "denied_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "used_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "expires_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_name", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "redirect_uris", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_uri", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "token_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "family_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "client_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "resource", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "scopes", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "org_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "user_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "roles", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "user_email", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "access_key_id", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "replaced_by_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "revoked_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "expires_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "family_expires_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "platform", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "token", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "environment", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "bundle_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "app_version", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "live_activity_start_token", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "device_name", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "preferences", + "position": 10, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "disabled_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "disabled_reason", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "last_pushed_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "last_seen_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "state", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "initiated_by_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "redirect_uri", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "return_to", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "code_verifier", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "expires_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_user_email", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_account_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "granted_accounts_json", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "connected_by_user_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "scope", + "position": 8, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_ciphertext", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_iv", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_tag", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_ciphertext", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_iv", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_tag", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "expires_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "revoked_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "workflow_instance_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "phase", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "current_migration", + "position": 4, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "steps_total", + "position": 5, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "steps_done", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "applied_versions", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "skipped", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "error_message", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "started_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "finished_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "created_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "updated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_url", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_user", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_ciphertext", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_iv", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_tag", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_database", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "sync_status", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "last_sync_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "last_sync_error", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "schema_version", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "created_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "updated_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "source_context", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "source_key", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "target_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "operation", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "enabled", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_key", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_key_hash", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_ciphertext", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_iv", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_tag", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_hash", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_rotated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_rotated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "created_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "updated_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "trace_sample_ratio", + "position": 1, + "type": "double precision", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "always_keep_error_spans", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "always_keep_slow_spans_ms", + "position": 3, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "user_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "email", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "role", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "demo_data_requested", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "onboarding_completed_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "checklist_dismissed_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "first_data_received_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "welcome_email_sent_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "connect_nudge_email_sent_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "stalled_email_sent_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "activation_email_sent_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "reward_claimed_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "reward_reserved_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_ciphertext", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_iv", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_tag", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_hint", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "number", + "position": 2, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "recommendation_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "source_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "canonical_key", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "status", + "position": 7, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "usage_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "opened_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "resolved_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "slack_channel_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "slack_channel_name", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "reservation_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "reserved_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "created_by_user_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "updated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "ps_organization", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "connected_by_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "scrape_target_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_ciphertext", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_iv", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_tag", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "detected_permissions_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "last_inventory_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "last_inventory_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "database_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": "'mysql'", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "state", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "region", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "plan", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "branches_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "deleted_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "database_id", + "position": 2, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "database_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "branch_name", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "category", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "event_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "state", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "external_id", + "position": 8, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "title", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "source", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "actor_login", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "url", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "payload_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "occurred_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "event_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "processed_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "dataset", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "database_id", + "position": 3, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "watermark_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_success_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_error", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_error_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "lease_until", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "reply_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "seq", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "path", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "old_text", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "new_text", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "finding_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "model", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "embedding", + "position": 4, + "type": "real[]", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "review_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "handle", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "path", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "line", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "category", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "title", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "status", + "position": 11, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "comment_id", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "resolved_sha", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "reactions_up", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "reactions_down", + "position": 15, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "updated_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "key", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "title", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "source", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "path", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "done_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "done_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "reminded_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "first_review_id", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "last_review_id", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "comment_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "surface", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "thread_root_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "author_login", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "command", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "head_sha", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "session_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "reply_url", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "commit_sha", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "error", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "model", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "defaults", + "position": 2, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "updated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "head_sha", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "base_sha", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "url", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "title", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "author_login", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "skip_reason", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "session_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "comment_attempt", + "position": 12, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "report_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "score", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "check_run_url", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "comment_url", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "review_url", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "publish_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "error", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "model", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "input_tokens", + "position": 21, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "output_tokens", + "position": 22, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "started_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "finished_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "merged_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "telemetry_json", + "position": 26, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "merge_commit_sha", + "position": 27, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_status", + "position": 28, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_after", + "position": 29, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_json", + "position": 30, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "created_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "updated_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_ciphertext", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_iv", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_tag", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "workspace_names", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "connected_by_user_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "auth_failed_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "discovered_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "lease_until", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_success_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_error_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "project_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "project_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "environment_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "environment_name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "services_json", + "position": 7, + "type": "text", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "enabled", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "watermark_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_success_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_error_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "id", + "position": 0, + "type": "integer", + "notNull": true, + "default": null, + "identity": "by default" + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "target_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "sub_target_key", + "position": 3, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "checked_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "error", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "duration_ms", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "samples_scraped", + "position": 7, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "samples_post_relabel", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "service_name", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "url", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "target_type", + "position": 5, + "type": "text", + "notNull": true, + "default": "'prometheus'", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "discovery_config_json", + "position": 6, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "scrape_interval_seconds", + "position": 7, + "type": "integer", + "notNull": true, + "default": "15", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "labels_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_type", + "position": 9, + "type": "text", + "notNull": true, + "default": "'none'", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "managed_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_ciphertext", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_iv", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_tag", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "enabled", + "position": 14, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "last_scrape_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "last_scrape_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "repository_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "sha", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "message", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_email", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_login", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_avatar_url", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "authored_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "committed_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "html_url", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "external_installation_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_login", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_type", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "external_account_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_avatar_url", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "repository_selection", + "position": 8, + "type": "text", + "notNull": true, + "default": "'all'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'active'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "suspended_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "ci_traces_enabled", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "installed_by_user_id", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "installation_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "external_repo_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "owner", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "full_name", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "default_branch", + "position": 8, + "type": "text", + "notNull": true, + "default": "'main'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "tracked_branch", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "html_url", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "is_private", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "is_archived", + "position": 12, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "status", + "position": 13, + "type": "text", + "notNull": true, + "default": "'active'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "sync_status", + "position": 14, + "type": "text", + "notNull": true, + "default": "'pending'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "last_synced_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "last_sync_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "pr_review_enabled", + "position": 17, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "pr_review_config", + "position": 18, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "repository_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "is_default", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "head_sha", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "updated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_agent_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"agent_name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_type_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"type\"" + ], + "where": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "agent_feedback", + "name": "agent_feedback_kind_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"kind\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "agent_feedback", + "name": "agent_feedback_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_claim_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"claim_expires_at\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_delivery_attempt_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"delivery_key\"", + "\"attempt_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_org_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_incident_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"incident_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_open_group_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"rule_id\"", + "\"group_key\"" + ], + "where": "\"status\" = 'open'" + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"error_issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_rule_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"rule_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rule_claims", + "name": "alert_rule_claims_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rule_states", + "name": "alert_rule_states_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_detector_states", + "name": "anomaly_detector_states_evaluated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"last_evaluated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_detector_states", + "name": "anomaly_detector_states_open_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"open_incident_id\"" + ], + "where": "\"open_incident_id\" is not null" + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_open_detector_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"detector_key\"" + ], + "where": "\"status\" = 'open'" + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_detector_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"detector_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"error_issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_status_triggered_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"", + "\"last_triggered_at\"", + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_triggered_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_triggered_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "api_keys", + "name": "api_keys_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "api_keys", + "name": "api_keys_org_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cancellation_reviews", + "name": "cancellation_reviews_subscription_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"plan_id\"", + "\"subscription_started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_connector_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"connector\"", + "\"external_user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_connector_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"connector\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_workspaces", + "name": "chat_workspaces_connector_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"connector\"", + "\"external_workspace_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_workspaces", + "name": "chat_workspaces_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cli_device_authorizations", + "name": "cli_device_authorizations_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cli_device_authorizations", + "name": "cli_device_authorizations_user_code_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"user_code_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_analytics_state", + "name": "cf_analytics_state_org_account_dataset_zone_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"account_id\"", + "\"dataset\"", + "\"zone_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_analytics_state", + "name": "cf_analytics_state_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_hyperdrive_configs", + "name": "cloudflare_hyperdrive_configs_org_config_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"config_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_hyperdrive_configs", + "name": "cloudflare_hyperdrive_configs_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_secret_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_live_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "coalesce(widget_id, '')" + ], + "where": "revoked_at is null" + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_org_dashboard_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_token_hash_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"token_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_versions", + "name": "dashboard_versions_org_dashboard_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "\"version_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_versions", + "name": "dashboard_versions_org_dashboard_version_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "\"version_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboards", + "name": "dashboards_org_name_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboards", + "name": "dashboards_org_updated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"updated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "digest_subscriptions", + "name": "digest_subscriptions_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "digest_subscriptions", + "name": "digest_subscriptions_org_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_fingerprint_candidates", + "name": "error_fingerprint_candidates_last_seen_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_seen_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_incidents", + "name": "error_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_incidents", + "name": "error_incidents_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"", + "\"last_triggered_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_actor_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"actor_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_type_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"type\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_issue_pr_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"", + "\"provider\"", + "\"repo_full_name\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_repo_number_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"repo_full_name\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"verify_after\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_open_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": "\"status\" in ('waiting', 'running')" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_lease_expiry_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"lease_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_archived_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"archived_at\"" + ], + "where": "\"archived_at\" is not null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_assignee_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"assigned_actor_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_fp_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"fingerprint_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_fp_version_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"fingerprint_version\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_live_seen_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_seen_at\"", + "\"id\"" + ], + "where": "\"archived_at\" is null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_severity_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"severity\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_workflow_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"workflow_state\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"", + "\"claim_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_key_destination_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"delivery_key\"", + "\"destination_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_tick_states", + "name": "error_tick_states_claim_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"claim_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_org_scope_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"scope_type\"", + "\"scope_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_secret_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_incident_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_kind\"", + "\"incident_id\"" + ], + "where": "\"incident_id\" is not null" + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"dedupe_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "live_activities", + "name": "live_activities_device_incident_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"device_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "live_activities", + "name": "live_activities_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_authorizations", + "name": "mcp_oauth_authorizations_code_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"authorization_code_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_authorizations", + "name": "mcp_oauth_authorizations_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_family_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"family_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"token_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_org_platform_token_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"platform\"", + "\"token\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_auth_states", + "name": "oauth_auth_states_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_org_provider_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_attribute_mappings", + "name": "org_ingest_attribute_mappings_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_private_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"private_key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_public_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"public_key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_public_key_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"public_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_recommendation_issues", + "name": "org_recommendation_issues_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_recommendation_issues", + "name": "org_recommendation_issues_org_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"recommendation_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_connections", + "name": "planetscale_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_databases", + "name": "planetscale_databases_org_db_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_databases", + "name": "planetscale_databases_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_name\"", + "\"event_type\"", + "\"external_id\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_org_db_time_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_name\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_org_time_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_issue_receipts", + "name": "planetscale_issue_receipts_processed_at_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"processed_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_poll_state", + "name": "planetscale_poll_state_org_dataset_db_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dataset\"", + "\"database_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_poll_state", + "name": "planetscale_poll_state_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_edits", + "name": "pr_review_edits_reply_seq_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"reply_id\"", + "\"seq\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_finding_embeddings", + "name": "pr_review_finding_embeddings_org_model_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"model\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_findings", + "name": "pr_review_findings_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_findings", + "name": "pr_review_findings_pr_handle_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"handle\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_merge_steps", + "name": "pr_review_merge_steps_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_merge_steps", + "name": "pr_review_merge_steps_pr_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_replies", + "name": "pr_review_replies_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_replies", + "name": "pr_review_replies_repo_comment_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"comment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_post_merge_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"post_merge_after\"" + ], + "where": "\"post_merge_status\" = 'waiting'" + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_repo_number_head_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"head_sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_repo_number_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_connections", + "name": "railway_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_environments", + "name": "railway_environments_connection_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"connection_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_environments", + "name": "railway_environments_org_env_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"environment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_target_checks", + "name": "scrape_target_checks_target_checked_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"target_id\"", + "\"checked_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_commits", + "name": "vcs_commits_org_sha_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_commits", + "name": "vcs_commits_repo_sha_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_provider_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"provider\"", + "\"external_installation_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_installation_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"installation_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_org_repo_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"external_repo_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repository_branches", + "name": "vcs_repository_branches_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repository_branches", + "name": "vcs_repository_branches_repo_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "foreign_key", + "table": "dashboard_shares", + "name": "dashboard_shares_dashboard_fk", + "columns": [ + "org_id", + "dashboard_id" + ], + "foreignTable": "dashboards", + "foreignColumns": [ + "org_id", + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "gcp_resources", + "name": "gcp_resources_connector_id_gcp_connectors_id_fk", + "columns": [ + "connector_id" + ], + "foreignTable": "gcp_connectors", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "scrape_target_checks", + "name": "scrape_target_checks_target_id_scrape_targets_id_fk", + "columns": [ + "target_id" + ], + "foreignTable": "scrape_targets", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + } + ] +} diff --git a/packages/db/src/tables/alerts.ts b/packages/db/src/tables/alerts.ts index 133bc293df..8b975a162f 100644 --- a/packages/db/src/tables/alerts.ts +++ b/packages/db/src/tables/alerts.ts @@ -10,6 +10,7 @@ import { import { Schema } from "effect" const StringArray = Schema.Array(Schema.String) +const TickHistory = Schema.Array(Schema.Number) export const AlertDestinations = PG.table("alert_destinations", { columns: { @@ -76,12 +77,19 @@ export const AlertRules = PG.table("alert_rules", { comparator: PG.text, threshold: PG.float8, thresholdUpper: PG.column(PG.nullable(PG.float8), { name: "threshold_upper" }), + /** + * `AlertThresholds`. Null on pre-tier rows, which read as one tier from the + * legacy columns; writes keep those columns in step until they are dropped. + */ + thresholdsJson: PG.column(PG.nullable(PG.jsonb()), { name: "thresholds_json" }), windowMinutes: PG.column(PG.int4, { name: "window_minutes" }), minimumSampleCount: PG.column(PG.int4, { name: "minimum_sample_count", default: 0 }), consecutiveBreachesRequired: PG.column(PG.int4, { name: "consecutive_breaches_required", default: 2, }), + /** M in "N of the last M checks"; null is N in a row. */ + breachesOutOf: PG.column(PG.nullable(PG.int4), { name: "breaches_out_of" }), consecutiveHealthyRequired: PG.column(PG.int4, { name: "consecutive_healthy_required", default: 2 }), renotifyIntervalMinutes: PG.column(PG.int4, { name: "renotify_interval_minutes", default: 30 }), apdexThresholdMs: PG.column(PG.nullable(PG.float8), { name: "apdex_threshold_ms" }), @@ -142,6 +150,11 @@ export const AlertRuleStates = PG.table("alert_rule_states", { groupKey: PG.column(PG.text, { name: "group_key", default: "__total__" }), consecutiveBreaches: PG.column(PG.int4, { name: "consecutive_breaches", default: 0 }), consecutiveHealthy: PG.column(PG.int4, { name: "consecutive_healthy", default: 0 }), + /** + * Recent non-skipped ticks, oldest first, one tier-status code each + * (`encodeTick`). Null on rows written before it; seeded from the counters. + */ + historyJson: PG.column(PG.nullable(PG.jsonb(TickHistory)), { name: "history_json" }), lastStatus: PG.column(PG.nullable(PG.text), { name: "last_status" }), lastValue: PG.column(PG.nullable(PG.float8), { name: "last_value" }), lastSampleCount: PG.column(PG.nullable(PG.int4), { name: "last_sample_count" }), @@ -166,7 +179,10 @@ export const AlertIncidents = PG.table("alert_incidents", { ruleName: PG.column(PG.text, { name: "rule_name" }), groupKey: PG.column(PG.nullable(PG.text), { name: "group_key" }), signalType: PG.column(PG.text, { name: "signal_type" }), + /** Current severity; a tiered rule moves it on escalation and de-escalation. */ severity: PG.text, + /** Highest severity reached; null on incidents opened before tiers. */ + peakSeverity: PG.column(PG.nullable(PG.text), { name: "peak_severity" }), status: PG.text, comparator: PG.text, threshold: PG.float8, diff --git a/packages/domain/src/clickhouse/migrations/0041_alert_checks_level.ts b/packages/domain/src/clickhouse/migrations/0041_alert_checks_level.ts new file mode 100644 index 0000000000..bf2b987ac1 --- /dev/null +++ b/packages/domain/src/clickhouse/migrations/0041_alert_checks_level.ts @@ -0,0 +1,10 @@ +/** + * Migration 0041: alert_checks level, the highest severity tier a check breached ('' when none). + * `requiredForIngest: false`: the gateway never writes alert_checks. + */ +export const migration_0041_alert_checks_level = { + version: 41, + description: "Add Level to alert_checks so checks say which severity tier breached", + requiredForIngest: false, + statements: ["ALTER TABLE alert_checks ADD COLUMN IF NOT EXISTS Level LowCardinality(String) DEFAULT ''"], +} as const diff --git a/packages/domain/src/clickhouse/migrations/index.ts b/packages/domain/src/clickhouse/migrations/index.ts index 34cae8f5cd..df9ae2a37c 100644 --- a/packages/domain/src/clickhouse/migrations/index.ts +++ b/packages/domain/src/clickhouse/migrations/index.ts @@ -39,6 +39,7 @@ import { migration_0037_service_overview_spans_span_kind } from "./0037_service_ import { migration_0038_external_edges_legacy_db_system } from "./0038_external_edges_legacy_db_system" import { migration_0039_error_fingerprint_v3 } from "./0039_error_fingerprint_v3" import { migration_0040_error_fingerprint_span_name_frames } from "./0040_error_fingerprint_span_name_frames" +import { migration_0041_alert_checks_level } from "./0041_alert_checks_level" /** * A migration statement is either a raw SQL string (structural DDL) or a @@ -110,6 +111,7 @@ export const migrations: ReadonlyArray = [ migration_0038_external_edges_legacy_db_system, migration_0039_error_fingerprint_v3, migration_0040_error_fingerprint_span_name_frames, + migration_0041_alert_checks_level, ] as const /** Highest migration `version` bundled — i.e. the schema level a fully-applied diff --git a/packages/domain/src/generated/clickhouse-schema.ts b/packages/domain/src/generated/clickhouse-schema.ts index 8ec220060d..e59170613f 100644 --- a/packages/domain/src/generated/clickhouse-schema.ts +++ b/packages/domain/src/generated/clickhouse-schema.ts @@ -1,12 +1,12 @@ // This file is generated by scripts/generate-clickhouse-schema.ts // Do not edit manually. -export const projectRevision = "64b7ae1279269efadefe2cb5773eaf28967c16da7af3d0659c297770532bbc97" as const +export const projectRevision = "9c4759bea56d9544000d0326a35b6224fa56ec76c373ce34bf16869430700c0a" as const export const latestSnapshotStatements: ReadonlyArray = [ "CREATE TABLE IF NOT EXISTS ai_crawler_requests (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n ServiceName LowCardinality(String),\n Crawler LowCardinality(String),\n Host LowCardinality(String),\n Path String,\n HttpStatus UInt16\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, Timestamp, TraceId)\nTTL toDate(Timestamp) + INTERVAL 30 DAY", "CREATE TABLE IF NOT EXISTS ai_trace_index (\n OrgId LowCardinality(String),\n Timestamp DateTime64(9),\n TraceId String,\n SessionId String,\n VendorId LowCardinality(String),\n ServiceName LowCardinality(String),\n DeploymentEnv LowCardinality(String),\n Model LowCardinality(String),\n AgentName LowCardinality(String),\n ToolName LowCardinality(String),\n SpanId String,\n ParentSpanId String,\n Duration UInt64,\n IsError UInt8,\n IsLlmCall UInt8,\n IsToolCall UInt8,\n Tokens Float64,\n Cost Float64,\n ResponseId String,\n VendorVersion LowCardinality(String),\n InputTokens Float64,\n CacheReadTokens Float64,\n CacheWriteTokens Float64,\n OutputTokens Float64,\n ReasoningTokens Float64,\n ErrorType LowCardinality(String),\n StatusMessage String,\n ToolDescription String,\n FailedToolCallResult String,\n ErrorFingerprint UInt64\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, Timestamp, TraceId)\nTTL toDate(Timestamp) + INTERVAL 30 DAY", - "CREATE TABLE IF NOT EXISTS alert_checks (\n OrgId LowCardinality(String),\n RuleId String,\n GroupKey String,\n Timestamp DateTime64(3),\n Status LowCardinality(String),\n SignalType LowCardinality(String),\n Comparator LowCardinality(String),\n Threshold Float64,\n ObservedValue Nullable(Float64),\n SampleCount UInt32,\n WindowMinutes UInt16,\n WindowStart DateTime64(3),\n WindowEnd DateTime64(3),\n ConsecutiveBreaches UInt16,\n ConsecutiveHealthy UInt16,\n IncidentId Nullable(String),\n IncidentTransition LowCardinality(String),\n EvaluationDurationMs UInt32,\n ErrorMessage Nullable(String),\n ErrorCategory LowCardinality(String),\n SkipReason LowCardinality(String) DEFAULT ''\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, RuleId, GroupKey, Timestamp)\nTTL toDate(Timestamp) + INTERVAL 365 DAY", + "CREATE TABLE IF NOT EXISTS alert_checks (\n OrgId LowCardinality(String),\n RuleId String,\n GroupKey String,\n Timestamp DateTime64(3),\n Status LowCardinality(String),\n SignalType LowCardinality(String),\n Comparator LowCardinality(String),\n Threshold Float64,\n ObservedValue Nullable(Float64),\n SampleCount UInt32,\n WindowMinutes UInt16,\n WindowStart DateTime64(3),\n WindowEnd DateTime64(3),\n ConsecutiveBreaches UInt16,\n ConsecutiveHealthy UInt16,\n IncidentId Nullable(String),\n IncidentTransition LowCardinality(String),\n EvaluationDurationMs UInt32,\n ErrorMessage Nullable(String),\n ErrorCategory LowCardinality(String),\n SkipReason LowCardinality(String) DEFAULT '',\n Level LowCardinality(String) DEFAULT ''\n)\nENGINE = MergeTree\nPARTITION BY toDate(Timestamp)\nORDER BY (OrgId, RuleId, GroupKey, Timestamp)\nTTL toDate(Timestamp) + INTERVAL 365 DAY", "CREATE TABLE IF NOT EXISTS attribute_keys_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n AttributeKey LowCardinality(String),\n AttributeScope LowCardinality(String),\n UsageCount SimpleAggregateFunction(sum, UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toDate(Hour)\nORDER BY (OrgId, AttributeScope, Hour, AttributeKey)\nTTL Hour + INTERVAL 90 DAY", "CREATE TABLE IF NOT EXISTS attribute_values_hourly (\n OrgId LowCardinality(String),\n Hour DateTime,\n AttributeKey LowCardinality(String),\n AttributeValue String,\n AttributeScope LowCardinality(String),\n UsageCount SimpleAggregateFunction(sum, UInt64)\n)\nENGINE = AggregatingMergeTree\nPARTITION BY toDate(Hour)\nORDER BY (OrgId, AttributeScope, AttributeKey, Hour, AttributeValue)\nTTL Hour + INTERVAL 90 DAY", "CREATE TABLE IF NOT EXISTS audit_log (\n OrgId LowCardinality(String),\n Id String,\n OccurredAt DateTime64(3),\n RecordedAt DateTime64(3),\n ActorType LowCardinality(String),\n UserId String,\n ApiKeyId String,\n ActorId String,\n ActorLabel String,\n AffectedUserId String,\n Source LowCardinality(String),\n Action LowCardinality(String),\n Outcome LowCardinality(String),\n DenialReason String,\n ResourceType LowCardinality(String),\n ResourceId String,\n ChangedFields Array(String),\n Changes String,\n Metadata String,\n RequestId String,\n OriginIp String,\n OriginCountry LowCardinality(String)\n)\nENGINE = ReplacingMergeTree\nPARTITION BY toYYYYMM(OccurredAt)\nORDER BY (OrgId, OccurredAt, Id)\nTTL toDate(OccurredAt) + INTERVAL 2190 DAY", diff --git a/packages/domain/src/generated/tinybird-project-manifest.ts b/packages/domain/src/generated/tinybird-project-manifest.ts index 55fc1c7273..eeb8843d69 100644 --- a/packages/domain/src/generated/tinybird-project-manifest.ts +++ b/packages/domain/src/generated/tinybird-project-manifest.ts @@ -1,7 +1,7 @@ // This file is generated by scripts/generate-tinybird-project-manifest.ts // Do not edit manually. -export const projectRevision = "64b7ae1279269efadefe2cb5773eaf28967c16da7af3d0659c297770532bbc97" as const +export const projectRevision = "9c4759bea56d9544000d0326a35b6224fa56ec76c373ce34bf16869430700c0a" as const export const datasources = [ { @@ -17,7 +17,7 @@ export const datasources = [ { name: "alert_checks", content: - 'DESCRIPTION >\n One row per alert rule evaluation. Durable audit trail of checks: status, observed value, threshold, sample count, incident linkage.\n\nSCHEMA >\n OrgId LowCardinality(String) `json:$.OrgId`,\n RuleId String `json:$.RuleId`,\n GroupKey String `json:$.GroupKey`,\n Timestamp DateTime64(3) `json:$.Timestamp`,\n Status LowCardinality(String) `json:$.Status`,\n SignalType LowCardinality(String) `json:$.SignalType`,\n Comparator LowCardinality(String) `json:$.Comparator`,\n Threshold Float64 `json:$.Threshold`,\n ObservedValue Nullable(Float64) `json:$.ObservedValue`,\n SampleCount UInt32 `json:$.SampleCount`,\n WindowMinutes UInt16 `json:$.WindowMinutes`,\n WindowStart DateTime64(3) `json:$.WindowStart`,\n WindowEnd DateTime64(3) `json:$.WindowEnd`,\n ConsecutiveBreaches UInt16 `json:$.ConsecutiveBreaches`,\n ConsecutiveHealthy UInt16 `json:$.ConsecutiveHealthy`,\n IncidentId Nullable(String) `json:$.IncidentId`,\n IncidentTransition LowCardinality(String) `json:$.IncidentTransition`,\n EvaluationDurationMs UInt32 `json:$.EvaluationDurationMs`,\n ErrorMessage Nullable(String) `json:$.ErrorMessage`,\n ErrorCategory LowCardinality(String) `json:$.ErrorCategory`,\n SkipReason LowCardinality(String) `json:$.SkipReason` DEFAULT \'\'\n\nENGINE "MergeTree"\nENGINE_PARTITION_KEY "toDate(Timestamp)"\nENGINE_SORTING_KEY "OrgId, RuleId, GroupKey, Timestamp"\nENGINE_TTL "toDate(Timestamp) + INTERVAL 365 DAY"', + 'DESCRIPTION >\n One row per alert rule evaluation. Durable audit trail of checks: status, observed value, threshold, sample count, incident linkage.\n\nSCHEMA >\n OrgId LowCardinality(String) `json:$.OrgId`,\n RuleId String `json:$.RuleId`,\n GroupKey String `json:$.GroupKey`,\n Timestamp DateTime64(3) `json:$.Timestamp`,\n Status LowCardinality(String) `json:$.Status`,\n SignalType LowCardinality(String) `json:$.SignalType`,\n Comparator LowCardinality(String) `json:$.Comparator`,\n Threshold Float64 `json:$.Threshold`,\n ObservedValue Nullable(Float64) `json:$.ObservedValue`,\n SampleCount UInt32 `json:$.SampleCount`,\n WindowMinutes UInt16 `json:$.WindowMinutes`,\n WindowStart DateTime64(3) `json:$.WindowStart`,\n WindowEnd DateTime64(3) `json:$.WindowEnd`,\n ConsecutiveBreaches UInt16 `json:$.ConsecutiveBreaches`,\n ConsecutiveHealthy UInt16 `json:$.ConsecutiveHealthy`,\n IncidentId Nullable(String) `json:$.IncidentId`,\n IncidentTransition LowCardinality(String) `json:$.IncidentTransition`,\n EvaluationDurationMs UInt32 `json:$.EvaluationDurationMs`,\n ErrorMessage Nullable(String) `json:$.ErrorMessage`,\n ErrorCategory LowCardinality(String) `json:$.ErrorCategory`,\n SkipReason LowCardinality(String) `json:$.SkipReason` DEFAULT \'\',\n Level LowCardinality(String) `json:$.Level` DEFAULT \'\'\n\nENGINE "MergeTree"\nENGINE_PARTITION_KEY "toDate(Timestamp)"\nENGINE_SORTING_KEY "OrgId, RuleId, GroupKey, Timestamp"\nENGINE_TTL "toDate(Timestamp) + INTERVAL 365 DAY"', }, { name: "attribute_keys_hourly", diff --git a/packages/domain/src/http/alerts.ts b/packages/domain/src/http/alerts.ts index 7ca4456774..aa0ded7f47 100644 --- a/packages/domain/src/http/alerts.ts +++ b/packages/domain/src/http/alerts.ts @@ -1,4 +1,4 @@ -import { Schema } from "effect" +import { Array as Arr, Order, Schema } from "effect" import { QueryEngineAlertReducer, QueryEngineNoDataBehavior } from "../query-engine" import { AlertDeliveryEventId, @@ -185,6 +185,80 @@ export const AlertWindowMinutes = PositiveInt.pipe( Schema.check(Schema.isLessThanOrEqualTo(MAX_ALERT_WINDOW_MINUTES)), ) +/** Longest "N of the last M checks" window a rule can count over. */ +export const MAX_ALERT_BREACH_WINDOW = 30 +export const AlertBreachWindow = PositiveInt.pipe( + Schema.check(Schema.isLessThanOrEqualTo(MAX_ALERT_BREACH_WINDOW)), +) + +/** + * One severity tier: the value that opens or escalates to it, an optional recovery value to cross + * back before it clears, and its destinations. A critical incident also notifies lower tiers'. + */ +export const AlertThreshold = Schema.Struct({ + severity: AlertSeverity, + value: Schema.Number, + /** Upper bound for `between` / `not_between`. */ + upper: Schema.NullOr(Schema.Number), + /** Null recovers as soon as the trigger stops holding. */ + recoveryValue: Schema.NullOr(Schema.Number), + destinationIds: Schema.Array(AlertDestinationId), +}).annotate({ identifier: "@maple/AlertThreshold", title: "Alert Threshold" }) +export type AlertThreshold = Schema.Schema.Type + +export const AlertThresholds = Schema.NonEmptyArray(AlertThreshold) + .check(Schema.isMaxLength(2)) + .annotate({ identifier: "@maple/AlertThresholds", title: "Alert Thresholds" }) +export type AlertThresholds = Schema.Schema.Type + +export const ALERT_SEVERITY_RANK = { warning: 1, critical: 2 } as const satisfies Record< + AlertSeverity, + number +> + +/** Lowest severity first. */ +export const alertSeverityOrder: Order.Order<{ readonly severity: AlertSeverity }> = Order.mapInput( + Order.Number, + (tier) => ALERT_SEVERITY_RANK[tier.severity], +) + +/** Tiers lowest severity first, each tier's destinations deduped. */ +export const sortAlertThresholds = (thresholds: AlertThresholds): AlertThresholds => + Arr.map(Arr.sort(thresholds, alertSeverityOrder), (tier) => ({ + ...tier, + destinationIds: Arr.dedupe(tier.destinationIds), + })) + +/** The single tier a rule written before tiers stands for. */ +export const legacyAlertThreshold = (fields: { + readonly severity: AlertSeverity + readonly threshold: number + readonly thresholdUpper: number | null + readonly destinationIds: ReadonlyArray +}): AlertThreshold => ({ + severity: fields.severity, + value: fields.threshold, + upper: fields.thresholdUpper, + recoveryValue: null, + destinationIds: fields.destinationIds, +}) + +/** + * The single-threshold fields pre-tier readers still use: the highest tier's + * severity, the lowest tier's value, and every tier's destinations. A one-tier + * rule maps onto exactly the fields it was written with. + */ +export const alertThresholdLegacyFields = (thresholds: AlertThresholds) => { + const sorted = sortAlertThresholds(thresholds) + const primary = Arr.headNonEmpty(sorted) + return { + severity: Arr.lastNonEmpty(sorted).severity, + threshold: primary.value, + thresholdUpper: primary.upper, + destinationIds: Arr.dedupe(sorted.flatMap((tier) => tier.destinationIds)), + } +} + export class PagerDutyAlertDestinationConfig extends Schema.Class( "PagerDutyAlertDestinationConfig", )({ @@ -476,7 +550,7 @@ export const ALERT_TEMPLATE_VARIABLES: ReadonlyArray<{ { key: "event.type", description: "trigger | resolve | renotify | test" }, { key: "event.label", description: 'Human label, e.g. "Triggered"' }, { key: "event.emoji", description: "Event emoji" }, - { key: "severity", description: "warning | critical" }, + { key: "severity", description: "warning | critical (the incident's current severity)" }, { key: "signal", description: "Raw signal type" }, { key: "signal.label", description: 'Human signal label, e.g. "Error Rate"' }, { key: "comparator.label", description: 'Comparison operator, e.g. ">"' }, @@ -500,7 +574,10 @@ export class AlertRuleDocument extends Schema.Class("AlertRul notes: Schema.NullOr(Schema.String), notificationTemplate: Schema.NullOr(AlertNotificationTemplate), enabled: Schema.Boolean, + /** The highest tier's severity. Derived from `thresholds`; kept for single-threshold readers. */ severity: AlertSeverity, + /** Severity tiers, lowest first. */ + thresholds: AlertThresholds, serviceNames: Schema.Array(Schema.String), excludeServiceNames: Schema.Array(Schema.String), /** @@ -514,17 +591,22 @@ export class AlertRuleDocument extends Schema.Class("AlertRul groupBy: Schema.NullOr(AlertGroupBy), signalType: AlertSignalType, comparator: AlertComparator, + /** The lowest tier's value. Derived from `thresholds`. */ threshold: Schema.Number, thresholdUpper: Schema.NullOr(Schema.Number), windowMinutes: AlertWindowMinutes, minimumSampleCount: NonNegativeInt, + /** N: breaching checks needed to open (or escalate). */ consecutiveBreachesRequired: PositiveInt, + /** M: the N are counted over the last M checks. Null means N in a row. */ + breachesOutOf: Schema.NullOr(AlertBreachWindow), consecutiveHealthyRequired: PositiveInt, renotifyIntervalMinutes: PositiveInt, apdexThresholdMs: Schema.NullOr(PositiveFloat), queryBuilderDraft: Schema.NullOr(QueryBuilderQueryDraftSchema), rawQuerySql: Schema.NullOr(Schema.String), rawQueryReducer: Schema.NullOr(QueryEngineAlertReducer), + /** Every tier's destinations. Derived from `thresholds`. */ destinationIds: Schema.Array(AlertDestinationId), /** What the evaluator does when the window has no data: skip the check or treat it as zero. */ noDataBehavior: QueryEngineNoDataBehavior, @@ -548,7 +630,12 @@ export class AlertRuleUpsertRequest extends Schema.Class notes: Schema.optionalKey(Schema.NullOr(Schema.String)), notificationTemplate: Schema.optionalKey(Schema.NullOr(AlertNotificationTemplate)), enabled: Schema.optionalKey(Schema.Boolean), - severity: AlertSeverity, + /** + * Severity tiers. When set, `severity`, `threshold`, `thresholdUpper` and + * `destinationIds` are ignored; when absent, those four make one tier. + */ + thresholds: Schema.optionalKey(AlertThresholds), + severity: Schema.optionalKey(AlertSeverity), serviceNames: Schema.optionalKey(Schema.Array(Schema.String)), excludeServiceNames: Schema.optionalKey(Schema.Array(Schema.String)), environments: Schema.optionalKey(Schema.Array(Schema.String)), @@ -556,11 +643,12 @@ export class AlertRuleUpsertRequest extends Schema.Class groupBy: Schema.optionalKey(Schema.NullOr(AlertGroupBy)), signalType: AlertSignalType, comparator: AlertComparator, - threshold: Schema.Number, + threshold: Schema.optionalKey(Schema.Number), thresholdUpper: Schema.optionalKey(Schema.NullOr(Schema.Number)), windowMinutes: AlertWindowMinutes, minimumSampleCount: Schema.optionalKey(NonNegativeInt), consecutiveBreachesRequired: Schema.optionalKey(PositiveInt), + breachesOutOf: Schema.optionalKey(Schema.NullOr(AlertBreachWindow)), consecutiveHealthyRequired: Schema.optionalKey(PositiveInt), renotifyIntervalMinutes: Schema.optionalKey(PositiveInt), apdexThresholdMs: Schema.optionalKey(Schema.NullOr(PositiveFloat)), @@ -569,7 +657,7 @@ export class AlertRuleUpsertRequest extends Schema.Class rawQueryReducer: Schema.optionalKey(Schema.NullOr(QueryEngineAlertReducer)), /** Count a window with no data as a breach instead of skipping it. Default false. */ alertOnNoData: Schema.optionalKey(Schema.Boolean), - destinationIds: Schema.Array(AlertDestinationId), + destinationIds: Schema.optionalKey(Schema.Array(AlertDestinationId)), }) {} export class AlertRulesListResponse extends Schema.Class("AlertRulesListResponse")({ @@ -619,6 +707,8 @@ export class AlertRulePreviewPoint extends Schema.Class(" value: Schema.NullOr(Schema.Number), sampleCount: Schema.Number, status: AlertEvaluationStatus, + /** The highest tier this window breached; absent when none did or it skipped. */ + level: Schema.optionalKey(AlertSeverity), /** Set on `status: "skipped"` points. */ skipReason: Schema.optionalKey(AlertSkipReason), /** @@ -640,6 +730,8 @@ export class AlertRulePreviewFiringSpan extends Schema.Class(" ruleName: Schema.String, groupKey: Schema.NullOr(Schema.String), signalType: AlertSignalType, + /** Current severity: moves when a tiered rule escalates or de-escalates. */ severity: AlertSeverity, + /** Highest severity reached; null on incidents recorded before tiers. */ + peakSeverity: Schema.NullOr(AlertSeverity), status: AlertIncidentStatus, comparator: AlertComparator, threshold: Schema.Number, @@ -944,6 +1040,7 @@ export class AlertRuleStoredConfigInvalidError extends HttpTaggedError("AlertC consecutiveHealthy: Schema.Number, incidentId: Schema.NullOr(AlertIncidentId), incidentTransition: AlertIncidentTransition, + /** Highest tier this check breached; null when none did or on rows before tiers. */ + level: Schema.NullOr(AlertSeverity), evaluationDurationMs: Schema.Number, /** Populated on `status: "error"` rows — why the evaluation failed. */ errorMessage: Schema.NullOr(Schema.String), diff --git a/packages/domain/src/http/v2/alert-incidents.ts b/packages/domain/src/http/v2/alert-incidents.ts index d0916f038f..cb6c880c35 100644 --- a/packages/domain/src/http/v2/alert-incidents.ts +++ b/packages/domain/src/http/v2/alert-incidents.ts @@ -23,6 +23,7 @@ const alertIncidentExample = { group_key: null, signal_type: "error_rate", severity: "critical", + peak_severity: "critical", status: "open", comparator: "gt", threshold: 0.05, @@ -63,7 +64,12 @@ export const V2AlertIncident = Schema.Struct({ examples: ["error_rate"], }), severity: AlertSeverity.annotate({ - description: "Severity inherited from the rule: `warning` or `critical`.", + description: + "Current severity: `warning` or `critical`. Moves when a rule with severity tiers escalates or de-escalates.", + examples: ["critical"], + }), + peak_severity: Schema.NullOr(AlertSeverity).annotate({ + description: "Highest severity the incident reached, or `null` on incidents opened before tiers.", examples: ["critical"], }), status: AlertIncidentStatus.annotate({ diff --git a/packages/domain/src/http/v2/alert-rules.ts b/packages/domain/src/http/v2/alert-rules.ts index d3f0e2d915..a9e95919ad 100644 --- a/packages/domain/src/http/v2/alert-rules.ts +++ b/packages/domain/src/http/v2/alert-rules.ts @@ -1,5 +1,5 @@ import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api" -import { Schema } from "effect" +import { Array as Arr, Effect, Schema } from "effect" import { QueryEngineAlertReducer, QueryEngineNoDataBehavior } from "../../query-engine" import { PostgresTransactionId, UserId } from "../../primitives" import { @@ -24,6 +24,8 @@ import { AlertSignalType, AlertValidationError, AlertWindowMinutes, + AlertBreachWindow, + type AlertThreshold, } from "../alerts" import { AlertDestinationPublicId } from "./alert-destinations" import { AuthorizationV2 } from "./auth" @@ -42,6 +44,60 @@ const PositiveFloat = Schema.Number.pipe(Schema.check(Schema.isFinite(), Schema. const GroupBy = Schema.Array(NonEmptyString).pipe(Schema.check(Schema.isMinLength(1))) +export const V2AlertThreshold = Schema.Struct({ + severity: AlertSeverity.annotate({ description: "The severity this tier opens or escalates to." }), + value: Schema.Number.annotate({ + description: "The value that breaches this tier, compared with the rule's `comparator`.", + examples: [0.05], + }), + // Both may be omitted on input and read as `null`; responses always carry them. + upper: Schema.NullOr(Schema.Number) + .annotate({ + description: + "Upper bound for range comparators (`between` / `not_between`), otherwise `null`. Defaults to `null`.", + }) + .pipe(Schema.withDecodingDefaultKey(Effect.succeed(null))), + recovery_value: Schema.NullOr(Schema.Number) + .annotate({ + description: + "The value the signal must cross back over before this tier clears, or `null` (the default) to clear as soon as it stops breaching. Must sit on the healthy side of `value`.", + examples: [0.03], + }) + .pipe(Schema.withDecodingDefaultKey(Effect.succeed(null))), + destination_ids: Schema.Array(AlertDestinationPublicId).annotate({ + description: + "Destinations (`dest_…`) this severity notifies. A `critical` incident also notifies every `warning` destination.", + }), +}).annotate({ + identifier: "AlertThreshold", + title: "Alert threshold", + description: "One severity tier of an alert rule.", +}) +export type V2AlertThreshold = Schema.Schema.Type + +// A bounded array on the wire, not a tuple: generated clients (the iOS app's) do not +// all read `prefixItems`. The refine narrows the decoded type to non-empty. +// The refine goes first: annotations land on the last check, and a guard has no JSON Schema. +const V2AlertThresholds = Schema.Array(V2AlertThreshold) + .pipe(Schema.refine(Arr.isReadonlyArrayNonEmpty)) + .check(Schema.isMinLength(1), Schema.isMaxLength(2)) + +export const fromV2Threshold = (tier: V2AlertThreshold): AlertThreshold => ({ + severity: tier.severity, + value: tier.value, + upper: tier.upper, + recoveryValue: tier.recovery_value, + destinationIds: tier.destination_ids, +}) + +export const toV2Threshold = (tier: AlertThreshold): V2AlertThreshold => ({ + severity: tier.severity, + value: tier.value, + upper: tier.upper, + recovery_value: tier.recoveryValue, + destination_ids: tier.destinationIds, +}) + /** Mirrors the v1 caps (alerts.ts): 32-char tags, at most 20 per rule. */ const RuleTags = Schema.Array(Schema.String.check(Schema.isMaxLength(32))).check(Schema.isMaxLength(20)) @@ -75,9 +131,19 @@ const alertRuleExample = { comparator: "gt", threshold: 0.05, threshold_upper: null, + thresholds: [ + { + severity: "critical", + value: 0.05, + upper: null, + recovery_value: null, + destination_ids: ["dest_oybbpTBhtSFGShMjjLiCrh"], + }, + ], window_minutes: 5, minimum_sample_count: 50, consecutive_breaches_required: 2, + breaches_out_of: null, consecutive_healthy_required: 3, renotify_interval_minutes: 60, apdex_threshold_ms: null, @@ -120,9 +186,14 @@ export const V2AlertRule = Schema.Struct({ examples: [true], }), severity: AlertSeverity.annotate({ - description: "Severity attached to incidents this rule opens: `warning` or `critical`.", + description: + "The highest severity this rule can open an incident at: `warning` or `critical`. Derived from `thresholds`.", examples: ["critical"], }), + thresholds: V2AlertThresholds.annotate({ + description: + "Severity tiers, lowest first. An incident opens at the tier the value breaches and moves between tiers as it changes.", + }), service_names: Schema.Array(Schema.String).annotate({ description: "Services the rule is scoped to. Empty for all services.", examples: [["checkout"]], @@ -154,11 +225,11 @@ export const V2AlertRule = Schema.Struct({ examples: ["gt"], }), threshold: Schema.Number.annotate({ - description: "The threshold the observed value is compared against. Error rates are 0–1 ratios.", + description: "The lowest tier's threshold. Error rates are 0–1 ratios. Derived from `thresholds`.", examples: [0.05], }), threshold_upper: Schema.NullOr(Schema.Number).annotate({ - description: "Upper bound for range comparators (`between` / `not_between`), otherwise `null`.", + description: "The lowest tier's upper bound for range comparators, otherwise `null`.", }), window_minutes: AlertWindowMinutes.annotate({ description: "Length of the evaluation window in minutes.", @@ -170,9 +241,15 @@ export const V2AlertRule = Schema.Struct({ examples: [50], }), consecutive_breaches_required: PositiveInt.annotate({ - description: "Consecutive breached checks required before an incident opens.", + description: + "Breached checks required before an incident opens or escalates (N). In a row unless `breaches_out_of` is set.", examples: [2], }), + breaches_out_of: Schema.NullOr(AlertBreachWindow).annotate({ + description: + 'Count the required breaches over the last M checks instead of in a row ("N of M"), or `null`.', + examples: [null], + }), consecutive_healthy_required: PositiveInt.annotate({ description: "Consecutive healthy checks required before an open incident resolves.", examples: [3], @@ -193,7 +270,7 @@ export const V2AlertRule = Schema.Struct({ "For `raw_query` rules: how multi-row results reduce to one value (`identity`, `sum`, `avg`, `min`, `max`).", }), destination_ids: Schema.Array(AlertDestinationPublicId).annotate({ - description: "The alert destinations (`dest_…`) this rule notifies.", + description: "Every destination (`dest_…`) any tier notifies. Derived from `thresholds`.", }), no_data_behavior: QueryEngineNoDataBehavior.annotate({ description: @@ -278,10 +355,18 @@ const createParamsFields = { enabled: Schema.optionalKey( Schema.Boolean.annotate({ description: "Whether the rule starts enabled. Defaults to `true`." }), ), - severity: AlertSeverity.annotate({ - description: "Severity attached to incidents this rule opens.", - examples: ["critical"], - }), + thresholds: Schema.optionalKey( + V2AlertThresholds.annotate({ + description: + "Severity tiers (at most one `warning` and one `critical`). When set, `severity`, `threshold`, `threshold_upper` and `destination_ids` are ignored.", + }), + ), + severity: Schema.optionalKey( + AlertSeverity.annotate({ + description: "Severity of a single-threshold rule. Required unless `thresholds` is set.", + examples: ["critical"], + }), + ), service_names: Schema.optionalKey(Schema.Array(Schema.String)), exclude_service_names: Schema.optionalKey(Schema.Array(Schema.String)), environments: Schema.optionalKey( @@ -302,10 +387,13 @@ const createParamsFields = { "Comparison operator. Range forms (`between` / `not_between`) also require `threshold_upper`.", examples: ["gt"], }), - threshold: Schema.Number.annotate({ - description: "The threshold to compare against. Error rates are 0–1 ratios.", - examples: [0.05], - }), + threshold: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Threshold of a single-threshold rule. Error rates are 0–1 ratios. Required unless `thresholds` is set.", + examples: [0.05], + }), + ), threshold_upper: Schema.optionalKey(Schema.NullOr(Schema.Number)), window_minutes: PositiveInt.annotate({ description: "Length of the evaluation window in minutes.", @@ -313,6 +401,7 @@ const createParamsFields = { }), minimum_sample_count: Schema.optionalKey(NonNegativeInt), consecutive_breaches_required: Schema.optionalKey(PositiveInt), + breaches_out_of: Schema.optionalKey(Schema.NullOr(AlertBreachWindow)), consecutive_healthy_required: Schema.optionalKey(PositiveInt), renotify_interval_minutes: Schema.optionalKey(PositiveInt), apdex_threshold_ms: Schema.optionalKey(Schema.NullOr(PositiveFloat)), @@ -325,9 +414,11 @@ const createParamsFields = { "Count a window with no data (e.g. a raw query returning no rows) as a breach, so a rule that goes blind opens an incident. Default `false`: such windows are skipped. Not supported on grouped rules, where a group that stops reporting keeps its incident open until telemetry returns.", }), ), - destination_ids: Schema.Array(AlertDestinationPublicId).annotate({ - description: "The alert destinations (`dest_…`) to notify. May be empty.", - }), + destination_ids: Schema.optionalKey( + Schema.Array(AlertDestinationPublicId).annotate({ + description: "Destinations (`dest_…`) of a single-threshold rule.", + }), + ), } export const V2AlertRuleCreateParams = Schema.Struct(createParamsFields).annotate({ @@ -353,12 +444,14 @@ export type V2AlertRuleCreateParams = Schema.Schema.Type { comparator: "gt", threshold: 0.05, threshold_upper: null, + thresholds: [ + { + severity: "critical", + value: 0.05, + upper: null, + recovery_value: null, + destination_ids: [encodePublicId("dest", DEST_UUID)], + }, + ], window_minutes: 5, minimum_sample_count: 50, consecutive_breaches_required: 2, + breaches_out_of: null, consecutive_healthy_required: 3, renotify_interval_minutes: 60, apdex_threshold_ms: null, @@ -389,6 +399,7 @@ describe("V2 alerts wire format", () => { const wire = Schema.encodeSync(V2AlertRule)(rule) expect(wire.id).toBe(encodePublicId("alrt", UUID)) expect(wire.destination_ids[0]).toMatch(/^dest_/) + expect(wire.thresholds[0]?.destination_ids[0]).toMatch(/^dest_/) expect("signalType" in wire).toBe(false) // The query-builder draft is opaque: its camelCase keys survive untouched. expect(wire.query_builder_draft).toEqual({ @@ -443,6 +454,7 @@ describe("V2 alerts wire format", () => { group_key: null, signal_type: "error_rate", severity: "critical", + peak_severity: "critical", status: "open", comparator: "gt", threshold: 0.05, diff --git a/packages/domain/src/tinybird/datasources.ts b/packages/domain/src/tinybird/datasources.ts index 80e298bf18..0a93fd1e37 100644 --- a/packages/domain/src/tinybird/datasources.ts +++ b/packages/domain/src/tinybird/datasources.ts @@ -1706,6 +1706,8 @@ export const alertChecks = defineDatasource("alert_checks", { // Why a skipped check skipped (no_data, below_min_samples, no_value); '' otherwise. // Last, where migration 0036's ADD COLUMN puts it. SkipReason: t.string().lowCardinality().default(""), + // Highest severity tier the check breached (warning, critical); '' otherwise. + Level: t.string().lowCardinality().default(""), }, engine: engine.mergeTree({ partitionKey: "toDate(Timestamp)", diff --git a/packages/query-engine/src/ch/queries/alert-checks.test.ts b/packages/query-engine/src/ch/queries/alert-checks.test.ts index 7c87a38d07..d5ff01748f 100644 --- a/packages/query-engine/src/ch/queries/alert-checks.test.ts +++ b/packages/query-engine/src/ch/queries/alert-checks.test.ts @@ -147,6 +147,7 @@ describe("alert check rows decode timestamps to DateTime.Utc", () => { errorMessage: null, errorCategory: "", skipReason: "", + level: "", }, ]) expect(DateTime.formatIso(row!.timestamp)).toBe("2024-01-01T10:00:00.250Z") diff --git a/packages/query-engine/src/ch/queries/alert-checks.ts b/packages/query-engine/src/ch/queries/alert-checks.ts index 9789ed06b8..ed771a36c5 100644 --- a/packages/query-engine/src/ch/queries/alert-checks.ts +++ b/packages/query-engine/src/ch/queries/alert-checks.ts @@ -40,6 +40,7 @@ export interface ListRuleChecksOutput { readonly errorMessage: string | null readonly errorCategory: string readonly skipReason: string + readonly level: string } export function listRuleChecksQuery(opts: ListRuleChecksOpts) { @@ -65,6 +66,7 @@ export function listRuleChecksQuery(opts: ListRuleChecksOpts) { errorMessage: $.ErrorMessage, errorCategory: $.ErrorCategory, skipReason: $.SkipReason, + level: $.Level, })) .where(($) => [ $.OrgId.eq(orgIdParam), diff --git a/scripts/tinybird-pr-branch.ts b/scripts/tinybird-pr-branch.ts index 19e40de0aa..1a0b40f47f 100644 --- a/scripts/tinybird-pr-branch.ts +++ b/scripts/tinybird-pr-branch.ts @@ -141,6 +141,32 @@ const resolveBranch = async ( return { id: id as string, token: token as string } } +/** + * The branch's "workspace admin token". Tinybird verifies self-signed JWTs only against + * that token; the environments list hands out a per-user admin token, which it rejects. + */ +const resolveSigningKey = async ( + branch: { host: string; token: string }, + branchName: string, +): Promise => { + const response = await fetch(`${branch.host}/v0/tokens`, { + headers: { Authorization: `Bearer ${branch.token}` }, + }) + if (!response.ok) + fail(`Could not list Tinybird tokens for branch ${branchName} (HTTP ${response.status}).`) + const parsed = (await response.json()) as { + tokens?: { name?: string; token?: string; scopes?: { type?: string }[] }[] + } + const admin = parsed.tokens?.find( + (candidate) => + candidate.name === "workspace admin token" && + candidate.scopes?.some((scope) => scope.type === "ADMIN"), + ) + const token = admin?.token?.trim() + if (!token) fail(`Branch ${branchName} has no workspace admin token to sign raw-SQL JWTs with.`) + return token as string +} + const exportToGithubEnv = (vars: Record): void => { const githubEnv = process.env.GITHUB_ENV?.trim() const lines = Object.entries(vars).map(([key, value]) => `${key}=${value}`) @@ -182,17 +208,19 @@ const up = async (branchName: string): Promise => { fail(`Failed to deploy project schema to Tinybird branch ${branchName}.`) } - // Mask the token in CI logs before it can appear anywhere downstream. + // Mask the tokens in CI logs before they can appear anywhere downstream. console.log(`::add-mask::${branch.token}`) + const signingKey = await resolveSigningKey(branch, branchName) + console.log(`::add-mask::${signingKey}`) // 4. Hand the branch creds to the rest of the workflow. The branch is reached on // the same regional host as its parent — the branch-scoped token does the // routing, so only the token changes. Raw-SQL JWTs are signed with the - // branch's admin token and name the branch, or they would query the parent. + // branch's workspace admin token and name the branch, or they would query the parent. exportToGithubEnv({ TINYBIRD_HOST: branch.host, TINYBIRD_TOKEN: branch.token, - TINYBIRD_SIGNING_KEY: branch.token, + TINYBIRD_SIGNING_KEY: signingKey, TINYBIRD_WORKSPACE_ID: branchId, }) console.log(`✓ Tinybird branch ${branchName} ready; preview stack will bind to it.`)