Skip to content
Merged
Show file tree
Hide file tree
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 Nov 12, 2025
64b4079
Forward IP header and signature for consent geolocation
frandiox Nov 12, 2025
1e8adbe
Make createRequestHandler an official wrapper for Hydrogen independen…
frandiox Nov 12, 2025
c7a7103
Add CrossRuntimeResponse
frandiox Nov 12, 2025
6c8715f
Add tracking utils to hydrogen-react
frandiox Nov 12, 2025
d7a9aab
Add storefront.fetchConsent utility
frandiox Nov 12, 2025
c22ae29
Call fetchConsent automatically in handleRequest wrapper
frandiox Nov 12, 2025
1684f1e
Limit getting consent to full-page rendering
frandiox Nov 12, 2025
151d3d7
Stop creating deprecated cookies
frandiox Nov 12, 2025
cbac773
Add local privacy banner
frandiox Nov 12, 2025
46a3c51
Make privacy banner update cookies in both domains
frandiox Nov 12, 2025
f053405
Revert "Make privacy banner update cookies in both domains"
frandiox Nov 13, 2025
84d5ecd
Make privacy banner update cookies in both domains one after another
frandiox Nov 13, 2025
c7e490a
Read server-timing from fetch requests using performance API
frandiox Nov 13, 2025
9593a44
Revert "Make privacy banner update cookies in both domains one after …
frandiox Nov 14, 2025
b078a93
Revert "Add local privacy banner"
frandiox Nov 14, 2025
9f2599a
Exclude headers from cache
frandiox Nov 14, 2025
f0dcd7a
Add internal proxy to SFAPI
frandiox Nov 17, 2025
3368532
Fix typecheck
frandiox Nov 17, 2025
5347d89
Revert "Stop creating deprecated cookies"
frandiox Nov 17, 2025
ee95d38
Use new proxy to get consent in same-origin
frandiox Nov 17, 2025
db17499
Revalidate cart.checkoutUrl after consent changes
frandiox Nov 17, 2025
5446cba
Cache tracking values from resource entries
frandiox Nov 18, 2025
33ea45e
Fallback to deprecated cookies when reading tracking values
frandiox Nov 18, 2025
aa46eac
Add tests for tracking-utils
frandiox Nov 18, 2025
e6ad9be
Update local cache for tests
frandiox Nov 18, 2025
76f9135
Refactor getTrackingValuesFromHeader
frandiox Nov 18, 2025
ba51da9
Replace usage of getShopifyCookies with getTrackingValues
frandiox Nov 18, 2025
7096ca7
Update packages/hydrogen/src/customer-privacy/ShopifyCustomerPrivacy.tsx
frandiox Nov 19, 2025
60b824b
Fix missing comma and minor things
frandiox Nov 19, 2025
cae4c24
Stricter types and string check
frandiox Nov 19, 2025
35a96c5
Fix reading tracking values from deprecated cookies
frandiox Nov 19, 2025
1da4639
Test refactoring
frandiox Nov 19, 2025
ac7ae2a
Fix tests
frandiox Nov 19, 2025
993662a
Minor fixes
frandiox Nov 19, 2025
a04cb53
Support cross-origin tracking values
frandiox Nov 25, 2025
59f45a1
Signal that sfapi-proxy is enabled to the browser and use it for consent
frandiox Nov 25, 2025
990731d
Use new proxy from frontend-only cart if enabled
frandiox Nov 25, 2025
085e358
Add a way to query cookies from the browser as a fallback
frandiox Nov 25, 2025
6035518
Collect subrequest headers in server response
frandiox Nov 26, 2025
8c411bf
Remove server-side fetchTrackingValues
frandiox Nov 27, 2025
e764bc8
Just collect tracking values in the server but don't request them spe…
frandiox Nov 27, 2025
cdc7703
Upgrade consent-tracking-api to v0.2
frandiox Nov 27, 2025
a22ba0b
Avoid using private token in proxy
frandiox Nov 27, 2025
1a2bda0
Cleanup
frandiox Nov 27, 2025
c98c204
Do not write mock tracking values to cookies
frandiox Nov 27, 2025
d8c1dfb
Avoid passing storefront token from server config
frandiox Nov 27, 2025
e732cfc
Fix tests
frandiox Nov 27, 2025
572b42d
Extract utility
frandiox Nov 28, 2025
6864193
Fix CORS requests to proxy. This makes the SFAPI proxy route behave t…
frandiox Nov 28, 2025
2a45acd
Warn about disabled features and add jsdoc
frandiox Nov 28, 2025
9b00a47
Do not mark analytics ready when using privacy banner until user acce…
frandiox Nov 28, 2025
ebc348f
Workaround double event firing bug in PrivacyBanner. This fixes setti…
frandiox Nov 28, 2025
f4964ef
Add comments and use constants
frandiox Nov 28, 2025
cf472a1
Workaround consent-tracking-api issue to use previously fetched consent
frandiox Nov 28, 2025
93a883d
Refactor and expose utility
frandiox Dec 1, 2025
74d8c27
Fix tests
frandiox Dec 1, 2025
0afb26e
Move browser request for cookies to useShopifyCookies hook, and call …
frandiox Dec 1, 2025
f3498e6
Fix existing bug where old cookies were not removed after user declin…
frandiox Dec 1, 2025
53831b0
Make getShopifyCookies work with new tracking values
frandiox Dec 1, 2025
0e5b8b6
Rename field
frandiox Dec 2, 2025
e4996b6
Make e2e port flexible and setup cookie test shell
frandiox Dec 2, 2025
a8e593c
Support --customer-account-push in e2e
frandiox Dec 2, 2025
b163a3c
Wrap in try/catch
frandiox Dec 2, 2025
1ba5fe6
Small fixes to e2e tests
frandiox Dec 3, 2025
e43a286
Add e2e for privacy-banner cookie checks
frandiox Dec 3, 2025
acdcd2e
Read tracking values from non SFAPI requests to same-origin
frandiox Dec 5, 2025
6fb7fa9
Support multiple stores in e2e tests
frandiox Dec 5, 2025
569cbb6
Rework e2e tests to use fixtures
frandiox Dec 5, 2025
b16199a
Assert perfkit requests contain proper tokens
frandiox Dec 5, 2025
a5ce7d3
Assert checkoutUrl params
frandiox Dec 5, 2025
5b4827e
Add e2e test for session migration
frandiox Dec 5, 2025
92b15a4
Remove old specs
frandiox Dec 5, 2025
f243d73
Minor fixes
frandiox Dec 5, 2025
7d06afb
Do not signal tracking is done from the server when missing _cmp
frandiox Dec 8, 2025
a3c9623
Mock withPrivacyBanner in e2e
frandiox Dec 8, 2025
d53c1c4
consent-tracking test for declined consent by default
frandiox Dec 8, 2025
ed3c823
Assert checkoutUrl params
frandiox Dec 8, 2025
9ca80ca
PR feedback
frandiox Dec 8, 2025
84fd77c
Split analytics requests in e2e tests
frandiox Dec 8, 2025
e91eae6
consent-tracking-accept e2e test
frandiox Dec 8, 2025
4c8f6e7
Spin servers per spec instead of per worker
frandiox Dec 8, 2025
3daf3a6
Test migration with default consent enabled
frandiox Dec 8, 2025
850cb3b
Change assertion for mock values in params
frandiox Dec 9, 2025
daa64ab
Remove old server-timing from unit test
frandiox Dec 9, 2025
069b16e
Fix tests flakiness when running in parallel
frandiox Dec 9, 2025
dcbb3d1
Rename cookie test folder
frandiox Dec 9, 2025
9b6dd41
Support production test stores in e2e
frandiox Dec 9, 2025
3ebac37
Run consent-tracking-accept spec with and without privacy banner
frandiox Dec 9, 2025
3ed452c
Add e2e test for old-cookies
frandiox Dec 9, 2025
b2946c9
Add e2e test for consent-tracking-accept in old cookies
frandiox Dec 9, 2025
375f5b9
Add e2e test for consent change mid-session
frandiox Dec 9, 2025
607bab7
Minor fixes to edge cases
frandiox Dec 9, 2025
54aefe2
Fix tests flakiness
frandiox Dec 9, 2025
3fb08b2
Changesets
frandiox Dec 9, 2025
f10aad3
Fix package.json e2e test commands
kdaviduik Dec 10, 2025
bb84b0f
Do not send empty headers
frandiox Dec 10, 2025
4499b1d
Add comments, rename functions, feedback
frandiox Dec 10, 2025
f9e4c62
Set playwright workers to 1 to avoid issues
frandiox Dec 10, 2025
4cc8576
Fix JSDoc comments
frandiox Dec 10, 2025
f5e08f9
Typo in changeset
frandiox Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/cold-rules-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
Copy link
Contributor

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-79

The Problem

The current matching logic only handles:

  1. Same origin (hydrogen.shophydrogen.shop)
  2. Checkout as subdomain of storefront (checkout.hydrogen.shophydrogen.shop)

But it fails when storefront and checkout are sibling subdomains under a shared parent:

Storefront: oldnavy.gapcanada.ca
Checkout:   secure-oldnavy.gapcanada.ca

// Current check:
'secure-oldnavy.gapcanada.ca'.endsWith('.oldnavy.gapcanada.ca')  // FALSE ❌

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:

const isMatch =
  matchedHost === currentHost ||
  (sfapiPath && matchedHost?.endsWith(`.${currentHost}`)) ||
  (sfapiPath && hasCommonAncestorDomain(matchedHost, currentHost)); // ← Add this

Where hasCommonAncestorDomain finds 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.com
  • shop.brand.co.uk + secure.brand.co.uk

Copy link
Contributor

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

'@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`.
- 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.
7 changes: 7 additions & 0 deletions .changeset/rotten-bobcats-grab.md
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).
5 changes: 5 additions & 0 deletions .changeset/two-melons-design.md
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.
5 changes: 5 additions & 0 deletions e2e/envs/.env.defaultConsentAllowed_cookiesDisabled
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"
5 changes: 5 additions & 0 deletions e2e/envs/.env.defaultConsentAllowed_cookiesEnabled
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"
5 changes: 5 additions & 0 deletions e2e/envs/.env.defaultConsentDisallowed_cookiesDisabled
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"
5 changes: 5 additions & 0 deletions e2e/envs/.env.defaultConsentDisallowed_cookiesEnabled
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"
PUBLIC_STOREFRONT_API_TOKEN="a79d329fc13657352c6e4734e5d4ca75"
PUBLIC_STOREFRONT_ID="1000061747"
5 changes: 5 additions & 0 deletions e2e/envs/.env.mockShop
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=""
63 changes: 63 additions & 0 deletions e2e/fixtures/index.ts
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();
});
};
172 changes: 172 additions & 0 deletions e2e/fixtures/server.ts
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);
});
}
}
Loading