@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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { detectStackLanguage } from "./fingerprint.js";
|
|
2
|
+
/**
|
|
3
|
+
* Raw stack text → structured frames, INNERMOST (crash site) FIRST.
|
|
4
|
+
*
|
|
5
|
+
* The legacy `message-v1` fingerprint hashes stack lines as text, which
|
|
6
|
+
* keeps Python line numbers, takes Python's outermost frames, and counts
|
|
7
|
+
* dependency frames. `frames-v3` (fingerprint-v3.ts) hashes structured
|
|
8
|
+
* frames instead, so every supported language is parsed into the sink's
|
|
9
|
+
* StackFrame shape here, in one consistent orientation:
|
|
10
|
+
*
|
|
11
|
+
* - V8 (Node, Chrome, Edge): `at fn (file:line:col)` / `at file:line:col`
|
|
12
|
+
* - Firefox / Safari: `fn@url:line:col` / `@url:line:col`
|
|
13
|
+
* - Python: `File "…", line N, in fn` — tracebacks list the OUTERMOST frame
|
|
14
|
+
* first, so the frames are reversed; only the last (reported) traceback
|
|
15
|
+
* of a chained exception is used
|
|
16
|
+
* - Go: function line + `\t<file>.go:<line> +0x…`, first goroutine only
|
|
17
|
+
* - JVM: `at pkg.Class.method(File.java:N)`, first exception block only
|
|
18
|
+
* (before `Caused by:`)
|
|
19
|
+
* - .NET: `at Ns.Type.Method(args) in <file>:line N`; for wrapped
|
|
20
|
+
* exceptions only the frames after the last "End of inner exception"
|
|
21
|
+
* - Rust: `N: path::to::fn` + `at <file>:<line>:<col>`
|
|
22
|
+
* - Ruby / PHP best effort, so those stacks do not collapse to the message
|
|
23
|
+
*
|
|
24
|
+
* `inApp` follows the contract §3 rule (isInAppFrame). `resolved` is always
|
|
25
|
+
* false here; symbolicate.ts sets it for frames a source map resolved.
|
|
26
|
+
* Shared parity vectors: stack-parse-vectors.json.
|
|
27
|
+
*/
|
|
28
|
+
/** Frames kept per stack (contract §1: at most 50). */
|
|
29
|
+
export const MAX_FRAMES = 50;
|
|
30
|
+
const MAX_FUNCTION_CHARS = 500;
|
|
31
|
+
const MAX_PATH_CHARS = 1000;
|
|
32
|
+
// ── In-app rule (contract §3) ─────────────────────────────────────────────
|
|
33
|
+
const NOT_IN_APP_PATH_PARTS = [
|
|
34
|
+
"node_modules/",
|
|
35
|
+
"/site-packages/",
|
|
36
|
+
"/dist-packages/",
|
|
37
|
+
"/vendor/",
|
|
38
|
+
"/.pnpm/",
|
|
39
|
+
];
|
|
40
|
+
const NOT_IN_APP_PATH_PREFIXES = ["node:", "internal/"];
|
|
41
|
+
const EXTENSION_SCHEMES = [
|
|
42
|
+
"chrome-extension:",
|
|
43
|
+
"moz-extension:",
|
|
44
|
+
"safari-extension:",
|
|
45
|
+
"safari-web-extension:",
|
|
46
|
+
"webkit-masked-url:",
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* Path half of §3: dependency directories, Node internals and browser
|
|
50
|
+
* extension scripts are not in-app. Windows separators are normalised so
|
|
51
|
+
* `C:\app\node_modules\x.js` counts too. A frame with no path is in-app.
|
|
52
|
+
*/
|
|
53
|
+
export function isInAppPath(path) {
|
|
54
|
+
if (!path)
|
|
55
|
+
return true;
|
|
56
|
+
const normalized = path.replace(/\\/g, "/");
|
|
57
|
+
if (NOT_IN_APP_PATH_PARTS.some((part) => normalized.includes(part)))
|
|
58
|
+
return false;
|
|
59
|
+
if (NOT_IN_APP_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix)))
|
|
60
|
+
return false;
|
|
61
|
+
const lower = normalized.toLowerCase();
|
|
62
|
+
if (EXTENSION_SCHEMES.some((scheme) => lower.startsWith(scheme)))
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/** Fully qualified name for the language rules (`module.function`). */
|
|
67
|
+
function qualifiedName(frame) {
|
|
68
|
+
if (frame.module && frame.function)
|
|
69
|
+
return `${frame.module}.${frame.function}`;
|
|
70
|
+
return frame.module ?? frame.function ?? "";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Contract §3. Source-map ignore lists are applied by symbolicate.ts; the
|
|
74
|
+
* language rules apply only to frames of that language, so a JS object
|
|
75
|
+
* that happens to be called `runtime` or `System` stays in-app.
|
|
76
|
+
*/
|
|
77
|
+
export function isInAppFrame(frame, language) {
|
|
78
|
+
if (!isInAppPath(frame.filename))
|
|
79
|
+
return false;
|
|
80
|
+
const qualified = qualifiedName(frame);
|
|
81
|
+
if (language === "go") {
|
|
82
|
+
if (qualified.startsWith("runtime."))
|
|
83
|
+
return false;
|
|
84
|
+
if ((frame.filename ?? "").replace(/\\/g, "/").includes("/usr/local/go/"))
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (language === "jvm" && /^(?:java|javax|kotlin|jdk)\./.test(qualified))
|
|
88
|
+
return false;
|
|
89
|
+
if (language === "dotnet" && /^(?:System|Microsoft)\./.test(qualified))
|
|
90
|
+
return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
94
|
+
/** Strip a query string and/or hash from a path or URL. */
|
|
95
|
+
export function stripQueryAndHash(path) {
|
|
96
|
+
const at = path.search(/[?#]/);
|
|
97
|
+
return at === -1 ? path : path.slice(0, at);
|
|
98
|
+
}
|
|
99
|
+
function clean(value, max) {
|
|
100
|
+
if (value == null)
|
|
101
|
+
return null;
|
|
102
|
+
const trimmed = value.trim();
|
|
103
|
+
return trimmed ? trimmed.slice(0, max) : null;
|
|
104
|
+
}
|
|
105
|
+
function toInt(value) {
|
|
106
|
+
if (value === undefined)
|
|
107
|
+
return null;
|
|
108
|
+
const n = Number.parseInt(value, 10);
|
|
109
|
+
return Number.isFinite(n) ? n : null;
|
|
110
|
+
}
|
|
111
|
+
function frame(language, parts) {
|
|
112
|
+
const filename = clean(parts.filename ? stripQueryAndHash(parts.filename) : null, MAX_PATH_CHARS);
|
|
113
|
+
const out = {
|
|
114
|
+
function: clean(parts.function, MAX_FUNCTION_CHARS),
|
|
115
|
+
module: clean(parts.module, MAX_FUNCTION_CHARS),
|
|
116
|
+
filename,
|
|
117
|
+
lineno: parts.lineno ?? null,
|
|
118
|
+
colno: parts.colno ?? null,
|
|
119
|
+
inApp: true,
|
|
120
|
+
resolved: false,
|
|
121
|
+
};
|
|
122
|
+
out.inApp = isInAppFrame(out, language);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Drop the trailing (balanced) call-argument group: "f(0x1, 0x2)" → "f",
|
|
127
|
+
* "C->handle(Object(Request))" → "C->handle". Receiver parentheses earlier
|
|
128
|
+
* in the name ("main.(*T).M(…)") are kept.
|
|
129
|
+
*/
|
|
130
|
+
function stripTrailingArgs(fn) {
|
|
131
|
+
const trimmed = fn.trim();
|
|
132
|
+
if (!trimmed.endsWith(")"))
|
|
133
|
+
return trimmed;
|
|
134
|
+
let depth = 0;
|
|
135
|
+
for (let i = trimmed.length - 1; i >= 0; i--) {
|
|
136
|
+
const ch = trimmed[i];
|
|
137
|
+
if (ch === ")")
|
|
138
|
+
depth++;
|
|
139
|
+
else if (ch === "(" && --depth === 0)
|
|
140
|
+
return i > 0 ? trimmed.slice(0, i).trim() : trimmed;
|
|
141
|
+
}
|
|
142
|
+
return trimmed;
|
|
143
|
+
}
|
|
144
|
+
/** Split at the last "." outside brackets/angles: "A.B.C[T]" → ["A.B", "C[T]"]. */
|
|
145
|
+
function splitLastDot(name) {
|
|
146
|
+
let depth = 0;
|
|
147
|
+
for (let i = name.length - 1; i > 0; i--) {
|
|
148
|
+
const ch = name[i];
|
|
149
|
+
if (ch === "]" || ch === ">" || ch === ")")
|
|
150
|
+
depth++;
|
|
151
|
+
else if (ch === "[" || ch === "<" || ch === "(")
|
|
152
|
+
depth = Math.max(0, depth - 1);
|
|
153
|
+
else if (ch === "." && depth === 0) {
|
|
154
|
+
// ".ctor" / ".cctor": the dot belongs to the method name.
|
|
155
|
+
if (name[i - 1] === ".")
|
|
156
|
+
return [name.slice(0, i - 1), name.slice(i)];
|
|
157
|
+
return [name.slice(0, i), name.slice(i + 1)];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [null, name];
|
|
161
|
+
}
|
|
162
|
+
// ── JavaScript ────────────────────────────────────────────────────────────
|
|
163
|
+
const V8_LINE_RE = /^\s*at\s+(.*)$/;
|
|
164
|
+
const V8_WITH_FN_RE = /^(.*?)\s+\((.*)\)$/;
|
|
165
|
+
const LOCATION_RE = /^(.*?):(\d+)(?::(\d+))?$/;
|
|
166
|
+
const GECKO_LINE_RE = /^\s*(.*?)@(.*?)\s*$/;
|
|
167
|
+
/** "file:line:col" (or a bare non-path marker) → location parts. */
|
|
168
|
+
function jsLocation(raw) {
|
|
169
|
+
let location = raw.trim();
|
|
170
|
+
// eval frames: "eval at fn (file:1:2), <anonymous>:3:4" — use the
|
|
171
|
+
// evaluating script's position.
|
|
172
|
+
if (location.startsWith("eval at ")) {
|
|
173
|
+
const inner = /\(([^()]+?):(\d+):(\d+)\)/.exec(location);
|
|
174
|
+
if (inner)
|
|
175
|
+
return { filename: inner[1], lineno: toInt(inner[2]), colno: toInt(inner[3]) };
|
|
176
|
+
}
|
|
177
|
+
if (/^(?:native|<anonymous>|\[native code\]|index \d+|unknown location)$/i.test(location)) {
|
|
178
|
+
return { filename: null, lineno: null, colno: null };
|
|
179
|
+
}
|
|
180
|
+
const m = LOCATION_RE.exec(location);
|
|
181
|
+
if (m && m[1])
|
|
182
|
+
return { filename: m[1], lineno: toInt(m[2]), colno: toInt(m[3]) };
|
|
183
|
+
location = location.replace(/^address at /, "");
|
|
184
|
+
return { filename: location || null, lineno: null, colno: null };
|
|
185
|
+
}
|
|
186
|
+
function parseV8(lines) {
|
|
187
|
+
const frames = [];
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
const m = V8_LINE_RE.exec(line);
|
|
190
|
+
if (!m)
|
|
191
|
+
continue;
|
|
192
|
+
const body = m[1].trim();
|
|
193
|
+
const withFn = V8_WITH_FN_RE.exec(body);
|
|
194
|
+
if (withFn) {
|
|
195
|
+
frames.push(frame("javascript", { function: withFn[1], ...jsLocation(withFn[2]) }));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// "at async file:1:2" is an anonymous async function: the location
|
|
199
|
+
// is what follows "async " (the platform parser mirrors this).
|
|
200
|
+
frames.push(frame("javascript", jsLocation(body.replace(/^async /, ""))));
|
|
201
|
+
}
|
|
202
|
+
if (frames.length >= MAX_FRAMES)
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
return frames;
|
|
206
|
+
}
|
|
207
|
+
/** A Firefox/Safari location: something with :line(:col), or [native code]. */
|
|
208
|
+
function isGeckoLocation(location) {
|
|
209
|
+
return /:\d+(?::\d+)?$/.test(location) || location === "[native code]";
|
|
210
|
+
}
|
|
211
|
+
function parseGecko(lines) {
|
|
212
|
+
const frames = [];
|
|
213
|
+
for (const line of lines) {
|
|
214
|
+
const m = GECKO_LINE_RE.exec(line);
|
|
215
|
+
if (!m || !isGeckoLocation(m[2]))
|
|
216
|
+
continue;
|
|
217
|
+
// Firefox eval frames: "fn@url line 2 > eval:1:5" — keep the script.
|
|
218
|
+
const location = m[2].replace(/ line (\d+) > (?:eval|Function)(?::\d+:\d+)?$/, ":$1");
|
|
219
|
+
frames.push(frame("javascript", { function: m[1] || null, ...jsLocation(location) }));
|
|
220
|
+
if (frames.length >= MAX_FRAMES)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return frames;
|
|
224
|
+
}
|
|
225
|
+
// ── Python ────────────────────────────────────────────────────────────────
|
|
226
|
+
const PY_FILE_RE = /^\s*File "(.+)", line (\d+)(?:, in (.+?))?\s*$/;
|
|
227
|
+
const PY_TRACEBACK_RE = /^\s*Traceback \(most recent call last\):\s*$/;
|
|
228
|
+
function parsePython(lines) {
|
|
229
|
+
// A chained exception prints one traceback per exception; the reported
|
|
230
|
+
// (final) exception's frames are the last block.
|
|
231
|
+
let start = 0;
|
|
232
|
+
for (let i = 0; i < lines.length; i++)
|
|
233
|
+
if (PY_TRACEBACK_RE.test(lines[i]))
|
|
234
|
+
start = i;
|
|
235
|
+
const outermostFirst = [];
|
|
236
|
+
for (const line of lines.slice(start)) {
|
|
237
|
+
const m = PY_FILE_RE.exec(line);
|
|
238
|
+
if (!m)
|
|
239
|
+
continue;
|
|
240
|
+
outermostFirst.push(frame("python", { function: m[3] ?? null, filename: m[1], lineno: toInt(m[2]) }));
|
|
241
|
+
}
|
|
242
|
+
return outermostFirst.reverse().slice(0, MAX_FRAMES);
|
|
243
|
+
}
|
|
244
|
+
// ── Go ────────────────────────────────────────────────────────────────────
|
|
245
|
+
const GO_LOCATION_RE = /^\s*(.+\.go):(\d+)(?:\s+\+0x[0-9a-f]+)?\s*$/;
|
|
246
|
+
const GO_GOROUTINE_RE = /\bgoroutine \d+ \[/;
|
|
247
|
+
/** "net/http.(*conn).serve" → module "net/http", function "(*conn).serve". */
|
|
248
|
+
function splitGoName(qualified) {
|
|
249
|
+
const slash = qualified.lastIndexOf("/", qualified.indexOf("(") === -1 ? qualified.length : qualified.indexOf("("));
|
|
250
|
+
const dot = qualified.indexOf(".", slash + 1);
|
|
251
|
+
if (dot <= 0)
|
|
252
|
+
return { module: null, function: qualified };
|
|
253
|
+
return { module: qualified.slice(0, dot), function: qualified.slice(dot + 1) };
|
|
254
|
+
}
|
|
255
|
+
function parseGo(lines) {
|
|
256
|
+
const frames = [];
|
|
257
|
+
let fn = "";
|
|
258
|
+
let goroutines = 0;
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
const trimmed = line.trim();
|
|
261
|
+
if (GO_GOROUTINE_RE.test(trimmed)) {
|
|
262
|
+
// Only the first goroutine is the one that failed.
|
|
263
|
+
if (++goroutines > 1 && frames.length > 0)
|
|
264
|
+
break;
|
|
265
|
+
fn = "";
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const loc = GO_LOCATION_RE.exec(line);
|
|
269
|
+
if (loc && fn) {
|
|
270
|
+
const name = stripTrailingArgs(fn.replace(/^created by\s+/, "").replace(/\s+in goroutine \d+\s*$/, ""));
|
|
271
|
+
const { module, function: func } = splitGoName(name);
|
|
272
|
+
frames.push(frame("go", { function: func, module, filename: loc[1].trim(), lineno: toInt(loc[2]) }));
|
|
273
|
+
if (frames.length >= MAX_FRAMES)
|
|
274
|
+
break;
|
|
275
|
+
fn = "";
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (trimmed && !loc)
|
|
279
|
+
fn = trimmed;
|
|
280
|
+
}
|
|
281
|
+
return frames;
|
|
282
|
+
}
|
|
283
|
+
// ── JVM ───────────────────────────────────────────────────────────────────
|
|
284
|
+
function parseJvm(lines) {
|
|
285
|
+
const frames = [];
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
// The reported exception's frames come first; causes follow.
|
|
288
|
+
if (/^\s*(?:Caused by|Suppressed):/.test(line) && frames.length > 0)
|
|
289
|
+
break;
|
|
290
|
+
const m = /^\s*at\s+(.+?)\((.*)\)\s*$/.exec(line);
|
|
291
|
+
if (!m)
|
|
292
|
+
continue;
|
|
293
|
+
// "java.base/java.lang.Thread.run", "app//com.example.Foo.bar"
|
|
294
|
+
const qualified = m[1].trim().replace(/^.*\//, "");
|
|
295
|
+
const [module, method] = splitLastDot(qualified);
|
|
296
|
+
const location = m[2].trim();
|
|
297
|
+
let filename = null;
|
|
298
|
+
let lineno = null;
|
|
299
|
+
if (!/^(?:Native Method|Unknown Source)$/.test(location)) {
|
|
300
|
+
const loc = /^(.*?)(?::(\d+))?$/.exec(location);
|
|
301
|
+
filename = loc[1] || null;
|
|
302
|
+
lineno = toInt(loc[2]);
|
|
303
|
+
}
|
|
304
|
+
frames.push(frame("jvm", { function: method, module, filename, lineno }));
|
|
305
|
+
if (frames.length >= MAX_FRAMES)
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
return frames;
|
|
309
|
+
}
|
|
310
|
+
// ── .NET ──────────────────────────────────────────────────────────────────
|
|
311
|
+
function parseDotnet(lines) {
|
|
312
|
+
let frames = [];
|
|
313
|
+
for (const line of lines) {
|
|
314
|
+
// "Outer ---> Inner\n at inner…\n --- End of inner exception stack
|
|
315
|
+
// trace ---\n at outer…": the reported (outer) frames come last.
|
|
316
|
+
if (/---\s*End of inner exception stack trace\s*---/i.test(line)) {
|
|
317
|
+
frames = [];
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const m = /^\s*at\s+(.+?)(?:\s+in\s+(.+?):line\s+(\d+))?\s*$/.exec(line);
|
|
321
|
+
if (!m)
|
|
322
|
+
continue;
|
|
323
|
+
const [module, method] = splitLastDot(stripTrailingArgs(m[1]));
|
|
324
|
+
frames.push(frame("dotnet", { function: method, module, filename: m[2] ?? null, lineno: toInt(m[3]) }));
|
|
325
|
+
}
|
|
326
|
+
return frames.slice(0, MAX_FRAMES);
|
|
327
|
+
}
|
|
328
|
+
// ── Rust ──────────────────────────────────────────────────────────────────
|
|
329
|
+
function parseRust(lines) {
|
|
330
|
+
const frames = [];
|
|
331
|
+
for (let i = 0; i < lines.length && frames.length < MAX_FRAMES; i++) {
|
|
332
|
+
const m = /^\s*\d+:\s+(?:0x[0-9a-f]+\s+-\s+)?(.+?)\s*$/.exec(lines[i]);
|
|
333
|
+
if (!m)
|
|
334
|
+
continue;
|
|
335
|
+
const fn = m[1].replace(/::h[0-9a-f]{6,}$/, "").trim();
|
|
336
|
+
if (!fn)
|
|
337
|
+
continue;
|
|
338
|
+
const at = /^\s*at\s+(.+?):(\d+)(?::(\d+))?\s*$/.exec(lines[i + 1] ?? "");
|
|
339
|
+
if (at)
|
|
340
|
+
i++;
|
|
341
|
+
frames.push(frame("rust", {
|
|
342
|
+
function: fn,
|
|
343
|
+
filename: at?.[1] ?? null,
|
|
344
|
+
lineno: toInt(at?.[2]),
|
|
345
|
+
colno: toInt(at?.[3]),
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
return frames;
|
|
349
|
+
}
|
|
350
|
+
// ── Ruby / PHP (best effort) ──────────────────────────────────────────────
|
|
351
|
+
// A .NET frame without source locations: "at Ns.Type.Method(Type arg)".
|
|
352
|
+
// V8 always puts a space before "(" or has no parentheses at all.
|
|
353
|
+
const DOTNET_BARE_RE = /^\s*at\s+[\w.`<>\[\],+|]+\([\w.`<>\[\],&*\s]*\)\s*$/;
|
|
354
|
+
const RUBY_RE = /^\s*(?:from\s+)?(.+?):(\d+)(?::in\s+[`'](?:[\w:#.]+[#.])?(.+?)')?\s*$/;
|
|
355
|
+
const PHP_RE = /^\s*#\d+\s+(.+?)\((\d+)\):\s+(.+?)\s*$/;
|
|
356
|
+
function parseRuby(lines) {
|
|
357
|
+
const frames = [];
|
|
358
|
+
for (const line of lines) {
|
|
359
|
+
const m = RUBY_RE.exec(line);
|
|
360
|
+
if (!m || !/\.(?:rb|erb|rake)$/.test(m[1]))
|
|
361
|
+
continue;
|
|
362
|
+
frames.push(frame("ruby", { function: m[3] ?? null, filename: m[1], lineno: toInt(m[2]) }));
|
|
363
|
+
if (frames.length >= MAX_FRAMES)
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
return frames;
|
|
367
|
+
}
|
|
368
|
+
function parsePhp(lines) {
|
|
369
|
+
const frames = [];
|
|
370
|
+
for (const line of lines) {
|
|
371
|
+
const m = PHP_RE.exec(line);
|
|
372
|
+
if (!m)
|
|
373
|
+
continue;
|
|
374
|
+
frames.push(frame("php", { function: stripTrailingArgs(m[3]), filename: m[1], lineno: toInt(m[2]) }));
|
|
375
|
+
if (frames.length >= MAX_FRAMES)
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
return frames;
|
|
379
|
+
}
|
|
380
|
+
// ── Entry point ───────────────────────────────────────────────────────────
|
|
381
|
+
/** The language of a raw stack, or null when it has no recognisable frames. */
|
|
382
|
+
export function detectFrameLanguage(lines) {
|
|
383
|
+
switch (detectStackLanguage(lines)) {
|
|
384
|
+
case "go":
|
|
385
|
+
return "go";
|
|
386
|
+
case "rust":
|
|
387
|
+
return "rust";
|
|
388
|
+
case "jvm":
|
|
389
|
+
return "jvm";
|
|
390
|
+
case "dotnet":
|
|
391
|
+
return "dotnet";
|
|
392
|
+
default:
|
|
393
|
+
}
|
|
394
|
+
if (lines.some((line) => PY_FILE_RE.test(line)))
|
|
395
|
+
return "python";
|
|
396
|
+
if (lines.some((line) => DOTNET_BARE_RE.test(line)) &&
|
|
397
|
+
!lines.some((line) => /:\d+(?::\d+)?\)?\s*$/.test(line))) {
|
|
398
|
+
return "dotnet";
|
|
399
|
+
}
|
|
400
|
+
if (lines.some((line) => V8_LINE_RE.test(line)))
|
|
401
|
+
return "javascript";
|
|
402
|
+
if (lines.some((line) => {
|
|
403
|
+
const m = GECKO_LINE_RE.exec(line);
|
|
404
|
+
return Boolean(m && isGeckoLocation(m[2]));
|
|
405
|
+
}))
|
|
406
|
+
return "javascript";
|
|
407
|
+
if (lines.some((line) => PHP_RE.test(line)))
|
|
408
|
+
return "php";
|
|
409
|
+
if (lines.some((line) => {
|
|
410
|
+
const m = RUBY_RE.exec(line);
|
|
411
|
+
return Boolean(m && /\.(?:rb|erb|rake)$/.test(m[1]));
|
|
412
|
+
}))
|
|
413
|
+
return "ruby";
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
/** Parse a raw stack into frames, innermost first (≤ MAX_FRAMES). */
|
|
417
|
+
export function parseStackFrames(stack) {
|
|
418
|
+
if (!stack)
|
|
419
|
+
return [];
|
|
420
|
+
const lines = stack.split(/\r?\n/);
|
|
421
|
+
switch (detectFrameLanguage(lines)) {
|
|
422
|
+
case "go":
|
|
423
|
+
return parseGo(lines);
|
|
424
|
+
case "rust":
|
|
425
|
+
return parseRust(lines);
|
|
426
|
+
case "jvm":
|
|
427
|
+
return parseJvm(lines);
|
|
428
|
+
case "dotnet":
|
|
429
|
+
return parseDotnet(lines);
|
|
430
|
+
case "python":
|
|
431
|
+
return parsePython(lines);
|
|
432
|
+
case "javascript":
|
|
433
|
+
return lines.some((line) => V8_LINE_RE.test(line)) ? parseV8(lines) : parseGecko(lines);
|
|
434
|
+
case "php":
|
|
435
|
+
return parsePhp(lines);
|
|
436
|
+
case "ruby":
|
|
437
|
+
return parseRuby(lines);
|
|
438
|
+
default:
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { TraceMap } from "@jridgewell/trace-mapping";
|
|
2
|
+
import { type GeneratedPosition, type MappedSegment } from "./source-map-mappings.js";
|
|
3
|
+
import { ScopeIndex } from "./source-map-scopes.js";
|
|
4
|
+
import type { SourceMapKey } from "./source-maps-lookup.js";
|
|
5
|
+
import type { StackFrame } from "./types.js";
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* - Function names: when the map carries function scopes
|
|
13
|
+
* (`x_autter_scopes`, uploaded by @autter/runtime-cli after 1.0.0), a
|
|
14
|
+
* frame is named after the innermost function containing it, with that
|
|
15
|
+
* function's original name (null in an anonymous function or top-level
|
|
16
|
+
* code) — see source-map-scopes.ts. Maps without scopes keep the earlier
|
|
17
|
+
* rule: the name mapped at the CALLER's position is the callee's original
|
|
18
|
+
* name; without one, a resolved frame keeps its parsed name only when
|
|
19
|
+
* that is not an obviously mangled one (≤ 2 characters), since a mangled
|
|
20
|
+
* name changes on every build. That rule is wrong for calls through a
|
|
21
|
+
* variable (`const run = actions.captured; run()` names the frame `run`)
|
|
22
|
+
* and for handlers a framework calls, which is why scopes exist.
|
|
23
|
+
* - Sources the map lists in `ignoreList` / `x_google_ignoreList` are not
|
|
24
|
+
* 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).
|
|
27
|
+
* - Never fails ingest: any error leaves the frame as parsed.
|
|
28
|
+
*
|
|
29
|
+
* Parsed maps are cached per (org, repository, release, file) — or per debug
|
|
30
|
+
* id — in a byte-bounded LRU, so a busy issue never refetches its map. A map
|
|
31
|
+
* that would take more than half of that budget is used for its lookup and
|
|
32
|
+
* not kept (source-map-mappings.ts reads it without decoding it), so one huge
|
|
33
|
+
* vendor chunk cannot flush every other map.
|
|
34
|
+
*/
|
|
35
|
+
export type MapFetcher = (key: SourceMapKey) => Promise<string | null>;
|
|
36
|
+
export interface SymbolicationScope {
|
|
37
|
+
orgId: string;
|
|
38
|
+
repositoryId: string;
|
|
39
|
+
release: string | null;
|
|
40
|
+
/** Script URL → debug id (EI-13), when the browser sent them. */
|
|
41
|
+
debugIds?: Record<string, string> | null;
|
|
42
|
+
}
|
|
43
|
+
/** Path-only form of a script URL — the key uploads are stored under. */
|
|
44
|
+
export declare function scriptPath(filename: string): string | null;
|
|
45
|
+
export interface SourceMapCacheOptions {
|
|
46
|
+
/** Budget for the estimated in-memory size of parsed maps (default 64 MiB).
|
|
47
|
+
* A map that would take more than half of it is not cached. */
|
|
48
|
+
maxBytes?: number;
|
|
49
|
+
maxEntries?: number;
|
|
50
|
+
/** How long a found map is reused (default 1 h). */
|
|
51
|
+
hitTtlMs?: number;
|
|
52
|
+
/** How long "no map uploaded" is remembered (default 5 min). */
|
|
53
|
+
missTtlMs?: number;
|
|
54
|
+
/** How long a failed lookup is remembered (default 30 s). */
|
|
55
|
+
errorTtlMs?: number;
|
|
56
|
+
now?: () => number;
|
|
57
|
+
}
|
|
58
|
+
/** A parsed map: its metadata, its function scopes, and position lookups. */
|
|
59
|
+
export interface LoadedSourceMap {
|
|
60
|
+
/** Sources, names, ignore list and sourcesContent. Its own `mappings`
|
|
61
|
+
* are left empty: positions go through `lookup`. */
|
|
62
|
+
map: TraceMap;
|
|
63
|
+
scopes: ScopeIndex | null;
|
|
64
|
+
/** Original positions for 0-based generated positions (one call per stack). */
|
|
65
|
+
lookup(positions: readonly GeneratedPosition[]): Array<MappedSegment | null>;
|
|
66
|
+
/** Estimated heap: decoded mappings (when decoded), strings and scopes. */
|
|
67
|
+
bytes: number;
|
|
68
|
+
/** False when the map was too big to keep, so its mappings were not decoded. */
|
|
69
|
+
cacheable: boolean;
|
|
70
|
+
}
|
|
71
|
+
export declare class SourceMapCache {
|
|
72
|
+
private readonly entries;
|
|
73
|
+
private readonly inFlight;
|
|
74
|
+
private weight;
|
|
75
|
+
private readonly maxBytes;
|
|
76
|
+
private readonly maxEntries;
|
|
77
|
+
private readonly hitTtlMs;
|
|
78
|
+
private readonly missTtlMs;
|
|
79
|
+
private readonly errorTtlMs;
|
|
80
|
+
private readonly now;
|
|
81
|
+
/** Fetches made (cache misses) — for tests and /healthz-style debugging. */
|
|
82
|
+
fetches: number;
|
|
83
|
+
/** Maps used without being cached because they were too big to keep. */
|
|
84
|
+
bypassed: number;
|
|
85
|
+
constructor(options?: SourceMapCacheOptions);
|
|
86
|
+
static keyOf(key: SourceMapKey): string;
|
|
87
|
+
get size(): number;
|
|
88
|
+
/** Estimated bytes of the cached maps (what `maxBytes` bounds). */
|
|
89
|
+
get bytes(): number;
|
|
90
|
+
/** The parsed map for `key` (null when none), fetching at most once at a time. */
|
|
91
|
+
get(key: SourceMapKey, fetchMap: MapFetcher): Promise<LoadedSourceMap | null>;
|
|
92
|
+
private load;
|
|
93
|
+
private store;
|
|
94
|
+
private remove;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse a stored map once: metadata, scopes (null when absent or malformed
|
|
98
|
+
* — a bad scope field never loses the map) and position lookups. When the
|
|
99
|
+
* estimated heap is at most `maxDecodedBytes`, the mappings are decoded
|
|
100
|
+
* once into a compact table; otherwise they are left encoded and each
|
|
101
|
+
* lookup reads them in one pass (`cacheable: false`). Throws when the map
|
|
102
|
+
* itself is unusable.
|
|
103
|
+
*/
|
|
104
|
+
export declare function parseSourceMap(json: string, maxDecodedBytes?: number): LoadedSourceMap;
|
|
105
|
+
export declare class Symbolicator {
|
|
106
|
+
private readonly fetchMap;
|
|
107
|
+
readonly cache: SourceMapCache;
|
|
108
|
+
constructor(fetchMap: MapFetcher, cache?: SourceMapCache);
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the JS frames of one stack. Returns a new array; frames that
|
|
111
|
+
* cannot be resolved (no map, no position, non-JS) are returned as given.
|
|
112
|
+
*/
|
|
113
|
+
symbolicate(frames: StackFrame[], scope: SymbolicationScope): Promise<StackFrame[]>;
|
|
114
|
+
private debugIdFor;
|
|
115
|
+
private keyFor;
|
|
116
|
+
private resolveAll;
|
|
117
|
+
}
|