From 0e9fbad9930a7f134d42def6389bf76da8f429f3 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 28 Jun 2024 11:48:09 -0300 Subject: [PATCH 1/4] add cause support on debug --- src/js/integrations/debugsymbolicator.ts | 47 ++++-- test/integrations/debugsymbolicator.test.ts | 172 ++++++++++++++++++++ 2 files changed, 208 insertions(+), 11 deletions(-) diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index de7a18c294..781ccf52a8 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -2,6 +2,7 @@ import { convertIntegrationFnToClass } from '@sentry/core'; import type { Event, EventHint, + Exception, Integration, IntegrationClass, IntegrationFnResult, @@ -12,6 +13,7 @@ import { addContextToFrame, logger } from '@sentry/utils'; import { getFramesToPop, isErrorLike } from '../utils/error'; import type * as ReactNative from '../vendor/react-native'; import { fetchSourceContext, getDevServer, parseErrorStack, symbolicateStackTrace } from './debugsymbolicatorutils'; +import { eventOriginIntegration } from './eventorigin'; const INTEGRATION_NAME = 'DebugSymbolicator'; @@ -28,6 +30,11 @@ export type ReactNativeError = Error & { componentStack?: string; }; +type ErrorLike = { + stack: string +}; + + /** Tries to symbolicate the JS stack trace on the device. */ export const debugSymbolicatorIntegration = (): IntegrationFnResult => { return { @@ -51,13 +58,18 @@ export const DebugSymbolicator = convertIntegrationFnToClass( ) as IntegrationClass; async function processEvent(event: Event, hint: EventHint): Promise { - if (event.exception && isErrorLike(hint.originalException)) { + if (event.exception?.values && isErrorLike(hint.originalException)) { // originalException is ErrorLike object - const symbolicatedFrames = await symbolicate( - hint.originalException.stack, - getFramesToPop(hint.originalException as Error), - ); - symbolicatedFrames && replaceExceptionFramesInEvent(event, symbolicatedFrames); + const errorGroup = getExceptionGroup(hint.originalException) + for (const [index, error] of errorGroup.entries()) { + const symbolicatedFrames = await symbolicate( + error.stack, + getFramesToPop(error as Error), + ); + + symbolicatedFrames && replaceExceptionFramesInException(event.exception.values[index], symbolicatedFrames); + } + } else if (hint.syntheticException && isErrorLike(hint.syntheticException)) { // syntheticException is Error object const symbolicatedFrames = await symbolicate( @@ -66,7 +78,7 @@ async function processEvent(event: Event, hint: EventHint): Promise { ); if (event.exception) { - symbolicatedFrames && replaceExceptionFramesInEvent(event, symbolicatedFrames); + symbolicatedFrames && event.exception.values && replaceExceptionFramesInException(event.exception.values[0], symbolicatedFrames); } else if (event.threads) { // RN JS doesn't have threads symbolicatedFrames && replaceThreadFramesInEvent(event, symbolicatedFrames); @@ -149,10 +161,10 @@ async function convertReactNativeFramesToSentryFrames(frames: ReactNative.StackF * @param event Event * @param frames StackFrame[] */ -function replaceExceptionFramesInEvent(event: Event, frames: SentryStackFrame[]): void { - if (event.exception && event.exception.values && event.exception.values[0] && event.exception.values[0].stacktrace) { - event.exception.values[0].stacktrace.frames = frames.reverse(); - } +function replaceExceptionFramesInException(exception: Exception, frames: SentryStackFrame[]): void { + if (exception.stacktrace) { + exception.stacktrace.frames = frames.reverse(); + }; } /** @@ -200,3 +212,16 @@ async function addSourceContext(frame: SentryStackFrame): Promise { const lines = sourceContext.split('\n'); addContextToFrame(lines, frame); } + +/** + * Return a list containing the original exception and also the cause if found. + * + * @param originalException The original exception. + */ +function getExceptionGroup(originalException: ErrorLike): ErrorLike[] { + const errorGroup: ErrorLike[] = [originalException]; + const cause = (originalException as { cause?: unknown }).cause; + isErrorLike(cause) && errorGroup.push(cause); + + return errorGroup; +} diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts index 035fa63fe2..298cffd69d 100644 --- a/test/integrations/debugsymbolicator.test.ts +++ b/test/integrations/debugsymbolicator.test.ts @@ -36,6 +36,13 @@ describe('Debug Symbolicator Integration', () => { at baz (native) `; + const mockRawStack2 = `Error2: This is mocked error stack trace + at foo2 (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3:3) + at bar2 (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:4:4) + at baz2 (native) + `; + + const mockSentryParsedFrames: Array = [ { function: '[native] baz', @@ -54,6 +61,24 @@ describe('Debug Symbolicator Integration', () => { }, ]; + const mockSentryParsedFrames2: Array = [ + { + function: '[native] baz2', + }, + { + function: 'bar2', + filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:4:4', + lineno: 4, + colno: 4, + }, + { + function: 'foo2', + filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3:3', + lineno: 3, + colno: 3, + }, + ]; + beforeEach(() => { (parseErrorStack as jest.Mock).mockReturnValue(>[ { @@ -334,5 +359,152 @@ describe('Debug Symbolicator Integration', () => { }, }); }); + + it('should symbolicate multiple error with cause ', async () => { + (parseErrorStack as jest.Mock).mockReturnValueOnce(>[ + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 1, + column: 1, + methodName: 'foo', + }, + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 2, + column: 2, + methodName: 'bar', + }, + ]).mockReturnValueOnce(>[ + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 3, + column: 3, + methodName: 'foo2', + }, + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 4, + column: 4, + methodName: 'bar2', + }, + ]); + (symbolicateStackTrace as jest.Mock).mockReturnValueOnce( + Promise.resolve({ + stack: [ + { + file: '/User/project/foo.js', + lineNumber: 1, + column: 1, + methodName: 'foo', + }, + { + file: '/User/project/node_modules/bar/bar.js', + lineNumber: 2, + column: 2, + methodName: 'bar', + }, + ], + }), + ).mockReturnValueOnce( + Promise.resolve({ + stack: [ + { + file: '/User/project/foo2.js', + lineNumber: 3, + column: 3, + methodName: 'foo2', + }, + { + file: '/User/project/node_modules/bar/bar2.js', + lineNumber: 4, + column: 4, + methodName: 'bar2', + }, + ], + }), + ); + + const symbolicatedEvent = await processEvent( + { + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + { + type: 'Error2', + value: 'Error2: test', + stacktrace: { + frames: mockSentryParsedFrames2, + }, + }, + ], + }, + }, + { + originalException: { + stack: mockRawStack, + cause: { + stack: mockRawStack2, + } + }, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + { + function: 'foo', + filename: '/User/project/foo.js', + lineno: 1, + colno: 1, + in_app: true, + }, + ], + }, + }, + { + type: 'Error2', + value: 'Error2: test', + stacktrace: { + frames: [ + { + function: 'bar2', + filename: '/User/project/node_modules/bar/bar2.js', + lineno: 4, + colno: 4, + in_app: false, + }, + { + function: 'foo2', + filename: '/User/project/foo2.js', + lineno: 3, + colno: 3, + in_app: true, + }, + ], + }, + }, + ], + }, + }); + }); }); }); From 24e59f70966966b3e91c7ee8aaf8f83e6c9bedb6 Mon Sep 17 00:00:00 2001 From: lucas Date: Fri, 28 Jun 2024 14:09:16 -0300 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 1 + test/integrations/debugsymbolicator.test.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a4e6ced29..e84eb191fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- errors with cause are now correctly serialized on debug build ([#3920](https://github.com/getsentry/sentry-react-native/pull/3920)) - `sentry-expo-upload-sourcemaps` no longer requires Sentry url when uploading sourcemaps to `sentry.io` ([#3915](https://github.com/getsentry/sentry-react-native/pull/3915)) ### Dependencies diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts index 298cffd69d..7fabc052b8 100644 --- a/test/integrations/debugsymbolicator.test.ts +++ b/test/integrations/debugsymbolicator.test.ts @@ -42,7 +42,6 @@ describe('Debug Symbolicator Integration', () => { at baz2 (native) `; - const mockSentryParsedFrames: Array = [ { function: '[native] baz', From ee7ea42838625a748d1cb3c4fef37f7f72a7a679 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 4 Jul 2024 17:25:11 -0300 Subject: [PATCH 3/4] requested changes + more tests --- src/js/integrations/debugsymbolicator.ts | 34 +- src/js/utils/error.ts | 1 + test/integrations/debugsymbolicator.test.ts | 509 ++++++++++++++++---- 3 files changed, 439 insertions(+), 105 deletions(-) diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 781ccf52a8..5375723f0b 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -10,10 +10,10 @@ import type { } from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; +import type { ExtendedError } from '../utils/error'; import { getFramesToPop, isErrorLike } from '../utils/error'; import type * as ReactNative from '../vendor/react-native'; import { fetchSourceContext, getDevServer, parseErrorStack, symbolicateStackTrace } from './debugsymbolicatorutils'; -import { eventOriginIntegration } from './eventorigin'; const INTEGRATION_NAME = 'DebugSymbolicator'; @@ -30,11 +30,6 @@ export type ReactNativeError = Error & { componentStack?: string; }; -type ErrorLike = { - stack: string -}; - - /** Tries to symbolicate the JS stack trace on the device. */ export const debugSymbolicatorIntegration = (): IntegrationFnResult => { return { @@ -60,16 +55,12 @@ export const DebugSymbolicator = convertIntegrationFnToClass( async function processEvent(event: Event, hint: EventHint): Promise { if (event.exception?.values && isErrorLike(hint.originalException)) { // originalException is ErrorLike object - const errorGroup = getExceptionGroup(hint.originalException) + const errorGroup = getExceptionGroup(hint.originalException); for (const [index, error] of errorGroup.entries()) { - const symbolicatedFrames = await symbolicate( - error.stack, - getFramesToPop(error as Error), - ); + const symbolicatedFrames = await symbolicate(error.stack, getFramesToPop(error)); symbolicatedFrames && replaceExceptionFramesInException(event.exception.values[index], symbolicatedFrames); } - } else if (hint.syntheticException && isErrorLike(hint.syntheticException)) { // syntheticException is Error object const symbolicatedFrames = await symbolicate( @@ -78,7 +69,9 @@ async function processEvent(event: Event, hint: EventHint): Promise { ); if (event.exception) { - symbolicatedFrames && event.exception.values && replaceExceptionFramesInException(event.exception.values[0], symbolicatedFrames); + symbolicatedFrames && + event.exception.values && + replaceExceptionFramesInException(event.exception.values[0], symbolicatedFrames); } else if (event.threads) { // RN JS doesn't have threads symbolicatedFrames && replaceThreadFramesInEvent(event, symbolicatedFrames); @@ -162,9 +155,9 @@ async function convertReactNativeFramesToSentryFrames(frames: ReactNative.StackF * @param frames StackFrame[] */ function replaceExceptionFramesInException(exception: Exception, frames: SentryStackFrame[]): void { - if (exception.stacktrace) { + if (exception?.stacktrace) { exception.stacktrace.frames = frames.reverse(); - }; + } } /** @@ -218,10 +211,11 @@ async function addSourceContext(frame: SentryStackFrame): Promise { * * @param originalException The original exception. */ -function getExceptionGroup(originalException: ErrorLike): ErrorLike[] { - const errorGroup: ErrorLike[] = [originalException]; - const cause = (originalException as { cause?: unknown }).cause; - isErrorLike(cause) && errorGroup.push(cause); - +function getExceptionGroup(originalException: unknown): (Error & { stack: string })[] { + const err = originalException as ExtendedError; + const errorGroup: (Error & { stack: string })[] = []; + for (let cause: ExtendedError | undefined = err; isErrorLike(cause); cause = cause.cause) { + errorGroup.push(cause); + } return errorGroup; } diff --git a/src/js/utils/error.ts b/src/js/utils/error.ts index 044e3a8516..6c79160739 100644 --- a/src/js/utils/error.ts +++ b/src/js/utils/error.ts @@ -1,5 +1,6 @@ export interface ExtendedError extends Error { framesToPop?: number | undefined; + cause?: Error | undefined; } // Sentry Stack Parser is skipping lines not frames diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts index 7fabc052b8..31c982b2ca 100644 --- a/test/integrations/debugsymbolicator.test.ts +++ b/test/integrations/debugsymbolicator.test.ts @@ -36,12 +36,6 @@ describe('Debug Symbolicator Integration', () => { at baz (native) `; - const mockRawStack2 = `Error2: This is mocked error stack trace - at foo2 (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3:3) - at bar2 (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:4:4) - at baz2 (native) - `; - const mockSentryParsedFrames: Array = [ { function: '[native] baz', @@ -60,23 +54,34 @@ describe('Debug Symbolicator Integration', () => { }, ]; - const mockSentryParsedFrames2: Array = [ - { - function: '[native] baz2', - }, - { - function: 'bar2', - filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:4:4', - lineno: 4, - colno: 4, - }, - { - function: 'foo2', - filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3:3', - lineno: 3, - colno: 3, - }, - ]; + const customMockRawStack = (errorNumber: number, value: number) => + `Error${errorNumber}: This is mocked error stack trace + at foo${errorNumber} (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value}:${value}) + at bar${errorNumber} (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value + 1}:${value + 1}) + at baz${errorNumber} (native) + `; + + const customMockSentryParsedFrames = (errorNumber: number, value: number): Array => { + const value2 = value + 1; + return [ + { + function: `[native] baz${errorNumber}`, + }, + { + function: `bar${errorNumber}`, + filename: `http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value}:${value}`, + lineno: value, + colno: value, + }, + { + function: `foo${errorNumber}`, + filename: `http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value2}:${value2}`, + lineno: value2, + colno: value2, + }, + + ]; + } beforeEach(() => { (parseErrorStack as jest.Mock).mockReturnValue(>[ @@ -359,86 +364,414 @@ describe('Debug Symbolicator Integration', () => { }); }); - it('should symbolicate multiple error with cause ', async () => { - (parseErrorStack as jest.Mock).mockReturnValueOnce(>[ + it('should symbolicate error with cause ', async () => { + (parseErrorStack as jest.Mock) + .mockReturnValueOnce(>[ + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 1, + column: 1, + methodName: 'foo', + }, + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 2, + column: 2, + methodName: 'bar', + }, + ]) + .mockReturnValueOnce(>[ + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 3, + column: 3, + methodName: 'foo2', + }, + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: 4, + column: 4, + methodName: 'bar2', + }, + ]); + (symbolicateStackTrace as jest.Mock) + .mockReturnValueOnce( + Promise.resolve({ + stack: [ + { + file: '/User/project/foo.js', + lineNumber: 1, + column: 1, + methodName: 'foo', + }, + { + file: '/User/project/node_modules/bar/bar.js', + lineNumber: 2, + column: 2, + methodName: 'bar', + }, + ], + }), + ) + .mockReturnValueOnce( + Promise.resolve({ + stack: [ + { + file: '/User/project/foo2.js', + lineNumber: 3, + column: 3, + methodName: 'foo2', + }, + { + file: '/User/project/node_modules/bar/bar2.js', + lineNumber: 4, + column: 4, + methodName: 'bar2', + }, + ], + }), + ); + + const symbolicatedEvent = await processEvent( { - file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', - lineNumber: 1, - column: 1, - methodName: 'foo', + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + { + type: 'Error2', + value: 'Error2: test', + stacktrace: { + frames: customMockSentryParsedFrames(2, 2), + }, + }, + ], + }, }, { - file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', - lineNumber: 2, - column: 2, - methodName: 'bar', + originalException: { + stack: mockRawStack, + cause: { + stack: customMockRawStack(1, 1), + }, + }, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [ + { + function: 'bar', + filename: '/User/project/node_modules/bar/bar.js', + lineno: 2, + colno: 2, + in_app: false, + }, + { + function: 'foo', + filename: '/User/project/foo.js', + lineno: 1, + colno: 1, + in_app: true, + }, + ], + }, + }, + { + type: 'Error2', + value: 'Error2: test', + stacktrace: { + frames: [ + { + function: 'bar2', + filename: '/User/project/node_modules/bar/bar2.js', + lineno: 4, + colno: 4, + in_app: false, + }, + { + function: 'foo2', + filename: '/User/project/foo2.js', + lineno: 3, + colno: 3, + in_app: true, + }, + ], + }, + }, + ], }, - ]).mockReturnValueOnce(>[ + }); + }); + + it('should symbolicate error with multiple causes ', async () => { + const setupStackFrame = (value: number): Array => [ { file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', - lineNumber: 3, - column: 3, - methodName: 'foo2', + lineNumber: value, + column: value, + methodName: `foo${value}`, }, { file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', - lineNumber: 4, - column: 4, - methodName: 'bar2', + lineNumber: value + 1, + column: value + 1, + methodName: `bar${value}`, }, - ]); - (symbolicateStackTrace as jest.Mock).mockReturnValueOnce( - Promise.resolve({ - stack: [ - { - file: '/User/project/foo.js', - lineNumber: 1, - column: 1, - methodName: 'foo', + ]; + + (parseErrorStack as jest.Mock) + .mockReturnValueOnce(setupStackFrame(1)) + .mockReturnValueOnce(setupStackFrame(3)) + .mockReturnValueOnce(setupStackFrame(5)) + .mockReturnValueOnce(setupStackFrame(7)); + + const mocksymbolicateStackTrace = (value: number): ReactNative.SymbolicatedStackTrace => + ({ + stack: [ + { + file: `/User/project/foo${value}.js`, + lineNumber: value, + column: value, + methodName: `foo${value}`, + }, + { + file: `/User/project/node_modules/bar/bar${value + 1}.js`, + lineNumber: value + 1, + column: value + 1, + methodName: `bar${value + 1}`, + }, + ] + }); + (symbolicateStackTrace as jest.Mock) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(1))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(3))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(5))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(7))) + + const symbolicatedEvent = await processEvent( + { + exception: { + values: [ + { + type: 'Error1', + value: 'Error1: test', + stacktrace: { + frames: customMockSentryParsedFrames(1, 1), + }, + }, + { + type: 'Error2', + value: 'Error2: test', + stacktrace: { + frames: customMockSentryParsedFrames(2, 2), + }, + }, + { + type: 'Error3', + value: 'Error3: test', + stacktrace: { + frames: customMockSentryParsedFrames(3, 3), + }, + }, + { + type: 'Error4', + value: 'Error4: test', + stacktrace: { + frames: customMockSentryParsedFrames(4, 4), + }, + }, + ], + }, + }, + { + originalException: { + stack: customMockRawStack(1, 1), + cause: { + stack: customMockRawStack(3, 3), + cause: { + stack: customMockRawStack(5, 5), + cause: { + stack: customMockRawStack(7, 7), + } + } }, + }, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + exception: { + values: [ { - file: '/User/project/node_modules/bar/bar.js', - lineNumber: 2, - column: 2, - methodName: 'bar', + type: 'Error1', + value: 'Error1: test', + stacktrace: { + frames: [ + { + function: 'bar2', + filename: '/User/project/node_modules/bar/bar2.js', + lineno: 2, + colno: 2, + in_app: false, + }, + { + function: 'foo1', + filename: '/User/project/foo1.js', + lineno: 1, + colno: 1, + in_app: true, + }, + ], + }, }, - ], - }), - ).mockReturnValueOnce( - Promise.resolve({ - stack: [ { - file: '/User/project/foo2.js', - lineNumber: 3, - column: 3, - methodName: 'foo2', + type: 'Error2', + value: 'Error2: test', + stacktrace: { + frames: [ + { + function: 'bar4', + filename: '/User/project/node_modules/bar/bar4.js', + lineno: 4, + colno: 4, + in_app: false, + }, + { + function: 'foo3', + filename: '/User/project/foo3.js', + lineno: 3, + colno: 3, + in_app: true, + }, + ], + }, }, { - file: '/User/project/node_modules/bar/bar2.js', - lineNumber: 4, - column: 4, - methodName: 'bar2', + type: 'Error3', + value: 'Error3: test', + stacktrace: { + frames: [ + { + function: 'bar6', + filename: '/User/project/node_modules/bar/bar6.js', + lineno: 6, + colno: 6, + in_app: false, + }, + { + function: 'foo5', + filename: '/User/project/foo5.js', + lineno: 5, + colno: 5, + in_app: true, + }, + ], + }, + }, { + type: 'Error4', + value: 'Error4: test', + stacktrace: { + frames: [ + { + function: 'bar8', + filename: '/User/project/node_modules/bar/bar8.js', + lineno: 8, + colno: 8, + in_app: false, + }, + { + function: 'foo7', + filename: '/User/project/foo7.js', + lineno: 7, + colno: 7, + in_app: true, + }, + ], + }, }, ], - }), - ); + }, + }); + }); + + it('should symbolicate error with different amount of exception hints ', async () => { + // Example: Sentry captures an Error with 20 Causes, but limits the captured exceptions to + // 5 in event.exception. Meanwhile, hint.originalException contains all 20 items. + // This test ensures no exceptions are thrown in an uneven scenario and ensures all + // consumed errors are symbolicated. + + const setupStackFrame = (value: number): Array => [ + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: value, + column: value, + methodName: `foo${value}`, + }, + { + file: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false', + lineNumber: value + 1, + column: value + 1, + methodName: `bar${value}`, + }, + ]; + + (parseErrorStack as jest.Mock) + .mockReturnValueOnce(setupStackFrame(1)) + .mockReturnValueOnce(setupStackFrame(3)) + .mockReturnValueOnce(setupStackFrame(5)) + .mockReturnValueOnce(setupStackFrame(7)); + + const mocksymbolicateStackTrace = (value: number): ReactNative.SymbolicatedStackTrace => + ({ + stack: [ + { + file: `/User/project/foo${value}.js`, + lineNumber: value, + column: value, + methodName: `foo${value}`, + }, + { + file: `/User/project/node_modules/bar/bar${value + 1}.js`, + lineNumber: value + 1, + column: value + 1, + methodName: `bar${value + 1}`, + }, + ] + }); + (symbolicateStackTrace as jest.Mock) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(1))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(3))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(5))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(7))) const symbolicatedEvent = await processEvent( { exception: { values: [ { - type: 'Error', - value: 'Error: test', + type: 'Error1', + value: 'Error1: test', stacktrace: { - frames: mockSentryParsedFrames, + frames: customMockSentryParsedFrames(1, 1), }, }, { type: 'Error2', value: 'Error2: test', stacktrace: { - frames: mockSentryParsedFrames2, + frames: customMockSentryParsedFrames(2, 2), }, }, ], @@ -446,10 +779,16 @@ describe('Debug Symbolicator Integration', () => { }, { originalException: { - stack: mockRawStack, + stack: customMockRawStack(1, 1), cause: { - stack: mockRawStack2, - } + stack: customMockRawStack(3, 3), + cause: { + stack: customMockRawStack(5, 5), + cause: { + stack: customMockRawStack(7, 7), + } + } + }, }, }, ); @@ -458,20 +797,20 @@ describe('Debug Symbolicator Integration', () => { exception: { values: [ { - type: 'Error', - value: 'Error: test', + type: 'Error1', + value: 'Error1: test', stacktrace: { frames: [ { - function: 'bar', - filename: '/User/project/node_modules/bar/bar.js', + function: 'bar2', + filename: '/User/project/node_modules/bar/bar2.js', lineno: 2, colno: 2, in_app: false, }, { - function: 'foo', - filename: '/User/project/foo.js', + function: 'foo1', + filename: '/User/project/foo1.js', lineno: 1, colno: 1, in_app: true, @@ -485,15 +824,15 @@ describe('Debug Symbolicator Integration', () => { stacktrace: { frames: [ { - function: 'bar2', - filename: '/User/project/node_modules/bar/bar2.js', + function: 'bar4', + filename: '/User/project/node_modules/bar/bar4.js', lineno: 4, colno: 4, in_app: false, }, { - function: 'foo2', - filename: '/User/project/foo2.js', + function: 'foo3', + filename: '/User/project/foo3.js', lineno: 3, colno: 3, in_app: true, From 1be666e020d4ed12776670df9cb4247527639bc8 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 4 Jul 2024 17:31:48 -0300 Subject: [PATCH 4/4] fix lint --- test/integrations/debugsymbolicator.test.ts | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts index 31c982b2ca..67718b8d93 100644 --- a/test/integrations/debugsymbolicator.test.ts +++ b/test/integrations/debugsymbolicator.test.ts @@ -57,7 +57,9 @@ describe('Debug Symbolicator Integration', () => { const customMockRawStack = (errorNumber: number, value: number) => `Error${errorNumber}: This is mocked error stack trace at foo${errorNumber} (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value}:${value}) - at bar${errorNumber} (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value + 1}:${value + 1}) + at bar${errorNumber} (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:${value + 1}:${ + value + 1 + }) at baz${errorNumber} (native) `; @@ -79,9 +81,8 @@ describe('Debug Symbolicator Integration', () => { lineno: value2, colno: value2, }, - ]; - } + }; beforeEach(() => { (parseErrorStack as jest.Mock).mockReturnValue(>[ @@ -537,8 +538,7 @@ describe('Debug Symbolicator Integration', () => { .mockReturnValueOnce(setupStackFrame(5)) .mockReturnValueOnce(setupStackFrame(7)); - const mocksymbolicateStackTrace = (value: number): ReactNative.SymbolicatedStackTrace => - ({ + const mocksymbolicateStackTrace = (value: number): ReactNative.SymbolicatedStackTrace => ({ stack: [ { file: `/User/project/foo${value}.js`, @@ -552,13 +552,13 @@ describe('Debug Symbolicator Integration', () => { column: value + 1, methodName: `bar${value + 1}`, }, - ] + ], }); (symbolicateStackTrace as jest.Mock) .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(1))) .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(3))) .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(5))) - .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(7))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(7))); const symbolicatedEvent = await processEvent( { @@ -604,8 +604,8 @@ describe('Debug Symbolicator Integration', () => { stack: customMockRawStack(5, 5), cause: { stack: customMockRawStack(7, 7), - } - } + }, + }, }, }, }, @@ -679,7 +679,8 @@ describe('Debug Symbolicator Integration', () => { }, ], }, - }, { + }, + { type: 'Error4', value: 'Error4: test', stacktrace: { @@ -733,8 +734,7 @@ describe('Debug Symbolicator Integration', () => { .mockReturnValueOnce(setupStackFrame(5)) .mockReturnValueOnce(setupStackFrame(7)); - const mocksymbolicateStackTrace = (value: number): ReactNative.SymbolicatedStackTrace => - ({ + const mocksymbolicateStackTrace = (value: number): ReactNative.SymbolicatedStackTrace => ({ stack: [ { file: `/User/project/foo${value}.js`, @@ -748,13 +748,13 @@ describe('Debug Symbolicator Integration', () => { column: value + 1, methodName: `bar${value + 1}`, }, - ] + ], }); (symbolicateStackTrace as jest.Mock) .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(1))) .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(3))) .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(5))) - .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(7))) + .mockReturnValueOnce(Promise.resolve(mocksymbolicateStackTrace(7))); const symbolicatedEvent = await processEvent( { @@ -786,8 +786,8 @@ describe('Debug Symbolicator Integration', () => { stack: customMockRawStack(5, 5), cause: { stack: customMockRawStack(7, 7), - } - } + }, + }, }, }, },