Skip to content

Commit f100c7c

Browse files
chargomeclaude
andauthored
test(e2e): Port the nestjs-12 E2E app to span streaming (#24098)
Removes the `traceLifecycle: 'static'` pin and rewrites the specs against streamed spans. Ref: #23801 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 2e1fe4c commit f100c7c

5 files changed

Lines changed: 486 additions & 818 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nestjs-12/src/instrument.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/nestjs';
22

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

‎dev-packages/e2e-tests/test-applications/nestjs-12/tests/errors.test.ts‎

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpansUntilSegment, waitForError } from '@sentry-internal/test-utils';
3+
4+
const APP_NAME = 'nestjs-12';
35

46
test('Sends exception to Sentry', async ({ baseURL }) => {
5-
const errorEventPromise = waitForError('nestjs-12', event => {
7+
const errorEventPromise = waitForError(APP_NAME, event => {
68
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
79
});
810

@@ -37,38 +39,35 @@ test('Sends exception to Sentry', async ({ baseURL }) => {
3739
test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
3840
let errorEventOccurred = false;
3941

40-
waitForError('nestjs-12', event => {
42+
waitForError(APP_NAME, event => {
4143
if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') {
4244
errorEventOccurred = true;
4345
}
4446

4547
return event?.transaction === 'GET /test-expected-400-exception/:id';
4648
});
4749

48-
waitForError('nestjs-12', event => {
50+
waitForError(APP_NAME, event => {
4951
if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') {
5052
errorEventOccurred = true;
5153
}
5254

5355
return event?.transaction === 'GET /test-expected-500-exception/:id';
5456
});
5557

56-
const transactionEventPromise400 = waitForTransaction('nestjs-12', transactionEvent => {
57-
return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id';
58-
});
59-
60-
const transactionEventPromise500 = waitForTransaction('nestjs-12', transactionEvent => {
61-
return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id';
62-
});
58+
// Waiting for each request's segment span is how this spec knows the request finished and
59+
// any error it would have produced had its chance to be sent.
60+
const spansPromise400 = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-expected-400-exception/:id');
61+
const spansPromise500 = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-expected-500-exception/:id');
6362

6463
const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`);
6564
expect(response400.status).toBe(400);
6665

6766
const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`);
6867
expect(response500.status).toBe(500);
6968

70-
await transactionEventPromise400;
71-
await transactionEventPromise500;
69+
await spansPromise400;
70+
await spansPromise500;
7271

7372
(await fetch(`${baseURL}/flush`)).text();
7473

@@ -78,22 +77,20 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
7877
test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => {
7978
let errorEventOccurred = false;
8079

81-
waitForError('nestjs-12', event => {
80+
waitForError(APP_NAME, event => {
8281
if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') {
8382
errorEventOccurred = true;
8483
}
8584

8685
return event?.transaction === 'GET /test-expected-rpc-exception/:id';
8786
});
8887

89-
const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => {
90-
return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id';
91-
});
88+
const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-expected-rpc-exception/:id');
9289

9390
const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`);
9491
expect(response.status).toBe(500);
9592

96-
await transactionEventPromise;
93+
await spansPromise;
9794

9895
(await fetch(`${baseURL}/flush`)).text();
9996

@@ -105,17 +102,15 @@ test('Global exception filter registered in main module is applied and exception
105102
}) => {
106103
let errorEventOccurred = false;
107104

108-
waitForError('nestjs-12', event => {
105+
waitForError(APP_NAME, event => {
109106
if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') {
110107
errorEventOccurred = true;
111108
}
112109

113110
return event?.transaction === 'GET /example-exception-global-filter';
114111
});
115112

116-
const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => {
117-
return transactionEvent?.transaction === 'GET /example-exception-global-filter';
118-
});
113+
const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /example-exception-global-filter');
119114

120115
const response = await fetch(`${baseURL}/example-exception-global-filter`);
121116
const responseBody = await response.json();
@@ -128,7 +123,7 @@ test('Global exception filter registered in main module is applied and exception
128123
message: 'Example exception was handled by global filter!',
129124
});
130125

131-
await transactionEventPromise;
126+
await spansPromise;
132127

133128
(await fetch(`${baseURL}/flush`)).text();
134129

@@ -140,17 +135,15 @@ test('Local exception filter registered in main module is applied and exception
140135
}) => {
141136
let errorEventOccurred = false;
142137

143-
waitForError('nestjs-12', event => {
138+
waitForError(APP_NAME, event => {
144139
if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') {
145140
errorEventOccurred = true;
146141
}
147142

148143
return event?.transaction === 'GET /example-exception-local-filter';
149144
});
150145

151-
const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => {
152-
return transactionEvent?.transaction === 'GET /example-exception-local-filter';
153-
});
146+
const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /example-exception-local-filter');
154147

155148
const response = await fetch(`${baseURL}/example-exception-local-filter`);
156149
const responseBody = await response.json();
@@ -163,7 +156,7 @@ test('Local exception filter registered in main module is applied and exception
163156
message: 'Example exception was handled by local filter!',
164157
});
165158

166-
await transactionEventPromise;
159+
await spansPromise;
167160

168161
(await fetch(`${baseURL}/flush`)).text();
169162

‎dev-packages/e2e-tests/test-applications/nestjs-12/tests/span-decorator.test.ts‎

Lines changed: 46 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,67 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import type { SerializedStreamedSpan } from '@sentry-internal/test-utils';
3+
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
34

4-
test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => {
5-
const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => {
6-
return (
7-
transactionEvent?.contexts?.trace?.op === 'http.server' &&
8-
transactionEvent?.transaction === 'GET /test-span-decorator-async'
9-
);
10-
});
5+
const APP_NAME = 'nestjs-12';
6+
7+
const SPAN_ID = /^[a-f0-9]{16}$/;
8+
9+
/** The full shape of a `@SentryTraced` span, so `toEqual` catches anything unexpected. */
10+
function tracedSpan(segmentSpan: SerializedStreamedSpan, name: string, op: string): Record<string, unknown> {
11+
return {
12+
name,
13+
span_id: expect.stringMatching(SPAN_ID),
14+
trace_id: segmentSpan.trace_id,
15+
parent_span_id: expect.stringMatching(SPAN_ID),
16+
start_timestamp: expect.any(Number),
17+
end_timestamp: expect.any(Number),
18+
is_segment: false,
19+
status: 'ok',
20+
attributes: {
21+
'sentry.trace_lifecycle': { type: 'string', value: 'stream' },
22+
'sentry.segment.name': { type: 'string', value: segmentSpan.name },
23+
'sentry.segment.id': { type: 'string', value: segmentSpan.span_id },
24+
'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.nestjs' },
25+
'sentry.sdk.version': { type: 'string', value: expect.any(String) },
26+
'sentry.environment': { type: 'string', value: 'qa' },
27+
// CI builds the apps with a release, local runs have none. It comes from the client
28+
// options, so whatever the segment span got, every other span of the trace got too.
29+
...(segmentSpan.attributes['sentry.release']
30+
? { 'sentry.release': { type: 'string', value: expect.any(String) } }
31+
: {}),
32+
'sentry.origin': { type: 'string', value: 'auto.function.nestjs.sentry_traced' },
33+
'sentry.op': { type: 'string', value: op },
34+
},
35+
};
36+
}
37+
38+
test('Trace includes span and correct value for decorated async function', async ({ baseURL }) => {
39+
const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-span-decorator-async');
1140

1241
const response = await fetch(`${baseURL}/test-span-decorator-async`);
1342
const body = await response.json();
1443

1544
expect(body.result).toEqual('test');
1645

17-
const transactionEvent = await transactionEventPromise;
46+
const spans = await spansPromise;
1847

19-
expect(transactionEvent.spans).toEqual(
20-
expect.arrayContaining([
21-
expect.objectContaining({
22-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
23-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
24-
data: {
25-
'sentry.origin': 'auto.function.nestjs.sentry_traced',
26-
'sentry.op': 'wait and return a string',
27-
},
28-
description: 'wait',
29-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
30-
start_timestamp: expect.any(Number),
31-
status: 'ok',
32-
op: 'wait and return a string',
33-
origin: 'auto.function.nestjs.sentry_traced',
34-
}),
35-
]),
36-
);
48+
const segmentSpan = spans.find(span => span.is_segment)!;
49+
expect(spans.find(span => span.name === 'wait')).toEqual(tracedSpan(segmentSpan, 'wait', 'wait and return a string'));
3750
});
3851

39-
test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => {
40-
const transactionEventPromise = waitForTransaction('nestjs-12', transactionEvent => {
41-
return (
42-
transactionEvent?.contexts?.trace?.op === 'http.server' &&
43-
transactionEvent?.transaction === 'GET /test-span-decorator-sync'
44-
);
45-
});
52+
test('Trace includes span and correct value for decorated sync function', async ({ baseURL }) => {
53+
const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-span-decorator-sync');
4654

4755
const response = await fetch(`${baseURL}/test-span-decorator-sync`);
4856
const body = await response.json();
4957

5058
expect(body.result).toEqual('test');
5159

52-
const transactionEvent = await transactionEventPromise;
60+
const spans = await spansPromise;
5361

54-
expect(transactionEvent.spans).toEqual(
55-
expect.arrayContaining([
56-
expect.objectContaining({
57-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
58-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
59-
data: {
60-
'sentry.origin': 'auto.function.nestjs.sentry_traced',
61-
'sentry.op': 'return a string',
62-
},
63-
description: 'getString',
64-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
65-
start_timestamp: expect.any(Number),
66-
status: 'ok',
67-
op: 'return a string',
68-
origin: 'auto.function.nestjs.sentry_traced',
69-
}),
70-
]),
62+
const segmentSpan = spans.find(span => span.is_segment)!;
63+
expect(spans.find(span => span.name === 'getString')).toEqual(
64+
tracedSpan(segmentSpan, 'getString', 'return a string'),
7165
);
7266
});
7367

0 commit comments

Comments
 (0)