@autter/otlp-ingester 1.6.0 → 1.7.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.
@@ -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
+ }
@@ -0,0 +1,190 @@
1
+ import { forEachVlqGroup } from "./source-map-mappings.js";
2
+ /**
3
+ * Function scopes uploaded with a source map (`x_autter_scopes`, written by
4
+ * `autter-runtime sourcemaps upload` in @autter/runtime-cli after 1.0.0):
5
+ * every function of the bundle as served, with its generated range and its
6
+ * ORIGINAL name. A frame is named after the innermost function that
7
+ * contains it — the name of the function it is in, as Sentry's
8
+ * js-source-scopes and PostHog cymbal's `token.scope()` do — instead of the
9
+ * name mapped at its caller's position, which is wrong whenever the call
10
+ * goes through a variable or a framework.
11
+ *
12
+ * Shape: `{ version: 1, names: (string | null)[], scopes: string }`.
13
+ * `scopes` is encoded like `mappings`: base64 VLQ, `;` between generated
14
+ * lines (the line each scope starts on), `,` between the scopes that start
15
+ * on one line, in start order (outer first on a tie). Each scope is 3 or 4
16
+ * values, 0-based and in UTF-16 columns:
17
+ *
18
+ * 1. start column, relative to the previous scope's start on the same
19
+ * line (absolute for a line's first scope);
20
+ * 2. end line, relative to the start line;
21
+ * 3. end column (exclusive), relative to the start column when the scope
22
+ * ends on its start line, else absolute;
23
+ * 4. the name, when the function has one: an index into the map's own
24
+ * `names` followed by `x_autter_scopes.names`, relative to the last
25
+ * name index written (starting from 0). A `null` entry in
26
+ * `x_autter_scopes.names` is a name the CLI could not map back.
27
+ *
28
+ * A 3-value scope is anonymous. Anything else (wrong version, bad VLQ,
29
+ * out-of-range names, ranges that overlap without nesting) makes the whole
30
+ * field unusable, and the map is used as if it had none.
31
+ */
32
+ export const SCOPES_KEY = "x_autter_scopes";
33
+ const ANONYMOUS = -1;
34
+ /** startLine, startCol, endLine, endCol, nameIndex, parent. */
35
+ const STRIDE = 6;
36
+ const MAX_POSITION = 0x7fffffff;
37
+ const ANONYMOUS_SCOPE = { kind: "anonymous" };
38
+ const UNKNOWN_SCOPE = { kind: "unknown" };
39
+ class MalformedScopes extends Error {
40
+ }
41
+ /** a < b for (line, column) pairs. */
42
+ function before(lineA, colA, lineB, colB) {
43
+ return lineA < lineB || (lineA === lineB && colA < colB);
44
+ }
45
+ /** Decode the `scopes` string into [startLine, startCol, endLine, endCol, nameIndex] rows. */
46
+ function decodeScopes(text, isName) {
47
+ const rows = [];
48
+ let currentLine = 0;
49
+ let startCol = 0;
50
+ let name = 0;
51
+ forEachVlqGroup(text, (line, values, count) => {
52
+ if (count !== 3 && count !== 4)
53
+ throw new MalformedScopes("a scope has 3 or 4 values");
54
+ if (line !== currentLine) {
55
+ currentLine = line;
56
+ startCol = 0;
57
+ }
58
+ startCol += values[0];
59
+ const endLine = line + values[1];
60
+ const endCol = values[1] === 0 ? startCol + values[2] : values[2];
61
+ let nameIndex = ANONYMOUS;
62
+ if (count === 4) {
63
+ name += values[3];
64
+ if (!isName(name))
65
+ throw new MalformedScopes("name out of range");
66
+ nameIndex = name;
67
+ }
68
+ if (startCol < 0 || values[1] < 0 || endCol < 0 || endLine > MAX_POSITION || Math.max(startCol, endCol) > MAX_POSITION) {
69
+ throw new MalformedScopes("position out of range");
70
+ }
71
+ // Non-empty: a function is at least `()=>0`.
72
+ if (!before(line, startCol, endLine, endCol))
73
+ throw new MalformedScopes("empty scope");
74
+ rows.push([line, startCol, endLine, endCol, nameIndex]);
75
+ });
76
+ return rows;
77
+ }
78
+ export class ScopeIndex {
79
+ table;
80
+ mapNames;
81
+ extraNames;
82
+ constructor(
83
+ /** STRIDE ints per scope, sorted by start (outer first on a tie). */
84
+ table,
85
+ /** The map's own `names`, then `x_autter_scopes.names`. */
86
+ mapNames, extraNames) {
87
+ this.table = table;
88
+ this.mapNames = mapNames;
89
+ this.extraNames = extraNames;
90
+ }
91
+ /** Number of function scopes. */
92
+ get size() {
93
+ return this.table.length / STRIDE;
94
+ }
95
+ /** Estimated memory, for the source map cache's byte budget (the map's
96
+ * own `names` are counted with the map). */
97
+ get bytes() {
98
+ let bytes = this.table.byteLength + 64;
99
+ for (const name of this.extraNames)
100
+ bytes += 2 * (name?.length ?? 0) + 32;
101
+ return bytes;
102
+ }
103
+ /**
104
+ * The parsed `x_autter_scopes` value, or null when it is absent or
105
+ * malformed. `mapNames` is the map's own `names` array, which scope name
106
+ * indexes continue.
107
+ */
108
+ static from(raw, mapNames = []) {
109
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
110
+ return null;
111
+ const { version, names, scopes } = raw;
112
+ if (version !== 1 || !Array.isArray(names) || typeof scopes !== "string")
113
+ return null;
114
+ if (!names.every((name) => typeof name === "string" || name === null))
115
+ return null;
116
+ const extraNames = names;
117
+ const isName = (index) => index >= 0 &&
118
+ (index < mapNames.length
119
+ ? typeof mapNames[index] === "string"
120
+ : index - mapNames.length < extraNames.length);
121
+ let entries;
122
+ try {
123
+ entries = decodeScopes(scopes, isName);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ // Encoded in order already; sort anyway so a hand-written field works too.
129
+ entries.sort((a, b) => a[0] - b[0] || a[1] - b[1] || b[2] - a[2] || b[3] - a[3]);
130
+ const table = new Int32Array(entries.length * STRIDE);
131
+ // Open scopes, outermost first: each new scope either nests in the
132
+ // innermost one still open or starts after it has ended.
133
+ const open = [];
134
+ for (let i = 0; i < entries.length; i++) {
135
+ const [startLine, startCol, endLine, endCol, nameIndex] = entries[i];
136
+ while (open.length > 0) {
137
+ const top = open[open.length - 1] * STRIDE;
138
+ if (before(startLine, startCol, table[top + 2], table[top + 3]))
139
+ break;
140
+ open.pop();
141
+ }
142
+ const parent = open.length > 0 ? open[open.length - 1] : -1;
143
+ if (parent >= 0 && before(table[parent * STRIDE + 2], table[parent * STRIDE + 3], endLine, endCol)) {
144
+ return null; // overlaps its parent without nesting in it
145
+ }
146
+ const at = i * STRIDE;
147
+ table[at] = startLine;
148
+ table[at + 1] = startCol;
149
+ table[at + 2] = endLine;
150
+ table[at + 3] = endCol;
151
+ table[at + 4] = nameIndex;
152
+ table[at + 5] = parent;
153
+ open.push(i);
154
+ }
155
+ return new ScopeIndex(table, mapNames, extraNames);
156
+ }
157
+ nameOf(index) {
158
+ if (index === ANONYMOUS)
159
+ return ANONYMOUS_SCOPE;
160
+ const name = index < this.mapNames.length ? this.mapNames[index] : this.extraNames[index - this.mapNames.length];
161
+ return typeof name === "string" ? { kind: "named", name } : UNKNOWN_SCOPE;
162
+ }
163
+ /**
164
+ * The name for a frame at a 0-based generated (line, column): the
165
+ * innermost function containing it. Outside every function (top-level
166
+ * code) it is anonymous.
167
+ */
168
+ nameAt(line, column) {
169
+ const t = this.table;
170
+ // Last scope that starts at or before the position…
171
+ let lo = 0;
172
+ let hi = this.size - 1;
173
+ let found = -1;
174
+ while (lo <= hi) {
175
+ const mid = (lo + hi) >> 1;
176
+ if (before(line, column, t[mid * STRIDE], t[mid * STRIDE + 1]))
177
+ hi = mid - 1;
178
+ else {
179
+ found = mid;
180
+ lo = mid + 1;
181
+ }
182
+ }
183
+ // …or, when it ended before the position, its closest ancestor that hasn't.
184
+ for (let i = found; i >= 0; i = t[i * STRIDE + 5]) {
185
+ if (before(line, column, t[i * STRIDE + 2], t[i * STRIDE + 3]))
186
+ return this.nameOf(t[i * STRIDE + 4]);
187
+ }
188
+ return ANONYMOUS_SCOPE;
189
+ }
190
+ }
@@ -21,9 +21,12 @@ export declare function lookupSourceMap(client: ClickHouseClient, database: stri
21
21
  */
22
22
  export declare const SOURCE_MAP_FRESH_DAYS = 7;
23
23
  /**
24
- * Which check entries the tenant already has: same release, path, debug id
25
- * and content hash, uploaded in the last SOURCE_MAP_FRESH_DAYS. Returns the
26
- * indexes of `check.entries` that need no upload.
24
+ * Which check entries the tenant already has. An entry with a hash: same
25
+ * release, path, debug id and content hash, uploaded in the last
26
+ * SOURCE_MAP_FRESH_DAYS (the upload skips those). An entry with only a
27
+ * debug id: any stored map with that id, whatever its release and path —
28
+ * exactly what lookupSourceMap would find for a stack from that bundle.
29
+ * Returns the indexes of `check.entries` that are present.
27
30
  */
28
31
  export declare function presentSourceMaps(client: ClickHouseClient, database: string, tenant: {
29
32
  orgId: string;
@@ -34,13 +34,41 @@ export async function lookupSourceMap(client, database, key) {
34
34
  */
35
35
  export const SOURCE_MAP_FRESH_DAYS = 7;
36
36
  /**
37
- * Which check entries the tenant already has: same release, path, debug id
38
- * and content hash, uploaded in the last SOURCE_MAP_FRESH_DAYS. Returns the
39
- * indexes of `check.entries` that need no upload.
37
+ * Which check entries the tenant already has. An entry with a hash: same
38
+ * release, path, debug id and content hash, uploaded in the last
39
+ * SOURCE_MAP_FRESH_DAYS (the upload skips those). An entry with only a
40
+ * debug id: any stored map with that id, whatever its release and path —
41
+ * exactly what lookupSourceMap would find for a stack from that bundle.
42
+ * Returns the indexes of `check.entries` that are present.
40
43
  */
41
44
  export async function presentSourceMaps(client, database, tenant, check) {
42
- if (check.entries.length === 0)
43
- return [];
45
+ const hashed = check.entries.some((entry) => entry.hash);
46
+ const byId = check.entries.filter((entry) => !entry.hash).map((entry) => entry.debugId);
47
+ const [withHash, ids] = await Promise.all([
48
+ hashed ? presentByHash(client, database, tenant, check) : new Set(),
49
+ byId.length ? presentDebugIds(client, database, tenant, byId) : new Set(),
50
+ ]);
51
+ const present = [];
52
+ check.entries.forEach((entry, index) => {
53
+ const found = entry.hash
54
+ ? withHash.has(`${entry.filename}\u0000${entry.debugId}\u0000${entry.hash}`)
55
+ : ids.has(entry.debugId);
56
+ if (found)
57
+ present.push(index);
58
+ });
59
+ return present;
60
+ }
61
+ async function presentDebugIds(client, database, tenant, debugIds) {
62
+ const rows = await client.query({
63
+ query: `SELECT DISTINCT debug_id FROM ${database}.runtime_source_maps
64
+ WHERE org_id = {org:String} AND repository_id = {repo:String}
65
+ AND has({debugIds:Array(String)}, debug_id)`,
66
+ query_params: { org: tenant.orgId, repo: tenant.repositoryId, debugIds: [...new Set(debugIds)] },
67
+ format: "JSONEachRow",
68
+ });
69
+ return new Set((await rows.json()).map((row) => row.debug_id));
70
+ }
71
+ async function presentByHash(client, database, tenant, check) {
44
72
  const rows = await client.query({
45
73
  query: `SELECT DISTINCT filename, debug_id, content_hash FROM ${database}.runtime_source_maps
46
74
  WHERE org_id = {org:String} AND repository_id = {repo:String} AND release = {release:String}
@@ -50,15 +78,9 @@ export async function presentSourceMaps(client, database, tenant, check) {
50
78
  org: tenant.orgId,
51
79
  repo: tenant.repositoryId,
52
80
  release: check.release,
53
- hashes: [...new Set(check.entries.map((entry) => entry.hash))],
81
+ hashes: [...new Set(check.entries.filter((entry) => entry.hash).map((entry) => entry.hash))],
54
82
  },
55
83
  format: "JSONEachRow",
56
84
  });
57
- const stored = new Set((await rows.json()).map((row) => `${row.filename}\u0000${row.debug_id}\u0000${row.content_hash}`));
58
- const present = [];
59
- check.entries.forEach((entry, index) => {
60
- if (stored.has(`${entry.filename}\u0000${entry.debugId}\u0000${entry.hash}`))
61
- present.push(index);
62
- });
63
- return present;
85
+ return new Set((await rows.json()).map((row) => `${row.filename}\u0000${row.debug_id}\u0000${row.content_hash}`));
64
86
  }
@@ -1,5 +1,14 @@
1
- /** Largest stored map (after `sourcesContent` is removed), in characters. */
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.
7
+ */
2
8
  export declare const SOURCE_MAP_MAX_CHARS: number;
9
+ /** `/v1/sourcemaps` body limit, counted after a gzip or deflate body is
10
+ * inflated: the largest map plus room for the rest of the request. */
11
+ export declare const SOURCE_MAP_MAX_BODY_BYTES: number;
3
12
  /** A debug id: 8-4-4-4-12 lowercase hex. Written by `autter-runtime
4
13
  * sourcemaps inject`, and by Rollup, Turbopack, esbuild and Sentry tooling. */
5
14
  export declare const DEBUG_ID_RE: RegExp;
@@ -40,6 +49,7 @@ export declare function validateSourceMap(body: unknown): ValidSourceMap | null;
40
49
  export interface SourceMapCheckEntry {
41
50
  filename: string;
42
51
  debugId: string;
52
+ /** sha256 of the stored map, or "" for a check by debug id alone (1.7.0+). */
43
53
  hash: string;
44
54
  }
45
55
  export interface SourceMapCheck {
@@ -50,9 +60,12 @@ export interface SourceMapCheck {
50
60
  export declare const SOURCE_MAP_CHECK_MAX = 500;
51
61
  /**
52
62
  * Validate a `/v1/sourcemaps/check` request:
53
- * `{ release, files: [{ filename, debugId?, hash }] }` (≤ 500 files). The
63
+ * `{ release, files: [{ filename, debugId?, hash? }] }` (≤ 500 files). The
54
64
  * filenames and debug ids are normalised exactly as an upload would store
55
- * them. Null when invalid.
65
+ * them. Each file needs a hash (is this exact map stored? — what `upload`
66
+ * asks to skip maps) or, from 1.7.0, a debug id alone (is any map stored
67
+ * for this bundle? — what `sourcemaps verify` asks after the maps were
68
+ * deleted). Null when invalid.
56
69
  */
57
70
  export declare function validateSourceMapCheck(body: unknown): SourceMapCheck | null;
58
71
  /** Most scripts one browser event may name in `debugIds`. */