Skip to content

feat(session-replay): enhance targeting evaluation with page URL#1571

Open
bravecod wants to merge 12 commits intomainfrom
SR-2702-Add-page-URL-targeting-condition-type
Open

feat(session-replay): enhance targeting evaluation with page URL#1571
bravecod wants to merge 12 commits intomainfrom
SR-2702-Add-page-URL-targeting-condition-type

Conversation

@bravecod
Copy link
Collaborator

@bravecod bravecod commented Mar 4, 2026

feat(session-replay): URL-based targeting re-evaluation and 100% test coverage

URL-based targeting

  • Re-evaluate session-replay targeting when the URL changes (SPA navigation).
  • Use shared subscribeToUrlChanges from the URL tracking plugin; on URL change call evaluateTargetingAndCapture(..., false, true) and skip IDB cache in evaluateTargetingAndStore when urlChange is true.
  • Stop recording when targeting no longer matches after re-evaluation.

session-replay-browser

  • setupUrlChangeListenerForTargeting() subscribes to URL changes and cleans up on shutdown.
  • Removed redundant globalScope.location?.href in URL plugin (already guarded by early return).
  • Removed debug console.log from URL plugin.

Tests

  • URL-change listener: no location / null scope, callback invokes evaluateTargetingAndCapture, cleanup on shutdown, no subscribe when no targetingConfig.
  • forceRestart + stop recording when no longer matching.
  • evaluateTargetingAndCapture with empty/undefined page.
  • URL plugin: polling, second subscriber, no history, pushState/replaceState with null url, getHref with empty href.
  • Fixed unbound-method lint in url-tracking-plugin tests.

Coverage

  • 100% statements, branches, functions, and lines for session-replay-browser, targeting, and plugin-session-replay-browser.
  • Addressed unreachable branches and removed dead code in URL plugin.

Note

Medium Risk
Changes when/why session replay starts recording by adding URL-driven targeting re-evaluations and new shared history/listener patching; regressions could affect capture rates or introduce subtle listener/async ordering issues.

Overview
Session Replay targeting now receives page.url context and can re-evaluate on SPA navigation: SessionReplay.evaluateTargetingAndCapture accepts page, passes it through to @amplitude/targeting, and SessionReplay subscribes to URL changes (with cleanup on re-init/shutdown and stale-evaluation protection) to queue forced re-evaluations.

The URL tracking plugin is refactored around a new shared subscribeToUrlChanges helper (history patching + popstate/hashchange or polling) to avoid double-patching and to share subscriptions across consumers, and targeting’s IDB cache is bypassed on URL-change re-evaluations. Tests are expanded substantially across plugin-session-replay-browser, session-replay-browser, url-tracking-plugin, and targeting (including tsconfig test inclusion) to cover the new behaviors and race conditions.

Written by Cursor Bugbot for commit 0a9bddb. This will update automatically on new commits. Configure here.

- Updated `evaluateTargetingAndCapture` to include page URL from `getGlobalScope`.
- Modified related types and functions to accommodate the new `page` parameter.
- Added tests to verify correct behavior when page URL is provided or absent.
@bravecod bravecod requested a review from a team as a code owner March 4, 2026 00:16
@jpollock-ampl
Copy link
Collaborator

bugbot run

/** Page data passed into targeting so the evaluator can use the current URL (or other page data). */
export interface TargetingPage {
url?: string;
}
Copy link

Choose a reason for hiding this comment

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

New TargetingPage type not exported from package

Low Severity

The new TargetingPage interface is defined with export in targeting.ts but is not re-exported from the package's index.ts. The index file exports TargetingFlag and TargetingParameters but omits TargetingPage, making it inaccessible to consumers who may want to type variables with the page structure directly. Other sibling types like TargetingFlag are properly re-exported.

Fix in Cursor Fix in Web

Copy link
Collaborator

@lewgordon-amplitude lewgordon-amplitude left a comment

Choose a reason for hiding this comment

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

Chatted about this in person. We want to hook this into url tracking so that we can capture when the url changes not just on events or init.

@bravecod
Copy link
Collaborator Author

bravecod commented Mar 4, 2026

Chatted about this in person. We want to hook this into url tracking so that we can capture when the url changes not just on events or init.

URL tracking (plugin / rrweb)

  • The URL tracking plugin is created by createUrlTrackingPlugin() and passed to rrweb’s record() as a plugin.
  • When recording starts, rrweb calls the plugin’s observer once with an emit callback and globalScope (window).
  • The observer defines emitUrlChange: it gets the current URL (via getCurrentUrl()), compares it to lastTrackedUrl, and if it changed (or is the first run) it builds a URL-change event (href, title, viewport) and calls the emit callback.
  • URL changes are detected via subscribeToUrlChanges(globalScope, emitUrlChange, options):
    • Event-based (default): patches history.pushState / replaceState and listens to popstate and hashchange; each change invokes the callback with the new href.
    • Polling (optional): when enablePolling: true, uses setInterval to periodically invoke the callback with the current href.
  • The observer calls emitUrlChange() once when it runs (initial URL), then relies on subscribeToUrlChanges for later changes. The returned cleanup unsubscribes (and clears the interval if polling).
  • UGC filter rules can be applied to the href before it’s emitted. Multiple plugins/subscribers share the same history patch per window via a WeakMap.

URL-based targeting re-evaluation (URL eval)

  • When session replay initializes with a targeting config, it calls setupUrlChangeListenerForTargeting(): gets globalScope (window), returns early if there’s no location, then calls subscribeToUrlChanges(globalScope, onUrlChange) (no polling).
  • onUrlChange is a callback that, when the URL changes, calls evaluateTargetingAndCapture({ userProperties: {}, event: undefined, page: { url: href } }, false, true) — i.e. re-evaluate targeting for the new URL with forceRestart = true.
  • In evaluateTargetingAndStore (targeting-manager), when urlChange is true we skip the IDB cache and always re-run the targeting evaluation so the decision reflects the new page.
  • If the new evaluation says the user no longer matches and we were recording, we call stopRecordingEvents() (and log that we’re stopping because targeting no longer matches). If they match, we may start or keep recording.
  • The cleanup returned by subscribeToUrlChanges is stored (e.g. as urlChangeCleanup) and invoked on shutdown so we don’t leave history/polling listeners or re-evaluation running after session replay is torn down.

@bravecod
Copy link
Collaborator Author

bravecod commented Mar 5, 2026

  • Targeting receives page.url consistently.
  • URL changes can start recording when a route matches targeting.
  • Recording remains active for the rest of the session after first match.

Copy link

@cursor cursor bot left a comment

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 2 potential issues.

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

Copy link

@cursor cursor bot left a comment

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.

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

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.

3 participants