@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/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
- metrics: metricPoints.map((p) => ({
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. Older consumers ignore unknown keys, so the version
326
- * stays 1.
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
- /** The stored map JSON for a minified file: by debug id first when given, else by release + filename (path only, no origin/query). Null when none. */
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 uploaded_at DESC LIMIT 1`,
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
- /** The stored map JSON for a minified file: by debug id first when given, else by release + filename (path only, no origin/query). Null when none. */
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
  /**
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Largest stored map (after `sourcesContent` is removed), in characters:
3
- * 32 MiB, room for vendor chunks of about 20 MB of minified code. Storage
4
- * is a ZSTD-compressed String column, so size only costs disk; a map this
5
- * big is still symbolicated, without being kept in the parse cache unless
6
- * SOURCE_MAP_CACHE_MB allows it (symbolicate.ts). Was 5 MiB up to 1.6.0.
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 without `sourcesContent`. */
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>;
@@ -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 (after `sourcesContent` is removed), in characters:
5
- * 32 MiB, room for vendor chunks of about 20 MB of minified code. Storage
6
- * is a ZSTD-compressed String column, so size only costs disk; a map this
7
- * big is still symbolicated, without being kept in the parse cache unless
8
- * SOURCE_MAP_CACHE_MB allows it (symbolicate.ts). Was 5 MiB up to 1.6.0.
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
- // Source contents are unnecessary for position lookup and may contain
96
- // secrets. Everything else is stored as sent, extension fields included:
97
- // symbolication reads `x_autter_scopes` (function scopes) from it, and the
98
- // CLI hashes exactly this JSON to skip maps already uploaded.
99
- const { sourcesContent: _omitted, ...safe } = parsed;
100
- const map = JSON.stringify(safe);
101
- if (map.length > SOURCE_MAP_MAX_CHARS)
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
+ }
@@ -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 are resolved through the
8
- * source maps uploaded for the release (`/v1/sourcemaps`) BEFORE they are
9
- * fingerprinted, so `frames-v3` hashes `checkout.tsx`, not
10
- * `index-5f3a9c1e.js` — and an issue survives deploys and preview domains.
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 only when the map carries `sourcesContent` (the upload
26
- * strips it today, so there are none for now).
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 browser sent them. */
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, ignore list and sourcesContent. Its own `mappings`
74
- * are left empty: positions go through `lookup`. */
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;