diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/assets/worker.js b/dev-packages/browser-integration-tests/suites/wasm/webWorker/assets/worker.js
new file mode 100644
index 000000000000..3e92191373c0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/assets/worker.js
@@ -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' });
diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js b/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js
new file mode 100644
index 000000000000..39007ede4bc0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js
@@ -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;
diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/subject.js b/dev-packages/browser-integration-tests/suites/wasm/webWorker/subject.js
new file mode 100644
index 000000000000..22d1f7c57f03
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/subject.js
@@ -0,0 +1,8 @@
+window.events = [];
+
+window.triggerWasmError = () => {
+ window.wasmWorker.postMessage({
+ type: 'load-wasm-and-crash',
+ wasmUrl: 'https://localhost:5887/simple.wasm',
+ });
+};
diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/template.html b/dev-packages/browser-integration-tests/suites/wasm/webWorker/template.html
new file mode 100644
index 000000000000..37ffd3e87b17
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/test.ts b/dev-packages/browser-integration-tests/suites/wasm/webWorker/test.ts
new file mode 100644
index 000000000000..312bfd98872f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/test.ts
@@ -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,
+ }),
+ );
+ }
+ },
+);
diff --git a/packages/browser/src/integrations/webWorker.ts b/packages/browser/src/integrations/webWorker.ts
index 5af6c3b2553a..dcf540f9a0c0 100644
--- a/packages/browser/src/integrations/webWorker.ts
+++ b/packages/browser/src/integrations/webWorker.ts
@@ -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';
@@ -12,6 +12,7 @@ interface WebWorkerMessage {
_sentryDebugIds?: Record;
_sentryModuleMetadata?: Record; // eslint-disable-line @typescript-eslint/no-explicit-any
_sentryWorkerError?: SerializedWorkerError;
+ _sentryWasmImages?: Array;
}
interface SerializedWorkerError {
@@ -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 })._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 })._sentryWasmImages = [
+ ...existingImages,
+ ...(newImages as Array),
+ ];
+ }
+
// Handle unhandled rejections forwarded from worker
if (event.data._sentryWorkerError) {
DEBUG_BUILD && debug.log('Sentry worker rejection message received', event.data._sentryWorkerError);
@@ -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;
}
@@ -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;
}
diff --git a/packages/browser/test/integrations/webWorker.test.ts b/packages/browser/test/integrations/webWorker.test.ts
index 584f18ee9a75..4696f78888e3 100644
--- a/packages/browser/test/integrations/webWorker.test.ts
+++ b/packages/browser/test/integrations/webWorker.test.ts
@@ -278,6 +278,108 @@ describe('webWorkerIntegration', () => {
expect(mockEvent.stopImmediatePropagation).not.toHaveBeenCalled();
});
+ it('processes WASM images from worker', () => {
+ (helpers.WINDOW as any)._sentryWasmImages = undefined;
+ const wasmImages = [
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/worker.wasm',
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ ];
+
+ mockEvent.data = {
+ _sentryMessage: true,
+ _sentryWasmImages: wasmImages,
+ };
+
+ messageHandler(mockEvent);
+
+ expect(mockEvent.stopImmediatePropagation).toHaveBeenCalled();
+ expect(mockDebugLog).toHaveBeenCalledWith('Sentry WASM images web worker message received', mockEvent.data);
+ expect((helpers.WINDOW as any)._sentryWasmImages).toEqual(wasmImages);
+ });
+
+ it('deduplicates WASM images by code_file URL', () => {
+ (helpers.WINDOW as any)._sentryWasmImages = [
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/existing.wasm',
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ ];
+
+ mockEvent.data = {
+ _sentryMessage: true,
+ _sentryWasmImages: [
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/existing.wasm', // duplicate, should be ignored
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ {
+ type: 'wasm',
+ code_id: 'def456',
+ code_file: 'http://localhost:8001/new.wasm', // new, should be added
+ debug_file: null,
+ debug_id: 'def45600000000000000000000000000',
+ },
+ ],
+ };
+
+ messageHandler(mockEvent);
+
+ expect((helpers.WINDOW as any)._sentryWasmImages).toEqual([
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/existing.wasm',
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ {
+ type: 'wasm',
+ code_id: 'def456',
+ code_file: 'http://localhost:8001/new.wasm',
+ debug_file: null,
+ debug_id: 'def45600000000000000000000000000',
+ },
+ ]);
+ });
+
+ it('ignores invalid WASM images (not an array)', () => {
+ mockEvent.data = {
+ _sentryMessage: true,
+ _sentryWasmImages: 'not-an-array',
+ };
+
+ messageHandler(mockEvent);
+
+ expect(mockEvent.stopImmediatePropagation).not.toHaveBeenCalled();
+ });
+
+ it('ignores WASM images with invalid array elements (null, undefined, missing code_file)', () => {
+ mockEvent.data = {
+ _sentryMessage: true,
+ _sentryWasmImages: [
+ null,
+ undefined,
+ { type: 'wasm' },
+ { code_file: 123 },
+ ],
+ };
+
+ messageHandler(mockEvent);
+
+ expect(mockEvent.stopImmediatePropagation).not.toHaveBeenCalled();
+ });
+
it('gives main thread precedence over worker for conflicting module metadata', () => {
(helpers.WINDOW as any)._sentryModuleMetadata = {
'Error\n at shared-file.js:1:1': { '_sentryBundlerPluginAppKey:main-app': true, source: 'main' },
diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts
index 84076285fcdd..dc29e3950ba7 100644
--- a/packages/wasm/src/index.ts
+++ b/packages/wasm/src/index.ts
@@ -1,10 +1,26 @@
-import type { Event, IntegrationFn, StackFrame } from '@sentry/core';
-import { defineIntegration } from '@sentry/core';
+import type { DebugImage, Event, IntegrationFn, StackFrame } from '@sentry/core';
+import { defineIntegration, GLOBAL_OBJ } from '@sentry/core';
import { patchWebAssembly } from './patchWebAssembly';
-import { getImage, getImages } from './registry';
+import { getImage, getImages, getModuleInfo } from './registry';
const INTEGRATION_NAME = 'Wasm';
+// We use the same prefix as bundler plugins so that thirdPartyErrorFilterIntegration
+// recognizes WASM frames as first-party code without needing modifications.
+const BUNDLER_PLUGIN_APP_KEY_PREFIX = '_sentryBundlerPluginAppKey:';
+
+/**
+ * Minimal interface for DedicatedWorkerGlobalScope.
+ * We can't use the actual type because it breaks everyone who doesn't have {"lib": ["WebWorker"]}
+ */
+interface MinimalDedicatedWorkerGlobalScope {
+ postMessage: (message: unknown) => void;
+}
+
+interface RegisterWebWorkerWasmOptions {
+ self: MinimalDedicatedWorkerGlobalScope;
+}
+
interface WasmIntegrationOptions {
/**
* Key to identify this application for third-party error filtering.
@@ -14,6 +30,11 @@ interface WasmIntegrationOptions {
applicationKey?: string;
}
+// Access WINDOW with proper typing for _sentryWasmImages
+const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
+ _sentryWasmImages?: Array;
+};
+
const _wasmIntegration = ((options: WasmIntegrationOptions = {}) => {
return {
name: INTEGRATION_NAME,
@@ -23,18 +44,23 @@ const _wasmIntegration = ((options: WasmIntegrationOptions = {}) => {
processEvent(event: Event): Event {
let hasAtLeastOneWasmFrameWithImage = false;
+ const existingImagesCount = event.debug_meta?.images?.length || 0;
+
if (event.exception?.values) {
event.exception.values.forEach(exception => {
if (exception.stacktrace?.frames) {
hasAtLeastOneWasmFrameWithImage =
- hasAtLeastOneWasmFrameWithImage || patchFrames(exception.stacktrace.frames, options.applicationKey);
+ hasAtLeastOneWasmFrameWithImage ||
+ patchFrames(exception.stacktrace.frames, options.applicationKey, existingImagesCount);
}
});
}
if (hasAtLeastOneWasmFrameWithImage) {
event.debug_meta = event.debug_meta || {};
- event.debug_meta.images = [...(event.debug_meta.images || []), ...getImages()];
+ const mainThreadImages = getImages();
+ const workerImages = WINDOW._sentryWasmImages || [];
+ event.debug_meta.images = [...(event.debug_meta.images || []), ...mainThreadImages, ...workerImages];
}
return event;
@@ -46,17 +72,22 @@ export const wasmIntegration = defineIntegration(_wasmIntegration);
const PARSER_REGEX = /^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/;
-// We use the same prefix as bundler plugins so that thirdPartyErrorFilterIntegration
-// recognizes WASM frames as first-party code without needing modifications.
-const BUNDLER_PLUGIN_APP_KEY_PREFIX = '_sentryBundlerPluginAppKey:';
-
/**
* 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.
+ *
+ * @param frames - Stack frames to patch
+ * @param applicationKey - Optional key for third-party error filtering
+ * @param existingImagesOffset - Number of existing debug images that will be prepended
+ * to the final images array (used to calculate correct addr_mode indices)
*/
// Only exported for tests
-export function patchFrames(frames: Array, applicationKey?: string): boolean {
+export function patchFrames(
+ frames: Array,
+ applicationKey?: string,
+ existingImagesOffset: number = 0,
+): boolean {
let hasAtLeastOneWasmFrameWithImage = false;
frames.forEach(frame => {
if (!frame.filename) {
@@ -80,6 +111,7 @@ export function patchFrames(frames: Array, applicationKey?: string):
if (match) {
const index = getImage(match[1]);
+ const workerImageIndex = getWorkerImage(match[1]);
frame.instruction_addr = match[2];
frame.filename = match[1];
frame.platform = 'native';
@@ -92,7 +124,11 @@ export function patchFrames(frames: Array, applicationKey?: string):
}
if (index >= 0) {
- frame.addr_mode = `rel:${index}`;
+ frame.addr_mode = `rel:${existingImagesOffset + index}`;
+ hasAtLeastOneWasmFrameWithImage = true;
+ } else if (workerImageIndex >= 0) {
+ const mainThreadImagesCount = getImages().length;
+ frame.addr_mode = `rel:${existingImagesOffset + mainThreadImagesCount + workerImageIndex}`;
hasAtLeastOneWasmFrameWithImage = true;
}
}
@@ -100,3 +136,101 @@ export function patchFrames(frames: Array, applicationKey?: string):
return hasAtLeastOneWasmFrameWithImage;
}
+
+/**
+ * Looks up an image by URL in worker images.
+ */
+function getWorkerImage(url: string): number {
+ const workerImages = WINDOW._sentryWasmImages || [];
+ return workerImages.findIndex(image => {
+ return image.type === 'wasm' && image.code_file === url;
+ });
+}
+
+/**
+ * Use this function to register WASM support in a web worker.
+ *
+ * This function will:
+ * - Patch WebAssembly.instantiateStreaming and WebAssembly.compileStreaming in the worker
+ * - Forward WASM debug images to the parent thread for symbolication
+ *
+ * @param options {RegisterWebWorkerWasmOptions} Options:
+ * - `self`: The worker's global scope (self).
+ */
+export function registerWebWorkerWasm({ self }: RegisterWebWorkerWasmOptions): void {
+ patchWebAssemblyWithForwarding(self);
+}
+
+/**
+ * Patches the WebAssembly object in the worker scope and forwards
+ * registered modules to the parent thread.
+ */
+function patchWebAssemblyWithForwarding(workerSelf: MinimalDedicatedWorkerGlobalScope): void {
+ if ('instantiateStreaming' in WebAssembly) {
+ const origInstantiateStreaming = WebAssembly.instantiateStreaming;
+ WebAssembly.instantiateStreaming = function instantiateStreaming(
+ response: Response | PromiseLike,
+ importObject: WebAssembly.Imports,
+ ): Promise {
+ return Promise.resolve(response).then(response => {
+ return origInstantiateStreaming(response, importObject).then(rv => {
+ if (response.url) {
+ registerModuleAndForward(rv.module, response.url, workerSelf);
+ }
+ return rv;
+ });
+ });
+ } as typeof WebAssembly.instantiateStreaming;
+ }
+
+ if ('compileStreaming' in WebAssembly) {
+ const origCompileStreaming = WebAssembly.compileStreaming;
+ WebAssembly.compileStreaming = function compileStreaming(
+ source: Response | Promise,
+ ): Promise {
+ return Promise.resolve(source).then(response => {
+ return origCompileStreaming(response).then(module => {
+ if (response.url) {
+ registerModuleAndForward(module, response.url, workerSelf);
+ }
+ return module;
+ });
+ });
+ } as typeof WebAssembly.compileStreaming;
+ }
+}
+
+/**
+ * Registers a WASM module and forwards its debug image to the parent thread.
+ */
+function registerModuleAndForward(
+ module: WebAssembly.Module,
+ url: string,
+ workerSelf: MinimalDedicatedWorkerGlobalScope,
+): void {
+ const { buildId, debugFile } = getModuleInfo(module);
+
+ if (buildId) {
+ let debugFileUrl = null;
+ if (debugFile) {
+ try {
+ debugFileUrl = new URL(debugFile, url).href;
+ } catch {
+ // Ignore
+ }
+ }
+
+ const image: DebugImage = {
+ type: 'wasm',
+ code_id: buildId,
+ code_file: url,
+ debug_file: debugFileUrl,
+ debug_id: `${buildId.padEnd(32, '0').slice(0, 32)}0`,
+ };
+
+ workerSelf.postMessage({
+ _sentryMessage: true,
+ _sentryWasmImages: [image],
+ });
+ }
+}
diff --git a/packages/wasm/test/webworker.test.ts b/packages/wasm/test/webworker.test.ts
new file mode 100644
index 000000000000..afaa5d999966
--- /dev/null
+++ b/packages/wasm/test/webworker.test.ts
@@ -0,0 +1,147 @@
+import type { DebugImage, StackFrame } from '@sentry/core';
+import { GLOBAL_OBJ } from '@sentry/core';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { patchFrames, registerWebWorkerWasm } from '../src/index';
+
+const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
+ _sentryWasmImages?: Array;
+};
+
+describe('registerWebWorkerWasm()', () => {
+ afterEach(() => {
+ delete WINDOW._sentryWasmImages;
+ vi.restoreAllMocks();
+ });
+
+ it('should patch WebAssembly.instantiateStreaming when available', () => {
+ const mockPostMessage = vi.fn();
+ const mockSelf = { postMessage: mockPostMessage };
+
+ const originalInstantiateStreaming = WebAssembly.instantiateStreaming;
+
+ registerWebWorkerWasm({ self: mockSelf });
+
+ expect(WebAssembly.instantiateStreaming).not.toBe(originalInstantiateStreaming);
+
+ WebAssembly.instantiateStreaming = originalInstantiateStreaming;
+ });
+
+ it('should patch WebAssembly.compileStreaming when available', () => {
+ const mockPostMessage = vi.fn();
+ const mockSelf = { postMessage: mockPostMessage };
+
+ const originalCompileStreaming = WebAssembly.compileStreaming;
+
+ registerWebWorkerWasm({ self: mockSelf });
+
+ expect(WebAssembly.compileStreaming).not.toBe(originalCompileStreaming);
+
+ WebAssembly.compileStreaming = originalCompileStreaming;
+ });
+});
+
+describe('patchFrames() with worker images', () => {
+ afterEach(() => {
+ delete WINDOW._sentryWasmImages;
+ });
+
+ it('should find image from worker when main thread has no matching image', () => {
+ WINDOW._sentryWasmImages = [
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/worker.wasm',
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ ];
+
+ const frames: StackFrame[] = [
+ {
+ filename: 'http://localhost:8001/worker.wasm:wasm-function[10]:0x1234',
+ function: 'worker_function',
+ in_app: true,
+ },
+ ];
+
+ const result = patchFrames(frames);
+
+ expect(result).toBe(true);
+ expect(frames[0]?.filename).toBe('http://localhost:8001/worker.wasm');
+ expect(frames[0]?.instruction_addr).toBe('0x1234');
+ expect(frames[0]?.platform).toBe('native');
+ expect(frames[0]?.addr_mode).toBe('rel:0');
+ });
+
+ it('should apply applicationKey to frames from worker images', () => {
+ // Set up worker images
+ WINDOW._sentryWasmImages = [
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/worker.wasm',
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ ];
+
+ const frames: StackFrame[] = [
+ {
+ filename: 'http://localhost:8001/worker.wasm:wasm-function[10]:0x1234',
+ function: 'worker_function',
+ in_app: true,
+ },
+ ];
+
+ patchFrames(frames, 'my-worker-app');
+
+ expect(frames[0]?.module_metadata).toEqual({
+ '_sentryBundlerPluginAppKey:my-worker-app': true,
+ });
+ });
+
+ it('should return false when no matching image exists in main thread or worker', () => {
+ WINDOW._sentryWasmImages = [];
+
+ const frames: StackFrame[] = [
+ {
+ filename: 'http://localhost:8001/unknown.wasm:wasm-function[10]:0x1234',
+ function: 'unknown_function',
+ in_app: true,
+ },
+ ];
+
+ const result = patchFrames(frames);
+
+ expect(result).toBe(false);
+ expect(frames[0]?.filename).toBe('http://localhost:8001/unknown.wasm');
+ expect(frames[0]?.instruction_addr).toBe('0x1234');
+ expect(frames[0]?.platform).toBe('native');
+ expect(frames[0]?.addr_mode).toBeUndefined();
+ });
+
+ it('should offset addr_mode indices when existingImagesOffset is provided', () => {
+ WINDOW._sentryWasmImages = [
+ {
+ type: 'wasm',
+ code_id: 'abc123',
+ code_file: 'http://localhost:8001/worker.wasm',
+ debug_file: null,
+ debug_id: 'abc12300000000000000000000000000',
+ },
+ ];
+
+ const frames: StackFrame[] = [
+ {
+ filename: 'http://localhost:8001/worker.wasm:wasm-function[10]:0x1234',
+ function: 'worker_function',
+ in_app: true,
+ },
+ ];
+
+ const result = patchFrames(frames, undefined, 3);
+
+ expect(result).toBe(true);
+ expect(frames[0]?.addr_mode).toBe('rel:3');
+ });
+});