@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
package/dist/server.js CHANGED
@@ -2,7 +2,10 @@ import express from "express";
2
2
  import { KeyResolver, RateLimiter } from "./auth.js";
3
3
  import { ClickHouseStore } from "./clickhouse.js";
4
4
  import { normalizeLatencyHistograms } from "./latency.js";
5
- import { deriveFields, occurrenceFingerprint, occurrenceIdFor, } from "./fingerprint.js";
5
+ import { deriveFields, occurrenceIdFor } from "./fingerprint.js";
6
+ import { occurrenceIdentity } from "./fingerprint-v3.js";
7
+ import { parseStackFrames } from "./stack-frames.js";
8
+ import { SourceMapCache, Symbolicator } from "./symbolicate.js";
6
9
  import { logPromotionCandidates, promotionLookups, promotionRollups, } from "./log-promotion.js";
7
10
  import { browserPayloadSchema, normalizeBrowserPayload, } from "./normalize-browser.js";
8
11
  import { normalizeMetrics, normalizeTraces, } from "./normalize-otlp.js";
@@ -10,7 +13,7 @@ import { decodeMetricsRequest, decodeTraceRequest, decodeLogsRequest } from "./o
10
13
  import { normalizeLogs } from "./logs.js";
11
14
  import { decodeProfile } from "./profiles.js";
12
15
  import { normalizeMemoryMetrics, normalizePlatformEvent, platformEventSchema } from "./memory.js";
13
- import { validateSourceMap } from "./source-maps.js";
16
+ import { SOURCE_MAP_MAX_BODY_BYTES, validateSourceMap, validateSourceMapCheck } from "./source-maps.js";
14
17
  import { SinkForwarder } from "./sink.js";
15
18
  import { configureRedaction, scrubOccurrence } from "./redact.js";
16
19
  import { buildCompatReport, evaluateCompat, INGESTER_VERSION_HEADER, } from "./compat.js";
@@ -22,6 +25,9 @@ const INGESTER_INFO_INTERVAL_MS = 6 * 60 * 60 * 1000;
22
25
  export function createIngesterApp(config) {
23
26
  configureRedaction(config);
24
27
  const store = new ClickHouseStore(config);
28
+ // Browser JS frames are resolved through uploaded source maps before they
29
+ // are fingerprinted (frames-v3), with parsed maps cached per release+file.
30
+ const symbolicator = new Symbolicator((key) => store.lookupSourceMap(key), new SourceMapCache({ maxBytes: (config.sourceMapCacheMb ?? 64) * 1024 * 1024 }));
25
31
  // Fingerprinted occurrences feed the consumer's issue grouping, metric
26
32
  // points feed the request/error-rate rollups, LLM calls feed spend
27
33
  // watching. Delivery is at-least-once with bounded retries — see sink.ts.
@@ -78,7 +84,40 @@ export function createIngesterApp(config) {
78
84
  next();
79
85
  });
80
86
  app.use(express.raw({ limit: "1mb", type: ["application/x-pprof"] }));
81
- app.use("/v1/sourcemaps", express.json({ limit: "5mb" }));
87
+ // A check names at most 500 maps: well under 1 MB, whatever MAX_BODY_BYTES is.
88
+ app.use("/v1/sourcemaps/check", express.json({ limit: "1mb" }));
89
+ // Source map uploads (up to 32 MiB of JSON, usually gzipped) are routed
90
+ // before the small-body parsers below, and read their body only once the
91
+ // key is checked: an anonymous client can't make the ingester inflate and
92
+ // parse one. body-parser inflates gzip/deflate and applies the limit to
93
+ // the inflated bytes, so a small compressed body can't expand past it.
94
+ const sourceMapBody = express.json({ limit: SOURCE_MAP_MAX_BODY_BYTES });
95
+ app.post("/v1/sourcemaps", async (req, res, next) => {
96
+ // On failure the body is never read (Node discards it).
97
+ const ctx = await authenticate(req, res, "otlp");
98
+ if (!ctx)
99
+ return;
100
+ sourceMapBody(req, res, async (err) => {
101
+ if (err) {
102
+ next(err);
103
+ return;
104
+ }
105
+ const sourceMap = validateSourceMap(req.body);
106
+ if (!sourceMap) {
107
+ res.status(400).json({ error: "invalid source map" });
108
+ return;
109
+ }
110
+ // Idempotent: the same release + path replaces the earlier row
111
+ // (ReplacingMergeTree), so CI retries and re-runs are harmless.
112
+ try {
113
+ await store.insertSourceMap(ctx, sourceMap);
114
+ res.status(202).json({ accepted: true });
115
+ }
116
+ catch (storeErr) {
117
+ storageError(res, storeErr);
118
+ }
119
+ });
120
+ });
82
121
  app.use(express.json({
83
122
  limit: config.maxBodyBytes,
84
123
  type: ["application/json"],
@@ -203,28 +242,64 @@ export function createIngesterApp(config) {
203
242
  }
204
243
  return ctx;
205
244
  }
245
+ /**
246
+ * Structured frames for the frames-v3 identity and the platform's stack
247
+ * view: parsed innermost-first, then — browser only, where bundles are
248
+ * minified — resolved through the release's source maps. Symbolication
249
+ * never fails ingest (it falls back to the parsed frames).
250
+ */
251
+ async function framesFor(ctx, input) {
252
+ let frames;
253
+ try {
254
+ frames = parseStackFrames(input.stack);
255
+ }
256
+ catch (err) {
257
+ console.warn("stack parse failed — no frames:", err instanceof Error ? err.message : err);
258
+ return [];
259
+ }
260
+ if (input.source !== "browser" || frames.length === 0)
261
+ return frames;
262
+ return symbolicator.symbolicate(frames, {
263
+ orgId: ctx.orgId,
264
+ repositoryId: ctx.repositoryId,
265
+ release: input.release,
266
+ debugIds: input.debugIds ?? null,
267
+ });
268
+ }
206
269
  /** Ids are content-derived (occurrenceIdFor), NOT random: an exporter
207
270
  * that retries a batch — after a 503 from a partially-failed ClickHouse
208
271
  * write, or when only our 2xx got lost — must produce the same ids, so
209
272
  * the sink consumer's per-occurrence dedupe holds across transport
210
- * retries and duplicated ClickHouse rows stay identifiable. */
211
- function fingerprintAll(ctx, inputs) {
212
- return inputs.map((raw, index) => {
273
+ * retries and duplicated ClickHouse rows stay identifiable.
274
+ *
275
+ * `fingerprint` / `fingerprintScheme` stay the legacy identity
276
+ * (message-v1 or code-v1) byte-for-byte; `fingerprints` carries every
277
+ * candidate (manual-v1, code-v1, frames-v3, message-v1) for the platform
278
+ * to resolve the issue from. */
279
+ async function fingerprintAll(ctx, inputs) {
280
+ return Promise.all(inputs.map(async (raw, index) => {
213
281
  // Second line of defence, before fingerprinting, storage, and the
214
282
  // sink (which feeds LLM fix generation): scrub secrets from the
215
283
  // free-text fields old SDKs and third-party OTLP senders pass
216
284
  // through verbatim. Attributes were already sanitised by the
217
285
  // normalisers (sanitizeRuntimeContext).
218
286
  const input = scrubOccurrence(raw);
219
- const { fingerprint, scheme } = occurrenceFingerprint(input);
287
+ const frames = await framesFor(ctx, input);
288
+ const identity = occurrenceIdentity(input, frames);
289
+ const { fingerprint, scheme } = identity.legacy;
290
+ // Debug ids only locate source maps; they are not stored or forwarded.
291
+ const { debugIds: _debugIds, ...stored } = input;
220
292
  return {
221
- ...input,
293
+ ...stored,
222
294
  occurrenceId: occurrenceIdFor(ctx, input, fingerprint, index),
223
295
  fingerprint,
224
296
  fingerprintScheme: scheme,
225
297
  ...deriveFields(input),
298
+ ...(frames.length > 0 ? { frames } : {}),
299
+ fingerprints: identity.candidates,
300
+ ...(identity.record ? { fingerprintRecord: identity.record } : {}),
226
301
  };
227
- });
302
+ }));
228
303
  }
229
304
  /** Fingerprint log-promotion candidates and drop the ones whose trace
230
305
  * already produced an occurrence (in this batch, or within ±60 s). Ids
@@ -234,7 +309,7 @@ export function createIngesterApp(config) {
234
309
  const candidates = logPromotionCandidates(logs);
235
310
  if (candidates.length === 0)
236
311
  return [];
237
- const fingerprinted = fingerprintAll(ctx, candidates);
312
+ const fingerprinted = await fingerprintAll(ctx, candidates);
238
313
  const seen = new Set();
239
314
  const ownIds = fingerprinted.map((o) => o.occurrenceId);
240
315
  for (const lookup of promotionLookups(candidates)) {
@@ -299,18 +374,19 @@ export function createIngesterApp(config) {
299
374
  storageError(res, err);
300
375
  }
301
376
  });
302
- app.post("/v1/sourcemaps", async (req, res) => {
377
+ /** Which of these maps does this tenant already have? Lets
378
+ * `autter-runtime sourcemaps upload` skip unchanged maps (1.6.0+). */
379
+ app.post("/v1/sourcemaps/check", async (req, res) => {
303
380
  const ctx = await authenticate(req, res, "otlp");
304
381
  if (!ctx)
305
382
  return;
306
- const sourceMap = validateSourceMap(req.body);
307
- if (!sourceMap) {
308
- res.status(400).json({ error: "invalid source map" });
383
+ const check = validateSourceMapCheck(req.body);
384
+ if (!check) {
385
+ res.status(400).json({ error: "invalid source map check" });
309
386
  return;
310
387
  }
311
388
  try {
312
- await store.insertSourceMap(ctx, sourceMap);
313
- res.status(202).json({ accepted: true });
389
+ res.status(200).json({ present: await store.sourceMapsPresent(ctx, check) });
314
390
  }
315
391
  catch (err) {
316
392
  storageError(res, err);
@@ -357,7 +433,7 @@ export function createIngesterApp(config) {
357
433
  request = req.body;
358
434
  }
359
435
  const { occurrences, spans, metricPoints, llmCalls } = normalizeTraces(request);
360
- const fingerprinted = fingerprintAll(ctx, occurrences);
436
+ const fingerprinted = await fingerprintAll(ctx, occurrences);
361
437
  // ClickHouse has no cross-table transaction, so these four inserts can
362
438
  // partially commit. Recovery boundary: any failure → 503 → the exporter
363
439
  // retries the whole batch. Deterministic occurrence ids make the retry
@@ -473,7 +549,7 @@ export function createIngesterApp(config) {
473
549
  return;
474
550
  }
475
551
  const { occurrences, metricPoints } = normalizeBrowserPayload(parsed.data);
476
- const fingerprinted = fingerprintAll(ctx, occurrences);
552
+ const fingerprinted = await fingerprintAll(ctx, occurrences);
477
553
  try {
478
554
  await Promise.all([
479
555
  store.insertOccurrences(ctx, fingerprinted),
package/dist/sink.d.ts CHANGED
@@ -107,6 +107,10 @@ export declare class SinkForwarder {
107
107
  * requestId, traceId, route, method, statusCode, fingerprintScheme. The
108
108
  * request-context four were previously spread as `null` when unknown and
109
109
  * are now simply left out, matching the documented optional-string shape.
110
- * Older consumers ignore unknown keys, so the version stays 1.
110
+ * 1.6.0 adds `frames` (innermost first, ≤ 50), `fingerprints` (every
111
+ * identity candidate, highest precedence first, ending with the legacy
112
+ * `fingerprint`) and `fingerprintRecord`; `fingerprint`/`fingerprintScheme`
113
+ * keep their legacy meaning. Older consumers ignore unknown keys, so the
114
+ * version stays 1.
111
115
  */
112
116
  export declare function sinkOccurrence(o: RuntimeOccurrence): Record<string, unknown>;
package/dist/sink.js CHANGED
@@ -301,10 +301,16 @@ export class SinkForwarder {
301
301
  * requestId, traceId, route, method, statusCode, fingerprintScheme. The
302
302
  * request-context four were previously spread as `null` when unknown and
303
303
  * are now simply left out, matching the documented optional-string shape.
304
- * Older consumers ignore unknown keys, so the version stays 1.
304
+ * 1.6.0 adds `frames` (innermost first, ≤ 50), `fingerprints` (every
305
+ * identity candidate, highest precedence first, ending with the legacy
306
+ * `fingerprint`) and `fingerprintRecord`; `fingerprint`/`fingerprintScheme`
307
+ * keep their legacy meaning. Older consumers ignore unknown keys, so the
308
+ * version stays 1.
305
309
  */
306
310
  export function sinkOccurrence(o) {
307
- const { traceId, route, method, statusCode, errorCode, why, fix, link, expected, requestId, ...rest } = o;
311
+ const { traceId, route, method, statusCode, errorCode, why, fix, link, expected, requestId, breadcrumbs, frames, fingerprints, fingerprintRecord,
312
+ // Input-only: locates source maps, never leaves the ingester.
313
+ debugIds: _debugIds, ...rest } = o;
308
314
  return {
309
315
  ...rest,
310
316
  occurredAt: o.occurredAt.toISOString(),
@@ -318,6 +324,11 @@ export function sinkOccurrence(o) {
318
324
  ...(link ? { link } : {}),
319
325
  ...(expected ? { expected: true } : {}),
320
326
  ...(requestId ? { requestId } : {}),
327
+ // 1.6.0: steps before the error, oldest first, ≤ 30.
328
+ ...(breadcrumbs?.length ? { breadcrumbs } : {}),
329
+ ...(frames?.length ? { frames } : {}),
330
+ ...(fingerprints?.length ? { fingerprints } : {}),
331
+ ...(fingerprintRecord ? { fingerprintRecord } : {}),
321
332
  };
322
333
  }
323
334
  /** ISO range across every signal in the batch — the replay hint on drops. */
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Position lookups in a source map's `mappings`, without the memory of a
3
+ * general-purpose decoder.
4
+ *
5
+ * @jridgewell/trace-mapping decodes `mappings` into one small array per
6
+ * segment: about 14 bytes of heap per byte of JSON (a 5 MB map takes 67 MB).
7
+ * Symbolication only needs "which original position is at or before this
8
+ * generated one", so a map that is kept in the cache is decoded into one
9
+ * Int32Array (5 ints per segment: about 3 bytes per byte of JSON), and a map
10
+ * too big to keep is not decoded at all: its frames are resolved in one
11
+ * streaming pass over the string.
12
+ *
13
+ * Lookup semantics match trace-mapping's `originalPositionFor` (greatest
14
+ * lower bound): the first segment that starts at the frame's column, else
15
+ * the last segment before it on the same line; a segment without a source
16
+ * maps to nothing.
17
+ */
18
+ /** An original position. Lines and columns are 0-based; `name` is -1 when the segment has none. */
19
+ export interface MappedSegment {
20
+ source: number;
21
+ line: number;
22
+ column: number;
23
+ name: number;
24
+ }
25
+ /** A 0-based generated (line, column). */
26
+ export interface GeneratedPosition {
27
+ line: number;
28
+ column: number;
29
+ }
30
+ /**
31
+ * Walk a mappings-style string — `;` between generated lines, `,` between
32
+ * groups of base64 VLQ values — calling `visit(line, values, count)` with
33
+ * each group's raw (relative) values. `values` is reused between calls and
34
+ * holds the first 6; `count` is how many the group had. Throws on
35
+ * malformed VLQ. Also reads `x_autter_scopes` (source-map-scopes.ts).
36
+ */
37
+ export declare function forEachVlqGroup(text: string, visit: (line: number, values: readonly number[], count: number) => void): void;
38
+ /** Heap a DecodedMappings of `mappings` would take, without decoding it. */
39
+ export declare function decodedMappingsBytes(mappings: string): number;
40
+ export declare class DecodedMappings {
41
+ /** FIELDS ints per segment, each line's segments sorted by generated column. */
42
+ private readonly segments;
43
+ /** Index of each line's first segment; one extra entry closes the last line. */
44
+ private readonly lineStarts;
45
+ private constructor();
46
+ static decode(mappings: string): DecodedMappings;
47
+ get bytes(): number;
48
+ /** Generators almost always emit a line's segments in column order; sort the rare line that isn't (stable, as trace-mapping does). */
49
+ private sortLine;
50
+ /** The original position at a 0-based generated (line, column), or null. */
51
+ find(line: number, column: number): MappedSegment | null;
52
+ }
53
+ /**
54
+ * Resolve several positions in one pass over `mappings` without decoding
55
+ * it — for maps too big to keep. Same results as DecodedMappings.find.
56
+ */
57
+ export declare function findInMappings(mappings: string, positions: readonly GeneratedPosition[]): Array<MappedSegment | null>;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Position lookups in a source map's `mappings`, without the memory of a
3
+ * general-purpose decoder.
4
+ *
5
+ * @jridgewell/trace-mapping decodes `mappings` into one small array per
6
+ * segment: about 14 bytes of heap per byte of JSON (a 5 MB map takes 67 MB).
7
+ * Symbolication only needs "which original position is at or before this
8
+ * generated one", so a map that is kept in the cache is decoded into one
9
+ * Int32Array (5 ints per segment: about 3 bytes per byte of JSON), and a map
10
+ * too big to keep is not decoded at all: its frames are resolved in one
11
+ * streaming pass over the string.
12
+ *
13
+ * Lookup semantics match trace-mapping's `originalPositionFor` (greatest
14
+ * lower bound): the first segment that starts at the frame's column, else
15
+ * the last segment before it on the same line; a segment without a source
16
+ * maps to nothing.
17
+ */
18
+ /** generated column, source, original line, original column, name. */
19
+ const FIELDS = 5;
20
+ const NONE = -1;
21
+ const BASE64 = new Int8Array(128).fill(-1);
22
+ for (const [i, ch] of [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"].entries()) {
23
+ BASE64[ch.charCodeAt(0)] = i;
24
+ }
25
+ /**
26
+ * Walk a mappings-style string — `;` between generated lines, `,` between
27
+ * groups of base64 VLQ values — calling `visit(line, values, count)` with
28
+ * each group's raw (relative) values. `values` is reused between calls and
29
+ * holds the first 6; `count` is how many the group had. Throws on
30
+ * malformed VLQ. Also reads `x_autter_scopes` (source-map-scopes.ts).
31
+ */
32
+ export function forEachVlqGroup(text, visit) {
33
+ const values = [0, 0, 0, 0, 0, 0];
34
+ let line = 0;
35
+ let i = 0;
36
+ while (i < text.length) {
37
+ const ch = text.charCodeAt(i);
38
+ if (ch === 59 /* ; */) {
39
+ line++;
40
+ i++;
41
+ continue;
42
+ }
43
+ if (ch === 44 /* , */) {
44
+ i++;
45
+ continue;
46
+ }
47
+ let count = 0;
48
+ while (i < text.length) {
49
+ const next = text.charCodeAt(i);
50
+ if (next === 44 || next === 59)
51
+ break;
52
+ let value = 0;
53
+ let shift = 0;
54
+ let digit;
55
+ do {
56
+ const code = text.charCodeAt(i++);
57
+ digit = code < 128 ? BASE64[code] : -1;
58
+ if (digit < 0 || shift > 30)
59
+ throw new Error("invalid VLQ");
60
+ value |= (digit & 31) << shift;
61
+ shift += 5;
62
+ } while (digit & 32);
63
+ if (count < values.length)
64
+ values[count] = value & 1 ? -(value >>> 1) : value >>> 1;
65
+ count++;
66
+ }
67
+ visit(line, values, count);
68
+ }
69
+ }
70
+ /**
71
+ * Walk every segment: `visit(line, generatedColumn, source, originalLine,
72
+ * originalColumn, name)`, with source and name -1 when absent. Throws on
73
+ * malformed VLQ.
74
+ */
75
+ function forEachSegment(mappings, visit) {
76
+ let currentLine = 0;
77
+ let column = 0;
78
+ let source = 0;
79
+ let originalLine = 0;
80
+ let originalColumn = 0;
81
+ let name = 0;
82
+ forEachVlqGroup(mappings, (line, values, count) => {
83
+ // The generated column restarts on each line; the other fields don't.
84
+ if (line !== currentLine) {
85
+ currentLine = line;
86
+ column = 0;
87
+ }
88
+ column += values[0];
89
+ if (count >= 4) {
90
+ source += values[1];
91
+ originalLine += values[2];
92
+ originalColumn += values[3];
93
+ }
94
+ if (count >= 5)
95
+ name += values[4];
96
+ visit(line, column, count >= 4 ? source : NONE, originalLine, originalColumn, count >= 5 ? name : NONE);
97
+ });
98
+ }
99
+ /** Segments and generated lines in `mappings`, from one scan without decoding. */
100
+ function countSegments(mappings) {
101
+ let segments = 0;
102
+ let lines = 1;
103
+ let inSegment = false;
104
+ for (let i = 0; i < mappings.length; i++) {
105
+ const ch = mappings.charCodeAt(i);
106
+ if (ch === 59) {
107
+ lines++;
108
+ inSegment = false;
109
+ }
110
+ else if (ch === 44) {
111
+ inSegment = false;
112
+ }
113
+ else if (!inSegment) {
114
+ segments++;
115
+ inSegment = true;
116
+ }
117
+ }
118
+ return { segments, lines };
119
+ }
120
+ /** Heap a DecodedMappings of `mappings` would take, without decoding it. */
121
+ export function decodedMappingsBytes(mappings) {
122
+ const { segments, lines } = countSegments(mappings);
123
+ return segments * FIELDS * 4 + (lines + 1) * 4 + 64;
124
+ }
125
+ export class DecodedMappings {
126
+ segments;
127
+ lineStarts;
128
+ constructor(
129
+ /** FIELDS ints per segment, each line's segments sorted by generated column. */
130
+ segments,
131
+ /** Index of each line's first segment; one extra entry closes the last line. */
132
+ lineStarts) {
133
+ this.segments = segments;
134
+ this.lineStarts = lineStarts;
135
+ }
136
+ static decode(mappings) {
137
+ const { segments: count, lines } = countSegments(mappings);
138
+ const segments = new Int32Array(count * FIELDS);
139
+ const lineStarts = new Int32Array(lines + 1);
140
+ let n = 0;
141
+ let currentLine = 0;
142
+ forEachSegment(mappings, (line, column, source, originalLine, originalColumn, name) => {
143
+ while (currentLine < line)
144
+ lineStarts[++currentLine] = n;
145
+ const at = n * FIELDS;
146
+ segments[at] = column;
147
+ segments[at + 1] = source;
148
+ segments[at + 2] = originalLine;
149
+ segments[at + 3] = originalColumn;
150
+ segments[at + 4] = name;
151
+ n++;
152
+ });
153
+ while (currentLine < lines)
154
+ lineStarts[++currentLine] = n;
155
+ const decoded = new DecodedMappings(segments, lineStarts);
156
+ for (let line = 0; line < lines; line++)
157
+ decoded.sortLine(line);
158
+ return decoded;
159
+ }
160
+ get bytes() {
161
+ return this.segments.byteLength + this.lineStarts.byteLength + 64;
162
+ }
163
+ /** Generators almost always emit a line's segments in column order; sort the rare line that isn't (stable, as trace-mapping does). */
164
+ sortLine(line) {
165
+ const start = this.lineStarts[line];
166
+ const end = this.lineStarts[line + 1];
167
+ const s = this.segments;
168
+ let sorted = true;
169
+ for (let i = start + 1; i < end && sorted; i++)
170
+ sorted = s[(i - 1) * FIELDS] <= s[i * FIELDS];
171
+ if (sorted)
172
+ return;
173
+ const rows = [];
174
+ for (let i = start; i < end; i++)
175
+ rows.push(Array.from(s.subarray(i * FIELDS, (i + 1) * FIELDS)));
176
+ rows.sort((a, b) => a[0] - b[0]);
177
+ rows.forEach((row, k) => s.set(row, (start + k) * FIELDS));
178
+ }
179
+ /** The original position at a 0-based generated (line, column), or null. */
180
+ find(line, column) {
181
+ if (line < 0 || line + 1 >= this.lineStarts.length)
182
+ return null;
183
+ const s = this.segments;
184
+ let lo = this.lineStarts[line];
185
+ let hi = this.lineStarts[line + 1] - 1;
186
+ let found = -1;
187
+ while (lo <= hi) {
188
+ const mid = (lo + hi) >> 1;
189
+ if (s[mid * FIELDS] <= column) {
190
+ found = mid;
191
+ lo = mid + 1;
192
+ }
193
+ else {
194
+ hi = mid - 1;
195
+ }
196
+ }
197
+ if (found < 0)
198
+ return null;
199
+ // Several segments at exactly this column: the first one (before it,
200
+ // the last one — as trace-mapping does).
201
+ if (s[found * FIELDS] === column) {
202
+ while (found > this.lineStarts[line] && s[(found - 1) * FIELDS] === column)
203
+ found--;
204
+ }
205
+ const at = found * FIELDS;
206
+ if (s[at + 1] === NONE)
207
+ return null;
208
+ return { source: s[at + 1], line: s[at + 2], column: s[at + 3], name: s[at + 4] };
209
+ }
210
+ }
211
+ /**
212
+ * Resolve several positions in one pass over `mappings` without decoding
213
+ * it — for maps too big to keep. Same results as DecodedMappings.find.
214
+ */
215
+ export function findInMappings(mappings, positions) {
216
+ const byLine = new Map();
217
+ positions.forEach((position, index) => {
218
+ const list = byLine.get(position.line);
219
+ if (list)
220
+ list.push(index);
221
+ else
222
+ byLine.set(position.line, [index]);
223
+ });
224
+ const best = positions.map(() => null);
225
+ forEachSegment(mappings, (line, column, source, originalLine, originalColumn, name) => {
226
+ const wanted = byLine.get(line);
227
+ if (!wanted)
228
+ return;
229
+ for (const index of wanted) {
230
+ const target = positions[index].column;
231
+ const current = best[index];
232
+ // Greatest column at or before the position. On a tie the first
233
+ // segment wins at the position itself and the last one before it.
234
+ if (column > target)
235
+ continue;
236
+ if (current && (current.column > column || (current.column === column && column === target)))
237
+ continue;
238
+ best[index] = {
239
+ column,
240
+ segment: source === NONE ? null : { source, line: originalLine, column: originalColumn, name },
241
+ };
242
+ }
243
+ });
244
+ return best.map((found) => found?.segment ?? null);
245
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Function scopes uploaded with a source map (`x_autter_scopes`, written by
3
+ * `autter-runtime sourcemaps upload` in @autter/runtime-cli after 1.0.0):
4
+ * every function of the bundle as served, with its generated range and its
5
+ * ORIGINAL name. A frame is named after the innermost function that
6
+ * contains it — the name of the function it is in, as Sentry's
7
+ * js-source-scopes and PostHog cymbal's `token.scope()` do — instead of the
8
+ * name mapped at its caller's position, which is wrong whenever the call
9
+ * goes through a variable or a framework.
10
+ *
11
+ * Shape: `{ version: 1, names: (string | null)[], scopes: string }`.
12
+ * `scopes` is encoded like `mappings`: base64 VLQ, `;` between generated
13
+ * lines (the line each scope starts on), `,` between the scopes that start
14
+ * on one line, in start order (outer first on a tie). Each scope is 3 or 4
15
+ * values, 0-based and in UTF-16 columns:
16
+ *
17
+ * 1. start column, relative to the previous scope's start on the same
18
+ * line (absolute for a line's first scope);
19
+ * 2. end line, relative to the start line;
20
+ * 3. end column (exclusive), relative to the start column when the scope
21
+ * ends on its start line, else absolute;
22
+ * 4. the name, when the function has one: an index into the map's own
23
+ * `names` followed by `x_autter_scopes.names`, relative to the last
24
+ * name index written (starting from 0). A `null` entry in
25
+ * `x_autter_scopes.names` is a name the CLI could not map back.
26
+ *
27
+ * A 3-value scope is anonymous. Anything else (wrong version, bad VLQ,
28
+ * out-of-range names, ranges that overlap without nesting) makes the whole
29
+ * field unusable, and the map is used as if it had none.
30
+ */
31
+ export declare const SCOPES_KEY = "x_autter_scopes";
32
+ export type ScopeName =
33
+ /** In a function with a known original name. */
34
+ {
35
+ kind: "named";
36
+ name: string;
37
+ }
38
+ /** In an anonymous function, or in no function at all (top-level code). */
39
+ | {
40
+ kind: "anonymous";
41
+ }
42
+ /** In a named function whose original name is unknown. */
43
+ | {
44
+ kind: "unknown";
45
+ };
46
+ export declare class ScopeIndex {
47
+ /** STRIDE ints per scope, sorted by start (outer first on a tie). */
48
+ private readonly table;
49
+ /** The map's own `names`, then `x_autter_scopes.names`. */
50
+ private readonly mapNames;
51
+ private readonly extraNames;
52
+ private constructor();
53
+ /** Number of function scopes. */
54
+ get size(): number;
55
+ /** Estimated memory, for the source map cache's byte budget (the map's
56
+ * own `names` are counted with the map). */
57
+ get bytes(): number;
58
+ /**
59
+ * The parsed `x_autter_scopes` value, or null when it is absent or
60
+ * malformed. `mapNames` is the map's own `names` array, which scope name
61
+ * indexes continue.
62
+ */
63
+ static from(raw: unknown, mapNames?: readonly unknown[]): ScopeIndex | null;
64
+ private nameOf;
65
+ /**
66
+ * The name for a frame at a 0-based generated (line, column): the
67
+ * innermost function containing it. Outside every function (top-level
68
+ * code) it is anonymous.
69
+ */
70
+ nameAt(line: number, column: number): ScopeName;
71
+ }