Skip to content

fix(experiment-tag): defer history API url change handler to avoid mid-navigation DOM mutations - #318

Open
tyiuhc wants to merge 3 commits into
mainfrom
web/mfe-fix
Open

fix(experiment-tag): defer history API url change handler to avoid mid-navigation DOM mutations#318
tyiuhc wants to merge 3 commits into
mainfrom
web/mfe-fix

Conversation

@tyiuhc

@tyiuhc tyiuhc commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

The SDK wraps history.pushState and history.replaceState to detect SPA navigations and re-evaluate experiments. Previously the handleUrlChange() callback — which runs applyVariants() and applies DOM mutations via dom-mutator — was called synchronously inside those wrappers.

This meant DOM mutations fired mid-navigation, before the calling framework (e.g. Angular, Vue, or any microfrontend router) had finished its own navigation cycle. In Angular specifically, this interleaved with component initialization: mutations ran while bindings were still being set up, causing the async pipe to receive a non-Observable value and throw:

You provided an invalid object where a stream was expected.

Fix: replace the synchronous call with setTimeout(handleUrlChange, 0), deferring experiment re-evaluation to the next event loop task. By that point the framework's navigation cycle — guards, resolvers, component creation — has fully completed and DOM bindings are stable.

Tradeoff: experiment mutations on SPA navigations apply ~1 event loop tick later than before. Anti-flicker on initial page load is unaffected (handled separately).

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?: No

Note

Medium Risk
Touches core SPA navigation instrumentation for all customers using the default handler; timing shift is small but could affect race-sensitive UIs, while fixing real framework breakage.

Overview
Defers SPA experiment re-application when navigation uses history.pushState or history.replaceState: handleUrlChange() (which publishes url_change and drives variant re-apply / DOM mutations) now runs via setTimeout(..., 0) instead of synchronously inside the history wrappers.

That lets the host router/framework finish its navigation/render pass before the SDK mutates the DOM, avoiding interleaved updates that broke Angular bindings (e.g. async pipe errors). popstate handling is unchanged (still immediate). Experiment updates on programmatic navigations may land one macrotask later; initial-load anti-flicker paths are outside this hook.

Reviewed by Cursor Bugbot for commit af1a093. Bugbot is set up for automated code reviews on this repo. Configure here.

@tyiuhc tyiuhc changed the title fix: defer history API url change handler to avoid mid-navigation DOM mutations fix(experiment-tag): defer history API url change handler to avoid mid-navigation DOM mutations May 12, 2026

@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 prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Internal ticket reference in public source code comment
    • Removed the internal Jira ticket reference (SKY-9617) from the comment while preserving the technical explanation of why setTimeout is used.

Create PR

Or push these changes by commenting:

@cursor push 0f39ec6aef
Preview (0f39ec6aef)
diff --git a/packages/experiment-tag/src/subscriptions/subscriptions.ts b/packages/experiment-tag/src/subscriptions/subscriptions.ts
--- a/packages/experiment-tag/src/subscriptions/subscriptions.ts
+++ b/packages/experiment-tag/src/subscriptions/subscriptions.ts
@@ -860,7 +860,7 @@
         // navigation cycle before we apply DOM mutations. Running synchronously
         // inside pushState interleaves with framework rendering and can corrupt
         // component bindings (e.g. Angular async pipe receiving non-Observable
-        // values — SKY-9617).
+        // values).
         setTimeout(handleUrlChange, 0);
         return result;
       };

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 560e10b. Configure here.

Comment thread packages/experiment-tag/src/subscriptions/subscriptions.ts Outdated
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