From 5fefdf7ccade18e5ac77ef152aa533e60d6d5b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:00:32 +0200 Subject: [PATCH] feat(sdk): Add Hermes Debug Info flag to React Native Context (#3290) --- CHANGELOG.md | 7 ++ src/js/integrations/reactnativeinfo.ts | 33 ++++++++- test/integrations/reactnativeinfo.test.ts | 85 +++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6502acc8e..c369ef789e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +- Add Hermes Debug Info flag to React Native Context ([#3290](https://github.com/getsentry/sentry-react-native/pull/3290)) + - This flag equals `true` when Hermes Bundle contains Debug Info (Hermes Source Map was not emitted) + ## 5.9.2 ### Fixes diff --git a/src/js/integrations/reactnativeinfo.ts b/src/js/integrations/reactnativeinfo.ts index e1aa23a052..c7b52cebbe 100644 --- a/src/js/integrations/reactnativeinfo.ts +++ b/src/js/integrations/reactnativeinfo.ts @@ -18,6 +18,7 @@ export interface ReactNativeContext extends Context { hermes_version?: string; react_native_version: string; component_stack?: string; + hermes_debug_info?: boolean; } /** Loads React Native context at runtime */ @@ -50,8 +51,9 @@ export class ReactNativeInfo implements Integration { reactNativeContext.js_engine = 'hermes'; const hermesVersion = getHermesVersion(); if (hermesVersion) { - reactNativeContext.hermes_version = getHermesVersion(); + reactNativeContext.hermes_version = hermesVersion; } + reactNativeContext.hermes_debug_info = !isEventWithHermesBytecodeFrames(event); } else if (reactNativeError?.jsEngine) { reactNativeContext.js_engine = reactNativeError.jsEngine; } @@ -76,3 +78,32 @@ export class ReactNativeInfo implements Integration { }); } } + +/** + * Guess if the event contains frames with Hermes bytecode + * (thus Hermes bundle doesn't contain debug info) + * based on the event exception/threads frames. + * + * This function can be relied on only if Hermes is enabled! + * + * Hermes bytecode position is always line 1 and column 0-based number. + * If Hermes bundle has debug info, the bytecode frames pos are calculated + * back to the plain bundle source code positions and line will be > 1. + * + * Line 1 contains start time var, it's safe to assume it won't crash. + * The above only applies when Hermes is enabled. + * + * Javascript/Hermes bytecode frames have platform === undefined. + * Native (Java, ObjC, C++) frames have platform === 'android'/'ios'/'native'. + */ +function isEventWithHermesBytecodeFrames(event: Event): boolean { + for (const value of event.exception?.values || event.threads?.values || []) { + for (const frame of value.stacktrace?.frames || []) { + // platform === undefined we assume it's javascript (only native frames use the platform attribute) + if (frame.platform === undefined && frame.lineno === 1) { + return true; + } + } + } + return false; +} diff --git a/test/integrations/reactnativeinfo.test.ts b/test/integrations/reactnativeinfo.test.ts index 52c9c5a823..62784f4dc4 100644 --- a/test/integrations/reactnativeinfo.test.ts +++ b/test/integrations/reactnativeinfo.test.ts @@ -49,6 +49,7 @@ describe('React Native Info', () => { turbo_module: false, fabric: false, js_engine: 'hermes', + hermes_debug_info: true, react_native_version: '1000.0.0-test', expo: false, }, @@ -148,6 +149,90 @@ describe('React Native Info', () => { test: 'context', }); }); + + it('add hermes_debug_info to react_native_context based on exception frames (hermes bytecode frames present -> no debug info)', async () => { + mockedIsHermesEnabled = jest.fn().mockReturnValue(true); + + const mockedEvent: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { + platform: 'java', + lineno: 2, + }, + { + lineno: 1, + }, + ], + }, + }, + ], + }, + }; + const actualEvent = await executeIntegrationFor(mockedEvent, {}); + + expectMocksToBeCalledOnce(); + expect(actualEvent?.contexts?.react_native_context?.hermes_debug_info).toEqual(false); + }); + + it('does not hermes_debug_info to react_native_context based on threads frames (hermes bytecode frames present -> no debug info)', async () => { + mockedIsHermesEnabled = jest.fn().mockReturnValue(true); + + const mockedEvent: Event = { + threads: { + values: [ + { + stacktrace: { + frames: [ + { + platform: 'java', + lineno: 2, + }, + { + lineno: 1, + }, + ], + }, + }, + ], + }, + }; + const actualEvent = await executeIntegrationFor(mockedEvent, {}); + + expectMocksToBeCalledOnce(); + expect(actualEvent?.contexts?.react_native_context?.hermes_debug_info).toEqual(false); + }); + + it('adds hermes_debug_info to react_native_context (no hermes bytecode frames found -> debug info present)', async () => { + mockedIsHermesEnabled = jest.fn().mockReturnValue(true); + + const mockedEvent: Event = { + threads: { + values: [ + { + stacktrace: { + frames: [ + { + platform: 'java', + lineno: 2, + }, + { + lineno: 2, + }, + ], + }, + }, + ], + }, + }; + const actualEvent = await executeIntegrationFor(mockedEvent, {}); + + expectMocksToBeCalledOnce(); + expect(actualEvent?.contexts?.react_native_context?.hermes_debug_info).toEqual(true); + }); }); function expectMocksToBeCalledOnce() {