Skip to content

Conversation

@onurtemizkan
Copy link
Collaborator

@onurtemizkan onurtemizkan commented Nov 5, 2025

Fixes an issue where consecutive navigations to different routes fail to create separate navigation spans, causing span leaks and missing transaction data.

This came up in a React Router v6/v7 application where the pageload / navigation transactions take longer and there is a high finalTimeout set in config. When users navigate between different routes (e.g., /users/:id/projects/:projectId/settings). The SDK was incorrectly preventing new navigation spans from being created whenever an ongoing navigation span was active, regardless of whether the navigation was to a different route. This resulted in only the first navigation being tracked, with subsequent navigations being silently ignored. Also, the spans that needed to be a part of the subsequent navigation were recorded as a part of the previous one.

The root cause was the if (!isAlreadyInNavigationSpan) check that we used to prevent cross-usage scenarios (multiple wrappers instrumenting the same navigation), which incorrectly blocked legitimate consecutive navigations to different routes.

So, this fix changes the logic to check both navigation span state and the route name: isSpanForSameRoute = isAlreadyInNavigationSpan && spanJson?.description === name. This allows consecutive navigations to different routes while preventing duplicate spans for the same route.

Also added 100ms window tracking using LAST_NAVIGATION_PER_CLIENT Map. When multiple wrappers (e.g., wrapCreateBrowserRouter + wrapUseRoutes) instrument the same application, they may each trigger span creation for the same navigation event. The 100ms window deduplicates these cross-usage scenarios by comparing:

  • Navigation key: ${location.pathname}${location.search}${location.hash}
  • Timestamp: Date.now() - lastNavigation.timestamp < 100

If the same navigation key appears within 100ms, the second wrapper updates the existing span name if it has better parameterization, rather than creating a duplicate span, which will keep cross-usage covered.

@onurtemizkan onurtemizkan force-pushed the onur/fix-react-router-navigation-leak branch from 63ebcd0 to d720cad Compare November 5, 2025 12:32
@github-actions
Copy link
Contributor

github-actions bot commented Nov 5, 2025

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 24.59 kB - -
@sentry/browser - with treeshaking flags 23.09 kB - -
@sentry/browser (incl. Tracing) 41.23 kB - -
@sentry/browser (incl. Tracing, Profiling) 45.5 kB - -
@sentry/browser (incl. Tracing, Replay) 79.7 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 69.37 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 84.39 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 96.56 kB - -
@sentry/browser (incl. Feedback) 41.27 kB - -
@sentry/browser (incl. sendFeedback) 29.26 kB - -
@sentry/browser (incl. FeedbackAsync) 34.19 kB - -
@sentry/react 26.28 kB - -
@sentry/react (incl. Tracing) 43.19 kB +0.01% +3 B 🔺
@sentry/vue 29.07 kB - -
@sentry/vue (incl. Tracing) 43 kB - -
@sentry/svelte 24.6 kB - -
CDN Bundle 26.89 kB - -
CDN Bundle (incl. Tracing) 41.78 kB - -
CDN Bundle (incl. Tracing, Replay) 78.3 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 83.77 kB - -
CDN Bundle - uncompressed 78.84 kB - -
CDN Bundle (incl. Tracing) - uncompressed 123.94 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 239.97 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 252.73 kB - -
@sentry/nextjs (client) 45.31 kB - -
@sentry/sveltekit (client) 41.62 kB - -
@sentry/node-core 50.76 kB - -
@sentry/node 157.84 kB - -
@sentry/node - without tracing 92.64 kB - -
@sentry/aws-serverless 106.41 kB - -

View base workflow run

@onurtemizkan onurtemizkan force-pushed the onur/fix-react-router-navigation-leak branch 4 times, most recently from cb20156 to f703ae9 Compare November 10, 2025 22:05
@github-actions
Copy link
Contributor

github-actions bot commented Nov 10, 2025

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 11,932 - 8,762 +36%
GET With Sentry 1,677 14% 1,394 +20%
GET With Sentry (error only) 7,939 67% 6,136 +29%
POST Baseline 1,223 - 1,200 +2%
POST With Sentry 592 48% 542 +9%
POST With Sentry (error only) 1,084 89% 1,062 +2%
MYSQL Baseline 4,153 - 3,361 +24%
MYSQL With Sentry 588 14% 476 +24%
MYSQL With Sentry (error only) 3,448 83% 2,740 +26%

View base workflow run

@onurtemizkan onurtemizkan force-pushed the onur/fix-react-router-navigation-leak branch from f703ae9 to 23f63b6 Compare November 11, 2025 10:00
@onurtemizkan onurtemizkan marked this pull request as ready for review November 11, 2025 12:52
@onurtemizkan onurtemizkan force-pushed the onur/fix-react-router-navigation-leak branch from 59e8f76 to 6dbf27e Compare November 11, 2025 12:52
}

return navigationSpan;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Premature State Blocks Navigation Permanently

LAST_NAVIGATION_PER_CLIENT is set before startBrowserTracingNavigationSpan is called, but the cleanup listener is only registered if the span creation succeeds. When startBrowserTracingNavigationSpan returns undefined, the navigation key remains in the map without cleanup, permanently blocking subsequent navigations to the same route for 100ms. The timestamp should only be set after confirming the span was successfully created.

Fix in Cursor Fix in Web

if (endedSpan === navigationSpan) {
LAST_NAVIGATION_PER_CLIENT.delete(client);
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Navigation deduplication fails prematurely.

The spanEnd listener unconditionally deletes LAST_NAVIGATION_PER_CLIENT when any navigation span ends, but multiple navigation spans can be active simultaneously when navigating between different routes. If span A for route /a ends while span B for route /b is still active, the deduplication entry for /b is incorrectly removed, allowing duplicate spans for subsequent navigations to /b within the 100ms window.

Fix in Cursor Fix in Web

Comment on lines +721 to 731
source,
version,
location,
routes,
basename,
allRoutes,
navigationKey: currentNavigationKey,
});

// Patch navigation span to handle early cancellation (e.g., document.hidden)
if (navigationSpan) {
patchNavigationSpanEnd(navigationSpan, location, routes, basename, allRoutes);
}
} else if (isNavDuplicate && isAlreadyInNavigationSpan && activeSpan) {
tryUpdateSpanName(activeSpan, spanJson?.description, name, source);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Stale LAST_NAVIGATION_PER_CLIENT entry can cause subsequent navigations to be silently ignored if span creation fails.
Severity: HIGH | Confidence: 0.90

🔍 Detailed Analysis

If startBrowserTracingNavigationSpan() returns undefined (e.g., if no idle span is created/set), the LAST_NAVIGATION_PER_CLIENT entry is still set, but the cleanup listener is not registered. This leaves a stale timestamp entry in LAST_NAVIGATION_PER_CLIENT. Consequently, a subsequent navigation to the same location within 100ms will be incorrectly identified as a duplicate by isDuplicateNavigation() and silently ignored, leading to missing navigation spans.

💡 Suggested Fix

Ensure LAST_NAVIGATION_PER_CLIENT entries are always cleaned up, even if startBrowserTracingNavigationSpan() returns undefined, or modify isDuplicateNavigation() to account for cases where no span was successfully created for the initial navigation.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/react/src/reactrouter-compat-utils/instrumentation.tsx#L717-L731

Potential issue: If `startBrowserTracingNavigationSpan()` returns `undefined` (e.g., if
no idle span is created/set), the `LAST_NAVIGATION_PER_CLIENT` entry is still set, but
the cleanup listener is not registered. This leaves a stale timestamp entry in
`LAST_NAVIGATION_PER_CLIENT`. Consequently, a subsequent navigation to the same location
within 100ms will be incorrectly identified as a duplicate by `isDuplicateNavigation()`
and silently ignored, leading to missing navigation spans.

Did we get this right? 👍 / 👎 to inform future reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants