Skip to content

Commit cf91b3b

Browse files
isaacsclaude
andcommitted
test(e2e): fix flaky hydrogen and cloudflare send-to-sentry tests
The hydrogen-react-router-7 and remix-hydrogen root loaders queried the public mock.shop Storefront API for header and footer menus. When mock.shop returns a GraphQL "internal error", the awaited header query throws, so every page returns a 500. Tests that click a button then time out after 30s. No UI reads this data, so remove the queries and their unused fragments. In cloudflare-workers-send-to-sentry, the tests got a 404 right after the deploy. The worker answers every path with a 200, so the 404 came from Cloudflare, not the worker. A new workers.dev route reaches the edge servers one at a time. One OK probe did not prove that the next request, which may reach a different server, would also succeed. Wait for 5 OK probes in a row, and retry 404 responses in the tests for up to 15s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d0b4cca commit cf91b3b

6 files changed

Lines changed: 37 additions & 302 deletions

File tree

‎dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/deployed-worker.mjs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,31 @@ export function keepsWorker() {
6767
return Boolean(process.env.GITHUB_ACTIONS || process.env.E2E_KEEP_WORKER);
6868
}
6969

70-
/** A freshly created workers.dev route can take a moment to become reachable. */
70+
/**
71+
* A freshly created workers.dev route can take a moment to become reachable, and it reaches the
72+
* edge servers one at a time. One OK response does not prove that the next request (which may
73+
* reach a different server) will not get a 404, so wait for several in a row.
74+
*/
7175
export async function waitForWorker(url) {
7276
const deadline = Date.now() + 60_000;
77+
const requiredSuccesses = 5;
78+
let successes = 0;
7379

7480
while (Date.now() < deadline) {
7581
try {
7682
// The SDK does not trace HEAD requests, so the probe leaves no spans behind in Sentry.
7783
const response = await fetch(url, { method: 'HEAD' });
78-
79-
if (response.ok) {
80-
return;
81-
}
84+
successes = response.ok ? successes + 1 : 0;
8285
} catch {
8386
// DNS for the new subdomain may not have propagated yet.
87+
successes = 0;
88+
}
89+
90+
if (successes >= requiredSuccesses) {
91+
return;
8492
}
8593

86-
await new Promise(resolve => setTimeout(resolve, 2_000));
94+
await new Promise(resolve => setTimeout(resolve, successes ? 1_000 : 2_000));
8795
}
8896

8997
throw new Error(`Worker at ${url} did not become reachable within 60s.`);

‎dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,22 @@ import {
1212
// Set by global-setup.mjs once the worker for this run is deployed.
1313
const workerUrl = process.env.E2E_TEST_WORKER_URL;
1414

15+
/**
16+
* The worker answers every path, so a 404 means the request reached an edge server that the new
17+
* workers.dev route has not reached yet. Retry those, and nothing else.
18+
*/
19+
async function fetchWorker(path: string, init?: RequestInit): Promise<Response> {
20+
const deadline = Date.now() + 15_000;
21+
let response = await fetch(`${workerUrl}${path}`, init);
22+
while (response.status === 404 && Date.now() < deadline) {
23+
await new Promise(resolve => setTimeout(resolve, 1_000));
24+
response = await fetch(`${workerUrl}${path}`, init);
25+
}
26+
return response;
27+
}
28+
1529
test('Sends a captured exception to Sentry', async () => {
16-
const response = await fetch(`${workerUrl}/test-error`);
30+
const response = await fetchWorker('/test-error');
1731
expect(response.status).toBe(200);
1832
const { eventId, traceId } = await response.json();
1933

@@ -25,7 +39,7 @@ test('Sends a captured exception to Sentry', async () => {
2539
test('Sends an unhandled exception and its request span to Sentry', async () => {
2640
const traceId = randomBytes(16).toString('hex');
2741
const publicKey = new URL(process.env.E2E_TEST_DSN!).username;
28-
const response = await fetch(`${workerUrl}/test-unhandled-error`, {
42+
const response = await fetchWorker('/test-unhandled-error', {
2943
headers: {
3044
'sentry-trace': `${traceId}-${randomBytes(8).toString('hex')}-1`,
3145
baggage: `sentry-trace_id=${traceId},sentry-public_key=${publicKey},sentry-sampled=true,sentry-sample_rate=1`,
@@ -40,7 +54,7 @@ test('Sends an unhandled exception and its request span to Sentry', async () =>
4054
});
4155

4256
test('Sends a request span to Sentry', async () => {
43-
const response = await fetch(`${workerUrl}/test-span`);
57+
const response = await fetchWorker('/test-span');
4458
expect(response.status).toBe(200);
4559
const { spanId, traceId } = await response.json();
4660

@@ -52,7 +66,7 @@ test('Sends a request span to Sentry', async () => {
5266
});
5367

5468
test('Sends the spans of Workflow steps before the Workflow goes to sleep', async () => {
55-
const response = await fetch(`${workerUrl}/test-workflow-sleep`);
69+
const response = await fetchWorker('/test-workflow-sleep');
5670
expect(response.status).toBe(200);
5771
const { instanceId, traceId } = await response.json();
5872

@@ -68,6 +82,6 @@ test('Sends the spans of Workflow steps before the Workflow goes to sleep', asyn
6882
)
6983
.toBe(3);
7084

71-
const { status } = await fetch(`${workerUrl}/test-workflow-status?id=${instanceId}`).then(res => res.json());
85+
const { status } = await fetchWorker(`/test-workflow-status?id=${instanceId}`).then(res => res.json());
7286
expect(['running', 'waiting']).toContain(status);
7387
});

‎dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/lib/fragments.ts‎

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -102,73 +102,3 @@ export const CART_QUERY_FRAGMENT = `#graphql
102102
}
103103
}
104104
` as const;
105-
106-
const MENU_FRAGMENT = `#graphql
107-
fragment MenuItem on MenuItem {
108-
id
109-
resourceId
110-
tags
111-
title
112-
type
113-
url
114-
}
115-
fragment ChildMenuItem on MenuItem {
116-
...MenuItem
117-
}
118-
fragment ParentMenuItem on MenuItem {
119-
...MenuItem
120-
items {
121-
...ChildMenuItem
122-
}
123-
}
124-
fragment Menu on Menu {
125-
id
126-
items {
127-
...ParentMenuItem
128-
}
129-
}
130-
` as const;
131-
132-
export const HEADER_QUERY = `#graphql
133-
fragment Shop on Shop {
134-
id
135-
name
136-
description
137-
primaryDomain {
138-
url
139-
}
140-
brand {
141-
logo {
142-
image {
143-
url
144-
}
145-
}
146-
}
147-
}
148-
query Header(
149-
$country: CountryCode
150-
$headerMenuHandle: String!
151-
$language: LanguageCode
152-
) @inContext(language: $language, country: $country) {
153-
shop {
154-
...Shop
155-
}
156-
menu(handle: $headerMenuHandle) {
157-
...Menu
158-
}
159-
}
160-
${MENU_FRAGMENT}
161-
` as const;
162-
163-
export const FOOTER_QUERY = `#graphql
164-
query Footer(
165-
$country: CountryCode
166-
$footerMenuHandle: String!
167-
$language: LanguageCode
168-
) @inContext(language: $language, country: $country) {
169-
menu(handle: $footerMenuHandle) {
170-
...Menu
171-
}
172-
}
173-
${MENU_FRAGMENT}
174-
` as const;

‎dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/root.tsx‎

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
Scripts,
99
ScrollRestoration,
1010
} from 'react-router';
11-
import { FOOTER_QUERY, HEADER_QUERY } from '~/lib/fragments';
1211

1312
import { useNonce } from '@shopify/hydrogen';
1413

@@ -56,17 +55,11 @@ export function links() {
5655
}
5756

5857
export async function loader(args: LoaderFunctionArgs) {
59-
// Start fetching non-critical data without blocking time to first byte
60-
const deferredData = loadDeferredData(args);
61-
62-
// Await the critical data required to render initial state of the page
63-
const criticalData = await loadCriticalData(args);
64-
65-
const { env } = args.context;
58+
const { env, customerAccount, cart } = args.context;
6659

6760
return {
68-
...deferredData,
69-
...criticalData,
61+
cart: cart.get(),
62+
isLoggedIn: customerAccount.isLoggedIn(),
7063
ENV: {
7164
sentryTrace: env.SENTRY_TRACE,
7265
sentryBaggage: env.SENTRY_BAGGAGE,
@@ -83,54 +76,6 @@ export async function loader(args: LoaderFunctionArgs) {
8376
};
8477
}
8578

86-
/**
87-
* Load data necessary for rendering content above the fold. This is the critical data
88-
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
89-
*/
90-
async function loadCriticalData({ context }: LoaderFunctionArgs) {
91-
const { storefront } = context;
92-
93-
const [header] = await Promise.all([
94-
storefront.query(HEADER_QUERY, {
95-
cache: storefront.CacheLong(),
96-
variables: {
97-
headerMenuHandle: 'main-menu', // Adjust to your header menu handle
98-
},
99-
}),
100-
// Add other queries here, so that they are loaded in parallel
101-
]);
102-
103-
return { header };
104-
}
105-
106-
/**
107-
* Load data for rendering content below the fold. This data is deferred and will be
108-
* fetched after the initial page load. If it's unavailable, the page should still 200.
109-
* Make sure to not throw any errors here, as it will cause the page to 500.
110-
*/
111-
function loadDeferredData({ context }: LoaderFunctionArgs) {
112-
const { storefront, customerAccount, cart } = context;
113-
114-
// defer the footer query (below the fold)
115-
const footer = storefront
116-
.query(FOOTER_QUERY, {
117-
cache: storefront.CacheLong(),
118-
variables: {
119-
footerMenuHandle: 'footer', // Adjust to your footer menu handle
120-
},
121-
})
122-
.catch((error: any) => {
123-
// Log query errors, but don't throw them so the page can still render
124-
console.error(error);
125-
return null;
126-
});
127-
return {
128-
cart: cart.get(),
129-
isLoggedIn: customerAccount.isLoggedIn(),
130-
footer,
131-
};
132-
}
133-
13479
export function Layout({ children }: { children?: React.ReactNode }) {
13580
const nonce = useNonce();
13681

‎dev-packages/e2e-tests/test-applications/remix-hydrogen/app/lib/fragments.ts‎

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -102,73 +102,3 @@ export const CART_QUERY_FRAGMENT = `#graphql
102102
}
103103
}
104104
` as const;
105-
106-
const MENU_FRAGMENT = `#graphql
107-
fragment MenuItem on MenuItem {
108-
id
109-
resourceId
110-
tags
111-
title
112-
type
113-
url
114-
}
115-
fragment ChildMenuItem on MenuItem {
116-
...MenuItem
117-
}
118-
fragment ParentMenuItem on MenuItem {
119-
...MenuItem
120-
items {
121-
...ChildMenuItem
122-
}
123-
}
124-
fragment Menu on Menu {
125-
id
126-
items {
127-
...ParentMenuItem
128-
}
129-
}
130-
` as const;
131-
132-
export const HEADER_QUERY = `#graphql
133-
fragment Shop on Shop {
134-
id
135-
name
136-
description
137-
primaryDomain {
138-
url
139-
}
140-
brand {
141-
logo {
142-
image {
143-
url
144-
}
145-
}
146-
}
147-
}
148-
query Header(
149-
$country: CountryCode
150-
$headerMenuHandle: String!
151-
$language: LanguageCode
152-
) @inContext(language: $language, country: $country) {
153-
shop {
154-
...Shop
155-
}
156-
menu(handle: $headerMenuHandle) {
157-
...Menu
158-
}
159-
}
160-
${MENU_FRAGMENT}
161-
` as const;
162-
163-
export const FOOTER_QUERY = `#graphql
164-
query Footer(
165-
$country: CountryCode
166-
$footerMenuHandle: String!
167-
$language: LanguageCode
168-
) @inContext(language: $language, country: $country) {
169-
menu(handle: $footerMenuHandle) {
170-
...Menu
171-
}
172-
}
173-
${MENU_FRAGMENT}
174-
` as const;

0 commit comments

Comments
 (0)