Skip to content

Commit a8cb0f6

Browse files
authored
feat(nextjs): Extract tracing logic from server component wrapper templates (#18408)
closes https://linear.app/getsentry/issue/JS-1207/remove-tracing-from-app-router-server-components-templates closes #18307
1 parent 4be48a6 commit a8cb0f6

File tree

11 files changed

+322
-187
lines changed

11 files changed

+322
-187
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PropsWithChildren } from 'react';
2+
3+
export const dynamic = 'force-dynamic';
4+
5+
export default function Layout({ children }: PropsWithChildren<{}>) {
6+
return (
7+
<div>
8+
<p>DynamicLayout</p>
9+
{children}
10+
</div>
11+
);
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const dynamic = 'force-dynamic';
2+
3+
export default async function Page() {
4+
return (
5+
<div>
6+
<p>Dynamic Page</p>
7+
</div>
8+
);
9+
}
10+
11+
export async function generateMetadata() {
12+
return {
13+
title: 'I am dynamic page generated metadata',
14+
};
15+
}

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
import { isNext13 } from './nextjsVersion';
34

45
test('Will create a transaction with spans for every server component and metadata generation functions when visiting a page', async ({
56
page,
@@ -14,8 +15,49 @@ test('Will create a transaction with spans for every server component and metada
1415
return span.description;
1516
});
1617

17-
expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout)/nested-layout)');
18-
expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout))');
19-
expect(spanDescriptions).toContainEqual('Page Server Component (/(nested-layout)/nested-layout)');
18+
expect(spanDescriptions).toContainEqual('render route (app) /nested-layout');
19+
expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page');
2020
expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)');
21+
22+
// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans
23+
if (!isNext13) {
24+
expect(spanDescriptions).toContainEqual('resolve page components');
25+
expect(spanDescriptions).toContainEqual('build component tree');
26+
expect(spanDescriptions).toContainEqual('resolve root layout server component');
27+
expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"');
28+
expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"');
29+
expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout"');
30+
expect(spanDescriptions).toContainEqual('start response');
31+
}
32+
});
33+
34+
test('Will create a transaction with spans for every server component and metadata generation functions when visiting a dynamic page', async ({
35+
page,
36+
}) => {
37+
const serverTransactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
38+
console.log(transactionEvent?.transaction);
39+
return transactionEvent?.transaction === 'GET /nested-layout/[dynamic]';
40+
});
41+
42+
await page.goto('/nested-layout/123');
43+
44+
const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => {
45+
return span.description;
46+
});
47+
48+
expect(spanDescriptions).toContainEqual('render route (app) /nested-layout/[dynamic]');
49+
expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page');
50+
expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout/[dynamic])');
51+
52+
// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans
53+
if (!isNext13) {
54+
expect(spanDescriptions).toContainEqual('resolve page components');
55+
expect(spanDescriptions).toContainEqual('build component tree');
56+
expect(spanDescriptions).toContainEqual('resolve root layout server component');
57+
expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"');
58+
expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"');
59+
expect(spanDescriptions).toContainEqual('resolve layout server component "[dynamic]"');
60+
expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout/[dynamic]"');
61+
expect(spanDescriptions).toContainEqual('start response');
62+
}
2163
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const packageJson = require('../package.json');
2+
const nextjsVersion = packageJson.dependencies.next;
3+
const nextjsMajor = Number(nextjsVersion.split('.')[0]);
4+
5+
export const isNext13 = !isNaN(nextjsMajor) && nextjsMajor === 13;
6+
export const nextjsMajorVersion = nextjsMajor;

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
import { isNext13 } from './nextjsVersion';
34

45
test('Sends a transaction for a request to app router', async ({ page }) => {
56
const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
@@ -70,19 +71,29 @@ test('Should set a "not_found" status on a server component span when notFound()
7071

7172
const transactionEvent = await serverComponentTransactionPromise;
7273

73-
// Transaction should have status ok, because the http status is ok, but the server component span should be not_found
74+
// Transaction should have status ok, because the http status is ok, but the render component span should be not_found
7475
expect(transactionEvent.contexts?.trace?.status).toBe('ok');
7576
expect(transactionEvent.spans).toContainEqual(
7677
expect.objectContaining({
77-
description: 'Page Server Component (/server-component/not-found)',
78-
op: 'function.nextjs',
78+
description: 'render route (app) /server-component/not-found',
7979
status: 'not_found',
80-
data: expect.objectContaining({
81-
'sentry.nextjs.ssr.function.type': 'Page',
82-
'sentry.nextjs.ssr.function.route': '/server-component/not-found',
83-
}),
8480
}),
8581
);
82+
83+
// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following span
84+
if (!isNext13) {
85+
// Page server component span should have the right name and attributes
86+
expect(transactionEvent.spans).toContainEqual(
87+
expect.objectContaining({
88+
description: 'resolve page server component "/server-component/not-found"',
89+
op: 'function.nextjs',
90+
data: expect.objectContaining({
91+
'sentry.nextjs.ssr.function.type': 'Page',
92+
'sentry.nextjs.ssr.function.route': '/server-component/not-found',
93+
}),
94+
}),
95+
);
96+
}
8697
});
8798

8899
test('Should capture an error and transaction for a app router page', async ({ page }) => {
@@ -102,20 +113,30 @@ test('Should capture an error and transaction for a app router page', async ({ p
102113
// Error event should have the right transaction name
103114
expect(errorEvent.transaction).toBe(`Page Server Component (/server-component/faulty)`);
104115

105-
// Transaction should have status ok, because the http status is ok, but the server component span should be internal_error
116+
// Transaction should have status ok, because the http status is ok, but the render component span should be internal_error
106117
expect(transactionEvent.contexts?.trace?.status).toBe('ok');
107118
expect(transactionEvent.spans).toContainEqual(
108119
expect.objectContaining({
109-
description: 'Page Server Component (/server-component/faulty)',
110-
op: 'function.nextjs',
120+
description: 'render route (app) /server-component/faulty',
111121
status: 'internal_error',
112-
data: expect.objectContaining({
113-
'sentry.nextjs.ssr.function.type': 'Page',
114-
'sentry.nextjs.ssr.function.route': '/server-component/faulty',
115-
}),
116122
}),
117123
);
118124

125+
// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following span
126+
if (!isNext13) {
127+
// The page server component span should have the right name and attributes
128+
expect(transactionEvent.spans).toContainEqual(
129+
expect.objectContaining({
130+
description: 'resolve page server component "/server-component/faulty"',
131+
op: 'function.nextjs',
132+
data: expect.objectContaining({
133+
'sentry.nextjs.ssr.function.type': 'Page',
134+
'sentry.nextjs.ssr.function.route': '/server-component/faulty',
135+
}),
136+
}),
137+
);
138+
}
139+
119140
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
120141
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
121142
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const ATTR_NEXT_SPAN_TYPE = 'next.span_type';
22
export const ATTR_NEXT_SPAN_NAME = 'next.span_name';
33
export const ATTR_NEXT_ROUTE = 'next.route';
4+
export const ATTR_NEXT_SPAN_DESCRIPTION = 'next.span_description';
5+
export const ATTR_NEXT_SEGMENT = 'next.segment';

packages/nextjs/src/common/utils/tracingUtils.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
import type { PropagationContext } from '@sentry/core';
2-
import { debug, getActiveSpan, getRootSpan, GLOBAL_OBJ, Scope, spanToJSON, startNewTrace } from '@sentry/core';
1+
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
2+
import type { PropagationContext, Span, SpanAttributes } from '@sentry/core';
3+
import {
4+
debug,
5+
getActiveSpan,
6+
getRootSpan,
7+
GLOBAL_OBJ,
8+
Scope,
9+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
10+
spanToJSON,
11+
startNewTrace,
12+
} from '@sentry/core';
313
import { DEBUG_BUILD } from '../debug-build';
14+
import { ATTR_NEXT_SEGMENT, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes';
415
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';
516

617
const commonPropagationContextMap = new WeakMap<object, PropagationContext>();
718

19+
const PAGE_SEGMENT = '__PAGE__';
20+
821
/**
922
* Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context.
1023
*
@@ -108,3 +121,61 @@ export function dropNextjsRootContext(): void {
108121
}
109122
}
110123
}
124+
125+
/**
126+
* Checks if the span is a resolve segment span.
127+
* @param spanAttributes The attributes of the span to check.
128+
* @returns True if the span is a resolve segment span, false otherwise.
129+
*/
130+
export function isResolveSegmentSpan(spanAttributes: SpanAttributes): boolean {
131+
return (
132+
spanAttributes[ATTR_NEXT_SPAN_TYPE] === 'NextNodeServer.getLayoutOrPageModule' &&
133+
spanAttributes[ATTR_NEXT_SPAN_NAME] === 'resolve segment modules' &&
134+
typeof spanAttributes[ATTR_NEXT_SEGMENT] === 'string'
135+
);
136+
}
137+
138+
/**
139+
* Returns the enhanced name for a resolve segment span.
140+
* @param segment The segment of the resolve segment span.
141+
* @param route The route of the resolve segment span.
142+
* @returns The enhanced name for the resolve segment span.
143+
*/
144+
export function getEnhancedResolveSegmentSpanName({ segment, route }: { segment: string; route: string }): string {
145+
if (segment === PAGE_SEGMENT) {
146+
return `resolve page server component "${route}"`;
147+
}
148+
149+
if (segment === '') {
150+
return 'resolve root layout server component';
151+
}
152+
153+
return `resolve layout server component "${segment}"`;
154+
}
155+
156+
/**
157+
* Maybe enhances the span name for a resolve segment span.
158+
* If the span is not a resolve segment span, this function does nothing.
159+
* @param activeSpan The active span.
160+
* @param spanAttributes The attributes of the span to check.
161+
* @param rootSpanAttributes The attributes of the according root span.
162+
*/
163+
export function maybeEnhanceServerComponentSpanName(
164+
activeSpan: Span,
165+
spanAttributes: SpanAttributes,
166+
rootSpanAttributes: SpanAttributes,
167+
): void {
168+
if (!isResolveSegmentSpan(spanAttributes)) {
169+
return;
170+
}
171+
172+
const segment = spanAttributes[ATTR_NEXT_SEGMENT] as string;
173+
const route = rootSpanAttributes[ATTR_HTTP_ROUTE];
174+
const enhancedName = getEnhancedResolveSegmentSpanName({ segment, route: typeof route === 'string' ? route : '' });
175+
activeSpan.updateName(enhancedName);
176+
activeSpan.setAttributes({
177+
'sentry.nextjs.ssr.function.type': segment === PAGE_SEGMENT ? 'Page' : 'Layout',
178+
'sentry.nextjs.ssr.function.route': route,
179+
});
180+
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.nextjs');
181+
}

0 commit comments

Comments
 (0)