Skip to content

feat(browser): Start a navigation span for bfcache restores - #23748

Merged
logaretm merged 5 commits into
developfrom
awad/bfcache-navigation-span
Sep 10, 2026
Merged

feat(browser): Start a navigation span for bfcache restores#23748
logaretm merged 5 commits into
developfrom
awad/bfcache-navigation-span

Conversation

@logaretm

@logaretm logaretm commented Aug 28, 2026

Copy link
Copy Markdown
Member

Starts a navigation span when the page is restored from the back/forward cache.

A restore resurrects the frozen document, so there is no document load and no usable history event: popstate either doesn't fire or is swallowed because the URL is unchanged. Without a span of its own, everything after the restore joins the trace the page had before it was frozen, which misattributes errors and breadcrumbs as much as vitals.

It lives in browserTracingIntegration rather than bfcacheIntegration so it doesn't depend on an opt-in integration being enabled, and carries browser.navigation.type: bfcache since a restore is near-instant and would otherwise drag navigation duration percentiles down with no way to tell it apart.

Stacked on #23425.

@logaretm logaretm changed the title awad/bfcache navigation span feat(browser): Report web vitals for bfcache restores Aug 28, 2026
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from c306449 to 831a206 Compare August 28, 2026 18:39
@logaretm logaretm changed the title feat(browser): Report web vitals for bfcache restores feat(browser): Start a navigation span for bfcache restores Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.92 kB - -
@sentry/browser - with treeshaking flags 27.21 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.11 kB - -
@sentry/browser (incl. Tracing) 50.36 kB +0.17% +81 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 50.37 kB +0.17% +83 B 🔺
@sentry/browser (incl. Tracing, Profiling) 53.35 kB +0.16% +81 B 🔺
@sentry/browser (incl. Tracing, Replay) 89.88 kB +0.13% +110 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.01 kB +0.19% +143 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 94.57 kB +0.13% +116 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 107.58 kB +0.11% +112 B 🔺
@sentry/browser (incl. Feedback) 46.41 kB - -
@sentry/browser (incl. sendFeedback) 33.97 kB - -
@sentry/browser (incl. FeedbackAsync) 39.08 kB - -
@sentry/browser (incl. Metrics) 29.93 kB - -
@sentry/browser (incl. Logs) 30.2 kB - -
@sentry/browser (incl. Metrics & Logs) 30.87 kB - -
@sentry/react 30.67 kB - -
@sentry/react (incl. Tracing) 52.67 kB +0.14% +73 B 🔺
@sentry/vue 36.16 kB - -
@sentry/vue (incl. Tracing) 52.6 kB +0.14% +70 B 🔺
@sentry/svelte 28.93 kB - -
CDN Bundle 30.66 kB - -
CDN Bundle (incl. Tracing) 50.86 kB +0.15% +74 B 🔺
CDN Bundle (incl. Logs, Metrics) 32.93 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.84 kB +0.19% +99 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.62 kB - -
CDN Bundle (incl. Tracing, Replay) 88.41 kB +0.1% +87 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.38 kB +0.1% +85 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 94.46 kB +0.1% +91 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.47 kB +0.1% +91 B 🔺
CDN Bundle - uncompressed 90.73 kB - -
CDN Bundle (incl. Tracing) - uncompressed 151.99 kB +0.2% +295 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.31 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 157.95 kB +0.19% +295 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.72 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 271.56 kB +0.11% +295 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 277.51 kB +0.11% +295 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.26 kB +0.11% +295 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.2 kB +0.11% +295 B 🔺
@sentry/nextjs (client) 55 kB +0.14% +76 B 🔺
@sentry/sveltekit (client) 50.77 kB +0.14% +66 B 🔺
@sentry/core/server 37.08 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 128.59 kB +0.02% +14 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.82 kB - -
@sentry/node - without tracing 89.55 kB +0.04% +29 B 🔺
@sentry/node - without channel injection 107.53 kB +0.03% +23 B 🔺
@sentry/aws-serverless 97.77 kB +0.03% +23 B 🔺
@sentry/cloudflare (withSentry) - minified 203.01 kB - -
@sentry/cloudflare (withSentry) 505.44 kB - -

View base workflow run

@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 831a206 to d7cd440 Compare August 31, 2026 16:03
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from d7cd440 to e67604c Compare September 8, 2026 14:56
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from e67604c to 08b4500 Compare September 8, 2026 14:57
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 08b4500 to 62457db Compare September 9, 2026 13:56
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 62457db to 7e4c2bc Compare September 9, 2026 16:06
@logaretm
logaretm marked this pull request as ready for review September 9, 2026 16:08
@logaretm
logaretm requested a review from a team as a code owner September 9, 2026 16:08
@logaretm
logaretm requested review from Lms24 and msonnb and removed request for a team September 9, 2026 16:08

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/browser/test/tracing/browserTracingIntegration.test.ts
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch 2 times, most recently from 48fcffe to b7439e2 Compare September 9, 2026 16:28
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from b7439e2 to 14b181f Compare September 9, 2026 16:40

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/browser/src/tracing/browserTracingIntegration.ts
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 14b181f to 18c743a Compare September 9, 2026 17:09
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 18c743a to 5c7be80 Compare September 9, 2026 17:37
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 5c7be80 to 18ab1ef Compare September 9, 2026 17:52

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 18ab1ef. Configure here.

Comment thread packages/browser/src/tracing/browserTracingIntegration.ts
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 18ab1ef to 268dcd7 Compare September 9, 2026 19:22
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 268dcd7 to 4568daa Compare September 9, 2026 19:26
Comment thread packages/browser/src/tracing/browserTracingIntegration.ts
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 4568daa to 83d3b23 Compare September 9, 2026 19:38
Base automatically changed from awad/js-1019-soft-nav-web-vitals-spans to develop September 9, 2026 23:06
… bfcache

Prototype. A bfcache restore resurrects the frozen document, so there is
no document load and no usable history event: `popstate` either doesn't
fire or is swallowed, because the URL is unchanged from when the page was
frozen. Two independent guards in the existing path suppress it, neither
written with bfcache in mind, so there is no small nudge that gets a span
out of it.

Without one, everything after the restore joins the trace the page had
before it was frozen, separated by however long it sat in the cache. That
misattributes errors, breadcrumbs, clicks and fetches, not just the web
vitals that prompted this.

The span is started from a `pageshow` listener in `browserTracingIntegration`
rather than `bfcacheIntegration`, so it does not depend on an opt-in
integration that is about hit/miss diagnostics. It is gated on
`instrumentNavigation` and on by default.

It carries `browser.navigation.type: bfcache`. A restore is near-instant,
so without a way to filter these out they would drag navigation duration
percentiles down exactly the way bfcache vitals would have dragged LCP.

The span deliberately starts at the `pageshow` event rather than from
`PerformanceNavigationTiming`, which is not replaced on restore and still
describes the original document load.

Known gap, pinned by a test: `bfcacheIntegration` registers its own
`pageshow` listener from `setupOnce`, which core always runs before every
`afterAllSetup`, so its hit/miss metric is emitted before this span exists
and still lands on the pre-freeze trace.
@logaretm
logaretm force-pushed the awad/bfcache-navigation-span branch from 83d3b23 to e2c0c91 Compare September 9, 2026 23:06

@Lms24 Lms24 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice change!

One thing to think about: I think we briefly discussed if these spans should have a different op than navigation. Something along the lines of navigation.cache_restore (analogous to navigation.redirect). Just curious on your thoughts if this makes sense?

Happy to leave the decision up to you, I can see the argument being made in both ways. If we go with the separate op, please add it to conventions (op, name and description rules).

// suppress the next one.
startingUrl = undefined;

startBrowserTracingNavigationSpan(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

q: At this point, is there any way another span could already be active? I guess with #23779 we eliminate one case but just curious if we should do something like

const maybeActiveSpan = getActiveSpan();
const segmentSpan = getRootSpan(maybeActiveSpan);
if (segmentSpan && segmentSpan.isRecording()) {
  segmentSpan.end()
}

startBrowserTracingNavigationSpan(...)

@logaretm logaretm Sep 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think we already do that?

I didn't see this happening, so I checked why it didn't occur in my tests and found that line. so feels redundant. WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ah yes, redundant then. sorry for the false flag!

// unchanged from when the page was frozen. Without a span of its own, everything after the
// restore joins the trace the page had before it was frozen, separated by however long it
// sat in the cache.
WINDOW.addEventListener?.('pageshow', (event: PageTransitionEvent) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

m: one more thing: Several browserTracingIntegrations call the base integration with instrumentNavigation: false. Meaning this code path won't be reached. Should we pull it out and gate it with another option? Or, we let the higher-level browserTracingIntegrations somehow control the behaviour in their implementations. Totally fine with whatever solution we find here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nice catch, I will add a instrumentBfcacheRestore option to control this.

The `pageshow` listener sat inside the `if (instrumentNavigation)` block, which
gates the History API handler. A restore is not a history change: `popstate`
doesn't fire, the URL is unchanged, and no router emits anything, so it never
goes through that mechanism and had no business inheriting its gate.

Every framework integration passes `instrumentNavigation: false` to the base
integration so its own router can own history spans (react, nextjs, vue,
angular, solid, sveltekit, ember), which left the restore span reaching plain
`@sentry/browser` only.

`instrumentBfcacheRestore` defaults to true and is independent of
`instrumentNavigation`. The point of the span is trace hygiene, keeping what
happens after a restore off the trace the page had before it was frozen, so it
is worth having even where history instrumentation is not.
`startingUrl?.indexOf(to) !== -1` is true when `startingUrl` is undefined, since
the optional chain short-circuits to undefined and `undefined !== -1`. Clearing
`startingUrl` therefore did not disable the guard, it reduced the condition to
`from === undefined` and made suppression unconditional.

`from` is undefined only on the first history event of the document, and module
state survives a restore, so this bites a page that never touched the History
API before it was frozen: the bfcache handler clears `startingUrl`, and the
first real navigation after the restore is then swallowed, leaving that route on
the restore span. Without the clearing, `startingUrl` still held the original
href and the guard correctly did not match.

The comment above the guard already says it should only fire when a valid
`startingUrl` exists, so the check now matches what it always meant.

The regression test needs `from` to be undefined, which is only the first
history event of the module's life, so it lives in its own file rather than
behind the `pushState` calls in browserTracingIntegration.test.ts.
@logaretm

Copy link
Copy Markdown
Member Author

One thing to think about: I think we briefly discussed if these spans should have a different op than navigation [...]

Yep! I thought about this last week and had some thoughts on keeping it:

  • we have the navigation type attribute singling it out.
  • a nerdy consistency:
    • a failed bfcache navigation is actually a pageload but we could detect it, but it behaves more like a pageload.
    • a successful bfcache navigation behaves more like a soft navigation, scroll/state survive, memory profile is similar, etc...
    • I think if we name the successful case, we should name the failed one the same op, but they both behave differently and have widely different numbers/characteristics.
    • current naming/state is an approximation of both cases, it's not perfect for either but it's a bit consistent.
    • we would have to deal with yet another navigation type prerender at some point lol

The bfcache e2e app only exercised `bfcacheMetricsIntegration`, so nothing
outside the jsdom unit tests asserted the navigation span this branch adds.

Tracing is opt-in per test via `?tracing=1` rather than on for the whole app.
Two of the existing metric tests assert the segment name the metric falls back
to when the scope has no transaction name, and a pageload span would put one
there.

The second test pins the start timestamp against the pageload span rather than
against the host clock. It is the property most likely to regress silently: the
performance entries folded in when the span ends all predate the restore, and
what keeps them from dragging the start timestamp back to before the freeze is a
guard in `entries.ts` keyed on the `navigation` op.
… name

No router event fires on a bfcache restore, so the span falls back to the raw
pathname. In a plain browser app that matches every other navigation, but in a
framework app it is the only navigation span not named from a parameterized
route. #23551 gives us `resolveCurrentRoute()` to fix it.
@logaretm
logaretm removed this pull request from stack #23780 September 10, 2026 17:20
@logaretm
logaretm added this pull request to stack #24320 September 10, 2026 17:21
@logaretm
logaretm merged commit 0046d17 into develop Sep 10, 2026
310 of 315 checks passed
@logaretm
logaretm deleted the awad/bfcache-navigation-span branch September 10, 2026 17:31
logaretm added a commit that referenced this pull request Sep 10, 2026
Reports web vitals for back/forward cache restores, which we dropped
outright before.

Now that a restore gets its own navigation span in #23748, the vitals
have a correct parent, so the hardcoded `withoutBfcache` drop becomes
`webVitals: { bfcache: true }`. Off by default, since a restore is a
different population from a page load.

Stacked on #23748.
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