Skip to content

Commit eaaefe1

Browse files
chargomeclaude
andcommitted
test(e2e): Port nextjs-14 to span streaming
Removes the `traceLifecycle: 'static'` pins and rewrites the specs onto streamed spans. Tests asserting on children of a segment span (generation functions, request instrumentation, trace propagation) use `collectStreamedSpans` and accumulate until the segment span, which ends last. The propagation specs match the inbound span, outbound span and the `http.client` span between them within one trace, since all three share it. `http.client` span names are low cardinality under streaming, so the request-instrumentation spans are now named `GET github.com` rather than `GET https://github.com/`. The transaction-side `tags` assertions were dropped, having no span v2 equivalent; the error-side assertions still cover isolation scope. Ref #23802 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 82a2df0 commit eaaefe1

6 files changed

Lines changed: 203 additions & 260 deletions

File tree

dev-packages/e2e-tests/test-applications/nextjs-14/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-14/instrumentation.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as Sentry from '@sentry/nextjs';
33
export function register() {
44
if (process.env.NEXT_RUNTIME === 'nodejs' || process.env.NEXT_RUNTIME === 'edge') {
55
Sentry.init({
6-
traceLifecycle: 'static',
76
environment: 'qa', // dynamic sampling bias to keep transactions
87
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
98
tunnel: `http://localhost:3031/`, // proxy server

dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,48 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
4+
// The generation-function spans are children of the segment span, which ends last, so accumulate
5+
// spans until the segment for this request arrives.
6+
function collectSpansForTarget(httpTarget: string) {
7+
return collectStreamedSpans('nextjs-14', spans =>
8+
spans.some(span => span.is_segment && span.attributes['http.target']?.value === httpTarget),
9+
);
10+
}
311

412
test('Should emit a span for a generateMetadata() function invocation', async ({ page }) => {
513
const testTitle = 'should-emit-span';
14+
const httpTarget = `/generation-functions?metadataTitle=${testTitle}`;
615

7-
const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
8-
return (
9-
transactionEvent.contexts?.trace?.data?.['http.target'] === `/generation-functions?metadataTitle=${testTitle}`
10-
);
11-
});
16+
const spansPromise = collectSpansForTarget(httpTarget);
1217

13-
await page.goto(`/generation-functions?metadataTitle=${testTitle}`);
18+
await page.goto(httpTarget);
1419

15-
const transaction = await transactionPromise;
20+
const spans = await spansPromise;
21+
const segmentSpan = spans.find(span => span.is_segment && span.attributes['http.target']?.value === httpTarget)!;
1622

17-
expect(transaction.spans).toContainEqual(
23+
expect(spans).toContainEqual(
1824
expect.objectContaining({
19-
description: 'generateMetadata /generation-functions/page',
20-
origin: 'auto',
25+
name: 'generateMetadata /generation-functions/page',
26+
status: 'ok',
27+
trace_id: segmentSpan.trace_id,
2128
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
2229
span_id: expect.stringMatching(/[a-f0-9]{16}/),
23-
status: 'ok',
24-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
30+
attributes: expect.objectContaining({
31+
'sentry.origin': { value: 'auto', type: 'string' },
32+
}),
2533
}),
2634
);
2735

2836
const pageTitle = await page.title();
2937
expect(pageTitle).toBe(testTitle);
3038
});
3139

32-
test('Should send a transaction and an error event for a faulty generateMetadata() function invocation', async ({
33-
page,
34-
}) => {
40+
test('Should send a span and an error event for a faulty generateMetadata() function invocation', async ({ page }) => {
3541
const testTitle = 'should-emit-error';
42+
const httpTarget = `/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1`;
3643

37-
const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
38-
return (
39-
transactionEvent.contexts?.trace?.data?.['http.target'] ===
40-
`/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1`
41-
);
44+
const spanPromise = waitForStreamedSpan('nextjs-14', span => {
45+
return span.is_segment && span.attributes['http.target']?.value === httpTarget;
4246
});
4347

4448
const errorEventPromise = waitForError('nextjs-14', errorEvent => {
@@ -48,61 +52,57 @@ test('Should send a transaction and an error event for a faulty generateMetadata
4852
);
4953
});
5054

51-
await page.goto(`/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1`);
55+
await page.goto(httpTarget);
5256

5357
const errorEvent = await errorEventPromise;
54-
const transactionEvent = await transactionPromise;
58+
expect(await spanPromise).toBeDefined();
5559

56-
// Assert that isolation scope works properly
60+
// Assert that isolation scope works properly. Span v2 carries no scope tags, so this is only
61+
// asserted on the error event; the span-side assertions were dropped in the streaming port.
5762
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
5863
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
59-
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
60-
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
6164
});
6265

63-
test('Should send a transaction event for a generateViewport() function invocation', async ({ page }) => {
66+
test('Should send a span for a generateViewport() function invocation', async ({ page }) => {
6467
const testTitle = 'floob';
68+
const httpTarget = `/generation-functions?viewportThemeColor=${testTitle}`;
6569

66-
const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
67-
return (
68-
transactionEvent.contexts?.trace?.data?.['http.target'] ===
69-
`/generation-functions?viewportThemeColor=${testTitle}`
70-
);
71-
});
70+
const spansPromise = collectSpansForTarget(httpTarget);
71+
72+
await page.goto(httpTarget);
7273

73-
await page.goto(`/generation-functions?viewportThemeColor=${testTitle}`);
74+
const spans = await spansPromise;
75+
const segmentSpan = spans.find(span => span.is_segment && span.attributes['http.target']?.value === httpTarget)!;
7476

75-
expect((await transactionPromise).spans).toContainEqual(
77+
expect(spans).toContainEqual(
7678
expect.objectContaining({
77-
description: 'generateViewport /generation-functions/page',
78-
origin: 'auto',
79+
name: 'generateViewport /generation-functions/page',
80+
status: 'ok',
81+
trace_id: segmentSpan.trace_id,
7982
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
8083
span_id: expect.stringMatching(/[a-f0-9]{16}/),
81-
status: 'ok',
82-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
84+
attributes: expect.objectContaining({
85+
'sentry.origin': { value: 'auto', type: 'string' },
86+
}),
8387
}),
8488
);
8589
});
8690

87-
test('Should send a transaction and an error event for a faulty generateViewport() function invocation', async ({
88-
page,
89-
}) => {
91+
test('Should send a span and an error event for a faulty generateViewport() function invocation', async ({ page }) => {
9092
const testTitle = 'blargh';
93+
const httpTarget = `/generation-functions?viewportThemeColor=${testTitle}&shouldThrowInGenerateViewport=1`;
9194

92-
const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
93-
return (
94-
transactionEvent.contexts?.trace?.data?.['http.target'] ===
95-
`/generation-functions?viewportThemeColor=${testTitle}&shouldThrowInGenerateViewport=1`
96-
);
95+
const spanPromise = waitForStreamedSpan('nextjs-14', span => {
96+
return span.is_segment && span.attributes['http.target']?.value === httpTarget;
9797
});
9898

9999
const errorEventPromise = waitForError('nextjs-14', errorEvent => {
100100
return errorEvent?.exception?.values?.[0]?.value === 'generateViewport Error';
101101
});
102102

103-
await page.goto(`/generation-functions?viewportThemeColor=${testTitle}&shouldThrowInGenerateViewport=1`);
103+
await page.goto(httpTarget);
104104

105-
expect(await transactionPromise).toBeDefined();
105+
expect(await spanPromise).toBeDefined();
106106
expect(await errorEventPromise).toBeDefined();
107107

108108
const errorEvent = await errorEventPromise;

0 commit comments

Comments
 (0)