From e4b3335fd12917bdde464a59dce62fce2787db68 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 17 Jul 2023 15:42:58 +0200 Subject: [PATCH 01/16] Add native cause tests --- test/integrations/nativecause.test.ts | 179 ++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 test/integrations/nativecause.test.ts diff --git a/test/integrations/nativecause.test.ts b/test/integrations/nativecause.test.ts new file mode 100644 index 000000000..1d4a4e4a1 --- /dev/null +++ b/test/integrations/nativecause.test.ts @@ -0,0 +1,179 @@ + + +// {"cause":{"name":"java.lang.RuntimeException","message":"The operation failed.","stackElements":[{"className":"com.sentryreactnativeamanightly.modules.TurboCrashModule","fileName":"TurboCrashModule.kt","lineNumber":10,"methodName":"getDataCrash"},{"className":"com.facebook.jni.NativeRunnable","fileName":"NativeRunnable.java","lineNumber":-2,"methodName":"run"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":942,"methodName":"handleCallback"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":99,"methodName":"dispatchMessage"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadHandler","fileName":"MessageQueueThreadHandler.java","lineNumber":27,"methodName":"dispatchMessage"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":201,"methodName":"loopOnce"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":288,"methodName":"loop"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadImpl$4","fileName":"MessageQueueThreadImpl.java","lineNumber":228,"methodName":"run"},{"className":"java.lang.Thread","fileName":"Thread.java","lineNumber":1012,"methodName":"run"}]}} + +import type { Event,EventHint, ExtendedError } from '@sentry/types'; + +describe('NativeCause', () => { + + it('keeps event without cause as is', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node' + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }, + {}, + ); + + expect(actualEvent).toEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node' + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }); + }); + + it('adds android java cause from the original error to the event', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node' + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }, + { + originalException: createNewError({ + message: 'JavaScript error message', + name: 'JavaScriptError', + stack: 'JavaScriptError: JavaScript error message\n' + + 'at onPress (index.bundle:75:33)\n' + + 'at _performTransitionSideEffects (index.bundle:65919:22)', + cause: { + name: 'java.lang.RuntimeException', + message: 'The operation failed.', + stackElements: [ + { + className: 'com.example.modules.Crash', + fileName: 'Crash.kt', + lineNumber: 10, + methodName: 'getDataCrash' + }, + { + className: 'com.facebook.jni.NativeRunnable', + fileName: 'NativeRunnable.java', + lineNumber: -2, + methodName: 'run' + } + ] + }, + }), + }, + ); + + expect(actualEvent).toEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node' + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }); + }); + +}); + +function executeIntegrationFor(_mockedEvent: Event, _mockedHint: EventHint): Promise { + // const integration = new ReactNativeInfo(); + // return new Promise((resolve, reject) => { + // integration.setupOnce(async eventProcessor => { + // try { + // const processedEvent = await eventProcessor(mockedEvent, mockedHint); + // resolve(processedEvent); + // } catch (e) { + // reject(e); + // } + // }); + // }); + return Promise.resolve(null); +} + +function createNewError(from: { + message: string; + name?: string; + stack?: string; + cause?: unknown; +}): ExtendedError { + const error: ExtendedError = new Error(from.message); + if (from.name) { + error.name = from.name; + } + error.stack = from.stack; + error.cause = from.cause; + return error; +} From ecd4c2d36df1b9dbbe5c1b6acfabb75b66b45cb1 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 18 Jul 2023 12:30:02 +0200 Subject: [PATCH 02/16] Add native error cause parsing --- src/js/integrations/nativelinkederrors.ts | 273 +++++++++++++ test/integrations/nativecause.test.ts | 179 --------- test/integrations/nativelinkederrors.test.ts | 382 +++++++++++++++++++ 3 files changed, 655 insertions(+), 179 deletions(-) create mode 100644 src/js/integrations/nativelinkederrors.ts delete mode 100644 test/integrations/nativecause.test.ts create mode 100644 test/integrations/nativelinkederrors.test.ts diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts new file mode 100644 index 000000000..6ea9cfb37 --- /dev/null +++ b/src/js/integrations/nativelinkederrors.ts @@ -0,0 +1,273 @@ +import { + EventHint, + EventProcessor, + Exception, + ExtendedError, + Hub, + Integration, + StackParser, + Event, + StackFrame, +} from '@sentry/types'; +import { isInstanceOf } from '@sentry/utils'; + +const DEFAULT_KEY = 'cause'; +const DEFAULT_LIMIT = 5; + +interface LinkedErrorsOptions { + key: string; + limit: number; +} + +export class NativeLinkedErrors implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'NativeLinkedErrors'; + + /** + * @inheritDoc + */ + public name: string = NativeLinkedErrors.id; + + /** + * @inheritDoc + */ + private readonly _key: LinkedErrorsOptions['key']; + + /** + * @inheritDoc + */ + private readonly _limit: LinkedErrorsOptions['limit']; + + /** + * @inheritDoc + */ + public constructor(options: Partial = {}) { + this._key = options.key || DEFAULT_KEY; + this._limit = options.limit || DEFAULT_LIMIT; + } + + /** + * @inheritDoc + */ + public setupOnce( + addGlobalEventProcessor: (callback: EventProcessor) => void, + getCurrentHub: () => Hub, + ): void { + const client = getCurrentHub().getClient(); + if (!client) { + return; + } + + addGlobalEventProcessor((event: Event, hint?: EventHint) => { + const self = getCurrentHub().getIntegration(NativeLinkedErrors); + return self + ? this._handler(client.getOptions().stackParser, self._key, self._limit, event, hint) + : event; + }); + } + + /** + * @inheritDoc + */ + public _handler( + parser: StackParser, + key: string, + limit: number, + event: Event, + hint?: EventHint, + ): Event | null { + if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { + return event; + } + const linkedErrors = this._walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); + event.exception.values = [...linkedErrors, ...event.exception.values]; + return event; + } + + /** + * @inheritDoc + */ + public _walkErrorTree( + parser: StackParser, + limit: number, + error: ExtendedError, + key: string, + stack: Exception[] = [], + ): Exception[] { + if (!isInstanceOf(error[key], Error) || stack.length + 1 >= limit) { + return stack; + } + + const linkedError = error[key]; + let exception: Exception; + if ('stackElements' in linkedError) { + // isJavaException + exception = exceptionFromJavaStackElements(linkedError); + } else if ('stackSymbols' in linkedError) { + // isObjCException symbolicated by local debug symbols + exception = exceptionFromAppleStackSymbols(linkedError); + } else if ('stackReturnAddresses' in linkedError) { + // isObjCException + exception = exceptionFromAppleStackReturnAddresses(linkedError); + } else { + exception = exceptionFromError(parser, error[key]); + } + + return this._walkErrorTree(parser, limit, error[key], key, [exception, ...stack]); + } +} + +export function exceptionFromJavaStackElements( + javaThrowable: { + name: string; + message: string; + stackElements: { + className: string; + fileName: string; + methodName: string; + lineNumber: number; + }[], + }, +): Exception { + return { + type: javaThrowable.name, + value: javaThrowable.message, + stacktrace: { + frames: javaThrowable.stackElements.map(stackElement => ({ + platform: 'java', + module: stackElement.className, + filename: stackElement.fileName, + lineno: stackElement.lineNumber, + function: stackElement.methodName, + })), + }, + }; +} + +export function exceptionFromAppleStackSymbols( + objCException: { + name: string; + message: string; + stackSymbols: string[]; + }, +): Exception { + return { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: objCException.stackSymbols.map(stackSymbol => { + const addrStartIndex = stackSymbol.indexOf(' 0x') + 1; + const addrEndIndex = stackSymbol.indexOf(' ', addrStartIndex); + const pkg = stackSymbol.substring(5, addrStartIndex).trimEnd(); + const addr = stackSymbol.substring(addrStartIndex + 2, addrEndIndex - 1); + const functionEndIndex = stackSymbol.indexOf(' + ', addrEndIndex); + const func = stackSymbol.substring(addrEndIndex + 1, functionEndIndex); + return { + platform: 'cocoa', + package: pkg, + function: func, + instruction_addr: addr, + }; + }), + }, + }; +} + +export function exceptionFromAppleStackReturnAddresses( + objCException: { + name: string; + message: string; + stackReturnAddresses: number[]; + }, +): Exception { + return { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: objCException.stackReturnAddresses.map( returnAddress => ({ + platform: 'cocoa', + instruction_addr: returnAddress.toString(16).padStart(16, '0'), + })), + }, + }; +} + +// TODO: Needs to be exported from @sentry/browser +/** + * This function creates an exception from a JavaScript Error + */ +export function exceptionFromError(stackParser: StackParser, ex: Error): Exception { + // Get the frames first since Opera can lose the stack if we touch anything else first + const frames = parseStackFrames(stackParser, ex); + + const exception: Exception = { + type: ex && ex.name, + value: extractMessage(ex), + }; + + if (frames.length) { + exception.stacktrace = { frames }; + } + + if (exception.type === undefined && exception.value === '') { + exception.value = 'Unrecoverable error caught'; + } + + return exception; +} + +/** Parses stack frames from an error */ +export function parseStackFrames( + stackParser: StackParser, + ex: Error & { framesToPop?: number; stacktrace?: string }, +): StackFrame[] { + // Access and store the stacktrace property before doing ANYTHING + // else to it because Opera is not very good at providing it + // reliably in other circumstances. + const stacktrace = ex.stacktrace || ex.stack || ''; + + const popSize = getPopSize(ex); + + try { + return stackParser(stacktrace, popSize); + } catch (e) { + // no-empty + } + + return []; +} + +/** + * 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 { + const message = ex && ex.message; + if (!message) { + return 'No error message'; + } + if (message.error && typeof message.error.message === 'string') { + return message.error.message; + } + return message; +} + +// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108 +const reactMinifiedRegexp = /Minified React error #\d+;/i; + +function getPopSize(ex: Error & { framesToPop?: number }): number { + if (ex) { + if (typeof ex.framesToPop === 'number') { + return ex.framesToPop; + } + + if (reactMinifiedRegexp.test(ex.message)) { + return 1; + } + } + + return 0; +} diff --git a/test/integrations/nativecause.test.ts b/test/integrations/nativecause.test.ts deleted file mode 100644 index 1d4a4e4a1..000000000 --- a/test/integrations/nativecause.test.ts +++ /dev/null @@ -1,179 +0,0 @@ - - -// {"cause":{"name":"java.lang.RuntimeException","message":"The operation failed.","stackElements":[{"className":"com.sentryreactnativeamanightly.modules.TurboCrashModule","fileName":"TurboCrashModule.kt","lineNumber":10,"methodName":"getDataCrash"},{"className":"com.facebook.jni.NativeRunnable","fileName":"NativeRunnable.java","lineNumber":-2,"methodName":"run"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":942,"methodName":"handleCallback"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":99,"methodName":"dispatchMessage"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadHandler","fileName":"MessageQueueThreadHandler.java","lineNumber":27,"methodName":"dispatchMessage"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":201,"methodName":"loopOnce"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":288,"methodName":"loop"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadImpl$4","fileName":"MessageQueueThreadImpl.java","lineNumber":228,"methodName":"run"},{"className":"java.lang.Thread","fileName":"Thread.java","lineNumber":1012,"methodName":"run"}]}} - -import type { Event,EventHint, ExtendedError } from '@sentry/types'; - -describe('NativeCause', () => { - - it('keeps event without cause as is', async () => { - const actualEvent = await executeIntegrationFor( - { - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - in_app: false, - platform: 'node' - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - } - ] - }, - }, - {}, - ); - - expect(actualEvent).toEqual({ - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - in_app: false, - platform: 'node' - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - } - ] - }, - }); - }); - - it('adds android java cause from the original error to the event', async () => { - const actualEvent = await executeIntegrationFor( - { - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - in_app: false, - platform: 'node' - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - } - ] - }, - }, - { - originalException: createNewError({ - message: 'JavaScript error message', - name: 'JavaScriptError', - stack: 'JavaScriptError: JavaScript error message\n' + - 'at onPress (index.bundle:75:33)\n' + - 'at _performTransitionSideEffects (index.bundle:65919:22)', - cause: { - name: 'java.lang.RuntimeException', - message: 'The operation failed.', - stackElements: [ - { - className: 'com.example.modules.Crash', - fileName: 'Crash.kt', - lineNumber: 10, - methodName: 'getDataCrash' - }, - { - className: 'com.facebook.jni.NativeRunnable', - fileName: 'NativeRunnable.java', - lineNumber: -2, - methodName: 'run' - } - ] - }, - }), - }, - ); - - expect(actualEvent).toEqual({ - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - in_app: false, - platform: 'node' - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - } - ] - }, - }); - }); - -}); - -function executeIntegrationFor(_mockedEvent: Event, _mockedHint: EventHint): Promise { - // const integration = new ReactNativeInfo(); - // return new Promise((resolve, reject) => { - // integration.setupOnce(async eventProcessor => { - // try { - // const processedEvent = await eventProcessor(mockedEvent, mockedHint); - // resolve(processedEvent); - // } catch (e) { - // reject(e); - // } - // }); - // }); - return Promise.resolve(null); -} - -function createNewError(from: { - message: string; - name?: string; - stack?: string; - cause?: unknown; -}): ExtendedError { - const error: ExtendedError = new Error(from.message); - if (from.name) { - error.name = from.name; - } - error.stack = from.stack; - error.cause = from.cause; - return error; -} diff --git a/test/integrations/nativelinkederrors.test.ts b/test/integrations/nativelinkederrors.test.ts new file mode 100644 index 000000000..53db9f663 --- /dev/null +++ b/test/integrations/nativelinkederrors.test.ts @@ -0,0 +1,382 @@ + + +// {"cause":{"name":"java.lang.RuntimeException","message":"The operation failed.","stackElements":[{"className":"com.sentryreactnativeamanightly.modules.TurboCrashModule","fileName":"TurboCrashModule.kt","lineNumber":10,"methodName":"getDataCrash"},{"className":"com.facebook.jni.NativeRunnable","fileName":"NativeRunnable.java","lineNumber":-2,"methodName":"run"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":942,"methodName":"handleCallback"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":99,"methodName":"dispatchMessage"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadHandler","fileName":"MessageQueueThreadHandler.java","lineNumber":27,"methodName":"dispatchMessage"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":201,"methodName":"loopOnce"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":288,"methodName":"loop"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadImpl$4","fileName":"MessageQueueThreadImpl.java","lineNumber":228,"methodName":"run"},{"className":"java.lang.Thread","fileName":"Thread.java","lineNumber":1012,"methodName":"run"}]}} + +import type { Event,EventHint, ExtendedError } from '@sentry/types'; + +describe('NativeLinkedErrors', () => { + + it('keeps event without cause as is', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node' + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }, + {}, + ); + + expect(actualEvent).toEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node' + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }); + }); + + it('adds android java cause from the original error to the event', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }, + { + originalException: createNewError({ + message: 'JavaScript error message', + name: 'JavaScriptError', + stack: 'JavaScriptError: JavaScript error message\n' + + 'at onPress (index.bundle:75:33)\n' + + 'at _performTransitionSideEffects (index.bundle:65919:22)', + cause: { + name: 'java.lang.RuntimeException', + message: 'Java error message.', + stackElements: [ + { + className: 'com.example.modules.Crash', + fileName: 'Crash.kt', + lineNumber: 10, + methodName: 'getDataCrash' + }, + { + className: 'com.facebook.jni.NativeRunnable', + fileName: 'NativeRunnable.java', + lineNumber: 2, + methodName: 'run' + } + ] + }, + }), + }, + ); + + expect(actualEvent).toEqual( > { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, + { + type: 'Error', + value: 'Java error message.', + stacktrace: { + frames: [ + { + platform: 'java', + module: 'com.example.modules.Crash', + filename: 'Crash.kt', + lineno: 10, + function: 'getDataCrash' + }, + { + platform: 'java', + module: 'com.facebook.jni.NativeRunnable', + filename: 'NativeRunnable.java', + lineno: 2, + function: 'run' + }, + ], + } + } + ] + }, + }); + }); + + it('adds ios objective-c cause from the original error to the event', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }, + { + originalException: createNewError({ + message: 'JavaScript error message', + name: 'JavaScriptError', + stack: 'JavaScriptError: JavaScript error message\n' + + 'at onPress (index.bundle:75:33)\n' + + 'at _performTransitionSideEffects (index.bundle:65919:22)', + cause: { + name: 'Error', + message: 'Objective-c error message.', + stackSymbols: [ + '0 CoreFoundation 0x0000000180437330 __exceptionPreprocess + 172', + '1 libobjc.A.dylib 0x0000000180051274 objc_exception_throw + 56', + '2 RNTester 0x0000000103535900 -[RCTSampleTurboModule getObjectThrows:] + 120', + ] + }, + }), + }, + ); + + expect(actualEvent).toEqual(>{ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, + { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + { + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + instruction_addr: '0000000180437330', + }, + { + platform: 'cocoa', + package: 'libobjc.A.dylib', + function: 'objc_exception_throw', + instruction_addr: '0000000180051274', + }, + { + platform: 'cocoa', + package: 'RNTester', + function: '-[RCTSampleTurboModule getObjectThrows:]', + instruction_addr: '0000000103535900', + }, + ], + } + } + ] + }, + }); + }); + + it('adds ios objective-c cause from the original error to the event', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + } + ] + }, + }, + { + originalException: createNewError({ + message: 'JavaScript error message', + name: 'JavaScriptError', + stack: 'JavaScriptError: JavaScript error message\n' + + 'at onPress (index.bundle:75:33)\n' + + 'at _performTransitionSideEffects (index.bundle:65919:22)', + cause: { + name: 'Error', + message: 'Objective-c error message.', + stackReturnAddresses: [ + 6446871344, + 6442783348, + 4350761216, + ], + }, + }), + }, + ); + + expect(actualEvent).toEqual( > { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, + { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + { + platform: 'cocoa', + instruction_addr: '0000000180437330', + }, + { + platform: 'cocoa', + instruction_addr: '0000000180051274', + }, + { + platform: 'cocoa', + instruction_addr: '0000000103535900', + }, + ], + } + } + ] + }, + }); + }); +}); + +function executeIntegrationFor(_mockedEvent: Event, _mockedHint: EventHint): Promise { + // const integration = new ReactNativeInfo(); + // return new Promise((resolve, reject) => { + // integration.setupOnce(async eventProcessor => { + // try { + // const processedEvent = await eventProcessor(mockedEvent, mockedHint); + // resolve(processedEvent); + // } catch (e) { + // reject(e); + // } + // }); + // }); + return Promise.resolve(null); +} + +function createNewError(from: { + message: string; + name?: string; + stack?: string; + cause?: unknown; +}): ExtendedError { + const error: ExtendedError = new Error(from.message); + if (from.name) { + error.name = from.name; + } + error.stack = from.stack; + error.cause = from.cause; + return error; +} From 24a6f575f3edae376ed3994c6b4fd2ad58f007e4 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 18 Jul 2023 13:12:55 +0200 Subject: [PATCH 03/16] Fix build and tests --- src/js/integrations/nativelinkederrors.ts | 32 ++-- src/js/sdk.tsx | 3 + test/integrations/nativelinkederrors.test.ts | 156 ++++++++++--------- 3 files changed, 108 insertions(+), 83 deletions(-) diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 6ea9cfb37..63310d9b1 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -1,13 +1,13 @@ -import { +import type { + Event, EventHint, EventProcessor, Exception, ExtendedError, Hub, Integration, - StackParser, - Event, StackFrame, + StackParser, } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; @@ -19,6 +19,9 @@ interface LinkedErrorsOptions { limit: number; } +/** + * + */ export class NativeLinkedErrors implements Integration { /** * @inheritDoc @@ -96,11 +99,11 @@ export class NativeLinkedErrors implements Integration { key: string, stack: Exception[] = [], ): Exception[] { - if (!isInstanceOf(error[key], Error) || stack.length + 1 >= limit) { + const linkedError = error[key]; + if (!linkedError || stack.length + 1 >= limit) { return stack; } - const linkedError = error[key]; let exception: Exception; if ('stackElements' in linkedError) { // isJavaException @@ -111,14 +114,19 @@ export class NativeLinkedErrors implements Integration { } else if ('stackReturnAddresses' in linkedError) { // isObjCException exception = exceptionFromAppleStackReturnAddresses(linkedError); - } else { + } else if (isInstanceOf(linkedError, Error)) { exception = exceptionFromError(parser, error[key]); + } else { + return stack; } return this._walkErrorTree(parser, limit, error[key], key, [exception, ...stack]); } } +/** + * + */ export function exceptionFromJavaStackElements( javaThrowable: { name: string; @@ -146,6 +154,9 @@ export function exceptionFromJavaStackElements( }; } +/** + * + */ export function exceptionFromAppleStackSymbols( objCException: { name: string; @@ -160,8 +171,8 @@ export function exceptionFromAppleStackSymbols( frames: objCException.stackSymbols.map(stackSymbol => { const addrStartIndex = stackSymbol.indexOf(' 0x') + 1; const addrEndIndex = stackSymbol.indexOf(' ', addrStartIndex); - const pkg = stackSymbol.substring(5, addrStartIndex).trimEnd(); - const addr = stackSymbol.substring(addrStartIndex + 2, addrEndIndex - 1); + const pkg = stackSymbol.substring(4, addrStartIndex).trim(); + const addr = stackSymbol.substring(addrStartIndex + 2, addrEndIndex); const functionEndIndex = stackSymbol.indexOf(' + ', addrEndIndex); const func = stackSymbol.substring(addrEndIndex + 1, functionEndIndex); return { @@ -175,6 +186,9 @@ export function exceptionFromAppleStackSymbols( }; } +/** + * + */ export function exceptionFromAppleStackReturnAddresses( objCException: { name: string; @@ -188,7 +202,7 @@ export function exceptionFromAppleStackReturnAddresses( stacktrace: { frames: objCException.stackReturnAddresses.map( returnAddress => ({ platform: 'cocoa', - instruction_addr: returnAddress.toString(16).padStart(16, '0'), + instruction_addr: returnAddress.toString(16), })), }, }; diff --git a/src/js/sdk.tsx b/src/js/sdk.tsx index ada2c99b8..5a0183fbe 100644 --- a/src/js/sdk.tsx +++ b/src/js/sdk.tsx @@ -22,6 +22,7 @@ import { Release, SdkInfo, } from './integrations'; +import { NativeLinkedErrors } from './integrations/nativelinkederrors'; import { createReactNativeRewriteFrames } from './integrations/rewriteframes'; import { Screenshot } from './integrations/screenshot'; import { ViewHierarchy } from './integrations/viewhierarchy'; @@ -37,6 +38,7 @@ import { NATIVE } from './wrapper'; const IGNORED_DEFAULT_INTEGRATIONS = [ 'GlobalHandlers', // We will use the react-native internal handlers 'TryCatch', // We don't need this + 'LinkedErrors', // We replace this with `NativeLinkedError` ]; const DEFAULT_OPTIONS: ReactNativeOptions = { enableNativeCrashHandling: true, @@ -105,6 +107,7 @@ export function init(passedOptions: ReactNativeOptions): void { ), ]); + defaultIntegrations.push(new NativeLinkedErrors()); defaultIntegrations.push(new EventOrigin()); defaultIntegrations.push(new SdkInfo()); defaultIntegrations.push(new ReactNativeInfo()); diff --git a/test/integrations/nativelinkederrors.test.ts b/test/integrations/nativelinkederrors.test.ts index 53db9f663..8b3f45c00 100644 --- a/test/integrations/nativelinkederrors.test.ts +++ b/test/integrations/nativelinkederrors.test.ts @@ -1,8 +1,7 @@ +import { defaultStackParser } from '@sentry/browser'; +import type { Event,EventHint, ExtendedError, Hub, Integration, IntegrationClass } from '@sentry/types'; - -// {"cause":{"name":"java.lang.RuntimeException","message":"The operation failed.","stackElements":[{"className":"com.sentryreactnativeamanightly.modules.TurboCrashModule","fileName":"TurboCrashModule.kt","lineNumber":10,"methodName":"getDataCrash"},{"className":"com.facebook.jni.NativeRunnable","fileName":"NativeRunnable.java","lineNumber":-2,"methodName":"run"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":942,"methodName":"handleCallback"},{"className":"android.os.Handler","fileName":"Handler.java","lineNumber":99,"methodName":"dispatchMessage"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadHandler","fileName":"MessageQueueThreadHandler.java","lineNumber":27,"methodName":"dispatchMessage"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":201,"methodName":"loopOnce"},{"className":"android.os.Looper","fileName":"Looper.java","lineNumber":288,"methodName":"loop"},{"className":"com.facebook.react.bridge.queue.MessageQueueThreadImpl$4","fileName":"MessageQueueThreadImpl.java","lineNumber":228,"methodName":"run"},{"className":"java.lang.Thread","fileName":"Thread.java","lineNumber":1012,"methodName":"run"}]}} - -import type { Event,EventHint, ExtendedError } from '@sentry/types'; +import { NativeLinkedErrors } from '../../src/js/integrations/nativelinkederrors'; describe('NativeLinkedErrors', () => { @@ -121,24 +120,7 @@ describe('NativeLinkedErrors', () => { exception: { values: [ { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - }, - { - type: 'Error', + type: 'java.lang.RuntimeException', value: 'Java error message.', stacktrace: { frames: [ @@ -158,7 +140,24 @@ describe('NativeLinkedErrors', () => { }, ], } - } + }, + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, ] }, }); @@ -212,23 +211,6 @@ describe('NativeLinkedErrors', () => { expect(actualEvent).toEqual(>{ exception: { values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - }, { type: 'Error', value: 'Objective-c error message.', @@ -254,7 +236,24 @@ describe('NativeLinkedErrors', () => { }, ], } - } + }, + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, ] }, }); @@ -308,6 +307,26 @@ describe('NativeLinkedErrors', () => { expect(actualEvent).toEqual( > { exception: { values: [ + { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + { + platform: 'cocoa', + instruction_addr: '180437330', + }, + { + platform: 'cocoa', + instruction_addr: '180051274', + }, + { + platform: 'cocoa', + instruction_addr: '103535900', + }, + ], + } + }, { type: 'Error', value: 'Captured exception', @@ -325,45 +344,34 @@ describe('NativeLinkedErrors', () => { handled: true } }, - { - type: 'Error', - value: 'Objective-c error message.', - stacktrace: { - frames: [ - { - platform: 'cocoa', - instruction_addr: '0000000180437330', - }, - { - platform: 'cocoa', - instruction_addr: '0000000180051274', - }, - { - platform: 'cocoa', - instruction_addr: '0000000103535900', - }, - ], - } - } ] }, }); }); }); -function executeIntegrationFor(_mockedEvent: Event, _mockedHint: EventHint): Promise { - // const integration = new ReactNativeInfo(); - // return new Promise((resolve, reject) => { - // integration.setupOnce(async eventProcessor => { - // try { - // const processedEvent = await eventProcessor(mockedEvent, mockedHint); - // resolve(processedEvent); - // } catch (e) { - // reject(e); - // } - // }); - // }); - return Promise.resolve(null); +function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Promise { + const integration = new NativeLinkedErrors(); + return new Promise((resolve, reject) => { + integration.setupOnce( + async eventProcessor => { + try { + const processedEvent = await eventProcessor(mockedEvent, mockedHint); + resolve(processedEvent); + } catch (e) { + reject(e); + } + }, + () => ({ + getClient: () => ({ + getOptions: () => ({ + stackParser: defaultStackParser, + }) + }), + getIntegration: () => integration, + } as unknown as Hub), + ); + }); } function createNewError(from: { From a6ee230991a963a0d7704c84ac3c752274ef2fd9 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 19 Jul 2023 16:29:24 +0200 Subject: [PATCH 04/16] Rewrite frames skips native frames --- src/js/integrations/rewriteframes.ts | 6 ++ test/integrations/rewriteframes.test.ts | 77 +++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/js/integrations/rewriteframes.ts b/src/js/integrations/rewriteframes.ts index 7fbf250e9..e059e5e90 100644 --- a/src/js/integrations/rewriteframes.ts +++ b/src/js/integrations/rewriteframes.ts @@ -16,6 +16,12 @@ const IOS_DEFAULT_BUNDLE_NAME = 'app:///main.jsbundle'; export function createReactNativeRewriteFrames(): RewriteFrames { return new RewriteFrames({ iteratee: (frame: StackFrame) => { + if (frame.platform === 'java' || frame.platform === 'cocoa') { + // Because platform is not required in StackFrame type + // we assume that if not set it's javascript + return frame; + } + if (!frame.filename) { return frame; } diff --git a/test/integrations/rewriteframes.test.ts b/test/integrations/rewriteframes.test.ts index 45f7ddace..a357fb081 100644 --- a/test/integrations/rewriteframes.test.ts +++ b/test/integrations/rewriteframes.test.ts @@ -1,5 +1,6 @@ import type { Exception } from '@sentry/browser'; import { defaultStackParser, eventFromException } from '@sentry/browser'; +import type { Event } from '@sentry/types'; import { Platform } from 'react-native'; import { createReactNativeRewriteFrames } from '../../src/js/integrations/rewriteframes'; @@ -31,6 +32,82 @@ describe('RewriteFrames', () => { jest.resetAllMocks(); }); + it('should not change cocoa frames', async () => { + const EXPECTED_SENTRY_COCOA_EXCEPTION = { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + { + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + instruction_addr: '0000000180437330', + }, + { + platform: 'cocoa', + package: 'libobjc.A.dylib', + function: 'objc_exception_throw', + instruction_addr: '0000000180051274', + }, + { + platform: 'cocoa', + package: 'RNTester', + function: '-[RCTSampleTurboModule getObjectThrows:]', + instruction_addr: '0000000103535900', + }, + ], + } + }; + + const SENTRY_COCOA_EXCEPTION_EVENT: Event = { + exception: { + values: [ + JSON.parse(JSON.stringify(EXPECTED_SENTRY_COCOA_EXCEPTION)), + ], + }, + }; + + const event = createReactNativeRewriteFrames().process(SENTRY_COCOA_EXCEPTION_EVENT) + expect(event.exception?.values?.[0]).toEqual(EXPECTED_SENTRY_COCOA_EXCEPTION); + }); + + it('should not change jvm frames', async () => { + const EXPECTED_SENTRY_JVM_EXCEPTION = { + type: 'java.lang.RuntimeException', + value: 'Java error message.', + stacktrace: { + frames: [ + { + platform: 'java', + module: 'com.example.modules.Crash', + filename: 'Crash.kt', + lineno: 10, + function: 'getDataCrash' + }, + { + platform: 'java', + module: 'com.facebook.jni.NativeRunnable', + filename: 'NativeRunnable.java', + lineno: 2, + function: 'run' + }, + ], + }, + }; + + const SENTRY_JVM_EXCEPTION_EVENT: Event = { + exception: { + values: [ + JSON.parse(JSON.stringify(EXPECTED_SENTRY_JVM_EXCEPTION)), + ], + }, + }; + + const event = createReactNativeRewriteFrames().process(SENTRY_JVM_EXCEPTION_EVENT) + expect(event.exception?.values?.[0]).toEqual(EXPECTED_SENTRY_JVM_EXCEPTION); + }); + it('should parse exceptions for react-native-v8', async () => { const REACT_NATIVE_V8_EXCEPTION = { message: 'Manually triggered crash to test Sentry reporting', From e2b297e6fd4975d5c06df6d6f22709ce1b7583ae Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 19 Jul 2023 17:41:50 +0200 Subject: [PATCH 05/16] Update native linked errors --- src/js/integrations/nativelinkederrors.ts | 208 ++++++++++--------- test/integrations/nativelinkederrors.test.ts | 146 +++++++------ 2 files changed, 192 insertions(+), 162 deletions(-) diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 63310d9b1..a5caa3f45 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -11,6 +11,8 @@ import type { } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; +import { NATIVE } from '../wrapper'; + const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; @@ -33,15 +35,9 @@ export class NativeLinkedErrors implements Integration { */ public name: string = NativeLinkedErrors.id; - /** - * @inheritDoc - */ private readonly _key: LinkedErrorsOptions['key']; - - /** - * @inheritDoc - */ private readonly _limit: LinkedErrorsOptions['limit']; + private _nativePackage: string | null = null; /** * @inheritDoc @@ -63,7 +59,10 @@ export class NativeLinkedErrors implements Integration { return; } - addGlobalEventProcessor((event: Event, hint?: EventHint) => { + addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { + if (this._nativePackage === null) { + this._nativePackage = await this._fetchNativePackage(); + } const self = getCurrentHub().getIntegration(NativeLinkedErrors); return self ? this._handler(client.getOptions().stackParser, self._key, self._limit, event, hint) @@ -72,9 +71,9 @@ export class NativeLinkedErrors implements Integration { } /** - * @inheritDoc + * */ - public _handler( + private _handler( parser: StackParser, key: string, limit: number, @@ -85,14 +84,14 @@ export class NativeLinkedErrors implements Integration { return event; } const linkedErrors = this._walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); - event.exception.values = [...linkedErrors, ...event.exception.values]; + event.exception.values = [...event.exception.values, ...linkedErrors]; return event; } /** - * @inheritDoc + * */ - public _walkErrorTree( + private _walkErrorTree( parser: StackParser, limit: number, error: ExtendedError, @@ -107,105 +106,124 @@ export class NativeLinkedErrors implements Integration { let exception: Exception; if ('stackElements' in linkedError) { // isJavaException - exception = exceptionFromJavaStackElements(linkedError); + exception = this._exceptionFromJavaStackElements(linkedError); } else if ('stackSymbols' in linkedError) { // isObjCException symbolicated by local debug symbols - exception = exceptionFromAppleStackSymbols(linkedError); + exception = this._exceptionFromAppleStackSymbols(linkedError); } else if ('stackReturnAddresses' in linkedError) { // isObjCException - exception = exceptionFromAppleStackReturnAddresses(linkedError); + exception = this._exceptionFromAppleStackReturnAddresses(linkedError); } else if (isInstanceOf(linkedError, Error)) { exception = exceptionFromError(parser, error[key]); } else { return stack; } - return this._walkErrorTree(parser, limit, error[key], key, [exception, ...stack]); + return this._walkErrorTree(parser, limit, error[key], key, [...stack, exception]); } -} -/** - * - */ -export function exceptionFromJavaStackElements( - javaThrowable: { - name: string; - message: string; - stackElements: { - className: string; - fileName: string; - methodName: string; - lineNumber: number; - }[], - }, -): Exception { - return { - type: javaThrowable.name, - value: javaThrowable.message, - stacktrace: { - frames: javaThrowable.stackElements.map(stackElement => ({ - platform: 'java', - module: stackElement.className, - filename: stackElement.fileName, - lineno: stackElement.lineNumber, - function: stackElement.methodName, - })), + /** + * + */ + private _exceptionFromJavaStackElements( + javaThrowable: { + name: string; + message: string; + stackElements: { + className: string; + fileName: string; + methodName: string; + lineNumber: number; + }[], }, - }; -} + ): Exception { + return { + type: javaThrowable.name, + value: javaThrowable.message, + stacktrace: { + frames: javaThrowable.stackElements.map(stackElement => ({ + platform: 'java', + module: stackElement.className, + filename: stackElement.fileName, + lineno: stackElement.lineNumber >= 0 ? stackElement.lineNumber : undefined, + function: stackElement.methodName, + in_app: this._nativePackage !== null && stackElement.className.startsWith(this._nativePackage) + ? true + : undefined, + })).reverse(), + }, + }; + } -/** - * - */ -export function exceptionFromAppleStackSymbols( - objCException: { - name: string; - message: string; - stackSymbols: string[]; - }, -): Exception { - return { - type: objCException.name, - value: objCException.message, - stacktrace: { - frames: objCException.stackSymbols.map(stackSymbol => { - const addrStartIndex = stackSymbol.indexOf(' 0x') + 1; - const addrEndIndex = stackSymbol.indexOf(' ', addrStartIndex); - const pkg = stackSymbol.substring(4, addrStartIndex).trim(); - const addr = stackSymbol.substring(addrStartIndex + 2, addrEndIndex); - const functionEndIndex = stackSymbol.indexOf(' + ', addrEndIndex); - const func = stackSymbol.substring(addrEndIndex + 1, functionEndIndex); - return { - platform: 'cocoa', - package: pkg, - function: func, - instruction_addr: addr, - }; - }), + /** + * + */ + private _exceptionFromAppleStackSymbols( + objCException: { + name: string; + message: string; + stackSymbols: string[]; }, - }; -} + ): Exception { + return { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: objCException.stackSymbols.map(stackSymbol => { + const addrStartIndex = stackSymbol.indexOf(' 0x') + 1; + const addrEndIndex = stackSymbol.indexOf(' ', addrStartIndex); + const pkg = stackSymbol.substring(4, addrStartIndex).trim(); + const addr = stackSymbol.substring(addrStartIndex + 2, addrEndIndex); + const functionEndIndex = stackSymbol.indexOf(' + ', addrEndIndex); + const func = stackSymbol.substring(addrEndIndex + 1, functionEndIndex); + return { + platform: 'cocoa', + package: pkg, + function: func, + instruction_addr: addr, + in_app: this._nativePackage !== null && pkg.startsWith(this._nativePackage) + ? true + : undefined, + }; + }).reverse(), + }, + }; + } -/** - * - */ -export function exceptionFromAppleStackReturnAddresses( - objCException: { - name: string; - message: string; - stackReturnAddresses: number[]; - }, -): Exception { - return { - type: objCException.name, - value: objCException.message, - stacktrace: { - frames: objCException.stackReturnAddresses.map( returnAddress => ({ - platform: 'cocoa', - instruction_addr: returnAddress.toString(16), - })), + /** + * + */ + private _exceptionFromAppleStackReturnAddresses( + objCException: { + name: string; + message: string; + stackReturnAddresses: number[]; }, - }; + ): Exception { + return { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: objCException.stackReturnAddresses.map( returnAddress => ({ + platform: 'cocoa', + instruction_addr: returnAddress.toString(16), + })).reverse(), + }, + }; + } + + /** + * + */ + private async _fetchNativePackage(): Promise { + try { + const release = await NATIVE.fetchNativeRelease(); + return release.id; + } catch (_Oo) { + // Something went wrong, we just continue + } + return null; + } } // TODO: Needs to be exported from @sentry/browser diff --git a/test/integrations/nativelinkederrors.test.ts b/test/integrations/nativelinkederrors.test.ts index 8b3f45c00..7e011d853 100644 --- a/test/integrations/nativelinkederrors.test.ts +++ b/test/integrations/nativelinkederrors.test.ts @@ -1,7 +1,17 @@ import { defaultStackParser } from '@sentry/browser'; -import type { Event,EventHint, ExtendedError, Hub, Integration, IntegrationClass } from '@sentry/types'; +import type { Event,EventHint, ExtendedError, Hub } from '@sentry/types'; import { NativeLinkedErrors } from '../../src/js/integrations/nativelinkederrors'; +import type { NativeReleaseResponse } from '../../src/js/NativeRNSentry'; +import { NATIVE } from '../../src/js/wrapper'; + +jest.mock('../../src/js/wrapper'); + +(NATIVE.fetchNativeRelease as jest.Mock).mockImplementation(() => Promise.resolve({ + id: 'mock.native.bundle.id', + build: 'mock.native.build', + version: 'mock.native.version', +})); describe('NativeLinkedErrors', () => { @@ -99,7 +109,7 @@ describe('NativeLinkedErrors', () => { message: 'Java error message.', stackElements: [ { - className: 'com.example.modules.Crash', + className: 'mock.native.bundle.id.Crash', fileName: 'Crash.kt', lineNumber: 10, methodName: 'getDataCrash' @@ -119,28 +129,6 @@ describe('NativeLinkedErrors', () => { expect(actualEvent).toEqual( > { exception: { values: [ - { - type: 'java.lang.RuntimeException', - value: 'Java error message.', - stacktrace: { - frames: [ - { - platform: 'java', - module: 'com.example.modules.Crash', - filename: 'Crash.kt', - lineno: 10, - function: 'getDataCrash' - }, - { - platform: 'java', - module: 'com.facebook.jni.NativeRunnable', - filename: 'NativeRunnable.java', - lineno: 2, - function: 'run' - }, - ], - } - }, { type: 'Error', value: 'Captured exception', @@ -158,6 +146,29 @@ describe('NativeLinkedErrors', () => { handled: true } }, + { + type: 'java.lang.RuntimeException', + value: 'Java error message.', + stacktrace: { + frames: [ + { + platform: 'java', + module: 'com.facebook.jni.NativeRunnable', + filename: 'NativeRunnable.java', + lineno: 2, + function: 'run', + }, + { + platform: 'java', + module: 'mock.native.bundle.id.Crash', + filename: 'Crash.kt', + lineno: 10, + function: 'getDataCrash', + in_app: true, + }, + ], + } + }, ] }, }); @@ -201,7 +212,7 @@ describe('NativeLinkedErrors', () => { stackSymbols: [ '0 CoreFoundation 0x0000000180437330 __exceptionPreprocess + 172', '1 libobjc.A.dylib 0x0000000180051274 objc_exception_throw + 56', - '2 RNTester 0x0000000103535900 -[RCTSampleTurboModule getObjectThrows:] + 120', + '2 mock.native.bundle.id 0x0000000103535900 -[RCTSampleTurboModule getObjectThrows:] + 120', ] }, }), @@ -211,6 +222,23 @@ describe('NativeLinkedErrors', () => { expect(actualEvent).toEqual(>{ exception: { values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, { type: 'Error', value: 'Objective-c error message.', @@ -218,9 +246,10 @@ describe('NativeLinkedErrors', () => { frames: [ { platform: 'cocoa', - package: 'CoreFoundation', - function: '__exceptionPreprocess', - instruction_addr: '0000000180437330', + package: 'mock.native.bundle.id', + function: '-[RCTSampleTurboModule getObjectThrows:]', + instruction_addr: '0000000103535900', + in_app: true, }, { platform: 'cocoa', @@ -230,30 +259,13 @@ describe('NativeLinkedErrors', () => { }, { platform: 'cocoa', - package: 'RNTester', - function: '-[RCTSampleTurboModule getObjectThrows:]', - instruction_addr: '0000000103535900', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + instruction_addr: '0000000180437330', }, ], } }, - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - }, ] }, }); @@ -307,6 +319,23 @@ describe('NativeLinkedErrors', () => { expect(actualEvent).toEqual( > { exception: { values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ] + }, + mechanism: { + type: 'generic', + handled: true + } + }, { type: 'Error', value: 'Objective-c error message.', @@ -314,7 +343,7 @@ describe('NativeLinkedErrors', () => { frames: [ { platform: 'cocoa', - instruction_addr: '180437330', + instruction_addr: '103535900', }, { platform: 'cocoa', @@ -322,28 +351,11 @@ describe('NativeLinkedErrors', () => { }, { platform: 'cocoa', - instruction_addr: '103535900', + instruction_addr: '180437330', }, ], } }, - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - }, ] }, }); From 30e9cfdec857a16f4f83d373fd3542deb564b25f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 2 Aug 2023 11:33:49 +0200 Subject: [PATCH 06/16] WIP! native linked errors changes --- src/js/integrations/nativelinkederrors.ts | 52 ++++++----------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index a5caa3f45..0710800ac 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -107,9 +107,9 @@ export class NativeLinkedErrors implements Integration { if ('stackElements' in linkedError) { // isJavaException exception = this._exceptionFromJavaStackElements(linkedError); - } else if ('stackSymbols' in linkedError) { - // isObjCException symbolicated by local debug symbols - exception = this._exceptionFromAppleStackSymbols(linkedError); + // } else if ('stackSymbols' in linkedError) { + // // isObjCException symbolicated by local debug symbols + // exception = this._exceptionFromAppleStackSymbols(linkedError); } else if ('stackReturnAddresses' in linkedError) { // isObjCException exception = this._exceptionFromAppleStackReturnAddresses(linkedError); @@ -155,41 +155,6 @@ export class NativeLinkedErrors implements Integration { }; } - /** - * - */ - private _exceptionFromAppleStackSymbols( - objCException: { - name: string; - message: string; - stackSymbols: string[]; - }, - ): Exception { - return { - type: objCException.name, - value: objCException.message, - stacktrace: { - frames: objCException.stackSymbols.map(stackSymbol => { - const addrStartIndex = stackSymbol.indexOf(' 0x') + 1; - const addrEndIndex = stackSymbol.indexOf(' ', addrStartIndex); - const pkg = stackSymbol.substring(4, addrStartIndex).trim(); - const addr = stackSymbol.substring(addrStartIndex + 2, addrEndIndex); - const functionEndIndex = stackSymbol.indexOf(' + ', addrEndIndex); - const func = stackSymbol.substring(addrEndIndex + 1, functionEndIndex); - return { - platform: 'cocoa', - package: pkg, - function: func, - instruction_addr: addr, - in_app: this._nativePackage !== null && pkg.startsWith(this._nativePackage) - ? true - : undefined, - }; - }).reverse(), - }, - }; - } - /** * */ @@ -206,14 +171,14 @@ export class NativeLinkedErrors implements Integration { stacktrace: { frames: objCException.stackReturnAddresses.map( returnAddress => ({ platform: 'cocoa', - instruction_addr: returnAddress.toString(16), + instruction_addr: `0x${returnAddress.toString(16)}`, })).reverse(), }, }; } /** - * + * Fetches the native package/image name from the native layer */ private async _fetchNativePackage(): Promise { try { @@ -224,6 +189,13 @@ export class NativeLinkedErrors implements Integration { } return null; } + + /** + * Gets a Debug Image for a given address via the native layer + */ + private async _getDebugImage(imageAddress: number): Promise<{ + + } | null> { } // TODO: Needs to be exported from @sentry/browser From a3cba558341fcd78c5db0f9df7689d69b52a3e63 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 3 Aug 2023 16:59:24 +0200 Subject: [PATCH 07/16] Add iOS NSException processing and local symbolication --- ios/RNSentry.h | 11 +++ ios/RNSentry.mm | 77 ++++++++++++++++ src/js/NativeRNSentry.ts | 45 ++++++++++ src/js/integrations/nativelinkederrors.ts | 104 ++++++++++++++-------- src/js/wrapper.ts | 30 +++++++ 5 files changed, 229 insertions(+), 38 deletions(-) diff --git a/ios/RNSentry.h b/ios/RNSentry.h index df9be9293..75858a5cf 100644 --- a/ios/RNSentry.h +++ b/ios/RNSentry.h @@ -4,7 +4,18 @@ #import "RCTBridge.h" #endif +#import #import +#import + +@interface SentryDebugImageProvider () +- (NSArray * _Nonnull)getDebugImagesForAddresses:(NSSet * _Nonnull)addresses isCrash:(BOOL)isCrash; +@end + +@interface +SentrySDK (Private) +@property (nonatomic, nullable, readonly, class) SentryOptions *options; +@end @interface RNSentry : NSObject diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 8afe998f4..f0d955bf0 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -1,3 +1,4 @@ +#import #import "RNSentry.h" #if __has_include() @@ -10,6 +11,9 @@ #import #import #import +#import +#import +#import #if __has_include() #define SENTRY_PROFILING_ENABLED 1 @@ -195,6 +199,79 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event resolve(modulesString); } +RCT_EXPORT_METHOD(fetchNativePackageName:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *packageName = [[NSBundle mainBundle] executablePath]; + resolve(packageName); +} + +RCT_EXPORT_METHOD(fetchNativeStackFramesBy: (NSArray *) instructionsAddr + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + BOOL shouldSymbolicateLocally = [SentrySDK.options debug]; + NSString *appPackageName = [[NSBundle mainBundle] executablePath]; + + NSMutableSet * _Nonnull imagesAddrToRetrieveDebugMetaImages = [[NSMutableSet alloc] init]; + NSMutableArray *> * _Nonnull serializedFrames = [[NSMutableArray alloc] init]; + + for (NSNumber *addr in instructionsAddr) { + SentryBinaryImageInfo * _Nullable image = [[SentryBinaryImageCache shared] imageByAddress:[addr unsignedLongLongValue]]; + if (image != nil) { + NSString * imageAddr = sentry_formatHexAddressUInt64([image address]); + [imagesAddrToRetrieveDebugMetaImages addObject: imageAddr]; + + NSDictionary * _Nonnull nativeFrame = @{ + @"platform": @"cocoa", + @"instruction_addr": sentry_formatHexAddress(addr), + @"package": [image name], + @"image_addr": imageAddr, + @"in_app": [NSNumber numberWithBool:[appPackageName isEqualToString:[image name]]], + }; + + if (shouldSymbolicateLocally) { + Dl_info symbolsBuffer; + bool symbols_succeed = false; + symbols_succeed = dladdr((void *) [addr unsignedLongLongValue], &symbolsBuffer) != 0; + if (symbols_succeed) { + NSMutableDictionary * _Nonnull symbolicated = nativeFrame.mutableCopy; + symbolicated[@"symbolAddress"] = sentry_formatHexAddressUInt64((uintptr_t)symbolsBuffer.dli_saddr); + symbolicated[@"function"] = [NSString stringWithCString:symbolsBuffer.dli_sname encoding:NSUTF8StringEncoding]; + + nativeFrame = symbolicated; + } + } + + [serializedFrames addObject:nativeFrame]; + } else { + [serializedFrames addObject: @{ + @"platform": @"cocoa", + @"instruction_addr": addr, + }]; + } + } + + if (shouldSymbolicateLocally) { + resolve(@{ + @"frames": serializedFrames, + }); + } else { + NSMutableArray *> * _Nonnull serializedDebugMetaImages = [[NSMutableArray alloc] init]; + + NSArray *debugMetaImages = [[[SentryDependencyContainer sharedInstance] debugImageProvider] getDebugImagesForAddresses:imagesAddrToRetrieveDebugMetaImages isCrash:false]; + + for (SentryDebugMeta *debugImage in debugMetaImages) { + [serializedDebugMetaImages addObject:[debugImage serialize]]; + } + + resolve(@{ + @"frames": serializedFrames, + @"debugMetaImages": serializedDebugMetaImages, + }); + } +} + RCT_EXPORT_METHOD(fetchNativeDeviceContexts:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index be3c11c12..1cb8bb562 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -33,6 +33,51 @@ export interface Spec extends TurboModule { fetchViewHierarchy(): Promise; startProfiling(): { started?: boolean; error?: string }; stopProfiling(): { profile?: string; error?: string }; + fetchNativePackageName(): Promise; + fetchNativeStackFramesBy(instructionsAddr: number[]): Promise; +} + +export type NativeStackFrame = { + platform: string; + /** + * The instruction address of this frame. + * Formatted as hex with 0x prefix. + */ + instruction_addr: string; + package?: string; + /** + * The debug image address of this frame. + * Formatted as hex with 0x prefix. + */ + image_addr?: string; + in_app?: boolean; + /** + * The symbol name of this frame. + * If symbolicated locally. + */ + function?: string; + /** + * The symbol address of this frame. + * If symbolicated locally. + * Formatted as hex with 0x prefix. + */ + symbol_addr?: string; +}; + +export type NativeDebugImage = { + name: string; + type?: string; + uuid?: string; + debug_id?: string; + image_addr?: string; + image_size?: string; + code_file?: string; + image_vmaddr?: string; +}; + +export type NativeStackFrames = { + frames: NativeStackFrame[]; + debugMetaImages?: NativeDebugImage[]; } export type NativeAppStartResponse = { diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 0710800ac..0a8f7c261 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -1,4 +1,5 @@ import type { + DebugImage, Event, EventHint, EventProcessor, @@ -9,8 +10,9 @@ import type { StackFrame, StackParser, } from '@sentry/types'; -import { isInstanceOf } from '@sentry/utils'; +import { isInstanceOf, isPlainObject } from '@sentry/utils'; +import type { NativeStackFrames } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; const DEFAULT_KEY = 'cause'; @@ -73,57 +75,84 @@ export class NativeLinkedErrors implements Integration { /** * */ - private _handler( + private async _handler( parser: StackParser, key: string, limit: number, event: Event, hint?: EventHint, - ): Event | null { + ): Promise { if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { return event; } - const linkedErrors = this._walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); + const { exceptions: linkedErrors, debugImages } = await this._walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); event.exception.values = [...event.exception.values, ...linkedErrors]; + + event.debug_meta = event.debug_meta || {}; + event.debug_meta.images = event.debug_meta.images || []; + event.debug_meta.images.push(...(debugImages || [])); + return event; } /** * */ - private _walkErrorTree( + private async _walkErrorTree( parser: StackParser, limit: number, error: ExtendedError, key: string, - stack: Exception[] = [], - ): Exception[] { + exceptions: Exception[] = [], + debugImages: DebugImage[] = [], + ): Promise<{ + exceptions: Exception[]; + debugImages?: DebugImage[]; + }> { const linkedError = error[key]; - if (!linkedError || stack.length + 1 >= limit) { - return stack; + if (!linkedError || exceptions.length + 1 >= limit) { + return { + exceptions, + debugImages, + }; } let exception: Exception; + let exceptionDebugImages: DebugImage[] | undefined; if ('stackElements' in linkedError) { // isJavaException exception = this._exceptionFromJavaStackElements(linkedError); - // } else if ('stackSymbols' in linkedError) { - // // isObjCException symbolicated by local debug symbols - // exception = this._exceptionFromAppleStackSymbols(linkedError); } else if ('stackReturnAddresses' in linkedError) { // isObjCException - exception = this._exceptionFromAppleStackReturnAddresses(linkedError); + const { appleException, appleDebugImages } = await this._exceptionFromAppleStackReturnAddresses(linkedError); + exception = appleException; + exceptionDebugImages = appleDebugImages; } else if (isInstanceOf(linkedError, Error)) { exception = exceptionFromError(parser, error[key]); + } else if (isPlainObject(linkedError)) { + exception = { + type: typeof linkedError.name === 'string' ? linkedError.name : undefined, + value: typeof linkedError.message === 'string' ? linkedError.message : undefined, + }; } else { - return stack; + return { + exceptions, + debugImages, + }; } - return this._walkErrorTree(parser, limit, error[key], key, [...stack, exception]); + return this._walkErrorTree( + parser, + limit, + error[key], + key, + [...exceptions, exception], + [...debugImages, ...(exceptionDebugImages || [])], + ); } /** - * + * Converts a Java Throwable to an SentryException */ private _exceptionFromJavaStackElements( javaThrowable: { @@ -156,46 +185,45 @@ export class NativeLinkedErrors implements Integration { } /** - * + * Converts StackAddresses to a SentryException with DebugMetaImages */ - private _exceptionFromAppleStackReturnAddresses( + private async _exceptionFromAppleStackReturnAddresses( objCException: { name: string; message: string; stackReturnAddresses: number[]; }, - ): Exception { + ): Promise<{ + appleException: Exception; + appleDebugImages: DebugImage[]; + }> { + const nativeStackFrames = await this._fetchNativeStackFrames(objCException.stackReturnAddresses); + return { - type: objCException.name, - value: objCException.message, - stacktrace: { - frames: objCException.stackReturnAddresses.map( returnAddress => ({ - platform: 'cocoa', - instruction_addr: `0x${returnAddress.toString(16)}`, - })).reverse(), + appleException: { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: (nativeStackFrames && nativeStackFrames.frames.reverse()) || [], + }, }, + appleDebugImages: (nativeStackFrames && nativeStackFrames.debugMetaImages as DebugImage[]) || [], }; } /** * Fetches the native package/image name from the native layer */ - private async _fetchNativePackage(): Promise { - try { - const release = await NATIVE.fetchNativeRelease(); - return release.id; - } catch (_Oo) { - // Something went wrong, we just continue - } - return null; + private _fetchNativePackage(): Promise { + return NATIVE.fetchNativePackageName(); } /** - * Gets a Debug Image for a given address via the native layer + * Fetches native debug image information on iOS */ - private async _getDebugImage(imageAddress: number): Promise<{ - - } | null> { + private _fetchNativeStackFrames(instructionsAddr: number[]): Promise { + return NATIVE.fetchNativeStackFramesBy(instructionsAddr); + } } // TODO: Needs to be exported from @sentry/browser diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 3677a0f6f..abfe2dea9 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -19,6 +19,7 @@ import type { NativeFramesResponse, NativeReleaseResponse, NativeScreenshot, + NativeStackFrames, Spec, } from './NativeRNSentry'; import type { ReactNativeClientOptions } from './options'; @@ -82,6 +83,13 @@ interface SentryNativeWrapper { startProfiling(): boolean; stopProfiling(): Hermes.Profile | null; + + fetchNativePackageName(): Promise; + + /** + * Fetches native stack frames and debug images for the instructions addresses. + */ + fetchNativeStackFramesBy(instructionsAddr: number[]): Promise; } /** @@ -531,6 +539,28 @@ export const NATIVE: SentryNativeWrapper = { } }, + async fetchNativePackageName(): Promise { + if (!this.enableNative) { + return null; + } + if (!this._isModuleLoaded(RNSentry)) { + return null; + } + + return await RNSentry.fetchNativePackageName() || null; + }, + + async fetchNativeStackFramesBy(instructionsAddr: number[]): Promise { + if (!this.enableNative) { + return null; + } + if (!this._isModuleLoaded(RNSentry)) { + return null; + } + + return await RNSentry.fetchNativeStackFramesBy(instructionsAddr) || null; + }, + /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. From e60ee545043e5de4fd26a7c4f27dfa20f924694d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 4 Aug 2023 22:59:43 +0200 Subject: [PATCH 08/16] Fix java impl, fix js tests --- .../io/sentry/react/RNSentryModuleImpl.java | 9 + .../java/io/sentry/react/RNSentryModule.java | 10 + .../java/io/sentry/react/RNSentryModule.java | 10 + src/js/NativeRNSentry.ts | 6 +- src/js/integrations/nativelinkederrors.ts | 78 ++-- src/js/wrapper.ts | 4 +- test/integrations/nativelinkederrors.test.ts | 406 ++++++++---------- test/integrations/rewriteframes.test.ts | 18 +- 8 files changed, 270 insertions(+), 271 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 845fc76ec..6633f07fe 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -704,6 +704,15 @@ public void fetchNativeSdkInfo(Promise promise) { } } + public void fetchNativePackageName(Promise promise) { + promise.resolve(packageInfo.packageName); + } + + public void fetchNativeStackFramesBy(Promise promise) { + logger.log(SentryLevel.ERROR, "This method works only on iOS."); + promise.resolve(null); + } + private void setEventOriginTag(SentryEvent event) { SdkVersion sdk = event.getSdk(); if (sdk != null) { diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 0310f537a..4c0bfba5c 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -133,4 +133,14 @@ public WritableMap startProfiling() { public WritableMap stopProfiling() { return this.impl.stopProfiling(); } + + @Override + public void fetchNativePackageName(Promise promise) { + this.impl.fetchNativePackageName(promise); + } + + @Override + public void fetchNativeStackFramesBy(Promise promise) { + this.impl.fetchNativeStackFramesBy(promise); + } } diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 3f0d70863..8ee9effb2 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -132,4 +132,14 @@ public WritableMap startProfiling() { public WritableMap stopProfiling() { return this.impl.stopProfiling(); } + + @ReactMethod + public void fetchNativePackageName(Promise promise) { + this.impl.fetchNativePackageName(promise); + } + + @ReactMethod + public void fetchNativeStackFramesBy(Promise promise) { + this.impl.fetchNativeStackFramesBy(promise); + } } diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index 1cb8bb562..582c32fdf 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -65,12 +65,12 @@ export type NativeStackFrame = { }; export type NativeDebugImage = { - name: string; + name?: string; type?: string; uuid?: string; debug_id?: string; image_addr?: string; - image_size?: string; + image_size?: number; code_file?: string; image_vmaddr?: string; }; @@ -78,7 +78,7 @@ export type NativeDebugImage = { export type NativeStackFrames = { frames: NativeStackFrame[]; debugMetaImages?: NativeDebugImage[]; -} +}; export type NativeAppStartResponse = { isColdStart: boolean; diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 0a8f7c261..8c5a90379 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -52,10 +52,7 @@ export class NativeLinkedErrors implements Integration { /** * @inheritDoc */ - public setupOnce( - addGlobalEventProcessor: (callback: EventProcessor) => void, - getCurrentHub: () => Hub, - ): void { + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { const client = getCurrentHub().getClient(); if (!client) { return; @@ -66,9 +63,7 @@ export class NativeLinkedErrors implements Integration { this._nativePackage = await this._fetchNativePackage(); } const self = getCurrentHub().getIntegration(NativeLinkedErrors); - return self - ? this._handler(client.getOptions().stackParser, self._key, self._limit, event, hint) - : event; + return self ? this._handler(client.getOptions().stackParser, self._key, self._limit, event, hint) : event; }); } @@ -85,7 +80,12 @@ export class NativeLinkedErrors implements Integration { if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { return event; } - const { exceptions: linkedErrors, debugImages } = await this._walkErrorTree(parser, limit, hint.originalException as ExtendedError, key); + const { exceptions: linkedErrors, debugImages } = await this._walkErrorTree( + parser, + limit, + hint.originalException as ExtendedError, + key, + ); event.exception.values = [...event.exception.values, ...linkedErrors]; event.debug_meta = event.debug_meta || {}; @@ -154,32 +154,36 @@ export class NativeLinkedErrors implements Integration { /** * Converts a Java Throwable to an SentryException */ - private _exceptionFromJavaStackElements( - javaThrowable: { - name: string; - message: string; - stackElements: { - className: string; - fileName: string; - methodName: string; - lineNumber: number; - }[], - }, - ): Exception { + private _exceptionFromJavaStackElements(javaThrowable: { + name: string; + message: string; + stackElements: { + className: string; + fileName: string; + methodName: string; + lineNumber: number; + }[]; + }): Exception { return { type: javaThrowable.name, value: javaThrowable.message, stacktrace: { - frames: javaThrowable.stackElements.map(stackElement => ({ - platform: 'java', - module: stackElement.className, - filename: stackElement.fileName, - lineno: stackElement.lineNumber >= 0 ? stackElement.lineNumber : undefined, - function: stackElement.methodName, - in_app: this._nativePackage !== null && stackElement.className.startsWith(this._nativePackage) - ? true - : undefined, - })).reverse(), + frames: javaThrowable.stackElements + .map( + stackElement => + { + platform: 'java', + module: stackElement.className, + filename: stackElement.fileName, + lineno: stackElement.lineNumber >= 0 ? stackElement.lineNumber : undefined, + function: stackElement.methodName, + in_app: + this._nativePackage !== null && stackElement.className.startsWith(this._nativePackage) + ? true + : undefined, + }, + ) + .reverse(), }, }; } @@ -187,13 +191,11 @@ export class NativeLinkedErrors implements Integration { /** * Converts StackAddresses to a SentryException with DebugMetaImages */ - private async _exceptionFromAppleStackReturnAddresses( - objCException: { - name: string; - message: string; - stackReturnAddresses: number[]; - }, - ): Promise<{ + private async _exceptionFromAppleStackReturnAddresses(objCException: { + name: string; + message: string; + stackReturnAddresses: number[]; + }): Promise<{ appleException: Exception; appleDebugImages: DebugImage[]; }> { @@ -207,7 +209,7 @@ export class NativeLinkedErrors implements Integration { frames: (nativeStackFrames && nativeStackFrames.frames.reverse()) || [], }, }, - appleDebugImages: (nativeStackFrames && nativeStackFrames.debugMetaImages as DebugImage[]) || [], + appleDebugImages: (nativeStackFrames && (nativeStackFrames.debugMetaImages as DebugImage[])) || [], }; } diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index abfe2dea9..7e875eee0 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -547,7 +547,7 @@ export const NATIVE: SentryNativeWrapper = { return null; } - return await RNSentry.fetchNativePackageName() || null; + return (await RNSentry.fetchNativePackageName()) || null; }, async fetchNativeStackFramesBy(instructionsAddr: number[]): Promise { @@ -558,7 +558,7 @@ export const NATIVE: SentryNativeWrapper = { return null; } - return await RNSentry.fetchNativeStackFramesBy(instructionsAddr) || null; + return (await RNSentry.fetchNativeStackFramesBy(instructionsAddr)) || null; }, /** diff --git a/test/integrations/nativelinkederrors.test.ts b/test/integrations/nativelinkederrors.test.ts index 7e011d853..b27258fe3 100644 --- a/test/integrations/nativelinkederrors.test.ts +++ b/test/integrations/nativelinkederrors.test.ts @@ -1,19 +1,20 @@ import { defaultStackParser } from '@sentry/browser'; -import type { Event,EventHint, ExtendedError, Hub } from '@sentry/types'; +import type { DebugImage, Event, EventHint, ExtendedError, Hub } from '@sentry/types'; import { NativeLinkedErrors } from '../../src/js/integrations/nativelinkederrors'; -import type { NativeReleaseResponse } from '../../src/js/NativeRNSentry'; +import type { NativeStackFrames } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; jest.mock('../../src/js/wrapper'); -(NATIVE.fetchNativeRelease as jest.Mock).mockImplementation(() => Promise.resolve({ - id: 'mock.native.bundle.id', - build: 'mock.native.build', - version: 'mock.native.version', -})); +(NATIVE.fetchNativePackageName as jest.Mock).mockImplementation(() => Promise.resolve('mock.native.bundle.id')); + +(NATIVE.fetchNativeStackFramesBy as jest.Mock).mockImplementation(() => Promise.resolve(null)); describe('NativeLinkedErrors', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('keeps event without cause as is', async () => { const actualEvent = await executeIntegrationFor( @@ -30,16 +31,16 @@ describe('NativeLinkedErrors', () => { filename: 'app:///Pressability.js', function: '_performTransitionSideEffects', in_app: false, - platform: 'node' + platform: 'node', }, - ] + ], }, mechanism: { type: 'generic', - handled: true - } - } - ] + handled: true, + }, + }, + ], }, }, {}, @@ -58,16 +59,16 @@ describe('NativeLinkedErrors', () => { filename: 'app:///Pressability.js', function: '_performTransitionSideEffects', in_app: false, - platform: 'node' + platform: 'node', }, - ] + ], }, mechanism: { type: 'generic', - handled: true - } - } - ] + handled: true, + }, + }, + ], }, }); }); @@ -87,21 +88,22 @@ describe('NativeLinkedErrors', () => { filename: 'app:///Pressability.js', function: '_performTransitionSideEffects', }, - ] + ], }, mechanism: { type: 'generic', - handled: true - } - } - ] + handled: true, + }, + }, + ], }, }, { originalException: createNewError({ message: 'JavaScript error message', name: 'JavaScriptError', - stack: 'JavaScriptError: JavaScript error message\n' + + stack: + 'JavaScriptError: JavaScript error message\n' + 'at onPress (index.bundle:75:33)\n' + 'at _performTransitionSideEffects (index.bundle:65919:22)', cause: { @@ -112,69 +114,112 @@ describe('NativeLinkedErrors', () => { className: 'mock.native.bundle.id.Crash', fileName: 'Crash.kt', lineNumber: 10, - methodName: 'getDataCrash' + methodName: 'getDataCrash', }, { className: 'com.facebook.jni.NativeRunnable', fileName: 'NativeRunnable.java', lineNumber: 2, - methodName: 'run' - } - ] + methodName: 'run', + }, + ], }, }), }, ); - expect(actualEvent).toEqual( > { - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] + expect(NATIVE.fetchNativePackageName).toBeCalledTimes(1); + expect(NATIVE.fetchNativeStackFramesBy).not.toBeCalled(); + expect(actualEvent).toEqual( + expect.objectContaining(>{ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, }, - mechanism: { - type: 'generic', - handled: true - } + { + type: 'java.lang.RuntimeException', + value: 'Java error message.', + stacktrace: { + frames: [ + expect.objectContaining({ + platform: 'java', + module: 'com.facebook.jni.NativeRunnable', + filename: 'NativeRunnable.java', + lineno: 2, + function: 'run', + }), + expect.objectContaining({ + platform: 'java', + module: 'mock.native.bundle.id.Crash', + filename: 'Crash.kt', + lineno: 10, + function: 'getDataCrash', + in_app: true, + }), + ], + }, + }, + ], + }, + }), + ); + }); + + it('adds ios objective-c cause from the original error to the event', async () => { + (NATIVE.fetchNativeStackFramesBy as jest.Mock).mockImplementation(() => + Promise.resolve({ + frames: [ + // Locally symbolicated frame + { + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + symbol_addr: '0x0000000180437330', + instruction_addr: '0x0000000180437330', + image_addr: '0x7fffe668e000', }, { - type: 'java.lang.RuntimeException', - value: 'Java error message.', - stacktrace: { - frames: [ - { - platform: 'java', - module: 'com.facebook.jni.NativeRunnable', - filename: 'NativeRunnable.java', - lineno: 2, - function: 'run', - }, - { - platform: 'java', - module: 'mock.native.bundle.id.Crash', - filename: 'Crash.kt', - lineno: 10, - function: 'getDataCrash', - in_app: true, - }, - ], - } + platform: 'cocoa', + function: 'objc_exception_throw', + instruction_addr: '0x0000000180051274', + image_addr: '0x7fffe668e000', }, - ] - }, - }); - }); + { + platform: 'cocoa', + package: 'mock.native.bundle.id', + instruction_addr: '0x0000000103535900', + image_addr: '0x7fffe668e000', + in_app: true, + }, + ], + debugMetaImages: [ + { + type: 'macho', + debug_id: '84a04d24-0e60-3810-a8c0-90a65e2df61a', + code_file: '/usr/lib/libDiagnosticMessagesClient.dylib', + image_addr: '0x7fffe668e000', + image_size: 8192, + image_vmaddr: '0x40000', + }, + ], + }), + ); - it('adds ios objective-c cause from the original error to the event', async () => { const actualEvent = await executeIntegrationFor( { exception: { @@ -189,21 +234,22 @@ describe('NativeLinkedErrors', () => { filename: 'app:///Pressability.js', function: '_performTransitionSideEffects', }, - ] + ], }, mechanism: { type: 'generic', - handled: true - } - } - ] + handled: true, + }, + }, + ], }, }, { originalException: createNewError({ message: 'JavaScript error message', name: 'JavaScriptError', - stack: 'JavaScriptError: JavaScript error message\n' + + stack: + 'JavaScriptError: JavaScript error message\n' + 'at onPress (index.bundle:75:33)\n' + 'at _performTransitionSideEffects (index.bundle:65919:22)', cause: { @@ -213,67 +259,30 @@ describe('NativeLinkedErrors', () => { '0 CoreFoundation 0x0000000180437330 __exceptionPreprocess + 172', '1 libobjc.A.dylib 0x0000000180051274 objc_exception_throw + 56', '2 mock.native.bundle.id 0x0000000103535900 -[RCTSampleTurboModule getObjectThrows:] + 120', - ] + ], + stackReturnAddresses: [6446871344, 6442783348, 4350761216], }, }), }, ); - expect(actualEvent).toEqual(>{ - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - }, - { - type: 'Error', - value: 'Objective-c error message.', - stacktrace: { - frames: [ - { - platform: 'cocoa', - package: 'mock.native.bundle.id', - function: '-[RCTSampleTurboModule getObjectThrows:]', - instruction_addr: '0000000103535900', - in_app: true, - }, - { - platform: 'cocoa', - package: 'libobjc.A.dylib', - function: 'objc_exception_throw', - instruction_addr: '0000000180051274', - }, - { - platform: 'cocoa', - package: 'CoreFoundation', - function: '__exceptionPreprocess', - instruction_addr: '0000000180437330', - }, - ], - } - }, - ] - }, - }); - }); - - it('adds ios objective-c cause from the original error to the event', async () => { - const actualEvent = await executeIntegrationFor( - { + expect(NATIVE.fetchNativePackageName).toBeCalledTimes(1); + expect(NATIVE.fetchNativeStackFramesBy).toBeCalledTimes(1); + expect(NATIVE.fetchNativeStackFramesBy).toBeCalledWith([6446871344, 6442783348, 4350761216]); + expect(actualEvent).toEqual( + expect.objectContaining(>{ + debug_meta: { + images: [ + { + type: 'macho', + debug_id: '84a04d24-0e60-3810-a8c0-90a65e2df61a', + code_file: '/usr/lib/libDiagnosticMessagesClient.dylib', + image_addr: '0x7fffe668e000', + image_size: 8192, + image_vmaddr: '0x40000', + } as unknown as DebugImage, + ], + }, exception: { values: [ { @@ -286,79 +295,46 @@ describe('NativeLinkedErrors', () => { filename: 'app:///Pressability.js', function: '_performTransitionSideEffects', }, - ] + ], }, mechanism: { type: 'generic', - handled: true - } - } - ] + handled: true, + }, + }, + { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + expect.objectContaining({ + platform: 'cocoa', + package: 'mock.native.bundle.id', + instruction_addr: '0x0000000103535900', + image_addr: '0x7fffe668e000', + in_app: true, + }), + expect.objectContaining({ + platform: 'cocoa', + function: 'objc_exception_throw', + instruction_addr: '0x0000000180051274', + image_addr: '0x7fffe668e000', + }), + expect.objectContaining({ + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + symbol_addr: '0x0000000180437330', + instruction_addr: '0x0000000180437330', + image_addr: '0x7fffe668e000', + }), + ], + }, + }, + ], }, - }, - { - originalException: createNewError({ - message: 'JavaScript error message', - name: 'JavaScriptError', - stack: 'JavaScriptError: JavaScript error message\n' + - 'at onPress (index.bundle:75:33)\n' + - 'at _performTransitionSideEffects (index.bundle:65919:22)', - cause: { - name: 'Error', - message: 'Objective-c error message.', - stackReturnAddresses: [ - 6446871344, - 6442783348, - 4350761216, - ], - }, - }), - }, + }), ); - - expect(actualEvent).toEqual( > { - exception: { - values: [ - { - type: 'Error', - value: 'Captured exception', - stacktrace: { - frames: [ - { - colno: 17, - filename: 'app:///Pressability.js', - function: '_performTransitionSideEffects', - }, - ] - }, - mechanism: { - type: 'generic', - handled: true - } - }, - { - type: 'Error', - value: 'Objective-c error message.', - stacktrace: { - frames: [ - { - platform: 'cocoa', - instruction_addr: '103535900', - }, - { - platform: 'cocoa', - instruction_addr: '180051274', - }, - { - platform: 'cocoa', - instruction_addr: '180437330', - }, - ], - } - }, - ] - }, - }); }); }); @@ -374,24 +350,20 @@ function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Promi reject(e); } }, - () => ({ - getClient: () => ({ - getOptions: () => ({ - stackParser: defaultStackParser, - }) - }), - getIntegration: () => integration, - } as unknown as Hub), + () => + ({ + getClient: () => ({ + getOptions: () => ({ + stackParser: defaultStackParser, + }), + }), + getIntegration: () => integration, + } as unknown as Hub), ); }); } -function createNewError(from: { - message: string; - name?: string; - stack?: string; - cause?: unknown; -}): ExtendedError { +function createNewError(from: { message: string; name?: string; stack?: string; cause?: unknown }): ExtendedError { const error: ExtendedError = new Error(from.message); if (from.name) { error.name = from.name; diff --git a/test/integrations/rewriteframes.test.ts b/test/integrations/rewriteframes.test.ts index a357fb081..58867ef34 100644 --- a/test/integrations/rewriteframes.test.ts +++ b/test/integrations/rewriteframes.test.ts @@ -57,18 +57,16 @@ describe('RewriteFrames', () => { instruction_addr: '0000000103535900', }, ], - } + }, }; const SENTRY_COCOA_EXCEPTION_EVENT: Event = { exception: { - values: [ - JSON.parse(JSON.stringify(EXPECTED_SENTRY_COCOA_EXCEPTION)), - ], + values: [JSON.parse(JSON.stringify(EXPECTED_SENTRY_COCOA_EXCEPTION))], }, }; - const event = createReactNativeRewriteFrames().process(SENTRY_COCOA_EXCEPTION_EVENT) + const event = createReactNativeRewriteFrames().process(SENTRY_COCOA_EXCEPTION_EVENT); expect(event.exception?.values?.[0]).toEqual(EXPECTED_SENTRY_COCOA_EXCEPTION); }); @@ -83,14 +81,14 @@ describe('RewriteFrames', () => { module: 'com.example.modules.Crash', filename: 'Crash.kt', lineno: 10, - function: 'getDataCrash' + function: 'getDataCrash', }, { platform: 'java', module: 'com.facebook.jni.NativeRunnable', filename: 'NativeRunnable.java', lineno: 2, - function: 'run' + function: 'run', }, ], }, @@ -98,13 +96,11 @@ describe('RewriteFrames', () => { const SENTRY_JVM_EXCEPTION_EVENT: Event = { exception: { - values: [ - JSON.parse(JSON.stringify(EXPECTED_SENTRY_JVM_EXCEPTION)), - ], + values: [JSON.parse(JSON.stringify(EXPECTED_SENTRY_JVM_EXCEPTION))], }, }; - const event = createReactNativeRewriteFrames().process(SENTRY_JVM_EXCEPTION_EVENT) + const event = createReactNativeRewriteFrames().process(SENTRY_JVM_EXCEPTION_EVENT); expect(event.exception?.values?.[0]).toEqual(EXPECTED_SENTRY_JVM_EXCEPTION); }); From 66ee69e32011f02273e395cbd2bc22b5dd05c416 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 7 Aug 2023 12:47:21 +0200 Subject: [PATCH 09/16] Add native ios tests --- .../RNSentry+initNativeSdk.h | 25 ++++ .../RNSentry+initNativeSdk.mm | 125 +++++++++++++++++- ios/RNSentry.h | 9 +- ios/RNSentry.mm | 37 +++--- 4 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h new file mode 100644 index 000000000..2690a7d73 --- /dev/null +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h @@ -0,0 +1,25 @@ +#import +#import + +@interface +SentrySDK (PrivateTests) +- (nullable SentryOptions *) options; +@end + +@interface SentryBinaryImageInfo : NSObject +@property (nonatomic, strong) NSString *name; +@property (nonatomic) uint64_t address; +@property (nonatomic) uint64_t size; +@end + +@interface SentryBinaryImageCache : NSObject +@property (nonatomic, readonly, class) SentryBinaryImageCache *shared; +- (void)start; +- (void)stop; +- (nullable SentryBinaryImageInfo *)imageByAddress:(const uint64_t)address; +@end + +@interface SentryDependencyContainer : NSObject ++ (instancetype)sharedInstance; +@property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider; +@end diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm index a6acb6429..01d5a5200 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm @@ -1,8 +1,8 @@ +#import "RNSentry+initNativeSdk.h" +#import #import #import -#import -#import -#import "RNSentry.h" +#import @interface RNSentryInitNativeSdkTests : XCTestCase @@ -168,4 +168,123 @@ - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); } +void (^expectRejecterNotCalled)(NSString*, NSString*, NSError*) = ^(NSString *code, NSString *message, NSError *error) { + @throw [NSException exceptionWithName:@"Promise Rejector should not be called." reason:nil userInfo:nil]; +}; + +uint64_t MOCKED_SYMBOL_ADDRESS = 123; +char const* MOCKED_SYMBOL_NAME = "symbolicatedname"; + +int sucessfulSymbolicate(const void *, Dl_info *info){ + info->dli_saddr = (void *) MOCKED_SYMBOL_ADDRESS; + info->dli_sname = MOCKED_SYMBOL_NAME; + return 1; +} + +- (void)prepareNativeFrameMocksWithLocalSymbolication: (BOOL) debug +{ + SentryOptions* sentryOptions = [[SentryOptions alloc] init]; + sentryOptions.debug = debug; //no local symbolication + + id sentrySDKMock = OCMClassMock([SentrySDK class]); + OCMStub([(SentrySDK*) sentrySDKMock options]).andReturn(sentryOptions); + + id sentryBinaryImageInfoMockOne = OCMClassMock([SentryBinaryImageInfo class]); + OCMStub([(SentryBinaryImageInfo*) sentryBinaryImageInfoMockOne address]).andReturn([@112233 unsignedLongLongValue]); + OCMStub([sentryBinaryImageInfoMockOne name]).andReturn(@"testnameone"); + + id sentryBinaryImageInfoMockTwo = OCMClassMock([SentryBinaryImageInfo class]); + OCMStub([(SentryBinaryImageInfo*) sentryBinaryImageInfoMockTwo address]).andReturn([@112233 unsignedLongLongValue]); + OCMStub([sentryBinaryImageInfoMockTwo name]).andReturn(@"testnametwo"); + + id sentryBinaryImageCacheMock = OCMClassMock([SentryBinaryImageCache class]); + OCMStub(ClassMethod([sentryBinaryImageCacheMock shared])).andReturn(sentryBinaryImageCacheMock); + OCMStub([sentryBinaryImageCacheMock imageByAddress:[@123 unsignedLongLongValue]]).andReturn(sentryBinaryImageInfoMockOne); + OCMStub([sentryBinaryImageCacheMock imageByAddress:[@456 unsignedLongLongValue]]).andReturn(sentryBinaryImageInfoMockTwo); + + NSDictionary* serializedDebugImage = @{ + @"uuid": @"mockuuid", + @"debug_id": @"mockdebugid", + @"type": @"macho", + @"image_addr": @"0x000000000001b669", + }; + id sentryDebugImageMock = OCMClassMock([SentryDebugMeta class]); + OCMStub([sentryDebugImageMock serialize]).andReturn(serializedDebugImage); + + id sentryDebugImageProviderMock = OCMClassMock([SentryDebugImageProvider class]); + OCMStub([sentryDebugImageProviderMock getDebugImagesForAddresses:[NSSet setWithObject:@"0x000000000001b669"] isCrash:false]).andReturn(@[sentryDebugImageMock]); + + id sentryDependencyContainerMock = OCMClassMock([SentryDependencyContainer class]); + OCMStub(ClassMethod([sentryDependencyContainerMock sharedInstance])).andReturn(sentryDependencyContainerMock); + OCMStub([sentryDependencyContainerMock debugImageProvider]).andReturn(sentryDebugImageProviderMock); +} + +- (void)testFetchNativeStackFramesByInstructionsServerSymbolication +{ + [self prepareNativeFrameMocksWithLocalSymbolication:NO]; + RNSentry* rnSentry = [[RNSentry alloc] init]; + NSDictionary* actual = [rnSentry fetchNativeStackFramesBy: @[@123, @456] + symbolicate: sucessfulSymbolicate]; + + NSDictionary* expected = @{ + @"debugMetaImages": @[ + @{ + @"uuid": @"mockuuid", + @"debug_id": @"mockdebugid", + @"type": @"macho", + @"image_addr": @"0x000000000001b669", + }, + ], + @"frames": @[ + @{ + @"package": @"testnameone", + @"in_app": @NO, + @"platform": @"cocoa", + @"instruction_addr": @"0x000000000000007b", //123 + @"image_addr": @"0x000000000001b669", //112233 + }, + @{ + @"package": @"testnametwo", + @"in_app": @NO, + @"platform": @"cocoa", + @"instruction_addr": @"0x00000000000001c8", //456 + @"image_addr": @"0x000000000001b669", //445566 + }, + ], + }; + XCTAssertTrue([actual isEqualToDictionary:expected]); +} + +- (void)testFetchNativeStackFramesByInstructionsOnDeviceSymbolication +{ + [self prepareNativeFrameMocksWithLocalSymbolication:YES]; + RNSentry* rnSentry = [[RNSentry alloc] init]; + NSDictionary* actual = [rnSentry fetchNativeStackFramesBy: @[@123, @456] + symbolicate: sucessfulSymbolicate]; + + NSDictionary* expected = @{ + @"frames": @[ + @{ + @"function": @"symbolicatedname", + @"package": @"testnameone", + @"in_app": @NO, + @"platform": @"cocoa", + @"symbol_addr": @"0x000000000000007b", //123 + @"instruction_addr": @"0x000000000000007b", //123 + @"image_addr": @"0x000000000001b669", //112233 + }, + @{ + @"function": @"symbolicatedname", + @"package": @"testnametwo", + @"in_app": @NO, + @"platform": @"cocoa", + @"symbol_addr": @"0x000000000000007b", //123 + @"instruction_addr": @"0x00000000000001c8", //456 + @"image_addr": @"0x000000000001b669", //445566 + }, + ], + }; + XCTAssertTrue([actual isEqualToDictionary:expected]); +} + @end diff --git a/ios/RNSentry.h b/ios/RNSentry.h index 75858a5cf..5a125edbd 100644 --- a/ios/RNSentry.h +++ b/ios/RNSentry.h @@ -4,10 +4,14 @@ #import "RCTBridge.h" #endif +#import + #import #import #import +typedef int (*SymbolicateCallbackType)(const void *, Dl_info *); + @interface SentryDebugImageProvider () - (NSArray * _Nonnull)getDebugImagesForAddresses:(NSSet * _Nonnull)addresses isCrash:(BOOL)isCrash; @end @@ -22,6 +26,9 @@ SentrySDK (Private) - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nullable*_Nonnull)errorPointer; -- (void)setEventOriginTag:(SentryEvent *)event; +- (void) setEventOriginTag: (SentryEvent*) event; + +- (NSDictionary*_Nonnull) fetchNativeStackFramesBy: (NSArray*)instructionsAddr + symbolicate: (SymbolicateCallbackType) symbolicate; @end diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index f0d955bf0..5b361e26d 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -206,9 +206,8 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event resolve(packageName); } -RCT_EXPORT_METHOD(fetchNativeStackFramesBy: (NSArray *) instructionsAddr - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) +- (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAddr + symbolicate: (SymbolicateCallbackType) symbolicate { BOOL shouldSymbolicateLocally = [SentrySDK.options debug]; NSString *appPackageName = [[NSBundle mainBundle] executablePath]; @@ -221,7 +220,7 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event if (image != nil) { NSString * imageAddr = sentry_formatHexAddressUInt64([image address]); [imagesAddrToRetrieveDebugMetaImages addObject: imageAddr]; - + NSDictionary * _Nonnull nativeFrame = @{ @"platform": @"cocoa", @"instruction_addr": sentry_formatHexAddress(addr), @@ -229,14 +228,14 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event @"image_addr": imageAddr, @"in_app": [NSNumber numberWithBool:[appPackageName isEqualToString:[image name]]], }; - + if (shouldSymbolicateLocally) { Dl_info symbolsBuffer; bool symbols_succeed = false; - symbols_succeed = dladdr((void *) [addr unsignedLongLongValue], &symbolsBuffer) != 0; + symbols_succeed = symbolicate((void *) [addr unsignedLongLongValue], &symbolsBuffer) != 0; if (symbols_succeed) { NSMutableDictionary * _Nonnull symbolicated = nativeFrame.mutableCopy; - symbolicated[@"symbolAddress"] = sentry_formatHexAddressUInt64((uintptr_t)symbolsBuffer.dli_saddr); + symbolicated[@"symbol_addr"] = sentry_formatHexAddressUInt64((uintptr_t)symbolsBuffer.dli_saddr); symbolicated[@"function"] = [NSString stringWithCString:symbolsBuffer.dli_sname encoding:NSUTF8StringEncoding]; nativeFrame = symbolicated; @@ -247,31 +246,39 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event } else { [serializedFrames addObject: @{ @"platform": @"cocoa", - @"instruction_addr": addr, + @"instruction_addr": sentry_formatHexAddress(addr), }]; } } - + if (shouldSymbolicateLocally) { - resolve(@{ + return @{ @"frames": serializedFrames, - }); + }; } else { NSMutableArray *> * _Nonnull serializedDebugMetaImages = [[NSMutableArray alloc] init]; NSArray *debugMetaImages = [[[SentryDependencyContainer sharedInstance] debugImageProvider] getDebugImagesForAddresses:imagesAddrToRetrieveDebugMetaImages isCrash:false]; - + for (SentryDebugMeta *debugImage in debugMetaImages) { [serializedDebugMetaImages addObject:[debugImage serialize]]; } - - resolve(@{ + + return @{ @"frames": serializedFrames, @"debugMetaImages": serializedDebugMetaImages, - }); + }; } } +RCT_EXPORT_METHOD(fetchNativeStackFramesBy:(NSArray *)instructionsAddr + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + resolve([self fetchNativeStackFramesBy:instructionsAddr + symbolicate:dladdr]); +} + RCT_EXPORT_METHOD(fetchNativeDeviceContexts:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { From 87bfdfd85ae710602489406e02652027423b59f4 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 7 Aug 2023 12:53:33 +0200 Subject: [PATCH 10/16] Update eslint rules --- .eslintrc.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 55a5c70e3..d829b5933 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,11 +12,7 @@ module.exports = { settings: { version: 'detect', // React version. "detect" automatically picks the version you have installed. }, - ignorePatterns: [ - 'test/react-native/versions/**/*', - 'coverage/**/*', - 'test/typescript/**/*', - ], + ignorePatterns: ['test/react-native/versions/**/*', 'coverage/**/*', 'test/typescript/**/*'], overrides: [ { // Typescript Files @@ -24,10 +20,7 @@ module.exports = { extends: ['plugin:react/recommended'], plugins: ['react', 'react-native'], rules: { - '@typescript-eslint/typedef': [ - 'error', - { arrowParameter: false, variableDeclarationIgnoreFunction: true }, - ], + '@typescript-eslint/typedef': ['error', { arrowParameter: false, variableDeclarationIgnoreFunction: true }], }, }, { @@ -37,6 +30,7 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/unbound-method': 'off', }, }, { @@ -55,7 +49,7 @@ module.exports = { parserOptions: { ecmaVersion: 2017, }, - } + }, ], rules: { // Bundle size isn't too much of an issue for React Native. From 3c9b856bc9477374b1ce5ddd6bf1d86ffbcbc7d3 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 7 Aug 2023 12:54:21 +0200 Subject: [PATCH 11/16] Update tester project pods and xcode project --- RNSentryCocoaTester/Podfile | 1 + .../project.pbxproj | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/RNSentryCocoaTester/Podfile b/RNSentryCocoaTester/Podfile index b2bd69527..f3d4c1b95 100644 --- a/RNSentryCocoaTester/Podfile +++ b/RNSentryCocoaTester/Podfile @@ -5,4 +5,5 @@ platform :ios, '12.4' target 'RNSentryCocoaTesterTests' do use_react_native!() pod 'RNSentry', :path => '../RNSentry.podspec' + pod 'OCMock', '3.9.1' end diff --git a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index a3cda2587..36e409870 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ /* Begin PBXFileReference section */ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; 3360898D29524164007C7730 /* RNSentryCocoaTesterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNSentryCocoaTesterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 338739072A7D7D2800950DDD /* RNSentry+initNativeSdk.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentry+initNativeSdk.h"; sourceTree = ""; }; 33F58ACF2977037D008F60EA /* RNSentry+initNativeSdk.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "RNSentry+initNativeSdk.mm"; sourceTree = ""; }; 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNSentryCocoaTesterTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E2321E7CFA55AB617247098E /* Pods-RNSentryCocoaTesterTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.debug.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.debug.xcconfig"; sourceTree = ""; }; @@ -62,6 +63,7 @@ isa = PBXGroup; children = ( 33F58ACF2977037D008F60EA /* RNSentry+initNativeSdk.mm */, + 338739072A7D7D2800950DDD /* RNSentry+initNativeSdk.h */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -249,6 +251,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\""; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -301,6 +304,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\""; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -316,6 +320,49 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/DoubleConversion\"", + "\"${PODS_ROOT}/Headers/Public/FBLazyVector\"", + "\"${PODS_ROOT}/Headers/Public/OCMock\"", + "\"${PODS_ROOT}/Headers/Public/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/RCTRequired\"", + "\"${PODS_ROOT}/Headers/Public/RCTTypeSafety\"", + "\"${PODS_ROOT}/Headers/Public/RNSentry\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen\"", + "\"${PODS_ROOT}/Headers/Public/React-Core\"", + "\"${PODS_ROOT}/Headers/Public/React-NativeModulesApple\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAnimation\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAppDelegate\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTBlob\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTText\"", + "\"${PODS_ROOT}/Headers/Public/React-callinvoker\"", + "\"${PODS_ROOT}/Headers/Public/React-cxxreact\"", + "\"${PODS_ROOT}/Headers/Public/React-debug\"", + "\"${PODS_ROOT}/Headers/Public/React-hermes\"", + "\"${PODS_ROOT}/Headers/Public/React-jsi\"", + "\"${PODS_ROOT}/Headers/Public/React-jsiexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-jsinspector\"", + "\"${PODS_ROOT}/Headers/Public/React-logger\"", + "\"${PODS_ROOT}/Headers/Public/React-perflogger\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimeexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimescheduler\"", + "\"${PODS_ROOT}/Headers/Public/React-utils\"", + "\"${PODS_ROOT}/Headers/Public/ReactCommon\"", + "\"${PODS_ROOT}/Headers/Public/Sentry\"", + "\"${PODS_ROOT}/Headers/Public/SocketRocket\"", + "\"${PODS_ROOT}/Headers/Public/Yoga\"", + "\"${PODS_ROOT}/Headers/Public/fmt\"", + "\"${PODS_ROOT}/Headers/Public/glog\"", + "\"${PODS_ROOT}/Headers/Public/hermes-engine\"", + "\"${PODS_ROOT}/Headers/Public/libevent\"", + "\"$(PODS_ROOT)/DoubleConversion\"", + "\"$(PODS_ROOT)/boost\"", + "\"$(PODS_ROOT)/Headers/Private/React-Core\"", + "\"$(PODS_TARGET_SRCROOT)/include/\"", + "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\"", + ); IPHONEOS_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.RNSentryCocoaTesterTests; @@ -335,6 +382,49 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/DoubleConversion\"", + "\"${PODS_ROOT}/Headers/Public/FBLazyVector\"", + "\"${PODS_ROOT}/Headers/Public/OCMock\"", + "\"${PODS_ROOT}/Headers/Public/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/RCTRequired\"", + "\"${PODS_ROOT}/Headers/Public/RCTTypeSafety\"", + "\"${PODS_ROOT}/Headers/Public/RNSentry\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen\"", + "\"${PODS_ROOT}/Headers/Public/React-Core\"", + "\"${PODS_ROOT}/Headers/Public/React-NativeModulesApple\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAnimation\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAppDelegate\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTBlob\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTText\"", + "\"${PODS_ROOT}/Headers/Public/React-callinvoker\"", + "\"${PODS_ROOT}/Headers/Public/React-cxxreact\"", + "\"${PODS_ROOT}/Headers/Public/React-debug\"", + "\"${PODS_ROOT}/Headers/Public/React-hermes\"", + "\"${PODS_ROOT}/Headers/Public/React-jsi\"", + "\"${PODS_ROOT}/Headers/Public/React-jsiexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-jsinspector\"", + "\"${PODS_ROOT}/Headers/Public/React-logger\"", + "\"${PODS_ROOT}/Headers/Public/React-perflogger\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimeexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimescheduler\"", + "\"${PODS_ROOT}/Headers/Public/React-utils\"", + "\"${PODS_ROOT}/Headers/Public/ReactCommon\"", + "\"${PODS_ROOT}/Headers/Public/Sentry\"", + "\"${PODS_ROOT}/Headers/Public/SocketRocket\"", + "\"${PODS_ROOT}/Headers/Public/Yoga\"", + "\"${PODS_ROOT}/Headers/Public/fmt\"", + "\"${PODS_ROOT}/Headers/Public/glog\"", + "\"${PODS_ROOT}/Headers/Public/hermes-engine\"", + "\"${PODS_ROOT}/Headers/Public/libevent\"", + "\"$(PODS_ROOT)/DoubleConversion\"", + "\"$(PODS_ROOT)/boost\"", + "\"$(PODS_ROOT)/Headers/Private/React-Core\"", + "\"$(PODS_TARGET_SRCROOT)/include/\"", + "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\"", + ); IPHONEOS_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.RNSentryCocoaTesterTests; From 585518a44d9d0d62041a6b8566404e5e92108fe4 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 9 Aug 2023 11:14:26 +0200 Subject: [PATCH 12/16] Fix tests, add new methods to the mock wrapper --- test/mockWrapper.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/mockWrapper.ts b/test/mockWrapper.ts index 0d2c6da08..d904b0c87 100644 --- a/test/mockWrapper.ts +++ b/test/mockWrapper.ts @@ -16,7 +16,6 @@ const NATIVE: MockInterface = { _processLevel: jest.fn(), _serializeObject: jest.fn(), _isModuleLoaded: jest.fn(), - _getBreadcrumbs: jest.fn(), isNativeAvailable: jest.fn(), @@ -49,6 +48,9 @@ const NATIVE: MockInterface = { startProfiling: jest.fn(), stopProfiling: jest.fn(), + + fetchNativePackageName: jest.fn(), + fetchNativeStackFramesBy: jest.fn(), }; NATIVE.isNativeAvailable.mockReturnValue(true); @@ -67,5 +69,7 @@ NATIVE.fetchModules.mockResolvedValue(null); NATIVE.fetchViewHierarchy.mockResolvedValue(null); NATIVE.startProfiling.mockReturnValue(false); NATIVE.stopProfiling.mockReturnValue(null); +NATIVE.fetchNativePackageName.mockResolvedValue('mock-native-package-name'); +NATIVE.fetchNativeStackFramesBy.mockResolvedValue(null); export { NATIVE }; From 23120a0236e4fb800bad8ac0413795ed8947b8f1 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 9 Aug 2023 11:28:16 +0200 Subject: [PATCH 13/16] Add changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17088eefa..ed7fc4b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +### Features + +- Add support for React Native mixed stacktraces ([#3201](https://github.com/getsentry/sentry-react-native/pull/3201)) + + In the current `react-native@nightly` JS errors from native modules can contain native JVM or Objective-C exception stack trace. + Both JS and native stack trace is processed by default no configuration needed. + ### Fixes - Use application variant instead of variant output to hook to correct package task for modules cleanup ([#3161](https://github.com/getsentry/sentry-react-native/pull/3161)) From d3ea823bbc2048300e907d76105991ea02e6c520 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 9 Aug 2023 11:58:05 +0200 Subject: [PATCH 14/16] Add integration JSDoc --- src/js/integrations/nativelinkederrors.ts | 88 ++--------------------- 1 file changed, 6 insertions(+), 82 deletions(-) diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts index 8c5a90379..6c0d8d394 100644 --- a/src/js/integrations/nativelinkederrors.ts +++ b/src/js/integrations/nativelinkederrors.ts @@ -1,3 +1,4 @@ +import { exceptionFromError } from '@sentry/browser'; import type { DebugImage, Event, @@ -24,7 +25,7 @@ interface LinkedErrorsOptions { } /** - * + * Processes JS and RN native linked errors. */ export class NativeLinkedErrors implements Integration { /** @@ -68,7 +69,7 @@ export class NativeLinkedErrors implements Integration { } /** - * + * Enriches passed event with linked exceptions and native debug meta images. */ private async _handler( parser: StackParser, @@ -96,7 +97,8 @@ export class NativeLinkedErrors implements Integration { } /** - * + * Walks linked errors and created Sentry exceptions chain. + * Collects debug images from native errors stack frames. */ private async _walkErrorTree( parser: StackParser, @@ -144,7 +146,7 @@ export class NativeLinkedErrors implements Integration { return this._walkErrorTree( parser, limit, - error[key], + linkedError, key, [...exceptions, exception], [...debugImages, ...(exceptionDebugImages || [])], @@ -227,81 +229,3 @@ export class NativeLinkedErrors implements Integration { return NATIVE.fetchNativeStackFramesBy(instructionsAddr); } } - -// TODO: Needs to be exported from @sentry/browser -/** - * This function creates an exception from a JavaScript Error - */ -export function exceptionFromError(stackParser: StackParser, ex: Error): Exception { - // Get the frames first since Opera can lose the stack if we touch anything else first - const frames = parseStackFrames(stackParser, ex); - - const exception: Exception = { - type: ex && ex.name, - value: extractMessage(ex), - }; - - if (frames.length) { - exception.stacktrace = { frames }; - } - - if (exception.type === undefined && exception.value === '') { - exception.value = 'Unrecoverable error caught'; - } - - return exception; -} - -/** Parses stack frames from an error */ -export function parseStackFrames( - stackParser: StackParser, - ex: Error & { framesToPop?: number; stacktrace?: string }, -): StackFrame[] { - // Access and store the stacktrace property before doing ANYTHING - // else to it because Opera is not very good at providing it - // reliably in other circumstances. - const stacktrace = ex.stacktrace || ex.stack || ''; - - const popSize = getPopSize(ex); - - try { - return stackParser(stacktrace, popSize); - } catch (e) { - // no-empty - } - - return []; -} - -/** - * 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 { - const message = ex && ex.message; - if (!message) { - return 'No error message'; - } - if (message.error && typeof message.error.message === 'string') { - return message.error.message; - } - return message; -} - -// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108 -const reactMinifiedRegexp = /Minified React error #\d+;/i; - -function getPopSize(ex: Error & { framesToPop?: number }): number { - if (ex) { - if (typeof ex.framesToPop === 'number') { - return ex.framesToPop; - } - - if (reactMinifiedRegexp.test(ex.message)) { - return 1; - } - } - - return 0; -} From 1b6645a880dbdb71494287dab04c8ef49133f53f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 10 Aug 2023 10:38:32 +0200 Subject: [PATCH 15/16] Update changelog with fixed nightly version --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa3544a6..67b835e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ - Add support for React Native mixed stacktraces ([#3201](https://github.com/getsentry/sentry-react-native/pull/3201)) - In the current `react-native@nightly` JS errors from native modules can contain native JVM or Objective-C exception stack trace. - Both JS and native stack trace is processed by default no configuration needed. + In the current `react-native@nightly` (`0.73.0-nightly-20230809-cb60e5c67`) JS errors from native modules can + contain native JVM or Objective-C exception stack trace. Both JS and native stack trace + are processed by default no configuration needed. ### Fixes From 8163cb0d0d025990aa0713f010ee5dbf54ff3f75 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 10 Aug 2023 10:42:03 +0200 Subject: [PATCH 16/16] Remove unused java code --- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 5 ----- android/src/newarch/java/io/sentry/react/RNSentryModule.java | 2 +- android/src/oldarch/java/io/sentry/react/RNSentryModule.java | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 6633f07fe..158847b0c 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -708,11 +708,6 @@ public void fetchNativePackageName(Promise promise) { promise.resolve(packageInfo.packageName); } - public void fetchNativeStackFramesBy(Promise promise) { - logger.log(SentryLevel.ERROR, "This method works only on iOS."); - promise.resolve(null); - } - private void setEventOriginTag(SentryEvent event) { SdkVersion sdk = event.getSdk(); if (sdk != null) { diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 4c0bfba5c..33bfeec44 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -141,6 +141,6 @@ public void fetchNativePackageName(Promise promise) { @Override public void fetchNativeStackFramesBy(Promise promise) { - this.impl.fetchNativeStackFramesBy(promise); + // Not used on Android } } diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 8ee9effb2..7a5a5f0db 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -140,6 +140,6 @@ public void fetchNativePackageName(Promise promise) { @ReactMethod public void fetchNativeStackFramesBy(Promise promise) { - this.impl.fetchNativeStackFramesBy(promise); + // Not used on Android } }