-
-
Notifications
You must be signed in to change notification settings - Fork 338
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(context): Add new arch info into event contexts (#2552)
- Loading branch information
1 parent
cc8db4f
commit 5053bd7
Showing
8 changed files
with
259 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Context, Event, EventHint, EventProcessor, Integration } from '@sentry/types'; | ||
|
||
import { isFabricEnabled, isHermesEnabled, isTurboModuleEnabled } from '../utils/environment'; | ||
import { ReactNativeError } from './debugsymbolicator'; | ||
|
||
export interface ReactNativeContext extends Context { | ||
js_engine?: string; | ||
turbo_module: boolean; | ||
fabric: boolean; | ||
component_stack?: string; | ||
} | ||
|
||
/** Loads React Native context at runtime */ | ||
export class ReactNativeInfo implements Integration { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
public static id: string = 'ReactNativeInfo'; | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public name: string = ReactNativeInfo.id; | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { | ||
addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { | ||
const reactNativeError = hint?.originalException | ||
? hint?.originalException as ReactNativeError | ||
: undefined; | ||
|
||
const reactNativeContext: ReactNativeContext = { | ||
turbo_module: isTurboModuleEnabled(), | ||
fabric: isFabricEnabled(), | ||
}; | ||
|
||
if (isHermesEnabled()) { | ||
reactNativeContext.js_engine = 'hermes'; | ||
} else if (reactNativeError?.jsEngine) { | ||
reactNativeContext.js_engine = reactNativeError.jsEngine; | ||
} | ||
|
||
if (reactNativeContext.js_engine === 'hermes') { | ||
event.tags = { | ||
hermes: 'true', | ||
...event.tags, | ||
}; | ||
} | ||
|
||
if (reactNativeError?.componentStack) { | ||
reactNativeContext.component_stack = reactNativeError.componentStack; | ||
} | ||
|
||
event.contexts = { | ||
react_native_context: reactNativeContext, | ||
...event.contexts, | ||
}; | ||
|
||
return event; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { getGlobalObject } from '@sentry/utils'; | ||
|
||
interface ReactNativeGlobal { | ||
HermesInternal: unknown; | ||
__turboModuleProxy: unknown; | ||
nativeFabricUIManager: unknown; | ||
} | ||
|
||
/** Safely gets global object with React Native specific properties */ | ||
function getRNGlobalObject<T>(): T & ReactNativeGlobal & ReturnType<typeof getGlobalObject> { | ||
return getGlobalObject<T & ReactNativeGlobal>(); | ||
} | ||
|
||
/** Checks if the React Native Hermes engine is running */ | ||
export function isHermesEnabled(): boolean { | ||
return !!getRNGlobalObject().HermesInternal; | ||
} | ||
|
||
/** Checks if the React Native TurboModules are enabled */ | ||
export function isTurboModuleEnabled(): boolean { | ||
return getRNGlobalObject().__turboModuleProxy != null; | ||
} | ||
|
||
/** Checks if the React Native Fabric renderer is running */ | ||
export function isFabricEnabled(): boolean { | ||
return getRNGlobalObject().nativeFabricUIManager != null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { Event, EventHint } from '@sentry/types'; | ||
|
||
import { ReactNativeError } from '../../src/js/integrations/debugsymbolicator'; | ||
import { ReactNativeContext, ReactNativeInfo } from '../../src/js/integrations/reactnativeinfo'; | ||
|
||
let mockedIsHermesEnabled: jest.Mock<boolean, []>; | ||
let mockedIsTurboModuleEnabled: jest.Mock<boolean, []>; | ||
let mockedIsFabricEnabled: jest.Mock<boolean, []>; | ||
|
||
jest.mock('../../src/js/utils/environment', () => ({ | ||
isHermesEnabled: () => mockedIsHermesEnabled(), | ||
isTurboModuleEnabled: () => mockedIsTurboModuleEnabled(), | ||
isFabricEnabled: () => mockedIsFabricEnabled(), | ||
})); | ||
|
||
describe('React Native Info', () => { | ||
beforeEach(() => { | ||
mockedIsHermesEnabled = jest.fn().mockReturnValue(false); | ||
mockedIsTurboModuleEnabled = jest.fn().mockReturnValue(false); | ||
mockedIsFabricEnabled = jest.fn().mockReturnValue(false); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it('does not pollute event with undefined fields', async () => { | ||
const mockEvent: Event = { | ||
message: 'test' | ||
}; | ||
const mockedHint: EventHint = {}; | ||
const actualEvent = await executeIntegrationFor(mockEvent, mockedHint); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect(actualEvent).toEqual(<Event>{ | ||
message: 'test', | ||
contexts: { | ||
react_native_context: <ReactNativeContext>{ | ||
turbo_module: false, | ||
fabric: false, | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('adds hermes tag and js_engine to context if hermes enabled', async () => { | ||
mockedIsHermesEnabled = jest.fn().mockReturnValue(true); | ||
const actualEvent = await executeIntegrationFor({}, {}); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect(actualEvent?.tags?.hermes).toEqual('true'); | ||
expect( | ||
(actualEvent?.contexts?.react_native_context as ReactNativeContext | undefined)?.js_engine, | ||
).toEqual('hermes'); | ||
}); | ||
|
||
it('does not override existing hermes tag', async () => { | ||
mockedIsHermesEnabled = jest.fn().mockReturnValue(true); | ||
const mockedEvent: Event = { | ||
tags: { | ||
hermes: 'test_hermes_tag', | ||
}, | ||
}; | ||
const actualEvent = await executeIntegrationFor(mockedEvent, {}); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect(actualEvent?.tags?.hermes).toEqual('test_hermes_tag'); | ||
}); | ||
|
||
it('adds engine from rn error', async () => { | ||
const mockedHint: EventHint = { | ||
originalException: <ReactNativeError>{ | ||
jsEngine: 'test_engine', | ||
}, | ||
}; | ||
const actualEvent = await executeIntegrationFor({}, mockedHint); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect(actualEvent?.tags?.hermes).toEqual(undefined); | ||
expect( | ||
(actualEvent?.contexts?.react_native_context as ReactNativeContext | undefined)?.js_engine, | ||
).toEqual('test_engine'); | ||
}); | ||
|
||
it('adds component stack', async () => { | ||
const mockedHint: EventHint = { | ||
originalException: <ReactNativeError>{ | ||
componentStack: 'test_stack', | ||
}, | ||
}; | ||
const actualEvent = await executeIntegrationFor({}, mockedHint); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect( | ||
(actualEvent?.contexts?.react_native_context as ReactNativeContext | undefined)?.component_stack, | ||
).toEqual('test_stack'); | ||
}); | ||
|
||
it('marks turbo modules enabled', async () => { | ||
mockedIsTurboModuleEnabled = jest.fn().mockReturnValue(true); | ||
const actualEvent = await executeIntegrationFor({}, {}); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect( | ||
(actualEvent?.contexts?.react_native_context as ReactNativeContext | undefined)?.turbo_module, | ||
).toEqual(true); | ||
}); | ||
|
||
it('marks fabric enabled', async () => { | ||
mockedIsFabricEnabled = jest.fn().mockReturnValue(true); | ||
const actualEvent = await executeIntegrationFor({}, {}); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect( | ||
(actualEvent?.contexts?.react_native_context as ReactNativeContext | undefined)?.fabric, | ||
).toEqual(true); | ||
}); | ||
|
||
it('does not override existing react_native_context', async () => { | ||
const mockedEvent: Event = { | ||
contexts: { | ||
react_native_context: { | ||
test: 'context', | ||
}, | ||
}, | ||
}; | ||
const actualEvent = await executeIntegrationFor(mockedEvent, {}); | ||
|
||
expectMocksToBeCalledOnce(); | ||
expect(actualEvent?.contexts?.react_native_context).toEqual({ | ||
test: 'context', | ||
}); | ||
}); | ||
}); | ||
|
||
function expectMocksToBeCalledOnce() { | ||
expect(mockedIsHermesEnabled).toBeCalledTimes(1); | ||
expect(mockedIsTurboModuleEnabled).toBeCalledTimes(1); | ||
expect(mockedIsFabricEnabled).toBeCalledTimes(1); | ||
} | ||
|
||
function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Promise<Event | null> { | ||
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); | ||
} | ||
}); | ||
}); | ||
} |