@autter/otlp-ingester 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +51 -0
  2. package/dist/breadcrumbs.d.ts +41 -0
  3. package/dist/breadcrumbs.js +151 -0
  4. package/dist/clickhouse.d.ts +42 -5
  5. package/dist/clickhouse.js +168 -7
  6. package/dist/compat-manifest.json +123 -0
  7. package/dist/compat.d.ts +149 -0
  8. package/dist/compat.js +221 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.js +19 -0
  11. package/dist/context.js +24 -11
  12. package/dist/error-fields.d.ts +41 -0
  13. package/dist/error-fields.js +103 -0
  14. package/dist/fingerprint-override.d.ts +42 -0
  15. package/dist/fingerprint-override.js +76 -0
  16. package/dist/fingerprint-v3.d.ts +54 -0
  17. package/dist/fingerprint-v3.js +96 -0
  18. package/dist/fingerprint.d.ts +54 -0
  19. package/dist/fingerprint.js +46 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +5 -3
  22. package/dist/llm.js +16 -3
  23. package/dist/log-promotion.d.ts +55 -0
  24. package/dist/log-promotion.js +119 -0
  25. package/dist/logs.d.ts +57 -1
  26. package/dist/logs.js +148 -11
  27. package/dist/memory.d.ts +2 -2
  28. package/dist/migrations.d.ts +13 -0
  29. package/dist/migrations.js +75 -2
  30. package/dist/normalize-browser.d.ts +67 -22
  31. package/dist/normalize-browser.js +46 -19
  32. package/dist/normalize-otlp.js +18 -2
  33. package/dist/redact.d.ts +33 -0
  34. package/dist/redact.js +96 -0
  35. package/dist/sdk-versions.d.ts +58 -0
  36. package/dist/sdk-versions.js +167 -0
  37. package/dist/server.d.ts +3 -0
  38. package/dist/server.js +219 -15
  39. package/dist/sink.d.ts +13 -0
  40. package/dist/sink.js +37 -4
  41. package/dist/source-maps-lookup.d.ts +31 -0
  42. package/dist/source-maps-lookup.js +64 -0
  43. package/dist/source-maps.d.ts +63 -2
  44. package/dist/source-maps.js +121 -13
  45. package/dist/stack-fixtures.d.ts +10 -2
  46. package/dist/stack-fixtures.js +13 -3
  47. package/dist/stack-frames.d.ts +48 -0
  48. package/dist/stack-frames.js +441 -0
  49. package/dist/symbolicate.d.ts +79 -0
  50. package/dist/symbolicate.js +257 -0
  51. package/dist/types.d.ts +86 -0
  52. package/package.json +2 -1
@@ -0,0 +1,119 @@
1
+ import { normalizeRoute } from "./fingerprint.js";
2
+ /**
3
+ * Log promotion — errors from SDKs running WITHOUT a trace pipeline.
4
+ *
5
+ * Logger-only mode (`initAutterLogging`) and `@autter/runtime-edge` have no
6
+ * span to record an exception on, so they emit an error log record with
7
+ * `exception.*` and `autter.capture.mode = "log"`. /v1/logs turns those
8
+ * records into ordinary server occurrences (fingerprinted, stored in
9
+ * runtime_error_occurrences, forwarded to the sink) — otherwise their
10
+ * errors would never reach issue grouping.
11
+ *
12
+ * Dedupe, so one failure never becomes two occurrences through two export
13
+ * paths:
14
+ * - in-batch: at most one promoted occurrence per trace id (first wins);
15
+ * - cross-batch: a trace id that already produced an occurrence for this
16
+ * tenant within ±PROMOTION_DEDUPE_WINDOW_MS (ClickHouse lookup) is skipped.
17
+ * Records without a trace id are always promoted. The lookup is
18
+ * best-effort and bounded (see promotionLookups, plus a per-tenant lookup
19
+ * budget in server.ts): when it fails, is skipped or is over budget the
20
+ * record is promoted anyway — a rare duplicate beats a silently dropped
21
+ * error.
22
+ */
23
+ export const PROMOTION_DEDUPE_WINDOW_MS = 60_000;
24
+ /** Widest single lookup window. Candidate timestamps come from the client,
25
+ * so one batch could otherwise ask ClickHouse to scan an arbitrary range. */
26
+ export const MAX_LOOKUP_SPAN_MS = 10 * 60_000;
27
+ /** At most this many lookup windows (queries) per request. */
28
+ export const MAX_LOOKUP_WINDOWS = 4;
29
+ /** Candidates outside [now − 24 h, now + 5 min] are never looked up: their
30
+ * trace cannot plausibly have a recent occurrence to dedupe against. */
31
+ export const LOOKUP_PAST_MS = 24 * 60 * 60_000;
32
+ export const LOOKUP_FUTURE_MS = 5 * 60_000;
33
+ /** Promotion candidates in record order, deduped by trace id in-batch. */
34
+ export function logPromotionCandidates(rows) {
35
+ const traces = new Set();
36
+ const out = [];
37
+ for (const row of rows) {
38
+ const occurrence = row.occurrence;
39
+ if (!occurrence)
40
+ continue;
41
+ if (occurrence.traceId) {
42
+ if (traces.has(occurrence.traceId))
43
+ continue;
44
+ traces.add(occurrence.traceId);
45
+ }
46
+ out.push(occurrence);
47
+ }
48
+ return out;
49
+ }
50
+ /**
51
+ * Bounded dedupe lookups for the candidates that carry a trace id: each
52
+ * window is [oldest − 60 s, newest + 60 s] over candidates taken newest first
53
+ * and spans at most MAX_LOOKUP_SPAN_MS; at most MAX_LOOKUP_WINDOWS windows are
54
+ * planned.
55
+ * Candidates outside the plausible time range or beyond the window cap are
56
+ * not looked up — they are still deduped in-batch and promoted.
57
+ */
58
+ export function promotionLookups(occurrences, now = Date.now()) {
59
+ const eligible = occurrences
60
+ .filter((o) => !!o.traceId)
61
+ .map((o) => ({ traceId: o.traceId, at: o.occurredAt.getTime() }))
62
+ .filter((o) => o.at >= now - LOOKUP_PAST_MS && o.at <= now + LOOKUP_FUTURE_MS)
63
+ // Newest first: recent records are the likeliest duplicates of an
64
+ // occurrence the trace path just wrote, so they get the windows.
65
+ .sort((a, b) => b.at - a.at);
66
+ const lookups = [];
67
+ let start = 0;
68
+ while (start < eligible.length && lookups.length < MAX_LOOKUP_WINDOWS) {
69
+ const newest = eligible[start].at;
70
+ let end = start;
71
+ while (end + 1 < eligible.length &&
72
+ newest - eligible[end + 1].at + 2 * PROMOTION_DEDUPE_WINDOW_MS <= MAX_LOOKUP_SPAN_MS)
73
+ end++;
74
+ lookups.push({
75
+ from: new Date(eligible[end].at - PROMOTION_DEDUPE_WINDOW_MS),
76
+ to: new Date(newest + PROMOTION_DEDUPE_WINDOW_MS),
77
+ traceIds: [...new Set(eligible.slice(start, end + 1).map((o) => o.traceId))],
78
+ });
79
+ start = end + 1;
80
+ }
81
+ return lookups;
82
+ }
83
+ /**
84
+ * One event / one error event per promoted occurrence — the same counting
85
+ * the trace path applies to occurrences outside a server span, so issue
86
+ * counters for logger-only services don't read 0.
87
+ */
88
+ export function promotionRollups(occurrences) {
89
+ const rollups = new Map();
90
+ for (const occ of occurrences) {
91
+ const bucketAt = new Date(Math.floor(occ.occurredAt.getTime() / 60_000) * 60_000);
92
+ const route = normalizeRoute(occ.route);
93
+ const key = [
94
+ occ.service,
95
+ occ.environment,
96
+ occ.release ?? "",
97
+ route,
98
+ bucketAt.getTime(),
99
+ ].join(" ");
100
+ const existing = rollups.get(key);
101
+ if (existing) {
102
+ existing.requestCount += 1;
103
+ existing.errorCount += 1;
104
+ continue;
105
+ }
106
+ rollups.set(key, {
107
+ service: occ.service,
108
+ environment: occ.environment,
109
+ release: occ.release,
110
+ route,
111
+ bucketAt,
112
+ requestCount: 1,
113
+ errorCount: 1,
114
+ durationSumMs: 0,
115
+ sessionCount: 0,
116
+ });
117
+ }
118
+ return [...rollups.values()];
119
+ }
package/dist/logs.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type OtlpAttribute, type OtlpValue } from "./context.js";
2
+ import type { RuntimeOccurrenceInput } from "./types.js";
2
3
  export interface RuntimeLogRecord {
3
4
  id: string;
4
5
  service: string;
@@ -15,6 +16,28 @@ export interface RuntimeLogRecord {
15
16
  durationMs: number;
16
17
  attributes: Record<string, unknown>;
17
18
  occurredAt: Date;
19
+ /**
20
+ * Lifted wide-event columns (migration 0012). `kind` is "request" for
21
+ * request summaries, "operation" for operation summaries (including
22
+ * pre-1.5.0 SDKs that never sent `autter.operation.kind`), "" for plain
23
+ * log records. The rest default to ""/0 when the record doesn't carry them.
24
+ */
25
+ kind: "" | "request" | "operation";
26
+ requestId: string;
27
+ /** `http.route`, query-stripped and id-normalised. */
28
+ route: string;
29
+ statusCode: number;
30
+ /** `autter.error.code`, only when it matches CODE_PATTERN. */
31
+ errorCode: string;
32
+ aiCostUsd: number;
33
+ aiCalls: number;
34
+ /**
35
+ * Not persisted on runtime_logs. Set for logger-only/edge error records
36
+ * (`autter.capture.mode = "log"`, severity ≥ error, `exception.*`
37
+ * present): the occurrence /v1/logs promotes into issue grouping —
38
+ * see log-promotion.ts.
39
+ */
40
+ occurrence?: RuntimeOccurrenceInput;
18
41
  }
19
42
  export interface OtlpLogsRequest {
20
43
  resourceLogs?: Array<{
@@ -39,4 +62,37 @@ export interface OtlpLogsRequest {
39
62
  }>;
40
63
  }
41
64
  export declare function normalizeLogs(request: OtlpLogsRequest): RuntimeLogRecord[];
42
- export declare function logTableDDL(db: string): string;
65
+ /**
66
+ * Baseline for fresh databases. The 0012 columns and request-id skip index
67
+ * are included so a fresh DB matches a migrated one. `ttlDays` is
68
+ * LOG_TTL_DAYS; existing tables are brought in line at boot
69
+ * (ClickHouseStore.applyLogTtl), not here.
70
+ */
71
+ export declare function logTableDDL(db: string, ttlDays?: number): string;
72
+ /** Migration 0012 columns — shared by the baseline DDL and the migration. */
73
+ export declare const LOG_REQUEST_COLUMNS: Array<{
74
+ name: string;
75
+ ddl: string;
76
+ }>;
77
+ export declare const LOG_REQUEST_ID_INDEX = "INDEX idx_logs_request_id request_id TYPE bloom_filter GRANULARITY 4";
78
+ /**
79
+ * Per-minute request rollup (migration 0014), fed by runtime_request_1m_mv
80
+ * from `runtime_logs` rows with kind = 'request'. Route stats read this
81
+ * instead of scanning raw summaries:
82
+ * sum(request_count), sum(failed_count), sum(duration_sum_ms),
83
+ * quantilesMerge(0.5, 0.95)(duration_quantiles)
84
+ */
85
+ export declare function requestRollupTableDDL(db: string): string;
86
+ /**
87
+ * The view only sees rows inserted AFTER it exists (no backfill) and is
88
+ * created by migration 0014 only — not the baseline — because it reads the
89
+ * 0012 columns, which an existing runtime_logs table lacks until migrations
90
+ * run. runtime_logs is a ReplacingMergeTree keyed by a content-derived
91
+ * event_id, so a retried batch collapses there on merge — but the view
92
+ * aggregates every INSERT, so retries can count a summary twice. The
93
+ * rollup is therefore approximate under exporter retries (rare: a retry
94
+ * needs a 503 or a lost 2xx); exact per-request answers come from
95
+ * runtime_logs itself. `method` is read from the attributes JSON since
96
+ * runtime_logs has no method column.
97
+ */
98
+ export declare function requestRollupViewDDL(db: string): string;
package/dist/logs.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { decodeOtlpAttributes, sanitizeRuntimeContext, } from "./context.js";
3
+ import { declaredLink, liftErrorFields } from "./error-fields.js";
4
+ import { fingerprintOverrideOf } from "./fingerprint-override.js";
3
5
  import { normalizeRoute } from "./fingerprint.js";
4
6
  export function normalizeLogs(request) {
5
7
  if (!request || !Array.isArray(request.resourceLogs))
@@ -73,6 +75,57 @@ export function normalizeLogs(request) {
73
75
  : "";
74
76
  if (typeof attributes.path === "string")
75
77
  attributes.path = normalizeRoute(attributes.path.split(/[?#]/)[0]);
78
+ const service = String(info["service.name"] ?? "unknown").slice(0, 200);
79
+ const environment = String(info["deployment.environment.name"] ??
80
+ info["deployment.environment"] ??
81
+ "production").slice(0, 100);
82
+ const release = String(info["service.version"] ?? "").slice(0, 200);
83
+ const declared = liftErrorFields(attributes);
84
+ // The sanitiser strips URL fragments; the declared docs link keeps
85
+ // its #anchor from the raw attribute (still validated).
86
+ const rawLink = declaredLink(record.attributes?.find((a) => a?.key === "autter.error.link")
87
+ ?.value?.stringValue);
88
+ if (rawLink)
89
+ declared.link = rawLink;
90
+ const rawRoute = typeof attributes["http.route"] === "string"
91
+ ? attributes["http.route"].split(/[?#]/)[0]
92
+ : "";
93
+ const route = normalizeRoute(rawRoute).slice(0, 500);
94
+ const status = Number(attributes["http.response.status_code"]);
95
+ const statusCode = Number.isInteger(status) && status > 0 && status < 1000 ? status : 0;
96
+ const declaredKind = attributes["autter.operation.kind"];
97
+ const type = attributes["autter.event.type"] === "operation" ? "operation" : "log";
98
+ const ai = attributes["autter.operation.ai"];
99
+ const aiRollup = ai && typeof ai === "object" && !Array.isArray(ai)
100
+ ? ai
101
+ : {};
102
+ const occurrence = attributes["autter.capture.mode"] === "log" &&
103
+ (severity === "error" || severity === "fatal") &&
104
+ hasException(attributes)
105
+ ? {
106
+ source: "server",
107
+ severity: severity,
108
+ service,
109
+ environment,
110
+ release: release || null,
111
+ errorType: String(attributes["exception.type"] || "Error").slice(0, 200),
112
+ message: String(attributes["exception.message"] || message || "Unknown error").slice(0, 4000),
113
+ stack: typeof attributes["exception.stacktrace"] === "string"
114
+ ? attributes["exception.stacktrace"]
115
+ : null,
116
+ route: rawRoute || stringOrNull(attributes["url.path"]),
117
+ method: typeof attributes["http.request.method"] === "string"
118
+ ? attributes["http.request.method"].toUpperCase().slice(0, 16)
119
+ : null,
120
+ statusCode: statusCode || null,
121
+ traceId: traceId || null,
122
+ sessionId: null,
123
+ attributes,
124
+ occurredAt,
125
+ ...declared,
126
+ ...fingerprintOverrideOf(attributes["autter.fingerprint"]),
127
+ }
128
+ : undefined;
76
129
  rows.push({
77
130
  id: createHash("sha256")
78
131
  .update(JSON.stringify([
@@ -86,31 +139,55 @@ export function normalizeLogs(request) {
86
139
  ]))
87
140
  .digest("hex")
88
141
  .slice(0, 32),
89
- service: String(info["service.name"] ?? "unknown").slice(0, 200),
90
- environment: String(info["deployment.environment.name"] ??
91
- info["deployment.environment"] ??
92
- "production").slice(0, 100),
93
- release: String(info["service.version"] ?? "").slice(0, 200),
142
+ service,
143
+ environment,
144
+ release,
94
145
  traceId,
95
146
  spanId,
96
147
  operationId,
97
148
  operation: String(attributes["autter.operation.name"] ?? "").slice(0, 200),
98
- type: attributes["autter.event.type"] === "operation"
99
- ? "operation"
100
- : "log",
149
+ type,
101
150
  severity,
102
151
  message,
103
152
  outcome: String(attributes["autter.operation.outcome"] ?? "").slice(0, 30),
104
153
  durationMs: Math.max(0, Number(attributes["autter.operation.duration_ms"]) || 0),
105
154
  attributes,
106
155
  occurredAt,
156
+ kind: declaredKind === "request" || declaredKind === "operation"
157
+ ? declaredKind
158
+ : type === "operation"
159
+ ? "operation"
160
+ : "",
161
+ requestId: declared.requestId ?? "",
162
+ route,
163
+ statusCode,
164
+ errorCode: declared.errorCode ?? "",
165
+ aiCostUsd: nonNegative(attributes["autter.operation.ai.cost_usd"] ?? aiRollup.cost_usd),
166
+ aiCalls: Math.min(4_294_967_295, Math.round(nonNegative(attributes["autter.operation.ai.calls"] ?? aiRollup.calls))),
167
+ ...(occurrence ? { occurrence } : {}),
107
168
  });
108
169
  }
109
170
  }
110
171
  }
111
172
  return rows;
112
173
  }
113
- export function logTableDDL(db) {
174
+ function hasException(attributes) {
175
+ return ["exception.type", "exception.message", "exception.stacktrace"].some((key) => typeof attributes[key] === "string" && attributes[key] !== "");
176
+ }
177
+ function stringOrNull(value) {
178
+ return typeof value === "string" && value ? value.split(/[?#]/)[0] : null;
179
+ }
180
+ function nonNegative(value) {
181
+ const n = Number(value);
182
+ return Number.isFinite(n) && n > 0 ? n : 0;
183
+ }
184
+ /**
185
+ * Baseline for fresh databases. The 0012 columns and request-id skip index
186
+ * are included so a fresh DB matches a migrated one. `ttlDays` is
187
+ * LOG_TTL_DAYS; existing tables are brought in line at boot
188
+ * (ClickHouseStore.applyLogTtl), not here.
189
+ */
190
+ export function logTableDDL(db, ttlDays = 14) {
114
191
  return `CREATE TABLE IF NOT EXISTS ${db}.runtime_logs (
115
192
  org_id String, repository_id String, event_id String,
116
193
  service LowCardinality(String), environment LowCardinality(String), release String DEFAULT '',
@@ -119,8 +196,68 @@ export function logTableDDL(db) {
119
196
  severity LowCardinality(String) DEFAULT 'info', message String CODEC(ZSTD(1)),
120
197
  outcome LowCardinality(String) DEFAULT '', duration_ms Float64 DEFAULT 0,
121
198
  attributes String DEFAULT '{}' CODEC(ZSTD(1)), occurred_at DateTime64(3, 'UTC'),
122
- ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
199
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
200
+ ${LOG_REQUEST_COLUMNS.map((column) => column.ddl).join(", ")},
201
+ ${LOG_REQUEST_ID_INDEX}
123
202
  ) ENGINE = ReplacingMergeTree PARTITION BY toDate(occurred_at)
124
203
  ORDER BY (org_id, repository_id, occurred_at, event_id)
125
- TTL toDateTime(occurred_at) + INTERVAL 14 DAY`;
204
+ TTL toDateTime(occurred_at) + INTERVAL ${ttlDays} DAY`;
205
+ }
206
+ /** Migration 0012 columns — shared by the baseline DDL and the migration. */
207
+ export const LOG_REQUEST_COLUMNS = [
208
+ { name: "kind", ddl: "kind LowCardinality(String) DEFAULT ''" },
209
+ { name: "request_id", ddl: "request_id String DEFAULT ''" },
210
+ { name: "route", ddl: "route String DEFAULT ''" },
211
+ { name: "status_code", ddl: "status_code UInt16 DEFAULT 0" },
212
+ { name: "error_code", ddl: "error_code String DEFAULT ''" },
213
+ { name: "ai_cost_usd", ddl: "ai_cost_usd Float64 DEFAULT 0" },
214
+ { name: "ai_calls", ddl: "ai_calls UInt32 DEFAULT 0" },
215
+ ];
216
+ export const LOG_REQUEST_ID_INDEX = "INDEX idx_logs_request_id request_id TYPE bloom_filter GRANULARITY 4";
217
+ /**
218
+ * Per-minute request rollup (migration 0014), fed by runtime_request_1m_mv
219
+ * from `runtime_logs` rows with kind = 'request'. Route stats read this
220
+ * instead of scanning raw summaries:
221
+ * sum(request_count), sum(failed_count), sum(duration_sum_ms),
222
+ * quantilesMerge(0.5, 0.95)(duration_quantiles)
223
+ */
224
+ export function requestRollupTableDDL(db) {
225
+ return `CREATE TABLE IF NOT EXISTS ${db}.runtime_request_1m (
226
+ org_id String, repository_id String,
227
+ service LowCardinality(String), environment LowCardinality(String),
228
+ route String, method LowCardinality(String), bucket_at DateTime('UTC'),
229
+ request_count SimpleAggregateFunction(sum, UInt64),
230
+ failed_count SimpleAggregateFunction(sum, UInt64),
231
+ duration_sum_ms SimpleAggregateFunction(sum, Float64),
232
+ duration_quantiles AggregateFunction(quantiles(0.5, 0.95), Float64)
233
+ ) ENGINE = AggregatingMergeTree PARTITION BY toYYYYMM(bucket_at)
234
+ ORDER BY (org_id, repository_id, service, environment, route, method, bucket_at)
235
+ TTL bucket_at + INTERVAL 90 DAY`;
236
+ }
237
+ /**
238
+ * The view only sees rows inserted AFTER it exists (no backfill) and is
239
+ * created by migration 0014 only — not the baseline — because it reads the
240
+ * 0012 columns, which an existing runtime_logs table lacks until migrations
241
+ * run. runtime_logs is a ReplacingMergeTree keyed by a content-derived
242
+ * event_id, so a retried batch collapses there on merge — but the view
243
+ * aggregates every INSERT, so retries can count a summary twice. The
244
+ * rollup is therefore approximate under exporter retries (rare: a retry
245
+ * needs a 503 or a lost 2xx); exact per-request answers come from
246
+ * runtime_logs itself. `method` is read from the attributes JSON since
247
+ * runtime_logs has no method column.
248
+ */
249
+ export function requestRollupViewDDL(db) {
250
+ return `CREATE MATERIALIZED VIEW IF NOT EXISTS ${db}.runtime_request_1m_mv
251
+ TO ${db}.runtime_request_1m AS
252
+ SELECT
253
+ org_id, repository_id, service, environment, route,
254
+ upper(JSONExtractString(attributes, 'http.request.method')) AS method,
255
+ toDateTime(toStartOfMinute(occurred_at), 'UTC') AS bucket_at,
256
+ count() AS request_count,
257
+ countIf(outcome = 'failed') AS failed_count,
258
+ sum(duration_ms) AS duration_sum_ms,
259
+ quantilesState(0.5, 0.95)(duration_ms) AS duration_quantiles
260
+ FROM ${db}.runtime_logs
261
+ WHERE kind = 'request'
262
+ GROUP BY org_id, repository_id, service, environment, route, method, bucket_at`;
126
263
  }
package/dist/memory.d.ts CHANGED
@@ -24,16 +24,16 @@ export declare const platformEventSchema: z.ZodObject<{
24
24
  service: string;
25
25
  environment: string;
26
26
  release: string;
27
+ occurredAt: string;
27
28
  kind: "oom_kill" | "restart";
28
29
  instanceId: string;
29
30
  platform: "ecs" | "kubernetes" | "other";
30
- occurredAt: string;
31
31
  }, {
32
32
  service: string;
33
+ occurredAt: string;
33
34
  kind: "oom_kill" | "restart";
34
35
  instanceId: string;
35
36
  platform: "ecs" | "kubernetes" | "other";
36
- occurredAt: string;
37
37
  environment?: string | undefined;
38
38
  release?: string | undefined;
39
39
  }>;
@@ -32,6 +32,19 @@
32
32
  * ],
33
33
  * },
34
34
  */
35
+ /** Migration 0013 columns — shared with the baseline DDL in clickhouse.ts. */
36
+ export declare const OCCURRENCE_CODE_COLUMNS: string[];
37
+ export declare const OCCURRENCE_REQUEST_ID_INDEX = "INDEX idx_occ_request_id request_id TYPE bloom_filter GRANULARITY 4";
38
+ /**
39
+ * Migration 0016 columns — shared with the baseline DDL in clickhouse.ts.
40
+ * `frames`: the structured (symbolicated) frames as JSON, '' when none.
41
+ * `fingerprints`: every identity candidate (manual-v1, code-v1, frames-v3,
42
+ * message-v1 — highest precedence first, ending with the legacy one), so
43
+ * readers find an issue keyed by ANY scheme with
44
+ * `(fingerprint = X OR has(fingerprints, X))`; rows written before 0016
45
+ * read `[]` and still match on the legacy `fingerprint`.
46
+ */
47
+ export declare const OCCURRENCE_GROUPING_COLUMNS: string[];
35
48
  export interface Migration {
36
49
  /** Unique, ordered id: "<serial>-<slug>". Never reuse or reorder. */
37
50
  id: string;
@@ -34,9 +34,34 @@
34
34
  */
35
35
  import { latencyTableDDL } from "./latency.js";
36
36
  import { profileTableDDL } from "./profiles.js";
37
- import { sourceMapTableDDL } from "./source-maps.js";
38
- import { logTableDDL } from "./logs.js";
37
+ import { SOURCE_MAP_DEBUG_COLUMNS, SOURCE_MAP_DEBUG_ID_INDEX, sourceMapTableDDL, } from "./source-maps.js";
38
+ import { LOG_REQUEST_COLUMNS, LOG_REQUEST_ID_INDEX, logTableDDL, requestRollupTableDDL, requestRollupViewDDL, } from "./logs.js";
39
39
  import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
40
+ import { ingesterInfoTableDDL, sdkVersionTableDDL } from "./sdk-versions.js";
41
+ /** Migration 0013 columns — shared with the baseline DDL in clickhouse.ts. */
42
+ export const OCCURRENCE_CODE_COLUMNS = [
43
+ "error_code String DEFAULT ''",
44
+ "error_why String DEFAULT ''",
45
+ "error_fix String DEFAULT ''",
46
+ "error_link String DEFAULT ''",
47
+ "expected UInt8 DEFAULT 0",
48
+ "request_id String DEFAULT ''",
49
+ "fingerprint_scheme LowCardinality(String) DEFAULT 'message-v1'",
50
+ ];
51
+ export const OCCURRENCE_REQUEST_ID_INDEX = "INDEX idx_occ_request_id request_id TYPE bloom_filter GRANULARITY 4";
52
+ /**
53
+ * Migration 0016 columns — shared with the baseline DDL in clickhouse.ts.
54
+ * `frames`: the structured (symbolicated) frames as JSON, '' when none.
55
+ * `fingerprints`: every identity candidate (manual-v1, code-v1, frames-v3,
56
+ * message-v1 — highest precedence first, ending with the legacy one), so
57
+ * readers find an issue keyed by ANY scheme with
58
+ * `(fingerprint = X OR has(fingerprints, X))`; rows written before 0016
59
+ * read `[]` and still match on the legacy `fingerprint`.
60
+ */
61
+ export const OCCURRENCE_GROUPING_COLUMNS = [
62
+ "frames String DEFAULT '' CODEC(ZSTD(3))",
63
+ "fingerprints Array(String) DEFAULT []",
64
+ ];
40
65
  export const MIGRATIONS = [
41
66
  // 0001 intentionally reserved as the baseline marker: databases created
42
67
  // before the migration runner existed record it as applied without
@@ -129,6 +154,54 @@ export const MIGRATIONS = [
129
154
  `ALTER TABLE {db}.runtime_memory_samples ADD COLUMN IF NOT EXISTS temporality LowCardinality(String) DEFAULT 'gauge'`,
130
155
  ] },
131
156
  { id: "0011-runtime-logs", statements: [logTableDDL("{db}")] },
157
+ // Request wide events: request summaries share runtime_logs with
158
+ // operations and plain logs; the lifted columns make "requests on this
159
+ // route", "everything for request X" and "coded failures" plain column
160
+ // filters. The skip index only covers parts written after it exists
161
+ // (no MATERIALIZE INDEX mutation) — older rows predate request ids.
162
+ {
163
+ id: "0012-runtime-logs-requests",
164
+ statements: [
165
+ ...LOG_REQUEST_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_logs ADD COLUMN IF NOT EXISTS ${column.ddl}`),
166
+ `ALTER TABLE {db}.runtime_logs ADD ${LOG_REQUEST_ID_INDEX.replace("INDEX ", "INDEX IF NOT EXISTS ")}`,
167
+ ],
168
+ },
169
+ // Declared error metadata and code-based grouping (fingerprint_scheme
170
+ // "code-v1" vs the historical "message-v1").
171
+ {
172
+ id: "0013-occurrence-codes",
173
+ statements: [
174
+ ...OCCURRENCE_CODE_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_error_occurrences ADD COLUMN IF NOT EXISTS ${column}`),
175
+ `ALTER TABLE {db}.runtime_error_occurrences ADD ${OCCURRENCE_REQUEST_ID_INDEX.replace("INDEX ", "INDEX IF NOT EXISTS ")}`,
176
+ ],
177
+ },
178
+ // Per-minute route rollup of request summaries. Table first, then the
179
+ // view (which needs both the table and the 0012 columns).
180
+ {
181
+ id: "0014-runtime-request-1m",
182
+ statements: [requestRollupTableDDL("{db}"), requestRollupViewDDL("{db}")],
183
+ },
184
+ // Version compatibility: SDK name/version seen per service, and this
185
+ // ingester's own version + schema level for the Autter dashboard.
186
+ { id: "0015-runtime-compat", statements: [sdkVersionTableDDL("{db}"), ingesterInfoTableDDL("{db}")] },
187
+ // Structured stacks and multi-scheme identity (frames-v3): the platform
188
+ // resolves an issue from any candidate, so occurrence lookups by an
189
+ // issue's fingerprint must match every candidate a row carried.
190
+ {
191
+ id: "0016-occurrence-frames-fingerprints",
192
+ statements: OCCURRENCE_GROUPING_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_error_occurrences ADD COLUMN IF NOT EXISTS ${column}`),
193
+ },
194
+ // Source map debug ids (EI-13): the bundle's debug id, so a map matches
195
+ // by the bytes it was built from whatever release the SDK reports, plus
196
+ // the stored map's sha256 so CI skips maps it already uploaded. The
197
+ // sorting key is unchanged: uploads still need a release.
198
+ {
199
+ id: "0017-source-maps-debug-id",
200
+ statements: [
201
+ ...SOURCE_MAP_DEBUG_COLUMNS.map((column) => `ALTER TABLE {db}.runtime_source_maps ADD COLUMN IF NOT EXISTS ${column}`),
202
+ `ALTER TABLE {db}.runtime_source_maps ADD ${SOURCE_MAP_DEBUG_ID_INDEX.replace("INDEX ", "INDEX IF NOT EXISTS ")}`,
203
+ ],
204
+ },
132
205
  ];
133
206
  /** The tracking table itself — created by the runner before anything else. */
134
207
  export function migrationsTableDDL(db) {