-
Notifications
You must be signed in to change notification settings - Fork 380
🍪 Hydrogen Cookie Migration for New Shopify Cookie Architecture #3309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 95 commits
Commits
Show all changes
101 commits
Select commit
Hold shift + click to select a range
59ff34a
Add new token headers and forward cookie header
frandiox 64b4079
Forward IP header and signature for consent geolocation
frandiox 1e8adbe
Make createRequestHandler an official wrapper for Hydrogen independen…
frandiox c7a7103
Add CrossRuntimeResponse
frandiox 6c8715f
Add tracking utils to hydrogen-react
frandiox d7a9aab
Add storefront.fetchConsent utility
frandiox c22ae29
Call fetchConsent automatically in handleRequest wrapper
frandiox 1684f1e
Limit getting consent to full-page rendering
frandiox 151d3d7
Stop creating deprecated cookies
frandiox cbac773
Add local privacy banner
frandiox 46a3c51
Make privacy banner update cookies in both domains
frandiox f053405
Revert "Make privacy banner update cookies in both domains"
frandiox 84d5ecd
Make privacy banner update cookies in both domains one after another
frandiox c7e490a
Read server-timing from fetch requests using performance API
frandiox 9593a44
Revert "Make privacy banner update cookies in both domains one after …
frandiox b078a93
Revert "Add local privacy banner"
frandiox 9f2599a
Exclude headers from cache
frandiox f0dcd7a
Add internal proxy to SFAPI
frandiox 3368532
Fix typecheck
frandiox 5347d89
Revert "Stop creating deprecated cookies"
frandiox ee95d38
Use new proxy to get consent in same-origin
frandiox db17499
Revalidate cart.checkoutUrl after consent changes
frandiox 5446cba
Cache tracking values from resource entries
frandiox 33ea45e
Fallback to deprecated cookies when reading tracking values
frandiox aa46eac
Add tests for tracking-utils
frandiox e6ad9be
Update local cache for tests
frandiox 76f9135
Refactor getTrackingValuesFromHeader
frandiox ba51da9
Replace usage of getShopifyCookies with getTrackingValues
frandiox 7096ca7
Update packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx
frandiox 60b824b
Fix missing comma and minor things
frandiox cae4c24
Stricter types and string check
frandiox 35a96c5
Fix reading tracking values from deprecated cookies
frandiox 1da4639
Test refactoring
frandiox ac7ae2a
Fix tests
frandiox 993662a
Minor fixes
frandiox a04cb53
Support cross-origin tracking values
frandiox 59f45a1
Signal that sfapi-proxy is enabled to the browser and use it for consent
frandiox 990731d
Use new proxy from frontend-only cart if enabled
frandiox 085e358
Add a way to query cookies from the browser as a fallback
frandiox 6035518
Collect subrequest headers in server response
frandiox 8c411bf
Remove server-side fetchTrackingValues
frandiox e764bc8
Just collect tracking values in the server but don't request them spe…
frandiox cdc7703
Upgrade consent-tracking-api to v0.2
frandiox a22ba0b
Avoid using private token in proxy
frandiox 1a2bda0
Cleanup
frandiox c98c204
Do not write mock tracking values to cookies
frandiox d8c1dfb
Avoid passing storefront token from server config
frandiox e732cfc
Fix tests
frandiox 572b42d
Extract utility
frandiox 6864193
Fix CORS requests to proxy. This makes the SFAPI proxy route behave t…
frandiox 2a45acd
Warn about disabled features and add jsdoc
frandiox 9b00a47
Do not mark analytics ready when using privacy banner until user acce…
frandiox ebc348f
Workaround double event firing bug in PrivacyBanner. This fixes setti…
frandiox f4964ef
Add comments and use constants
frandiox cf472a1
Workaround consent-tracking-api issue to use previously fetched consent
frandiox 93a883d
Refactor and expose utility
frandiox 74d8c27
Fix tests
frandiox 0afb26e
Move browser request for cookies to useShopifyCookies hook, and call …
frandiox f3498e6
Fix existing bug where old cookies were not removed after user declin…
frandiox 53831b0
Make getShopifyCookies work with new tracking values
frandiox 0e5b8b6
Rename field
frandiox e4996b6
Make e2e port flexible and setup cookie test shell
frandiox a8e593c
Support --customer-account-push in e2e
frandiox b163a3c
Wrap in try/catch
frandiox 1ba5fe6
Small fixes to e2e tests
frandiox e43a286
Add e2e for privacy-banner cookie checks
frandiox acdcd2e
Read tracking values from non SFAPI requests to same-origin
frandiox 6fb7fa9
Support multiple stores in e2e tests
frandiox 569cbb6
Rework e2e tests to use fixtures
frandiox b16199a
Assert perfkit requests contain proper tokens
frandiox a5ce7d3
Assert checkoutUrl params
frandiox 5b4827e
Add e2e test for session migration
frandiox 92b15a4
Remove old specs
frandiox f243d73
Minor fixes
frandiox 7d06afb
Do not signal tracking is done from the server when missing _cmp
frandiox a3c9623
Mock withPrivacyBanner in e2e
frandiox d53c1c4
consent-tracking test for declined consent by default
frandiox ed3c823
Assert checkoutUrl params
frandiox 9ca80ca
PR feedback
frandiox 84fd77c
Split analytics requests in e2e tests
frandiox e91eae6
consent-tracking-accept e2e test
frandiox 4c8f6e7
Spin servers per spec instead of per worker
frandiox 3daf3a6
Test migration with default consent enabled
frandiox 850cb3b
Change assertion for mock values in params
frandiox daa64ab
Remove old server-timing from unit test
frandiox 069b16e
Fix tests flakiness when running in parallel
frandiox dcbb3d1
Rename cookie test folder
frandiox 9b6dd41
Support production test stores in e2e
frandiox 3ebac37
Run consent-tracking-accept spec with and without privacy banner
frandiox 3ed452c
Add e2e test for old-cookies
frandiox b2946c9
Add e2e test for consent-tracking-accept in old cookies
frandiox 375f5b9
Add e2e test for consent change mid-session
frandiox 607bab7
Minor fixes to edge cases
frandiox 54aefe2
Fix tests flakiness
frandiox 3fb08b2
Changesets
frandiox f10aad3
Fix package.json e2e test commands
kdaviduik bb84b0f
Do not send empty headers
frandiox 4499b1d
Add comments, rename functions, feedback
frandiox f9e4c62
Set playwright workers to 1 to avoid issues
frandiox 4cc8576
Fix JSDoc comments
frandiox f5e08f9
Typo in changeset
frandiox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@shopify/hydrogen': patch | ||
| --- | ||
|
|
||
| This version adds support for the new cookie system in Shopify (`_shopify_analytics` and `_shopify_marketing` http-only cookies). It is backward compatible and still supports the deprecated `_shopify_y` and `_shopify_s` cookies. | ||
|
|
||
| - `createRequestHandler` can now be used for every Hydrogen app, not only the onex deployed to Oxygen. It is now exported from `@shopify/hydrogen`. | ||
frandiox marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| - A new Storefront API proxy is now available in Hydrogen's `createRequestHandler`. This will be used to obtain http-only cookies from Storefront API. In general, it should be transparent but it can be disabled with the `proxyStandardRoutes` option. | ||
| - `Analytics.Provider` component and `useCustomerPrivacy` hook now make a request internally to the mentioned proxy to obtain cookies in the storefront domain. | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@shopify/hydrogen-react': patch | ||
| --- | ||
|
|
||
| New export `getTrackingValues` to obtain information for analytics and marketing. Use this instead of `getShopifyCookies` (which is now deprecated). | ||
|
|
||
| `useShopifyCookies` now accepts a `fetchTrackingValues` parameter that can be used to make a Storefront API request and obtain Shopify http-only cookies, `_shopify_analytics` and `_shopify_marketing` (which replace the deprecated `_shopify_y` and `_shopify_s` cookies). |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@shopify/hydrogen': patch | ||
| --- | ||
|
|
||
| Fixed a number of issues related to irregular behaviors between Privacy Banner and Hydrogen's analytics events. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| SESSION_SECRET="mock-session" | ||
| PUBLIC_CHECKOUT_DOMAIN="checkout.hydrogen.shop" | ||
| PUBLIC_STORE_DOMAIN="checkout.hydrogen.shop" | ||
| PUBLIC_STOREFRONT_API_TOKEN="b97a750a8afa8fe33f2b4012cb3a9f6f" | ||
| PUBLIC_STOREFRONT_ID="1000014875" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| SESSION_SECRET="mock-session" | ||
| PUBLIC_CHECKOUT_DOMAIN="www.iwantacheapdomainfortesting12345.club" | ||
| PUBLIC_STORE_DOMAIN="www.iwantacheapdomainfortesting12345.club" | ||
| PUBLIC_STOREFRONT_API_TOKEN="2aac2e4420f32ba0c7dadf55c7cc387b" | ||
| PUBLIC_STOREFRONT_ID="1000070232" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| SESSION_SECRET="mock-session" | ||
| PUBLIC_CHECKOUT_DOMAIN="www.kara2345.xyz" | ||
| PUBLIC_STORE_DOMAIN="www.kara2345.xyz" | ||
| PUBLIC_STOREFRONT_API_TOKEN="8eece95833df895900c1b285987c7f40" | ||
| PUBLIC_STOREFRONT_ID="1000070242" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| SESSION_SECRET="mock-session" | ||
| PUBLIC_CHECKOUT_DOMAIN="checkout.daviduik.com" | ||
| PUBLIC_STORE_DOMAIN="checkout.daviduik.com" | ||
frandiox marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| PUBLIC_STOREFRONT_API_TOKEN="a79d329fc13657352c6e4734e5d4ca75" | ||
| PUBLIC_STOREFRONT_ID="1000061747" | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| SESSION_SECRET="mock-session" | ||
| PUBLIC_CHECKOUT_DOMAIN="mock.shop" | ||
| PUBLIC_STORE_DOMAIN="mock.shop" | ||
| PUBLIC_STOREFRONT_API_TOKEN="" | ||
| PUBLIC_STOREFRONT_ID="" |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import {test as base} from '@playwright/test'; | ||
| import {DevServer} from './server'; | ||
| import path from 'node:path'; | ||
| import {stat} from 'node:fs/promises'; | ||
| import {StorefrontPage} from './storefront'; | ||
|
|
||
| export * from '@playwright/test'; | ||
| export * from './storefront'; | ||
|
|
||
| export const test = base.extend< | ||
| {storefront: StorefrontPage}, | ||
| {forEachWorker: void} | ||
| >({ | ||
| storefront: async ({page}, use) => { | ||
| const storefront = new StorefrontPage(page); | ||
| await use(storefront); | ||
| }, | ||
| }); | ||
|
|
||
| const TEST_STORE_KEYS = [ | ||
| 'mockShop', | ||
| 'defaultConsentDisallowed_cookiesEnabled', | ||
| 'defaultConsentAllowed_cookiesEnabled', | ||
| 'defaultConsentDisallowed_cookiesDisabled', | ||
| 'defaultConsentAllowed_cookiesDisabled', | ||
| ] as const; | ||
|
|
||
| type TestStoreKey = (typeof TEST_STORE_KEYS)[number]; | ||
|
|
||
| export const setTestStore = async ( | ||
| testStore: TestStoreKey | `https://${string}`, | ||
| ) => { | ||
| const isLocal = !testStore.startsWith('https://'); | ||
| let server: DevServer | null = null; | ||
|
|
||
| test.use({ | ||
| baseURL: async ({}, use) => { | ||
| await use(isLocal ? server?.getUrl() : testStore); | ||
| }, | ||
| }); | ||
|
|
||
| if (!isLocal) { | ||
| console.log(`Using test store: ${testStore}`); | ||
| return; | ||
| } | ||
|
|
||
| test.afterAll(async () => { | ||
| await server?.stop(); | ||
| }); | ||
|
|
||
| test.beforeAll(async ({}) => { | ||
| const filepath = path.resolve(__dirname, `../envs/.env.${testStore}`); | ||
| await stat(filepath); // Ensure the file exists | ||
|
|
||
| server = new DevServer({ | ||
| storeKey: testStore, | ||
| customerAccountPush: false, | ||
| envFile: filepath, | ||
| }); | ||
|
|
||
| await server.start(); | ||
| }); | ||
| }; |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import {spawn} from 'node:child_process'; | ||
| import path from 'node:path'; | ||
|
|
||
| type DevServerOptions = { | ||
| id?: number; | ||
| port?: number; | ||
| projectPath?: string; | ||
| customerAccountPush?: boolean; | ||
| envFile?: string; | ||
| storeKey?: string; | ||
| }; | ||
|
|
||
| export class DevServer { | ||
| process: ReturnType<typeof spawn> | undefined; | ||
| port: number; | ||
| projectPath: string; | ||
| customerAccountPush: boolean; | ||
| capturedUrl?: string; | ||
| id?: number; | ||
| envFile?: string; | ||
| storeKey?: string; | ||
|
|
||
| constructor(options: DevServerOptions = {}) { | ||
| this.id = options.id; | ||
| this.storeKey = options.storeKey; | ||
| this.port = options.port ?? 3100; | ||
| this.projectPath = | ||
| options.projectPath ?? path.join(__dirname, '../../templates/skeleton'); | ||
| this.customerAccountPush = options.customerAccountPush ?? false; | ||
| this.envFile = options.envFile; | ||
| } | ||
|
|
||
| getUrl() { | ||
| return this.capturedUrl || `http://localhost:${this.port}`; | ||
| } | ||
|
|
||
| start() { | ||
| if (this.process) { | ||
| throw new Error(`Server ${this.id} is already running`); | ||
| } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const args = ['run', 'dev', '--']; | ||
| if (this.customerAccountPush) { | ||
| args.push('--customer-account-push'); | ||
| } | ||
|
|
||
| if (this.envFile) { | ||
| args.push('--env-file', this.envFile); | ||
| } | ||
|
|
||
| this.process = spawn('npm', args, { | ||
| cwd: this.projectPath, | ||
| env: { | ||
| ...process.env, | ||
| NODE_ENV: 'development', | ||
| SHOPIFY_HYDROGEN_FLAG_PORT: this.port.toString(), | ||
| }, | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
|
|
||
| let started = false; | ||
| const timeout = setTimeout(() => { | ||
| if (!started) { | ||
| this.stop(); | ||
| reject(new Error(`Server ${this.id} failed to start within timeout`)); | ||
| } | ||
| }, 60000); | ||
|
|
||
| let localUrl: string | undefined; | ||
| let tunnelUrl: string | undefined; | ||
|
|
||
| const handleOutput = (output: string) => { | ||
| if (!localUrl) { | ||
| localUrl = output.match(/(http:\/\/localhost:\d+)/)?.[1]; | ||
| } | ||
| if (this.customerAccountPush && !tunnelUrl) { | ||
| tunnelUrl = output.match(/(https:\/\/[^\s]+)/)?.[1]; | ||
| } | ||
|
|
||
| if (!started && output.includes('success')) { | ||
| started = true; | ||
| clearTimeout(timeout); | ||
| this.capturedUrl = tunnelUrl || localUrl; | ||
| const port = this.capturedUrl?.match(/:(\d+)/)?.[1]; | ||
| if (port) { | ||
| this.port = parseInt(port, 10); | ||
| } | ||
| if (!this.id) { | ||
| this.id = | ||
| this.port || parseInt((Math.random() * 1000).toFixed(0), 10); | ||
| } | ||
| console.log( | ||
| `[test-server ${this.id}] Server started on ${this.capturedUrl} [${this.storeKey}]`, | ||
| ); | ||
| // Give the tunnel a bit more time to ensure everything is ready | ||
| setTimeout(resolve, tunnelUrl ? 5000 : 0); | ||
| } | ||
|
|
||
| if ( | ||
| output.includes('log in to Shopify') || | ||
| output.includes('User verification code:') | ||
| ) { | ||
| clearTimeout(timeout); | ||
| this.stop(); | ||
| reject( | ||
| new Error( | ||
| 'Not logged in to Shopify CLI. Run: cd templates/skeleton && npx shopify auth login', | ||
| ), | ||
| ); | ||
| } else if ( | ||
| output.includes('Failed to prompt') || | ||
| output.includes('Select a shop to log in') | ||
| ) { | ||
| clearTimeout(timeout); | ||
| this.stop(); | ||
| reject( | ||
| new Error( | ||
| 'Storefront not linked. Run: cd templates/skeleton && npx shopify hydrogen link', | ||
| ), | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| if (this.process.stdout) { | ||
| this.process.stdout.on('data', (data) => { | ||
| const output = data.toString(); | ||
| // !started && console.log(output); | ||
| handleOutput(output); | ||
| }); | ||
| } | ||
|
|
||
| if (this.process.stderr) { | ||
| this.process.stderr.on('data', (data) => { | ||
| const output = data.toString(); | ||
| // !started && console.log(output); | ||
| handleOutput(output); | ||
| }); | ||
| } | ||
|
|
||
| this.process.on('error', (error) => { | ||
| clearTimeout(timeout); | ||
| reject(error); | ||
| }); | ||
|
|
||
| this.process.on('exit', (code) => { | ||
| if (!started) { | ||
| clearTimeout(timeout); | ||
| reject(new Error(`Server ${this.id} exited with code ${code}`)); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| stop() { | ||
| return new Promise((resolve) => { | ||
| if (!this.process) return resolve(false); | ||
| console.log(`[test-server ${this.id}] Stopping server...`); | ||
|
|
||
| this.process.on('exit', () => { | ||
| this.process = undefined; | ||
| resolve(true); | ||
| }); | ||
|
|
||
| this.process.kill('SIGTERM'); | ||
|
|
||
| setTimeout(() => { | ||
| this.process?.kill('SIGKILL'); | ||
| }, 5000); | ||
| }); | ||
| } | ||
| } |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(threading)
Bug: Cross-subdomain matching fails for sibling domains
Location:
packages/hydrogen-react/src/tracking-utils.ts:75-79The Problem
The current matching logic only handles:
hydrogen.shop↔hydrogen.shop)checkout.hydrogen.shop→hydrogen.shop)But it fails when storefront and checkout are sibling subdomains under a shared parent:
This means SFAPI responses from checkout are ignored, and we fall back to stale navigation entry values.
The Fix
Add a third condition that checks if both hosts share a common ancestor domain:
Where
hasCommonAncestorDomainfinds the common suffix parts (e.g.,gapcanada.ca) and requires at least 2 matching parts (or 3 for multi-part TLDs like.co.uk) to prevent matching unrelated domains.Other affected merchants
This would also affect setups like:
www.store.com+checkout.store.comshop.brand.co.uk+secure.brand.co.ukThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this we decided we're okay leaving this as is, since this only affects merchants with sibling domains AND who aren't using the proxy. We will explicitly document this limitation instead of supporting it here