Conversation
🦋 Changeset detectedLatest commit: a23f595 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codecov Report✅ All modified and coverable lines are covered by tests. Please upload reports for the commit a23f595 to get more accurate results. Additional details and impacted files@@ Coverage Diff @@
## master #1397 +/- ##
==========================================
+ Coverage 91.59% 91.67% +0.07%
==========================================
Files 127 127
Lines 4142 4155 +13
Branches 1033 1036 +3
==========================================
+ Hits 3794 3809 +15
+ Misses 348 346 -2
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tld() resolving the shared, cross-subdomain cookie domain requires a live cookie set/get/remove round-trip, and multiple CookieStorage instances get constructed per page load (identity + legacy stores, x2 for Group). Each one was re-running that probe from scratch, so a single transient failure (extension interference, cookie-jar pressure, timing) could silently downgrade just that instance to a host-only cookie while others kept the shared one -- a plausible source of user reports where anonymousId/userId intermittently loses cross-subdomain persistence (see #706). - memoize tld()'s resolved domain per hostname instead of re-probing cookies on every CookieStorage instantiation - warn instead of silently falling back to a host-only cookie when tld() fails to resolve a domain (as opposed to the expected undefined result for localhost/IP hosts) - let CookieStorage.remove()/set(key, null) also clean up a stray host-only duplicate cookie, which was previously unreachable even via reset() No behavior change on any currently-working path; these only do anything in a scenario that was already broken.
abueide
force-pushed
the
fix/anonymous-id-cross-subdomain-persistence
branch
from
September 10, 2026 15:36
f73db75 to
316335d
Compare
5 tasks
The memoization cache, warning, and CookieStorage cleanup fallback add a few dozen bytes minified+gzipped, tripping the size-limit check (master sits at 29.78 KB against a 29.8 KB budget, so there was ~20 B of headroom before this). Bumped 29.8 KB -> 30.0 KB; confirmed locally (npx size-limit in packages/browser) that actual size is ~29.95 KB.
abueide
added a commit
that referenced
this pull request
Sep 11, 2026
…kie disagree (#1398) ## Summary Split out from #1397, which addresses one source of the underlying `anonymousId`/`userId` cross-subdomain divergence issue (see #706). This PR addresses the actual store-reconciliation gap. `localStorage` is per-origin, but sits ahead of the shared, cross-subdomain cookie in the default store priority (`[localStorage, cookie, memory]`). Once the two disagree -- for *any* reason (cookie eviction, a consent tool clearing cookies pre-consent, a past `tld()` failure, a user clearing cookies/site data on just one subdomain, etc.) -- whichever value is in `localStorage` wins forever on that origin, with no way for it to resync to the cookie. Each origin can then end up parroting a different `anonymousId` back into the shared cookie, causing it to visibly ping-pong between subdomains. - Added **`UniversalStorage#getConsistent`**: when the underlying stores disagree, the cookie's value wins and every store is resynced to it, instead of the divergence persisting indefinitely. When stores already agree (the common case), this returns the exact same value `getAndSync` would. - An empty string is treated the same as no value, so a cookie that was blanked out rather than deleted (e.g. a third-party script running `document.cookie = 'ajs_anonymous_id=;path=/'` with no expiry) can't win a disagreement and wipe out a real id elsewhere. - **Gated behind a new opt-in `resolveAnonymousIdConflicts` option** (`user: { resolveAnonymousIdConflicts: true }` in load options), **off by default**. `anonymousId()` only switches from `getAndSync` to `getConsistent` when it's enabled. This ships in a release without changing anyone's behavior; it can be staged in with specific customers/accounts before considering flipping the default in a later release. (There's no infra-level staged-rollout mechanism in this repo to hook into for a true percentage rollout -- this opt-in flag is the equivalent we can offer at the application-config level.) ### A deterministic reproduction (from #706's own report) The original reporter on #706 already described a concrete, deterministic trigger, independent of any probe flakiness: 1. User is on subdomain A. They clear cookies + site data for that origin. This deletes the **shared** cookie entirely (its `Domain` covers both subdomains, so it's visible/clearable from A) and subdomain A's **own** localStorage entry (localStorage is origin-scoped, so this doesn't touch B). 2. Subdomain B was never visited during this -- its localStorage still holds the old id, untouched. 3. Back on A: both localStorage(A) and the cookie are empty, so a fresh id (`NEW`) is generated and written to both. 4. User navigates to B: localStorage(B) still has the old id (`OLD`), the cookie now has `NEW`. localStorage outranks the cookie in priority order, so B returns `OLD` -- and `getAndSync` writes that back to *every* store, overwriting the shared cookie with `OLD`. 5. User navigates back to A: localStorage(A) has `NEW`, the cookie now has `OLD` (just clobbered in step 4) -- A's localStorage wins, returns `NEW`, overwrites the cookie back to `NEW`. Steps 4-5 repeat indefinitely -- exactly the reporter's "the cookie value for the TLD seemingly switching between these values as you navigate between the pages," with no probe failure or flakiness required. With `resolveAnonymousIdConflicts` enabled: the next visit to either subdomain reads both stores, sees they disagree, and takes the cookie's value -- resyncing the losing localStorage entry to match. Both subdomains converge on the same id after one round trip and stay converged; the ping-pong stops for good, regardless of which id happened to win. ## Test plan - [x] Added unit tests for `UniversalStorage#getConsistent`, including the empty-string edge case (`packages/browser/src/core/storage/__tests__/universalStorage.test.ts`) - [x] Added `User` tests confirming the flag defaults off (unchanged behavior) and confirming the reported drift scenario self-heals when enabled (`packages/browser/src/core/user/__tests__/index.test.ts`) - [x] Added a Playwright e2e test reproducing the exact #706 sequence across two real (mocked) same-site subdomains, confirming both the bug (without the flag) and the fix (with it) (`packages/browser-integration-tests/src/anonymous-id-subdomain-sync.test.ts`) - [x] Full `packages/browser` test suite passes - [x] `tsc --noEmit` and `eslint` clean on all touched files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part of investigating a support ticket where a customer's
anonymousIdintermittently changed when moving between subdomains of the same site, with noreset()call involved. That's the same underlying issue as #706 (open since 2023). This PR addresses one contributing cause; a separate, more invasive fix for the actual store-reconciliation behavior is split into #1398 for independent review.tld()resolves the shared, cross-subdomain cookie domain via a live cookie set/get/remove round-trip, and analytics-next constructs multipleCookieStorageinstances per page load (identity + legacy stores, x2 forGroup). Each one was re-running that probe from scratch, so a single transient failure (extension interference, cookie-jar pressure, timing) could silently downgrade just that instance to a host-only cookie while the others kept the shared, cross-subdomain one -- a plausible source of the "intermittent, hard to reproduce" nature of these reports.tld()now memoizes the resolved domain per hostname, so the probe only runs once per page load instead of once per store instance.tld()now warns instead of silently falling back to a host-only cookie when the probe genuinely fails (as opposed to the expectedundefinedforlocalhost/IP hosts). This failure mode was previously invisible.CookieStorage.remove()/.set(key, null)now also clear a possible host-only duplicate. If atld()failure ever did leave a stray host-scoped cookie behind, nothing -- not evenreset()-- could clean it up, since removal only ever targeted the currently-configured domain.No behavior change on any currently-working path -- these only do anything in a scenario that was already broken. This alone reduces how often the underlying divergence occurs, but doesn't resync stores that have already diverged for other reasons (cookie eviction, a consent tool clearing cookies pre-consent, etc.) -- that's what #1398 adds.
Test plan
tld()memoization and the new warning (packages/browser/src/core/user/__tests__/tld.test.ts)CookieStoragecleanup fallback (packages/browser/src/core/storage/__tests__/cookieStorage.test.ts)packages/browsertest suite passes (847 passed, 4 pre-existing skips)tsc --noEmitandeslintclean on all touched files