Skip to content

feat(browser): Report web vitals for soft navigations - #23425

Merged
logaretm merged 10 commits into
developfrom
awad/js-1019-soft-nav-web-vitals-spans
Sep 9, 2026
Merged

feat(browser): Report web vitals for soft navigations#23425
logaretm merged 10 commits into
developfrom
awad/js-1019-soft-nav-web-vitals-spans

Conversation

@logaretm

@logaretm logaretm commented Aug 13, 2026

Copy link
Copy Markdown
Member

Reports LCP, CLS and INP for soft navigations, opt-in via webVitalsIntegration({ reportSoftNavs: true }). Correlates them to navigation spans via the triggering interactionId, since navigationId isn't available at history-change time. Requires span streaming.

Verified in Chrome 151, which surfaced two pre-existing attribution bugs, fixed here. One changes the default path: vital spans read the route name from the current scope, so a page load's LCP and CLS were labelled with the next route.

One interesting case is web-vitals synthesizes an INP with no entries so fast soft navs still report (GoogleChrome/web-vitals#724). That leaves no interaction type, and a bare op would hide those spans from aggregation. So it falls back to click.

closes #17857

@linear-code

linear-code Bot commented Aug 13, 2026

Copy link
Copy Markdown

JS-1019

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.81 kB - -
@sentry/browser - with treeshaking flags 27.12 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.01 kB - -
@sentry/browser (incl. Tracing) 50.19 kB +1.86% +912 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 50.2 kB +1.88% +924 B 🔺
@sentry/browser (incl. Tracing, Profiling) 53.18 kB +1.87% +972 B 🔺
@sentry/browser (incl. Tracing, Replay) 89.68 kB +0.99% +873 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 78.79 kB +1.04% +804 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 94.38 kB +0.95% +883 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 107.38 kB +0.89% +939 B 🔺
@sentry/browser (incl. Feedback) 46.3 kB - -
@sentry/browser (incl. sendFeedback) 33.87 kB - -
@sentry/browser (incl. FeedbackAsync) 38.99 kB - -
@sentry/browser (incl. Metrics) 29.84 kB - -
@sentry/browser (incl. Logs) 30.1 kB - -
@sentry/browser (incl. Metrics & Logs) 30.78 kB - -
@sentry/react 30.57 kB - -
@sentry/react (incl. Tracing) 52.52 kB +1.71% +880 B 🔺
@sentry/vue 36.07 kB - -
@sentry/vue (incl. Tracing) 52.43 kB +1.69% +871 B 🔺
@sentry/svelte 28.85 kB - -
CDN Bundle 30.56 kB - -
CDN Bundle (incl. Tracing) 50.67 kB +1.73% +860 B 🔺
CDN Bundle (incl. Logs, Metrics) 32.83 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.63 kB +1.66% +856 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.53 kB +0.07% +51 B 🔺
CDN Bundle (incl. Tracing, Replay) 88.2 kB +0.98% +854 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.17 kB +1.02% +906 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 94.23 kB +1.02% +951 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.26 kB +1.04% +983 B 🔺
CDN Bundle - uncompressed 90.47 kB - -
CDN Bundle (incl. Tracing) - uncompressed 151.35 kB +2.02% +2.99 kB 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.04 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 157.31 kB +1.94% +2.99 kB 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.45 kB +0.07% +151 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 270.92 kB +1.11% +2.97 kB 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 276.87 kB +1.09% +2.97 kB 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 284.62 kB +1.06% +2.97 kB 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 290.56 kB +1.04% +2.97 kB 🔺
@sentry/nextjs (client) 54.84 kB +1.4% +756 B 🔺
@sentry/sveltekit (client) 50.62 kB +1.81% +897 B 🔺
@sentry/core/server 36.99 kB - -
@sentry/core/browser 13.55 kB - -
@sentry/node 127.95 kB +0.02% +23 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.61 kB - -
@sentry/node - without tracing 88.73 kB +0.03% +22 B 🔺
@sentry/node - without channel injection 107.16 kB +0.02% +18 B 🔺
@sentry/aws-serverless 97.12 kB +0.03% +25 B 🔺
@sentry/cloudflare (withSentry) - minified 202.05 kB - -
@sentry/cloudflare (withSentry) 502.91 kB - -

View base workflow run

@logaretm logaretm changed the title feat(browser): Experimental soft navigation web vitals feat(browser): Report web vitals for soft navigations Aug 13, 2026
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from 7105e45 to 0d9c2e6 Compare August 28, 2026 15:28
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from b9d4e64 to 1e0115b Compare August 31, 2026 16:03
@logaretm
logaretm changed the base branch from develop to awad/pagehide-idle-span August 31, 2026 16:03
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch 2 times, most recently from d159dc7 to 1e06724 Compare September 8, 2026 14:57
Base automatically changed from awad/pagehide-idle-span to develop September 9, 2026 13:56
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from 1e06724 to abfcedf Compare September 9, 2026 13:56
Report LCP, CLS and INP for soft navigations, using upstream web-vitals'
`reportSoftNavs` option and the browser's Soft Navigations API.

Soft navigation vitals are correlated back to the SDK's navigation spans
through the `interactionId` of the interaction that triggered the
navigation, which both the `soft-navigation` entry and the interaction's
own Event Timing entry carry.

Opt out via `webVitalsIntegration({ reportSoftNavs: false })` or
`browserTracingIntegration({ enableSoftNavWebVitals: false })`.
Drop `reportAllChanges` when soft navigations are on, so every value a
handler receives is already final for its navigation and can be sent as
a span directly. Replaces the accumulate/flush state machine, and leaves
the default path untouched.
Soft navigations the browser doesn't detect (programmatic navigations,
navigations that never paint) report no vitals at all, so coverage is
lower than for page loads. That is worth stating next to the option
rather than leaving it to be discovered from missing data.

Also covers that `browserTracingIntegration` forwards the option to the
`webVitalsIntegration` it auto-registers.
INP took its parent from the interaction cache, which records whichever
root span was active when the interaction's entry was observed. For a
click that drives a navigation that is the destination navigation span,
so the page load's INP was reported against the first navigation.

With soft navigations the metric carries the navigation it belongs to,
so INP now flows through the same path as LCP and CLS. The cache is
still used for the element name, and still parents INP when soft
navigations are off.
web-vitals decides what an INP is; we map its emissions to telemetry
rather than filtering them. It reports a soft navigation's INP with no
entries when every interaction stayed below the Event Timing threshold,
and we were dropping those, so a fast navigation reported no INP at all.

Without an entry there is no element or interaction type to describe, so
the span falls back to a bare `ui.interaction` op and a generic name,
the same way CLS falls back to a "Layout shift" span. It is placed at
the start of the navigation it belongs to.
web-vitals synthesizes an INP value with no entries when a soft
navigation's interactions all stayed below the Event Timing threshold,
so that fast navigations are not excluded from INP reporting
(GoogleChrome/web-vitals#724).

Emitting those under a bare `ui.interaction` op would hide them from
any aggregation matching `ui.interaction.*`, reintroducing exactly the
bias upstream added the synthetic value to remove. Without an entry
there is no interaction type to derive, so the op falls back to
`click`.
A page's vitals are only comparable across routes if every route reports
its own. Behind an opt-in, the default stayed a single page-lifetime LCP,
CLS and INP that implicitly belong to whichever route happened to load
first, which is the less useful of the two behaviours in an app that soft
navigates at all.

Soft navigation vitals are now on by default and configured through a
nested `webVitals` bag on `browserTracingIntegration`, typed as the
`webVitalsIntegration` options it forwards to, so the two ways of
configuring the same integration cannot drift:

    browserTracingIntegration({ webVitals: { softNavigations: false } })
    webVitalsIntegration({ softNavigations: false })

The keys are positive with per-key defaults rather than `disable*` flags,
because the defaults in this bag point in opposite directions: soft
navigations are reported by default, while bfcache re-reports are dropped
(see `withoutBfcache`). A negated scheme would have to spell an
off-by-default feature as `disableBfcache: true` in the defaults.

`enableInp` is deprecated in favour of `webVitals: { ignore: ['inp'] }`,
which is the same switch spelled twice. It keeps working and is merged
into any user-provided ignore list.

Soft navigation vitals are still ignored when span streaming is off or
the browser has no Soft Navigations API, so nothing changes for a browser
that can't detect soft navigations in the first place.
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from abfcedf to 2aa6b25 Compare September 9, 2026 16:06
@logaretm
logaretm marked this pull request as ready for review September 9, 2026 16:06
@logaretm
logaretm requested a review from a team as a code owner September 9, 2026 16:06
@logaretm
logaretm requested review from Lms24 and msonnb and removed request for a team September 9, 2026 16:06
Comment thread packages/browser-utils/src/web-vitals/spans.ts

@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-utils/src/web-vitals/spans.ts
Comment thread packages/browser/src/integrations/webVitals.ts
Comment thread packages/browser-utils/src/web-vitals/softNavs.ts
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from 2aa6b25 to 0371626 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-utils/src/web-vitals/spans.ts
Comment thread packages/browser-utils/src/web-vitals/spans.ts
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from 0371626 to 5116ecb Compare September 9, 2026 17:09
Comment thread packages/browser-utils/src/instrumentation/performanceObserver.ts

@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/integrations/webVitals.ts
Comment thread packages/browser/src/integrations/webVitals.ts
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from 5116ecb to e56b122 Compare September 9, 2026 17:37
Comment thread packages/browser-utils/src/instrumentation/performanceObserver.ts

@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.

One question, one concern, otherwise just nits (feel free to ignore em)

Comment thread packages/browser-utils/src/instrumentation/performanceObserver.ts
Comment thread packages/browser-utils/src/web-vitals/emitSpan.ts Outdated
Comment on lines +176 to +177
'sentry.replay_id': replayId,
'sentry._internal.replay_is_buffering': replay!.getRecordingMode() === 'buffer' ? true : undefined,

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.

super-l: can both be replaced with consts from conventions

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 _internal isn't covered by the conventions? unless both are the same?

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 right, sorry, I thought this was covered. I don't know what the difference is, but let's just leave it as-is

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 remember somehwere that some of these get mapped to the _internal prefix by relay or somehting but maybe something for us to check later.

Comment thread packages/browser/src/tracing/browserTracingIntegration.ts

@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-utils/src/web-vitals/softNavs.ts
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from e56b122 to e1db9ed 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 e1db9ed. Configure here.

Comment thread packages/browser/src/integrations/webVitals.ts
@logaretm
logaretm requested a review from Lms24 September 9, 2026 19:11
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from e1db9ed to ad4d1c5 Compare September 9, 2026 19:22
Comment thread packages/browser-utils/src/web-vitals/softNavs.ts
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from ad4d1c5 to a391d98 Compare September 9, 2026 19:26
…red on

Adds `browser.navigation.type` to every emitted LCP, CLS and INP span, so
a vital can be read against the kind of navigation that produced it. A
soft navigation and a cold page load are not comparable measurements, and
without this there is no way to separate them after the fact.

web-vitals reports a wider set of navigation types than the attribute
defines, so only the states Navigation Timing cannot express keep their
own value. Every ordinary document navigation folds into `navigate`,
including a back/forward that missed the bfcache (per the spec) and a
`document.wasDiscarded` restore, which the spec does not name at all.

The `back-forward-cache` -> `bfcache` mapping is included but unreachable
today: `withoutBfcache` drops those metrics before they reach a span.
It's here so that enabling bfcache vitals later doesn't silently report
them as `navigate`.

Spec: getsentry/sentry-conventions#600
@logaretm
logaretm force-pushed the awad/js-1019-soft-nav-web-vitals-spans branch from a391d98 to d138028 Compare September 9, 2026 19:38
Comment thread packages/browser-utils/src/web-vitals/spans.ts

@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.

Exciting change!

Probably makes sense to add a note to the migration guide. Just to prepare users that their values will most likely shift. wdyt?

Comment thread packages/browser-utils/src/web-vitals/softNavs.ts
Comment on lines +72 to +90
/**
* Start correlating the browser's soft navigations with the SDK's navigation spans.
*
* A navigation span is created synchronously on the history change, but the browser only mints the
* `soft-navigation` entry (and with it the `navigationId` that web vitals are reported against)
* once the navigation has been confirmed by a paint. So the `navigationId` cannot be known at span
* creation time and the two have to be joined after the fact.
*
* The join key is the `interactionId` of the interaction that drove the navigation: per the Soft
* Navigations spec the `soft-navigation` entry carries the `interactionId` of the interaction that
* triggered it, which is the same id the interaction's own `PerformanceEventTiming` entry carries.
* So we bind a navigation span to the interaction it happened during, and the soft navigation
* joins back to that span through the shared id.
*
* This is inherently partial. Navigations that don't meet the browser's soft navigation heuristic
* (programmatic navigations, navigations that never paint, back/forward from the browser chrome)
* produce no entry at all, so those navigation spans simply have no web vitals.
*/
export function startSoftNavigationCorrelation(client: Client): void {

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.

reading this: have we tested this with framework router instrumentations where we no longer necessarily start a navigation on history change? Might be worth adding a react or Vue e2e test to one of our existing apps. wdyt?

@logaretm logaretm Sep 9, 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 mostly tested all of this in Vue initially, and then vanilla JS, i never tried react or others but worth it yes, thanks for the catch!

I will add a follow up PR tho for these tests because this is getting harder to read here, also allows us to test the bfcache variant as well.

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.

On

where we no longer necessarily start a navigation on history change...

I haven't observed a case where this happened which suggests we have a good match with the pre-existing browser huerstics, but if it happens we would lose these spans since the corrolation will fail.

Switching over to metrics would mitigate this, I will try to verify it via tests as well.

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.

mmm, we would need a newer playwright, I will create a new PR so we can grab a newer chromuim with soft navs.

Web vitals values shift on upgrade for apps that do client-side routing:
page load vitals are now finalized at the first soft navigation instead of
accumulating over the page lifetime, and CLS/LCP no longer report every
intermediate update, which changes what Session Replay records.
@logaretm
logaretm merged commit 67ede89 into develop Sep 9, 2026
313 of 314 checks passed
@logaretm
logaretm deleted the awad/js-1019-soft-nav-web-vitals-spans branch September 9, 2026 23:06
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.

Investigate and Experiment with soft navigation web vitals

2 participants