Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk): Add Hermes Debug Info flag to React Native Context #3290

Merged
merged 8 commits into from
Sep 19, 2023
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
33 changes: 32 additions & 1 deletion src/js/integrations/reactnativeinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
85 changes: 85 additions & 0 deletions test/integrations/reactnativeinfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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() {
Expand Down