You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Are you using any other error monitoring solution alongside Sentry?
No
@sentry/react-native SDK Version
7.13.0
We verified the same code path is unchanged on main (cda137c68027b9b6c46a2bac28e9d81567b4d5bc), so this is not specific to the 7.x line. Where the two differ, we call it out below.
How does your development environment look like?
React Native 0.86.0, React 19.2.3, Hermes, New Architecture enabled (newArchEnabled=true).
@react-navigation/native 7.3.1, @react-navigation/stack 7.10.3, @react-navigation/bottom-tabs 7.18.0,
react-native-screens 4.25.2.
Observed in production on both Android and iOS. Self-hosted Sentry.
Sentry.init()
constnavigationIntegration=Sentry.reactNavigationIntegration({enableTimeToInitialDisplay: true,useDispatchedActionData: true,});Sentry.init({dsn: 'https://...',enableAutoSessionTracking: true,enableAutoPerformanceTracing: true,tracesSampleRate: 0.2,integrations: [navigationIntegration],// Our current mitigation, described below. We would like to delete it.beforeSendTransaction: dropOrphanedNavigationTransaction,});
Steps to Reproduce
Register reactNavigationIntegration as above and navigate to a screen, e.g. navigation.navigate('Chat', { id: 1 }).
While that screen is focused, dispatch a NAVIGATE that resolves onto the route already on top of the stack — for example navigation.navigate('Chat', { id: 2 }). React Navigation updates the params of the existing screen, so getCurrentRoute().key is unchanged.
In our app this is a real user flow: tapping a push notification for conversation B while conversation A is already open.
Observe the transaction the SDK emits for that dispatch.
Expected Result
Either the navigation span is discarded (it carries no route information, which is what ignoreEmptyRouteChangeTransactions exists to prevent shipping), or it is treated as a genuine screen entry and named/attributed like any other navigation so that its duration and its display-timing children are attributable to the screen.
Whichever is chosen, the navigation.processing child should be ended on this path and the state-change timeout cleared, as they are on the normal path.
Actual Result
An unnamed transaction named Route Change is emitted with no route.name, route.key, route.has_been_seen or previous_route.* attributes. Because it has op: navigation but no route attribution, it lands in navigation data as an anonymous entry and drags navigation-latency aggregates with it, and the screen entry it represents contributes nothing to per-screen TTID/TTFD aggregates.
Mechanism
Line references are to packages/core/src/js/tracing/reactnavigation.ts at cda137c6 (behaviour identical in the 7.13.0 dist/js/tracing/reactnavigation.js, cited in parentheses).
The dispatch is not filtered.startIdleNavigationSpan's useDispatchedActionData skip list covers PRELOAD, SET_PARAMS and the three drawer actions (L552-L565; 7.13.0 L178-L191). A NAVIGATE is not in it, so a navigation span is started as normal — correctly, since at dispatch time the SDK cannot yet know the state change will be a no-op.
The state listener bails out before doing any of its bookkeeping. In updateLatestNavigationSpanWithCurrentRoute, previousRoute?.key === route.key takes an early return (L720-L751; 7.13.0 L239-L246) that:
does not end navigationProcessingSpan, which the normal path ends at L793-L796 (7.13.0 L254-L257);
does not call clearStateChangeTimeout(), so the setTimeout(_discardLatestTransaction, routeChangeTimeoutMs) armed at L666 (7.13.0 L217) is still pending;
sets latestNavigationSpan = undefined at L749 (7.13.0 L244) and returns, leaving the root span unnamed and unattributed.
Clearing the handle disarms the SDK's own discard.ignoreEmptyRouteChangeTransactions is installed at L651 (7.13.0 L206-L207) with isSpanStillTracked = () => latestNavigationSpan === spanToCheck. That predicate is exactly what step 2 falsifies, so shouldDiscardFn in onSpanEndUtils.ts returns false and the empty transaction is sent. This looks unintentional rather than deliberate: the check was added in #5387 for the getCurrentRoute() === undefined case (#3991), where the state listener returns without clearing the handle. The same-route return clears the handle as though the span had been handled, but it was never named.
ignoreEmptyBackNavigationcannot cover it either, because it discards only when route.has_been_seen === true, and that attribute is set in the naming block the early return skipped.
The stray timeout then fires with nothing left to discard._discardLatestTransaction runs at routeChangeTimeoutMs, but latestNavigationSpan is already undefined, so the root span is untouched.
This is where the two versions diverge. On main, _discardLatestTransaction ends navigationProcessingSpan (L859-L865), so the child closes at ~routeChangeTimeoutMs and the idle span closes shortly after — the transaction is short but still empty and unnamed. On 7.13.0 the same function only drops the reference without ending the span (L313-L315), so the child stays open and the idle span is held until @sentry/core's childSpanTimeout (15000 ms, tracing/idleSpan.js) ends it with heartbeatFailed. That is the ~15 s duration we see in production; it is an artifact of the timeout, not of the navigation.
Steps 1-4 are the same on both versions; only the resulting duration differs.
Our current mitigation
We drop these in beforeSendTransaction, matching on the signature the SDK's own check looks for:
This keeps the aggregates clean but is indiscriminate — it also drops the transactions the SDK intends to discard for other reasons, and it cannot recover the screen entry that went unmeasured. We would rather delete it than carry it.
What React Native libraries do you use?
React Native without Frameworks, React Navigation, Hermes, RN New Architecture
Are you using sentry.io or on-premise?
on-premise (Self-Hosted)
Are you using any other error monitoring solution alongside Sentry?
No
@sentry/react-native SDK Version
7.13.0
We verified the same code path is unchanged on
main(cda137c68027b9b6c46a2bac28e9d81567b4d5bc), so this is not specific to the 7.x line. Where the two differ, we call it out below.How does your development environment look like?
Sentry.init()
Steps to Reproduce
Register
reactNavigationIntegrationas above and navigate to a screen, e.g.navigation.navigate('Chat', { id: 1 }).While that screen is focused, dispatch a
NAVIGATEthat resolves onto the route already on top of the stack — for examplenavigation.navigate('Chat', { id: 2 }). React Navigation updates the params of the existing screen, sogetCurrentRoute().keyis unchanged.In our app this is a real user flow: tapping a push notification for conversation B while conversation A is already open.
Observe the transaction the SDK emits for that dispatch.
Expected Result
Either the navigation span is discarded (it carries no route information, which is what
ignoreEmptyRouteChangeTransactionsexists to prevent shipping), or it is treated as a genuine screen entry and named/attributed like any other navigation so that its duration and its display-timing children are attributable to the screen.Whichever is chosen, the
navigation.processingchild should be ended on this path and the state-change timeout cleared, as they are on the normal path.Actual Result
An unnamed transaction named
Route Changeis emitted with noroute.name,route.key,route.has_been_seenorprevious_route.*attributes. Because it hasop: navigationbut no route attribution, it lands in navigation data as an anonymous entry and drags navigation-latency aggregates with it, and the screen entry it represents contributes nothing to per-screen TTID/TTFD aggregates.Mechanism
Line references are to
packages/core/src/js/tracing/reactnavigation.tsatcda137c6(behaviour identical in the 7.13.0dist/js/tracing/reactnavigation.js, cited in parentheses).The dispatch is not filtered.
startIdleNavigationSpan'suseDispatchedActionDataskip list coversPRELOAD,SET_PARAMSand the three drawer actions (L552-L565; 7.13.0 L178-L191). ANAVIGATEis not in it, so a navigation span is started as normal — correctly, since at dispatch time the SDK cannot yet know the state change will be a no-op.The state listener bails out before doing any of its bookkeeping. In
updateLatestNavigationSpanWithCurrentRoute,previousRoute?.key === route.keytakes an early return (L720-L751; 7.13.0 L239-L246) that:navigationProcessingSpan, which the normal path ends at L793-L796 (7.13.0 L254-L257);clearStateChangeTimeout(), so thesetTimeout(_discardLatestTransaction, routeChangeTimeoutMs)armed at L666 (7.13.0 L217) is still pending;latestNavigationSpan = undefinedat L749 (7.13.0 L244) and returns, leaving the root span unnamed and unattributed.mainadded a discard here forPOP_TOspecifically (L741-L746, fromwithAnchor's internalPOP_TObookkeeping dispatch creates a spurious duplicate navigation span #6434). A plainNAVIGATEfalls through it.Clearing the handle disarms the SDK's own discard.
ignoreEmptyRouteChangeTransactionsis installed at L651 (7.13.0 L206-L207) withisSpanStillTracked = () => latestNavigationSpan === spanToCheck. That predicate is exactly what step 2 falsifies, soshouldDiscardFninonSpanEndUtils.tsreturnsfalseand the empty transaction is sent. This looks unintentional rather than deliberate: the check was added in #5387 for thegetCurrentRoute() === undefinedcase (#3991), where the state listener returns without clearing the handle. The same-route return clears the handle as though the span had been handled, but it was never named.ignoreEmptyBackNavigationcannot cover it either, because it discards only whenroute.has_been_seen === true, and that attribute is set in the naming block the early return skipped.The stray timeout then fires with nothing left to discard.
_discardLatestTransactionruns atrouteChangeTimeoutMs, butlatestNavigationSpanis alreadyundefined, so the root span is untouched.This is where the two versions diverge. On
main,_discardLatestTransactionendsnavigationProcessingSpan(L859-L865), so the child closes at ~routeChangeTimeoutMsand the idle span closes shortly after — the transaction is short but still empty and unnamed. On 7.13.0 the same function only drops the reference without ending the span (L313-L315), so the child stays open and the idle span is held until@sentry/core'schildSpanTimeout(15000 ms,tracing/idleSpan.js) ends it withheartbeatFailed. That is the ~15 s duration we see in production; it is an artifact of the timeout, not of the navigation.Steps 1-4 are the same on both versions; only the resulting duration differs.
Our current mitigation
We drop these in
beforeSendTransaction, matching on the signature the SDK's own check looks for:This keeps the aggregates clean but is indiscriminate — it also drops the transactions the SDK intends to discard for other reasons, and it cannot recover the screen entry that went unmeasured. We would rather delete it than carry it.