Skip to content

Commit 5711976

Browse files
chargomeclaude
andcommitted
test(e2e): Port nextjs-16-tunnel to span streaming
Removes the `traceLifecycle: 'static'` pins and rewrites the specs onto streamed spans. The span-filtering test collected transactions by racing `waitForTransaction` against a 500ms timeout in a 3 second loop. Streamed children arrive across several envelopes, so it now accumulates spans in a `waitForStreamedSpans` callback that never resolves, matching the accumulator idiom already used elsewhere in the Next.js suites. That drops the polling loop entirely. Ref #23802 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 22d729f commit 5711976

4 files changed

Lines changed: 62 additions & 75 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/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
environment: 'qa', // dynamic sampling bias to keep transactions
65
// Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing
76
dsn: 'https://public@o12345.ingest.us.sentry.io/67890',

‎dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/sentry.edge.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
// Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing
76
dsn: 'https://public@o12345.ingest.us.sentry.io/67890',

‎dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/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
// Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing
76
dsn: 'https://public@o12345.ingest.us.sentry.io/67890',
Lines changed: 62 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,81 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
3-
4-
test('Tunnel route should proxy pageload transaction to Sentry', async ({ page }) => {
5-
// Wait for the pageload transaction to be sent through the tunnel
6-
const pageloadTransactionPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => {
7-
return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
2+
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils';
3+
import type { SerializedStreamedSpan } from '@sentry/core';
4+
5+
function getStringAttribute(span: SerializedStreamedSpan, key: string): string | undefined {
6+
const attribute = span.attributes[key];
7+
return attribute?.type === 'string' ? attribute.value : undefined;
8+
}
9+
10+
test('Tunnel route should proxy pageload span to Sentry', async ({ page }) => {
11+
// Wait for the pageload span to be sent through the tunnel
12+
const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-tunnel', span => {
13+
return getSpanOp(span) === 'pageload' && span.name === '/' && span.is_segment;
814
});
915

1016
// Navigate to the page
1117
await page.goto('/');
1218

13-
const pageloadTransaction = await pageloadTransactionPromise;
19+
const pageloadSpan = await pageloadSpanPromise;
1420

15-
// Verify the pageload transaction was received successfully
16-
expect(pageloadTransaction).toBeDefined();
17-
expect(pageloadTransaction.transaction).toBe('/');
18-
expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload');
19-
expect(pageloadTransaction.contexts?.trace?.status).toBe('ok');
20-
expect(pageloadTransaction.type).toBe('transaction');
21+
// Verify the pageload span was received successfully
22+
expect(pageloadSpan).toBeDefined();
23+
expect(pageloadSpan.name).toBe('/');
24+
expect(getSpanOp(pageloadSpan)).toBe('pageload');
25+
expect(pageloadSpan.status).toBe('ok');
2126
});
2227

23-
test('Tunnel route should send multiple pageload transactions consistently', async ({ page }) => {
28+
test('Tunnel route should send multiple pageload spans consistently', async ({ page }) => {
2429
// This test verifies that the tunnel route remains consistent across multiple page loads
2530
// (important for Turbopack which could generate different tunnel routes for client/server)
2631

2732
// First pageload
28-
const firstPageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => {
29-
return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
33+
const firstPageloadPromise = waitForStreamedSpan('nextjs-16-tunnel', span => {
34+
return getSpanOp(span) === 'pageload' && span.name === '/' && span.is_segment;
3035
});
3136

3237
await page.goto('/');
3338
const firstPageload = await firstPageloadPromise;
3439

3540
expect(firstPageload).toBeDefined();
36-
expect(firstPageload.transaction).toBe('/');
37-
expect(firstPageload.contexts?.trace?.op).toBe('pageload');
38-
expect(firstPageload.contexts?.trace?.status).toBe('ok');
41+
expect(firstPageload.name).toBe('/');
42+
expect(getSpanOp(firstPageload)).toBe('pageload');
43+
expect(firstPageload.status).toBe('ok');
3944

4045
// Second pageload (reload)
41-
const secondPageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => {
42-
return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
46+
const secondPageloadPromise = waitForStreamedSpan('nextjs-16-tunnel', span => {
47+
return getSpanOp(span) === 'pageload' && span.name === '/' && span.is_segment;
4348
});
4449

4550
await page.reload();
4651
const secondPageload = await secondPageloadPromise;
4752

4853
expect(secondPageload).toBeDefined();
49-
expect(secondPageload.transaction).toBe('/');
50-
expect(secondPageload.contexts?.trace?.op).toBe('pageload');
51-
expect(secondPageload.contexts?.trace?.status).toBe('ok');
54+
expect(secondPageload.name).toBe('/');
55+
expect(getSpanOp(secondPageload)).toBe('pageload');
56+
expect(secondPageload.status).toBe('ok');
5257
});
5358

5459
test('Tunnel requests should not create middleware or fetch spans', async ({ page }) => {
5560
// This test verifies that our span filtering logic works correctly
56-
// The proxy runs on all routes, so we'll get a middleware transaction for `/`
57-
// But we should NOT get middleware or fetch transactions for the tunnel route itself
58-
59-
const allTransactions: any[] = [];
60-
61-
// Collect all transactions
62-
const collectPromise = (async () => {
63-
// Keep collecting for 3 seconds after pageload
64-
const endTime = Date.now() + 3000;
65-
while (Date.now() < endTime) {
66-
try {
67-
const tx = await Promise.race([
68-
waitForTransaction('nextjs-16-tunnel', () => true),
69-
new Promise((_, reject) => setTimeout(() => reject(), 500)),
70-
]);
71-
allTransactions.push(tx);
72-
} catch {
73-
// Timeout, continue collecting
74-
}
75-
}
76-
})();
77-
78-
// Wait for pageload transaction
79-
const pageloadPromise = waitForTransaction('nextjs-16-tunnel', async transactionEvent => {
80-
return transactionEvent?.contexts?.trace?.op === 'pageload';
61+
// The proxy runs on all routes, so we'll get a middleware span for `/`
62+
// But we should NOT get middleware or fetch spans for the tunnel route itself
63+
64+
// Accumulate every streamed span for the duration of the test. The callback never returns true,
65+
// so this promise is deliberately left unsettled - the assertions below read the array instead.
66+
const allSpans: SerializedStreamedSpan[] = [];
67+
void waitForStreamedSpans('nextjs-16-tunnel', spans => {
68+
allSpans.push(...spans);
69+
return false;
70+
});
71+
72+
// Wait for pageload span
73+
const pageloadPromise = waitForStreamedSpan('nextjs-16-tunnel', span => {
74+
return getSpanOp(span) === 'pageload' && span.is_segment;
8175
});
8276

8377
await page.goto('/');
84-
const pageloadTransaction = await pageloadPromise;
78+
const pageloadSpan = await pageloadPromise;
8579

8680
// Trigger errors to force tunnel POST requests
8781
await page
@@ -100,33 +94,29 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag
10094
// Expected to throw
10195
});
10296

103-
// Wait for events to be sent through tunnel
104-
await page.waitForTimeout(2000);
97+
// Wait for events to be sent through tunnel, and for the spans they would wrongly produce to arrive
98+
await page.waitForTimeout(3000);
10599

106-
// Continue collecting for a bit
107-
await collectPromise;
100+
// We should have received the pageload span
101+
expect(pageloadSpan).toBeDefined();
102+
expect(getSpanOp(pageloadSpan)).toBe('pageload');
108103

109-
// We should have received the pageload transaction
110-
expect(pageloadTransaction).toBeDefined();
111-
expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload');
104+
const middlewareSpans = allSpans.filter(span => getSpanOp(span) === 'middleware');
112105

113-
const middlewareTransactions = allTransactions.filter(tx => tx.contexts?.trace?.op === 'middleware');
114-
115-
// We WILL have a middleware transaction for GET / (the pageload)
116-
// But we should NOT have middleware transactions for POST requests (tunnel route)
117-
const postMiddlewareTransactions = middlewareTransactions.filter(
118-
tx => tx.transaction?.includes('POST') || tx.contexts?.trace?.data?.['http.request.method'] === 'POST',
106+
// We WILL have a middleware span for GET / (the pageload)
107+
// But we should NOT have middleware spans for POST requests (tunnel route)
108+
const postMiddlewareSpans = middlewareSpans.filter(
109+
span => span.name.includes('POST') || getStringAttribute(span, 'http.request.method') === 'POST',
119110
);
120111

121-
expect(postMiddlewareTransactions).toHaveLength(0);
112+
expect(postMiddlewareSpans).toHaveLength(0);
122113

123-
// We should NOT have any fetch transactions to Sentry ingest
124-
const sentryFetchTransactions = allTransactions.filter(
125-
tx =>
126-
tx.contexts?.trace?.op === 'http.client' &&
127-
(tx.contexts?.trace?.data?.['url.full']?.includes('sentry.io') ||
128-
tx.contexts?.trace?.data?.['url.full']?.includes('ingest')),
129-
);
114+
// We should NOT have any fetch spans to Sentry ingest. Matched on the host attribute rather than a
115+
// substring of the full URL, which would also match an arbitrary host with `sentry.io` elsewhere in it.
116+
const sentryFetchSpans = allSpans.filter(span => {
117+
const host = getStringAttribute(span, 'server.address') ?? '';
118+
return getSpanOp(span) === 'http.client' && (host === 'sentry.io' || host.endsWith('.sentry.io'));
119+
});
130120

131-
expect(sentryFetchTransactions).toHaveLength(0);
121+
expect(sentryFetchSpans).toHaveLength(0);
132122
});

0 commit comments

Comments
 (0)