From 9d63f2508aea252dab2a505b9775d935474f6d5a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 14:44:40 +0200 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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') { From 254298f514a67edcba5c16997d3c6ad5b376768d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 26 May 2026 09:52:01 +0200 Subject: [PATCH 07/10] feat(core): Add TypeScript types to Sentry.NavigationContainer props Co-Authored-By: Claude Opus 4.6 --- packages/core/etc/sentry-react-native.api.md | 89 +++++++++++++++++++- packages/core/src/js/NavigationContainer.tsx | 7 +- packages/core/src/js/index.ts | 2 +- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/core/etc/sentry-react-native.api.md b/packages/core/etc/sentry-react-native.api.md index c10ab0ae6a..f10485b6a0 100644 --- a/packages/core/etc/sentry-react-native.api.md +++ b/packages/core/etc/sentry-react-native.api.md @@ -492,7 +492,29 @@ export interface NativeLogEntry { export const nativeReleaseIntegration: () => Integration; // @public -export const NavigationContainer: React_2.ForwardRefExoticComponent, "ref"> & React_2.RefAttributes>; +export const NavigationContainer: React_2.ForwardRefExoticComponent & React_2.RefAttributes>; + +// @public +export interface NavigationTheme { + // (undocumented) + colors: { + primary: string; + background: string; + card: string; + text: string; + border: string; + notification: string; + }; + // (undocumented) + dark: boolean; + // (undocumented) + fonts: { + regular: FontStyle; + medium: FontStyle; + bold: FontStyle; + heavy: FontStyle; + }; +} export { OpenAiClient } @@ -589,6 +611,70 @@ export const sdkInfoIntegration: () => Integration; export { SendFeedbackParams } +// @public +export interface SentryNavigationContainerProps { + // (undocumented) + [key: string]: unknown; + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + direction?: 'ltr' | 'rtl'; + // (undocumented) + documentTitle?: { + enabled?: boolean; + formatter?: (options: Record | undefined, route: { + key: string; + name: string; + params?: object; + } | undefined) => string; + }; + // (undocumented) + fallback?: React_2.ReactNode; + // (undocumented) + initialState?: { + index?: number; + key?: string; + routeNames?: string[]; + routes: Array<{ + key?: string; + name: string; + params?: object; + state?: object; + }>; + stale?: true; + type?: string; + }; + // (undocumented) + linking?: { + enabled?: boolean; + prefixes: string[]; + filter?: (url: string) => boolean; + config?: { + path?: string; + screens: Record; + initialRouteName?: string; + }; + getInitialURL?: () => string | null | undefined | Promise; + subscribe?: (listener: (url: string) => void) => undefined | void | (() => void); + getStateFromPath?: (path: string, options?: object) => object | undefined; + getPathFromState?: (state: object, options?: object) => string; + getActionFromState?: (state: object, options?: object) => object | undefined; + }; + // (undocumented) + onReady?: () => void; + // (undocumented) + onStateChange?: (state: Readonly> | undefined) => void; + // (undocumented) + onUnhandledAction?: (action: Readonly<{ + type: string; + payload?: object; + source?: string; + target?: string; + }>) => void; + // (undocumented) + theme?: NavigationTheme; +} + // @public export function sentryTraceGesture( label: string, gesture: GestureT): GestureT; @@ -761,6 +847,7 @@ export function wrapExpoRouter(router: T): T; // Warnings were encountered during analysis: // +// src/js/NavigationContainer.tsx:26:5 - (ae-forgotten-export) The symbol "FontStyle" needs to be exported by the entry point index.d.ts // src/js/feedback/integration.ts:21:5 - (ae-forgotten-export) The symbol "ScreenshotButtonProps" needs to be exported by the entry point index.d.ts // src/js/feedback/integration.ts:23:5 - (ae-forgotten-export) The symbol "FeedbackFormTheme" needs to be exported by the entry point index.d.ts // src/js/tracing/reactnativetracing.ts:94:3 - (ae-forgotten-export) The symbol "ReactNativeTracingState" needs to be exported by the entry point index.d.ts diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx index f6685e297f..20326b3b4b 100644 --- a/packages/core/src/js/NavigationContainer.tsx +++ b/packages/core/src/js/NavigationContainer.tsx @@ -12,7 +12,7 @@ type FontStyle = { /** * Mirrors the `Theme` type from `@react-navigation/native`. */ -interface NavigationTheme { +export interface NavigationTheme { dark: boolean; colors: { primary: string; @@ -75,7 +75,10 @@ export interface SentryNavigationContainerProps { fallback?: React.ReactNode; documentTitle?: { enabled?: boolean; - formatter?: (options: Record | undefined, route: { key: string; name: string; params?: object } | undefined) => string; + formatter?: ( + options: Record | undefined, + route: { key: string; name: string; params?: object } | undefined, + ) => string; }; [key: string]: unknown; } diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 5bbf3fc4b7..0de30fa7d5 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -112,7 +112,7 @@ export { } from './sdk'; export { TouchEventBoundary, withTouchEventBoundary } from './touchevents'; export { NavigationContainer } from './NavigationContainer'; -export type { SentryNavigationContainerProps } from './NavigationContainer'; +export type { NavigationTheme, SentryNavigationContainerProps } from './NavigationContainer'; export { GlobalErrorBoundary, withGlobalErrorBoundary } from './GlobalErrorBoundary'; export type { GlobalErrorBoundaryProps } from './GlobalErrorBoundary'; From 7d90c083ae275bdc9d06d2076604c51c2de6ba86 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 26 May 2026 11:11:36 +0200 Subject: [PATCH 08/10] feat(tracing): Add breadcrumbs for dispatched React Navigation events Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 1 + .../core/src/js/tracing/reactnavigation.ts | 22 ++- .../core/test/tracing/reactnavigation.test.ts | 165 ++++++++++++++++++ 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec133fa87a..ee5dcf1a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Features - Add `Sentry.NavigationContainer` drop-in wrapper for React Navigation ([#6199](https://github.com/getsentry/sentry-react-native/pull/6199)) +- Add breadcrumbs for dispatched React Navigation events ([#6199](https://github.com/getsentry/sentry-react-native/pull/6199)) - Add `disableAutoUpload` option to Expo plugin to disable source map and debug symbol uploads ([#6195](https://github.com/getsentry/sentry-react-native/pull/6195)) - Expose `pauseAppHangTracking` and `resumeAppHangTracking` APIs on iOS ([#6192](https://github.com/getsentry/sentry-react-native/pull/6192)) - Better route and dynamic param extraction for Expo Router ([#6197](https://github.com/getsentry/sentry-react-native/pull/6197)) diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index 0490285510..c55f1fcc91 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -347,12 +347,30 @@ export const reactNavigationIntegration = ({ // oxlint-disable-next-line eslint(complexity) const startIdleNavigationSpan = (unknownEvent?: unknown, isAppRestart = false): void => { const event = unknownEvent as UnsafeAction | undefined; + const actionType = event?.data?.action?.type; + const targetRouteName = getRouteNameFromAction(event); + if (useDispatchedActionData && event?.data.noop) { debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`); return; } - const navigationActionType = useDispatchedActionData ? event?.data.action.type : undefined; + if (event && !isAppRestart) { + addBreadcrumb({ + category: 'navigation.dispatch', + type: 'navigation', + message: targetRouteName + ? `Dispatched ${actionType ?? 'NAVIGATE'} to ${targetRouteName}` + : `Dispatched ${actionType ?? 'NAVIGATE'}`, + data: { + ...(actionType ? { action_type: actionType } : undefined), + ...(targetRouteName ? { to: targetRouteName } : undefined), + }, + level: 'info', + }); + } + + const navigationActionType = useDispatchedActionData ? actionType : undefined; // Handle PRELOAD actions separately if prefetch tracking is enabled if (enablePrefetchTracking && navigationActionType === 'PRELOAD') { @@ -407,7 +425,7 @@ export const reactNavigationIntegration = ({ } // Extract route name from dispatch action payload when available - const dispatchedRouteName = useDispatchedActionData ? getRouteNameFromAction(event) : undefined; + const dispatchedRouteName = useDispatchedActionData ? targetRouteName : undefined; if (useDispatchedActionData && event && !dispatchedRouteName && !isAppRestart) { debug.log(`${INTEGRATION_NAME} Navigation action has no route name in payload, not starting navigation span.`); return; diff --git a/packages/core/test/tracing/reactnavigation.test.ts b/packages/core/test/tracing/reactnavigation.test.ts index 0bf59db43b..7b3ea3af07 100644 --- a/packages/core/test/tracing/reactnavigation.test.ts +++ b/packages/core/test/tracing/reactnavigation.test.ts @@ -1,5 +1,6 @@ import type { Event, Measurements, SentrySpan, StartSpanOptions } from '@sentry/core'; +import * as core from '@sentry/core'; import { getActiveSpan, getCurrentScope, @@ -1641,6 +1642,170 @@ describe('ReactNavigationInstrumentation', () => { }); }); + describe('dispatch breadcrumbs', () => { + let addBreadcrumbSpy: jest.SpyInstance; + + beforeEach(() => { + addBreadcrumbSpy = jest.spyOn(core, 'addBreadcrumb'); + }); + + afterEach(() => { + addBreadcrumbSpy.mockRestore(); + }); + + it('includes action type and route name even when useDispatchedActionData is disabled', async () => { + setupTestClient(); + mockNavigation.navigateToNewScreenWithPayload(); + await jest.advanceTimersByTimeAsync(500); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'navigation.dispatch', + type: 'navigation', + message: 'Dispatched NAVIGATE to New Screen', + data: expect.objectContaining({ + action_type: 'NAVIGATE', + to: 'New Screen', + }), + }), + ); + }); + + it('includes action type and route name when useDispatchedActionData is enabled', async () => { + setupTestClient({ useDispatchedActionData: true }); + mockNavigation.navigateToNewScreenWithPayload(); + await jest.advanceTimersByTimeAsync(500); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'navigation.dispatch', + type: 'navigation', + message: 'Dispatched NAVIGATE to New Screen', + data: expect.objectContaining({ + action_type: 'NAVIGATE', + to: 'New Screen', + }), + }), + ); + }); + + it('creates dispatch breadcrumb without route name for GO_BACK', async () => { + setupTestClient({ useDispatchedActionData: true }); + mockNavigation.emitGoBackWithStateChange(); + await jest.advanceTimersByTimeAsync(500); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'navigation.dispatch', + type: 'navigation', + message: 'Dispatched GO_BACK', + data: expect.objectContaining({ + action_type: 'GO_BACK', + }), + }), + ); + }); + + it('creates dispatch breadcrumb for filtered actions like SET_PARAMS', async () => { + setupTestClient({ useDispatchedActionData: true }); + mockNavigation.emitWithoutStateChange({ + data: { + action: { type: 'SET_PARAMS' }, + noop: false, + stack: undefined, + }, + }); + await jest.advanceTimersByTimeAsync(500); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'navigation.dispatch', + message: 'Dispatched SET_PARAMS', + data: expect.objectContaining({ + action_type: 'SET_PARAMS', + }), + }), + ); + }); + + it('creates dispatch breadcrumb for drawer actions', async () => { + setupTestClient({ useDispatchedActionData: true }); + mockNavigation.emitWithoutStateChange({ + data: { + action: { type: 'OPEN_DRAWER' }, + noop: false, + stack: undefined, + }, + }); + await jest.advanceTimersByTimeAsync(500); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'navigation.dispatch', + message: 'Dispatched OPEN_DRAWER', + data: expect.objectContaining({ + action_type: 'OPEN_DRAWER', + }), + }), + ); + }); + + it('does not create dispatch breadcrumb for noop actions', async () => { + setupTestClient({ useDispatchedActionData: true }); + mockNavigation.emitWithoutStateChange({ + data: { + action: { type: 'NAVIGATE' }, + noop: true, + stack: undefined, + }, + }); + await jest.advanceTimersByTimeAsync(500); + + const dispatchCall = addBreadcrumbSpy.mock.calls.find( + (call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch', + ); + expect(dispatchCall).toBeUndefined(); + }); + + it('still creates navigation breadcrumb on completed navigation', async () => { + setupTestClient(); + mockNavigation.navigateToNewScreen(); + await jest.advanceTimersByTimeAsync(500); + + expect(addBreadcrumbSpy).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'navigation', + type: 'navigation', + message: 'Navigation to New Screen', + }), + ); + }); + + it('does not create dispatch breadcrumb for app restart', async () => { + const rNavigation = reactNavigationIntegration({ routeChangeTimeoutMs: 200 }); + const rnTracing = reactNativeTracingIntegration(); + const options = getDefaultTestClientOptions({ + enableNativeFramesTracking: false, + enableStallTracking: false, + tracesSampleRate: 1.0, + integrations: [rNavigation, rnTracing], + enableAppStartTracking: false, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + mockNavigation = createMockNavigationAndAttachTo(rNavigation); + + mockNavigation.finishAppStartNavigation(); + await jest.advanceTimersByTimeAsync(500); + + const dispatchCall = addBreadcrumbSpy.mock.calls.find( + (call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch', + ); + expect(dispatchCall).toBeUndefined(); + }); + }); + function setupTestClient( setupOptions: { beforeSpanStart?: (options: StartSpanOptions) => StartSpanOptions; From 2ddd72988b8394d4a305e90dff2dead37b5fd1a1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 26 May 2026 11:13:31 +0200 Subject: [PATCH 09/10] Revert "feat(tracing): Add breadcrumbs for dispatched React Navigation events" This reverts commit 7d90c083ae275bdc9d06d2076604c51c2de6ba86. --- CHANGELOG.md | 1 - .../core/src/js/tracing/reactnavigation.ts | 22 +-- .../core/test/tracing/reactnavigation.test.ts | 165 ------------------ 3 files changed, 2 insertions(+), 186 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5dcf1a32..ec133fa87a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ ### Features - Add `Sentry.NavigationContainer` drop-in wrapper for React Navigation ([#6199](https://github.com/getsentry/sentry-react-native/pull/6199)) -- Add breadcrumbs for dispatched React Navigation events ([#6199](https://github.com/getsentry/sentry-react-native/pull/6199)) - Add `disableAutoUpload` option to Expo plugin to disable source map and debug symbol uploads ([#6195](https://github.com/getsentry/sentry-react-native/pull/6195)) - Expose `pauseAppHangTracking` and `resumeAppHangTracking` APIs on iOS ([#6192](https://github.com/getsentry/sentry-react-native/pull/6192)) - Better route and dynamic param extraction for Expo Router ([#6197](https://github.com/getsentry/sentry-react-native/pull/6197)) diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index c55f1fcc91..0490285510 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -347,30 +347,12 @@ export const reactNavigationIntegration = ({ // oxlint-disable-next-line eslint(complexity) const startIdleNavigationSpan = (unknownEvent?: unknown, isAppRestart = false): void => { const event = unknownEvent as UnsafeAction | undefined; - const actionType = event?.data?.action?.type; - const targetRouteName = getRouteNameFromAction(event); - if (useDispatchedActionData && event?.data.noop) { debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`); return; } - if (event && !isAppRestart) { - addBreadcrumb({ - category: 'navigation.dispatch', - type: 'navigation', - message: targetRouteName - ? `Dispatched ${actionType ?? 'NAVIGATE'} to ${targetRouteName}` - : `Dispatched ${actionType ?? 'NAVIGATE'}`, - data: { - ...(actionType ? { action_type: actionType } : undefined), - ...(targetRouteName ? { to: targetRouteName } : undefined), - }, - level: 'info', - }); - } - - const navigationActionType = useDispatchedActionData ? actionType : undefined; + const navigationActionType = useDispatchedActionData ? event?.data.action.type : undefined; // Handle PRELOAD actions separately if prefetch tracking is enabled if (enablePrefetchTracking && navigationActionType === 'PRELOAD') { @@ -425,7 +407,7 @@ export const reactNavigationIntegration = ({ } // Extract route name from dispatch action payload when available - const dispatchedRouteName = useDispatchedActionData ? targetRouteName : undefined; + const dispatchedRouteName = useDispatchedActionData ? getRouteNameFromAction(event) : undefined; if (useDispatchedActionData && event && !dispatchedRouteName && !isAppRestart) { debug.log(`${INTEGRATION_NAME} Navigation action has no route name in payload, not starting navigation span.`); return; diff --git a/packages/core/test/tracing/reactnavigation.test.ts b/packages/core/test/tracing/reactnavigation.test.ts index 7b3ea3af07..0bf59db43b 100644 --- a/packages/core/test/tracing/reactnavigation.test.ts +++ b/packages/core/test/tracing/reactnavigation.test.ts @@ -1,6 +1,5 @@ import type { Event, Measurements, SentrySpan, StartSpanOptions } from '@sentry/core'; -import * as core from '@sentry/core'; import { getActiveSpan, getCurrentScope, @@ -1642,170 +1641,6 @@ describe('ReactNavigationInstrumentation', () => { }); }); - describe('dispatch breadcrumbs', () => { - let addBreadcrumbSpy: jest.SpyInstance; - - beforeEach(() => { - addBreadcrumbSpy = jest.spyOn(core, 'addBreadcrumb'); - }); - - afterEach(() => { - addBreadcrumbSpy.mockRestore(); - }); - - it('includes action type and route name even when useDispatchedActionData is disabled', async () => { - setupTestClient(); - mockNavigation.navigateToNewScreenWithPayload(); - await jest.advanceTimersByTimeAsync(500); - - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'navigation.dispatch', - type: 'navigation', - message: 'Dispatched NAVIGATE to New Screen', - data: expect.objectContaining({ - action_type: 'NAVIGATE', - to: 'New Screen', - }), - }), - ); - }); - - it('includes action type and route name when useDispatchedActionData is enabled', async () => { - setupTestClient({ useDispatchedActionData: true }); - mockNavigation.navigateToNewScreenWithPayload(); - await jest.advanceTimersByTimeAsync(500); - - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'navigation.dispatch', - type: 'navigation', - message: 'Dispatched NAVIGATE to New Screen', - data: expect.objectContaining({ - action_type: 'NAVIGATE', - to: 'New Screen', - }), - }), - ); - }); - - it('creates dispatch breadcrumb without route name for GO_BACK', async () => { - setupTestClient({ useDispatchedActionData: true }); - mockNavigation.emitGoBackWithStateChange(); - await jest.advanceTimersByTimeAsync(500); - - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'navigation.dispatch', - type: 'navigation', - message: 'Dispatched GO_BACK', - data: expect.objectContaining({ - action_type: 'GO_BACK', - }), - }), - ); - }); - - it('creates dispatch breadcrumb for filtered actions like SET_PARAMS', async () => { - setupTestClient({ useDispatchedActionData: true }); - mockNavigation.emitWithoutStateChange({ - data: { - action: { type: 'SET_PARAMS' }, - noop: false, - stack: undefined, - }, - }); - await jest.advanceTimersByTimeAsync(500); - - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'navigation.dispatch', - message: 'Dispatched SET_PARAMS', - data: expect.objectContaining({ - action_type: 'SET_PARAMS', - }), - }), - ); - }); - - it('creates dispatch breadcrumb for drawer actions', async () => { - setupTestClient({ useDispatchedActionData: true }); - mockNavigation.emitWithoutStateChange({ - data: { - action: { type: 'OPEN_DRAWER' }, - noop: false, - stack: undefined, - }, - }); - await jest.advanceTimersByTimeAsync(500); - - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'navigation.dispatch', - message: 'Dispatched OPEN_DRAWER', - data: expect.objectContaining({ - action_type: 'OPEN_DRAWER', - }), - }), - ); - }); - - it('does not create dispatch breadcrumb for noop actions', async () => { - setupTestClient({ useDispatchedActionData: true }); - mockNavigation.emitWithoutStateChange({ - data: { - action: { type: 'NAVIGATE' }, - noop: true, - stack: undefined, - }, - }); - await jest.advanceTimersByTimeAsync(500); - - const dispatchCall = addBreadcrumbSpy.mock.calls.find( - (call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch', - ); - expect(dispatchCall).toBeUndefined(); - }); - - it('still creates navigation breadcrumb on completed navigation', async () => { - setupTestClient(); - mockNavigation.navigateToNewScreen(); - await jest.advanceTimersByTimeAsync(500); - - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'navigation', - type: 'navigation', - message: 'Navigation to New Screen', - }), - ); - }); - - it('does not create dispatch breadcrumb for app restart', async () => { - const rNavigation = reactNavigationIntegration({ routeChangeTimeoutMs: 200 }); - const rnTracing = reactNativeTracingIntegration(); - const options = getDefaultTestClientOptions({ - enableNativeFramesTracking: false, - enableStallTracking: false, - tracesSampleRate: 1.0, - integrations: [rNavigation, rnTracing], - enableAppStartTracking: false, - }); - client = new TestClient(options); - setCurrentClient(client); - client.init(); - mockNavigation = createMockNavigationAndAttachTo(rNavigation); - - mockNavigation.finishAppStartNavigation(); - await jest.advanceTimersByTimeAsync(500); - - const dispatchCall = addBreadcrumbSpy.mock.calls.find( - (call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch', - ); - expect(dispatchCall).toBeUndefined(); - }); - }); - function setupTestClient( setupOptions: { beforeSpanStart?: (options: StartSpanOptions) => StartSpanOptions; From 04d17af0b5e0af1c64de95718206310639a865c0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 26 May 2026 16:35:57 +0200 Subject: [PATCH 10/10] fix(core): Export FontStyle type used by NavigationTheme Co-Authored-By: Claude Opus 4.6 --- packages/core/etc/sentry-react-native.api.md | 7 ++++++- packages/core/src/js/NavigationContainer.tsx | 2 +- packages/core/src/js/index.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/etc/sentry-react-native.api.md b/packages/core/etc/sentry-react-native.api.md index f10485b6a0..5c1ca742ea 100644 --- a/packages/core/etc/sentry-react-native.api.md +++ b/packages/core/etc/sentry-react-native.api.md @@ -343,6 +343,12 @@ export const feedbackIntegration: (initOptions?: Partial & { // @public export function flush(): Promise; +// @public (undocumented) +export type FontStyle = { + fontFamily: string; + fontWeight: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; +}; + export { functionToStringIntegration } export { getActiveSpan } @@ -847,7 +853,6 @@ export function wrapExpoRouter(router: T): T; // Warnings were encountered during analysis: // -// src/js/NavigationContainer.tsx:26:5 - (ae-forgotten-export) The symbol "FontStyle" needs to be exported by the entry point index.d.ts // src/js/feedback/integration.ts:21:5 - (ae-forgotten-export) The symbol "ScreenshotButtonProps" needs to be exported by the entry point index.d.ts // src/js/feedback/integration.ts:23:5 - (ae-forgotten-export) The symbol "FeedbackFormTheme" needs to be exported by the entry point index.d.ts // src/js/tracing/reactnativetracing.ts:94:3 - (ae-forgotten-export) The symbol "ReactNativeTracingState" needs to be exported by the entry point index.d.ts diff --git a/packages/core/src/js/NavigationContainer.tsx b/packages/core/src/js/NavigationContainer.tsx index 20326b3b4b..e4c0be5e2a 100644 --- a/packages/core/src/js/NavigationContainer.tsx +++ b/packages/core/src/js/NavigationContainer.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { getNavigationContainerComponent } from './reactNavigationImport'; import { getReactNavigationIntegration } from './tracing/reactnavigation'; -type FontStyle = { +export type FontStyle = { fontFamily: string; fontWeight: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; }; diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 0de30fa7d5..17feb632cb 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -112,7 +112,7 @@ export { } from './sdk'; export { TouchEventBoundary, withTouchEventBoundary } from './touchevents'; export { NavigationContainer } from './NavigationContainer'; -export type { NavigationTheme, SentryNavigationContainerProps } from './NavigationContainer'; +export type { FontStyle, NavigationTheme, SentryNavigationContainerProps } from './NavigationContainer'; export { GlobalErrorBoundary, withGlobalErrorBoundary } from './GlobalErrorBoundary'; export type { GlobalErrorBoundaryProps } from './GlobalErrorBoundary';