Skip to content

Commit aa366e6

Browse files
chargomeclaude
andcommitted
test(e2e): Port nextjs-16 to span streaming
Removes the `traceLifecycle: 'static'` pins and rewrites the specs onto streamed spans. Tests asserting on children of a segment span (server components, DB instrumentation, middleware fetch, prefetch) use `collectStreamedSpans` and accumulate until the segment, which ends last. The route-handler specs correlate an error or message event with its server span; those match the span on the event's own trace, so batching cannot pair spans across specs. `http.client` span names are low cardinality under streaming, so the middleware fetch span is `GET localhost` rather than `GET http://localhost:3030/` - which also mattered for the matcher, since the old name was used as a predicate and would have hung rather than failed. Dropped, having no span v2 equivalent: - The middleware isolation-scope assertions, including the guard that middleware tags do not leak onto the following route span. Span v2 carries no scope tags, so neither the tag nor its absence is observable. - The middleware `breadcrumbs` assertion. The fetch stays covered by its `http.client` span. - The `contexts.runtime.name` matchers, which only disambiguated uniquely named spans. Request headers carry over as `http.request.header.*` attributes, so the route-handler and pageload header assertions keep their coverage. Ref #23802 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dd5edcd commit aa366e6

18 files changed

Lines changed: 380 additions & 533 deletions

dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts

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

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

dev-packages/e2e-tests/test-applications/nextjs-16/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-16/sentry.server.config.ts

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

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

dev-packages/e2e-tests/test-applications/nextjs-16/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-16', async transactionEvent => {
10-
return transactionEvent.transaction === 'GET /ai-error-test';
9+
const aiSpanPromise = waitForStreamedSpan('nextjs-16', 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-16/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-16', async transactionEvent => {
10-
return transactionEvent.transaction === 'GET /ai-test';
9+
const aiSpanPromise = waitForStreamedSpan('nextjs-16', 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

dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,55 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans } from '@sentry-internal/test-utils';
33

44
test('Instruments DB calls made during server-side rendering of a page', async ({ page }) => {
5-
const transactionEventPromise = waitForTransaction('nextjs-16', transactionEvent => {
6-
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-page';
7-
});
5+
// The db spans are children of the segment span, which ends last.
6+
const spansPromise = collectStreamedSpans('nextjs-16', spans =>
7+
spans.some(span => span.name === 'GET /db-page' && span.is_segment),
8+
);
89

910
await page.goto('/db-page');
1011
await expect(page.locator('#answer')).toHaveText('answer: 42');
1112
await expect(page.locator('#cached')).toHaveText('cached: 42');
1213

13-
const transactionEvent = await transactionEventPromise;
14-
15-
const spans = transactionEvent.spans || [];
14+
const spans = await spansPromise;
1615

1716
// One page render produces spans from both injection paths: pg (externalized → runtime module
1817
// hook) and ioredis (bundle-safe allowlisted → build-time loader).
1918
expect(spans).toContainEqual(
2019
expect.objectContaining({
21-
op: 'db',
22-
origin: 'auto.db.postgres',
23-
description: 'SELECT 40 + 2 AS answer',
20+
name: 'SELECT 40 + 2 AS answer',
2421
status: 'ok',
25-
data: expect.objectContaining({
26-
'db.system.name': 'postgresql',
27-
'db.query.text': 'SELECT 40 + 2 AS answer',
22+
attributes: expect.objectContaining({
23+
'sentry.op': { value: 'db', type: 'string' },
24+
'sentry.origin': { value: 'auto.db.postgres', type: 'string' },
25+
'db.system.name': { value: 'postgresql', type: 'string' },
26+
'db.query.text': { value: 'SELECT 40 + 2 AS answer', type: 'string' },
2827
}),
2928
}),
3029
);
3130
expect(spans).toContainEqual(
3231
expect.objectContaining({
33-
op: 'db.query',
34-
origin: 'auto.db.redis',
35-
description: 'set page-key [1 other arguments]',
32+
name: 'set page-key [1 other arguments]',
3633
status: 'ok',
37-
data: expect.objectContaining({
38-
'db.system.name': 'redis',
39-
'db.operation.name': 'set',
40-
'db.query.text': 'set page-key [1 other arguments]',
34+
attributes: expect.objectContaining({
35+
'sentry.op': { value: 'db.query', type: 'string' },
36+
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
37+
'db.system.name': { value: 'redis', type: 'string' },
38+
'db.operation.name': { value: 'set', type: 'string' },
39+
'db.query.text': { value: 'set page-key [1 other arguments]', type: 'string' },
4140
}),
4241
}),
4342
);
4443
expect(spans).toContainEqual(
4544
expect.objectContaining({
46-
op: 'db.query',
47-
origin: 'auto.db.redis',
48-
description: 'get page-key',
45+
name: 'get page-key',
4946
status: 'ok',
50-
data: expect.objectContaining({
51-
'db.system.name': 'redis',
52-
'db.operation.name': 'get',
53-
'db.query.text': 'get page-key',
47+
attributes: expect.objectContaining({
48+
'sentry.op': { value: 'db.query', type: 'string' },
49+
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
50+
'db.system.name': { value: 'redis', type: 'string' },
51+
'db.operation.name': { value: 'get', type: 'string' },
52+
'db.query.text': { value: 'get page-key', type: 'string' },
5453
}),
5554
}),
5655
);

dev-packages/e2e-tests/test-applications/nextjs-16/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-16', async transactionEvent => {
50-
return !!(
51-
transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload'
52-
);
49+
const spanPromise = waitForStreamedSpan('nextjs-16', 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-16', async transactionEvent => {
76-
return transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload';
73+
const spanPromise = waitForStreamedSpan('nextjs-16', 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)