Skip to content

Commit df6fc2c

Browse files
chargomeclaude
andcommitted
test(e2e): Port nextjs-orpc to span streaming
Removes the `traceLifecycle: 'static'` pins and rewrites the tracing specs onto streamed spans. Both tests assert that an `ORPC Middleware` child span sits under the route handler segment. Streamed children arrive in later envelopes than their segment, so these use `collectStreamedSpans` and wait for both the segment and the child before asserting. Ref #23802 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1e5542c commit df6fc2c

4 files changed

Lines changed: 89 additions & 84 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 & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,119 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
4+
const ORPC_SEGMENT_NAME = 'POST /rpc/[[...rest]]';
35

46
test('should trace orpc server component', async ({ page }) => {
5-
const pageloadPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
6-
return transactionEvent.transaction === '/';
7-
});
7+
// The server component calls `planet.list` over HTTP while rendering, so the RPC span belongs to
8+
// the same trace as the pageload. Every span here is matched on that trace: rendering `/` is also
9+
// what the error spec does, and streamed spans are flushed in batches, so an identically shaped
10+
// RPC span from an earlier spec can still arrive inside this window. Waiting for the RPC span on
11+
// *this* pageload's trace is what asserts the SSR request propagated its trace to the RPC call.
12+
const spansPromise = collectStreamedSpans('nextjs-orpc', spans => {
13+
const pageload = spans.find(span => span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment);
814

9-
const orpcTxPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
10-
return transactionEvent.transaction === 'POST /rpc/[[...rest]]';
15+
return (
16+
!!pageload &&
17+
spans.some(span => span.name === ORPC_SEGMENT_NAME && span.is_segment && span.trace_id === pageload.trace_id) &&
18+
spans.some(span => span.name === 'ORPC Middleware' && span.trace_id === pageload.trace_id)
19+
);
1120
});
1221

1322
await page.goto('/');
14-
const pageloadTx = await pageloadPromise;
15-
const orpcTx = await orpcTxPromise;
23+
const spans = await spansPromise;
24+
25+
const pageloadSpan = spans.find(span => span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment)!;
26+
const orpcSpan = spans.find(
27+
span => span.name === ORPC_SEGMENT_NAME && span.is_segment && span.trace_id === pageloadSpan.trace_id,
28+
)!;
29+
const orpcSpans = spans.filter(span => span.trace_id === pageloadSpan.trace_id);
1630

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',
31+
expect(pageloadSpan.parent_span_id).toEqual(expect.any(String));
32+
expect(pageloadSpan.span_id).toEqual(expect.any(String));
33+
expect(pageloadSpan.trace_id).toEqual(expect.any(String));
34+
expect(pageloadSpan.attributes).toMatchObject({
35+
'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
36+
'sentry.op': { value: 'pageload', type: 'string' },
37+
'sentry.segment.name.source': { value: 'route', type: 'string' },
2838
});
2939

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',
40+
// `orpcSpan` was matched on the pageload's trace above, so its presence is the trace assertion.
41+
expect(orpcSpan.parent_span_id).toEqual(expect.any(String));
42+
expect(orpcSpan.span_id).toEqual(expect.any(String));
43+
expect(orpcSpan.attributes).toMatchObject({
44+
'sentry.op': { value: 'http.server', type: 'string' },
45+
'sentry.origin': { value: 'auto', type: 'string' },
46+
'sentry.segment.name.source': { value: 'route', type: 'string' },
47+
'sentry.kind': { value: 'server', type: 'string' },
48+
'http.response.status_code': { value: 200, type: 'integer' },
49+
'next.span_name': { value: 'POST /rpc/[[...rest]]/route', type: 'string' },
50+
'next.span_type': { value: 'BaseServer.handleRequest', type: 'string' },
51+
'http.method': { value: 'POST', type: 'string' },
52+
'http.target': { value: '/rpc/planet/list', type: 'string' },
53+
'next.rsc': { value: false, type: 'boolean' },
54+
'http.route': { value: '/rpc/[[...rest]]', type: 'string' },
55+
'next.route': { value: '/rpc/[[...rest]]', type: 'string' },
56+
'http.status_code': { value: 200, type: 'integer' },
5157
});
5258

53-
expect(orpcTx.spans?.map(span => span.description)).toContain('ORPC Middleware');
59+
expect(orpcSpans.map(span => span.name)).toContain('ORPC Middleware');
5460
});
5561

5662
test('should trace orpc client component', async ({ page }) => {
57-
const navigationPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
58-
return transactionEvent.transaction === '/client';
63+
const navigationSpanPromise = waitForStreamedSpan('nextjs-orpc', span => {
64+
return span.name === '/client' && getSpanOp(span) === 'navigation' && span.is_segment;
5965
});
6066

61-
const orpcTxPromise = waitForTransaction('nextjs-orpc', transactionEvent => {
67+
const orpcSpansPromise = collectStreamedSpans('nextjs-orpc', spans => {
6268
return (
63-
transactionEvent.transaction === 'POST /rpc/[[...rest]]' &&
64-
transactionEvent.contexts?.trace?.data?.['http.target'] === '/rpc/planet/find'
69+
spans.some(
70+
span =>
71+
span.name === ORPC_SEGMENT_NAME &&
72+
span.is_segment &&
73+
span.attributes['http.target']?.value === '/rpc/planet/find',
74+
) && spans.some(span => span.name === 'ORPC Middleware')
6575
);
6676
});
6777

6878
await page.goto('/');
6979
await page.waitForTimeout(500);
7080
await page.getByRole('link', { name: 'Client' }).click();
71-
const navigationTx = await navigationPromise;
72-
const orpcTx = await orpcTxPromise;
81+
const navigationSpan = await navigationSpanPromise;
82+
const orpcSpans = await orpcSpansPromise;
83+
const orpcSpan = orpcSpans.find(
84+
span =>
85+
span.name === ORPC_SEGMENT_NAME &&
86+
span.is_segment &&
87+
span.attributes['http.target']?.value === '/rpc/planet/find',
88+
)!;
7389

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',
90+
expect(navigationSpan.span_id).toEqual(expect.any(String));
91+
expect(navigationSpan.trace_id).toEqual(expect.any(String));
92+
expect(navigationSpan.attributes).toMatchObject({
93+
'sentry.op': { value: 'navigation', type: 'string' },
94+
'sentry.origin': { value: 'auto.navigation.nextjs.app_router_instrumentation', type: 'string' },
95+
'sentry.segment.name.source': { value: 'route', type: 'string' },
96+
'sentry.previous_trace': { value: expect.any(String), type: 'string' },
8597
});
8698

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',
99+
expect(orpcSpan.parent_span_id).toEqual(expect.any(String));
100+
expect(orpcSpan.span_id).toEqual(expect.any(String));
101+
expect(orpcSpan.trace_id).toBe(navigationSpan.trace_id);
102+
expect(orpcSpan.attributes).toMatchObject({
103+
'sentry.op': { value: 'http.server', type: 'string' },
104+
'sentry.origin': { value: 'auto', type: 'string' },
105+
'sentry.segment.name.source': { value: 'route', type: 'string' },
106+
'sentry.kind': { value: 'server', type: 'string' },
107+
'http.response.status_code': { value: 200, type: 'integer' },
108+
'next.span_name': { value: 'POST /rpc/[[...rest]]/route', type: 'string' },
109+
'next.span_type': { value: 'BaseServer.handleRequest', type: 'string' },
110+
'http.method': { value: 'POST', type: 'string' },
111+
'http.target': { value: '/rpc/planet/find', type: 'string' },
112+
'next.rsc': { value: false, type: 'boolean' },
113+
'http.route': { value: '/rpc/[[...rest]]', type: 'string' },
114+
'next.route': { value: '/rpc/[[...rest]]', type: 'string' },
115+
'http.status_code': { value: 200, type: 'integer' },
108116
});
109117

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

0 commit comments

Comments
 (0)