Skip to content

Commit 31a2a31

Browse files
chargomeclaude
andauthored
test(e2e): Port nextjs-orpc to span streaming (#23870)
Ports `nextjs-orpc` to span streaming: removes the `traceLifecycle: 'static'` pins and rewrites the tracing specs onto streamed spans, using `collectStreamedSpans` for the `ORPC Middleware` child assertions. Ref #23802 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent cf0c4ca commit 31a2a31

4 files changed

Lines changed: 89 additions & 87 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nextjs-orpc/sentry.edge.config.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import * as Sentry from '@sentry/nextjs';
77

88
Sentry.init({
9-
traceLifecycle: 'static',
109
environment: 'qa', // dynamic sampling bias to keep transactions
1110
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
1211
tunnel: `http://localhost:3031/`, // proxy server

‎dev-packages/e2e-tests/test-applications/nextjs-orpc/sentry.server.config.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as Sentry from '@sentry/nextjs';
22

33
Sentry.init({
4-
traceLifecycle: 'static',
54
environment: 'qa', // dynamic sampling bias to keep transactions
65
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
76
tunnel: `http://localhost:3031/`, // proxy server

‎dev-packages/e2e-tests/test-applications/nextjs-orpc/src/instrumentation-client.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as Sentry from '@sentry/nextjs';
22

33
Sentry.init({
4-
traceLifecycle: 'static',
54
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
65
tunnel: `http://localhost:3031/`, // proxy server
76
tracesSampleRate: 1,
Lines changed: 89 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,116 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
33

4-
test('should trace orpc server component', async ({ page }) => {
5-
const pageloadPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
6-
return transactionEvent.transaction === '/';
7-
});
4+
const ORPC_SEGMENT_NAME = 'POST /rpc/[[...rest]]';
85

9-
const orpcTxPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
10-
return transactionEvent.transaction === 'POST /rpc/[[...rest]]';
6+
test('should trace orpc server component', async ({ page }) => {
7+
// The server component calls `planet.list` over HTTP while rendering, so the RPC span belongs to
8+
// the same trace as the pageload. `collectStreamedSpans` evaluates one trace at a time, so requiring
9+
// the pageload and the RPC spans together is what asserts the SSR request propagated its trace.
10+
const spansPromise = collectStreamedSpans('nextjs-orpc', spans => {
11+
return (
12+
spans.some(span => span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment) &&
13+
spans.some(span => span.name === ORPC_SEGMENT_NAME && span.is_segment) &&
14+
spans.some(span => span.name === 'ORPC Middleware')
15+
);
1116
});
1217

1318
await page.goto('/');
14-
const pageloadTx = await pageloadPromise;
15-
const orpcTx = await orpcTxPromise;
19+
const orpcSpans = await spansPromise;
1620

17-
expect(pageloadTx.contexts?.trace).toMatchObject({
18-
parent_span_id: expect.any(String),
19-
span_id: expect.any(String),
20-
trace_id: expect.any(String),
21-
data: {
22-
'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
23-
'sentry.op': 'pageload',
24-
'sentry.segment.name.source': 'route',
25-
},
26-
op: 'pageload',
27-
origin: 'auto.pageload.nextjs.app_router_instrumentation',
21+
const pageloadSpan = orpcSpans.find(span => span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment)!;
22+
const orpcSpan = orpcSpans.find(span => span.name === ORPC_SEGMENT_NAME && span.is_segment)!;
23+
24+
expect(pageloadSpan.parent_span_id).toEqual(expect.any(String));
25+
expect(pageloadSpan.span_id).toEqual(expect.any(String));
26+
expect(pageloadSpan.trace_id).toEqual(expect.any(String));
27+
expect(pageloadSpan.attributes).toMatchObject({
28+
'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
29+
'sentry.op': { value: 'pageload', type: 'string' },
30+
'sentry.segment.name.source': { value: 'route', type: 'string' },
2831
});
2932

30-
expect(orpcTx.contexts?.trace).toMatchObject({
31-
parent_span_id: expect.any(String),
32-
span_id: expect.any(String),
33-
trace_id: pageloadTx.contexts?.trace?.trace_id,
34-
data: {
35-
'sentry.op': 'http.server',
36-
'sentry.origin': 'auto',
37-
'sentry.segment.name.source': 'route',
38-
'sentry.kind': 'server',
39-
'http.response.status_code': 200,
40-
'next.span_name': 'POST /rpc/[[...rest]]/route',
41-
'next.span_type': 'BaseServer.handleRequest',
42-
'http.method': 'POST',
43-
'http.target': '/rpc/planet/list',
44-
'next.rsc': false,
45-
'http.route': '/rpc/[[...rest]]',
46-
'next.route': '/rpc/[[...rest]]',
47-
'http.status_code': 200,
48-
},
49-
op: 'http.server',
50-
origin: 'auto',
33+
// `orpcSpan` comes from the same trace as the pageload, so its presence is the trace assertion.
34+
expect(orpcSpan.parent_span_id).toEqual(expect.any(String));
35+
expect(orpcSpan.span_id).toEqual(expect.any(String));
36+
expect(orpcSpan.attributes).toMatchObject({
37+
'sentry.op': { value: 'http.server', type: 'string' },
38+
'sentry.origin': { value: 'auto', type: 'string' },
39+
'sentry.segment.name.source': { value: 'route', type: 'string' },
40+
'sentry.kind': { value: 'server', type: 'string' },
41+
'http.response.status_code': { value: 200, type: 'integer' },
42+
'next.span_name': { value: 'POST /rpc/[[...rest]]/route', type: 'string' },
43+
'next.span_type': { value: 'BaseServer.handleRequest', type: 'string' },
44+
'http.method': { value: 'POST', type: 'string' },
45+
'http.target': { value: '/rpc/planet/list', type: 'string' },
46+
'next.rsc': { value: false, type: 'boolean' },
47+
'http.route': { value: '/rpc/[[...rest]]', type: 'string' },
48+
'next.route': { value: '/rpc/[[...rest]]', type: 'string' },
49+
'http.status_code': { value: 200, type: 'integer' },
5150
});
5251

53-
expect(orpcTx.spans?.map(span => span.description)).toContain('ORPC Middleware');
52+
expect(orpcSpans.map(span => span.name)).toContain('ORPC Middleware');
5453
});
5554

5655
test('should trace orpc client component', async ({ page }) => {
57-
const navigationPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
58-
return transactionEvent.transaction === '/client';
59-
});
60-
61-
const orpcTxPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
56+
// Awaiting the navigation and RPC spans separately could pair spans from different traces. One
57+
// `collectStreamedSpans` evaluates a single trace at a time, so requiring both together keeps them
58+
// on the same trace and makes the `trace_id` assertion below meaningful.
59+
const spansPromise = collectStreamedSpans('nextjs-orpc', spans => {
6260
return (
63-
transactionEvent.transaction === 'POST /rpc/[[...rest]]' &&
64-
transactionEvent.contexts?.trace?.data?.['http.target'] === '/rpc/planet/find'
61+
spans.some(span => span.name === '/client' && getSpanOp(span) === 'navigation' && span.is_segment) &&
62+
spans.some(
63+
span =>
64+
span.name === ORPC_SEGMENT_NAME &&
65+
span.is_segment &&
66+
span.attributes['http.target']?.value === '/rpc/planet/find',
67+
) &&
68+
spans.some(span => span.name === 'ORPC Middleware')
6569
);
6670
});
6771

6872
await page.goto('/');
6973
await page.waitForTimeout(500);
7074
await page.getByRole('link', { name: 'Client' }).click();
71-
const navigationTx = await navigationPromise;
72-
const orpcTx = await orpcTxPromise;
7375

74-
expect(navigationTx.contexts?.trace).toMatchObject({
75-
span_id: expect.any(String),
76-
trace_id: expect.any(String),
77-
data: {
78-
'sentry.op': 'navigation',
79-
'sentry.origin': 'auto.navigation.nextjs.app_router_instrumentation',
80-
'sentry.segment.name.source': 'route',
81-
'sentry.previous_trace': expect.any(String),
82-
},
83-
op: 'navigation',
84-
origin: 'auto.navigation.nextjs.app_router_instrumentation',
76+
const orpcSpans = await spansPromise;
77+
const navigationSpan = orpcSpans.find(
78+
span => span.name === '/client' && getSpanOp(span) === 'navigation' && span.is_segment,
79+
)!;
80+
const orpcSpan = orpcSpans.find(
81+
span =>
82+
span.name === ORPC_SEGMENT_NAME &&
83+
span.is_segment &&
84+
span.attributes['http.target']?.value === '/rpc/planet/find',
85+
)!;
86+
87+
expect(navigationSpan.span_id).toEqual(expect.any(String));
88+
expect(navigationSpan.trace_id).toEqual(expect.any(String));
89+
expect(navigationSpan.attributes).toMatchObject({
90+
'sentry.op': { value: 'navigation', type: 'string' },
91+
'sentry.origin': { value: 'auto.navigation.nextjs.app_router_instrumentation', type: 'string' },
92+
'sentry.segment.name.source': { value: 'route', type: 'string' },
93+
'sentry.previous_trace': { value: expect.any(String), type: 'string' },
8594
});
8695

87-
expect(orpcTx?.contexts?.trace).toMatchObject({
88-
parent_span_id: expect.any(String),
89-
span_id: expect.any(String),
90-
trace_id: navigationTx?.contexts?.trace?.trace_id,
91-
data: {
92-
'sentry.op': 'http.server',
93-
'sentry.origin': 'auto',
94-
'sentry.segment.name.source': 'route',
95-
'sentry.kind': 'server',
96-
'http.response.status_code': 200,
97-
'next.span_name': 'POST /rpc/[[...rest]]/route',
98-
'next.span_type': 'BaseServer.handleRequest',
99-
'http.method': 'POST',
100-
'http.target': '/rpc/planet/find',
101-
'next.rsc': false,
102-
'http.route': '/rpc/[[...rest]]',
103-
'next.route': '/rpc/[[...rest]]',
104-
'http.status_code': 200,
105-
},
106-
op: 'http.server',
107-
origin: 'auto',
96+
expect(orpcSpan.parent_span_id).toEqual(expect.any(String));
97+
expect(orpcSpan.span_id).toEqual(expect.any(String));
98+
expect(orpcSpan.trace_id).toBe(navigationSpan.trace_id);
99+
expect(orpcSpan.attributes).toMatchObject({
100+
'sentry.op': { value: 'http.server', type: 'string' },
101+
'sentry.origin': { value: 'auto', type: 'string' },
102+
'sentry.segment.name.source': { value: 'route', type: 'string' },
103+
'sentry.kind': { value: 'server', type: 'string' },
104+
'http.response.status_code': { value: 200, type: 'integer' },
105+
'next.span_name': { value: 'POST /rpc/[[...rest]]/route', type: 'string' },
106+
'next.span_type': { value: 'BaseServer.handleRequest', type: 'string' },
107+
'http.method': { value: 'POST', type: 'string' },
108+
'http.target': { value: '/rpc/planet/find', type: 'string' },
109+
'next.rsc': { value: false, type: 'boolean' },
110+
'http.route': { value: '/rpc/[[...rest]]', type: 'string' },
111+
'next.route': { value: '/rpc/[[...rest]]', type: 'string' },
112+
'http.status_code': { value: 200, type: 'integer' },
108113
});
109114

110-
expect(orpcTx.spans?.map(span => span.description)).toContain('ORPC Middleware');
115+
expect(orpcSpans.map(span => span.name)).toContain('ORPC Middleware');
111116
});

0 commit comments

Comments
 (0)