What
There is no way for a framework SDK to tell the browser SDK what route the user is on. A framework parameterizes the pageload or navigation span it owns, and everything else in the SDK is left to work the route out for itself, from a different source, with a different fallback:
| Reader |
Source |
Fallback |
browser-utils/performance/interactions.ts:73-96 |
root pageload/nav span name, tracked on spanStart and re-read on spanEnd |
none, drops the span |
browser-utils/web-vitals/spans.ts:97-98 |
root span name, else scope transactionName |
none |
browser/integrations/bfcache.ts:137 |
scope transactionName |
raw location.pathname |
Two of them carry comments apologizing for the race ("routing instrumentation frequently renames the pageload span once the route is resolved"). That is workaround, not design.
What it costs today:
bfcacheMetricsIntegration emits a raw URL as sentry.segment.name on TanStack Router, react-router, Next.js and Solid. That is a metric dimension, so it is unbounded cardinality, and a user has no way to fix it. This is the sharpest case, not the only one.
interactionsIntegration drops the ui.action.click span entirely when it has no route name, and names it Pageload under span streaming, because it can only read whatever the root span happens to be called at that moment.
- Standalone web vital spans attribute to
Pageload or a raw URL for the same reason.
- Pageload and navigation spans get non-descriptive names. Nothing can supply a route when the span is named, so streaming forces
PAGELOAD_SPAN_NAME_FALLBACK ('Pageload') and each framework renames it later. NAVIGATION_SPAN_NAME_FALLBACK is defined and never used, so navigation spans still get raw location.pathname names under streaming.
None of this is fixable per integration. A framework would have to reach into each one and override it, which is why the same page can be attributed three different ways in a single event. Framework routers write the route to the span; only some also write it to the scope. startBrowserTracingNavigationSpan syncs the scope (browserTracingIntegration.ts:717), but startBrowserTracingPageLoadSpan sets it to raw location.pathname (line 680) and a late span.updateName() never corrects it. Roughly half the client routing instrumentations never sync the scope: Next.js (App and Pages), TanStack Router (react, solid, vue), react-router framework mode, Solid Router, Astro, and react-router v3. The other half (Angular, Ember, Remix, SvelteKit, Vue Router, react-router v4-v7) do, which is itself the problem: the route reaches the scope through a second, informal channel that each SDK opted into by hand.
Drift. 18 files outside @sentry/browser call startBrowserTracingNavigationSpan, each repeating its own version of the same rename dance. The three TanStack files (react, solid, vue) are ~60% identical, and #23299 taught only the react copy to prefer router.state.location for the initial pageload match. Same library, same bug, fixed in one copy of three.
A fifth implementation. replay-internal/src/replay.ts:839 has its own getCurrentRoute() that walks to the root span and filters on source. It name-collides with the API proposed below and needs resolving either way.
How
Add a route provider API to @sentry/core: framework SDKs register how to resolve a URL to a parameterized route name, and every integration that needs a route asks core instead of reaching into spans, the scope, or location itself.
interface RouteProvider {
resolveRoute(url: URL): string | undefined;
getCurrentRoute(): string | undefined;
}
setRouteProvider(provider, client?)
resolveRoute(url: string | URL): string | undefined
getCurrentRoute(): string | undefined
createUrlRouteProvider(resolve) // for routers whose location is the address bar
URL in, route name out. The provider knows nothing about spans, scopes, or attributes.
A provider returns a URL path template, never a route identifier. Callers set url.template from the resolved string, and an identifier is not a template. Routers that name routes independently of their path (Vue Router's route.name, Ember's posts.show) return the matched path instead, and name their span after the identifier on the span itself.
Next.js already ships exactly this shape in client/routing/parameterization.ts: maybeParameterizeRoute(pathname): string | undefined, pure and cached, backed by a build-time route manifest. It was just trapped inside one package.
Core normalizes to a real URL before handing it to the provider, so no provider has to parse, strip auth, or handle relative paths.
Rollout
Scoped down to the smallest thing that is useful on its own: route parameterization that does not
require tracing. Everything else follows once a second SDK can register a provider.
Deliberately out of scope for now, each tracked separately when picked up:
- Naming pageload and navigation spans from the provider. Tracing-side, and only pays off for the
SDKs whose provider can answer before the span is named.
- Attribute de-duplication. ~103 lines of
url.template, sentry.segment.name.source and route-param
emission are duplicated across 19 framework files, including three byte-identical copies of
routeMatchToParamSpanAttributes. Collapsing them needs the provider to return params, not just a
template string.
- Providers for SvelteKit, Solid, Angular and React Router. All four currently record behind an
active-span guard or register from a tracing integration, so each needs decoupling work first.
- Vue and TanStack Router. Both receive the router only through the tracing integration, so they need
a decoupled entry point before a provider is possible.
- Route identifiers (Vue's
route.name, Ember's posts.show) are not URL templates, so a provider
must return the matched path. A richer return type would let both travel.
Follow-ups surfaced by this work, tracked separately:
What
There is no way for a framework SDK to tell the browser SDK what route the user is on. A framework parameterizes the pageload or navigation span it owns, and everything else in the SDK is left to work the route out for itself, from a different source, with a different fallback:
browser-utils/performance/interactions.ts:73-96spanStartand re-read onspanEndbrowser-utils/web-vitals/spans.ts:97-98transactionNamebrowser/integrations/bfcache.ts:137transactionNamelocation.pathnameTwo of them carry comments apologizing for the race ("routing instrumentation frequently renames the pageload span once the route is resolved"). That is workaround, not design.
What it costs today:
bfcacheMetricsIntegrationemits a raw URL assentry.segment.nameon TanStack Router, react-router, Next.js and Solid. That is a metric dimension, so it is unbounded cardinality, and a user has no way to fix it. This is the sharpest case, not the only one.interactionsIntegrationdrops theui.action.clickspan entirely when it has no route name, and names itPageloadunder span streaming, because it can only read whatever the root span happens to be called at that moment.Pageloador a raw URL for the same reason.PAGELOAD_SPAN_NAME_FALLBACK('Pageload') and each framework renames it later.NAVIGATION_SPAN_NAME_FALLBACKis defined and never used, so navigation spans still get rawlocation.pathnamenames under streaming.None of this is fixable per integration. A framework would have to reach into each one and override it, which is why the same page can be attributed three different ways in a single event. Framework routers write the route to the span; only some also write it to the scope.
startBrowserTracingNavigationSpansyncs the scope (browserTracingIntegration.ts:717), butstartBrowserTracingPageLoadSpansets it to rawlocation.pathname(line 680) and a latespan.updateName()never corrects it. Roughly half the client routing instrumentations never sync the scope: Next.js (App and Pages), TanStack Router (react, solid, vue), react-router framework mode, Solid Router, Astro, and react-router v3. The other half (Angular, Ember, Remix, SvelteKit, Vue Router, react-router v4-v7) do, which is itself the problem: the route reaches the scope through a second, informal channel that each SDK opted into by hand.Drift. 18 files outside
@sentry/browsercallstartBrowserTracingNavigationSpan, each repeating its own version of the same rename dance. The three TanStack files (react, solid, vue) are ~60% identical, and #23299 taught only the react copy to preferrouter.state.locationfor the initial pageload match. Same library, same bug, fixed in one copy of three.A fifth implementation.
replay-internal/src/replay.ts:839has its owngetCurrentRoute()that walks to the root span and filters on source. It name-collides with the API proposed below and needs resolving either way.How
Add a route provider API to
@sentry/core: framework SDKs register how to resolve a URL to a parameterized route name, and every integration that needs a route asks core instead of reaching into spans, the scope, orlocationitself.URL in, route name out. The provider knows nothing about spans, scopes, or attributes.
A provider returns a URL path template, never a route identifier. Callers set
url.templatefrom the resolved string, and an identifier is not a template. Routers that name routes independently of their path (Vue Router'sroute.name, Ember'sposts.show) return the matched path instead, and name their span after the identifier on the span itself.Next.js already ships exactly this shape in
client/routing/parameterization.ts:maybeParameterizeRoute(pathname): string | undefined, pure and cached, backed by a build-time route manifest. It was just trapped inside one package.Core normalizes to a real
URLbefore handing it to the provider, so no provider has to parse, strip auth, or handle relative paths.Rollout
Scoped down to the smallest thing that is useful on its own: route parameterization that does not
require tracing. Everything else follows once a second SDK can register a provider.
bfcacheMetricsIntegrationas the first consumerinit(), so parameterization works withbrowserTracingIntegrationabsentDeliberately out of scope for now, each tracked separately when picked up:
SDKs whose provider can answer before the span is named.
url.template,sentry.segment.name.sourceand route-paramemission are duplicated across 19 framework files, including three byte-identical copies of
routeMatchToParamSpanAttributes. Collapsing them needs the provider to return params, not just atemplate string.
active-span guard or register from a tracing integration, so each needs decoupling work first.
a decoupled entry point before a provider is possible.
route.name, Ember'sposts.show) are not URL templates, so a providermust return the matched path. A richer return type would let both travel.
Follow-ups surfaced by this work, tracked separately:
resolve the(core's is namedgetCurrentRoutename collision with ReplayresolveCurrentRoute, no collision)(now used on develop)NAVIGATION_SPAN_NAME_FALLBACKis unused