Skip to content

⚗️ Add soft navigation LCP tracking for route_change views - #4966

Draft
mormubis wants to merge 10 commits into
mainfrom
adrian.delarosa/soft-navigation
Draft

⚗️ Add soft navigation LCP tracking for route_change views#4966
mormubis wants to merge 10 commits into
mainfrom
adrian.delarosa/soft-navigation

Conversation

@mormubis

Copy link
Copy Markdown
Contributor

Motivation

route_change views never report LCP today. trackInitialViewMetrics only runs for initial_load, so SPAs get zero LCP after the first page. Chrome's Soft Navigation API (stable since Chrome 151, no flag needed on their end) actually solves this now: soft-navigation + interaction-contentful-paint entries let us compute LCP per client-side navigation.

There's an open GitHub issue for this (#2696) and an earlier draft PR (#4154) that only added a boolean is_soft_navigation flag without touching LCP, which isn't really what the issue is asking for. This PR does the actual LCP computation.

Changes

New ExperimentalFeature.SOFT_NAVIGATION flag. When enabled and the browser supports it, route_change views get a trackRouteChangeViewMetrics tracker (same shape as trackInitialViewMetrics) that subscribes to soft-navigation and interaction-contentful-paint entries, correlates them by interactionId, and fills in view.performance.lcp.*. No schema changes, reuses the existing field.

The tricky part was figuring out when to stop listening for the soft-navigation entry. It's per-view and Chrome fires it async, so a view whose interaction never actually produced a soft navigation would keep listening and could steal the next view's entry if I didn't unsubscribe it in time. Left a comment on that.

There's a design doc and implementation plan committed under docs/superpowers/specs/ and docs/superpowers/plans/ if you want the full reasoning trail, including why I went with a per-view subscription instead of a global one.

Also snuck in an unrelated one-line fix to playwright.config.ts (ccf6ef335). The nuxt-vue-router-v4-app E2E webServer was crashing locally, blocking every E2E test, not just mine. I know it's out of scope, happy to pull it into its own PR if you'd rather keep this one clean.

Test instructions

  1. enableExperimentalFeatures: ['soft_navigation'] in your init config.
  2. Chrome 151+, click something that does a synchronous DOM mutation followed by history.pushState in the same task (that's the heuristic Chrome uses to detect a soft navigation).
  3. Check the resulting route_change view event, view.performance.lcp.timestamp should be populated.
  4. Without the flag, or on any other browser, it stays undefined like before.

Unit tests: yarn test:unit --spec packages/browser-rum-core/src/domain/view/viewMetrics/trackRouteChangeViewMetrics.spec.ts and yarn test:unit --spec packages/browser-rum-core/src/domain/view/trackViews.spec.ts

E2E: yarn test:e2e -g "soft navigation" (Chromium only). I couldn't get a clean run of this locally, looks like a VPN/network issue unrelated to the change (an already-merged, unrelated E2E test failed the exact same way). Would appreciate someone running this in CI or a clean env before merging.

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file — skipping for now since it's experimental/flag-gated, will document properly once it's stable enough to drop the flag

…kspace app

The nuxt-vue-router-v4-app is a generated app (gitignored, not registered in the
root Yarn workspace). Playwright's webServer config starts every listed server
before any test run, regardless of -g filtering. When Playwright spawned
`yarn dev` in that directory, Yarn 4 traversed up to the workspace root and
failed with "Package for nuxt-vue-router-v4-app@workspace:. not found" because
the package isn't registered there.

Switch to `yarn start` (nuxt preview of the pre-built .output/) which runs the
built app directly via node and does not trigger Yarn workspace resolution.
export interface RumInteractionContentfulPaintTiming {
entryType: RumPerformanceEntryType.INTERACTION_CONTENTFUL_PAINT
interactionId: number
largestContentfulPaint: RumLargestContentfulPaintTiming

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reused RumLargestContentfulPaintTiming here instead of a flat shape. The WICG spec types InteractionContentfulPaint.largestContentfulPaint as the full LargestContentfulPaint interface, not a subset, so this felt more accurate. Also gets toJSON() for free this way.

}
biggestIcpSize = entry.largestContentfulPaint.size
const lcpEntry = entry.largestContentfulPaint
const largestContentfulPaint: LargestContentfulPaint = {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No subParts here on purpose. That breakdown (loadDelay/loadTime/renderDelay) needs a TTFB baseline from the hard nav, which doesn't really apply to a soft navigation. Left it undefined for now. Would it make more sense to have a soft-nav-specific breakdown eventually, or is undefined fine long term?

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 181.27 KiB 182.25 KiB +1002 B +0.54%
Rum Profiler 8.43 KiB 8.43 KiB 0 B 0.00%
Rum Recorder 21.12 KiB 21.12 KiB 0 B 0.00%
Logs 57.50 KiB 57.53 KiB +36 B +0.06%
Rum Salesforce N/A 140.18 KiB N/A N/A N/A
Rum Slim 139.21 KiB 140.18 KiB +990 B +0.69%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 201.96 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.43 KiB N/A N/A N/A
Rum-shopify Recorder N/A 3.72 KiB N/A N/A N/A

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Aug 20, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

🚦 2 Pipeline jobs failed

DataDog/browser-sdk | e2e: [chromium-pinned] — ❌ 3 tests failed · 🔧 Needs a code fix, caused by this PR

View in Datadog · View in GitLab

❌ soft navigation › reports LCP on a route_change view created from a user-initiated soft navigation › async from rum/softNavigation.scenario.ts   View in Datadog
expect(received).toBeGreaterThan(expected)

[chromium-pinned] › rum/softNavigation.scenario.ts:17:13 › soft navigation › reports LCP on a route_change view created from a user-initiated soft navigation › async 

    Error: expect(received).toBeGreaterThan(expected)

    Matcher error: received value must be a number or bigint

    Received has value: undefined

...
❌ soft navigation › reports LCP on a route_change view created from a user-initiated soft navigation › bundle from rum/softNavigation.scenario.ts   View in Datadog
expect(received).toBeGreaterThan(expected)

[chromium-pinned] › rum/softNavigation.scenario.ts:17:13 › soft navigation › reports LCP on a route_change view created from a user-initiated soft navigation › bundle 

    Error: expect(received).toBeGreaterThan(expected)

    Matcher error: received value must be a number or bigint

    Received has value: undefined

...
❌ soft navigation › reports LCP on a route_change view created from a user-initiated soft navigation › npm from rum/softNavigation.scenario.ts   View in Datadog
expect(received).toBeGreaterThan(expected)

[chromium-pinned] › rum/softNavigation.scenario.ts:17:13 › soft navigation › reports LCP on a route_change view created from a user-initiated soft navigation › npm 

    Error: expect(received).toBeGreaterThan(expected)

    Matcher error: received value must be a number or bigint

    Received has value: undefined

...
DataDog/browser-sdk | format — 🔧 Needs a code fix, caused by this PR

View in Datadog · View in GitLab

ℹ️ Info

No other issues found (see more)

❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 85.37%
Overall Coverage: 77.25% (+0.03%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 7a7ccd7 | Docs | View more details | Give us feedback!

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.

1 participant