Skip to content

Commit 7f05a4b

Browse files
Lms24claude
andcommitted
feat(nextjs): Emit low cardinality function span names for server components
The `resolve segment modules` span Next.js emits per page/layout carries `sentry.op: 'function'`, so its name has to follow the conventions rule for that op (`code.function.name`). With span streaming it is now `Page` or `Layout`; with `traceLifecycle: 'static'` the descriptive name is unchanged. `sentry.description` carries the old text in both lifecycles, because `resolve page server component "<route>"` cannot be expressed as a conventions name rule. The route the name no longer carries stays on `http.route`, next to the existing `sentry.nextjs.ssr.function.*` attributes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0644592 commit 7f05a4b

8 files changed

Lines changed: 630 additions & 126 deletions

File tree

dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/server-components.test.ts

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { expect, test } from '@playwright/test';
2-
import { collectSpanNamesUntilSegment, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
3+
4+
// Next.js emits these spans itself. The SDK attaches no op, description or function name to
5+
// them, so asserting `undefined` pins that they stay untouched.
6+
const nextjsSpan = { op: undefined, description: undefined, codeFunctionName: undefined };
37

48
test('Sends a span for a request to app router with URL', async ({ page }) => {
59
const spansPromise = collectStreamedSpansUntilSegment(
@@ -43,39 +47,103 @@ test('Sends a span for a request to app router with URL', async ({ page }) => {
4347
test('Will create spans for every server component and metadata generation functions when visiting a page', async ({
4448
page,
4549
}) => {
46-
const spanNamesPromise = collectSpanNamesUntilSegment('nextjs-16-bun', 'GET /nested-layout');
50+
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-bun', 'GET /nested-layout');
4751

4852
await page.goto('/nested-layout');
4953

50-
const spanNames = await spanNamesPromise;
54+
const fullSpans = await spansPromise;
55+
const spans = fullSpans.map(span => ({
56+
name: span.name,
57+
op: getSpanOp(span),
58+
description: span.attributes['sentry.description']?.value,
59+
codeFunctionName: span.attributes['code.function.name']?.value,
60+
}));
5161

52-
expect(spanNames).toContainEqual('render route (app) /nested-layout');
53-
expect(spanNames).toContainEqual('build component tree');
54-
expect(spanNames).toContainEqual('resolve root layout server component');
55-
expect(spanNames).toContainEqual('resolve layout server component "(nested-layout)"');
56-
expect(spanNames).toContainEqual('resolve layout server component "nested-layout"');
57-
expect(spanNames).toContainEqual('resolve page server component "/nested-layout"');
58-
expect(spanNames).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page');
59-
expect(spanNames).toContainEqual('start response');
62+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'render route (app) /nested-layout' });
63+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'build component tree' });
64+
// Server component spans: the name is the low-cardinality `code.function.name`, and the
65+
// segment each one resolved for is on the description.
66+
expect(spans).toContainEqual({
67+
name: 'Layout',
68+
op: 'function',
69+
description: 'resolve root layout server component',
70+
codeFunctionName: 'Layout',
71+
});
72+
expect(spans).toContainEqual({
73+
name: 'Layout',
74+
op: 'function',
75+
description: 'resolve layout server component "(nested-layout)"',
76+
codeFunctionName: 'Layout',
77+
});
78+
expect(spans).toContainEqual({
79+
name: 'Layout',
80+
op: 'function',
81+
description: 'resolve layout server component "nested-layout"',
82+
codeFunctionName: 'Layout',
83+
});
84+
expect(spans).toContainEqual({
85+
name: 'Page',
86+
op: 'function',
87+
description: 'resolve page server component "/nested-layout"',
88+
codeFunctionName: 'Page',
89+
});
90+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'generateMetadata /(nested-layout)/nested-layout/page' });
91+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'start response' });
6092
});
6193

6294
test('Will create spans for every server component and metadata generation functions when visiting a dynamic page', async ({
6395
page,
6496
}) => {
65-
const spanNamesPromise = collectSpanNamesUntilSegment('nextjs-16-bun', 'GET /nested-layout/[dynamic]');
97+
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-bun', 'GET /nested-layout/[dynamic]');
6698

6799
await page.goto('/nested-layout/123');
68100

69-
const spanNames = await spanNamesPromise;
101+
const fullSpans = await spansPromise;
102+
const spans = fullSpans.map(span => ({
103+
name: span.name,
104+
op: getSpanOp(span),
105+
description: span.attributes['sentry.description']?.value,
106+
codeFunctionName: span.attributes['code.function.name']?.value,
107+
}));
70108

71-
expect(spanNames).toContainEqual('resolve page components');
72-
expect(spanNames).toContainEqual('render route (app) /nested-layout/[dynamic]');
73-
expect(spanNames).toContainEqual('build component tree');
74-
expect(spanNames).toContainEqual('resolve root layout server component');
75-
expect(spanNames).toContainEqual('resolve layout server component "(nested-layout)"');
76-
expect(spanNames).toContainEqual('resolve layout server component "nested-layout"');
77-
expect(spanNames).toContainEqual('resolve layout server component "[dynamic]"');
78-
expect(spanNames).toContainEqual('resolve page server component "/nested-layout/[dynamic]"');
79-
expect(spanNames).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page');
80-
expect(spanNames).toContainEqual('start response');
109+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'resolve page components' });
110+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'render route (app) /nested-layout/[dynamic]' });
111+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'build component tree' });
112+
// Server component spans: the name is the low-cardinality `code.function.name`, and the
113+
// segment each one resolved for is on the description.
114+
expect(spans).toContainEqual({
115+
name: 'Layout',
116+
op: 'function',
117+
description: 'resolve root layout server component',
118+
codeFunctionName: 'Layout',
119+
});
120+
expect(spans).toContainEqual({
121+
name: 'Layout',
122+
op: 'function',
123+
description: 'resolve layout server component "(nested-layout)"',
124+
codeFunctionName: 'Layout',
125+
});
126+
expect(spans).toContainEqual({
127+
name: 'Layout',
128+
op: 'function',
129+
description: 'resolve layout server component "nested-layout"',
130+
codeFunctionName: 'Layout',
131+
});
132+
expect(spans).toContainEqual({
133+
name: 'Layout',
134+
op: 'function',
135+
description: 'resolve layout server component "[dynamic]"',
136+
codeFunctionName: 'Layout',
137+
});
138+
expect(spans).toContainEqual({
139+
name: 'Page',
140+
op: 'function',
141+
description: 'resolve page server component "/nested-layout/[dynamic]"',
142+
codeFunctionName: 'Page',
143+
});
144+
expect(spans).toContainEqual({
145+
...nextjsSpan,
146+
name: 'generateMetadata /(nested-layout)/nested-layout/[dynamic]/page',
147+
});
148+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'start response' });
81149
});

dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { expect, test } from '@playwright/test';
2-
import { collectSpanNamesUntilSegment, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
3+
4+
// Next.js emits these spans itself. The SDK attaches no op, description or function name to
5+
// them, so asserting `undefined` pins that they stay untouched.
6+
const nextjsSpan = { op: undefined, description: undefined, codeFunctionName: undefined };
37

48
// TODO: Server component tests need SDK adjustments for Cloudflare Workers
59
test.skip('Sends a span for a request to app router with URL', async ({ page }) => {
@@ -45,40 +49,104 @@ test.skip('Sends a span for a request to app router with URL', async ({ page })
4549
test.skip('Will create spans for every server component and metadata generation functions when visiting a page', async ({
4650
page,
4751
}) => {
48-
const spanNamesPromise = collectSpanNamesUntilSegment('nextjs-16-cf-workers', 'GET /nested-layout');
52+
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /nested-layout');
4953

5054
await page.goto('/nested-layout');
5155

52-
const spanNames = await spanNamesPromise;
56+
const fullSpans = await spansPromise;
57+
const spans = fullSpans.map(span => ({
58+
name: span.name,
59+
op: getSpanOp(span),
60+
description: span.attributes['sentry.description']?.value,
61+
codeFunctionName: span.attributes['code.function.name']?.value,
62+
}));
5363

54-
expect(spanNames).toContainEqual('render route (app) /nested-layout');
55-
expect(spanNames).toContainEqual('build component tree');
56-
expect(spanNames).toContainEqual('resolve root layout server component');
57-
expect(spanNames).toContainEqual('resolve layout server component "(nested-layout)"');
58-
expect(spanNames).toContainEqual('resolve layout server component "nested-layout"');
59-
expect(spanNames).toContainEqual('resolve page server component "/nested-layout"');
60-
expect(spanNames).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page');
61-
expect(spanNames).toContainEqual('start response');
64+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'render route (app) /nested-layout' });
65+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'build component tree' });
66+
// Server component spans: the name is the low-cardinality `code.function.name`, and the
67+
// segment each one resolved for is on the description.
68+
expect(spans).toContainEqual({
69+
name: 'Layout',
70+
op: 'function',
71+
description: 'resolve root layout server component',
72+
codeFunctionName: 'Layout',
73+
});
74+
expect(spans).toContainEqual({
75+
name: 'Layout',
76+
op: 'function',
77+
description: 'resolve layout server component "(nested-layout)"',
78+
codeFunctionName: 'Layout',
79+
});
80+
expect(spans).toContainEqual({
81+
name: 'Layout',
82+
op: 'function',
83+
description: 'resolve layout server component "nested-layout"',
84+
codeFunctionName: 'Layout',
85+
});
86+
expect(spans).toContainEqual({
87+
name: 'Page',
88+
op: 'function',
89+
description: 'resolve page server component "/nested-layout"',
90+
codeFunctionName: 'Page',
91+
});
92+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'generateMetadata /(nested-layout)/nested-layout/page' });
93+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'start response' });
6294
});
6395

6496
// TODO: Server component span tests need SDK adjustments for Cloudflare Workers
6597
test.skip('Will create spans for every server component and metadata generation functions when visiting a dynamic page', async ({
6698
page,
6799
}) => {
68-
const spanNamesPromise = collectSpanNamesUntilSegment('nextjs-16-cf-workers', 'GET /nested-layout/[dynamic]');
100+
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /nested-layout/[dynamic]');
69101

70102
await page.goto('/nested-layout/123');
71103

72-
const spanNames = await spanNamesPromise;
104+
const fullSpans = await spansPromise;
105+
const spans = fullSpans.map(span => ({
106+
name: span.name,
107+
op: getSpanOp(span),
108+
description: span.attributes['sentry.description']?.value,
109+
codeFunctionName: span.attributes['code.function.name']?.value,
110+
}));
73111

74-
expect(spanNames).toContainEqual('resolve page components');
75-
expect(spanNames).toContainEqual('render route (app) /nested-layout/[dynamic]');
76-
expect(spanNames).toContainEqual('build component tree');
77-
expect(spanNames).toContainEqual('resolve root layout server component');
78-
expect(spanNames).toContainEqual('resolve layout server component "(nested-layout)"');
79-
expect(spanNames).toContainEqual('resolve layout server component "nested-layout"');
80-
expect(spanNames).toContainEqual('resolve layout server component "[dynamic]"');
81-
expect(spanNames).toContainEqual('resolve page server component "/nested-layout/[dynamic]"');
82-
expect(spanNames).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page');
83-
expect(spanNames).toContainEqual('start response');
112+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'resolve page components' });
113+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'render route (app) /nested-layout/[dynamic]' });
114+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'build component tree' });
115+
// Server component spans: the name is the low-cardinality `code.function.name`, and the
116+
// segment each one resolved for is on the description.
117+
expect(spans).toContainEqual({
118+
name: 'Layout',
119+
op: 'function',
120+
description: 'resolve root layout server component',
121+
codeFunctionName: 'Layout',
122+
});
123+
expect(spans).toContainEqual({
124+
name: 'Layout',
125+
op: 'function',
126+
description: 'resolve layout server component "(nested-layout)"',
127+
codeFunctionName: 'Layout',
128+
});
129+
expect(spans).toContainEqual({
130+
name: 'Layout',
131+
op: 'function',
132+
description: 'resolve layout server component "nested-layout"',
133+
codeFunctionName: 'Layout',
134+
});
135+
expect(spans).toContainEqual({
136+
name: 'Layout',
137+
op: 'function',
138+
description: 'resolve layout server component "[dynamic]"',
139+
codeFunctionName: 'Layout',
140+
});
141+
expect(spans).toContainEqual({
142+
name: 'Page',
143+
op: 'function',
144+
description: 'resolve page server component "/nested-layout/[dynamic]"',
145+
codeFunctionName: 'Page',
146+
});
147+
expect(spans).toContainEqual({
148+
...nextjsSpan,
149+
name: 'generateMetadata /(nested-layout)/nested-layout/[dynamic]/page',
150+
});
151+
expect(spans).toContainEqual({ ...nextjsSpan, name: 'start response' });
84152
});

0 commit comments

Comments
 (0)