Skip to content

feat: centralize soft navigation handling in EmbracePageManager#1418

Merged
joaquin-diaz merged 10 commits into
mainfrom
joaquin-diaz/feat/EMBR-12647-page-manager-soft-nav-changes
Jul 16, 2026
Merged

feat: centralize soft navigation handling in EmbracePageManager#1418
joaquin-diaz merged 10 commits into
mainfrom
joaquin-diaz/feat/EMBR-12647-page-manager-soft-nav-changes

Conversation

@joaquin-diaz

Copy link
Copy Markdown
Contributor

What problem is this solving?

Route (ux.surface) span creation/ending was split across two independent, unordered actors: EmbraceUserSessionManager reacting to the Navigation API's currententrychange to roll over session parts, and NavigationInstrumentation reacting to react-router calling setCurrentRoute directly. Since neither ordering was guaranteed, a soft navigation could produce a spurious extra route span or incorrectly split a route span across a session-part boundary. React-router integrations also had a direct, unnecessary dependency on NavigationInstrumentation.

Note: this removes public API surface (getNavigationInstrumentation, createReactRouterNavigationInstrumentation, NavigationInstrumentation.setCurrentRoute/setInstrumentationType) — a breaking change for any manual integrator calling these directly.

Short description of changes

  • EmbracePageManager is now the single source of truth for the current route and the sole listener of the Navigation API. On soft navigation, it rolls over the session part (via a newly-public rolloverSessionPartInternal on EmbraceUserSessionManager) before setting a placeholder route, so the outgoing route span and outgoing session-part span end in the correct order/batch.
  • React-router integrations (withEmbraceRouting, withEmbraceRoutingLegacy, listenToRouterChanges) now only call page.setCurrentRoute(...) — no more dependency on NavigationInstrumentation.
  • NavigationInstrumentation is simplified to a pure mirror: it reacts to page.addRouteChangedListener (same url → rename span in place, different url → end old span + start new one) and to session-part-ended (so a route span never outlives the session part it started in). It's now wired up automatically as a default instrumentation via setupDefaultInstrumentations/initSDK instead of being manually constructed by consumers.
  • EmbraceUserSessionManager no longer knows the Navigation API exists — that listening moved to EmbracePageManager.
  • Dropped the emb.instrumentation attribute on route spans (which react-router flavor reported the route); removed the now-dead EMB_NAVIGATION_INSTRUMENTATIONS enum.

@joaquin-diaz joaquin-diaz changed the title EMBR-12647 feat: centralize soft navigation handling in EmbracePageManager feat: centralize soft navigation handling in EmbracePageManager Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
Chrome DevTools Protocol Tracing (Script: 115.63ms, Heap: 13.20MB)
Number of Requests Size of Requests Script Duration Task Duration Heap Used Size
Requests +13 requests +42.74 KB
Page Loaded +19.18 ms +17.92 ms +1.19 MB
Generate 100 fetch requests +21.14 ms +86.46 ms +1.71 MB
Generate 100 XHR requests +38.60 ms +93.22 ms +2.80 MB
Click 100 buttons and generate 100 logs +25.44 ms +57.10 ms +2.64 MB
Throw a 100 exceptions +0.89 ms +30.07 ms +2.65 MB
End Session +10.39 ms +36.39 ms +2.21 MB
Total +13 requests +42.74 KB +115.63 ms +321.16 ms +13.20 MB
Lighthouse (Script Eval: 128.62ms)
Difference Description
Total Blocking Time +2 ms Difference in Total Blocking Time: Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. Learn more about the Total Blocking Time metric.
Main Thread Time +103.99 ms Difference in Main Thread Time: Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this. Learn how to minimize main-thread work
Script Evaluation Time +128.62 ms Difference in Script Evaluation Time: Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. Learn how to reduce Javascript execution time.
Platform Tests (vite-7 es2015 gzip: 70.77KB)

vite-6 Platform Tests

Total Uncompressed Size Total Gzip Size
vite-6 - es2015 +209.27 KB +70.83 KB

vite-7 Platform Tests

Total Uncompressed Size Total Gzip Size
vite-7 - es2015 +209.27 KB +70.77 KB

webpack-5 Platform Tests

Total Uncompressed Size Total Gzip Size
webpack-5 - es2015 0 KB 0 KB

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.27273% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.73%. Comparing base (d25ec02) to head (461c3a6).

Files with missing lines Patch % Lines
...gationInstrumentation/NavigationInstrumentation.ts 97.59% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1418   +/-   ##
=======================================
  Coverage   96.73%   96.73%           
=======================================
  Files         234      233    -1     
  Lines       10126    10183   +57     
  Branches     1423     1436   +13     
=======================================
+ Hits         9795     9851   +56     
- Misses        329      330    +1     
  Partials        2        2           
Files with missing lines Coverage Δ
...pi-page/manager/NoOpPageManager/NoOpPageManager.ts 100.00% <100.00%> (ø)
...-page/manager/ProxyPageManager/ProxyPageManager.ts 95.45% <100.00%> (+0.45%) ⬆️
packages/web-sdk/src/api-sessions/index.ts 100.00% <100.00%> (ø)
...r/NoOpUserSessionManager/NoOpUserSessionManager.ts 92.92% <100.00%> (+0.46%) ⬆️
...ProxyUserSessionManager/ProxyUserSessionManager.ts 92.30% <100.00%> (+0.52%) ⬆️
packages/web-sdk/src/api-sessions/manager/index.ts 100.00% <100.00%> (ø)
packages/web-sdk/src/constants/attributes.ts 100.00% <ø> (ø)
packages/web-sdk/src/constants/index.ts 100.00% <ø> (ø)
packages/web-sdk/src/index.ts 100.00% <ø> (ø)
...eInstrumentationBase/EmbraceInstrumentationBase.ts 97.07% <100.00%> (+0.08%) ⬆️
... and 13 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@joaquin-diaz
joaquin-diaz marked this pull request as ready for review July 13, 2026 17:19
@joaquin-diaz
joaquin-diaz requested a review from a team as a code owner July 13, 2026 17:19

@jpmunz jpmunz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For:

Note: this removes public API surface (getNavigationInstrumentation, createReactRouterNavigationInstrumentation, NavigationInstrumentation.setCurrentRoute/setInstrumentationType) — a breaking change for any manual integrator calling these directly.

Since we're not doing a major version bump can we make these non-breaking? Since Navigation Instrumentation is being setup automatically now if we switched these to no-ops would we preserve the same behaviour as today?

For:

React-router integrations (withEmbraceRouting, withEmbraceRoutingLegacy, listenToRouterChanges) now only call page.setCurrentRoute(...) — no more dependency on NavigationInstrumentation.

Does this collide with the new soft nav detection? E.g. would we create 1 route span from the soft nav detection and then end up creating a 2nd from the React-router?

// A route span must not outlive the session part it started in — e.g. the
// tab backgrounds, or the session ends, with no further navigation to
// trigger _onRouteChanged. Only the ending side is wired up: resuming a
// span on session-part-start was deliberately dropped in favor of relying

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this would be a behaviour change from before then right?

If we have:

Hard load Page A -> background tab -> foreground tab -> soft navigate to Page B -> background tab

Previously we would end up with:

  • SurfaceSpan1: Hard load Page A -> background tab
  • SurfaceSpan2: foreground tab -> soft navigate to Page B
  • SurfaceSpan3: soft navigate to Page B -> background tab

Now is the behaviour this?:

  • SurfaceSpan1: Hard load Page A -> background tab
  • SurfaceSpan2: soft navigate to Page B -> background tab

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.

That's updated now. It should behave the same as it was doing when the react instrumentations where in charge of ending the route spans

);
}

if (!config.omit?.has('navigation')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we be allowing this to be omittable?

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.

Nope, not anymore given that it's doing session part rollover. We can wait for user feedback for another ways of disabling or changing how soft nav works

Comment on lines +5 to +9
Centralizing both in one place is what avoids the race that used to exist
between the session-part lifecycle and route-span creation: previously two
independent actors (the session manager reacting to the Navigation API, and
the navigation instrumentation reacting to route reports) could observe the
same soft navigation in either order. Now there's one actor driving both.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this context is not needed once we complete the refactor

Suggested change
Centralizing both in one place is what avoids the race that used to exist
between the session-part lifecycle and route-span creation: previously two
independent actors (the session manager reacting to the Navigation API, and
the navigation instrumentation reacting to route reports) could observe the
same soft navigation in either order. Now there's one actor driving both.

Comment on lines +28 to +29
- **`EmbraceUserSessionManager`** — owns session-part start/end/rollover, but
no longer knows the Navigation API exists.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- **`EmbraceUserSessionManager`** — owns session-part start/end/rollover, but
no longer knows the Navigation API exists.
- **`EmbraceUserSessionManager`** — owns session-part start/end/rollover, no interaction with Navigation API

Comment on lines +90 to +94
Resuming a span on the next session-part-*start* was deliberately dropped:
if the same route is still current when a new part starts, the next
`setCurrentRoute` report (or, for frameworks that only report on navigation,
none at all) is what determines whether a span reopens — not the session
part boundary itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Resuming a span on the next session-part-*start* was deliberately dropped:
if the same route is still current when a new part starts, the next
`setCurrentRoute` report (or, for frameworks that only report on navigation,
none at all) is what determines whether a span reopens — not the session
part boundary itself.
Route spans are not started on the next session-part-*start*:
if the same route is still current when a new part starts, the next
`setCurrentRoute` report (or, for frameworks that only report on navigation,
none at all) is what determines whether a span reopens — not the session
part boundary itself.

// span before the rollover, so the session-part-ended listener would
// incorrectly close the placeholder instead of the outgoing span.
this._userSessionManager?.rolloverSessionPartInternal({
endReason: 'web_soft_navigation',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since we're rolling over wouldn't there only ever be a single reason for both the end and start?

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.

Yeah but I think it doesn't hurt to keep the option open for different reasons just in case

@joaquin-diaz
joaquin-diaz force-pushed the joaquin-diaz/feat/EMBR-12647-page-manager-soft-nav-changes branch 2 times, most recently from cb61ab0 to ab81e9b Compare July 14, 2026 14:31
@joaquin-diaz
joaquin-diaz force-pushed the joaquin-diaz/feat/EMBR-12647-page-manager-soft-nav-changes branch from 1dcc6eb to c99c4a8 Compare July 15, 2026 14:43
@joaquin-diaz

Copy link
Copy Markdown
Contributor Author

@jpmunz Updated the readme so it represents current state instead of how we got there or other changes

@jpmunz jpmunz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A couple follow-up comments but overall lgtm

return;
}

updateZeroTimeMillis(window.performance.timeOrigin + event.timeStamp);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think maybe for consistency we should have:

Suggested change
updateZeroTimeMillis(window.performance.timeOrigin + event.timeStamp);
updateZeroTimeMillis(epochMillisFromOrigin(event.timeStamp));

just so that nothing outside of the PerformanceManager is referencing timeOrigin directly

the same reset `initSDK` performs on `pageshow` for bfcache restores.
- **React-router integrations** (`withEmbraceRouting`,
`withEmbraceRoutingLegacy`, `listenToRouterChanges`) — call
`page.setCurrentRoute({ path, url })` once they've resolved the templated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this have a possibility of colliding with the soft nav detection? E.g. if this is set from the router instrumentation first and then later the soft nav detection doesn't trigger because the url hasn't updated from the page managers perspective? If this is triggered by the router without a soft nav being detected at all would it end up creating a 2nd route span in the same session part?

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.

Good thinking, I had to validate this. Calling setCurrentRoute does not create a route span directly. We have two guards preventing this:

  1. Prevents creating a new route id in EmbracePageManager
public setCurrentRoute = (route: Route) => {
  if (!this._currentRoute || this._currentRoute.url !== route.url) {
    this._currentPageId = generateUUID();
  }
  
  ..
  1. Prevents creating a route span on a same URL
private readonly _onRouteChanged = (route: Route): void => {
  if (!this._config.enabled) {
    return;
  }

  if (this._currentRouteSpan && this._currentRouteSpanUrl === route.url) {
    // Same url as the currently open span: either the templated path just
    // resolved (rename in place), or a redundant re-render — no-op if the
    // path hasn't actually changed.
    if (this._currentRouteSpanPath !== route.path) {
      this._renameCurrentRouteSpan(route.path);
    }
    return;
  }
  
  this._endRouteSpan();
  this._startRouteSpan(route);
};

@joaquin-diaz
joaquin-diaz merged commit 49ebdb2 into main Jul 16, 2026
20 checks passed
@joaquin-diaz
joaquin-diaz deleted the joaquin-diaz/feat/EMBR-12647-page-manager-soft-nav-changes branch July 16, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants