Skip to content

Commit 0a7aabd

Browse files
authored
fix(wasm): Fix wasm integration stacktrace parsing for filename (#15572)
1 parent e5c6ab0 commit 0a7aabd

File tree

7 files changed

+294
-4
lines changed

7 files changed

+294
-4
lines changed

packages/browser/test/tracekit/chromium.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,97 @@ describe('Tracekit - Chrome Tests', () => {
643643
},
644644
});
645645
});
646+
647+
it('should correctly parse a wasm stack trace', () => {
648+
const WASM_ERROR = {
649+
message: 'memory access out of bounds',
650+
name: 'RuntimeError',
651+
stack: `RuntimeError: memory access out of bounds
652+
at MyClass::bar(int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb)
653+
at MyClass::foo(int) const (http://localhost:8001/main.wasm:wasm-function[186]:0x5637)
654+
at MyClass::getAt(int) const (http://localhost:8001/main.wasm:wasm-function[182]:0x540b)
655+
at emscripten::internal::MethodInvoker<int (MyClass::*)(int) const, int, MyClass const*, int>::invoke(int (MyClass::* const&)(int) const, MyClass const*, int) (http://localhost:8001/main.wasm:wasm-function[152]:0x47df)
656+
at ClassHandle.MyClass$getAt [as getAt] (eval at newFunc (http://localhost:8001/main.js:2201:27), <anonymous>:9:10)
657+
at myFunctionVectorOutOfBounds (http://localhost:8001/main.html:18:22)
658+
at captureError (http://localhost:8001/main.html:27:11)
659+
at Object.onRuntimeInitialized (http://localhost:8001/main.html:39:9)
660+
at doRun (http://localhost:8001/main.js:7084:71)
661+
at run (http://localhost:8001/main.js:7101:5)`,
662+
};
663+
664+
const ex = exceptionFromError(parser, WASM_ERROR);
665+
666+
// This is really ugly but the wasm integration should clean up these stack frames
667+
expect(ex).toStrictEqual({
668+
stacktrace: {
669+
frames: [
670+
{
671+
colno: 5,
672+
filename: 'http://localhost:8001/main.js',
673+
function: 'run',
674+
in_app: true,
675+
lineno: 7101,
676+
},
677+
{
678+
colno: 71,
679+
filename: 'http://localhost:8001/main.js',
680+
function: 'doRun',
681+
in_app: true,
682+
lineno: 7084,
683+
},
684+
{
685+
colno: 9,
686+
filename: 'http://localhost:8001/main.html',
687+
function: 'Object.onRuntimeInitialized',
688+
in_app: true,
689+
lineno: 39,
690+
},
691+
{
692+
colno: 11,
693+
filename: 'http://localhost:8001/main.html',
694+
function: 'captureError',
695+
in_app: true,
696+
lineno: 27,
697+
},
698+
{
699+
colno: 22,
700+
filename: 'http://localhost:8001/main.html',
701+
function: 'myFunctionVectorOutOfBounds',
702+
in_app: true,
703+
lineno: 18,
704+
},
705+
{
706+
colno: 27,
707+
filename: 'http://localhost:8001/main.js',
708+
function: 'ClassHandle.MyClass$getAt [as getAt]',
709+
in_app: true,
710+
lineno: 2201,
711+
},
712+
{
713+
filename:
714+
'int) const, int, MyClass const*, int>::invoke(int (MyClass::* const&)(int) const, MyClass const*, int) (http://localhost:8001/main.wasm:wasm-function[152]:0x47df',
715+
function: 'emscripten::internal::MethodInvoker<int (MyClass::*)',
716+
in_app: true,
717+
},
718+
{
719+
filename: 'int) const (http://localhost:8001/main.wasm:wasm-function[182]:0x540b',
720+
function: 'MyClass::getAt',
721+
in_app: true,
722+
},
723+
{
724+
filename: 'int) const (http://localhost:8001/main.wasm:wasm-function[186]:0x5637',
725+
function: 'MyClass::foo',
726+
in_app: true,
727+
},
728+
{
729+
filename: 'int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb',
730+
function: 'MyClass::bar',
731+
in_app: true,
732+
},
733+
],
734+
},
735+
type: 'RuntimeError',
736+
value: 'memory access out of bounds',
737+
});
738+
});
646739
});

packages/wasm/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
5757
"build:types:watch": "tsc -p tsconfig.types.json --watch",
5858
"build:tarball": "npm pack",
59+
"test": "vitest run",
60+
"test:watch": "vitest --watch",
5961
"circularDepCheck": "madge --circular src/index.ts",
6062
"clean": "rimraf build coverage sentry-wasm-*.tgz",
6163
"fix": "eslint . --format stylish --fix",

packages/wasm/src/index.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,36 @@ const _wasmIntegration = (() => {
3636

3737
export const wasmIntegration = defineIntegration(_wasmIntegration);
3838

39+
const PARSER_REGEX = /^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/;
40+
3941
/**
4042
* Patches a list of stackframes with wasm data needed for server-side symbolication
4143
* if applicable. Returns true if the provided list of stack frames had at least one
4244
* matching registered image.
4345
*/
44-
function patchFrames(frames: Array<StackFrame>): boolean {
46+
// Only exported for tests
47+
export function patchFrames(frames: Array<StackFrame>): boolean {
4548
let hasAtLeastOneWasmFrameWithImage = false;
4649
frames.forEach(frame => {
4750
if (!frame.filename) {
4851
return;
4952
}
50-
const match = frame.filename.match(/^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as
51-
| null
52-
| [string, string, string];
53+
54+
const split = frame.filename.split('(');
55+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56+
const lastSplit = split[split.length - 1]!;
57+
58+
// Let's call this first match a "messy match".
59+
// The browser stacktrace parser spits out frames that have a filename like this: "int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb"
60+
// 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)"
61+
// This first match simply tries to mitigate the mess up until the first opening parens.
62+
// The match afterwards is a sensible fallback
63+
let match = lastSplit.match(PARSER_REGEX) as null | [string, string, string];
64+
65+
if (!match) {
66+
match = frame.filename.match(PARSER_REGEX) as null | [string, string, string];
67+
}
68+
5369
if (match) {
5470
const index = getImage(match[1]);
5571
frame.instruction_addr = match[2];
@@ -62,5 +78,6 @@ function patchFrames(frames: Array<StackFrame>): boolean {
6278
}
6379
}
6480
});
81+
6582
return hasAtLeastOneWasmFrameWithImage;
6683
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { patchFrames } from '../src/index';
3+
4+
describe('patchFrames()', () => {
5+
it('should correctly extract instruction addresses', () => {
6+
const frames = [
7+
{
8+
colno: 5,
9+
filename: 'http://localhost:8001/main.js',
10+
function: 'run',
11+
in_app: true,
12+
lineno: 7101,
13+
},
14+
{
15+
colno: 71,
16+
filename: 'http://localhost:8001/main.js',
17+
function: 'doRun',
18+
in_app: true,
19+
lineno: 7084,
20+
},
21+
{
22+
colno: 9,
23+
filename: 'http://localhost:8001/main.html',
24+
function: 'Object.onRuntimeInitialized',
25+
in_app: true,
26+
lineno: 39,
27+
},
28+
{
29+
colno: 11,
30+
filename: 'http://localhost:8001/main.html',
31+
function: 'captureError',
32+
in_app: true,
33+
lineno: 27,
34+
},
35+
{
36+
colno: 22,
37+
filename: 'http://localhost:8001/main.html',
38+
function: 'myFunctionVectorOutOfBounds',
39+
in_app: true,
40+
lineno: 18,
41+
},
42+
{
43+
colno: 27,
44+
filename: 'http://localhost:8001/main.js',
45+
function: 'ClassHandle.MyClass$getAt [as getAt]',
46+
in_app: true,
47+
lineno: 2201,
48+
},
49+
{
50+
filename:
51+
'int) const, int, MyClass const*, int>::invoke(int (MyClass::* const&)(int) const, MyClass const*, int) (http://localhost:8001/main.wasm:wasm-function[152]:0x47df',
52+
function: 'emscripten::internal::MethodInvoker<int (MyClass::*)',
53+
in_app: true,
54+
},
55+
{
56+
filename: 'int) const (http://localhost:8001/main.wasm:wasm-function[182]:0x540b',
57+
function: 'MyClass::getAt',
58+
in_app: true,
59+
},
60+
{
61+
filename: 'int) const (http://localhost:8001/main.wasm:wasm-function[186]:0x5637',
62+
function: 'MyClass::foo',
63+
in_app: true,
64+
},
65+
{
66+
filename: 'int) const (http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb',
67+
function: 'MyClass::bar',
68+
in_app: true,
69+
},
70+
{
71+
filename: 'http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb',
72+
function: 'MyClass::bar',
73+
in_app: true,
74+
},
75+
];
76+
77+
patchFrames(frames);
78+
79+
expect(frames).toStrictEqual([
80+
{
81+
colno: 5,
82+
filename: 'http://localhost:8001/main.js',
83+
function: 'run',
84+
in_app: true,
85+
lineno: 7101,
86+
},
87+
{
88+
colno: 71,
89+
filename: 'http://localhost:8001/main.js',
90+
function: 'doRun',
91+
in_app: true,
92+
lineno: 7084,
93+
},
94+
{
95+
colno: 9,
96+
filename: 'http://localhost:8001/main.html',
97+
function: 'Object.onRuntimeInitialized',
98+
in_app: true,
99+
lineno: 39,
100+
},
101+
{
102+
colno: 11,
103+
filename: 'http://localhost:8001/main.html',
104+
function: 'captureError',
105+
in_app: true,
106+
lineno: 27,
107+
},
108+
{
109+
colno: 22,
110+
filename: 'http://localhost:8001/main.html',
111+
function: 'myFunctionVectorOutOfBounds',
112+
in_app: true,
113+
lineno: 18,
114+
},
115+
{
116+
colno: 27,
117+
filename: 'http://localhost:8001/main.js',
118+
function: 'ClassHandle.MyClass$getAt [as getAt]',
119+
in_app: true,
120+
lineno: 2201,
121+
},
122+
{
123+
filename: 'http://localhost:8001/main.wasm',
124+
function: 'emscripten::internal::MethodInvoker<int (MyClass::*)',
125+
in_app: true,
126+
instruction_addr: '0x47df',
127+
platform: 'native',
128+
},
129+
{
130+
filename: 'http://localhost:8001/main.wasm',
131+
function: 'MyClass::getAt',
132+
in_app: true,
133+
instruction_addr: '0x540b',
134+
platform: 'native',
135+
},
136+
{
137+
filename: 'http://localhost:8001/main.wasm',
138+
function: 'MyClass::foo',
139+
in_app: true,
140+
instruction_addr: '0x5637',
141+
platform: 'native',
142+
},
143+
{
144+
filename: 'http://localhost:8001/main.wasm',
145+
function: 'MyClass::bar',
146+
in_app: true,
147+
instruction_addr: '0x5aeb',
148+
platform: 'native',
149+
},
150+
{
151+
filename: 'http://localhost:8001/main.wasm',
152+
function: 'MyClass::bar',
153+
in_app: true,
154+
instruction_addr: '0x5aeb',
155+
platform: 'native',
156+
},
157+
]);
158+
});
159+
});

packages/wasm/test/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../tsconfig.test.json"
3+
}

packages/wasm/tsconfig.test.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["test/**/*", "vite.config.ts"],
4+
"compilerOptions": {
5+
"types": ["node"],
6+
"lib": ["DOM", "ESNext"]
7+
}
8+
}

packages/wasm/vite.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import baseConfig from '../../vite/vite.config';
2+
3+
export default {
4+
...baseConfig,
5+
test: {
6+
...baseConfig.test,
7+
},
8+
};

0 commit comments

Comments
 (0)