diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index ebd4cee78b2a..cff9f0fe4632 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -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), }; @@ -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; } diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index 0f43e7495efd..31112abbfc7e 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -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 { @@ -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(); + }); +}); diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 13ae0edce489..f019508662cd 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -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); diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index 1ccfc2cd1754..853fae168681 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -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; @@ -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()) {