Skip to content

Commit d39e2d2

Browse files
chargomeclaude
andcommitted
test(e2e): Port nextjs-15 to span streaming
Removes the `traceLifecycle: 'static'` pins and rewrites the specs onto streamed spans. Tests asserting on children of a segment span use `collectStreamedSpans` and accumulate until the segment, which ends last. The RSC error specs correlate an error event with its server span. Those match the span on the error's own trace, so a span from an earlier spec cannot satisfy the correlation once streamed spans are flushed in batches. Request headers carry over as `http.request.header.*` span attributes, and the release as `sentry.release`, so the header-extraction and release-injection specs keep their coverage. The middleware specs asserted `request.url` / `request.method`; those become the `http.target` and `http.request.method` attributes. The `contexts.runtime.name === 'vercel-edge'` matcher was dropped, having no span v2 equivalent and only ever disambiguating a uniquely named span. Ref #23802 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1499f50 commit d39e2d2

18 files changed

Lines changed: 300 additions & 412 deletions

dev-packages/e2e-tests/test-applications/nextjs-15/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
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
76
tunnel: `http://localhost:3031/`, // proxy server

dev-packages/e2e-tests/test-applications/nextjs-15/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
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
76
tunnel: `http://localhost:3031/`, // proxy server

dev-packages/e2e-tests/test-applications/nextjs-15/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-15/tests/ai-error.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { expect, test } from '@playwright/test';
2-
import { getSpanOp, waitForError, waitForStreamedSpans, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForError, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils';
33

44
// FIXME: This app uses `ai@^3`, which the channel-based Vercel AI integration doesn't instrument
55
// (it supports v4-v6 via the orchestrion transform and v7 via the native `ai:telemetry` channel).
66
// With channel-based instrumentation now the default, no gen_ai spans are produced. Re-enable once
77
// the app is upgraded to `ai@v7` (or v3 support is restored).
88
test.fixme('should create AI spans with correct attributes and error linking', async ({ page }) => {
9-
const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
10-
return transactionEvent.transaction === 'GET /ai-error-test';
9+
const aiSpanPromise = waitForStreamedSpan('nextjs-15', span => {
10+
return span.name === 'GET /ai-error-test' && span.is_segment;
1111
});
1212

1313
// gen_ai spans are extracted into a separate span v2 envelope item
@@ -21,12 +21,12 @@ test.fixme('should create AI spans with correct attributes and error linking', a
2121

2222
await page.goto('/ai-error-test');
2323

24-
const aiTransaction = await aiTransactionPromise;
24+
const aiSpan = await aiSpanPromise;
2525
const genAiSpans = await genAiSpansPromise;
2626
const errorEvent = await errorEventPromise;
2727

28-
expect(aiTransaction).toBeDefined();
29-
expect(aiTransaction.transaction).toBe('GET /ai-error-test');
28+
expect(aiSpan).toBeDefined();
29+
expect(aiSpan.name).toBe('GET /ai-error-test');
3030

3131
// Each generateText call should create 2 spans: one for the pipeline and one for doGenerate
3232
// Plus a span for the tool call
@@ -44,5 +44,5 @@ test.fixme('should create AI spans with correct attributes and error linking', a
4444
expect(errorEvent).toBeDefined();
4545

4646
//Verify error is linked to the same trace as the transaction
47-
expect(errorEvent?.contexts?.trace?.trace_id).toBe(aiTransaction.contexts?.trace?.trace_id);
47+
expect(errorEvent?.contexts?.trace?.trace_id).toBe(aiSpan.trace_id);
4848
});

dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { expect, test } from '@playwright/test';
2-
import { getSpanOp, waitForStreamedSpans, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils';
33

44
// FIXME: This app uses `ai@^3`, which the channel-based Vercel AI integration doesn't instrument
55
// (it supports v4-v6 via the orchestrion transform and v7 via the native `ai:telemetry` channel).
66
// With channel-based instrumentation now the default, no gen_ai spans are produced. Re-enable once
77
// the app is upgraded to `ai@v7` (or v3 support is restored).
88
test.fixme('should create AI spans with correct attributes', async ({ page }) => {
9-
const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
10-
return transactionEvent.transaction === 'GET /ai-test';
9+
const aiSpanPromise = waitForStreamedSpan('nextjs-15', span => {
10+
return span.name === 'GET /ai-test' && span.is_segment;
1111
});
1212

1313
// gen_ai spans are extracted into a separate span v2 envelope item
@@ -17,11 +17,11 @@ test.fixme('should create AI spans with correct attributes', async ({ page }) =>
1717

1818
await page.goto('/ai-test');
1919

20-
const aiTransaction = await aiTransactionPromise;
20+
const aiSpan = await aiSpanPromise;
2121
const genAiSpans = await genAiSpansPromise;
2222

23-
expect(aiTransaction).toBeDefined();
24-
expect(aiTransaction.transaction).toBe('GET /ai-test');
23+
expect(aiSpan).toBeDefined();
24+
expect(aiSpan.name).toBe('GET /ai-test');
2525

2626
// We expect spans for the first 3 AI calls (4th is disabled)
2727
// Each generateText call should create 2 spans: one for the pipeline and one for doGenerate
Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33
import { parseSemver } from '@sentry/core';
44

55
const packageJson = require('../package.json');
@@ -12,29 +12,33 @@ test('Should propagate traces from server to client in pages router', async ({ p
1212
'Next.js version does not support clientside instrumentation',
1313
);
1414

15-
const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
16-
return transactionEvent?.transaction === 'GET /[locale]/pages-router-client-trace-propagation';
15+
const serverSpanPromise = waitForStreamedSpan('nextjs-15', span => {
16+
return span.name === 'GET /[locale]/pages-router-client-trace-propagation' && span.is_segment;
1717
});
1818

19-
const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
20-
return transactionEvent?.transaction === '/[locale]/pages-router-client-trace-propagation';
19+
const pageloadSpanPromise = waitForStreamedSpan('nextjs-15', span => {
20+
return (
21+
span.name === '/[locale]/pages-router-client-trace-propagation' &&
22+
getSpanOp(span) === 'pageload' &&
23+
span.is_segment
24+
);
2125
});
2226

2327
await page.goto(`/123/pages-router-client-trace-propagation`);
2428

25-
const serverTransaction = await serverTransactionPromise;
26-
const pageloadTransaction = await pageloadTransactionPromise;
29+
const serverSpan = await serverSpanPromise;
30+
const pageloadSpan = await pageloadSpanPromise;
2731

28-
expect(serverTransaction.contexts?.trace?.trace_id).toBeDefined();
29-
expect(pageloadTransaction.contexts?.trace?.trace_id).toBe(serverTransaction.contexts?.trace?.trace_id);
32+
expect(serverSpan.trace_id).toBeDefined();
33+
expect(pageloadSpan.trace_id).toBe(serverSpan.trace_id);
3034

3135
await test.step('release was successfully injected on the serverside', () => {
3236
// Release as defined in next.config.js
33-
expect(serverTransaction.release).toBe('foobar123');
37+
expect(serverSpan.attributes['sentry.release']?.value).toBe('foobar123');
3438
});
3539

3640
await test.step('release was successfully injected on the clientside', () => {
3741
// Release as defined in next.config.js
38-
expect(pageloadTransaction.release).toBe('foobar123');
42+
expect(pageloadSpan.attributes['sentry.release']?.value).toBe('foobar123');
3943
});
4044
});
Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,21 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33

4-
test('should create consistent parameterized transaction for i18n routes - locale: en', async ({ page }) => {
5-
const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
6-
return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload';
7-
});
8-
9-
await page.goto(`/en/i18n-test`);
10-
11-
const transaction = await transactionPromise;
12-
13-
expect(transaction).toMatchObject({
14-
contexts: {
15-
trace: {
16-
data: {
17-
'sentry.op': 'pageload',
18-
'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
19-
'sentry.segment.name.source': 'route',
20-
},
21-
op: 'pageload',
22-
origin: 'auto.pageload.nextjs.app_router_instrumentation',
23-
},
24-
},
25-
transaction: '/:locale/i18n-test',
26-
transaction_info: { source: 'route' },
27-
type: 'transaction',
28-
});
29-
});
30-
31-
test('should create consistent parameterized transaction for i18n routes - locale: ar', async ({ page }) => {
32-
const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
33-
return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload';
34-
});
4+
for (const locale of ['en', 'ar']) {
5+
test(`should create consistent parameterized span for i18n routes - locale: ${locale}`, async ({ page }) => {
6+
const spanPromise = waitForStreamedSpan('nextjs-15', span => {
7+
return span.name === '/:locale/i18n-test' && getSpanOp(span) === 'pageload' && span.is_segment;
8+
});
359

36-
await page.goto(`/ar/i18n-test`);
10+
await page.goto(`/${locale}/i18n-test`);
3711

38-
const transaction = await transactionPromise;
12+
const span = await spanPromise;
3913

40-
expect(transaction).toMatchObject({
41-
contexts: {
42-
trace: {
43-
data: {
44-
'sentry.op': 'pageload',
45-
'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
46-
'sentry.segment.name.source': 'route',
47-
},
48-
op: 'pageload',
49-
origin: 'auto.pageload.nextjs.app_router_instrumentation',
50-
},
51-
},
52-
transaction: '/:locale/i18n-test',
53-
transaction_info: { source: 'route' },
54-
type: 'transaction',
14+
expect(span.name).toBe('/:locale/i18n-test');
15+
expect(span.attributes).toMatchObject({
16+
'sentry.op': { value: 'pageload', type: 'string' },
17+
'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
18+
'sentry.segment.name.source': { value: 'route', type: 'string' },
19+
});
5520
});
56-
});
21+
}

dev-packages/e2e-tests/test-applications/nextjs-15/tests/isr-routes.test.ts

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33

44
test('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => {
55
// Navigate to ISR page
@@ -41,15 +41,13 @@ test('should remove meta tags for different ISR dynamic route values', async ({
4141
await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
4242
});
4343

44-
test('should create unique transactions for ISR pages on each visit', async ({ page }) => {
44+
test('should create unique traces for ISR pages on each visit', async ({ page }) => {
4545
const traceIds: string[] = [];
4646

4747
// Load the same ISR page 5 times to ensure cached HTML meta tags are consistently removed
4848
for (let i = 0; i < 5; i++) {
49-
const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
50-
return !!(
51-
transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload'
52-
);
49+
const spanPromise = waitForStreamedSpan('nextjs-15', span => {
50+
return span.name === '/isr-test/:product' && getSpanOp(span) === 'pageload' && span.is_segment;
5351
});
5452

5553
if (i === 0) {
@@ -58,8 +56,8 @@ test('should create unique transactions for ISR pages on each visit', async ({ p
5856
await page.reload();
5957
}
6058

61-
const transaction = await transactionPromise;
62-
const traceId = transaction.contexts?.trace?.trace_id;
59+
const span = await spanPromise;
60+
const traceId = span.trace_id;
6361

6462
expect(traceId).toBeDefined();
6563
expect(traceId).toMatch(/[a-f0-9]{32}/);
@@ -72,23 +70,14 @@ test('should create unique transactions for ISR pages on each visit', async ({ p
7270
});
7371

7472
test('ISR route should be identified correctly in the route manifest', async ({ page }) => {
75-
const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
76-
return transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload';
73+
const spanPromise = waitForStreamedSpan('nextjs-15', span => {
74+
return span.name === '/isr-test/:product' && getSpanOp(span) === 'pageload' && span.is_segment;
7775
});
7876

7977
await page.goto('/isr-test/laptop');
80-
const transaction = await transactionPromise;
81-
82-
// Verify the transaction is properly parameterized
83-
expect(transaction).toMatchObject({
84-
transaction: '/isr-test/:product',
85-
transaction_info: { source: 'route' },
86-
contexts: {
87-
trace: {
88-
data: {
89-
'sentry.segment.name.source': 'route',
90-
},
91-
},
92-
},
93-
});
78+
const span = await spanPromise;
79+
80+
// Verify the span is properly parameterized
81+
expect(span.name).toBe('/isr-test/:product');
82+
expect(span.attributes['sentry.segment.name.source']?.value).toBe('route');
9483
});

0 commit comments

Comments
 (0)