Skip to content

Commit

Permalink
feat(context): Add new arch info into event contexts (#2552)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Oct 19, 2022
1 parent cc8db4f commit 5053bd7
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 22 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# Changelog

## Unreleased

### Features

- Send react native js engine, turbo module, fabric flags and component stack in Event contexts ([#2552](https://github.com/getsentry/sentry-react-native/pull/2552))

## 5.0.0-alpha.6

- Latest changes from 4.6.1

### Features

- Add initial support for the RN New Architecture, backwards compatible RNSentry Turbo Module ([#2522](https://github.com/getsentry/sentry-react-native/pull/2522))

### Breaking changes
Expand Down
9 changes: 1 addition & 8 deletions src/js/integrations/debugsymbolicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ReactNativeFrame {
/**
* React Native Error
*/
type ReactNativeError = Error & {
export type ReactNativeError = Error & {
framesToPop?: number;
jsEngine?: string;
preventSymbolication?: boolean;
Expand Down Expand Up @@ -66,13 +66,6 @@ export class DebugSymbolicator implements Integration {
stack = parseErrorStack(reactError.stack);
}

// Ideally this should go into contexts but android sdk doesn't support it
event.extra = {
...event.extra,
componentStack: reactError.componentStack,
jsEngine: reactError.jsEngine,
};

await self._symbolicate(event, stack);

event.platform = 'node'; // Setting platform node makes sure we do not show source maps errors
Expand Down
1 change: 1 addition & 0 deletions src/js/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { ReactNativeErrorHandlers } from './reactnativeerrorhandlers';
export { Release } from './release';
export { EventOrigin } from './eventorigin';
export { SdkInfo } from './sdkinfo';
export { ReactNativeInfo } from './reactnativeinfo';
64 changes: 64 additions & 0 deletions src/js/integrations/reactnativeinfo.ts
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;
});
}
}
9 changes: 3 additions & 6 deletions src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getCurrentHub,
} from '@sentry/react';
import { Integration, Scope, StackFrame, UserFeedback } from '@sentry/types';
import { getGlobalObject, logger, stackParserFromStackParserOptions } from '@sentry/utils';
import { logger, stackParserFromStackParserOptions } from '@sentry/utils';
import * as React from 'react';

import { ReactNativeClient } from './client';
Expand All @@ -15,6 +15,7 @@ import {
DeviceContext,
EventOrigin,
ReactNativeErrorHandlers,
ReactNativeInfo,
Release,
SdkInfo,
} from './integrations';
Expand Down Expand Up @@ -86,6 +87,7 @@ export function init(passedOptions: ReactNativeOptions): void {

defaultIntegrations.push(new EventOrigin());
defaultIntegrations.push(new SdkInfo());
defaultIntegrations.push(new ReactNativeInfo());

if (__DEV__) {
defaultIntegrations.push(new DebugSymbolicator());
Expand Down Expand Up @@ -129,11 +131,6 @@ export function init(passedOptions: ReactNativeOptions): void {
defaultIntegrations,
});
initAndBind(ReactNativeClient, options);

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
if (getGlobalObject<any>().HermesInternal) {
getCurrentHub().setTag('hermes', 'true');
}
}

/**
Expand Down
27 changes: 27 additions & 0 deletions src/js/utils/environment.ts
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;
}
10 changes: 2 additions & 8 deletions src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,10 @@ import {
Spec,
} from './NativeRNSentry';
import { ReactNativeOptions } from './options';
import { isTurboModuleEnabled } from './utils/environment'
import { utf8ToBytes } from './vendor';

declare global {
// eslint-disable-next-line no-var
var __turboModuleProxy: unknown;
}

const isTurboModuleEnabled = globalThis.__turboModuleProxy != null;

const RNSentry: Spec | undefined = isTurboModuleEnabled
const RNSentry: Spec | undefined = isTurboModuleEnabled()
? TurboModuleRegistry.getEnforcing<Spec>('RNSentry')
: NativeModules.RNSentry;

Expand Down
154 changes: 154 additions & 0 deletions test/integrations/reactnativeinfo.test.ts
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);
}
});
});
}

0 comments on commit 5053bd7

Please sign in to comment.