@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.
- package/README.md +1 -0
- package/dist/breadcrumbs.d.ts +41 -0
- package/dist/breadcrumbs.js +151 -0
- package/dist/clickhouse.d.ts +10 -6
- package/dist/clickhouse.js +38 -5
- package/dist/compat-manifest.json +29 -0
- package/dist/compat.d.ts +3 -1
- package/dist/compat.js +3 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +1 -0
- package/dist/context.js +4 -1
- package/dist/fingerprint-override.d.ts +42 -0
- package/dist/fingerprint-override.js +76 -0
- package/dist/fingerprint-v3.d.ts +54 -0
- package/dist/fingerprint-v3.js +96 -0
- package/dist/fingerprint.d.ts +25 -0
- package/dist/fingerprint.js +1 -1
- package/dist/logs.js +2 -0
- package/dist/memory.d.ts +2 -2
- package/dist/migrations.d.ts +10 -0
- package/dist/migrations.js +32 -1
- package/dist/normalize-browser.d.ts +67 -34
- package/dist/normalize-browser.js +24 -1
- package/dist/normalize-otlp.js +4 -0
- package/dist/server.js +94 -18
- package/dist/sink.d.ts +5 -1
- package/dist/sink.js +13 -2
- package/dist/source-map-mappings.d.ts +57 -0
- package/dist/source-map-mappings.js +245 -0
- package/dist/source-map-scopes.d.ts +71 -0
- package/dist/source-map-scopes.js +190 -0
- package/dist/source-maps-lookup.d.ts +31 -0
- package/dist/source-maps-lookup.js +64 -0
- package/dist/source-maps.d.ts +72 -2
- package/dist/source-maps.js +134 -14
- package/dist/stack-fixtures.d.ts +10 -2
- package/dist/stack-fixtures.js +13 -3
- package/dist/stack-frames.d.ts +48 -0
- package/dist/stack-frames.js +441 -0
- package/dist/symbolicate.d.ts +117 -0
- package/dist/symbolicate.js +363 -0
- package/dist/types.d.ts +68 -0
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
307
|
-
if (!
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
+
}
|