Skip to content

Commit 46225eb

Browse files
authored
feat(wasm): Add applicationKey option for third-party error filtering (#18762)
Adds support for applying an application key to WASM stack frames that can be then used in the `thirdPartyErrorFilterIntegration` for detection of first-party code. This changes how `thirdPartyErrorFilterIntegration` deals with native frames to also check for a `instruction_addr` for WASM-native code. Usage: ```js Sentry.init({ integrations: [ wasmIntegration({ applicationKey: 'your-custom-application-key' }), ←───┐ thirdPartyErrorFilterIntegration({ │ behaviour: 'drop-error-if-exclusively-contains-third-party-frames', ├─ matching keys filterKeys: ['your-custom-application-key'] ←─────────────────────────┘ }), ], }); ``` Closes: #18705
1 parent fd42f3b commit 46225eb

File tree

7 files changed

+236
-6
lines changed

7 files changed

+236
-6
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@
2323
Sentry.metrics.count('response_time', 283.33, { unit: 'millisecond' });
2424
```
2525

26+
- **feat(wasm): Add applicationKey option for third-party error filtering ([#18762])(https://github.com/getsentry/sentry-javascript/pull/18762)**
27+
28+
Adds support for applying an application key to WASM stack frames that can be then used in the `thirdPartyErrorFilterIntegration` for detection of first-party code.
29+
30+
Usage:
31+
32+
```js
33+
Sentry.init({
34+
integrations: [
35+
// Integration order matters: wasmIntegration needs to be before thirdPartyErrorFilterIntegration
36+
wasmIntegration({ applicationKey: 'your-custom-application-key' }), ←───┐
37+
thirdPartyErrorFilterIntegration({ │
38+
behaviour: 'drop-error-if-exclusively-contains-third-party-frames', ├─ matching keys
39+
filterKeys: ['your-custom-application-key'] ←─────────────────────────┘
40+
}),
41+
],
42+
});
43+
```
44+
2645
- ref(nextjs): Drop `resolve` dependency from the Next.js SDK ([#18618](https://github.com/getsentry/sentry-javascript/pull/18618))
2746

2847
## 10.32.1
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { thirdPartyErrorFilterIntegration } from '@sentry/browser';
3+
import { wasmIntegration } from '@sentry/wasm';
4+
5+
// Simulate what the bundler plugin would inject to mark JS code as first-party
6+
var _sentryModuleMetadataGlobal =
7+
typeof window !== 'undefined'
8+
? window
9+
: typeof global !== 'undefined'
10+
? global
11+
: typeof self !== 'undefined'
12+
? self
13+
: {};
14+
15+
_sentryModuleMetadataGlobal._sentryModuleMetadata = _sentryModuleMetadataGlobal._sentryModuleMetadata || {};
16+
17+
_sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.assign(
18+
{},
19+
_sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack],
20+
{
21+
'_sentryBundlerPluginAppKey:wasm-test-app': true,
22+
},
23+
);
24+
25+
window.Sentry = Sentry;
26+
27+
Sentry.init({
28+
dsn: 'https://[email protected]/1337',
29+
integrations: [
30+
wasmIntegration({ applicationKey: 'wasm-test-app' }),
31+
thirdPartyErrorFilterIntegration({
32+
behaviour: 'apply-tag-if-contains-third-party-frames',
33+
filterKeys: ['wasm-test-app'],
34+
}),
35+
],
36+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Simulate what the bundler plugin would inject to mark this JS file as first-party
2+
var _sentryModuleMetadataGlobal =
3+
typeof window !== 'undefined'
4+
? window
5+
: typeof global !== 'undefined'
6+
? global
7+
: typeof self !== 'undefined'
8+
? self
9+
: {};
10+
11+
_sentryModuleMetadataGlobal._sentryModuleMetadata = _sentryModuleMetadataGlobal._sentryModuleMetadata || {};
12+
13+
_sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.assign(
14+
{},
15+
_sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack],
16+
{
17+
'_sentryBundlerPluginAppKey:wasm-test-app': true,
18+
},
19+
);
20+
21+
async function runWasm() {
22+
function crash() {
23+
throw new Error('WASM triggered error');
24+
}
25+
26+
const { instance } = await WebAssembly.instantiateStreaming(fetch('https://localhost:5887/simple.wasm'), {
27+
env: {
28+
external_func: crash,
29+
},
30+
});
31+
32+
instance.exports.internal_func();
33+
}
34+
35+
runWasm();
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect } from '@playwright/test';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { sentryTest } from '../../../utils/fixtures';
5+
import { envelopeRequestParser, waitForErrorRequest } from '../../../utils/helpers';
6+
import { shouldSkipWASMTests } from '../../../utils/wasmHelpers';
7+
8+
const bundle = process.env.PW_BUNDLE || '';
9+
// We only want to run this in non-CDN bundle mode because both
10+
// wasmIntegration and thirdPartyErrorFilterIntegration are only available in NPM packages
11+
if (bundle.startsWith('bundle')) {
12+
sentryTest.skip();
13+
}
14+
15+
sentryTest(
16+
'WASM frames should be recognized as first-party when applicationKey is configured',
17+
async ({ getLocalTestUrl, page, browserName }) => {
18+
if (shouldSkipWASMTests(browserName)) {
19+
sentryTest.skip();
20+
}
21+
22+
const url = await getLocalTestUrl({ testDir: __dirname });
23+
24+
await page.route('**/simple.wasm', route => {
25+
const wasmModule = fs.readFileSync(path.resolve(__dirname, '../simple.wasm'));
26+
27+
return route.fulfill({
28+
status: 200,
29+
body: wasmModule,
30+
headers: {
31+
'Content-Type': 'application/wasm',
32+
},
33+
});
34+
});
35+
36+
const errorEventPromise = waitForErrorRequest(page, e => {
37+
return e.exception?.values?.[0]?.value === 'WASM triggered error';
38+
});
39+
40+
await page.goto(url);
41+
42+
const errorEvent = envelopeRequestParser(await errorEventPromise);
43+
44+
expect(errorEvent.tags?.third_party_code).toBeUndefined();
45+
46+
// Verify we have WASM frames in the stacktrace
47+
expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toEqual(
48+
expect.arrayContaining([
49+
expect.objectContaining({
50+
filename: expect.stringMatching(/simple\.wasm$/),
51+
platform: 'native',
52+
}),
53+
]),
54+
);
55+
},
56+
);

packages/core/src/integrations/third-party-errors-filter.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,13 @@ function getBundleKeysForAllFramesWithFilenames(
153153

154154
return frames
155155
.filter((frame, index) => {
156-
// Exclude frames without a filename or without lineno and colno,
157-
// since these are likely native code or built-ins
158-
if (!frame.filename || (frame.lineno == null && frame.colno == null)) {
156+
// Exclude frames without a filename
157+
if (!frame.filename) {
158+
return false;
159+
}
160+
// Exclude frames without location info, since these are likely native code or built-ins.
161+
// JS frames have lineno/colno, WASM frames have instruction_addr instead.
162+
if (frame.lineno == null && frame.colno == null && frame.instruction_addr == null) {
159163
return false;
160164
}
161165
// Optionally ignore Sentry internal frames

packages/wasm/src/index.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ import { getImage, getImages } from './registry';
55

66
const INTEGRATION_NAME = 'Wasm';
77

8-
const _wasmIntegration = (() => {
8+
interface WasmIntegrationOptions {
9+
/**
10+
* Key to identify this application for third-party error filtering.
11+
* This key should match one of the keys provided to the `filterKeys` option
12+
* of the `thirdPartyErrorFilterIntegration`.
13+
*/
14+
applicationKey?: string;
15+
}
16+
17+
const _wasmIntegration = ((options: WasmIntegrationOptions = {}) => {
918
return {
1019
name: INTEGRATION_NAME,
1120
setupOnce() {
@@ -18,7 +27,7 @@ const _wasmIntegration = (() => {
1827
event.exception.values.forEach(exception => {
1928
if (exception.stacktrace?.frames) {
2029
hasAtLeastOneWasmFrameWithImage =
21-
hasAtLeastOneWasmFrameWithImage || patchFrames(exception.stacktrace.frames);
30+
hasAtLeastOneWasmFrameWithImage || patchFrames(exception.stacktrace.frames, options.applicationKey);
2231
}
2332
});
2433
}
@@ -37,13 +46,17 @@ export const wasmIntegration = defineIntegration(_wasmIntegration);
3746

3847
const PARSER_REGEX = /^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/;
3948

49+
// We use the same prefix as bundler plugins so that thirdPartyErrorFilterIntegration
50+
// recognizes WASM frames as first-party code without needing modifications.
51+
const BUNDLER_PLUGIN_APP_KEY_PREFIX = '_sentryBundlerPluginAppKey:';
52+
4053
/**
4154
* Patches a list of stackframes with wasm data needed for server-side symbolication
4255
* if applicable. Returns true if the provided list of stack frames had at least one
4356
* matching registered image.
4457
*/
4558
// Only exported for tests
46-
export function patchFrames(frames: Array<StackFrame>): boolean {
59+
export function patchFrames(frames: Array<StackFrame>, applicationKey?: string): boolean {
4760
let hasAtLeastOneWasmFrameWithImage = false;
4861
frames.forEach(frame => {
4962
if (!frame.filename) {
@@ -71,6 +84,13 @@ export function patchFrames(frames: Array<StackFrame>): boolean {
7184
frame.filename = match[1];
7285
frame.platform = 'native';
7386

87+
if (applicationKey) {
88+
frame.module_metadata = {
89+
...frame.module_metadata,
90+
[`${BUNDLER_PLUGIN_APP_KEY_PREFIX}${applicationKey}`]: true,
91+
};
92+
}
93+
7494
if (index >= 0) {
7595
frame.addr_mode = `rel:${index}`;
7696
hasAtLeastOneWasmFrameWithImage = true;

packages/wasm/test/stacktrace-parsing.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,67 @@
1+
import type { StackFrame } from '@sentry/core';
12
import { describe, expect, it } from 'vitest';
23
import { patchFrames } from '../src/index';
34

45
describe('patchFrames()', () => {
6+
it('should add module_metadata with applicationKey when provided', () => {
7+
const frames: StackFrame[] = [
8+
{
9+
filename: 'http://localhost:8001/main.js',
10+
function: 'run',
11+
in_app: true,
12+
},
13+
{
14+
filename: 'http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb',
15+
function: 'MyClass::bar',
16+
in_app: true,
17+
},
18+
];
19+
20+
patchFrames(frames, 'my-app');
21+
22+
// Non-WASM frame should not have module_metadata
23+
expect(frames[0]?.module_metadata).toBeUndefined();
24+
25+
// WASM frame should have module_metadata with the application key
26+
expect(frames[1]?.module_metadata).toEqual({
27+
'_sentryBundlerPluginAppKey:my-app': true,
28+
});
29+
});
30+
31+
it('should preserve existing module_metadata when adding applicationKey', () => {
32+
const frames: StackFrame[] = [
33+
{
34+
filename: 'http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb',
35+
function: 'MyClass::bar',
36+
in_app: true,
37+
module_metadata: {
38+
existingKey: 'existingValue',
39+
},
40+
},
41+
];
42+
43+
patchFrames(frames, 'my-app');
44+
45+
expect(frames[0]?.module_metadata).toEqual({
46+
existingKey: 'existingValue',
47+
'_sentryBundlerPluginAppKey:my-app': true,
48+
});
49+
});
50+
51+
it('should not add module_metadata when applicationKey is not provided', () => {
52+
const frames: StackFrame[] = [
53+
{
54+
filename: 'http://localhost:8001/main.wasm:wasm-function[190]:0x5aeb',
55+
function: 'MyClass::bar',
56+
in_app: true,
57+
},
58+
];
59+
60+
patchFrames(frames);
61+
62+
expect(frames[0]?.module_metadata).toBeUndefined();
63+
});
64+
565
it('should correctly extract instruction addresses', () => {
666
const frames = [
767
{

0 commit comments

Comments
 (0)