Skip to content

Commit

Permalink
fix(wasm): Integration wasm uncaught WebAssembly.Exception (#13787) (#…
Browse files Browse the repository at this point in the history
…13854)

Add support for wasm WebAssembly.Exception (uncaught exception in
emscripten).

## References
- [WebAssembly.Exception | MDN Web
Docs](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Exception)
- #13787

---------

Co-authored-by: s1gr1d <[email protected]>
Co-authored-by: Sigrid Huemer <[email protected]>
Co-authored-by: Luca Forstner <[email protected]>
  • Loading branch information
4 people authored Oct 4, 2024
1 parent db77c11 commit d891e06
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 3 deletions.
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

0 comments on commit d891e06

Please sign in to comment.