Skip to content

Commit 9f0f16b

Browse files
chargomeclaude
andcommitted
feat(nextjs): Only wait for the trace meta tag when cacheComponents is on
The wait exists for a document that resumes a prerendered shell, whose tag arrives with the resumed part. Without `cacheComponents` a tag is either in `<head>` from the start or never comes, so waiting only held the pageload back: fetches before the tag were not attached and the span ended earlier. The build now injects `_sentryCacheComponents`, and the client only waits when it is set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent e0b0cae commit 9f0f16b

5 files changed

Lines changed: 48 additions & 3 deletions

File tree

‎packages/nextjs/src/client/routing/traceMetaTags.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Span } from '@sentry/core';
22
import { extractTraceparentData } from '@sentry/core';
3+
import { GLOBAL_OBJ } from '@sentry/core';
34
import { WINDOW } from '@sentry/react';
45

56
// `@opentelemetry/api` `TraceFlags`, inlined because core does not export its copies.
@@ -15,6 +16,13 @@ function getDocument(): Document | undefined {
1516
return WINDOW.document;
1617
}
1718

19+
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryCacheComponents?: string };
20+
21+
/** Whether the build enabled `cacheComponents`, so a document can resume a prerendered shell. */
22+
function hasCacheComponents(): boolean {
23+
return (process.env._sentryCacheComponents ?? globalWithInjectedValues._sentryCacheComponents) === 'true';
24+
}
25+
1826
function lastMetaContent(document: Document, name: string): string | undefined {
1927
const tags = document.querySelectorAll(`meta[name="${name}"]`);
2028
return tags[tags.length - 1]?.getAttribute('content') || undefined;
@@ -44,12 +52,18 @@ export function hasServerTimingTraceContext(): boolean {
4452
}
4553

4654
/**
47-
* Whether the SDK can still expect a `sentry-trace` meta tag to arrive: the document is streaming
48-
* and nothing else already carries the trace.
55+
* Whether the SDK can still expect a `sentry-trace` meta tag to arrive: the app has `cacheComponents`
56+
* enabled, so a document can resume a prerendered shell and get its tag with the resumed part, the
57+
* document is still streaming, and nothing else already carries the trace. Without `cacheComponents`
58+
* a tag is either in `<head>` from the start or never comes, so waiting would only hold the pageload
59+
* back.
4960
*/
5061
export function canWaitForTraceMetaTag(): boolean {
5162
return (
52-
getDocument()?.readyState === 'loading' && typeof MutationObserver !== 'undefined' && !hasServerTimingTraceContext()
63+
hasCacheComponents() &&
64+
getDocument()?.readyState === 'loading' &&
65+
typeof MutationObserver !== 'undefined' &&
66+
!hasServerTimingTraceContext()
5367
);
5468
}
5569

‎packages/nextjs/src/config/withSentryConfig/buildTime.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export function setUpBuildTimeVariables(
6969
buildTimeVariables._sentryRelease = releaseName;
7070
}
7171

72+
// The client only waits for a late trace meta tag when a document can resume a prerendered shell.
73+
if (userNextConfig.cacheComponents) {
74+
buildTimeVariables._sentryCacheComponents = 'true';
75+
}
76+
7277
// See `client/routing/nextRoutingInstrumentation.ts`.
7378
if (hasOnlyAppRouterPages(projectDir)) {
7479
buildTimeVariables._sentryHasPagesRouter = 'false';

‎packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ describe('appRouterInstrumentPageLoad', () => {
275275
window.history.replaceState({}, '', '/navigation?from=document');
276276
Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true });
277277
vi.spyOn(performance, 'getEntriesByType').mockReturnValue([]);
278+
vi.stubEnv('_sentryCacheComponents', 'true');
278279
});
279280

280281
afterEach(() => {
@@ -286,6 +287,7 @@ describe('appRouterInstrumentPageLoad', () => {
286287
Object.defineProperty(Document.prototype, 'readyState', originalReadyState);
287288
}
288289
vi.restoreAllMocks();
290+
vi.unstubAllEnvs();
289291
});
290292

291293
it('records the URL of the document when the pageload starts after a navigation changed it', async () => {

‎packages/nextjs/test/client/traceMetaTags.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function setReadyState(value: DocumentReadyState): void {
2828
beforeEach(() => {
2929
document.head.innerHTML = '';
3030
document.body.innerHTML = '';
31+
vi.stubEnv('_sentryCacheComponents', 'true');
3132
});
3233

3334
afterEach(() => {
@@ -37,6 +38,7 @@ afterEach(() => {
3738
Object.defineProperty(Document.prototype, 'readyState', originalReadyState);
3839
}
3940
vi.restoreAllMocks();
41+
vi.unstubAllEnvs();
4042
vi.useRealTimers();
4143
});
4244

@@ -77,6 +79,14 @@ describe('canWaitForTraceMetaTag', () => {
7779
expect(canWaitForTraceMetaTag()).toBe(true);
7880
});
7981

82+
it('is false when the build did not enable cacheComponents', () => {
83+
vi.stubEnv('_sentryCacheComponents', '');
84+
setReadyState('loading');
85+
vi.spyOn(performance, 'getEntriesByType').mockReturnValue([]);
86+
87+
expect(canWaitForTraceMetaTag()).toBe(false);
88+
});
89+
8090
it('is false once the document finished parsing', () => {
8191
setReadyState('interactive');
8292

‎packages/nextjs/test/config/withSentryConfig/buildTime.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,17 @@ describe('setUpBuildTimeVariables', () => {
5858
});
5959
});
6060
});
61+
62+
describe('_sentryCacheComponents', () => {
63+
it('is set when the Next.js config enables cacheComponents', () => {
64+
const config: NextConfigObject = { cacheComponents: true };
65+
setUpBuildTimeVariables(config, {}, undefined);
66+
expect(config.env?._sentryCacheComponents).toBe('true');
67+
});
68+
69+
it('is left unset otherwise', () => {
70+
const config: NextConfigObject = {};
71+
setUpBuildTimeVariables(config, {}, undefined);
72+
expect(config.env?._sentryCacheComponents).toBeUndefined();
73+
});
74+
});

0 commit comments

Comments
 (0)