From 552727be47896f69efc5c9acc934395c5646f77e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 4 Mar 2025 15:00:30 +0100 Subject: [PATCH 1/2] fix(wasm): Fix wasm integration stacktrace parsing for filename --- .../browser/test/tracekit/chromium.test.ts | 93 ++++++++++ packages/wasm/package.json | 2 + packages/wasm/src/index.ts | 17 +- packages/wasm/test/stacktrace-parsing.test.ts | 159 ++++++++++++++++++ packages/wasm/test/tsconfig.json | 3 + packages/wasm/tsconfig.test.json | 8 + packages/wasm/vite.config.ts | 8 + 7 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 packages/wasm/test/stacktrace-parsing.test.ts create mode 100644 packages/wasm/test/tsconfig.json create mode 100644 packages/wasm/tsconfig.test.json create mode 100644 packages/wasm/vite.config.ts 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): 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 + + // I will 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 stackframes 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 = frame.filename.match(/^.*\((.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as | null | [string, string, string]; + + if (!match) { + match = frame.filename.match(/^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as null | [string, string, string]; + } + if (match) { const index = getImage(match[1]); frame.instruction_addr = match[2]; @@ -62,5 +74,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 Date: Tue, 4 Mar 2025 15:19:42 +0100 Subject: [PATCH 2/2] no redos thx --- packages/wasm/src/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index 92d235e391bd..b7ed1bf0860b 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -36,6 +36,8 @@ const _wasmIntegration = (() => { 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 @@ -49,17 +51,19 @@ export function patchFrames(frames: Array): boolean { return; } - // I will call this first match a "messy match". + 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 stackframes are more complicated than our parser can handle: "at MyClass::bar(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 = frame.filename.match(/^.*\((.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as - | null - | [string, string, string]; + let match = lastSplit.match(PARSER_REGEX) as null | [string, string, string]; if (!match) { - match = frame.filename.match(/^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as null | [string, string, string]; + match = frame.filename.match(PARSER_REGEX) as null | [string, string, string]; } if (match) {