diff --git a/CHANGELOG.md b/CHANGELOG.md index 51248fd473..dbd1e20772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Add experimental version of `init` with optional `OptionsConfiguration` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) - Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) - Add initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) +- Merge options from file with `Sentry.init` options in JS ([#4510](https://github.com/getsentry/sentry-react-native/pull/4510)) ### Internal diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 3c6fdff90c..3ff40508af 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -19,6 +19,7 @@ import { useEncodePolyfill } from './transports/encodePolyfill'; import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native'; import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer } from './utils/environment'; import { safeFactory, safeTracesSampler } from './utils/safe'; +import { RN_GLOBAL_OBJ } from './utils/worldwide'; import { NATIVE } from './wrapper'; const DEFAULT_OPTIONS: ReactNativeOptions = { @@ -47,12 +48,17 @@ export function init(passedOptions: ReactNativeOptions): void { return; } - const maxQueueSize = passedOptions.maxQueueSize + const userOptions = { + ...RN_GLOBAL_OBJ.__SENTRY_OPTIONS__, + ...passedOptions, + }; + + const maxQueueSize = userOptions.maxQueueSize // eslint-disable-next-line deprecation/deprecation - ?? passedOptions.transportOptions?.bufferSize + ?? userOptions.transportOptions?.bufferSize ?? DEFAULT_OPTIONS.maxQueueSize; - const enableNative = passedOptions.enableNative === undefined || passedOptions.enableNative + const enableNative = userOptions.enableNative === undefined || userOptions.enableNative ? NATIVE.isNativeAvailable() : false; @@ -75,11 +81,11 @@ export function init(passedOptions: ReactNativeOptions): void { return `${dsnComponents.protocol}://${dsnComponents.host}${port}`; }; - const userBeforeBreadcrumb = safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); + const userBeforeBreadcrumb = safeFactory(userOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); // Exclude Dev Server and Sentry Dsn request from Breadcrumbs const devServerUrl = getDevServer()?.url; - const dsn = getURLFromDSN(passedOptions.dsn); + const dsn = getURLFromDSN(userOptions.dsn); const defaultBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb | null => { const type = breadcrumb.type || ''; const url = typeof breadcrumb.data?.url === 'string' ? breadcrumb.data.url : ''; @@ -103,26 +109,34 @@ export function init(passedOptions: ReactNativeOptions): void { const options: ReactNativeClientOptions = { ...DEFAULT_OPTIONS, - ...passedOptions, + ...userOptions, enableNative, - enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger), + enableNativeNagger: shouldEnableNativeNagger(userOptions.enableNativeNagger), // If custom transport factory fails the SDK won't initialize - transport: passedOptions.transport + transport: userOptions.transport || makeNativeTransportFactory({ enableNative, }) || makeFetchTransport, transportOptions: { ...DEFAULT_OPTIONS.transportOptions, - ...(passedOptions.transportOptions ?? {}), + ...(userOptions.transportOptions ?? {}), bufferSize: maxQueueSize, }, maxQueueSize, integrations: [], - stackParser: stackParserFromStackParserOptions(passedOptions.stackParser || defaultStackParser), + stackParser: stackParserFromStackParserOptions(userOptions.stackParser || defaultStackParser), beforeBreadcrumb: chainedBeforeBreadcrumb, - initialScope: safeFactory(passedOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), + initialScope: safeFactory(userOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), }; + + if (!('autoInitializeNativeSdk' in userOptions) && RN_GLOBAL_OBJ.__SENTRY_OPTIONS__) { + // We expect users to use the file options only in combination with manual native initialization + // eslint-disable-next-line no-console + console.info('Initializing Sentry JS with the options file. Expecting manual native initialization before JS. Native will not be initialized automatically.'); + options.autoInitializeNativeSdk = false; + } + if ('tracesSampler' in options) { options.tracesSampler = safeTracesSampler(options.tracesSampler); } @@ -131,12 +145,12 @@ export function init(passedOptions: ReactNativeOptions): void { options.environment = getDefaultEnvironment(); } - const defaultIntegrations: false | Integration[] = passedOptions.defaultIntegrations === undefined + const defaultIntegrations: false | Integration[] = userOptions.defaultIntegrations === undefined ? getDefaultIntegrations(options) - : passedOptions.defaultIntegrations; + : userOptions.defaultIntegrations; options.integrations = getIntegrationsToSetup({ - integrations: safeFactory(passedOptions.integrations, { loggerMessage: 'The integrations threw an error' }), + integrations: safeFactory(userOptions.integrations, { loggerMessage: 'The integrations threw an error' }), defaultIntegrations, }); initAndBind(ReactNativeClient, options); @@ -145,6 +159,10 @@ export function init(passedOptions: ReactNativeOptions): void { logger.info('Offline caching, native errors features are not available in Expo Go.'); logger.info('Use EAS Build / Native Release Build to test these features.'); } + + if (RN_GLOBAL_OBJ.__SENTRY_OPTIONS__) { + logger.info('Sentry JS initialized with options from the options file.'); + } } /** diff --git a/packages/core/src/js/utils/worldwide.ts b/packages/core/src/js/utils/worldwide.ts index c1a4ae5dbb..0dc265763d 100644 --- a/packages/core/src/js/utils/worldwide.ts +++ b/packages/core/src/js/utils/worldwide.ts @@ -2,6 +2,7 @@ import type { InternalGlobal } from '@sentry/core'; import { GLOBAL_OBJ } from '@sentry/core'; import type { ErrorUtils } from 'react-native/types'; +import type { ReactNativeOptions } from '../options'; import type { ExpoGlobalObject } from './expoglobalobject'; /** Internal Global object interface with common and Sentry specific properties */ @@ -25,6 +26,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { __BUNDLE_START_TIME__?: number; nativePerformanceNow?: () => number; TextEncoder?: TextEncoder; + __SENTRY_OPTIONS__?: ReactNativeOptions; } type TextEncoder = { diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index afd6137c8a..0e64264899 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -1,13 +1,15 @@ -import type { BaseTransportOptions, Breadcrumb, BreadcrumbHint, ClientOptions, Integration, Scope } from '@sentry/core'; +import type { Breadcrumb, BreadcrumbHint, Integration, Scope } from '@sentry/core'; import { initAndBind, logger } from '@sentry/core'; import { makeFetchTransport } from '@sentry/react'; import { getDevServer } from '../src/js/integrations/debugsymbolicatorutils'; +import type { ReactNativeClientOptions } from '../src/js/options'; import { init, withScope } from '../src/js/sdk'; import type { ReactNativeTracingIntegration } from '../src/js/tracing'; import { REACT_NATIVE_TRACING_INTEGRATION_NAME, reactNativeTracingIntegration } from '../src/js/tracing'; import { makeNativeTransport } from '../src/js/transports/native'; import { getDefaultEnvironment, isExpoGo, notWeb } from '../src/js/utils/environment'; +import { RN_GLOBAL_OBJ } from '../src/js/utils/worldwide'; import { NATIVE } from './mockWrapper'; import { firstArg, secondArg } from './testutils'; @@ -109,6 +111,60 @@ describe('Tests the SDK functionality', () => { }); }); + describe('initialization from sentry.options.json', () => { + it('initializes without __SENTRY_OPTIONS__', () => { + delete RN_GLOBAL_OBJ.__SENTRY_OPTIONS__; + init({}); + expect(initAndBind).toHaveBeenCalledOnce(); + }); + + it('adds options from __SENTRY_OPTIONS__', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = { + dsn: 'https://key@example.io/value', + }; + init({}); + expect(usedOptions()?.dsn).toBe('https://key@example.io/value'); + }); + + it('options init override options from __SENTRY_OPTIONS__', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = { + dsn: 'https://key@example.io/file', + }; + init({ + dsn: 'https://key@example.io/code', + }); + expect(usedOptions()?.dsn).toBe('https://key@example.io/code'); + }); + + it('initializing with __SENTRY_OPTIONS__ disabled native auto initialization', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({}); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(false); + }); + + it('initializing without __SENTRY_OPTIONS__ enables native auto initialization', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = undefined; + init({}); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(true); + }); + + it('initializing with __SENTRY_OPTIONS__ keeps native auto initialization true if set', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({ + autoInitializeNativeSdk: true, + }); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(true); + }); + + it('initializing with __SENTRY_OPTIONS__ keeps native auto initialization false if set', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({ + autoInitializeNativeSdk: false, + }); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(false); + }); + }); + describe('environment', () => { it('detect development environment', () => { (getDefaultEnvironment as jest.Mock).mockImplementation(() => 'development'); @@ -173,7 +229,6 @@ describe('Tests the SDK functionality', () => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); init({}); expect(NATIVE.isNativeAvailable).toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); expect(usedOptions()?.transport).toEqual(makeFetchTransport); }); @@ -182,7 +237,6 @@ describe('Tests the SDK functionality', () => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); init({ enableNative: true }); expect(NATIVE.isNativeAvailable).toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); expect(usedOptions()?.transport).toEqual(makeFetchTransport); }); @@ -191,7 +245,6 @@ describe('Tests the SDK functionality', () => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); init({ enableNative: false }); expect(NATIVE.isNativeAvailable).not.toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); expect(usedOptions()?.transport).toEqual(makeFetchTransport); }); @@ -204,7 +257,6 @@ describe('Tests the SDK functionality', () => { }); expect(usedOptions()?.transport).toEqual(mockTransport); expect(NATIVE.isNativeAvailable).toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); }); }); @@ -1058,7 +1110,7 @@ function createMockedIntegration({ name }: { name?: string } = {}): Integration }; } -function usedOptions(): ClientOptions | undefined { +function usedOptions(): ReactNativeClientOptions | undefined { return (initAndBind as jest.MockedFunction).mock.calls[0]?.[secondArg]; }