@autter/otlp-ingester 1.5.0 → 1.6.1

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 (43) hide show
  1. package/README.md +1 -0
  2. package/dist/breadcrumbs.d.ts +41 -0
  3. package/dist/breadcrumbs.js +151 -0
  4. package/dist/clickhouse.d.ts +10 -6
  5. package/dist/clickhouse.js +38 -5
  6. package/dist/compat-manifest.json +29 -0
  7. package/dist/compat.d.ts +3 -1
  8. package/dist/compat.js +3 -1
  9. package/dist/config.d.ts +6 -0
  10. package/dist/config.js +1 -0
  11. package/dist/context.js +4 -1
  12. package/dist/fingerprint-override.d.ts +42 -0
  13. package/dist/fingerprint-override.js +76 -0
  14. package/dist/fingerprint-v3.d.ts +54 -0
  15. package/dist/fingerprint-v3.js +96 -0
  16. package/dist/fingerprint.d.ts +25 -0
  17. package/dist/fingerprint.js +1 -1
  18. package/dist/logs.js +2 -0
  19. package/dist/memory.d.ts +2 -2
  20. package/dist/migrations.d.ts +10 -0
  21. package/dist/migrations.js +32 -1
  22. package/dist/normalize-browser.d.ts +67 -34
  23. package/dist/normalize-browser.js +24 -1
  24. package/dist/normalize-otlp.js +4 -0
  25. package/dist/server.js +94 -18
  26. package/dist/sink.d.ts +5 -1
  27. package/dist/sink.js +13 -2
  28. package/dist/source-map-mappings.d.ts +57 -0
  29. package/dist/source-map-mappings.js +245 -0
  30. package/dist/source-map-scopes.d.ts +71 -0
  31. package/dist/source-map-scopes.js +190 -0
  32. package/dist/source-maps-lookup.d.ts +31 -0
  33. package/dist/source-maps-lookup.js +64 -0
  34. package/dist/source-maps.d.ts +72 -2
  35. package/dist/source-maps.js +134 -14
  36. package/dist/stack-fixtures.d.ts +10 -2
  37. package/dist/stack-fixtures.js +13 -3
  38. package/dist/stack-frames.d.ts +48 -0
  39. package/dist/stack-frames.js +441 -0
  40. package/dist/symbolicate.d.ts +117 -0
  41. package/dist/symbolicate.js +363 -0
  42. package/dist/types.d.ts +68 -0
  43. package/package.json +2 -1
@@ -0,0 +1,96 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizeMessage, occurrenceFingerprint } from "./fingerprint.js";
3
+ import { manualFingerprint } from "./fingerprint-override.js";
4
+ /**
5
+ * `frames-v3` — the structural issue identity (contract §2). Byte-for-byte
6
+ * shared with the platform mirror (`runtime-frames-fingerprint.ts`); the
7
+ * vectors in fingerprint-v3-vectors.json pin both.
8
+ *
9
+ * It hashes service + error type + up to five in-app frames reduced to
10
+ * `function@file`, where file is a lowercased basename with build hashes
11
+ * (`index-5f3a9c1e.js` → `index-*.js`) templated out. Line and column
12
+ * numbers, route, source (browser vs server) and release are never part of
13
+ * it, so one bug stays one issue across deploys, pages, preview domains and
14
+ * tiers. The normalised message is hashed only when no frame was picked.
15
+ */
16
+ export const FRAMES_V3 = "frames-v3";
17
+ const MAX_PICKED_FRAMES = 5;
18
+ /** §2.3 — the function half of a frame token. */
19
+ export function frameTokenFunction(frame) {
20
+ let fn = frame.function ?? "";
21
+ fn = fn.replace(/^async /, "");
22
+ fn = fn.replace(/^new /, "");
23
+ fn = fn.replace(/^(?:Object|Module)\./, "");
24
+ if (frame.resolved === false && fn.length <= 2)
25
+ fn = "";
26
+ if (fn === "<anonymous>" || fn === "anonymous")
27
+ fn = "";
28
+ return fn;
29
+ }
30
+ /** §2.3 — the file half of a frame token. */
31
+ export function frameTokenFile(frame) {
32
+ let file = frame.filename ?? frame.module ?? "";
33
+ const cut = file.search(/[?#]/);
34
+ if (cut !== -1)
35
+ file = file.slice(0, cut);
36
+ file = file.slice(Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")) + 1);
37
+ file = file.replace(/[A-Za-z0-9]{8,}/g, (run) => /[0-9]/.test(run) || run === run.toUpperCase() ? "*" : run);
38
+ return file.toLowerCase();
39
+ }
40
+ export function frameToken(frame) {
41
+ return `${frameTokenFunction(frame)}@${frameTokenFile(frame)}`;
42
+ }
43
+ /** §2.1–2.2 — in-app frames in order, else only the first frame; at most 5. */
44
+ export function pickFrames(frames) {
45
+ const inApp = frames.filter((frame) => frame.inApp === true);
46
+ const picked = inApp.length > 0 ? inApp : frames.slice(0, 1);
47
+ return picked.slice(0, MAX_PICKED_FRAMES);
48
+ }
49
+ export function framesV3Fingerprint(input) {
50
+ const tokens = pickFrames(input.frames).map(frameToken);
51
+ const message = tokens.length === 0 ? normalizeMessage(input.message) : "";
52
+ const fingerprint = createHash("sha256")
53
+ .update([FRAMES_V3, input.service, input.errorType, tokens.join("\u0001"), message].join("\u0000"))
54
+ .digest("hex")
55
+ .slice(0, 32);
56
+ return {
57
+ fingerprint,
58
+ record: {
59
+ scheme: FRAMES_V3,
60
+ parts: [input.errorType, ...tokens, message].filter((part) => part !== ""),
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * The candidate list the sink carries (contract §1/§4), highest precedence
66
+ * first:
67
+ * manual-v1 (SDK override) → code-v1 (valid error code) → frames-v3 →
68
+ * message-v1 (legacy).
69
+ * A coded error's legacy identity IS code-v1, which outranks every auto
70
+ * scheme, so its list is [manual-v1?, code-v1]; an uncoded one is
71
+ * [manual-v1?, frames-v3, message-v1]. Either way the list ends with the
72
+ * legacy fingerprint, so the platform can always fall back to it.
73
+ */
74
+ export function occurrenceIdentity(input, frames) {
75
+ const legacy = occurrenceFingerprint(input);
76
+ const candidates = [];
77
+ const manual = manualFingerprint(input);
78
+ if (manual)
79
+ candidates.push({ scheme: "manual-v1", fingerprint: manual });
80
+ let record = null;
81
+ if (legacy.scheme === "code-v1") {
82
+ candidates.push({ scheme: "code-v1", fingerprint: legacy.fingerprint });
83
+ }
84
+ else {
85
+ const v3 = framesV3Fingerprint({
86
+ service: input.service,
87
+ errorType: input.errorType,
88
+ message: input.message,
89
+ frames,
90
+ });
91
+ record = v3.record;
92
+ candidates.push({ scheme: FRAMES_V3, fingerprint: v3.fingerprint });
93
+ candidates.push({ scheme: "message-v1", fingerprint: legacy.fingerprint });
94
+ }
95
+ return { legacy, candidates, record };
96
+ }
@@ -2,6 +2,31 @@ import type { RuntimeOccurrenceInput } from "./types.js";
2
2
  export declare function normalizeMessage(message: string): string;
3
3
  /** Replace id-like path segments so /orders/812 and /orders/44 group. */
4
4
  export declare function normalizeRoute(route: string | null): string;
5
+ /**
6
+ * Backend exceptions reach us in many native stack formats. The fingerprint
7
+ * groups by the top stack frames, so a format we cannot parse yields an EMPTY
8
+ * frame list — and then every error sharing a message collapses into one
9
+ * issue (two unrelated Go panics, say, become a single group). To keep
10
+ * unrelated defects apart, each officially-supported language is parsed and
11
+ * every frame reduced to a stable `function (file)` token that preserves the
12
+ * function, module and file.
13
+ *
14
+ * Volatile parts are stripped: line/column numbers, `+0x` offsets, pointer
15
+ * arguments and memory addresses. Line numbers are parsed but deliberately
16
+ * left OUT of the token — they shift on every re-deploy, and the JS path has
17
+ * always dropped them for exactly this reason, so the same defect stays one
18
+ * issue across rebuilds. The raw stack is retained verbatim on its own column.
19
+ *
20
+ * JavaScript/TypeScript, Firefox `@` frames and Python `File` frames keep
21
+ * their historical normalisation byte-for-byte (parseScriptFrames) so existing
22
+ * issues never regroup.
23
+ */
24
+ export type StackLanguage = "go" | "rust" | "jvm" | "dotnet" | "script";
25
+ /**
26
+ * Classify a whole stack by language. Signatures are chosen to be unique to
27
+ * each runtime so a JS or Python stack always falls through to "script".
28
+ */
29
+ export declare function detectStackLanguage(lines: string[]): StackLanguage;
5
30
  export declare function normalizeStackFrames(stack: string | null, topN?: number): string[];
6
31
  /**
7
32
  * Error codes (`autter.error.code`) — namespaced, stable, low-cardinality
@@ -70,7 +70,7 @@ const RUST_AT_RS_RE = /^\s*at\s+\S+\.rs:\d+/;
70
70
  * Classify a whole stack by language. Signatures are chosen to be unique to
71
71
  * each runtime so a JS or Python stack always falls through to "script".
72
72
  */
73
- function detectStackLanguage(lines) {
73
+ export function detectStackLanguage(lines) {
74
74
  let hasDotnet = false;
75
75
  let hasJvm = false;
76
76
  let hasRust = false;
package/dist/logs.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { decodeOtlpAttributes, sanitizeRuntimeContext, } from "./context.js";
3
3
  import { declaredLink, liftErrorFields } from "./error-fields.js";
4
+ import { fingerprintOverrideOf } from "./fingerprint-override.js";
4
5
  import { normalizeRoute } from "./fingerprint.js";
5
6
  export function normalizeLogs(request) {
6
7
  if (!request || !Array.isArray(request.resourceLogs))
@@ -122,6 +123,7 @@ export function normalizeLogs(request) {
122
123
  attributes,
123
124
  occurredAt,
124
125
  ...declared,
126
+ ...fingerprintOverrideOf(attributes["autter.fingerprint"]),
125
127
  }
126
128
  : undefined;
127
129
  rows.push({
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
  }>;
@@ -35,6 +35,16 @@
35
35
  /** Migration 0013 columns — shared with the baseline DDL in clickhouse.ts. */
36
36
  export declare const OCCURRENCE_CODE_COLUMNS: string[];
37
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[];
38
48
  export interface Migration {
39
49
  /** Unique, ordered id: "<serial>-<slug>". Never reuse or reorder. */
40
50
  id: string;
@@ -34,7 +34,7 @@
34
34
  */
35
35
  import { latencyTableDDL } from "./latency.js";
36
36
  import { profileTableDDL } from "./profiles.js";
37
- import { sourceMapTableDDL } from "./source-maps.js";
37
+ import { SOURCE_MAP_DEBUG_COLUMNS, SOURCE_MAP_DEBUG_ID_INDEX, sourceMapTableDDL, } from "./source-maps.js";
38
38
  import { LOG_REQUEST_COLUMNS, LOG_REQUEST_ID_INDEX, logTableDDL, requestRollupTableDDL, requestRollupViewDDL, } from "./logs.js";
39
39
  import { memoryTableDDL, platformEventTableDDL } from "./memory.js";
40
40
  import { ingesterInfoTableDDL, sdkVersionTableDDL } from "./sdk-versions.js";
@@ -49,6 +49,19 @@ export const OCCURRENCE_CODE_COLUMNS = [
49
49
  "fingerprint_scheme LowCardinality(String) DEFAULT 'message-v1'",
50
50
  ];
51
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
+ ];
52
65
  export const MIGRATIONS = [
53
66
  // 0001 intentionally reserved as the baseline marker: databases created
54
67
  // before the migration runner existed record it as applied without
@@ -171,6 +184,24 @@ export const MIGRATIONS = [
171
184
  // Version compatibility: SDK name/version seen per service, and this
172
185
  // ingester's own version + schema level for the Autter dashboard.
173
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
+ },
174
205
  ];
175
206
  /** The tracking table itself — created by the runner before anything else. */
176
207
  export function migrationsTableDDL(db) {
@@ -34,56 +34,85 @@ export declare const browserPayloadSchema: z.ZodObject<{
34
34
  * instead of rejecting the whole beacon.
35
35
  */
36
36
  context: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
37
+ /** runtime-browser `/breadcrumbs`: steps before a failure. Validated entry by entry in breadcrumbs.ts; a malformed list is dropped, never the beacon. */
38
+ breadcrumbs: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>>;
39
+ /** runtime-browser ≥1.6.0: script URL → debug id for the scripts in this
40
+ * event's stack (EI-13). Malformed values are dropped, never the beacon. */
41
+ debugIds: z.ZodCatch<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
42
+ /**
43
+ * Fingerprint override (runtime-browser 1.6.0+): one string or a list.
44
+ * Bounded and scrubbed in fingerprint-override.ts; a malformed value is
45
+ * dropped on its own.
46
+ */
47
+ fingerprint: z.ZodCatch<z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>>;
48
+ /**
49
+ * runtime-browser ≥1.6.0: W3C trace id of the request behind the event
50
+ * (the `traceparent` it sent), joining it to the server trace. A malformed
51
+ * id is dropped on its own instead of rejecting the beacon.
52
+ */
53
+ traceId: z.ZodCatch<z.ZodOptional<z.ZodString>>;
37
54
  }, "strip", z.ZodTypeAny, {
38
- type: "exception" | "message" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
39
55
  message: string;
56
+ type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
40
57
  timestamp: string;
41
- durationMs?: number | undefined;
58
+ severity?: "info" | "warning" | "error" | "fatal" | undefined;
59
+ errorType?: string | undefined;
60
+ stack?: string | undefined;
42
61
  route?: string | undefined;
62
+ traceId?: string | undefined;
63
+ breadcrumbs?: unknown[] | undefined;
64
+ debugIds?: Record<string, unknown> | undefined;
65
+ durationMs?: number | undefined;
43
66
  filename?: string | undefined;
44
- id?: string | undefined;
45
- severity?: "fatal" | "error" | "warning" | "info" | undefined;
46
- name?: string | undefined;
47
- stack?: string | undefined;
48
- errorType?: string | undefined;
49
- line?: number | undefined;
50
67
  column?: number | undefined;
68
+ line?: number | undefined;
69
+ name?: string | undefined;
70
+ id?: string | undefined;
51
71
  context?: Record<string, unknown> | undefined;
72
+ fingerprint?: string | string[] | undefined;
52
73
  }, {
53
- type: "exception" | "message" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
74
+ type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
54
75
  timestamp: string;
55
- durationMs?: number | undefined;
56
- route?: string | undefined;
57
76
  message?: string | undefined;
58
- filename?: string | undefined;
59
- id?: string | undefined;
60
- severity?: "fatal" | "error" | "warning" | "info" | undefined;
61
- name?: string | undefined;
62
- stack?: string | undefined;
77
+ severity?: "info" | "warning" | "error" | "fatal" | undefined;
63
78
  errorType?: string | undefined;
64
- line?: number | undefined;
79
+ stack?: string | undefined;
80
+ route?: string | undefined;
81
+ traceId?: unknown;
82
+ breadcrumbs?: unknown;
83
+ debugIds?: unknown;
84
+ durationMs?: number | undefined;
85
+ filename?: string | undefined;
65
86
  column?: number | undefined;
87
+ line?: number | undefined;
88
+ name?: string | undefined;
89
+ id?: string | undefined;
66
90
  context?: Record<string, unknown> | undefined;
91
+ fingerprint?: unknown;
67
92
  }>, "many">;
68
93
  }, "strip", z.ZodTypeAny, {
69
94
  service: string;
70
95
  environment: string;
71
96
  version: 1;
72
97
  events: {
73
- type: "exception" | "message" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
74
98
  message: string;
99
+ type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
75
100
  timestamp: string;
76
- durationMs?: number | undefined;
101
+ severity?: "info" | "warning" | "error" | "fatal" | undefined;
102
+ errorType?: string | undefined;
103
+ stack?: string | undefined;
77
104
  route?: string | undefined;
105
+ traceId?: string | undefined;
106
+ breadcrumbs?: unknown[] | undefined;
107
+ debugIds?: Record<string, unknown> | undefined;
108
+ durationMs?: number | undefined;
78
109
  filename?: string | undefined;
79
- id?: string | undefined;
80
- severity?: "fatal" | "error" | "warning" | "info" | undefined;
81
- name?: string | undefined;
82
- stack?: string | undefined;
83
- errorType?: string | undefined;
84
- line?: number | undefined;
85
110
  column?: number | undefined;
111
+ line?: number | undefined;
112
+ name?: string | undefined;
113
+ id?: string | undefined;
86
114
  context?: Record<string, unknown> | undefined;
115
+ fingerprint?: string | string[] | undefined;
87
116
  }[];
88
117
  release?: string | undefined;
89
118
  sessionId?: string | undefined;
@@ -93,20 +122,24 @@ export declare const browserPayloadSchema: z.ZodObject<{
93
122
  environment: string;
94
123
  version: 1;
95
124
  events: {
96
- type: "exception" | "message" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
125
+ type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
97
126
  timestamp: string;
98
- durationMs?: number | undefined;
99
- route?: string | undefined;
100
127
  message?: string | undefined;
101
- filename?: string | undefined;
102
- id?: string | undefined;
103
- severity?: "fatal" | "error" | "warning" | "info" | undefined;
104
- name?: string | undefined;
105
- stack?: string | undefined;
128
+ severity?: "info" | "warning" | "error" | "fatal" | undefined;
106
129
  errorType?: string | undefined;
107
- line?: number | undefined;
130
+ stack?: string | undefined;
131
+ route?: string | undefined;
132
+ traceId?: unknown;
133
+ breadcrumbs?: unknown;
134
+ debugIds?: unknown;
135
+ durationMs?: number | undefined;
136
+ filename?: string | undefined;
108
137
  column?: number | undefined;
138
+ line?: number | undefined;
139
+ name?: string | undefined;
140
+ id?: string | undefined;
109
141
  context?: Record<string, unknown> | undefined;
142
+ fingerprint?: unknown;
110
143
  }[];
111
144
  release?: string | undefined;
112
145
  sessionId?: string | undefined;
@@ -1,8 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { liftErrorFields } from "./error-fields.js";
3
+ import { fingerprintOverrideOf } from "./fingerprint-override.js";
3
4
  import { sanitizeRuntimeContext } from "./context.js";
4
5
  import { normalizeRoute } from "./fingerprint.js";
5
6
  import { scrubText } from "./redact.js";
7
+ import { breadcrumbsField } from "./breadcrumbs.js";
8
+ import { browserDebugIds } from "./source-maps.js";
6
9
  import { asSeverity, } from "./types.js";
7
10
  /**
8
11
  * The compact browser payload (`version: 1`) emitted by
@@ -46,6 +49,23 @@ const browserEventSchema = z.object({
46
49
  * instead of rejecting the whole beacon.
47
50
  */
48
51
  context: z.record(z.unknown()).optional(),
52
+ /** runtime-browser `/breadcrumbs`: steps before a failure. Validated entry by entry in breadcrumbs.ts; a malformed list is dropped, never the beacon. */
53
+ breadcrumbs: z.array(z.unknown()).optional().catch(undefined),
54
+ /** runtime-browser ≥1.6.0: script URL → debug id for the scripts in this
55
+ * event's stack (EI-13). Malformed values are dropped, never the beacon. */
56
+ debugIds: z.record(z.string().max(1000), z.unknown()).optional().catch(undefined),
57
+ /**
58
+ * Fingerprint override (runtime-browser 1.6.0+): one string or a list.
59
+ * Bounded and scrubbed in fingerprint-override.ts; a malformed value is
60
+ * dropped on its own.
61
+ */
62
+ fingerprint: z.union([z.string().max(2000), z.array(z.string().max(2000)).max(50)]).optional().catch(undefined),
63
+ /**
64
+ * runtime-browser ≥1.6.0: W3C trace id of the request behind the event
65
+ * (the `traceparent` it sent), joining it to the server trace. A malformed
66
+ * id is dropped on its own instead of rejecting the beacon.
67
+ */
68
+ traceId: z.string().regex(/^(?!0{32})[0-9a-f]{32}$/).optional().catch(undefined),
49
69
  });
50
70
  export const browserPayloadSchema = z.object({
51
71
  version: z.literal(1),
@@ -156,7 +176,7 @@ export function normalizeBrowserPayload(payload) {
156
176
  route: event.route ? (event.route.split("?")[0] ?? null) : null,
157
177
  method: null,
158
178
  statusCode: null,
159
- traceId: null,
179
+ traceId: event.traceId ?? null,
160
180
  sessionId: payload.sessionId ?? null,
161
181
  attributes: {
162
182
  ...(event.filename ? { filename: event.filename.split("?")[0] } : {}),
@@ -166,6 +186,9 @@ export function normalizeBrowserPayload(payload) {
166
186
  },
167
187
  occurredAt,
168
188
  ...liftErrorFields(event.context),
189
+ ...breadcrumbsField(event.breadcrumbs),
190
+ ...browserDebugIds(event.debugIds),
191
+ ...fingerprintOverrideOf(event.fingerprint),
169
192
  });
170
193
  }
171
194
  return { occurrences, metricPoints: [...rollups.values()] };
@@ -1,8 +1,10 @@
1
1
  import { decodeOtlpAttributes, sanitizeRuntimeContext } from "./context.js";
2
2
  import { liftErrorFields, validRequestId } from "./error-fields.js";
3
+ import { fingerprintOverrideOf } from "./fingerprint-override.js";
3
4
  import { scrubText } from "./redact.js";
4
5
  import { normalizeRoute } from "./fingerprint.js";
5
6
  import { extractLlmCall } from "./llm.js";
7
+ import { BREADCRUMBS_ATTRIBUTE, breadcrumbsField } from "./breadcrumbs.js";
6
8
  import { asSeverity, } from "./types.js";
7
9
  const EMAIL_VALUE_RE = /[A-Z0-9._%+-]{1,64}@[A-Z0-9.-]{1,253}\.[A-Z]{2,63}/gi;
8
10
  function attrMap(attributes) {
@@ -172,6 +174,8 @@ export function normalizeTraces(request) {
172
174
  // Event attributes first: one span can carry several
173
175
  // exceptions, each with its own code.
174
176
  ...liftErrorFields(eventAttrs, attrs),
177
+ ...breadcrumbsField(eventAttrs.get(BREADCRUMBS_ATTRIBUTE), attrs.get(BREADCRUMBS_ATTRIBUTE)),
178
+ ...fingerprintOverrideOf(eventAttrs.get("autter.fingerprint") ?? attrs.get("autter.fingerprint")),
175
179
  });
176
180
  }
177
181
  const failedOutcomes = (span.events ?? []).filter((event) => {