Skip to content

fix(settings): Carry pairing attribution to the device approval page - #21011

Draft
dschom wants to merge 1 commit into
mainfrom
FXA-14132
Draft

fix(settings): Carry pairing attribution to the device approval page#21011
dschom wants to merge 1 commit into
mainfrom
FXA-14132

Conversation

@dschom

@dschom dschom commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Because

  • After a user scans the Sync pairing QR code, the desktop "Did you just sign in
    to Firefox?" approval page (/pair/auth/allow) carried no entrypoint, so
    Glean's session.entrypoint was empty on cad_approve_device.view and
    cad_approve_device.submit. Approval-page conversion could not be segmented
    by the surface the pairing started from.
  • Firefox itself opens that page as a brand-new navigation with only
    client_id, scope, email, uid, channel_id and redirect_uri, so
    nothing in the web app had an entrypoint to propagate.
  • Per the ticket comment, other paths into the pairing pages dropped the
    entrypoint too, so the fix needs to cover them rather than just the approval
    page.

This pull request

  • Adds packages/fxa-settings/src/lib/pairing-attribution.ts. /pair stashes
    the eight attribution params (entrypoint, entrypoint_experiment,
    entrypoint_variation, five utm_*) into namespaced localStorage with a
    30 minute TTL at the moment it hands control to Firefox chrome; the app
    bootstrap restores them onto the pairing-authority URL.
  • Calls restorePairingAttribution() as the first statement of
    packages/fxa-settings/src/index.tsx, before BrowserRouter mounts.
    UrlQueryData writes via a raw history.replaceState that react-router never
    observes, so writing through integration.data later would be clobbered by
    the first navigateWithQuery(). This mirrors the existing
    public/query-fix.js precedent and makes the URL the single source of truth.
  • Defaults entrypoint to preferences when nothing was stashed, for pairings
    started straight from Firefox's about:preferences dialog.
  • Merges the attribution into the sync OAuth handoff URL in
    packages/fxa-settings/src/pages/Pair/Index/index.tsx, so it survives the
    sign-in round trip. buildSyncOAuthSearch emits OAuth params only, so without
    this the sign-in-first path lost the entrypoint before pairing began. This
    also repairs /pair's own post-signin metrics.
  • Changes hardNavigate('/pair') to hardNavigate('/pair', {}, true) in
    packages/fxa-settings/src/pages/ConnectAnotherDevice/index.tsx so the
    redirect keeps its query params.
  • Adds packages/fxa-settings/src/lib/pairing-attribution.test.ts (36 tests),
    extends the /pair and CAD suites, and adds
    packages/functional-tests/tests/pairing/pairAttribution.spec.ts asserting
    both the approval URL and the cad_approve_device.view ping.

Issue that this pull request solves

Closes: FXA-14132

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on: lib/pairing-attribution.ts is the whole
    feature; the other production changes are three small call sites.
  • Suggested review order: lib/pairing-attribution.tsindex.tsx
    (bootstrap) → pages/Pair/Index/index.tsx
    pages/ConnectAnotherDevice/index.tsx → tests.
  • Risky or complex parts: the bootstrap ordering constraint. The restore must
    happen before BrowserRouter mounts; anything later is invisible to
    react-router and gets clobbered by navigateWithQuery. Also worth a look:
    applyPairingAttribution appends rather than re-serializing the query string,
    because URLSearchParams.toString() form-encodes (space → +) and would
    silently rewrite params query-fix.js already normalized.

Screenshots (Optional)

N/A — no user-visible change. The only .tsx edits are inside callbacks and a
redirect; nothing rendered changes.

Other information (Optional)

Behaviour change slightly wider than Glean. The restore lands before
flowQueryParams is read in index.tsx, so the flow/amplitude path and the
auth-server's session_entrypoint now receive these params too. That is
desirable — 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.ts needs a local stack.
Everything else is verified: full fxa-settings suite green at 266 suites /
2927 tests, plus typecheck, lint and Prettier.

Backbone pair flow intentionally unfixed. Per CLAUDE.md, fxa-settings is
preferred and fxa-content-server views/pair/* is legacy. pair and
pair/auth/allow are one route group with fullProdRollout: true, so a server
config serves both from the same stack; a flag flip mid-flow degrades
gracefully to the preferences fallback.

Judgment call for a second opinion: the CAD redirect uses
hardNavigate('/pair', {}, true), which forwards the whole query string rather
than only the eight attribution params. This matches the four sibling
hardNavigate('/pair', {}, true) call sites and getSyncNavigate, and it keeps
flow_id/flow_begin_time/device_id joined per
.claude/rules/metrics-flow.md. Narrowing it would make CAD the only caller
that strips params. Happy to change it if reviewers prefer.

Surfaced but not fixed here

  • lib/integrations/integration-factory.ts:244-249 — the
    entryPointentrypoint normalization only fires when both keys are
    present, so it can never rescue an entryPoint-only URL, and when both are
    present it overwrites the lowercase value. Side-stepped in
    pickPairingAttribution rather than edited; deserves its own ticket.
  • QA note: pages/Pair/AuthComplete/index.tsx:67 gates a Send Tab copy
    variant on the entrypoint, which was permanently false in production for
    the same root cause. This fix revives it, so those strings will render for
    genuine send-tab-* journeys for the first time — worth eyeballing.
  • The stash is TTL-expired but never cleared, so a pairing started from
    about:preferences within 30 minutes of a /pair visit inherits the earlier
    entrypoint. Deliberate: clearing on read would make .view and .submit
    disagree.

Comment thread packages/fxa-settings/src/lib/pairing-attribution.ts Outdated
Comment thread packages/fxa-settings/src/lib/pairing-attribution.ts Outdated
@dschom dschom changed the title fix(settings): carry pairing attribution to the device approval page fix(settings): Carry pairing attribution to the device approval page Aug 12, 2026
@dschom
dschom force-pushed the FXA-14132 branch 2 times, most recently from 5b78317 to 503fe67 Compare August 12, 2026 16:11
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.
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