Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// This worker manually just replicates what the actual Sentry.registerWebWorkerWasm() does

const origInstantiateStreaming = WebAssembly.instantiateStreaming;
WebAssembly.instantiateStreaming = function instantiateStreaming(response, importObject) {
return Promise.resolve(response).then(res => {
return origInstantiateStreaming(res, importObject).then(rv => {
if (res.url) {
registerModuleAndForward(rv.module, res.url);
}
return rv;
});
});
};

function registerModuleAndForward(module, url) {
const buildId = getBuildId(module);

if (buildId) {
const image = {
type: 'wasm',
code_id: buildId,
code_file: url,
debug_file: null,
debug_id: (buildId + '00000000000000000000000000000000').slice(0, 32) + '0',
};

self.postMessage({
_sentryMessage: true,
_sentryWasmImages: [image],
});
}
}

// Extract build ID from WASM module
function getBuildId(module) {
const sections = WebAssembly.Module.customSections(module, 'build_id');
if (sections.length > 0) {
const buildId = Array.from(new Uint8Array(sections[0]))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return buildId;
}
return null;
}

// Handle messages from the main thread
self.addEventListener('message', async event => {
if (event.data.type === 'load-wasm-and-crash') {
const wasmUrl = event.data.wasmUrl;

function crash() {
throw new Error('WASM error from worker');
}

try {
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
env: {
external_func: crash,
},
});

instance.exports.internal_func();
} catch (err) {
self.postMessage({
_sentryMessage: true,
_sentryWorkerError: {
reason: err,
filename: self.location.href,
},
});
}
}
});

self.addEventListener('unhandledrejection', event => {
self.postMessage({
_sentryMessage: true,
_sentryWorkerError: {
reason: event.reason,
filename: self.location.href,
},
});
});

// Let the main thread know that worker is ready
self.postMessage({ _sentryMessage: false, type: 'WORKER_READY' });
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/browser';
import { wasmIntegration } from '@sentry/wasm';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [wasmIntegration({ applicationKey: 'wasm-worker-app' })],
});

const worker = new Worker('/worker.js');

Sentry.addIntegration(Sentry.webWorkerIntegration({ worker }));

window.wasmWorker = worker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
window.events = [];

window.triggerWasmError = () => {
window.wasmWorker.postMessage({
type: 'load-wasm-and-crash',
wasmUrl: 'https://localhost:5887/simple.wasm',
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="triggerWasmError">Trigger WASM Error in Worker</button>
</body>
</html>
138 changes: 138 additions & 0 deletions dev-packages/browser-integration-tests/suites/wasm/webWorker/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../utils/helpers';
import { shouldSkipWASMTests } from '../../../utils/wasmHelpers';

declare global {
interface Window {
wasmWorker: Worker;
triggerWasmError: () => void;
}
}

const bundle = process.env.PW_BUNDLE || '';
if (bundle.startsWith('bundle')) {
sentryTest.skip();
}

sentryTest(
'WASM debug images from worker should be forwarded to main thread and attached to events',
async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipWASMTests(browserName)) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/simple.wasm', route => {
const wasmModule = fs.readFileSync(path.resolve(__dirname, '../simple.wasm'));
return route.fulfill({
status: 200,
body: wasmModule,
headers: {
'Content-Type': 'application/wasm',
},
});
});

await page.route('**/worker.js', route => {
return route.fulfill({
path: `${__dirname}/assets/worker.js`,
});
});

const errorEventPromise = waitForErrorRequest(page, e => {
return e.exception?.values?.[0]?.value === 'WASM error from worker';
});

await page.goto(url);

await page.waitForFunction(() => window.wasmWorker !== undefined);

await page.evaluate(() => {
window.triggerWasmError();
});

const errorEvent = envelopeRequestParser(await errorEventPromise);

expect(errorEvent.exception?.values?.[0]?.value).toBe('WASM error from worker');

expect(errorEvent.debug_meta?.images).toBeDefined();
expect(errorEvent.debug_meta?.images).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'wasm',
code_file: expect.stringMatching(/simple\.wasm$/),
code_id: '0ba020cdd2444f7eafdd25999a8e9010',
debug_id: '0ba020cdd2444f7eafdd25999a8e90100',
}),
]),
);

const wasmFrame = errorEvent.exception?.values?.[0]?.stacktrace?.frames?.find(
frame => frame.filename && frame.filename.includes('simple.wasm'),
);

if (wasmFrame) {
expect(wasmFrame.platform).toBe('native');
expect(wasmFrame.instruction_addr).toBeDefined();
expect(wasmFrame.addr_mode).toMatch(/^rel:\d+$/);
}
},
);

sentryTest(
'WASM frames from worker should be recognized as first-party when applicationKey is configured',
async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipWASMTests(browserName)) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('**/simple.wasm', route => {
const wasmModule = fs.readFileSync(path.resolve(__dirname, '../simple.wasm'));
return route.fulfill({
status: 200,
body: wasmModule,
headers: {
'Content-Type': 'application/wasm',
},
});
});

await page.route('**/worker.js', route => {
return route.fulfill({
path: `${__dirname}/assets/worker.js`,
});
});

const errorEventPromise = waitForErrorRequest(page, e => {
return e.exception?.values?.[0]?.value === 'WASM error from worker';
});

await page.goto(url);

await page.waitForFunction(() => window.wasmWorker !== undefined);

await page.evaluate(() => {
window.triggerWasmError();
});

const errorEvent = envelopeRequestParser(await errorEventPromise);

const wasmFrame = errorEvent.exception?.values?.[0]?.stacktrace?.frames?.find(
frame => frame.filename && frame.filename.includes('simple.wasm'),
);

if (wasmFrame) {
expect(wasmFrame.module_metadata).toEqual(
expect.objectContaining({
'_sentryBundlerPluginAppKey:wasm-worker-app': true,
}),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test conditionals may cause silent assertion skipping

Medium Severity · Bugbot Rules

The integration tests use if (wasmFrame) conditionals that silently skip assertions when the frame isn't found. The second test ("WASM frames from worker should be recognized as first-party when applicationKey is configured") has no assertions at all before the conditional, so if wasmFrame is undefined, the entire test passes without verifying anything. This violates the rule about conditionals in tests: "Flag usage of conditionals in one test and recommend splitting up the test for the different paths." The existing WASM test in suites/wasm/test.ts uses expect.arrayContaining to directly assert on frames, which properly fails if the expected frame is missing.

Additional Locations (1)

Fix in Cursor Fix in Web

},
);
36 changes: 33 additions & 3 deletions packages/browser/src/integrations/webWorker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Integration, IntegrationFn } from '@sentry/core';
import type { DebugImage, Integration, IntegrationFn } from '@sentry/core';
import { captureEvent, debug, defineIntegration, getClient, isPlainObject, isPrimitive } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
import { eventFromUnknownInput } from '../eventbuilder';
Expand All @@ -12,6 +12,7 @@ interface WebWorkerMessage {
_sentryDebugIds?: Record<string, string>;
_sentryModuleMetadata?: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
_sentryWorkerError?: SerializedWorkerError;
_sentryWasmImages?: Array<DebugImage>;
}

interface SerializedWorkerError {
Expand Down Expand Up @@ -135,6 +136,23 @@ function listenForSentryMessages(worker: Worker): void {
};
}

// Handle WASM images from worker
if (event.data._sentryWasmImages) {
DEBUG_BUILD && debug.log('Sentry WASM images web worker message received', event.data);
const existingImages =
(WINDOW as typeof WINDOW & { _sentryWasmImages?: Array<DebugImage> })._sentryWasmImages || [];
const newImages = event.data._sentryWasmImages.filter(
(newImg: unknown) =>
isPlainObject(newImg) &&
typeof newImg.code_file === 'string' &&
!existingImages.some(existing => existing.code_file === newImg.code_file),
);
(WINDOW as typeof WINDOW & { _sentryWasmImages?: Array<DebugImage> })._sentryWasmImages = [
...existingImages,
...(newImages as Array<DebugImage>),
];
}

// Handle unhandled rejections forwarded from worker
if (event.data._sentryWorkerError) {
DEBUG_BUILD && debug.log('Sentry worker rejection message received', event.data._sentryWorkerError);
Expand Down Expand Up @@ -270,12 +288,13 @@ function isSentryMessage(eventData: unknown): eventData is WebWorkerMessage {
return false;
}

// Must have at least one of: debug IDs, module metadata, or worker error
// Must have at least one of: debug IDs, module metadata, worker error, or WASM images
const hasDebugIds = '_sentryDebugIds' in eventData;
const hasModuleMetadata = '_sentryModuleMetadata' in eventData;
const hasWorkerError = '_sentryWorkerError' in eventData;
const hasWasmImages = '_sentryWasmImages' in eventData;

if (!hasDebugIds && !hasModuleMetadata && !hasWorkerError) {
if (!hasDebugIds && !hasModuleMetadata && !hasWorkerError && !hasWasmImages) {
return false;
}

Expand All @@ -297,5 +316,16 @@ function isSentryMessage(eventData: unknown): eventData is WebWorkerMessage {
return false;
}

// Validate WASM images if present
if (
hasWasmImages &&
(!Array.isArray(eventData._sentryWasmImages) ||
!eventData._sentryWasmImages.every(
(img: unknown) => isPlainObject(img) && typeof (img as { code_file?: unknown }).code_file === 'string',
))
) {
return false;
}

return true;
}
Loading