From 9d63f2508aea252dab2a505b9775d935474f6d5a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 14:44:40 +0200 Subject: [PATCH 1/6] feat(tracing): Add Sentry.NavigationContainer wrapper for React Navigation Drop-in replacement for NavigationContainer that automatically wires up reactNavigationIntegration, removing the manual ref/onReady boilerplate that is a recurring source of misconfiguration. Closes #6065 Co-Authored-By: Claude Opus 4.6 --- packages/core/etc/sentry-react-native.api.md | 3 + packages/core/src/js/NavigationContainer.tsx | 72 +++++++++++ packages/core/src/js/index.ts | 1 + packages/core/src/js/reactNavigationImport.ts | 25 ++++ .../test/NavigationContainer.missing.test.tsx | 32 +++++ .../core/test/NavigationContainer.test.tsx | 121 ++++++++++++++++++ 6 files changed, 254 insertions(+) create mode 100644 packages/core/src/js/NavigationContainer.tsx create mode 100644 packages/core/src/js/reactNavigationImport.ts create mode 100644 packages/core/test/NavigationContainer.missing.test.tsx create mode 100644 packages/core/test/NavigationContainer.test.tsx diff --git a/packages/core/etc/sentry-react-native.api.md b/packages/core/etc/sentry-react-native.api.md index 3957c06956..c51b04bb83 100644 --- a/packages/core/etc/sentry-react-native.api.md +++ b/packages/core/etc/sentry-react-native.api.md @@ -491,6 +491,9 @@ export interface NativeLogEntry { // @public export const nativeReleaseIntegration: () => Integration; +// @public +export const NavigationContainer: React_2.ForwardRefExoticComponent, "ref"> & React_2.RefAttributes>; + export { OpenAiClient } export { OpenAiOptions } diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx new file mode 100644 index 0000000000..e4bfcf27f4 --- /dev/null +++ b/packages/core/src/js/NavigationContainer.tsx @@ -0,0 +1,72 @@ +import { debug, getClient } from '@sentry/core'; +import * as React from 'react'; + +import { getNavigationContainerComponent } from './reactNavigationImport'; +import { getReactNavigationIntegration } from './tracing/reactnavigation'; + +let _warnedMissing = false; +let _warnedNoIntegration = false; + +/** + * Drop-in replacement for `NavigationContainer` from `@react-navigation/native` + * that automatically wires up Sentry's `reactNavigationIntegration`. + * + * Sentry registers the navigation container before the user-provided `onReady` + * callback fires, so navigation spans are captured from the first route. + * + * @example + * ```jsx + * + * + * ... + * + * + * ``` + */ +export const NavigationContainer = React.forwardRef>((props, forwardedRef) => { + const { onReady: userOnReady, ...restProps } = props; + const RealNavigationContainer = getNavigationContainerComponent(); + + const internalRef = React.useRef(null); + + const mergedRef = React.useCallback( + (instance: unknown) => { + internalRef.current = instance; + if (typeof forwardedRef === 'function') { + forwardedRef(instance); + } else if (forwardedRef != null) { + (forwardedRef as React.MutableRefObject).current = instance; + } + }, + [forwardedRef], + ); + + const onReady = React.useCallback(() => { + const client = getClient(); + if (client) { + const integration = getReactNavigationIntegration(client); + if (integration) { + integration.registerNavigationContainer(internalRef); + } else if (!_warnedNoIntegration) { + _warnedNoIntegration = true; + debug.log( + '[Sentry] NavigationContainer: reactNavigationIntegration is not registered. Navigation spans will not be captured.', + ); + } + } + + if (typeof userOnReady === 'function') { + (userOnReady as () => void)(); + } + }, [userOnReady]); + + if (!RealNavigationContainer) { + if (!_warnedMissing) { + _warnedMissing = true; + debug.warn('[Sentry] NavigationContainer requires @react-navigation/native to be installed.'); + } + return <>{restProps.children as React.ReactNode}; + } + + return ; +}); diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 81734f21c5..71bb583e85 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -100,6 +100,7 @@ export { ReactNativeClient } from './client'; export { init, wrap, nativeCrash, flush, close, withScope, crashedLastRun, appLoaded } from './sdk'; export { TouchEventBoundary, withTouchEventBoundary } from './touchevents'; +export { NavigationContainer } from './NavigationContainer'; export { GlobalErrorBoundary, withGlobalErrorBoundary } from './GlobalErrorBoundary'; export type { GlobalErrorBoundaryProps } from './GlobalErrorBoundary'; diff --git a/packages/core/src/js/reactNavigationImport.ts b/packages/core/src/js/reactNavigationImport.ts new file mode 100644 index 0000000000..23eb707153 --- /dev/null +++ b/packages/core/src/js/reactNavigationImport.ts @@ -0,0 +1,25 @@ +import type * as React from 'react'; + +type NavigationContainerComponent = React.ComponentType>; + +let _cached: NavigationContainerComponent | null | undefined; + +/** + * @returns NavigationContainer from @react-navigation/native or null if not installed. + * The result is cached after the first call. + */ +export function getNavigationContainerComponent(): NavigationContainerComponent | null { + if (_cached !== undefined) { + return _cached; + } + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const mod = require('@react-navigation/native') as { + NavigationContainer?: NavigationContainerComponent; + }; + _cached = mod?.NavigationContainer ?? null; + } catch { + _cached = null; + } + return _cached; +} diff --git a/packages/core/test/NavigationContainer.missing.test.tsx b/packages/core/test/NavigationContainer.missing.test.tsx new file mode 100644 index 0000000000..e4cdb40b50 --- /dev/null +++ b/packages/core/test/NavigationContainer.missing.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +import { NavigationContainer } from '../src/js/NavigationContainer'; + +const mockDebugWarn = jest.fn(); + +jest.mock('@sentry/core', () => ({ + getClient: () => undefined, + debug: { get log() { return jest.fn(); }, get warn() { return mockDebugWarn; } }, +})); + +jest.mock('../src/js/tracing/reactnavigation', () => ({ + getReactNavigationIntegration: () => undefined, +})); + +jest.mock('../src/js/reactNavigationImport', () => ({ + getNavigationContainerComponent: () => null, +})); + +describe('NavigationContainer without @react-navigation/native', () => { + it('renders children directly and warns', () => { + const { getByText } = render( + + Fallback Content + , + ); + expect(getByText('Fallback Content')).toBeTruthy(); + expect(mockDebugWarn).toHaveBeenCalled(); + }); +}); diff --git a/packages/core/test/NavigationContainer.test.tsx b/packages/core/test/NavigationContainer.test.tsx new file mode 100644 index 0000000000..098b175862 --- /dev/null +++ b/packages/core/test/NavigationContainer.test.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { Text, View } from 'react-native'; +import { render } from '@testing-library/react-native'; + +import { NavigationContainer } from '../src/js/NavigationContainer'; + +const mockRegisterNavigationContainer = jest.fn(); +const mockGetClient = jest.fn(); +const mockDebugLog = jest.fn(); +const mockDebugWarn = jest.fn(); + +jest.mock('@sentry/core', () => ({ + getClient: (...args: unknown[]) => mockGetClient(...args), + debug: { get log() { return mockDebugLog; }, get warn() { return mockDebugWarn; } }, +})); + +jest.mock('../src/js/tracing/reactnavigation', () => ({ + getReactNavigationIntegration: (client: unknown) => { + if (client) { + return { registerNavigationContainer: mockRegisterNavigationContainer }; + } + return undefined; + }, +})); + +const MockNavigationContainerComponent = React.forwardRef>((props, ref) => { + const { onReady, children, ...rest } = props; + React.useEffect(() => { + if (typeof onReady === 'function') { + (onReady as () => void)(); + } + }, [onReady]); + return ( + } testID="mock-navigation-container" {...rest}> + {children as React.ReactNode} + + ); +}); + +jest.mock('../src/js/reactNavigationImport', () => ({ + getNavigationContainerComponent: () => MockNavigationContainerComponent, +})); + +describe('NavigationContainer', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetClient.mockReturnValue({ getIntegrationByName: jest.fn() }); + }); + + it('renders children through to the underlying NavigationContainer', () => { + const { getByText } = render( + + Child Content + , + ); + expect(getByText('Child Content')).toBeTruthy(); + }); + + it('calls registerNavigationContainer on ready', () => { + render( + + App + , + ); + expect(mockRegisterNavigationContainer).toHaveBeenCalledTimes(1); + expect(mockRegisterNavigationContainer).toHaveBeenCalledWith(expect.objectContaining({ current: expect.anything() })); + }); + + it('forwards ref to the underlying NavigationContainer', () => { + const ref = React.createRef(); + render( + + App + , + ); + expect(ref.current).toBeTruthy(); + }); + + it('calls registerNavigationContainer before user onReady', () => { + const callOrder: string[] = []; + mockRegisterNavigationContainer.mockImplementation(() => callOrder.push('sentry')); + const userOnReady = jest.fn(() => callOrder.push('user')); + render( + + App + , + ); + expect(callOrder).toEqual(['sentry', 'user']); + }); + + it('chains user-provided onReady callback', () => { + const userOnReady = jest.fn(); + render( + + App + , + ); + expect(userOnReady).toHaveBeenCalledTimes(1); + expect(mockRegisterNavigationContainer).toHaveBeenCalledTimes(1); + }); + + it('no-ops when client is not available', () => { + mockGetClient.mockReturnValue(undefined); + render( + + App + , + ); + expect(mockRegisterNavigationContainer).not.toHaveBeenCalled(); + }); + + it('passes through all props to NavigationContainer', () => { + const { getByTestId } = render( + + App + , + ); + const container = getByTestId('mock-navigation-container'); + expect(container.props.accessibilityLabel).toBe('nav'); + }); +}); From 8fb22abae748954300e05b8ce4ed145426b9b4e6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 17:20:46 +0200 Subject: [PATCH 2/6] feat(core): Add changelog, fix lint, update sample apps for NavigationContainer Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 ++++++ .../test/NavigationContainer.missing.test.tsx | 11 +++++++++-- packages/core/test/NavigationContainer.test.tsx | 15 ++++++++++++--- samples/react-native-macos/src/App.tsx | 14 ++------------ samples/react-native/src/App.tsx | 16 +++------------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b71240024..4c22089f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Features + +- Add `Sentry.NavigationContainer` drop-in wrapper for React Navigation ([#6199](https://github.com/getsentry/sentry-react-native/pull/6199)) + ## 8.12.0 ### Features diff --git a/packages/core/test/NavigationContainer.missing.test.tsx b/packages/core/test/NavigationContainer.missing.test.tsx index e4cdb40b50..0d12ef6594 100644 --- a/packages/core/test/NavigationContainer.missing.test.tsx +++ b/packages/core/test/NavigationContainer.missing.test.tsx @@ -1,6 +1,6 @@ +import { render } from '@testing-library/react-native'; import * as React from 'react'; import { Text } from 'react-native'; -import { render } from '@testing-library/react-native'; import { NavigationContainer } from '../src/js/NavigationContainer'; @@ -8,7 +8,14 @@ const mockDebugWarn = jest.fn(); jest.mock('@sentry/core', () => ({ getClient: () => undefined, - debug: { get log() { return jest.fn(); }, get warn() { return mockDebugWarn; } }, + debug: { + get log() { + return jest.fn(); + }, + get warn() { + return mockDebugWarn; + }, + }, })); jest.mock('../src/js/tracing/reactnavigation', () => ({ diff --git a/packages/core/test/NavigationContainer.test.tsx b/packages/core/test/NavigationContainer.test.tsx index 098b175862..bb6055834a 100644 --- a/packages/core/test/NavigationContainer.test.tsx +++ b/packages/core/test/NavigationContainer.test.tsx @@ -1,6 +1,6 @@ +import { render } from '@testing-library/react-native'; import * as React from 'react'; import { Text, View } from 'react-native'; -import { render } from '@testing-library/react-native'; import { NavigationContainer } from '../src/js/NavigationContainer'; @@ -11,7 +11,14 @@ const mockDebugWarn = jest.fn(); jest.mock('@sentry/core', () => ({ getClient: (...args: unknown[]) => mockGetClient(...args), - debug: { get log() { return mockDebugLog; }, get warn() { return mockDebugWarn; } }, + debug: { + get log() { + return mockDebugLog; + }, + get warn() { + return mockDebugWarn; + }, + }, })); jest.mock('../src/js/tracing/reactnavigation', () => ({ @@ -63,7 +70,9 @@ describe('NavigationContainer', () => { , ); expect(mockRegisterNavigationContainer).toHaveBeenCalledTimes(1); - expect(mockRegisterNavigationContainer).toHaveBeenCalledWith(expect.objectContaining({ current: expect.anything() })); + expect(mockRegisterNavigationContainer).toHaveBeenCalledWith( + expect.objectContaining({ current: expect.anything() }), + ); }); it('forwards ref to the underlying NavigationContainer', () => { diff --git a/samples/react-native-macos/src/App.tsx b/samples/react-native-macos/src/App.tsx index a43fd6793b..0e43c01c29 100644 --- a/samples/react-native-macos/src/App.tsx +++ b/samples/react-native-macos/src/App.tsx @@ -1,8 +1,4 @@ import React from 'react'; -import { - NavigationContainer, - NavigationContainerRef, -} from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import Animated, { @@ -172,14 +168,8 @@ const TabTwoStack = Sentry.withProfiler( ); function BottomTabs() { - const navigation = React.useRef>(null); - return ( - { - reactNavigationIntegration.registerNavigationContainer(navigation); - }}> + - + ); } diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 5501974645..e6e6971ea6 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -2,11 +2,7 @@ import React from 'react'; import { Ionicons } from '@react-native-vector-icons/ionicons'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { - NavigationContainer, - NavigationContainerRef, - TypedNavigator, -} from '@react-navigation/native'; +import { TypedNavigator } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createStackNavigator } from '@react-navigation/stack'; import * as Sentry from '@sentry/react-native'; @@ -234,14 +230,8 @@ function BottomTabsNavigator() { } function RootNavigationContainer() { - const navigation = React.useRef>(null); - return ( - { - reactNavigationIntegration.registerNavigationContainer(navigation); - }}> + - + ); } From 240b1fb3d5c2221e98da8e699d0dcb30ce616ed4 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 18:37:24 +0200 Subject: [PATCH 3/6] fix(core): Add warnings for missing client and missing integration in NavigationContainer Co-Authored-By: Claude Opus 4.6 --- packages/core/src/js/NavigationContainer.tsx | 10 +++++++- .../core/test/NavigationContainer.test.tsx | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx index e4bfcf27f4..5f83369cfe 100644 --- a/packages/core/src/js/NavigationContainer.tsx +++ b/packages/core/src/js/NavigationContainer.tsx @@ -5,6 +5,7 @@ import { getNavigationContainerComponent } from './reactNavigationImport'; import { getReactNavigationIntegration } from './tracing/reactnavigation'; let _warnedMissing = false; +let _warnedNoClient = false; let _warnedNoIntegration = false; /** @@ -43,7 +44,14 @@ export const NavigationContainer = React.forwardRef { const client = getClient(); - if (client) { + if (!client) { + if (!_warnedNoClient) { + _warnedNoClient = true; + debug.warn( + '[Sentry] NavigationContainer: Sentry is not initialized. Call Sentry.init() before mounting NavigationContainer.', + ); + } + } else { const integration = getReactNavigationIntegration(client); if (integration) { integration.registerNavigationContainer(internalRef); diff --git a/packages/core/test/NavigationContainer.test.tsx b/packages/core/test/NavigationContainer.test.tsx index bb6055834a..d5f358459e 100644 --- a/packages/core/test/NavigationContainer.test.tsx +++ b/packages/core/test/NavigationContainer.test.tsx @@ -21,13 +21,9 @@ jest.mock('@sentry/core', () => ({ }, })); +const mockGetReactNavigationIntegration = jest.fn(); jest.mock('../src/js/tracing/reactnavigation', () => ({ - getReactNavigationIntegration: (client: unknown) => { - if (client) { - return { registerNavigationContainer: mockRegisterNavigationContainer }; - } - return undefined; - }, + getReactNavigationIntegration: (...args: unknown[]) => mockGetReactNavigationIntegration(...args), })); const MockNavigationContainerComponent = React.forwardRef>((props, ref) => { @@ -52,6 +48,9 @@ describe('NavigationContainer', () => { beforeEach(() => { jest.clearAllMocks(); mockGetClient.mockReturnValue({ getIntegrationByName: jest.fn() }); + mockGetReactNavigationIntegration.mockReturnValue({ + registerNavigationContainer: mockRegisterNavigationContainer, + }); }); it('renders children through to the underlying NavigationContainer', () => { @@ -108,7 +107,7 @@ describe('NavigationContainer', () => { expect(mockRegisterNavigationContainer).toHaveBeenCalledTimes(1); }); - it('no-ops when client is not available', () => { + it('warns and skips registration when client is not available', () => { mockGetClient.mockReturnValue(undefined); render( @@ -116,6 +115,18 @@ describe('NavigationContainer', () => { , ); expect(mockRegisterNavigationContainer).not.toHaveBeenCalled(); + expect(mockDebugWarn).toHaveBeenCalledWith(expect.stringContaining('Sentry is not initialized')); + }); + + it('logs when client exists but reactNavigationIntegration is not registered', () => { + mockGetReactNavigationIntegration.mockReturnValue(undefined); + render( + + App + , + ); + expect(mockRegisterNavigationContainer).not.toHaveBeenCalled(); + expect(mockDebugLog).toHaveBeenCalledWith(expect.stringContaining('reactNavigationIntegration is not registered')); }); it('passes through all props to NavigationContainer', () => { From fc3fa414355d947fd18746e8868ddc50064fc84a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 18:50:17 +0200 Subject: [PATCH 4/6] fix(core): Call onReady in NavigationContainer fallback path Co-Authored-By: Claude Opus 4.6 --- packages/core/src/js/NavigationContainer.tsx | 6 ++++++ .../core/test/NavigationContainer.missing.test.tsx | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx index 5f83369cfe..1fbd7129b0 100644 --- a/packages/core/src/js/NavigationContainer.tsx +++ b/packages/core/src/js/NavigationContainer.tsx @@ -68,6 +68,12 @@ export const NavigationContainer = React.forwardRef { + if (!RealNavigationContainer && typeof userOnReady === 'function') { + (userOnReady as () => void)(); + } + }, [RealNavigationContainer, userOnReady]); + if (!RealNavigationContainer) { if (!_warnedMissing) { _warnedMissing = true; diff --git a/packages/core/test/NavigationContainer.missing.test.tsx b/packages/core/test/NavigationContainer.missing.test.tsx index 0d12ef6594..98c8383cc5 100644 --- a/packages/core/test/NavigationContainer.missing.test.tsx +++ b/packages/core/test/NavigationContainer.missing.test.tsx @@ -36,4 +36,14 @@ describe('NavigationContainer without @react-navigation/native', () => { expect(getByText('Fallback Content')).toBeTruthy(); expect(mockDebugWarn).toHaveBeenCalled(); }); + + it('calls onReady in the fallback path', () => { + const userOnReady = jest.fn(); + render( + + Fallback Content + , + ); + expect(userOnReady).toHaveBeenCalledTimes(1); + }); }); From 7af354af6c753cdc7791dd850aec996e623946a4 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 22:04:04 +0200 Subject: [PATCH 5/6] fix(core): Remove useEffect from NavigationContainer, document fallback behavior Co-Authored-By: Claude Opus 4.6 --- packages/core/src/js/NavigationContainer.tsx | 9 +++------ .../core/test/NavigationContainer.missing.test.tsx | 10 ---------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx index 1fbd7129b0..7c6481d36e 100644 --- a/packages/core/src/js/NavigationContainer.tsx +++ b/packages/core/src/js/NavigationContainer.tsx @@ -15,6 +15,9 @@ let _warnedNoIntegration = false; * Sentry registers the navigation container before the user-provided `onReady` * callback fires, so navigation spans are captured from the first route. * + * If `@react-navigation/native` is not installed, children are rendered directly + * and `onReady` is not called. + * * @example * ```jsx * @@ -68,12 +71,6 @@ export const NavigationContainer = React.forwardRef { - if (!RealNavigationContainer && typeof userOnReady === 'function') { - (userOnReady as () => void)(); - } - }, [RealNavigationContainer, userOnReady]); - if (!RealNavigationContainer) { if (!_warnedMissing) { _warnedMissing = true; diff --git a/packages/core/test/NavigationContainer.missing.test.tsx b/packages/core/test/NavigationContainer.missing.test.tsx index 98c8383cc5..0d12ef6594 100644 --- a/packages/core/test/NavigationContainer.missing.test.tsx +++ b/packages/core/test/NavigationContainer.missing.test.tsx @@ -36,14 +36,4 @@ describe('NavigationContainer without @react-navigation/native', () => { expect(getByText('Fallback Content')).toBeTruthy(); expect(mockDebugWarn).toHaveBeenCalled(); }); - - it('calls onReady in the fallback path', () => { - const userOnReady = jest.fn(); - render( - - Fallback Content - , - ); - expect(userOnReady).toHaveBeenCalledTimes(1); - }); }); From 134cc57b6fb5de061c5f93c39290b753e3f334f2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 22:08:26 +0200 Subject: [PATCH 6/6] fix(core): Wrap SDK logic in try/catch in NavigationContainer onReady Co-Authored-By: Claude Opus 4.6 --- packages/core/src/js/NavigationContainer.tsx | 38 +++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx index 7c6481d36e..31671edb2d 100644 --- a/packages/core/src/js/NavigationContainer.tsx +++ b/packages/core/src/js/NavigationContainer.tsx @@ -46,24 +46,28 @@ export const NavigationContainer = React.forwardRef { - const client = getClient(); - if (!client) { - if (!_warnedNoClient) { - _warnedNoClient = true; - debug.warn( - '[Sentry] NavigationContainer: Sentry is not initialized. Call Sentry.init() before mounting NavigationContainer.', - ); - } - } else { - const integration = getReactNavigationIntegration(client); - if (integration) { - integration.registerNavigationContainer(internalRef); - } else if (!_warnedNoIntegration) { - _warnedNoIntegration = true; - debug.log( - '[Sentry] NavigationContainer: reactNavigationIntegration is not registered. Navigation spans will not be captured.', - ); + try { + const client = getClient(); + if (!client) { + if (!_warnedNoClient) { + _warnedNoClient = true; + debug.warn( + '[Sentry] NavigationContainer: Sentry is not initialized. Call Sentry.init() before mounting NavigationContainer.', + ); + } + } else { + const integration = getReactNavigationIntegration(client); + if (integration) { + integration.registerNavigationContainer(internalRef); + } else if (!_warnedNoIntegration) { + _warnedNoIntegration = true; + debug.log( + '[Sentry] NavigationContainer: reactNavigationIntegration is not registered. Navigation spans will not be captured.', + ); + } } + } catch { + // SDK failures must never break the host app } if (typeof userOnReady === 'function') {