diff --git a/packages/browser/test/tracekit/chromium.test.ts b/packages/browser/test/tracekit/chromium.test.ts index 36d728f9cbea..2a6589c32151 100644 --- a/packages/browser/test/tracekit/chromium.test.ts +++ b/packages/browser/test/tracekit/chromium.test.ts @@ -643,4 +643,97 @@ describe('Tracekit - Chrome Tests', () => { }, }); }); + + it('should correctly parse a wasm stack trace', () => { + const WASM_ERROR = { + message: 'memory access out of bounds', + name: 'RuntimeError', + stack: `RuntimeError: memory access out of bounds + at MyClass::bar(int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb) + at MyClass::foo(int) const (http://localhost:8001/main.wasm:wasm-function[186]:0x5637) + at MyClass::getAt(int) const (http://localhost:8001/main.wasm:wasm-function[182]:0x540b) + at emscripten::internal::MethodInvoker::invoke(int (MyClass::* const&)(int) const, MyClass const*, int) (http://localhost:8001/main.wasm:wasm-function[152]:0x47df) + at ClassHandle.MyClass$getAt [as getAt] (eval at newFunc (http://localhost:8001/main.js:2201:27), :9:10) + at myFunctionVectorOutOfBounds (http://localhost:8001/main.html:18:22) + at captureError (http://localhost:8001/main.html:27:11) + at Object.onRuntimeInitialized (http://localhost:8001/main.html:39:9) + at doRun (http://localhost:8001/main.js:7084:71) + at run (http://localhost:8001/main.js:7101:5)`, + }; + + const ex = exceptionFromError(parser, WASM_ERROR); + + // This is really ugly but the wasm integration should clean up these stack frames + expect(ex).toStrictEqual({ + stacktrace: { + frames: [ + { + colno: 5, + filename: 'http://localhost:8001/main.js', + function: 'run', + in_app: true, + lineno: 7101, + }, + { + colno: 71, + filename: 'http://localhost:8001/main.js', + function: 'doRun', + in_app: true, + lineno: 7084, + }, + { + colno: 9, + filename: 'http://localhost:8001/main.html', + function: 'Object.onRuntimeInitialized', + in_app: true, + lineno: 39, + }, + { + colno: 11, + filename: 'http://localhost:8001/main.html', + function: 'captureError', + in_app: true, + lineno: 27, + }, + { + colno: 22, + filename: 'http://localhost:8001/main.html', + function: 'myFunctionVectorOutOfBounds', + in_app: true, + lineno: 18, + }, + { + colno: 27, + filename: 'http://localhost:8001/main.js', + function: 'ClassHandle.MyClass$getAt [as getAt]', + in_app: true, + lineno: 2201, + }, + { + filename: + 'int) const, int, MyClass const*, int>::invoke(int (MyClass::* const&)(int) const, MyClass const*, int) (http://localhost:8001/main.wasm:wasm-function[152]:0x47df', + function: 'emscripten::internal::MethodInvoker { export const wasmIntegration = defineIntegration(_wasmIntegration); +const PARSER_REGEX = /^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/; + /** * Patches a list of stackframes with wasm data needed for server-side symbolication * if applicable. Returns true if the provided list of stack frames had at least one * matching registered image. */ -function patchFrames(frames: Array): boolean { +// Only exported for tests +export function patchFrames(frames: Array): boolean { let hasAtLeastOneWasmFrameWithImage = false; frames.forEach(frame => { if (!frame.filename) { return; } - const match = frame.filename.match(/^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as - | null - | [string, string, string]; + + const split = frame.filename.split('('); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const lastSplit = split[split.length - 1]!; + + // Let's call this first match a "messy match". + // The browser stacktrace parser spits out frames that have a filename like this: "int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb" + // It contains some leftover mess because wasm stack frames are more complicated than our parser can handle: "at MyClass::bar(int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb)" + // This first match simply tries to mitigate the mess up until the first opening parens. + // The match afterwards is a sensible fallback + let match = lastSplit.match(PARSER_REGEX) as null | [string, string, string]; + + if (!match) { + match = frame.filename.match(PARSER_REGEX) as null | [string, string, string]; + } + if (match) { const index = getImage(match[1]); frame.instruction_addr = match[2]; @@ -62,5 +78,6 @@ function patchFrames(frames: Array): boolean { } } }); + return hasAtLeastOneWasmFrameWithImage; } diff --git a/packages/wasm/test/stacktrace-parsing.test.ts b/packages/wasm/test/stacktrace-parsing.test.ts new file mode 100644 index 000000000000..14f0d6965825 --- /dev/null +++ b/packages/wasm/test/stacktrace-parsing.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect } from 'vitest'; +import { patchFrames } from '../src/index'; + +describe('patchFrames()', () => { + it('should correctly extract instruction addresses', () => { + const frames = [ + { + colno: 5, + filename: 'http://localhost:8001/main.js', + function: 'run', + in_app: true, + lineno: 7101, + }, + { + colno: 71, + filename: 'http://localhost:8001/main.js', + function: 'doRun', + in_app: true, + lineno: 7084, + }, + { + colno: 9, + filename: 'http://localhost:8001/main.html', + function: 'Object.onRuntimeInitialized', + in_app: true, + lineno: 39, + }, + { + colno: 11, + filename: 'http://localhost:8001/main.html', + function: 'captureError', + in_app: true, + lineno: 27, + }, + { + colno: 22, + filename: 'http://localhost:8001/main.html', + function: 'myFunctionVectorOutOfBounds', + in_app: true, + lineno: 18, + }, + { + colno: 27, + filename: 'http://localhost:8001/main.js', + function: 'ClassHandle.MyClass$getAt [as getAt]', + in_app: true, + lineno: 2201, + }, + { + filename: + 'int) const, int, MyClass const*, int>::invoke(int (MyClass::* const&)(int) const, MyClass const*, int) (http://localhost:8001/main.wasm:wasm-function[152]:0x47df', + function: 'emscripten::internal::MethodInvoker