Skip to content

Commit 10c1fe8

Browse files
committed
add cache components streaming variant test app
1 parent 7aef0f7 commit 10c1fe8

29 files changed

Lines changed: 620 additions & 0 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Suspense } from 'react';
2+
import { headers } from 'next/headers';
3+
import * as Sentry from '@sentry/nextjs';
4+
5+
async function CachedContent() {
6+
const getTodos = async () => {
7+
return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
8+
'use cache';
9+
await new Promise(resolve => setTimeout(resolve, 100));
10+
return [1, 2, 3, 4, 5];
11+
});
12+
};
13+
14+
const todos = await getTodos();
15+
16+
return <div id="todos-fetched">Todos fetched: {todos.length}</div>;
17+
}
18+
19+
async function DynamicContent() {
20+
await headers();
21+
return (
22+
<>
23+
<CachedContent />
24+
</>
25+
);
26+
}
27+
28+
export default function Page() {
29+
return (
30+
<>
31+
<h1>Cache Pageload Tracing</h1>
32+
<Suspense fallback={<div>Loading...</div>}>
33+
<DynamicContent />
34+
</Suspense>
35+
</>
36+
);
37+
}

dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,33 @@ test('Should generate metadata async', async ({ page }) => {
5252
await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
5353
await expect(page).toHaveTitle('Product: 1');
5454
});
55+
56+
test('Metatag injection produces exactly one pageload trace with cache components', async ({ page }) => {
57+
const serverTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => {
58+
return (
59+
transactionEvent.contexts?.trace?.op === 'http.server' &&
60+
transactionEvent.transaction === 'GET /pageload-tracing'
61+
);
62+
});
63+
64+
const pageloadTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => {
65+
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/pageload-tracing';
66+
});
67+
68+
await page.goto('/pageload-tracing');
69+
70+
await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
71+
72+
const [serverTx, pageloadTx] = await Promise.all([serverTxPromise, pageloadTxPromise]);
73+
74+
const serverTraceId = serverTx.contexts?.trace?.trace_id;
75+
const pageloadTraceId = pageloadTx.contexts?.trace?.trace_id;
76+
77+
// Server and client pageload must share the same trace via metatag injection
78+
expect(pageloadTraceId).toBeTruthy();
79+
expect(serverTraceId).toBe(pageloadTraceId);
80+
81+
// Exactly one set of trace meta tags — no duplicates that would cause multiple pageloads
82+
expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(1);
83+
expect(await page.locator('meta[name="baggage"]').count()).toBe(1);
84+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
42+
43+
# Sentry Config File
44+
.env.sentry-build-plugin
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Suspense } from 'react';
2+
import * as Sentry from '@sentry/nextjs';
3+
4+
export default function Page() {
5+
return (
6+
<>
7+
<h1>This will be pre-rendered</h1>
8+
<DynamicContent />
9+
</>
10+
);
11+
}
12+
13+
async function DynamicContent() {
14+
const getTodos = async () => {
15+
return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
16+
'use cache';
17+
await new Promise(resolve => setTimeout(resolve, 100));
18+
return [1, 2, 3, 4, 5];
19+
});
20+
};
21+
22+
const todos = await getTodos();
23+
24+
return <div id="todos-fetched">Todos fetched: {todos.length}</div>;
25+
}
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
import NextError from 'next/error';
5+
import { useEffect } from 'react';
6+
7+
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
8+
useEffect(() => {
9+
Sentry.captureException(error);
10+
}, [error]);
11+
12+
return (
13+
<html>
14+
<body>
15+
{/* `NextError` is the default Next.js error page component. Its type
16+
definition requires a `statusCode` prop. However, since the App Router
17+
does not expose status codes for errors, we simply pass 0 to render a
18+
generic error message. */}
19+
<NextError statusCode={0} />
20+
</body>
21+
</html>
22+
);
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Layout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<html lang="en">
4+
<body>{children}</body>
5+
</html>
6+
);
7+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
function fetchPost() {
4+
return Promise.resolve({ id: '1', title: 'Post 1' });
5+
}
6+
7+
export async function generateMetadata() {
8+
const { id } = await fetchPost();
9+
const product = `Product: ${id}`;
10+
11+
return {
12+
title: product,
13+
};
14+
}
15+
16+
export default function Page() {
17+
return (
18+
<>
19+
<h1>This will be pre-rendered</h1>
20+
<DynamicContent />
21+
</>
22+
);
23+
}
24+
25+
async function DynamicContent() {
26+
const getTodos = async () => {
27+
return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
28+
'use cache';
29+
await new Promise(resolve => setTimeout(resolve, 100));
30+
return [1, 2, 3, 4, 5];
31+
});
32+
};
33+
34+
const todos = await getTodos();
35+
36+
return <div id="todos-fetched">Todos fetched: {todos.length}</div>;
37+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
/**
4+
* Tests generateMetadata function with cache components, this calls the propagation context to be set
5+
* Which will generate and set a trace id in the propagation context, which should trigger the random API error if unpatched
6+
* See: https://github.com/getsentry/sentry-javascript/issues/18392
7+
*/
8+
export function generateMetadata() {
9+
return {
10+
title: 'Cache Components Metadata Test',
11+
};
12+
}
13+
14+
export default function Page() {
15+
return (
16+
<>
17+
<h1>This will be pre-rendered</h1>
18+
<DynamicContent />
19+
</>
20+
);
21+
}
22+
23+
async function DynamicContent() {
24+
const getTodos = async () => {
25+
return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
26+
'use cache';
27+
await new Promise(resolve => setTimeout(resolve, 100));
28+
return [1, 2, 3, 4, 5];
29+
});
30+
};
31+
32+
const todos = await getTodos();
33+
34+
return <div id="todos-fetched">Todos fetched: {todos.length}</div>;
35+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Next 16 CacheComponents test app</p>;
3+
}

0 commit comments

Comments
 (0)