fix(consent): bypass Shopify privacy banner SDK on subdomain-checkout setups#387
Merged
Conversation
… setups
Shopify's hosted privacy banner script (`storefront-banner.js`) and the
underlying `consent-tracking-api.js` v0.2 SDK have a URL-construction
bug on any Hydrogen storefront that has a checkout subdomain (the
standard Hydrogen setup — storefront on the apex, checkout on a
subdomain).
Hydrogen passes the SDK a config field `storefrontRootDomain` derived
as `"." + commonAncestorDomain(checkoutDomain, location.host)`. The
leading dot is intentional and only meant for cookie `Domain=` scoping
so the consent cookie is readable across subdomains. But both the
banner script and the core SDK take this same string and use it as a
URL hostname for an SFAPI POST to record the consent decision —
producing `https://.mystore.com/api/unstable/graphql.json`.
The leading dot is invalid in a hostname, DNS fails with
`ERR_NAME_NOT_RESOLVED`. In the banner-script case the SDK init crashes
before installing `setTrackingConsent` / `currentVisitorConsent` on
`window.Shopify.customerPrivacy`. In the core SDK case the cookie
write is chained after the failed fetch's `.then()`, so
`_tracking_consent` is never persisted.
User-visible symptom: the banner reappears on every refresh, and
Google Consent Mode v2 status reverts from G111 (granted) to G100
(denied) on the second page view.
Verified by reading the SDK source on cdn.shopify.com (both v0.1 and
v0.2), reproducing in production, and observing
`window.Shopify.customerPrivacy.setTrackingConsent === undefined` after
clicking Accept on the banner. The relevant Hydrogen comment in
`@shopify/hydrogen/dist/development/index.cjs` acknowledges the issue:
"Once consent-tracking-api is updated to not rely on cookies anymore,
we can remove this."
Workaround (this commit):
- `withPrivacyBanner: false` in the root loader's consent config.
Hydrogen now loads only the core consent-tracking-api.js (no banner
script). `setTrackingConsent` and friends remain available on
`window.Shopify.customerPrivacy`.
- Render a minimal custom `<ConsentBanner />` styled with Pilot's
design tokens (`bg-background`, `text-body`, `border-line`,
`rounded-md`).
- On Accept / Decline, write the `_tracking_consent` cookie directly
in the exact serialized format Shopify expects. The format was
reverse-engineered from the SDK source: a custom serializer (NOT
JSON.stringify) with unquoted object keys, JSON-stringified strings,
toString'd booleans/numbers, omitting undefined and empty-string
fields. The Shopify checkout reads the same cookie via
`Domain=.mystore.com` scoping and honors it.
- Dispatch `visitorConsentCollected` on `window` so listeners (Google
Consent Mode v2 updaters, custom analytics wiring, etc.) see the
same payload shape they would from the SDK.
When Shopify ships the SDK fix upstream:
1. Flip `withPrivacyBanner: false` back to `true` in
`app/.server/root.ts`.
2. Delete `app/components/root/consent-banner.tsx`.
3. Remove the import + mount line in `app/root.tsx`.
No behavioural change for storefronts where the checkout lives on the
same domain (no dot in `storefrontRootDomain`) — those setups never
hit the bug, but they will still render our banner correctly.
Member
Author
|
Filed upstream: Shopify/hydrogen#3761 — same root cause analysis, with reproduction steps and code evidence from both broken SDK paths. References this PR as the reference workaround. When Shopify ships the CDN-side fix (or merges a docs/code change on their end), the 3-step revert procedure in this PR's description stays valid. |
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.
Problem
On any Hydrogen storefront with a checkout subdomain (the standard setup — storefront on the apex, checkout on a subdomain like
checkout.mystore.com), Shopify's hosted privacy banner is broken:G111(granted) →G100(denied) on the second page viewPOST https://.mystore.com/api/unstable/graphql.json net::ERR_NAME_NOT_RESOLVEDwindow.Shopify.customerPrivacy.setTrackingConsentisundefinedafter the banner script loadsRoot cause
Hydrogen passes the Shopify Customer Privacy SDK a config field
storefrontRootDomainderived from"." + commonAncestorDomain(checkoutDomain, location.host). The leading dot is intentional and only meant for cookieDomain=scoping so the consent cookie is readable across subdomains.But both
storefront-banner.jsand the underlyingconsent-tracking-api.js(v0.1 and v0.2) take this same string and use it as a URL hostname for an SFAPI POST that records the consent decision:In the banner-script case the SDK init crashes before installing
setTrackingConsent/currentVisitorConsentonwindow.Shopify.customerPrivacy. In the core SDK case the cookie write is chained after the failed fetch's.then(), so_tracking_consentis never persisted either way.Hydrogen has no public prop to override this. The relevant comment in
@shopify/hydrogen/dist/development/index.cjsalready acknowledges the underlying limitation:Fix
withPrivacyBanner: falsein the root loader's consent config skips loading the broken banner script. Only the coreconsent-tracking-api.jsloads.Then a minimal custom
<ConsentBanner />component:Renders on absence of
_tracking_consentcookie (the canonical first-visit signal —shouldShowBanner()in v0.2 is gated on a serverdisplay_bannerflag that Hydrogen never triggers, not interaction state).On Accept / Decline writes
_tracking_consentdirectly with the exact format Shopify expects. Format reverse-engineered from the SDK source — a custom serializer (NOTJSON.stringify): unquoted object keys,JSON.stringifyfor strings,toStringfor booleans/numbers, omitsundefinedand empty-string fields. Example payload:Sets
Domain=.mystore.comso the Shopify checkout reads the same cookie and honors it.Dispatches
visitorConsentCollectedonwindowwith the sameVisitorConsentCollectedshape Shopify's SDK emits, so anything listening (Google Consent Mode v2 updaters, custom analytics wiring, etc.) keeps working unchanged.Reverting when Shopify fixes the SDK
Three steps:
withPrivacyBanner: false→trueinapp/.server/root.ts.app/components/root/consent-banner.tsx.app/root.tsx.Verification
Tested on a production Hydrogen 2026.4 storefront with the same checkout-subdomain setup:
_tracking_consentabsent in cookies after clicking Accept; GA4collectURLgcs=G100after refresh._tracking_consentwritten withDomain=.mystore.com, 365-day expiry, format byte-compatible with the SDK's writer; banner does not reappear on refresh; GA4collectURL staysgcs=G111; Shopify checkout honors the consent.Files changed
app/.server/root.ts—withPrivacyBanner: false+ a long inline comment documenting whyapp/components/root/consent-banner.tsx(new) — the custom banner UI + cookie writerapp/root.tsx— import + mount the new component inside the existing<Analytics.Provider>No new dependencies. ~250 lines net.
No behavioural change for same-domain setups
Storefronts where the checkout lives on the same domain (so
storefrontRootDomainhas no leading dot) never hit the bug, but they will still render the new banner correctly — it's the same UX, just from a different file.