From 8f58e1d8c036b8255dd64a1240fa15ce999abac6 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 24 Nov 2023 20:15:19 +0100 Subject: [PATCH 1/3] update changelog --- CHANGELOG.md | 6 ++ src/js/integrations/debugsymbolicator.ts | 112 +++++++++++++---------- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b635eeac6..0037e7e0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Symbolicate message and non-Error stacktraces locally in debug mode ([#3420](https://github.com/getsentry/sentry-react-native/pull/3420)) + ## 5.14.1 ### Fixes diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 4d1ee69f25..1c8fc1bd2a 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -44,28 +44,37 @@ export class DebugSymbolicator implements Integration { * @inheritDoc */ public setupOnce(): void { - addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { + addGlobalEventProcessor(async (event: Event, hint: EventHint) => { const self = getCurrentHub().getIntegration(DebugSymbolicator); - if (!self || hint === undefined || hint.originalException === undefined) { + if (!self) { return event; } - const reactError = hint.originalException as ReactNativeError; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); - - let stack; - try { - stack = parseErrorStack(reactError); - } catch (e) { - // In RN 0.64 `parseErrorStack` now only takes a string - stack = parseErrorStack(reactError.stack); + if (event.exception + && hint.originalException + && typeof hint.originalException === 'object' + && 'stack' in hint.originalException + && typeof hint.originalException.stack === 'string') { + // originalException is ErrorLike object + const symbolicatedFrames = await this._symbolicate(hint.originalException.stack); + symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); + } else if (hint.syntheticException + && typeof hint.syntheticException === 'object' + && 'stack' in hint.syntheticException + && typeof hint.syntheticException.stack === 'string') { + // syntheticException is Error object + const symbolicatedFrames = await this._symbolicate(hint.syntheticException.stack); + + if (event.exception) { + symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); + } else if (event.threads) { + // RN JS doesn't have threads + // syntheticException is used for Sentry.captureMessage() threads + symbolicatedFrames && this._replaceThreadFramesInEvent(event, symbolicatedFrames); + } } - await self._symbolicate(event, stack); - return event; }); } @@ -74,36 +83,38 @@ export class DebugSymbolicator implements Integration { * Symbolicates the stack on the device talking to local dev server. * Mutates the passed event. */ - private async _symbolicate(event: Event, stack: string | undefined): Promise { + private async _symbolicate(rawStack: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); + const parsedStack = parseErrorStack(rawStack); + try { // eslint-disable-next-line @typescript-eslint/no-var-requires const symbolicateStackTrace = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace'); - const prettyStack = await symbolicateStackTrace(stack); + const prettyStack = await symbolicateStackTrace(parsedStack); - if (prettyStack) { - let newStack = prettyStack; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (prettyStack.stack) { - // This has been changed in an react-native version so stack is contained in here - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - newStack = prettyStack.stack; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const stackWithoutInternalCallsites = newStack.filter( - (frame: { file?: string }) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, - ); - - const symbolicatedFrames = await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); - this._replaceFramesInEvent(event, symbolicatedFrames); - } else { - logger.error('The stack is null'); + if (!prettyStack) { + logger.error('React Native DevServer could not symbolicate the stack trace.'); + return null; } + + // This has been changed in an react-native version so stack is contained in here + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const newStack = prettyStack.stack || prettyStack; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const stackWithoutInternalCallsites = newStack.filter( + (frame: { file?: string }) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, + ); + + return await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); } catch (error) { if (error instanceof Error) { logger.warn(`Unable to symbolicate stack trace: ${error.message}`); } + return null; } } @@ -135,17 +146,6 @@ export class DebugSymbolicator implements Integration { in_app: inApp, }; - // The upstream `react-native@0.61` delegates parsing of stacks to `stacktrace-parser`, which is buggy and - // leaves a trailing `(address at` in the function name. - // `react-native@0.62` seems to have custom logic to parse hermes frames specially. - // Anyway, all we do here is throw away the bogus suffix. - if (newFrame.function) { - const addressAtPos = newFrame.function.indexOf('(address at'); - if (addressAtPos >= 0) { - newFrame.function = newFrame.function.substring(0, addressAtPos).trim(); - } - } - if (inApp) { await this._addSourceContext(newFrame, getDevServer); } @@ -160,7 +160,7 @@ export class DebugSymbolicator implements Integration { * @param event Event * @param frames StackFrame[] */ - private _replaceFramesInEvent(event: Event, frames: StackFrame[]): void { + private _replaceExceptionFramesInEvent(event: Event, frames: StackFrame[]): void { if ( event.exception && event.exception.values && @@ -171,6 +171,22 @@ export class DebugSymbolicator implements Integration { } } + /** + * Replaces the frames in the thread of a message. + * @param event Event + * @param frames StackFrame[] + */ + private _replaceThreadFramesInEvent(event: Event, frames: StackFrame[]): void { + if ( + event.threads && + event.threads.values && + event.threads.values[0] && + event.threads.values[0].stacktrace + ) { + event.threads.values[0].stacktrace.frames = frames.reverse(); + } + } + /** * This tries to add source context for in_app Frames * From 0b4490d3ffec5c50b11ee2ece56917441762f5c3 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 09:58:46 +0100 Subject: [PATCH 2/3] fix lint --- src/js/integrations/debugsymbolicator.ts | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 1c8fc1bd2a..3849c59450 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -51,18 +51,22 @@ export class DebugSymbolicator implements Integration { return event; } - if (event.exception - && hint.originalException - && typeof hint.originalException === 'object' - && 'stack' in hint.originalException - && typeof hint.originalException.stack === 'string') { + if ( + event.exception && + hint.originalException && + typeof hint.originalException === 'object' && + 'stack' in hint.originalException && + typeof hint.originalException.stack === 'string' + ) { // originalException is ErrorLike object const symbolicatedFrames = await this._symbolicate(hint.originalException.stack); symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames); - } else if (hint.syntheticException - && typeof hint.syntheticException === 'object' - && 'stack' in hint.syntheticException - && typeof hint.syntheticException.stack === 'string') { + } else if ( + hint.syntheticException && + typeof hint.syntheticException === 'object' && + 'stack' in hint.syntheticException && + typeof hint.syntheticException.stack === 'string' + ) { // syntheticException is Error object const symbolicatedFrames = await this._symbolicate(hint.syntheticException.stack); @@ -177,12 +181,7 @@ export class DebugSymbolicator implements Integration { * @param frames StackFrame[] */ private _replaceThreadFramesInEvent(event: Event, frames: StackFrame[]): void { - if ( - event.threads && - event.threads.values && - event.threads.values[0] && - event.threads.values[0].stacktrace - ) { + if (event.threads && event.threads.values && event.threads.values[0] && event.threads.values[0].stacktrace) { event.threads.values[0].stacktrace.frames = frames.reverse(); } } From 9d7c3bac9051f03ce791301b9137cdfe74c6cb7d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 27 Nov 2023 16:20:39 +0100 Subject: [PATCH 3/3] More refactor and add tests --- src/js/integrations/debugsymbolicator.ts | 140 +++++----- src/js/vendor/index.ts | 1 + src/js/vendor/react-native/index.ts | 55 ++++ test/integrations/debugsymbolicator.test.ts | 268 ++++++++++++++++++++ 4 files changed, 405 insertions(+), 59 deletions(-) create mode 100644 src/js/vendor/react-native/index.ts create mode 100644 test/integrations/debugsymbolicator.test.ts diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index 3849c59450..ef29bd9a3d 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -1,23 +1,9 @@ -import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import type { Event, EventHint, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventHint, EventProcessor, Hub, Integration, StackFrame as SentryStackFrame } from '@sentry/types'; import { addContextToFrame, logger } from '@sentry/utils'; -const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); - -interface GetDevServer { - (): { url: string }; -} +import type * as ReactNative from '../vendor/react-native'; -/** - * React Native Stack Frame - */ -interface ReactNativeFrame { - // arguments: [] - column: number; - file: string; - lineNumber: number; - methodName: string; -} +const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); /** * React Native Error @@ -43,7 +29,7 @@ export class DebugSymbolicator implements Integration { /** * @inheritDoc */ - public setupOnce(): void { + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(async (event: Event, hint: EventHint) => { const self = getCurrentHub().getIntegration(DebugSymbolicator); @@ -87,30 +73,21 @@ export class DebugSymbolicator implements Integration { * Symbolicates the stack on the device talking to local dev server. * Mutates the passed event. */ - private async _symbolicate(rawStack: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); - const parsedStack = parseErrorStack(rawStack); + private async _symbolicate(rawStack: string): Promise { + const parsedStack = this._parseErrorStack(rawStack); try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const symbolicateStackTrace = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace'); - const prettyStack = await symbolicateStackTrace(parsedStack); - + const prettyStack = await this._symbolicateStackTrace(parsedStack); if (!prettyStack) { logger.error('React Native DevServer could not symbolicate the stack trace.'); return null; } // This has been changed in an react-native version so stack is contained in here - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const newStack = prettyStack.stack || prettyStack; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const stackWithoutInternalCallsites = newStack.filter( - (frame: { file?: string }) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, + (frame: { file?: string }) => frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null, ); return await this._convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites); @@ -126,15 +103,9 @@ export class DebugSymbolicator implements Integration { * Converts ReactNativeFrames to frames in the Sentry format * @param frames ReactNativeFrame[] */ - private async _convertReactNativeFramesToSentryFrames(frames: ReactNativeFrame[]): Promise { - let getDevServer: GetDevServer; - try { - getDevServer = require('react-native/Libraries/Core/Devtools/getDevServer'); - } catch (_oO) { - // We can't load devserver URL - } + private async _convertReactNativeFramesToSentryFrames(frames: ReactNative.StackFrame[]): Promise { return Promise.all( - frames.map(async (frame: ReactNativeFrame): Promise => { + frames.map(async (frame: ReactNative.StackFrame): Promise => { let inApp = !!frame.column && !!frame.lineNumber; inApp = inApp && @@ -142,7 +113,7 @@ export class DebugSymbolicator implements Integration { !frame.file.includes('node_modules') && !frame.file.includes('native code'); - const newFrame: StackFrame = { + const newFrame: SentryStackFrame = { lineno: frame.lineNumber, colno: frame.column, filename: frame.file, @@ -151,7 +122,7 @@ export class DebugSymbolicator implements Integration { }; if (inApp) { - await this._addSourceContext(newFrame, getDevServer); + await this._addSourceContext(newFrame); } return newFrame; @@ -164,7 +135,7 @@ export class DebugSymbolicator implements Integration { * @param event Event * @param frames StackFrame[] */ - private _replaceExceptionFramesInEvent(event: Event, frames: StackFrame[]): void { + private _replaceExceptionFramesInEvent(event: Event, frames: SentryStackFrame[]): void { if ( event.exception && event.exception.values && @@ -180,7 +151,7 @@ export class DebugSymbolicator implements Integration { * @param event Event * @param frames StackFrame[] */ - private _replaceThreadFramesInEvent(event: Event, frames: StackFrame[]): void { + private _replaceThreadFramesInEvent(event: Event, frames: SentryStackFrame[]): void { if (event.threads && event.threads.values && event.threads.values[0] && event.threads.values[0].stacktrace) { event.threads.values[0].stacktrace.frames = frames.reverse(); } @@ -192,30 +163,81 @@ export class DebugSymbolicator implements Integration { * @param frame StackFrame * @param getDevServer function from RN to get DevServer URL */ - private async _addSourceContext(frame: StackFrame, getDevServer?: GetDevServer): Promise { - let response; + private async _addSourceContext(frame: SentryStackFrame): Promise { + let sourceContext: string | null = null; const segments = frame.filename?.split('/') ?? []; - if (getDevServer) { - for (const idx in segments) { - if (Object.prototype.hasOwnProperty.call(segments, idx)) { - response = await fetch(`${getDevServer().url}${segments.slice(-idx).join('/')}`, { - method: 'GET', - }); + const serverUrl = this._getDevServer()?.url; + if (!serverUrl) { + return; + } - if (response.ok) { - break; - } - } + for (const idx in segments) { + if (!Object.prototype.hasOwnProperty.call(segments, idx)) { + continue; } + + sourceContext = await this._fetchSourceContext(serverUrl, segments, -idx); + if (sourceContext) { + break; + } + } + + if (!sourceContext) { + return; + } + + const lines = sourceContext.split('\n'); + addContextToFrame(lines, frame); + } + + /** + * Get source context for segment + */ + private async _fetchSourceContext(url: string, segments: Array, start: number): Promise { + const response = await fetch(`${url}${segments.slice(start).join('/')}`, { + method: 'GET', + }); + + if (response.ok) { + return response.text(); } + return null; + } - if (response && response.ok) { - const content = await response.text(); - const lines = content.split('\n'); + /** + * Loads and calls RN Core Devtools parseErrorStack function. + */ + private _parseErrorStack(errorStack: string): Array { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack'); + return parseErrorStack(errorStack); + } - addContextToFrame(lines, frame); + /** + * Loads and calls RN Core Devtools symbolicateStackTrace function. + */ + private _symbolicateStackTrace( + stack: Array, + extraData?: Record, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const symbolicateStackTrace = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace'); + return symbolicateStackTrace(stack, extraData); + } + + /** + * Loads and returns the RN DevServer URL. + */ + private _getDevServer(): ReactNative.DevServerInfo | undefined { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const getDevServer = require('react-native/Libraries/Core/Devtools/getDevServer'); + return getDevServer(); + } catch (_oO) { + // We can't load devserver URL } + return undefined; } } diff --git a/src/js/vendor/index.ts b/src/js/vendor/index.ts index 80d5ec4054..c26529aed0 100644 --- a/src/js/vendor/index.ts +++ b/src/js/vendor/index.ts @@ -1 +1,2 @@ export { utf8ToBytes } from './buffer'; +export * from './react-native'; diff --git a/src/js/vendor/react-native/index.ts b/src/js/vendor/react-native/index.ts new file mode 100644 index 0000000000..d3affa091c --- /dev/null +++ b/src/js/vendor/react-native/index.ts @@ -0,0 +1,55 @@ +// MIT License + +// Copyright (c) Meta Platforms, Inc. and affiliates. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/NativeExceptionsManager.js#L17 +export type StackFrame = { + column?: number; + file?: string; + lineNumber?: number; + methodName: string; + collapse?: boolean; +}; + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js#L17 +export type CodeFrame = Readonly<{ + content: string; + location?: { + [key: string]: unknown; + row: number; + column: number; + }; + fileName: string; +}>; + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js#L27 +export type SymbolicatedStackTrace = Readonly<{ + stack: Array; + codeFrame?: CodeFrame; +}>; + +// Adapted from https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/Libraries/Core/Devtools/getDevServer.js#L17 +export type DevServerInfo = { + [key: string]: unknown; + url: string; + fullBundleUrl?: string; + bundleLoadedFromServer: boolean; +}; diff --git a/test/integrations/debugsymbolicator.test.ts b/test/integrations/debugsymbolicator.test.ts new file mode 100644 index 0000000000..34cebc688d --- /dev/null +++ b/test/integrations/debugsymbolicator.test.ts @@ -0,0 +1,268 @@ +import type { Event, EventHint, Hub, Integration, StackFrame } from '@sentry/types'; + +import { DebugSymbolicator } from '../../src/js/integrations/debugsymbolicator'; +import type * as ReactNative from '../../src/js/vendor/react-native'; + +interface MockDebugSymbolicator extends Integration { + _parseErrorStack: jest.Mock, [string]>; + _symbolicateStackTrace: jest.Mock< + Promise, + [Array, Record | undefined] + >; + _getDevServer: jest.Mock; + _fetchSourceContext: jest.Mock, [string, Array, number]>; +} + +describe('Debug Symbolicator Integration', () => { + let integration: MockDebugSymbolicator; + const mockGetCurrentHub = () => + ({ + getIntegration: () => integration, + } as unknown as Hub); + + beforeEach(() => { + integration = new DebugSymbolicator() as unknown as MockDebugSymbolicator; + integration._parseErrorStack = jest.fn().mockReturnValue([]); + integration._symbolicateStackTrace = jest.fn().mockReturnValue( + Promise.resolve({ + stack: [], + }), + ); + integration._getDevServer = jest.fn().mockReturnValue({ + url: 'http://localhost:8081', + }); + integration._fetchSourceContext = jest.fn().mockReturnValue(Promise.resolve(null)); + }); + + describe('parse stack', () => { + const mockRawStack = `Error: This is mocked error stack trace + at foo (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:1:1) + at bar (http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2:2) + at baz (native) +`; + + const mockSentryParsedFrames: Array = [ + { + function: '[native] baz', + }, + { + function: 'bar', + filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2:2', + lineno: 2, + colno: 2, + }, + { + function: 'foo', + filename: 'http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:1:1', + lineno: 1, + colno: 1, + }, + ]; + + beforeEach(() => { + integration._parseErrorStack = jest.fn().mockReturnValue(>[ + { + 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', + }, + ]); + + integration._symbolicateStackTrace = jest.fn().mockReturnValue( + 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', + }, + ], + }), + ); + }); + + it('should symbolicate errors stack trace', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + ], + }, + }, + { + originalException: { + stack: mockRawStack, + }, + }, + ); + + 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, + }, + ], + }, + }, + ], + }, + }); + }); + + it('should symbolicate synthetic error stack trace for exception', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Error: test', + stacktrace: { + frames: [], + }, + }, + ], + }, + }, + { + originalException: 'Error: test', + syntheticException: { + stack: mockRawStack, + } as unknown as Error, + }, + ); + + 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, + }, + ], + }, + }, + ], + }, + }); + }); + + it('should symbolicate synthetic error stack trace for message', async () => { + const symbolicatedEvent = await executeIntegrationFor( + { + threads: { + values: [ + { + stacktrace: { + frames: mockSentryParsedFrames, + }, + }, + ], + }, + }, + { + syntheticException: { + stack: mockRawStack, + } as unknown as Error, + }, + ); + + expect(symbolicatedEvent).toStrictEqual({ + threads: { + values: [ + { + 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, + }, + ], + }, + }, + ], + }, + }); + }); + }); + + function executeIntegrationFor(mockedEvent: Event, hint: EventHint): Promise { + return new Promise((resolve, reject) => { + if (!integration) { + throw new Error('Setup integration before executing the test.'); + } + + integration.setupOnce(async eventProcessor => { + try { + const processedEvent = await eventProcessor(mockedEvent, hint); + resolve(processedEvent); + } catch (e) { + reject(e); + } + }, mockGetCurrentHub); + }); + } +});