Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c1ab06b
Add tracking cookies migration infrastructure
kdaviduik Dec 11, 2025
52ece80
Add E2E tests for tracking cookies migration
kdaviduik Dec 11, 2025
2700afb
Fix: Delay PerfKit loading until consent is collected
kdaviduik Dec 11, 2025
b7c13ea
Add SFAPI proxy support to createRequestHandler
kdaviduik Dec 11, 2025
456ad3c
Update package-lock.json for version bumps
kdaviduik Dec 11, 2025
f9b8411
Align createRequestHandler with PR #3309
kdaviduik Dec 11, 2025
5510849
Add missing getTrackingValues export from hydrogen package
kdaviduik Dec 11, 2025
41ea850
Add buyerIpSig to remix-oxygen StorefrontHeaders
kdaviduik Dec 11, 2025
99214a0
Add sec-purpose header check to remix-oxygen getStorefrontHeaders
kdaviduik Dec 11, 2025
6d91665
Rename proxyStorefrontApiRequests to proxyStandardRoutes
kdaviduik Dec 11, 2025
8a91d21
Update Playwright to 1.57.0
kdaviduik Dec 11, 2025
426d54d
Fix e2e test bundle interception pattern for Vite pre-bundled deps
kdaviduik Dec 11, 2025
bcc704d
Fix
kdaviduik Dec 11, 2025
aa4bc92
fix
kdaviduik Dec 11, 2025
e82a2aa
Fix create-hydrogen snapshot for TokenlessApi route
kdaviduik Dec 11, 2025
c58e776
Disable proxyStandardRoutes in classic-remix example
kdaviduik Dec 11, 2025
7364bf8
Also disable collectTrackingInformation in classic-remix example
kdaviduik Dec 11, 2025
f80c9fc
Guard getSetCookie call for environments without Headers support
kdaviduik Dec 11, 2025
e87f738
Remove redundant in skeleton template; update changesets to be more …
kdaviduik 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 @@
---
'@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 ones 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.
6 changes: 6 additions & 0 deletions .changeset/remix-oxygen-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/remix-oxygen': minor
---


Support Shopify's new consolidated cookie architecture. Adds built-in Storefront API proxy support to `createRequestHandler`.
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=""
67 changes: 67 additions & 0 deletions e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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);
// eslint-disable-next-line react-hooks/rules-of-hooks
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({
// eslint-disable-next-line no-empty-pattern
baseURL: async ({}, use) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(isLocal ? server?.getUrl() : testStore);
},
});

if (!isLocal) {
console.log(`Using test store: ${testStore}`);
return;
}

test.afterAll(async () => {
await server?.stop();
});

// eslint-disable-next-line no-empty-pattern
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