Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(wasm): Integration wasm uncaught WebAssembly.Exception (#13787) #13854

Merged
merged 8 commits into from
Oct 4, 2024
44 changes: 42 additions & 2 deletions packages/browser/src/eventbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function exceptionFromError(stackParser: StackParser, ex: Error): Excepti
const frames = parseStackFrames(stackParser, ex);

const exception: Exception = {
type: ex && ex.name,
type: extractType(ex),
value: extractMessage(ex),
};

Expand Down Expand Up @@ -159,19 +159,59 @@ function getPopFirstTopFrames(ex: Error & { framesToPop?: unknown }): number {
return 0;
}

// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Exception
// @ts-expect-error - WebAssembly.Exception is a valid class
function isWebAssemblyException(exception: unknown): exception is WebAssembly.Exception {
// Check for support
// @ts-expect-error - WebAssembly.Exception is a valid class
if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.Exception !== 'undefined') {
// @ts-expect-error - WebAssembly.Exception is a valid class
return exception instanceof WebAssembly.Exception;
} else {
return false;
}
}

/**
* Extracts from errors what we use as the exception `type` in error events.
*
* Usually, this is the `name` property on Error objects but WASM errors need to be treated differently.
*/
export function extractType(ex: Error & { message: { error?: Error } }): string | undefined {
const name = ex && ex.name;

// The name for WebAssembly.Exception Errors needs to be extracted differently.
// Context: https://github.com/getsentry/sentry-javascript/issues/13787
if (!name && isWebAssemblyException(ex)) {
// Emscripten sets array[type, message] to the "message" property on the WebAssembly.Exception object
const hasTypeInMessage = ex.message && Array.isArray(ex.message) && ex.message.length == 2;
return hasTypeInMessage ? ex.message[0] : 'WebAssembly.Exception';
}

return name;
}

/**
* There are cases where stacktrace.message is an Event object
* https://github.com/getsentry/sentry-javascript/issues/1949
* In this specific case we try to extract stacktrace.message.error.message
*/
function extractMessage(ex: Error & { message: { error?: Error } }): string {
export function extractMessage(ex: Error & { message: { error?: Error } }): string {
const message = ex && ex.message;

if (!message) {
return 'No error message';
}

if (message.error && typeof message.error.message === 'string') {
return message.error.message;
}

// Emscripten sets array[type, message] to the "message" property on the WebAssembly.Exception object
if (isWebAssemblyException(ex) && Array.isArray(ex.message) && ex.message.length == 2) {
return ex.message[1];
}

return message;
}

Expand Down
64 changes: 63 additions & 1 deletion packages/browser/test/eventbuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import { defaultStackParser } from '../src';
import { eventFromUnknownInput } from '../src/eventbuilder';
import { eventFromUnknownInput, extractMessage, extractType } from '../src/eventbuilder';

vi.mock('@sentry/core', async requireActual => {
return {
Expand Down Expand Up @@ -169,3 +169,65 @@ describe('eventFromUnknownInput', () => {
});
});
});

describe('extractMessage', () => {
it('should extract message from a standard Error object', () => {
const error = new Error('Test error message');
const message = extractMessage(error);
expect(message).toBe('Test error message');
});

it('should extract message from a WebAssembly.Exception object', () => {
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Exception/Exception#examples
// @ts-expect-error - WebAssembly.Tag is a valid constructor
const tag = new WebAssembly.Tag({ parameters: ['i32', 'f32'] });
// @ts-expect-error - WebAssembly.Exception is a valid constructor
const wasmException = new WebAssembly.Exception(tag, [42, 42.3]);

const message = extractMessage(wasmException);
expect(message).toBe('wasm exception');
});

it('should extract nested error message', () => {
const nestedError = {
message: {
error: new Error('Nested error message'),
},
};
const message = extractMessage(nestedError as any);
expect(message).toBe('Nested error message');
});

it('should return "No error message" if message is undefined', () => {
const error = new Error();
error.message = undefined as any;
const message = extractMessage(error);
expect(message).toBe('No error message');
});
});

describe('extractName', () => {
it('should extract name from a standard Error object', () => {
const error = new Error('Test error message');
const name = extractType(error);
expect(name).toBe('Error');
});

it('should extract name from a WebAssembly.Exception object', () => {
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Exception/Exception#examples
// @ts-expect-error - WebAssembly.Tag is a valid constructor
const tag = new WebAssembly.Tag({ parameters: ['i32', 'f32'] });
// @ts-expect-error - WebAssembly.Exception is a valid constructor
const wasmException = new WebAssembly.Exception(tag, [42, 42.3]);

const name = extractType(wasmException);
expect(name).toBe('WebAssembly.Exception');
});

it('should return undefined if name is not present', () => {
const error = new Error('Test error message');
error.name = undefined as any;
const name = extractType(error);
expect(name).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions packages/utils/src/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function isError(wat: unknown): wat is Error {
case '[object Error]':
case '[object Exception]':
case '[object DOMException]':
case '[object WebAssembly.Exception]':
return true;
default:
return isInstanceOf(wat, Error);
Expand Down
9 changes: 9 additions & 0 deletions packages/utils/test/is.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../src/is';
import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../src/supports';
import { resolvedSyncPromise } from '../src/syncpromise';
import { testOnlyIfNodeVersionAtLeast } from './testutils';

class SentryError extends Error {
public name: string;
Expand Down Expand Up @@ -56,6 +57,14 @@ describe('isError()', () => {
expect(isError('')).toEqual(false);
expect(isError(true)).toEqual(false);
});

testOnlyIfNodeVersionAtLeast(18)('should detect WebAssembly.Exceptions', () => {
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Exception/Exception#examples
// @ts-expect-error - WebAssembly.Tag is a valid constructor
const tag = new WebAssembly.Tag({ parameters: ['i32', 'f32'] });
// @ts-expect-error - WebAssembly.Exception is a valid constructor
expect(isError(new WebAssembly.Exception(tag, [42, 42.3]))).toBe(true);
});
});

if (supportsErrorEvent()) {
Expand Down
Loading