Skip to content

Framework routing integrations start the pageload span at Sentry.init instead of timeOrigin #23469

Description

@imolinaDev

SDK

@sentry/react 10.70.0 (also present in @sentry/browser framework integrations generally)

Description

browserTracingIntegration deliberately anchors the pageload span to the performance time origin:

// packages/browser/src/tracing/browserTracingIntegration.ts:690-700
if (instrumentPageLoad) {
  const origin = browserPerformanceTimeOrigin();
  startBrowserTracingPageLoadSpan(client, {
    name: WINDOW.location.pathname,
    // pageload should always start at timeOrigin (and needs to be in s, not ms)
    startTime: origin ? origin / 1000 : undefined,
    ...

The framework routing integrations start their own pageload span and omit startTime:

// packages/react/src/tanstackrouter.ts:90-99
const pageloadSpan = startBrowserTracingPageLoadSpan(client, {
  name: routeMatch ? routeMatch.routeId : initialWindowLocation.pathname,
  attributes: { ... },
  // no startTime
});

Same omission in packages/react/src/reactrouter-compat-utils/instrumentation.ts (~line 385, covers React Router v6/v7), reactrouter.ts (v4/v5) and reactrouterv3.ts. reactRouterV6/V7BrowserTracingIntegration are affected via the compat helper.

Neither _createRouteSpan nor startIdleSpan in @sentry/core special-cases op: 'pageload', so the span simply starts whenever startIdleSpan is called.

Expected

Pageload span duration measures from navigationStart, matching browserTracingIntegration.

Actual

Pageload span duration measures from whenever Sentry.init runs. Everything before that — DNS, TLS, TTFB, HTML parse, and any bundle chunks loaded before init — is excluded from the transaction duration, while the browser-instrumentation child spans still carry their real absolute timestamps from the Performance API.

The gap is largest for apps that init from a dynamically imported bootstrap chunk, but it is nonzero for every consumer, so pageload P95 is systematically under-reported and is not comparable to the generic integration's numbers.

Suggested fix

Pass the same anchor the generic integration uses wherever a framework integration starts a pageload span:

const origin = browserPerformanceTimeOrigin();
startBrowserTracingPageLoadSpan(client, {
  name: ...,
  startTime: origin ? origin / 1000 : undefined,
  attributes: { ... },
});

Alternatively, default it inside startBrowserTracingPageLoadSpan (or in _createRouteSpan when op === 'pageload') so every caller gets it and the invariant in the existing comment holds in one place.

Notes

Reported from source reading rather than a runtime repro. Possibly related: #23253 (pageload transaction name ignores router basepath).

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status
    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions