@autter/otlp-ingester 1.10.0 → 1.11.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.
- package/README.md +1 -1
- package/dist/cause-chain.d.ts +32 -0
- package/dist/cause-chain.js +122 -0
- package/dist/clickhouse.js +31 -2
- package/dist/compat-manifest.json +29 -0
- package/dist/fingerprint-v3.d.ts +63 -7
- package/dist/fingerprint-v3.js +96 -7
- package/dist/ingest-drops.d.ts +10 -0
- package/dist/ingest-drops.js +13 -0
- package/dist/latency.d.ts +42 -0
- package/dist/latency.js +80 -0
- package/dist/log-promotion.js +20 -1
- package/dist/logs.d.ts +3 -3
- package/dist/logs.js +13 -4
- package/dist/migrations.d.ts +3 -2
- package/dist/migrations.js +12 -3
- package/dist/normalize-browser.d.ts +12 -12
- package/dist/normalize-browser.js +17 -3
- package/dist/normalize-otlp.js +39 -5
- package/dist/server.js +56 -18
- package/dist/sink.d.ts +5 -2
- package/dist/sink.js +7 -3
- package/dist/source-maps-lookup.d.ts +14 -1
- package/dist/source-maps-lookup.js +13 -3
- package/dist/source-maps.d.ts +49 -6
- package/dist/source-maps.js +122 -12
- package/dist/symbolicate.d.ts +83 -11
- package/dist/symbolicate.js +183 -31
- package/dist/trace-dedupe.d.ts +1 -1
- package/dist/trace-dedupe.js +3 -1
- package/dist/types.d.ts +26 -11
- package/package.json +1 -1
package/dist/sink.js
CHANGED
|
@@ -120,7 +120,8 @@ export class SinkForwarder {
|
|
|
120
120
|
orgId: ctx.orgId,
|
|
121
121
|
repositoryId: ctx.repositoryId,
|
|
122
122
|
occurrences: occurrences.map(sinkOccurrence),
|
|
123
|
-
|
|
123
|
+
// Latency samples feed runtime_route_latency_1m only (IP-02).
|
|
124
|
+
metrics: metricPoints.map(({ latency: _latency, ...p }) => ({
|
|
124
125
|
...p,
|
|
125
126
|
bucketAt: p.bucketAt.toISOString(),
|
|
126
127
|
})),
|
|
@@ -322,8 +323,11 @@ export class SinkForwarder {
|
|
|
322
323
|
* keep their legacy meaning. 1.9.0 adds `count`: the occurrences this
|
|
323
324
|
* record stands for, sent only when above 1 (a loop-collapse sample:
|
|
324
325
|
* 1 + collapsedCount, see loop-collapse.ts); consumers add `count ?? 1` to
|
|
325
|
-
* their event counts.
|
|
326
|
-
*
|
|
326
|
+
* their event counts. Unreleased (RF-06): `fingerprints` may end with a
|
|
327
|
+
* `cause-v1` candidate after the legacy one; consumers that do not know the
|
|
328
|
+
* scheme drop it (the platform validates schemes one by one). RF-05: in-app
|
|
329
|
+
* frames' context lines are up to 5 on each side (3 before). Older consumers
|
|
330
|
+
* ignore unknown keys, so the version stays 1.
|
|
327
331
|
*/
|
|
328
332
|
export function sinkOccurrence(o) {
|
|
329
333
|
const { traceId, route, method, statusCode, errorCode, why, fix, link, expected, requestId, breadcrumbs, frames, fingerprints, fingerprintRecord,
|
|
@@ -11,8 +11,21 @@ export interface SourceMapKey {
|
|
|
11
11
|
release: string | null;
|
|
12
12
|
filename: string;
|
|
13
13
|
debugId?: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Server frames (RF-05): `filename` is the path the process ran the
|
|
16
|
+
* bundle from (`/app/dist/server.js`, `/var/task/.next/server/app/page.js`),
|
|
17
|
+
* which the upload can't know. Without a debug id, the release's map
|
|
18
|
+
* whose stored path is the longest suffix of it wins (`/server.js`,
|
|
19
|
+
* `/.next/server/app/page.js`); an exact path is the longest suffix of all.
|
|
20
|
+
*/
|
|
21
|
+
suffix?: boolean;
|
|
14
22
|
}
|
|
15
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The stored map JSON for a minified file: by debug id first when given,
|
|
25
|
+
* else by release + filename (path only, no origin/query) — for a server
|
|
26
|
+
* frame (`suffix`), the release's map stored under the longest suffix of
|
|
27
|
+
* its path. Null when none.
|
|
28
|
+
*/
|
|
16
29
|
export declare function lookupSourceMap(client: ClickHouseClient, database: string, key: SourceMapKey): Promise<string | null>;
|
|
17
30
|
/**
|
|
18
31
|
* Re-uploading a map refreshes its 30-day TTL, so a map only counts as
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { normalizeDebugId, sourceMapPath, } from "./source-maps.js";
|
|
2
|
-
async function firstMap(client, database, where, params) {
|
|
2
|
+
async function firstMap(client, database, where, params, order = "uploaded_at DESC") {
|
|
3
3
|
const rows = await client.query({
|
|
4
4
|
query: `SELECT map FROM ${database}.runtime_source_maps
|
|
5
5
|
WHERE org_id = {org:String} AND repository_id = {repo:String} AND ${where}
|
|
6
|
-
ORDER BY
|
|
6
|
+
ORDER BY ${order} LIMIT 1`,
|
|
7
7
|
query_params: params,
|
|
8
8
|
format: "JSONEachRow",
|
|
9
9
|
});
|
|
10
10
|
const [row] = await rows.json();
|
|
11
11
|
return row?.map ?? null;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The stored map JSON for a minified file: by debug id first when given,
|
|
15
|
+
* else by release + filename (path only, no origin/query) — for a server
|
|
16
|
+
* frame (`suffix`), the release's map stored under the longest suffix of
|
|
17
|
+
* its path. Null when none.
|
|
18
|
+
*/
|
|
14
19
|
export async function lookupSourceMap(client, database, key) {
|
|
15
20
|
const tenant = { org: key.orgId, repo: key.repositoryId };
|
|
16
21
|
// A debug id names the exact bundle bytes, so it matches whatever release
|
|
@@ -25,6 +30,11 @@ export async function lookupSourceMap(client, database, key) {
|
|
|
25
30
|
const filename = sourceMapPath(key.filename);
|
|
26
31
|
if (!key.release || !filename)
|
|
27
32
|
return null;
|
|
33
|
+
if (key.suffix) {
|
|
34
|
+
// Stored paths always start with "/", so a suffix match ends on a
|
|
35
|
+
// path segment boundary. The release scan is a primary-key range.
|
|
36
|
+
return firstMap(client, database, "release = {release:String} AND endsWith({filename:String}, filename)", { ...tenant, release: key.release, filename }, "length(filename) DESC, uploaded_at DESC");
|
|
37
|
+
}
|
|
28
38
|
return firstMap(client, database, "release = {release:String} AND filename = {filename:String}", { ...tenant, release: key.release, filename });
|
|
29
39
|
}
|
|
30
40
|
/**
|
package/dist/source-maps.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Largest stored map
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Largest stored map, in characters: 32 MiB, room for vendor chunks of
|
|
3
|
+
* about 20 MB of minified code. Storage is a ZSTD-compressed String column,
|
|
4
|
+
* so size only costs disk; a map this big is still symbolicated, without
|
|
5
|
+
* being kept in the parse cache unless SOURCE_MAP_CACHE_MB allows it
|
|
6
|
+
* (symbolicate.ts). Was 5 MiB up to 1.6.0. The map without
|
|
7
|
+
* `sourcesContent` must fit it; the source contents kept (RF-05) fit in
|
|
8
|
+
* what is left of it, counted in UTF-8 bytes (storedSourcesContent).
|
|
7
9
|
*/
|
|
8
10
|
export declare const SOURCE_MAP_MAX_CHARS: number;
|
|
9
11
|
/** `/v1/sourcemaps` body limit, counted after a gzip or deflate body is
|
|
@@ -28,7 +30,7 @@ export interface ValidSourceMap {
|
|
|
28
30
|
release: string;
|
|
29
31
|
/** sourceMapPath() of the uploaded filename. */
|
|
30
32
|
filename: string;
|
|
31
|
-
/** The map JSON
|
|
33
|
+
/** The map JSON as stored: `sourcesContent` bounded by storedSourceMapJson. */
|
|
32
34
|
map: string;
|
|
33
35
|
/** Lowercase debug id, or "" when the upload has none. */
|
|
34
36
|
debugId: string;
|
|
@@ -38,6 +40,30 @@ export interface ValidSourceMap {
|
|
|
38
40
|
/** The content hash of a stored map — `autter-runtime sourcemaps upload`
|
|
39
41
|
* computes the same value to skip maps the ingester already has. */
|
|
40
42
|
export declare function sourceMapContentHash(map: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* The `sourcesContent` a stored map keeps (RF-05), or null to leave the
|
|
45
|
+
* field out. Context lines for in-app frames come from it, so only in-app
|
|
46
|
+
* sources keep theirs: a source the map's `ignoreList` /
|
|
47
|
+
* `x_google_ignoreList` names, or whose path the in-app rule rejects
|
|
48
|
+
* (`node_modules/`, `/.pnpm/`, `/vendor/`, … — isInAppPath), gets `null`,
|
|
49
|
+
* like any entry that is not a string.
|
|
50
|
+
*
|
|
51
|
+
* The map must stay within the stored-map limit, so contents are kept
|
|
52
|
+
* whole, smallest first (ties in source order), while the whole JSON fits
|
|
53
|
+
* `limit` UTF-8 bytes given the `baseBytes` of the map without the field;
|
|
54
|
+
* the rest become `null`. Nothing is ever cut inside a file, so kept line
|
|
55
|
+
* numbers stay right. `@autter/runtime-cli` (upload.ts storedMapJson) does
|
|
56
|
+
* exactly this, so it hashes the map the ingester stores and skips it on
|
|
57
|
+
* the next run.
|
|
58
|
+
*/
|
|
59
|
+
export declare function storedSourcesContent(fields: Record<string, unknown>, baseBytes: number, limit?: number): Array<string | null> | null;
|
|
60
|
+
/**
|
|
61
|
+
* The stored JSON of a map: every field as sent except `sourcesContent`,
|
|
62
|
+
* which goes last, bounded by storedSourcesContent (left out when nothing
|
|
63
|
+
* is kept, so a map without in-app contents is stored byte-for-byte as it
|
|
64
|
+
* was before contents were kept).
|
|
65
|
+
*/
|
|
66
|
+
export declare function storedSourceMapJson(fields: Record<string, unknown>, limit?: number): string;
|
|
41
67
|
/**
|
|
42
68
|
* Validate one `/v1/sourcemaps` upload: `{ release, filename, map, debugId? }`.
|
|
43
69
|
* `map` may be an object or a JSON string. The debug id comes from the body,
|
|
@@ -78,3 +104,20 @@ export declare const DEBUG_IDS_MAX = 50;
|
|
|
78
104
|
export declare function browserDebugIds(raw: Record<string, unknown> | undefined): {
|
|
79
105
|
debugIds?: Record<string, string>;
|
|
80
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* The attribute server SDKs put the debug ids of the bundles in an
|
|
109
|
+
* exception's stack under (`@autter/runtime-core` errorAttributes, RF-05):
|
|
110
|
+
* a JSON object of script path, as it appears in the stack → debug id.
|
|
111
|
+
*/
|
|
112
|
+
export declare const DEBUG_IDS_ATTRIBUTE = "autter.debug_ids";
|
|
113
|
+
/**
|
|
114
|
+
* `{ debugIds }` from a server exception's `autter.debug_ids` (a JSON
|
|
115
|
+
* string, or the object the sanitiser made of it), validated exactly like a
|
|
116
|
+
* browser event's (browserDebugIds); `{}` when absent or malformed. Only
|
|
117
|
+
* locates source maps: like the browser's, never stored or forwarded.
|
|
118
|
+
*/
|
|
119
|
+
export declare function serverDebugIds(raw: unknown): {
|
|
120
|
+
debugIds?: Record<string, string>;
|
|
121
|
+
};
|
|
122
|
+
/** `attributes` without `autter.debug_ids`, which locates maps and is not kept. */
|
|
123
|
+
export declare function withoutDebugIdsAttribute(attributes: Record<string, unknown>): Record<string, unknown>;
|
package/dist/source-maps.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { scrubText } from "./redact.js";
|
|
3
|
+
import { isInAppPath } from "./stack-frames.js";
|
|
3
4
|
/**
|
|
4
|
-
* Largest stored map
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Largest stored map, in characters: 32 MiB, room for vendor chunks of
|
|
6
|
+
* about 20 MB of minified code. Storage is a ZSTD-compressed String column,
|
|
7
|
+
* so size only costs disk; a map this big is still symbolicated, without
|
|
8
|
+
* being kept in the parse cache unless SOURCE_MAP_CACHE_MB allows it
|
|
9
|
+
* (symbolicate.ts). Was 5 MiB up to 1.6.0. The map without
|
|
10
|
+
* `sourcesContent` must fit it; the source contents kept (RF-05) fit in
|
|
11
|
+
* what is left of it, counted in UTF-8 bytes (storedSourcesContent).
|
|
9
12
|
*/
|
|
10
13
|
export const SOURCE_MAP_MAX_CHARS = 32 * 1024 * 1024;
|
|
11
14
|
/** `/v1/sourcemaps` body limit, counted after a gzip or deflate body is
|
|
@@ -62,6 +65,78 @@ const RELEASE_RE = /^[a-zA-Z0-9._-]{1,200}$/;
|
|
|
62
65
|
export function sourceMapContentHash(map) {
|
|
63
66
|
return createHash("sha256").update(map).digest("hex");
|
|
64
67
|
}
|
|
68
|
+
/** UTF-8 bytes `,"sourcesContent":[` and `]` add around the entries. */
|
|
69
|
+
const SOURCES_CONTENT_WRAPPER_BYTES = Buffer.byteLength(',"sourcesContent":[]');
|
|
70
|
+
/** UTF-8 bytes of a `null` entry. */
|
|
71
|
+
const NULL_ENTRY_BYTES = 4;
|
|
72
|
+
/**
|
|
73
|
+
* The `sourcesContent` a stored map keeps (RF-05), or null to leave the
|
|
74
|
+
* field out. Context lines for in-app frames come from it, so only in-app
|
|
75
|
+
* sources keep theirs: a source the map's `ignoreList` /
|
|
76
|
+
* `x_google_ignoreList` names, or whose path the in-app rule rejects
|
|
77
|
+
* (`node_modules/`, `/.pnpm/`, `/vendor/`, … — isInAppPath), gets `null`,
|
|
78
|
+
* like any entry that is not a string.
|
|
79
|
+
*
|
|
80
|
+
* The map must stay within the stored-map limit, so contents are kept
|
|
81
|
+
* whole, smallest first (ties in source order), while the whole JSON fits
|
|
82
|
+
* `limit` UTF-8 bytes given the `baseBytes` of the map without the field;
|
|
83
|
+
* the rest become `null`. Nothing is ever cut inside a file, so kept line
|
|
84
|
+
* numbers stay right. `@autter/runtime-cli` (upload.ts storedMapJson) does
|
|
85
|
+
* exactly this, so it hashes the map the ingester stores and skips it on
|
|
86
|
+
* the next run.
|
|
87
|
+
*/
|
|
88
|
+
export function storedSourcesContent(fields, baseBytes, limit = SOURCE_MAP_MAX_CHARS) {
|
|
89
|
+
const { sources, sourcesContent } = fields;
|
|
90
|
+
if (!Array.isArray(sources) || !Array.isArray(sourcesContent) || sources.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const ignored = new Set();
|
|
93
|
+
for (const list of [fields.ignoreList, fields.x_google_ignoreList]) {
|
|
94
|
+
if (!Array.isArray(list))
|
|
95
|
+
continue;
|
|
96
|
+
for (const index of list)
|
|
97
|
+
if (Number.isInteger(index))
|
|
98
|
+
ignored.add(index);
|
|
99
|
+
}
|
|
100
|
+
const count = sources.length;
|
|
101
|
+
const candidates = [];
|
|
102
|
+
for (let index = 0; index < count; index++) {
|
|
103
|
+
const source = sources[index];
|
|
104
|
+
const content = sourcesContent[index];
|
|
105
|
+
if (typeof source !== "string" || typeof content !== "string")
|
|
106
|
+
continue;
|
|
107
|
+
if (ignored.has(index) || !isInAppPath(source))
|
|
108
|
+
continue;
|
|
109
|
+
candidates.push({ index, content, bytes: Buffer.byteLength(JSON.stringify(content)) });
|
|
110
|
+
}
|
|
111
|
+
if (candidates.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
candidates.sort((a, b) => a.bytes - b.bytes || a.index - b.index);
|
|
114
|
+
// Every entry starts as `null`, with a comma between entries.
|
|
115
|
+
let remaining = limit - baseBytes - SOURCES_CONTENT_WRAPPER_BYTES - NULL_ENTRY_BYTES * count - (count - 1);
|
|
116
|
+
const kept = new Array(count).fill(null);
|
|
117
|
+
let any = false;
|
|
118
|
+
for (const candidate of candidates) {
|
|
119
|
+
const extra = candidate.bytes - NULL_ENTRY_BYTES;
|
|
120
|
+
if (extra > remaining)
|
|
121
|
+
break;
|
|
122
|
+
remaining -= extra;
|
|
123
|
+
kept[candidate.index] = candidate.content;
|
|
124
|
+
any = true;
|
|
125
|
+
}
|
|
126
|
+
return any ? kept : null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The stored JSON of a map: every field as sent except `sourcesContent`,
|
|
130
|
+
* which goes last, bounded by storedSourcesContent (left out when nothing
|
|
131
|
+
* is kept, so a map without in-app contents is stored byte-for-byte as it
|
|
132
|
+
* was before contents were kept).
|
|
133
|
+
*/
|
|
134
|
+
export function storedSourceMapJson(fields, limit = SOURCE_MAP_MAX_CHARS) {
|
|
135
|
+
const { sourcesContent: _content, ...rest } = fields;
|
|
136
|
+
const base = JSON.stringify(rest);
|
|
137
|
+
const content = storedSourcesContent(fields, Buffer.byteLength(base), limit);
|
|
138
|
+
return content ? JSON.stringify({ ...rest, sourcesContent: content }) : base;
|
|
139
|
+
}
|
|
65
140
|
/**
|
|
66
141
|
* Validate one `/v1/sourcemaps` upload: `{ release, filename, map, debugId? }`.
|
|
67
142
|
* `map` may be an object or a JSON string. The debug id comes from the body,
|
|
@@ -92,14 +167,16 @@ export function validateSourceMap(body) {
|
|
|
92
167
|
const debugId = rawDebugId === undefined || rawDebugId === null || rawDebugId === "" ? "" : normalizeDebugId(rawDebugId);
|
|
93
168
|
if (debugId === null)
|
|
94
169
|
return null;
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
// Everything is stored as sent, extension fields included: symbolication
|
|
171
|
+
// reads `x_autter_scopes` (function scopes) from it, and the CLI hashes
|
|
172
|
+
// exactly this JSON to skip maps already uploaded. `sourcesContent` is
|
|
173
|
+
// kept for in-app sources (RF-05: context lines around in-app frames),
|
|
174
|
+
// within the stored-map limit; the lines taken from it are scrubbed when
|
|
175
|
+
// they are attached to an occurrence (symbolicate.ts).
|
|
176
|
+
const { sourcesContent: _content, ...withoutContent } = parsed;
|
|
177
|
+
if (JSON.stringify(withoutContent).length > SOURCE_MAP_MAX_CHARS)
|
|
102
178
|
return null;
|
|
179
|
+
const map = storedSourceMapJson(parsed);
|
|
103
180
|
return { release: input.release, filename, map, debugId, contentHash: sourceMapContentHash(map) };
|
|
104
181
|
}
|
|
105
182
|
/** Most entries one check request may name. */
|
|
@@ -167,3 +244,36 @@ export function browserDebugIds(raw) {
|
|
|
167
244
|
}
|
|
168
245
|
return count ? { debugIds } : {};
|
|
169
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* The attribute server SDKs put the debug ids of the bundles in an
|
|
249
|
+
* exception's stack under (`@autter/runtime-core` errorAttributes, RF-05):
|
|
250
|
+
* a JSON object of script path, as it appears in the stack → debug id.
|
|
251
|
+
*/
|
|
252
|
+
export const DEBUG_IDS_ATTRIBUTE = "autter.debug_ids";
|
|
253
|
+
/**
|
|
254
|
+
* `{ debugIds }` from a server exception's `autter.debug_ids` (a JSON
|
|
255
|
+
* string, or the object the sanitiser made of it), validated exactly like a
|
|
256
|
+
* browser event's (browserDebugIds); `{}` when absent or malformed. Only
|
|
257
|
+
* locates source maps: like the browser's, never stored or forwarded.
|
|
258
|
+
*/
|
|
259
|
+
export function serverDebugIds(raw) {
|
|
260
|
+
let value = raw;
|
|
261
|
+
if (typeof value === "string") {
|
|
262
|
+
try {
|
|
263
|
+
value = JSON.parse(value);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
270
|
+
return {};
|
|
271
|
+
return browserDebugIds(value);
|
|
272
|
+
}
|
|
273
|
+
/** `attributes` without `autter.debug_ids`, which locates maps and is not kept. */
|
|
274
|
+
export function withoutDebugIdsAttribute(attributes) {
|
|
275
|
+
if (!(DEBUG_IDS_ATTRIBUTE in attributes))
|
|
276
|
+
return attributes;
|
|
277
|
+
const { [DEBUG_IDS_ATTRIBUTE]: _ids, ...rest } = attributes;
|
|
278
|
+
return rest;
|
|
279
|
+
}
|
package/dist/symbolicate.d.ts
CHANGED
|
@@ -4,10 +4,19 @@ import { ScopeIndex } from "./source-map-scopes.js";
|
|
|
4
4
|
import type { SourceMapKey } from "./source-maps-lookup.js";
|
|
5
5
|
import type { StackFrame } from "./types.js";
|
|
6
6
|
/**
|
|
7
|
-
* Symbolication at ingest: minified browser frames
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `index-5f3a9c1e.js` — and an issue
|
|
7
|
+
* Symbolication at ingest: minified browser frames, and bundled Node and
|
|
8
|
+
* edge frames (RF-05), are resolved through the source maps uploaded for
|
|
9
|
+
* the release (`/v1/sourcemaps`) BEFORE they are fingerprinted, so
|
|
10
|
+
* `frames-v3` hashes `checkout.tsx`, not `index-5f3a9c1e.js` — and an issue
|
|
11
|
+
* survives deploys and preview domains.
|
|
12
|
+
*
|
|
13
|
+
* - Browser frames find their map by debug id, else by release + the
|
|
14
|
+
* script's path. Server frames (`server: true`) by debug id (server SDKs
|
|
15
|
+
* send `autter.debug_ids`), else by the release's map stored
|
|
16
|
+
* under the longest suffix of the path the process ran the bundle from
|
|
17
|
+
* (source-maps-lookup.ts): the upload knows `/server.js`, the stack says
|
|
18
|
+
* `/app/dist/server.js`. Only in-app server frames are looked up — a
|
|
19
|
+
* `node_modules/` or `node:` frame never belongs to an uploaded bundle.
|
|
11
20
|
*
|
|
12
21
|
* - Function names: when the map carries function scopes
|
|
13
22
|
* (`x_autter_scopes`, uploaded by @autter/runtime-cli after 1.0.0), a
|
|
@@ -22,24 +31,38 @@ import type { StackFrame } from "./types.js";
|
|
|
22
31
|
* and for handlers a framework calls, which is why scopes exist.
|
|
23
32
|
* - Sources the map lists in `ignoreList` / `x_google_ignoreList` are not
|
|
24
33
|
* in-app; a resolved path under node_modules/ etc. is not in-app either.
|
|
25
|
-
* - Context lines
|
|
26
|
-
*
|
|
34
|
+
* - Context lines (RF-05): an in-app resolved frame gets its original line
|
|
35
|
+
* and up to CONTEXT_LINES lines before and after it, each cut to 300
|
|
36
|
+
* characters and scrubbed like the stack, when the map carries the
|
|
37
|
+
* source's `sourcesContent` (kept at upload for in-app sources,
|
|
38
|
+
* source-maps.ts). Dependency frames get none.
|
|
27
39
|
* - Never fails ingest: any error leaves the frame as parsed.
|
|
28
40
|
*
|
|
29
41
|
* Parsed maps are cached per (org, repository, release, file) — or per debug
|
|
30
42
|
* id — in a byte-bounded LRU, so a busy issue never refetches its map. A map
|
|
31
43
|
* that would take more than half of that budget is used for its lookup and
|
|
32
44
|
* not kept (source-map-mappings.ts reads it without decoding it), so one huge
|
|
33
|
-
* vendor chunk cannot flush every other map.
|
|
45
|
+
* vendor chunk cannot flush every other map. A map's source contents are
|
|
46
|
+
* never part of its entry (it caches exactly as a map without them would):
|
|
47
|
+
* they sit in a separate store, one file per entry, bounded the same way and
|
|
48
|
+
* dropped with the map.
|
|
34
49
|
*/
|
|
35
50
|
export type MapFetcher = (key: SourceMapKey) => Promise<string | null>;
|
|
36
51
|
export interface SymbolicationScope {
|
|
37
52
|
orgId: string;
|
|
38
53
|
repositoryId: string;
|
|
39
54
|
release: string | null;
|
|
40
|
-
/** Script URL → debug id (EI-13), when the
|
|
55
|
+
/** Script URL (or, on a server, script path) → debug id (EI-13), when the SDK sent them. */
|
|
41
56
|
debugIds?: Record<string, string> | null;
|
|
57
|
+
/**
|
|
58
|
+
* A Node or edge stack (RF-05): only in-app frames are looked up, their
|
|
59
|
+
* paths are normalised (serverScriptPath), and without a debug id the
|
|
60
|
+
* release's map is found by path suffix.
|
|
61
|
+
*/
|
|
62
|
+
server?: boolean;
|
|
42
63
|
}
|
|
64
|
+
/** Lines kept before and after an in-app frame's own line (RF-05). */
|
|
65
|
+
export declare const CONTEXT_LINES = 5;
|
|
43
66
|
/**
|
|
44
67
|
* In-app browser frames a source map could resolve, counted after
|
|
45
68
|
* symbolication (EI-27): `resolved` ones, and `unresolved` .js/.mjs/.cjs
|
|
@@ -55,10 +78,25 @@ export declare function inAppFrameResolution(frames: readonly StackFrame[] | und
|
|
|
55
78
|
};
|
|
56
79
|
/** Path-only form of a script URL — the key uploads are stored under. */
|
|
57
80
|
export declare function scriptPath(filename: string): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* The path form of a server frame's file, comparable with stored upload
|
|
83
|
+
* paths: `file:///app/dist/index.mjs` → `/app/dist/index.mjs`,
|
|
84
|
+
* `C:\app\dist\index.js` → `/app/dist/index.js`, `index.js` (a Worker
|
|
85
|
+
* script) → `/index.js`. Null when it is not a path.
|
|
86
|
+
*/
|
|
87
|
+
export declare function serverScriptPath(filename: string): string | null;
|
|
58
88
|
export interface SourceMapCacheOptions {
|
|
59
89
|
/** Budget for the estimated in-memory size of parsed maps (default 64 MiB).
|
|
60
90
|
* A map that would take more than half of it is not cached. */
|
|
61
91
|
maxBytes?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Budget for the source contents of cached maps (RF-05; default:
|
|
94
|
+
* `maxBytes`). They are kept apart from the maps, one entry per source
|
|
95
|
+
* file and bounded the same way (least recently used first, a file over
|
|
96
|
+
* half the budget not kept), so a map caches exactly as it would without
|
|
97
|
+
* them.
|
|
98
|
+
*/
|
|
99
|
+
maxContentBytes?: number;
|
|
62
100
|
maxEntries?: number;
|
|
63
101
|
/** How long a found map is reused (default 1 h). */
|
|
64
102
|
hitTtlMs?: number;
|
|
@@ -70,22 +108,43 @@ export interface SourceMapCacheOptions {
|
|
|
70
108
|
}
|
|
71
109
|
/** A parsed map: its metadata, its function scopes, and position lookups. */
|
|
72
110
|
export interface LoadedSourceMap {
|
|
73
|
-
/** Sources, names
|
|
74
|
-
*
|
|
111
|
+
/** Sources, names and ignore list. Its own `mappings` are left empty
|
|
112
|
+
* (positions go through `lookup`), and it holds no `sourcesContent`
|
|
113
|
+
* (see `contentOf`). */
|
|
75
114
|
map: TraceMap;
|
|
76
115
|
scopes: ScopeIndex | null;
|
|
77
116
|
/** Original positions for 0-based generated positions (one call per stack). */
|
|
78
117
|
lookup(positions: readonly GeneratedPosition[]): Array<MappedSegment | null>;
|
|
79
|
-
/** Estimated heap: decoded mappings (when decoded), strings and scopes
|
|
118
|
+
/** Estimated heap: decoded mappings (when decoded), strings and scopes;
|
|
119
|
+
* source contents are not part of it. */
|
|
80
120
|
bytes: number;
|
|
81
121
|
/** False when the map was too big to keep, so its mappings were not decoded. */
|
|
82
122
|
cacheable: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The stored original source of `sources[index]` (in-app sources only),
|
|
125
|
+
* or null. For a cached map it is read from the cache's separate content
|
|
126
|
+
* store, so a content evicted there gives null (no context lines) until
|
|
127
|
+
* the map is parsed again.
|
|
128
|
+
*/
|
|
129
|
+
contentOf(index: number): string | null;
|
|
130
|
+
}
|
|
131
|
+
/** A parsed map and its source contents, which are never part of the map's cache entry. */
|
|
132
|
+
export interface ParsedSourceMap {
|
|
133
|
+
loaded: LoadedSourceMap;
|
|
134
|
+
/** `sourcesContent` as stored (aligned with `sources`), or null. */
|
|
135
|
+
contents: Array<string | null> | null;
|
|
83
136
|
}
|
|
84
137
|
export declare class SourceMapCache {
|
|
85
138
|
private readonly entries;
|
|
86
139
|
private readonly inFlight;
|
|
87
140
|
private weight;
|
|
141
|
+
/** Source contents of cached maps, keyed by parse id and source index. */
|
|
142
|
+
private readonly contents;
|
|
143
|
+
private contentWeight;
|
|
144
|
+
/** Distinguishes every parse, so a re-parsed (maybe re-uploaded) map never reads an older parse's contents. */
|
|
145
|
+
private parses;
|
|
88
146
|
private readonly maxBytes;
|
|
147
|
+
private readonly maxContentBytes;
|
|
89
148
|
private readonly maxEntries;
|
|
90
149
|
private readonly hitTtlMs;
|
|
91
150
|
private readonly missTtlMs;
|
|
@@ -100,9 +159,14 @@ export declare class SourceMapCache {
|
|
|
100
159
|
get size(): number;
|
|
101
160
|
/** Estimated bytes of the cached maps (what `maxBytes` bounds). */
|
|
102
161
|
get bytes(): number;
|
|
162
|
+
/** Estimated bytes of the cached source contents (what `maxContentBytes` bounds). */
|
|
163
|
+
get contentBytes(): number;
|
|
103
164
|
/** The parsed map for `key` (null when none), fetching at most once at a time. */
|
|
104
165
|
get(key: SourceMapKey, fetchMap: MapFetcher): Promise<LoadedSourceMap | null>;
|
|
105
166
|
private load;
|
|
167
|
+
private contentOf;
|
|
168
|
+
private storeContent;
|
|
169
|
+
private removeContent;
|
|
106
170
|
private store;
|
|
107
171
|
private remove;
|
|
108
172
|
}
|
|
@@ -113,7 +177,15 @@ export declare class SourceMapCache {
|
|
|
113
177
|
* once into a compact table; otherwise they are left encoded and each
|
|
114
178
|
* lookup reads them in one pass (`cacheable: false`). Throws when the map
|
|
115
179
|
* itself is unusable.
|
|
180
|
+
*
|
|
181
|
+
* `sourcesContent` (RF-05) is taken out before anything else: the TraceMap
|
|
182
|
+
* never holds it and `bytes` never counts it, so whether a map is decoded
|
|
183
|
+
* and cached is decided exactly as for a map uploaded without contents.
|
|
184
|
+
* They are returned beside the map; SourceMapCache keeps them in their own
|
|
185
|
+
* store.
|
|
116
186
|
*/
|
|
187
|
+
export declare function parseSourceMapWithContents(json: string, maxDecodedBytes?: number): ParsedSourceMap;
|
|
188
|
+
/** parseSourceMapWithContents, the map alone (its contents through `contentOf`). */
|
|
117
189
|
export declare function parseSourceMap(json: string, maxDecodedBytes?: number): LoadedSourceMap;
|
|
118
190
|
export declare class Symbolicator {
|
|
119
191
|
private readonly fetchMap;
|