fix(checkout): forward ad-attribution URL params to checkout#388
Merged
Conversation
Hydrogen storefronts live on one origin (`mystore.com`) and Shopify
checkout lives on another (`checkout.mystore.com` or
`mystore.myshopify.com`). When a buyer arrives via a paid ad and the
landing URL carries identifiers like `?gclid=...`, `?fbclid=...`, or
`?utm_source=...`, those identifiers stay on the storefront's URL
only — they do NOT automatically propagate to the checkout
subdomain.
Shopify's built-in tracking running on the checkout origin
(Customer Events / GA4 / Meta Pixel) then sees the customer's
checkout-page URL with no attribution params, and reports every
paid-ad order as organic / direct. Last-click attribution silently
breaks for every paid order — the kind of bug that's invisible in
acceptance tests and only surfaces weeks later when ad-platform
reports stop matching reality.
The fix is small and universal: when handing off to the checkout
URL, append the standard set of attribution params (gclid, gbraid,
wbraid, fbclid, ttclid, msclkid, utm_source/medium/campaign/content/
term) from the storefront's current URL onto the outbound checkout
URL. The checkout's own tracking then sees the same identifiers and
ad-platform reports stay accurate.
Existing params already on the checkout URL are never overwritten —
if the caller set something explicitly, that value wins.
Implementation:
+ app/utils/checkout-attribution.ts (new)
- `FORWARDED_ATTRIBUTION_KEYS` — the canonical set
- `appendForwardedAttribution(checkoutUrl, searchString)` —
pure function, safe inputs, returns the original URL
unchanged on parse failure so a malformed URL never breaks
checkout
M app/routes/cart/lines.tsx
- Buy-now / cart-permalink flow (/cart/<variantId>:<qty>?checkout)
forwards from `request.url`'s search on the server before
`redirect()`. Pure SSR, no client-side step needed.
M app/components/cart/cart-summary.tsx
- Regular cart's "Continue to Checkout" link reads
`window.location.search` after hydration via useEffect and
swaps the link's href to the enhanced URL. First render uses
the raw checkoutUrl so SSR + client first-paint stay
consistent (no hydration mismatch).
No new cookies, no new infrastructure, no opt-in flag — every Pilot
user with paid traffic benefits immediately. Affiliate / vendor-
specific click IDs (tj_clickid, awc, irclickid, …) usually live in
a first-party attribution cookie scoped to the apex domain so they
don't need this URL round-trip; the forwarded set is deliberately
limited to widely-recognised standard params. Storefronts capturing
custom URL-only identifiers can extend `FORWARDED_ATTRIBUTION_KEYS`
in user-space.
The pattern complements server-side conversion tracking work but
doesn't require any of it: it helps Shopify's stock checkout-side
tracking, which every storefront has by default.
hta218
approved these changes
May 18, 2026
hta218
added a commit
that referenced
this pull request
May 18, 2026
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
Forward ad-attribution URL params (
gclid,fbclid,utm_*, …) from the storefront onto the outbound Shopify checkout URL so Shopify's built-in tracking on the checkout subdomain sees consistent last-click identifiers.The problem
Hydrogen storefronts live on one origin (
mystore.com) and Shopify checkout lives on another (checkout.mystore.comormystore.myshopify.com). When a buyer arrives via a paid ad and the landing URL carries identifiers like?gclid=...,?fbclid=..., or?utm_source=..., those identifiers stay on the storefront's URL only — they do NOT automatically propagate to the checkout subdomain.Shopify's built-in tracking running on the checkout origin (Customer Events, GA4, Meta Pixel) then sees the checkout-page URL with no attribution params, and reports every paid-ad order as organic / direct. Last-click attribution silently breaks for every paid order — the kind of bug that's invisible in acceptance tests and only surfaces weeks later when ad-platform reports stop matching reality.
Two cart → checkout transition points are affected:
<a href={cart.checkoutUrl}>link inapp/components/cart/cart-summary.tsx.app/routes/cart/lines.tsxloader for/cart/<variantId>:<qty>?checkoutURLs thatcart.create()+redirect()straight to checkout.Both currently hand off the raw
cart.checkoutUrlwith no attribution forwarding.The fix
A small, self-contained helper:
…wired into both transition points. The helper:
gclid,gbraid,wbraid,fbclid,ttclid,msclkid, and the fiveutm_*params (deliberately scoped to widely-recognised standard params; affiliate / vendor-specific click IDs usually live in first-party cookies scoped to the apex domain and don't need this URL round-trip).checkoutUrlunchanged on parse failure so a malformed input never breaks the checkout redirect.lines.tsx(Buy-now)Pure server-side — reads
request.url's search and forwards before theredirect(). No client step.cart-summary.tsx(Continue to Checkout link)Reads
window.location.searchafter hydration viauseEffectand swaps the link'shrefto the enhanced URL. First render uses the rawcheckoutUrlso SSR + client first-paint stay consistent (no hydration mismatch warning).Verification
https://yourstore.com/?utm_source=test&gclid=GCL_TEST_001.…?…&utm_source=test&gclid=GCL_TEST_001.For the Buy-now path, use a "Buy now" button on a PDP (any Pilot derivative that wires the
showBuyNowButtonschema option) and confirm the redirect destination carries the params.What this does NOT do
Out of scope / future work
Storefronts capturing affiliate or vendor-specific click IDs (Traffic Junky
tj_clickid, Awinawc, Impact Radiusirclickid, etc.) usually round-trip them through a first-party attribution cookie scoped to the apex domain — a separate concern from this URL-only pass-through. Users can extendFORWARDED_ATTRIBUTION_KEYSin user-space if needed.Files
app/utils/checkout-attribution.ts(new, ~90 lines incl. JSDoc)app/routes/cart/lines.tsx(+10 LOC)app/components/cart/cart-summary.tsx(+20 LOC)Net diff: ~124 insertions, 3 deletions. No new dependencies.