Skip to content

reactNavigationIntegration: navigation transaction can be named/attributed for the previous route instead of the destination when using a route override provider (e.g. expoRouterIntegration) #6436

Description

@kanzelm3

What React Native libraries do you use?

Expo Router

Are you using sentry.io or on-premise?

sentry.io (SaS)

Are you using any other error monitoring solution alongside Sentry?

No

Other Error Monitoring Solution Name

No response

@sentry/react-native SDK Version

8.16.0

How does your development environment look like?

⬇  Place the `npx react-native@latest info` output here. ⬇

System:
  OS: macOS 26.5.2
  CPU: (16) arm64 Apple M3 Max
  Memory: 5.48 GB / 64.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 24.12.0
    path: /Users/joel.kanzelmeyer/.nvm/versions/node/v24.12.0/bin/node
  Yarn:
    version: 4.12.0
    path: /Users/joel.kanzelmeyer/.nvm/versions/node/v24.12.0/bin/yarn
  npm:
    version: 11.6.2
    path: /Users/joel.kanzelmeyer/.nvm/versions/node/v24.12.0/bin/npm
  Watchman:
    version: 2024.10.14.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.15.2
    path: /usr/local/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.2
      - iOS 26.2
      - macOS 26.2
      - tvOS 26.2
      - visionOS 26.2
      - watchOS 26.2
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.3 AI-253.31033.145.2533.15113396
  Xcode:
    version: 26.2/17C52
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.11
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.2.0
    wanted: latest
  react:
    installed: 19.2.0
    wanted: 19.2.0
  react-native:
    installed: 0.83.6
    wanted: 0.83.6
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

Sentry.init()

Sentry.init({
    // double the default app hang time to 4 seconds
    appHangTimeoutInterval: 4,
    debug: process.env.EXPO_PUBLIC_SENTRY_DEBUG === 'true',
    dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
    enableAppStartTracking: true,
    // disable in local development
    enabled: isSentryEnabled,
    enableNativeFramesTracking: true,
    enableStallTracking: true,
    enableUserInteractionTracing: true,
    ignoreErrors: ignoredErrors,
    integrations: [
      Sentry.expoRouterIntegration({
        enableTimeToInitialDisplay: true,
        useDispatchedActionData: true,
      }),
      Sentry.reactNativeTracingIntegration(...)
    ],
    // default is 250 char, but some of our errors (e.g. zod) are being truncated
    maxValueLength: 5000,
    profilesSampleRate: isProfilingEnabled ? 1 : undefined,
    sendDefaultPii: true,
    spotlight:
      __DEV__ && process.env.EXPO_PUBLIC_ENABLE_SENTRY_LOCALLY === 'true',
});

Steps to Reproduce

  1. Initialize Sentry with expoRouterIntegration() (which registers a route override provider that resolves route info via Expo Router's store.getRouteInfo()).
  2. Navigate to screen A.
  3. Before screen A's data/transaction has finished loading, navigate again to a different screen B (e.g. tap a tab bar item or another nav trigger while A is still fetching).
  4. Inspect the resulting Sentry transaction for the second navigation (to screen B).

Expected Result

The transaction's name, route.name, and route.path reflect screen B — the screen actually navigated to.

Actual Result

The transaction is named and attributed as screen A (the previous route), even though route.key — which is read directly from React Navigation's own navigationContainer.getCurrentRoute(), not through the override — correctly shows screen B. So the same span ends up with a route.key pointing at the new screen but a route.name/route.path/transaction name pointing at the old one.

Root cause: reactNavigationIntegration's updateLatestNavigationSpanWithCurrentRoute is registered via navigationContainer.addListener('state', ...). Expo Router's own router-store, however, refreshes its cached routeInfo (which store.getRouteInfo() reads from) through the NavigationContainer's onStateChange prop, not through addListener('state', ...). In @react-navigation/core's BaseNavigationContainer.tsx, on every state change these fire in this fixed order:

emitter.emit({ type: 'state', data: { state } });   // notifies addListener('state', ...) subscribers — this is what reactNavigationIntegration listens on

if (!isFirstMountRef.current && onStateChangeRef.current) {
  onStateChangeRef.current(hydratedState);          // the onStateChange PROP — this is how Expo Router's own store.onStateChange refreshes its routeInfo cache
}

Because emit('state') always runs before the onStateChange prop callback, reactNavigationIntegration's listener reads Expo Router's store.getRouteInfo() one step before Expo Router itself has updated it for that specific transition — so the override returns the templated path for the route being navigated away from. Since updateLatestNavigationSpanWithCurrentRoute clears its span reference (latestNavigationSpan = undefined) as soon as it runs once, there's no second chance to pick up the corrected value on a later 'state' event for the same navigation. This is deterministic for the affected dispatch, not a flaky timing race — it's most visible when navigating away from a screen quickly, since there's less unrelated state churn to have coincidentally already nudged Expo Router's cache forward beforehand.

Metadata

Metadata

Assignees

Fields

No fields configured for issues without a type.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions