From 16526779b5a8ec889f4380f0c242c536f74083dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Fri, 12 May 2023 17:11:37 +0200 Subject: [PATCH] Add expo, hermes version, react_native_version to react_native_context (#3050) --- CHANGELOG.md | 6 ++++++ src/js/integrations/reactnativeinfo.ts | 18 +++++++++++++++- src/js/utils/environment.ts | 21 ++++++++++++++++++ src/js/utils/worldwide.ts | 5 ++++- test/integrations/reactnativeinfo.test.ts | 26 +++++++++++++++++++---- tsconfig.build.json | 3 ++- tsconfig.json | 3 ++- typings/react-native.d.ts | 8 +++++++ 8 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 typings/react-native.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc56f4e0..cad9569c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add `expo`, `react_native_version` and `hermes_version` to React Native Context ([#3050](https://github.com/getsentry/sentry-react-native/pull/3050)) + ## 5.4.2 ### Fixes diff --git a/src/js/integrations/reactnativeinfo.ts b/src/js/integrations/reactnativeinfo.ts index 756145084..e1aa23a05 100644 --- a/src/js/integrations/reactnativeinfo.ts +++ b/src/js/integrations/reactnativeinfo.ts @@ -1,12 +1,22 @@ import type { Context, Event, EventHint, EventProcessor, Integration } from '@sentry/types'; -import { isFabricEnabled, isHermesEnabled, isTurboModuleEnabled } from '../utils/environment'; +import { + getHermesVersion, + getReactNativeVersion, + isExpo, + isFabricEnabled, + isHermesEnabled, + isTurboModuleEnabled, +} from '../utils/environment'; import type { ReactNativeError } from './debugsymbolicator'; export interface ReactNativeContext extends Context { js_engine?: string; turbo_module: boolean; fabric: boolean; + expo: boolean; + hermes_version?: string; + react_native_version: string; component_stack?: string; } @@ -32,10 +42,16 @@ export class ReactNativeInfo implements Integration { const reactNativeContext: ReactNativeContext = { turbo_module: isTurboModuleEnabled(), fabric: isFabricEnabled(), + react_native_version: getReactNativeVersion(), + expo: isExpo(), }; if (isHermesEnabled()) { reactNativeContext.js_engine = 'hermes'; + const hermesVersion = getHermesVersion(); + if (hermesVersion) { + reactNativeContext.hermes_version = getHermesVersion(); + } } else if (reactNativeError?.jsEngine) { reactNativeContext.js_engine = reactNativeError.jsEngine; } diff --git a/src/js/utils/environment.ts b/src/js/utils/environment.ts index b6296b25f..64f0449f9 100644 --- a/src/js/utils/environment.ts +++ b/src/js/utils/environment.ts @@ -1,3 +1,5 @@ +import { version as RNV } from 'react-native/Libraries/Core/ReactNativeVersion'; + import { RN_GLOBAL_OBJ } from '../utils/worldwide'; /** Checks if the React Native Hermes engine is running */ @@ -14,3 +16,22 @@ export function isTurboModuleEnabled(): boolean { export function isFabricEnabled(): boolean { return RN_GLOBAL_OBJ.nativeFabricUIManager != null; } + +/** Returns React Native Version as semver string */ +export function getReactNativeVersion(): string { + return `${RNV.major}.${RNV.minor}.${RNV.patch}${RNV.prerelease != null ? `-${RNV.prerelease}` : ''}`; +} + +/** Checks if Expo is present in the runtime */ +export function isExpo(): boolean { + return RN_GLOBAL_OBJ.expo != null; +} + +/** Returns Hermes Version if hermes is present in the runtime */ +export function getHermesVersion(): string | undefined { + return ( + RN_GLOBAL_OBJ.HermesInternal && + RN_GLOBAL_OBJ.HermesInternal.getRuntimeProperties && + RN_GLOBAL_OBJ.HermesInternal.getRuntimeProperties()['OSS Release Version'] + ); +} diff --git a/src/js/utils/worldwide.ts b/src/js/utils/worldwide.ts index 18ea412f3..705453758 100644 --- a/src/js/utils/worldwide.ts +++ b/src/js/utils/worldwide.ts @@ -6,11 +6,14 @@ import type { ErrorUtils } from 'react-native/types'; export interface ReactNativeInternalGlobal extends InternalGlobal { __sentry_rn_v4_registered?: boolean; __sentry_rn_v5_registered?: boolean; - HermesInternal: unknown; + HermesInternal?: { + getRuntimeProperties?: () => Record; + }; Promise: unknown; __turboModuleProxy: unknown; nativeFabricUIManager: unknown; ErrorUtils?: ErrorUtils; + expo: unknown; } /** Get's the global object for the current JavaScript runtime */ diff --git a/test/integrations/reactnativeinfo.test.ts b/test/integrations/reactnativeinfo.test.ts index 67bed8233..4e2113590 100644 --- a/test/integrations/reactnativeinfo.test.ts +++ b/test/integrations/reactnativeinfo.test.ts @@ -7,18 +7,27 @@ import { ReactNativeInfo } from '../../src/js/integrations/reactnativeinfo'; let mockedIsHermesEnabled: jest.Mock; let mockedIsTurboModuleEnabled: jest.Mock; let mockedIsFabricEnabled: jest.Mock; +let mockedGetReactNativeVersion: jest.Mock; +let mockedGetHermesVersion: jest.Mock; +let mockedIsExpo: jest.Mock; jest.mock('../../src/js/utils/environment', () => ({ isHermesEnabled: () => mockedIsHermesEnabled(), isTurboModuleEnabled: () => mockedIsTurboModuleEnabled(), isFabricEnabled: () => mockedIsFabricEnabled(), + getReactNativeVersion: () => mockedGetReactNativeVersion(), + getHermesVersion: () => mockedGetHermesVersion(), + isExpo: () => mockedIsExpo(), })); describe('React Native Info', () => { beforeEach(() => { - mockedIsHermesEnabled = jest.fn().mockReturnValue(false); + mockedIsHermesEnabled = jest.fn().mockReturnValue(true); mockedIsTurboModuleEnabled = jest.fn().mockReturnValue(false); mockedIsFabricEnabled = jest.fn().mockReturnValue(false); + mockedGetReactNativeVersion = jest.fn().mockReturnValue('1000.0.0-test'); + mockedGetHermesVersion = jest.fn().mockReturnValue(undefined); + mockedIsExpo = jest.fn().mockReturnValue(false); }); afterEach(() => { @@ -39,20 +48,28 @@ describe('React Native Info', () => { react_native_context: { turbo_module: false, fabric: false, + js_engine: 'hermes', + react_native_version: '1000.0.0-test', + expo: false, }, }, + tags: { + hermes: 'true', + }, }); }); it('adds hermes tag and js_engine to context if hermes enabled', async () => { mockedIsHermesEnabled = jest.fn().mockReturnValue(true); + mockedGetHermesVersion = jest.fn().mockReturnValue('for RN 999.0.0'); const actualEvent = await executeIntegrationFor({}, {}); expectMocksToBeCalledOnce(); expect(actualEvent?.tags?.hermes).toEqual('true'); - expect((actualEvent?.contexts?.react_native_context as ReactNativeContext | undefined)?.js_engine).toEqual( - 'hermes', - ); + expect(actualEvent?.contexts?.react_native_context).toEqual(expect.objectContaining({ + js_engine: 'hermes', + hermes_version: 'for RN 999.0.0', + })); }); it('does not override existing hermes tag', async () => { @@ -69,6 +86,7 @@ describe('React Native Info', () => { }); it('adds engine from rn error', async () => { + mockedIsHermesEnabled = jest.fn().mockReturnValue(false); const mockedHint: EventHint = { originalException: { jsEngine: 'test_engine', diff --git a/tsconfig.build.json b/tsconfig.build.json index ed728c282..4bbaba5ee 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,7 +1,8 @@ { "extends": "./node_modules/@sentry/typescript/tsconfig.json", "include": [ - "src/js/*.ts" + "src/js/*.ts", + "typings/*.d.ts" ], "exclude": [ "node_modules" diff --git a/tsconfig.json b/tsconfig.json index cc6676216..f528b7058 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "src/js/*.ts", "src/js/*.tsx", "test/**/*.ts", - "test/**/*.tsx" + "test/**/*.tsx", + "typings/*.d.ts" ], "exclude": [ "dist" diff --git a/typings/react-native.d.ts b/typings/react-native.d.ts new file mode 100644 index 000000000..6cd00da3b --- /dev/null +++ b/typings/react-native.d.ts @@ -0,0 +1,8 @@ +declare module 'react-native/Libraries/Core/ReactNativeVersion' { + export const version: { + major: number; + minor: number; + patch: number; + prerelease: string | null; + }; +};