Skip to content

Commit

Permalink
feat(expo): Dynamically resolve default integrations based on platform (
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Jan 9, 2024
1 parent 55fe14c commit dc8966b
Showing 6 changed files with 180 additions and 71 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -13,6 +13,10 @@ This release is compatible with `expo@50.0.0-preview.6` and newer.
const config = getSentryExpoConfig(config);
```

- Resolve Default Integrations based on current platform ([#3465](https://github.com/getsentry/sentry-react-native/pull/3465))
- Native Integrations are only added if Native Module is available
- Web Integrations only for React Native Web builds

- Includes fixes from version 5.15.2

## 5.15.2
87 changes: 87 additions & 0 deletions src/js/integrations/default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { hasTracingEnabled } from '@sentry/core';
import { HttpClient } from '@sentry/integrations';
import { Integrations as BrowserReactIntegrations } from '@sentry/react';
import type { Integration } from '@sentry/types';

import type { ReactNativeClientOptions } from '../options';
import { HermesProfiling } from '../profiling/integration';
import { ReactNativeTracing } from '../tracing';
import { notWeb } from '../utils/environment';
import { DebugSymbolicator } from './debugsymbolicator';
import { DeviceContext } from './devicecontext';
import { EventOrigin } from './eventorigin';
import { ModulesLoader } from './modulesloader';
import { NativeLinkedErrors } from './nativelinkederrors';
import { ReactNativeErrorHandlers } from './reactnativeerrorhandlers';
import { ReactNativeInfo } from './reactnativeinfo';
import { Release } from './release';
import { createReactNativeRewriteFrames } from './rewriteframes';
import { Screenshot } from './screenshot';
import { SdkInfo } from './sdkinfo';
import { ViewHierarchy } from './viewhierarchy';

/**
* Returns the default ReactNative integrations based on the current environment.
*
* Native integrations are only returned when native is enabled.
*
* Web integrations are only returned when running on web.
*/
export function getDefaultIntegrations(options: ReactNativeClientOptions): Integration[] {
const integrations: Integration[] = [];

if (notWeb()) {
integrations.push(
new ReactNativeErrorHandlers({
patchGlobalPromise: options.patchGlobalPromise,
}),
);
integrations.push(new NativeLinkedErrors());
} else {
integrations.push(new BrowserReactIntegrations.TryCatch());
integrations.push(new BrowserReactIntegrations.GlobalHandlers());
integrations.push(new BrowserReactIntegrations.LinkedErrors());
}

// @sentry/react default integrations
integrations.push(new BrowserReactIntegrations.InboundFilters());
integrations.push(new BrowserReactIntegrations.FunctionToString());
integrations.push(new BrowserReactIntegrations.Breadcrumbs());
integrations.push(new BrowserReactIntegrations.Dedupe());
integrations.push(new BrowserReactIntegrations.HttpContext());
// end @sentry/react-native default integrations

integrations.push(new Release());
integrations.push(new EventOrigin());
integrations.push(new SdkInfo());
integrations.push(new ReactNativeInfo());

if (__DEV__ && notWeb()) {
integrations.push(new DebugSymbolicator());
}

integrations.push(createReactNativeRewriteFrames());

if (options.enableNative) {
integrations.push(new DeviceContext());
integrations.push(new ModulesLoader());
if (options.attachScreenshot) {
integrations.push(new Screenshot());
}
if (options.attachViewHierarchy) {
integrations.push(new ViewHierarchy());
}
if (options._experiments && typeof options._experiments.profilesSampleRate === 'number') {
integrations.push(new HermesProfiling());
}
}

if (hasTracingEnabled(options) && options.enableAutoPerformanceTracing) {
integrations.push(new ReactNativeTracing());
}
if (options.enableCaptureFailedRequests) {
integrations.push(new HttpClient());
}

return integrations;
}
73 changes: 5 additions & 68 deletions src/js/sdk.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
/* eslint-disable complexity */
import type { Scope } from '@sentry/core';
import { getIntegrationsToSetup, hasTracingEnabled, Hub, initAndBind, makeMain, setExtra } from '@sentry/core';
import { HttpClient } from '@sentry/integrations';
import { getIntegrationsToSetup, Hub, initAndBind, makeMain, setExtra } from '@sentry/core';
import {
defaultIntegrations as reactDefaultIntegrations,
defaultStackParser,
getCurrentHub,
makeFetchTransport,
} from '@sentry/react';
import type { Integration, UserFeedback } from '@sentry/types';
import { logger, stackParserFromStackParserOptions } from '@sentry/utils';
import * as React from 'react';
import { Platform } from 'react-native';

import { ReactNativeClient } from './client';
import {
DebugSymbolicator,
DeviceContext,
EventOrigin,
HermesProfiling,
ModulesLoader,
ReactNativeErrorHandlers,
ReactNativeInfo,
Release,
SdkInfo,
} from './integrations';
import { NativeLinkedErrors } from './integrations/nativelinkederrors';
import { createReactNativeRewriteFrames } from './integrations/rewriteframes';
import { Screenshot } from './integrations/screenshot';
import { ViewHierarchy } from './integrations/viewhierarchy';
import { getDefaultIntegrations } from './integrations/default';
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
import { ReactNativeScope } from './scope';
import { TouchEventBoundary } from './touchevents';
@@ -39,11 +22,6 @@ import { getDefaultEnvironment } from './utils/environment';
import { safeFactory, safeTracesSampler } from './utils/safe';
import { NATIVE } from './wrapper';

const IGNORED_DEFAULT_INTEGRATIONS = [
'GlobalHandlers', // We will use the react-native internal handlers
'TryCatch', // We don't need this
'LinkedErrors', // We replace this with `NativeLinkedError`
];
const DEFAULT_OPTIONS: ReactNativeOptions = {
enableNativeCrashHandling: true,
enableNativeNagger: true,
@@ -102,50 +80,9 @@ export function init(passedOptions: ReactNativeOptions): void {
options.environment = getDefaultEnvironment();
}

const defaultIntegrations: Integration[] = passedOptions.defaultIntegrations || [];
if (passedOptions.defaultIntegrations === undefined) {
defaultIntegrations.push(new ModulesLoader());
if (Platform.OS !== 'web') {
defaultIntegrations.push(new ReactNativeErrorHandlers({
patchGlobalPromise: options.patchGlobalPromise,
}));
}
defaultIntegrations.push(new Release());
defaultIntegrations.push(...[
...reactDefaultIntegrations.filter(
(i) => !IGNORED_DEFAULT_INTEGRATIONS.includes(i.name)
),
]);

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

if (__DEV__) {
defaultIntegrations.push(new DebugSymbolicator());
}

defaultIntegrations.push(createReactNativeRewriteFrames());
if (options.enableNative) {
defaultIntegrations.push(new DeviceContext());
}
if (options._experiments && typeof options._experiments.profilesSampleRate === 'number') {
defaultIntegrations.push(new HermesProfiling());
}
if (hasTracingEnabled(options) && options.enableAutoPerformanceTracing) {
defaultIntegrations.push(new ReactNativeTracing());
}
if (options.attachScreenshot) {
defaultIntegrations.push(new Screenshot());
}
if (options.attachViewHierarchy) {
defaultIntegrations.push(new ViewHierarchy());
}
if (options.enableCaptureFailedRequests) {
defaultIntegrations.push(new HttpClient());
}
}
const defaultIntegrations: false | Integration[] = passedOptions.defaultIntegrations === undefined
? getDefaultIntegrations(options)
: passedOptions.defaultIntegrations;

options.integrations = getIntegrationsToSetup({
integrations: safeFactory(passedOptions.integrations, { loggerMessage: 'The integrations threw an error' }),
7 changes: 7 additions & 0 deletions src/js/utils/environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Platform } from 'react-native';

import { RN_GLOBAL_OBJ } from '../utils/worldwide';
import { ReactNativeLibraries } from './rnlibraries';

@@ -30,6 +32,11 @@ export function isExpo(): boolean {
return RN_GLOBAL_OBJ.expo != null;
}

/** Checks if the current platform is not web */
export function notWeb(): boolean {
return Platform.OS !== 'web';
}

/** Returns Hermes Version if hermes is present in the runtime */
export function getHermesVersion(): string | undefined {
return (
3 changes: 2 additions & 1 deletion test/profiling/integration.test.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import * as Sentry from '../../src/js';
import { HermesProfiling } from '../../src/js/integrations';
import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry';
import { getDebugMetadata } from '../../src/js/profiling/debugid';
import { getDefaultEnvironment, isHermesEnabled } from '../../src/js/utils/environment';
import { getDefaultEnvironment, isHermesEnabled, notWeb } from '../../src/js/utils/environment';
import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide';
import { MOCK_DSN } from '../mockDsn';
import { envelopeItemPayload, envelopeItems } from '../testutils';
@@ -24,6 +24,7 @@ describe('profiling integration', () => {
};

beforeEach(() => {
(notWeb as jest.Mock).mockReturnValue(true);
(isHermesEnabled as jest.Mock).mockReturnValue(true);
mockWrapper.NATIVE.startProfiling.mockReturnValue(true);
mockWrapper.NATIVE.stopProfiling.mockReturnValue({
77 changes: 75 additions & 2 deletions test/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -31,7 +31,6 @@ jest.mock('@sentry/react', () => {
configureScope: mockedGetCurrentHubConfigureScope,
};
}),
defaultIntegrations: [{ name: 'MockedDefaultReactIntegration', setupOnce: jest.fn() }],
};
});

@@ -64,6 +63,7 @@ jest.mock('../src/js/client', () => {
});

jest.mock('../src/js/wrapper');
jest.mock('../src/js/utils/environment');

jest.spyOn(logger, 'error');

@@ -75,6 +75,7 @@ import type { ReactNativeClientOptions } from '../src/js/options';
import { configureScope, flush, init, withScope } from '../src/js/sdk';
import { ReactNativeTracing, ReactNavigationInstrumentation } from '../src/js/tracing';
import { makeNativeTransport } from '../src/js/transports/native';
import { getDefaultEnvironment, notWeb } from '../src/js/utils/environment';
import { firstArg, secondArg } from './testutils';

const mockedInitAndBind = initAndBind as jest.MockedFunction<typeof initAndBind>;
@@ -83,6 +84,11 @@ const usedOptions = (): ClientOptions<BaseTransportOptions> | undefined => {
};

describe('Tests the SDK functionality', () => {
beforeEach(() => {
(NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => true);
(notWeb as jest.Mock).mockImplementation(() => true);
});

afterEach(() => {
jest.clearAllMocks();
});
@@ -186,6 +192,7 @@ describe('Tests the SDK functionality', () => {

describe('environment', () => {
it('detect development environment', () => {
(getDefaultEnvironment as jest.Mock).mockImplementation(() => 'development');
init({
enableNative: true,
});
@@ -618,7 +625,73 @@ describe('Tests the SDK functionality', () => {
const actualIntegrations = actualOptions.integrations;

expect(actualIntegrations).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'MockedDefaultReactIntegration' })]),
expect.arrayContaining([
expect.objectContaining({ name: 'InboundFilters' }),
expect.objectContaining({ name: 'FunctionToString' }),
expect.objectContaining({ name: 'Breadcrumbs' }),
expect.objectContaining({ name: 'Dedupe' }),
expect.objectContaining({ name: 'HttpContext' }),
]),
);
});

it('adds all platform default integrations', () => {
init({});

const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions;
const actualIntegrations = actualOptions.integrations;

expect(actualIntegrations).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'Release' }),
expect.objectContaining({ name: 'EventOrigin' }),
expect.objectContaining({ name: 'SdkInfo' }),
expect.objectContaining({ name: 'ReactNativeInfo' }),
]),
);
});

it('adds web platform specific default integrations', () => {
(notWeb as jest.Mock).mockImplementation(() => false);
init({});

const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions;
const actualIntegrations = actualOptions.integrations;

expect(actualIntegrations).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'TryCatch' }),
expect.objectContaining({ name: 'GlobalHandlers' }),
expect.objectContaining({ name: 'LinkedErrors' }),
]),
);
});

it('does not add native integrations if native disabled', () => {
(NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false);
init({
attachScreenshot: true,
attachViewHierarchy: true,
_experiments: {
profilesSampleRate: 0.7,
},
});

const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions;
const actualIntegrations = actualOptions.integrations;

expect(actualIntegrations).toEqual(
expect.not.arrayContaining([expect.objectContaining({ name: 'DeviceContext' })]),
);
expect(actualIntegrations).toEqual(
expect.not.arrayContaining([expect.objectContaining({ name: 'ModulesLoader' })]),
);
expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Screenshot' })]));
expect(actualIntegrations).toEqual(
expect.not.arrayContaining([expect.objectContaining({ name: 'ViewHierarchy' })]),
);
expect(actualIntegrations).toEqual(
expect.not.arrayContaining([expect.objectContaining({ name: 'HermesProfiling' })]),
);
});
});

0 comments on commit dc8966b

Please sign in to comment.