Skip to content

Commit

Permalink
feat(sdk): Add Hermes Debug Info flag to React Native Context (#3290)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Sep 19, 2023
1 parent e5c9b8b commit 5fefdf7
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 1 deletion.
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

0 comments on commit 5fefdf7

Please sign in to comment.