Skip to content

Route parameterization is fragmented across browser integrations #23556

Description

@logaretm

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:

  • bfcacheIntegration 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. Only 2 of the 10 client routing instrumentations call setTransactionName (ember, react/reactrouter.tsx).

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.

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

Base:

Providers:

Consumers:

  • interactionsIntegration (drops its spanStart/spanEnd tracking and its hard dependency on browserTracingIntegration)
  • standalone web vital spans
  • browserTracingIntegration pageload naming (removes the rename race for matcher-backed routers)

Follow-ups surfaced by this work, tracked separately:

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions