Conversation
dschom
commented
Aug 12, 2026
dschom
commented
Aug 12, 2026
dschom
force-pushed
the
FXA-14132
branch
2 times, most recently
from
August 12, 2026 16:11
5b78317 to
503fe67
Compare
Because:
- After scanning the Sync pairing QR code, the desktop device approval page
(/pair/auth/allow) had no entrypoint query param, so Glean's
session.entrypoint was empty on cad_approve_device.view and .submit.
- Firefox opens that page as a brand-new navigation carrying only client_id,
scope, email, uid, channel_id and redirect_uri, so the web app had no
entrypoint to propagate.
- Two in-app paths dropped the entrypoint before pairing even began: /pair's
sync sign-in round trip, and the connect-another-device redirect.
This commit:
- Adds lib/pairing-attribution.ts, which stashes the attribution params
(entrypoint, entrypoint_experiment, entrypoint_variation and the five utm_*)
in namespaced localStorage with a 30 minute TTL when /pair hands control to
Firefox, then restores them onto the pairing-authority URL.
- Restores at bootstrap, before BrowserRouter mounts, because UrlQueryData
writes via a raw history.replaceState that react-router never observes, so a
later write would be clobbered by the first navigateWithQuery().
- Defaults entrypoint to 'preferences' when nothing was stashed, covering
pairings started straight from Firefox's about:preferences dialog.
- Merges the attribution into /pair's sync OAuth handoff URL so it survives the
sign-in round trip, which also repairs /pair's own post-signin metrics.
- Makes the connect-another-device redirect to /pair carry its query params,
matching the four sibling hardNavigate('/pair', {}, true) call sites.
- Adds unit coverage for the stash, TTL, merge and bootstrap rewrite, a
Playwright spec asserting the approval URL and the cad_approve_device.view
ping, and first-time coverage of the CAD bootstrap redirect.
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.
Because
to Firefox?" approval page (
/pair/auth/allow) carried noentrypoint, soGlean's
session.entrypointwas empty oncad_approve_device.viewandcad_approve_device.submit. Approval-page conversion could not be segmentedby the surface the pairing started from.
client_id,scope,email,uid,channel_idandredirect_uri, sonothing in the web app had an entrypoint to propagate.
entrypoint too, so the fix needs to cover them rather than just the approval
page.
This pull request
packages/fxa-settings/src/lib/pairing-attribution.ts./pairstashesthe eight attribution params (
entrypoint,entrypoint_experiment,entrypoint_variation, fiveutm_*) into namespaced localStorage with a30 minute TTL at the moment it hands control to Firefox chrome; the app
bootstrap restores them onto the pairing-authority URL.
restorePairingAttribution()as the first statement ofpackages/fxa-settings/src/index.tsx, beforeBrowserRoutermounts.UrlQueryDatawrites via a rawhistory.replaceStatethat react-router neverobserves, so writing through
integration.datalater would be clobbered bythe first
navigateWithQuery(). This mirrors the existingpublic/query-fix.jsprecedent and makes the URL the single source of truth.entrypointtopreferenceswhen nothing was stashed, for pairingsstarted straight from Firefox's
about:preferencesdialog.packages/fxa-settings/src/pages/Pair/Index/index.tsx, so it survives thesign-in round trip.
buildSyncOAuthSearchemits OAuth params only, so withoutthis the sign-in-first path lost the entrypoint before pairing began. This
also repairs
/pair's own post-signin metrics.hardNavigate('/pair')tohardNavigate('/pair', {}, true)inpackages/fxa-settings/src/pages/ConnectAnotherDevice/index.tsxso theredirect keeps its query params.
packages/fxa-settings/src/lib/pairing-attribution.test.ts(36 tests),extends the
/pairand CAD suites, and addspackages/functional-tests/tests/pairing/pairAttribution.spec.tsassertingboth the approval URL and the
cad_approve_device.viewping.Issue that this pull request solves
Closes: FXA-14132
Checklist
Put an
xin the boxes that applyHow to review (Optional)
lib/pairing-attribution.tsis the wholefeature; the other production changes are three small call sites.
lib/pairing-attribution.ts→index.tsx(bootstrap) →
pages/Pair/Index/index.tsx→pages/ConnectAnotherDevice/index.tsx→ tests.happen before
BrowserRoutermounts; anything later is invisible toreact-router and gets clobbered by
navigateWithQuery. Also worth a look:applyPairingAttributionappends rather than re-serializing the query string,because
URLSearchParams.toString()form-encodes (space →+) and wouldsilently rewrite params
query-fix.jsalready normalized.Screenshots (Optional)
N/A — no user-visible change. The only
.tsxedits are inside callbacks and aredirect; nothing rendered changes.
Other information (Optional)
Behaviour change slightly wider than Glean. The restore lands before
flowQueryParamsis read inindex.tsx, so the flow/amplitude path and theauth-server's
session_entrypointnow receive these params too. That isdesirable — client and server Glean finally agree on the entrypoint — but it is
broader than the ticket and worth a look.
Scope note. The ticket asks only about the approval-page URL. This also
fixes two adjacent hops (the Sync OAuth sign-in round trip, and CAD →
/pair)that would otherwise drop the entrypoint before it ever reached the stash, so
the approval-page fix would not actually work for the sign-in-first path
without them.
Functional test not yet run.
pairAttribution.spec.tsneeds a local stack.Everything else is verified: full
fxa-settingssuite green at 266 suites /2927 tests, plus typecheck, lint and Prettier.
Backbone pair flow intentionally unfixed. Per CLAUDE.md,
fxa-settingsispreferred and
fxa-content-serverviews/pair/*is legacy.pairandpair/auth/alloware one route group withfullProdRollout: true, so a serverconfig serves both from the same stack; a flag flip mid-flow degrades
gracefully to the
preferencesfallback.Judgment call for a second opinion: the CAD redirect uses
hardNavigate('/pair', {}, true), which forwards the whole query string ratherthan only the eight attribution params. This matches the four sibling
hardNavigate('/pair', {}, true)call sites andgetSyncNavigate, and it keepsflow_id/flow_begin_time/device_idjoined per.claude/rules/metrics-flow.md. Narrowing it would make CAD the only callerthat strips params. Happy to change it if reviewers prefer.
Surfaced but not fixed here
lib/integrations/integration-factory.ts:244-249— theentryPoint→entrypointnormalization only fires when both keys arepresent, so it can never rescue an
entryPoint-only URL, and when both arepresent it overwrites the lowercase value. Side-stepped in
pickPairingAttributionrather than edited; deserves its own ticket.pages/Pair/AuthComplete/index.tsx:67gates a Send Tab copyvariant on the entrypoint, which was permanently
falsein production forthe same root cause. This fix revives it, so those strings will render for
genuine
send-tab-*journeys for the first time — worth eyeballing.about:preferenceswithin 30 minutes of a/pairvisit inherits the earlierentrypoint. Deliberate: clearing on read would make
.viewand.submitdisagree.