Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
@@ -1,20 +1,15 @@
import * as Sentry from '@sentry/browser';
import { registerWebWorkerWasm } from '@sentry/wasm';
import { wasmIntegration } from '@sentry/wasm';

window.Sentry = Sentry;

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
});

// `registerWebWorkerWasm` installs the same patches a worker would, and reports
// every registered module to the scope it is given. Collecting them here is the
// only way to observe registration from the page, since main-thread images stay
// module-internal until a frame matches one.
window.registeredImages = [];
registerWebWorkerWasm({
self: {
postMessage: message => window.registeredImages.push(...(message._sentryWasmImages || [])),
integrations: [wasmIntegration()],
beforeSend: event => {
window.events.push(event);
return null;
},
});
window.events = [];
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
window.loadWasmFromBuffer = async () => {
const response = await fetch('https://localhost:5887/simple.wasm');
const buffer = await response.arrayBuffer();
window.getEvent = async () => {
function crash() {
throw new Error('whoops');
}

await WebAssembly.instantiate(new Uint8Array(buffer), {
const response = await fetch('https://localhost:5887/named.wasm');
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(new Uint8Array(buffer), {
env: {
external_func: () => {},
external_func: crash,
},
});

return window.registeredImages;
try {
instance.exports.internal_func();
} catch (err) {
Sentry.captureException(err);
return window.events.pop();
}
};
Original file line number Diff line number Diff line change
@@ -1,49 +1,68 @@
import type { Page, Route } from '@playwright/test';
import { expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
import { sentryTest } from '../../../utils/fixtures';
import { shouldSkipWASMTests } from '../../../utils/wasmHelpers';

async function serveWasmFixture(page: Page): Promise<void> {
// `page.route` resolves with a `Disposable` as of Playwright 1.63, so it can't be returned directly.
await page.route('**/simple.wasm', (route: Route) => {
const wasmModule = fs.readFileSync(path.resolve(__dirname, '..', 'simple.wasm'));

return route.fulfill({
status: 200,
body: wasmModule,
headers: {
'Content-Type': 'application/wasm',
},
});
});
}

// `named.wasm` is `../simple.wasm` plus a module-name subsection (`namedmodule`)
// in its `name` section. Chrome labels bytes-compiled modules with that name
// as `wasm://wasm/namedmodule-<hash>`, not with the fetch URL.
sentryTest(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we add a worker case? Only the main thread is covered right now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Added a bytes-compiled case to the existing webWorker suite, using named.wasm so Chrome produces the wasm://wasm/namedmodule-<hash> label.

'registers a module loaded via fetch, arrayBuffer and instantiate under its response url',
'maps frames of a module compiled from fetched bytes to its debug image',
async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipWASMTests(browserName)) {
sentryTest.skip();
}

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

await page.route('**/named.wasm', route => {
const wasmModule = fs.readFileSync(path.resolve(__dirname, '..', 'named.wasm'));

return route.fulfill({
status: 200,
body: wasmModule,
headers: {
'Content-Type': 'application/wasm',
},
});
});

await page.goto(url);

const images = await page.evaluate(async () => {
const event = await page.evaluate(async () => {
// @ts-expect-error this function exists
return window.loadWasmFromBuffer();
return window.getEvent();
});

expect(images).toEqual([
{
type: 'wasm',
code_file: 'https://localhost:5887/simple.wasm',
code_id: '0ba020cdd2444f7eafdd25999a8e9010',
debug_file: null,
debug_id: '0ba020cdd2444f7eafdd25999a8e90100',
},
]);
expect(event.exception.values[0].stacktrace.frames).toEqual(
expect.arrayContaining([
expect.objectContaining({
filename: 'https://localhost:5887/named.wasm',
function: 'namedmodule.internal_func',
in_app: true,
instruction_addr: '0x8c',
addr_mode: 'rel:0',
platform: 'native',
}),
expect.objectContaining({
filename: expect.stringMatching(/subject\.bundle\.js$/),
function: 'crash',
in_app: true,
}),
]),
);

expect(event.debug_meta).toMatchObject({
images: [
{
code_file: 'https://localhost:5887/named.wasm',
code_id: '0ba020cdd2444f7eafdd25999a8e9010',
debug_file: null,
debug_id: '0ba020cdd2444f7eafdd25999a8e90100',
type: 'wasm',
},
],
});
},
);
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
// between worker and main thread independent of SDK implementation details
// in production code you would do: registerWebWorkerWasm({ self });

// Bytes-compiled modules carry no URL, so remember the fetch URL per buffer
// like the SDK's Response patch does.
const bufferUrls = new WeakMap();
const origArrayBuffer = Response.prototype.arrayBuffer;
Response.prototype.arrayBuffer = function arrayBuffer() {
return origArrayBuffer.call(this).then(buffer => {
if (this.url) {
bufferUrls.set(buffer, this.url);
}
return buffer;
});
};

const origInstantiate = WebAssembly.instantiate;
WebAssembly.instantiate = function instantiate(source, importObject) {
return origInstantiate(source, importObject).then(result => {
const url = bufferUrls.get(source instanceof ArrayBuffer ? source : source.buffer);
if (url && result.module) {
registerModuleAndForward(result.module, url);
}
return result;
});
};

const origInstantiateStreaming = WebAssembly.instantiateStreaming;
WebAssembly.instantiateStreaming = function instantiateStreaming(response, importObject) {
return Promise.resolve(response).then(res => {
Expand All @@ -26,6 +50,10 @@
debug_file: null,
debug_id: `${`${buildId}00000000000000000000000000000000`.slice(0, 32)}0`,
};
const moduleName = getModuleName(module);
if (moduleName) {
image.moduleName = moduleName;
}

self.postMessage({
_sentryMessage: true,
Expand All @@ -46,6 +74,24 @@
return null;
}

// Read the module name from the `name` custom section. Chrome labels
// bytes-compiled modules `wasm://wasm/<name>-<hash>`, and the main thread
// links such frames to this image by that name.
function getModuleName(module) {
const sections = WebAssembly.Module.customSections(module, 'name');
if (sections.length === 0) {
return null;
}
const bytes = new Uint8Array(sections[0]);
// Subsection id 0 is the module name: id, payload size, name length, name.
// Sizes are LEB128, but the fixture's fit in one byte each.
if (bytes[0] !== 0) {
return null;
}
const length = bytes[2];
return new TextDecoder().decode(bytes.subarray(3, 3 + length));
}

// Handle messages from the main thread
self.addEventListener('message', async event => {
if (event.origin !== '' && event.origin !== self.location.origin) {
Expand All @@ -56,15 +102,23 @@
throw new Error('WASM error from worker');
}

if (event.data.type === 'load-wasm-and-crash') {
if (event.data.type === 'load-wasm-and-crash' || event.data.type === 'load-wasm-bytes-and-crash') {
const wasmUrl = event.data.wasmUrl;
const imports = {
env: {
external_func: crash,
},
};

try {
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
env: {
external_func: crash,
},
});
let instance;
if (event.data.type === 'load-wasm-bytes-and-crash') {
const response = await fetch(wasmUrl);
Comment thread
timfish marked this conversation as resolved.
Dismissed
const buffer = await response.arrayBuffer();
({ instance } = await WebAssembly.instantiate(new Uint8Array(buffer), imports));
} else {
({ instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), imports));
Comment thread
timfish marked this conversation as resolved.
Dismissed
}

instance.exports.internal_func();
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ window.triggerWasmError = () => {
wasmUrl: 'https://localhost:5887/simple.wasm',
});
};

window.triggerWasmBufferError = () => {
window.wasmWorker.postMessage({
type: 'load-wasm-bytes-and-crash',
wasmUrl: 'https://localhost:5887/named.wasm',
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare global {
interface Window {
wasmWorker: Worker;
triggerWasmError: () => void;
triggerWasmBufferError: () => void;
}
}

Expand Down Expand Up @@ -137,3 +138,70 @@ sentryTest(
);
},
);

// `named.wasm` is `simple.wasm` plus a module-name subsection (`namedmodule`).
// Chrome labels bytes-compiled modules with a name as `wasm://wasm/namedmodule-<hash>`,
// not with the fetch URL, so the frame has to be linked to the image by that name.
sentryTest(
'WASM frames from a module compiled from fetched bytes in a worker should map to its debug image',
async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipWASMTests(browserName)) {
sentryTest.skip();
}

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

await page.route('**/named.wasm', route => {
const wasmModule = fs.readFileSync(path.resolve(__dirname, '../named.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.triggerWasmBufferError();
});

const errorEvent = envelopeRequestParser(await errorEventPromise);

expect(errorEvent.debug_meta?.images).toEqual([
{
type: 'wasm',
code_file: 'https://localhost:5887/named.wasm',
code_id: '0ba020cdd2444f7eafdd25999a8e9010',
debug_file: null,
debug_id: '0ba020cdd2444f7eafdd25999a8e90100',
},
]);

expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toEqual(
expect.arrayContaining([
expect.objectContaining({
filename: 'https://localhost:5887/named.wasm',
function: 'namedmodule.internal_func',
platform: 'native',
instruction_addr: '0x8c',
addr_mode: 'rel:0',
}),
]),
);
},
);
Loading
Loading