From 0c6424df380cddd14ee44b35dd5108df19f86265 Mon Sep 17 00:00:00 2001 From: Damian Trzeciak Date: Wed, 2 Oct 2024 18:38:56 +0200 Subject: [PATCH 1/6] fix(wasm): Integration wasm uncaught WebAssembly.Exception (#13787) --- packages/browser/src/eventbuilder.ts | 23 +++++++++++++++++++++-- packages/utils/src/is.ts | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index ebd4cee78b2a..14de3a7af084 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: extractName(ex), value: extractMessage(ex), }; @@ -159,12 +159,27 @@ function getPopFirstTopFrames(ex: Error & { framesToPop?: unknown }): number { return 0; } +/** + * There are cases where error is an WebAssembly.Exception object + * https://github.com/getsentry/sentry-javascript/issues/13787 + * In this specific case we try to extract name(type) from WebAssembly.Exception.message + */ +function extractName(ex) { + const name = ex && ex.name; + if (!name && ex instanceof WebAssembly.Exception) { + // 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; // May return undefined value +} + /** * 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 { +function extractMessage(ex) { const message = ex && ex.message; if (!message) { return 'No error message'; @@ -172,6 +187,10 @@ function extractMessage(ex: Error & { message: { error?: Error } }): string { if (message.error && typeof message.error.message === 'string') { return message.error.message; } + if (ex instanceof WebAssembly.Exception && Array.isArray(ex.message) && ex.message.length == 2) { + // Emscripten sets array[type, message] to the "message" property on the WebAssembly.Exception object + return ex.message[1]; + } return message; } 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); From 7895693ba3b782abfa1d02602c55b596b754f7b9 Mon Sep 17 00:00:00 2001 From: Damian Trzeciak Date: Wed, 2 Oct 2024 19:01:37 +0200 Subject: [PATCH 2/6] Fix: Wasm integration doesn't work for uncaught WebAssembly.Exception https://github.com/getsentry/sentry-javascript/issues/13787 --- packages/browser/src/eventbuilder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 14de3a7af084..ab10ce66e175 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -162,9 +162,9 @@ function getPopFirstTopFrames(ex: Error & { framesToPop?: unknown }): number { /** * There are cases where error is an WebAssembly.Exception object * https://github.com/getsentry/sentry-javascript/issues/13787 - * In this specific case we try to extract name(type) from WebAssembly.Exception.message + * In this specific case we try to extract name/type from .message in WebAssembly.Exception */ -function extractName(ex) { +function extractName(ex: Error & { message: { error?: Error } }): string { const name = ex && ex.name; if (!name && ex instanceof WebAssembly.Exception) { // Emscripten sets array[type, message] to the "message" property on the WebAssembly.Exception object @@ -179,7 +179,7 @@ function extractName(ex) { * https://github.com/getsentry/sentry-javascript/issues/1949 * In this specific case we try to extract stacktrace.message.error.message */ -function extractMessage(ex) { +function extractMessage(ex: Error & { message: { error?: Error } }): string { const message = ex && ex.message; if (!message) { return 'No error message'; From 14b7526179f3fb71a658c055da36c0bb10ca5ceb Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 3 Oct 2024 10:14:38 +0200 Subject: [PATCH 3/6] resolve Type errors and add test --- packages/browser/src/eventbuilder.ts | 13 ++++++++++--- packages/utils/test/is.test.ts | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index ab10ce66e175..973515b0a2e8 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -159,14 +159,21 @@ 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 { + // @ts-expect-error - WebAssembly.Exception is a valid class + return exception instanceof WebAssembly.Exception; +} + /** * There are cases where error is an WebAssembly.Exception object * https://github.com/getsentry/sentry-javascript/issues/13787 * In this specific case we try to extract name/type from .message in WebAssembly.Exception */ -function extractName(ex: Error & { message: { error?: Error } }): string { +function extractName(ex: Error & { message: { error?: Error } }): string | undefined { const name = ex && ex.name; - if (!name && ex instanceof WebAssembly.Exception) { + 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'; @@ -187,7 +194,7 @@ function extractMessage(ex: Error & { message: { error?: Error } }): string { if (message.error && typeof message.error.message === 'string') { return message.error.message; } - if (ex instanceof WebAssembly.Exception && Array.isArray(ex.message) && ex.message.length == 2) { + if (isWebAssemblyException(ex) && Array.isArray(ex.message) && ex.message.length == 2) { // Emscripten sets array[type, message] to the "message" property on the WebAssembly.Exception object return ex.message[1]; } diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index 1ccfc2cd1754..c1a1c15dcbb4 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -46,6 +46,11 @@ describe('isError()', () => { expect(isError(new Error())).toEqual(true); expect(isError(new ReferenceError())).toEqual(true); expect(isError(new SentryError('message'))).toEqual(true); + // 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); expect(isError({})).toEqual(false); expect( isError({ From ea6dbda46e0b878329ff9bbd43c3e3e2a5a0db12 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 3 Oct 2024 10:35:00 +0200 Subject: [PATCH 4/6] test(wasm): Add unit tests --- packages/browser/src/eventbuilder.ts | 14 ++++- packages/browser/test/eventbuilder.test.ts | 64 +++++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 973515b0a2e8..8f8da0ca022f 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -167,26 +167,34 @@ function isWebAssemblyException(exception: unknown): exception is WebAssembly.Ex } /** + * Only exported for testing + * * There are cases where error is an WebAssembly.Exception object * https://github.com/getsentry/sentry-javascript/issues/13787 * In this specific case we try to extract name/type from .message in WebAssembly.Exception + * + * @hidden */ -function extractName(ex: Error & { message: { error?: Error } }): string | undefined { +export function extractName(ex: Error & { message: { error?: Error } }): string | undefined { const name = ex && ex.name; 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; // May return undefined value + return name; } /** + * Only exported for testing + * * 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 + * + * @hidden */ -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'; diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index 0f43e7495efd..7ff6e74aba03 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, extractName } 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 = extractName(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 = extractName(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 = extractName(error); + expect(name).toBeUndefined(); + }); +}); From 414127ff726738165e8e9416f89ac14545e17bc7 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Oct 2024 11:03:49 +0000 Subject: [PATCH 5/6] Clean up, check for support, fix tests on older node versions --- packages/browser/src/eventbuilder.ts | 34 +++++++++++++--------- packages/browser/test/eventbuilder.test.ts | 8 ++--- packages/utils/test/is.test.ts | 16 ++++++---- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 8f8da0ca022f..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: extractName(ex), + type: extractType(ex), value: extractMessage(ex), }; @@ -162,50 +162,56 @@ function getPopFirstTopFrames(ex: Error & { framesToPop?: unknown }): number { // 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 - return exception instanceof WebAssembly.Exception; + 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; + } } /** - * Only exported for testing - * - * There are cases where error is an WebAssembly.Exception object - * https://github.com/getsentry/sentry-javascript/issues/13787 - * In this specific case we try to extract name/type from .message in WebAssembly.Exception + * Extracts from errors what we use as the exception `type` in error events. * - * @hidden + * Usually, this is the `name` property on Error objects but WASM errors need to be treated differently. */ -export function extractName(ex: Error & { message: { error?: Error } }): string | undefined { +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; } /** - * Only exported for testing - * * 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 - * - * @hidden */ 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) { - // Emscripten sets array[type, message] to the "message" property on the WebAssembly.Exception object return ex.message[1]; } + return message; } diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index 7ff6e74aba03..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, extractMessage, extractName } from '../src/eventbuilder'; +import { eventFromUnknownInput, extractMessage, extractType } from '../src/eventbuilder'; vi.mock('@sentry/core', async requireActual => { return { @@ -209,7 +209,7 @@ describe('extractMessage', () => { describe('extractName', () => { it('should extract name from a standard Error object', () => { const error = new Error('Test error message'); - const name = extractName(error); + const name = extractType(error); expect(name).toBe('Error'); }); @@ -220,14 +220,14 @@ describe('extractName', () => { // @ts-expect-error - WebAssembly.Exception is a valid constructor const wasmException = new WebAssembly.Exception(tag, [42, 42.3]); - const name = extractName(wasmException); + 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 = extractName(error); + const name = extractType(error); expect(name).toBeUndefined(); }); }); diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index c1a1c15dcbb4..fb0a56e6f33d 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; @@ -46,11 +47,6 @@ describe('isError()', () => { expect(isError(new Error())).toEqual(true); expect(isError(new ReferenceError())).toEqual(true); expect(isError(new SentryError('message'))).toEqual(true); - // 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); expect(isError({})).toEqual(false); expect( isError({ @@ -61,6 +57,16 @@ describe('isError()', () => { expect(isError('')).toEqual(false); expect(isError(true)).toEqual(false); }); + + if (testOnlyIfNodeVersionAtLeast(18)) { + test('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()) { From bdea5c73ab702512714e5fc6f0bb16969585e462 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 4 Oct 2024 11:56:08 +0000 Subject: [PATCH 6/6] Actually skip tests on older node versions --- packages/utils/test/is.test.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index fb0a56e6f33d..853fae168681 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -58,15 +58,13 @@ describe('isError()', () => { expect(isError(true)).toEqual(false); }); - if (testOnlyIfNodeVersionAtLeast(18)) { - test('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); - }); - } + 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()) {