Skip to content

Commit c8df5ab

Browse files
authored
test(react): Add gql tests for react router (#19844)
- Adds E2E tests verifying that GraphQL fetch spans are attributed to the correct navigation transaction in React Router 7 lazy routes - Test 1: Navigate from index to lazy GQL page → asserts UserAQuery span is in the navigation transaction (not the pageload) - Test 2: Navigate between two lazy GQL pages → asserts UserAQuery only in first nav, UserBQuery only in second nav, no cross-leaking Closes #19845 (added automatically)
1 parent ca1a724 commit c8df5ab

File tree

5 files changed

+223
-0
lines changed

5 files changed

+223
-0
lines changed

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ const lazyRouteManifest = [
7373
'/deep/level2/level3/:id',
7474
'/slow-fetch/:id',
7575
'/wildcard-lazy/:id',
76+
'/lazy-gql-a/fetch',
77+
'/lazy-gql-b/fetch',
7678
];
7779

7880
Sentry.init({
@@ -169,6 +171,18 @@ const router = sentryCreateBrowserRouter(
169171
lazyChildren: () => import('./pages/WildcardLazyRoutes').then(module => module.wildcardRoutes),
170172
},
171173
},
174+
{
175+
path: '/lazy-gql-a',
176+
handle: {
177+
lazyChildren: () => import('./pages/LazyFetchRoutes').then(module => module.lazyGqlARoutes),
178+
},
179+
},
180+
{
181+
path: '/lazy-gql-b',
182+
handle: {
183+
lazyChildren: () => import('./pages/LazyFetchSubRoutes').then(module => module.lazyGqlBRoutes),
184+
},
185+
},
172186
],
173187
{
174188
async patchRoutesOnNavigation({ matches, patch }: Parameters<PatchRoutesOnNavigationFunction>[0]) {

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/pages/Index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ const Index = () => {
3939
<Link to="/wildcard-lazy/789" id="navigation-to-wildcard-lazy">
4040
Navigate to Wildcard Lazy Route (500ms delay, no fetch)
4141
</Link>
42+
<br />
43+
<Link to="/lazy-gql-a/fetch" id="navigation-to-gql-a">
44+
Navigate to GQL Page A
45+
</Link>
4246
</>
4347
);
4448
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom';
3+
4+
const GqlPageA = () => {
5+
const [data, setData] = React.useState<{ data?: unknown } | null>(null);
6+
7+
React.useEffect(() => {
8+
fetch('/api/graphql?op=UserAQuery', {
9+
method: 'POST',
10+
headers: { 'Content-Type': 'application/json' },
11+
body: JSON.stringify({ query: '{ userA { id name } }', operationName: 'UserAQuery' }),
12+
})
13+
.then(res => res.json())
14+
.then(setData)
15+
.catch(() => setData({ data: { error: 'failed' } }));
16+
}, []);
17+
18+
return (
19+
<div id="gql-page-a">
20+
<h1>GQL Page A</h1>
21+
<p id="gql-page-a-data">{data ? JSON.stringify(data) : 'loading...'}</p>
22+
<Link to="/lazy-gql-b/fetch" id="navigate-to-gql-b">
23+
Navigate to GQL Page B
24+
</Link>
25+
</div>
26+
);
27+
};
28+
29+
export const lazyGqlARoutes = [
30+
{
31+
path: 'fetch',
32+
element: <GqlPageA />,
33+
},
34+
];
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom';
3+
4+
const GqlPageB = () => {
5+
const [data, setData] = React.useState<{ data?: unknown } | null>(null);
6+
7+
React.useEffect(() => {
8+
fetch('/api/graphql?op=UserBQuery', {
9+
method: 'POST',
10+
headers: { 'Content-Type': 'application/json' },
11+
body: JSON.stringify({ query: '{ userB { id email } }', operationName: 'UserBQuery' }),
12+
})
13+
.then(res => res.json())
14+
.then(setData)
15+
.catch(() => setData({ data: { error: 'failed' } }));
16+
}, []);
17+
18+
return (
19+
<div id="gql-page-b">
20+
<h1>GQL Page B</h1>
21+
<p id="gql-page-b-data">{data ? JSON.stringify(data) : 'loading...'}</p>
22+
<Link to="/" id="gql-b-home-link">
23+
Go Home
24+
</Link>
25+
</div>
26+
);
27+
};
28+
29+
export const lazyGqlBRoutes = [
30+
{
31+
path: 'fetch',
32+
element: <GqlPageB />,
33+
},
34+
];

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,3 +1484,140 @@ test('Route manifest provides correct name when pageload span ends before lazy r
14841484
expect(event.contexts?.trace?.op).toBe('pageload');
14851485
expect(event.contexts?.trace?.data?.['sentry.source']).toBe('route');
14861486
});
1487+
1488+
test('GQL fetch span is attributed to the correct navigation transaction when navigating from index to lazy GQL page', async ({
1489+
page,
1490+
}) => {
1491+
const pageloadPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
1492+
return (
1493+
!!transactionEvent?.transaction &&
1494+
transactionEvent.contexts?.trace?.op === 'pageload' &&
1495+
transactionEvent.transaction === '/'
1496+
);
1497+
});
1498+
1499+
const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
1500+
return (
1501+
!!transactionEvent?.transaction &&
1502+
transactionEvent.contexts?.trace?.op === 'navigation' &&
1503+
transactionEvent.transaction === '/lazy-gql-a/fetch'
1504+
);
1505+
});
1506+
1507+
await page.goto('/');
1508+
const pageloadEvent = await pageloadPromise;
1509+
1510+
// Pageload should NOT contain any /api/graphql spans (neither UserAQuery nor UserBQuery)
1511+
const pageloadSpans = pageloadEvent.spans || [];
1512+
const pageloadGqlSpans = pageloadSpans.filter(
1513+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1514+
span.op === 'http.client' &&
1515+
(span.description?.includes('/api/graphql') || span.data?.url?.includes('/api/graphql')),
1516+
);
1517+
expect(pageloadGqlSpans.length).toBe(0);
1518+
1519+
// Navigate to lazy GQL page A
1520+
const gqlLink = page.locator('id=navigation-to-gql-a');
1521+
await expect(gqlLink).toBeVisible();
1522+
await gqlLink.click();
1523+
1524+
const navigationEvent = await navigationPromise;
1525+
1526+
// Verify the lazy GQL page rendered
1527+
await expect(page.locator('id=gql-page-a')).toBeVisible();
1528+
1529+
// Verify the navigation transaction has the correct name
1530+
expect(navigationEvent.transaction).toBe('/lazy-gql-a/fetch');
1531+
expect(navigationEvent.contexts?.trace?.op).toBe('navigation');
1532+
1533+
// Verify the UserAQuery GQL fetch span is inside this navigation transaction
1534+
const navSpans = navigationEvent.spans || [];
1535+
const userASpans = navSpans.filter(
1536+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1537+
span.op === 'http.client' && (span.description?.includes('UserAQuery') || span.data?.url?.includes('UserAQuery')),
1538+
);
1539+
expect(userASpans.length).toBe(1);
1540+
1541+
// Verify NO UserBQuery spans leaked into this transaction
1542+
const userBSpans = navSpans.filter(
1543+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1544+
span.op === 'http.client' && (span.description?.includes('UserBQuery') || span.data?.url?.includes('UserBQuery')),
1545+
);
1546+
expect(userBSpans.length).toBe(0);
1547+
});
1548+
1549+
test('GQL fetch spans are attributed to correct navigation transactions when navigating between two lazy GQL pages', async ({
1550+
page,
1551+
}) => {
1552+
await page.goto('/');
1553+
await page.waitForTimeout(500);
1554+
1555+
// Navigate to GQL page A
1556+
const firstNavPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
1557+
return (
1558+
!!transactionEvent?.transaction &&
1559+
transactionEvent.contexts?.trace?.op === 'navigation' &&
1560+
transactionEvent.transaction === '/lazy-gql-a/fetch'
1561+
);
1562+
});
1563+
1564+
const gqlALink = page.locator('id=navigation-to-gql-a');
1565+
await expect(gqlALink).toBeVisible();
1566+
await gqlALink.click();
1567+
1568+
const firstNavEvent = await firstNavPromise;
1569+
await expect(page.locator('id=gql-page-a')).toBeVisible();
1570+
1571+
// First navigation should have exactly the UserAQuery span
1572+
const firstNavSpans = firstNavEvent.spans || [];
1573+
const firstUserASpans = firstNavSpans.filter(
1574+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1575+
span.op === 'http.client' && (span.description?.includes('UserAQuery') || span.data?.url?.includes('UserAQuery')),
1576+
);
1577+
expect(firstUserASpans.length).toBe(1);
1578+
1579+
// First navigation must NOT contain UserBQuery spans
1580+
const firstUserBSpans = firstNavSpans.filter(
1581+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1582+
span.op === 'http.client' && (span.description?.includes('UserBQuery') || span.data?.url?.includes('UserBQuery')),
1583+
);
1584+
expect(firstUserBSpans.length).toBe(0);
1585+
1586+
// Now navigate from GQL page A to GQL page B
1587+
const secondNavPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
1588+
return (
1589+
!!transactionEvent?.transaction &&
1590+
transactionEvent.contexts?.trace?.op === 'navigation' &&
1591+
transactionEvent.transaction === '/lazy-gql-b/fetch'
1592+
);
1593+
});
1594+
1595+
const gqlBLink = page.locator('id=navigate-to-gql-b');
1596+
await expect(gqlBLink).toBeVisible();
1597+
await gqlBLink.click();
1598+
1599+
const secondNavEvent = await secondNavPromise;
1600+
await expect(page.locator('id=gql-page-b')).toBeVisible();
1601+
1602+
// Second navigation should have exactly the UserBQuery span
1603+
const secondNavSpans = secondNavEvent.spans || [];
1604+
const secondUserBSpans = secondNavSpans.filter(
1605+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1606+
span.op === 'http.client' && (span.description?.includes('UserBQuery') || span.data?.url?.includes('UserBQuery')),
1607+
);
1608+
expect(secondUserBSpans.length).toBe(1);
1609+
1610+
// Second navigation must NOT contain UserAQuery spans (no leaking from first nav)
1611+
const secondUserASpans = secondNavSpans.filter(
1612+
(span: { op?: string; description?: string; data?: { url?: string } }) =>
1613+
span.op === 'http.client' && (span.description?.includes('UserAQuery') || span.data?.url?.includes('UserAQuery')),
1614+
);
1615+
expect(secondUserASpans.length).toBe(0);
1616+
1617+
// Verify the two transactions have different trace IDs
1618+
const firstTraceId = firstNavEvent.contexts?.trace?.trace_id;
1619+
const secondTraceId = secondNavEvent.contexts?.trace?.trace_id;
1620+
expect(firstTraceId).toBeDefined();
1621+
expect(secondTraceId).toBeDefined();
1622+
expect(firstTraceId).not.toBe(secondTraceId);
1623+
});

0 commit comments

Comments
 (0)