-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(react): Prevent navigation span leaks for consecutive navigations #18098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
63ebcd0 to
d720cad
Compare
size-limit report 📦
|
cb20156 to
f703ae9
Compare
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.
|
f703ae9 to
23f63b6
Compare
59e8f76 to
6dbf27e
Compare
| } | ||
|
|
||
| return navigationSpan; | ||
| } |
There was a problem hiding this comment.
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.
| if (endedSpan === navigationSpan) { | ||
| LAST_NAVIGATION_PER_CLIENT.delete(client); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
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
finalTimeoutset 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_CLIENTMap. 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:${location.pathname}${location.search}${location.hash}Date.now() - lastNavigation.timestamp < 100If 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.