Skip to content

Commit bf21b16

Browse files
chargomeclaude
andauthored
fix(nextjs): Don't inject trace meta tags when Cache Components is enabled (#21141)
Problem is that we do not have a consistent way of detecting stale meta tags so we rather disable them for `cacheComponents`. This stops the SDK from enabling Next's `experimental.clientTraceMetadata` (`sentry-trace`/`baggage`) when `cacheComponents` is enabled. With no meta tags injected, the browser pageload starts a fresh, self-contained trace instead of stitching onto a stale one. Apps that don't use Cache Components are unaffected. closes https://linear.app/getsentry/issue/JS-2782/cachecomponents-setup-breaks-meta-tag-injection --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c5b5683 commit bf21b16

33 files changed

Lines changed: 663 additions & 1 deletion

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/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
2727
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
2828
"import-in-the-middle": "^1",
29-
"next": "16.2.3",
29+
"next": "^16",
3030
"react": "19.1.0",
3131
"react-dom": "19.1.0",
3232
"require-in-the-middle": "^7",

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,38 @@ test('Should generate metadata async', async ({ page }) => {
8181
await expect(page).toHaveTitle('Product: 1');
8282
});
8383

84+
test('Prerendered shell does not stitch the pageload onto a stale trace', async ({ page }) => {
85+
const serverTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => {
86+
return (
87+
transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /pageload-tracing'
88+
);
89+
});
90+
91+
const pageloadTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => {
92+
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/pageload-tracing';
93+
});
94+
95+
await page.goto('/pageload-tracing');
96+
97+
await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
98+
99+
const [serverTx, pageloadTx] = await Promise.all([serverTxPromise, pageloadTxPromise]);
100+
101+
const serverTraceId = serverTx.contexts?.trace?.trace_id;
102+
const pageloadTraceId = pageloadTx.contexts?.trace?.trace_id;
103+
104+
// Under Cache Components the shell is prerendered and rendered in a context detached from the
105+
// runtime server request, so a `sentry-trace` meta tag would carry a stale/unrelated trace. The
106+
// SDK therefore does not enable the trace meta tags, and the browser pageload starts a fresh trace
107+
// instead of stitching onto a trace that doesn't match the server request.
108+
expect(pageloadTraceId).toBeTruthy();
109+
expect(serverTraceId).not.toBe(pageloadTraceId);
110+
111+
// No trace meta tags should be injected when Cache Components is enabled.
112+
expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
113+
expect(await page.locator('meta[name="baggage"]').count()).toBe(0);
114+
});
115+
84116
test('Should prerender a page that captures an exception in generateMetadata', async ({ page }) => {
85117
await page.goto('/capture-metadata');
86118

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+
}

0 commit comments

Comments
 (0)