diff --git a/CHANGELOG.md b/CHANGELOG.md index fc28054845..0154e2a0a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Note: Expo Router span/breadcrumb attributes that may contain user identifiers (`route.href`, `route.params`, and concrete pathnames derived from string hrefs such as `/users/42`) are now gated behind `sendDefaultPii`. When `sendDefaultPii` is off (the default), prefetch spans for string hrefs use `route.name: 'unknown'` and omit `route.href`. Templated object hrefs (e.g. `{ pathname: '/users/[id]' }`) are unaffected. - Warn when Gradle resolves `sentry-android` to a version incompatible with the SDK ([#6238](https://github.com/getsentry/sentry-react-native/pull/6238)) - Attach the active TurboModule method to native crash reports as `contexts.turbo_module` + `turbo_module.name` / `turbo_module.method` tags ([#6227](https://github.com/getsentry/sentry-react-native/pull/6227)) +- Warn during dev builds when multiple versions of Sentry JS SDK are detected ([#6269](https://github.com/getsentry/sentry-react-native/pull/6269)) ### Fixes diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 84cee9a088..9e4a877d84 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -31,6 +31,7 @@ import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/na import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer, isWeb } from './utils/environment'; import { getDefaultRelease } from './utils/release'; import { safeFactory, safeTracesSampler } from './utils/safe'; +import { checkSentryJsSdkVersionMismatch } from './utils/sdkVersionCheck'; import { RN_GLOBAL_OBJ } from './utils/worldwide'; import { NATIVE } from './wrapper'; @@ -172,6 +173,7 @@ export function init(passedOptions: ReactNativeOptions): void { defaultIntegrations, }); initAndBind(ReactNativeClient, options); + checkSentryJsSdkVersionMismatch(); if (isExpoGo()) { debug.log('Offline caching, native errors features are not available in Expo Go.'); diff --git a/packages/core/src/js/utils/sdkVersionCheck.ts b/packages/core/src/js/utils/sdkVersionCheck.ts new file mode 100644 index 0000000000..15098a4f82 --- /dev/null +++ b/packages/core/src/js/utils/sdkVersionCheck.ts @@ -0,0 +1,30 @@ +import { debug, getMainCarrier, SDK_VERSION as CORE_SDK_VERSION } from '@sentry/core'; + +export function checkSentryJsSdkVersionMismatch(): void { + if (!__DEV__) { + return; + } + + try { + const carrier = getMainCarrier(); + const sentryCarrier = carrier.__SENTRY__; + if (!sentryCarrier) { + return; + } + + const versions = Object.keys(sentryCarrier).filter(key => key !== CORE_SDK_VERSION && key !== 'version'); + if (versions.length === 0) { + return; + } + + debug.warn( + `Multiple versions of Sentry JavaScript SDKs were detected in your application. ` + + `Found versions: ${[CORE_SDK_VERSION, ...versions].join(', ')}. ` + + `This may cause unexpected behavior. ` + + `Ensure all Sentry packages use the same version. ` + + `See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.`, + ); + } catch (_e) { + // Ignore errors from version check + } +} diff --git a/packages/core/test/utils/sdkVersionCheck.test.ts b/packages/core/test/utils/sdkVersionCheck.test.ts new file mode 100644 index 0000000000..954e078ce2 --- /dev/null +++ b/packages/core/test/utils/sdkVersionCheck.test.ts @@ -0,0 +1,93 @@ +const mockDebugWarn = jest.fn(); +const mockCarrier: Record = {}; + +jest.mock('@sentry/core', () => ({ + debug: { + get warn() { + return mockDebugWarn; + }, + }, + getMainCarrier: () => mockCarrier, + SDK_VERSION: '10.0.0', +})); + +import { checkSentryJsSdkVersionMismatch } from '../../src/js/utils/sdkVersionCheck'; + +describe('checkSentryJsSdkVersionMismatch', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete mockCarrier.__SENTRY__; + }); + + it('does not warn when only one SDK version is present', () => { + mockCarrier.__SENTRY__ = { '10.0.0': {} }; + + checkSentryJsSdkVersionMismatch(); + + expect(mockDebugWarn).not.toHaveBeenCalled(); + }); + + it('warns when multiple SDK versions are present', () => { + mockCarrier.__SENTRY__ = { '10.0.0': {}, '9.0.0': {} }; + + checkSentryJsSdkVersionMismatch(); + + expect(mockDebugWarn).toHaveBeenCalledTimes(1); + expect(mockDebugWarn).toHaveBeenCalledWith(expect.stringContaining('Multiple versions of Sentry JavaScript SDKs')); + expect(mockDebugWarn).toHaveBeenCalledWith(expect.stringContaining('9.0.0')); + expect(mockDebugWarn).toHaveBeenCalledWith(expect.stringContaining('10.0.0')); + }); + + it('warns when more than two SDK versions are present', () => { + mockCarrier.__SENTRY__ = { '10.0.0': {}, '9.0.0': {}, '8.0.0': {} }; + + checkSentryJsSdkVersionMismatch(); + + expect(mockDebugWarn).toHaveBeenCalledTimes(1); + expect(mockDebugWarn).toHaveBeenCalledWith(expect.stringContaining('9.0.0')); + expect(mockDebugWarn).toHaveBeenCalledWith(expect.stringContaining('8.0.0')); + }); + + it('does not warn when carrier has the version key set by @sentry/core', () => { + mockCarrier.__SENTRY__ = { '10.0.0': {}, version: '10.0.0' }; + + checkSentryJsSdkVersionMismatch(); + + expect(mockDebugWarn).not.toHaveBeenCalled(); + }); + + it('does not warn when __SENTRY__ is not set', () => { + checkSentryJsSdkVersionMismatch(); + + expect(mockDebugWarn).not.toHaveBeenCalled(); + }); + + it('does not warn in production builds', () => { + // @ts-expect-error -- __DEV__ is a RN global + globalThis.__DEV__ = false; + try { + mockCarrier.__SENTRY__ = { '10.0.0': {}, '9.0.0': {} }; + + checkSentryJsSdkVersionMismatch(); + + expect(mockDebugWarn).not.toHaveBeenCalled(); + } finally { + // @ts-expect-error -- __DEV__ is a RN global + globalThis.__DEV__ = true; + } + }); + + it('does not throw on unexpected errors', () => { + Object.defineProperty(mockCarrier, '__SENTRY__', { + get() { + throw new Error('test error'); + }, + configurable: true, + }); + + expect(() => checkSentryJsSdkVersionMismatch()).not.toThrow(); + expect(mockDebugWarn).not.toHaveBeenCalled(); + + delete mockCarrier.__SENTRY__; + }); +});