Skip to content

Commit 149bf8e

Browse files
msonnbcodex
andcommitted
test(e2e): Migrate Express 4 and TSX to span streaming
Co-Authored-By: GPT-6 <codex@openai.com>
1 parent 37118dd commit 149bf8e

12 files changed

Lines changed: 949 additions & 836 deletions

File tree

‎dev-packages/e2e-tests/test-applications/node-express/src/app.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ declare global {
99
}
1010

1111
Sentry.init({
12-
traceLifecycle: 'static',
1312
environment: 'qa', // dynamic sampling bias to keep transactions
1413
dsn: process.env.E2E_TEST_DSN,
1514
includeLocalVariables: true,

‎dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { waitForError, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
33

44
test('Sends correct error event', async ({ baseURL }) => {
55
const errorEventPromise = waitForError('node-express', event => {
66
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
77
});
88

9-
const transactionEventPromise = waitForTransaction('node-express', event => {
10-
return event.transaction === 'GET /test-exception/:id';
11-
});
9+
const segmentEventPromise = collectStreamedSpansUntilSegment('node-express', 'GET /test-exception/:id');
1210

1311
await fetch(`${baseURL}/test-exception/123`);
1412

1513
const errorEvent = await errorEventPromise;
16-
const transactionEvent = await transactionEventPromise;
14+
const segmentEventSpans = await segmentEventPromise;
15+
const segmentEvent = segmentEventSpans.find(
16+
segment => segment.is_segment && segment.name === 'GET /test-exception/:id',
17+
)!;
1718

1819
expect(errorEvent.exception?.values).toHaveLength(1);
1920
const exception = errorEvent.exception?.values?.[0];
@@ -38,13 +39,18 @@ test('Sends correct error event', async ({ baseURL }) => {
3839
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
3940
});
4041

41-
// The error is attached to the same trace as the request transaction, and to a
42-
// span that belongs to that transaction (its root span or one of its children).
43-
const transactionTrace = transactionEvent.contexts?.trace;
44-
expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id);
42+
// The error is attached to the same trace as the request segment, and to a
43+
// span in that segment.
44+
const segmentTrace = segmentEvent;
45+
expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentTrace?.trace_id);
4546

46-
const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)];
47-
expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id);
47+
const segmentSpanIds = [
48+
segmentTrace?.span_id,
49+
...segmentEventSpans
50+
.filter(span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id)
51+
.map(span => span.span_id),
52+
];
53+
expect(segmentSpanIds).toContain(errorEvent.contexts?.trace?.span_id);
4854
});
4955

5056
test('Should record caught exceptions with local variable', async ({ baseURL }) => {

‎dev-packages/e2e-tests/test-applications/node-express/tests/mcp.test.ts‎

Lines changed: 137 additions & 126 deletions
Large diffs are not rendered by default.
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
3+
4+
test('Sends an API route span', async ({ baseURL }) => {
5+
const pageloadSegmentEventPromise = collectStreamedSpansUntilSegment(
6+
'node-express',
7+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
8+
);
9+
10+
await fetch(`${baseURL}/test-transaction`);
11+
12+
const segmentEventSpans = await pageloadSegmentEventPromise;
13+
const segmentEvent = segmentEventSpans.find(
14+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
15+
)!;
16+
17+
expect(segmentEvent).toEqual(
18+
expect.objectContaining({
19+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
20+
status: 'ok',
21+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
22+
attributes: expect.objectContaining({
23+
'sentry.segment.name.source': { value: 'route', type: 'string' },
24+
'sentry.origin': { value: 'auto.http.http_server', type: 'string' },
25+
'sentry.op': { value: 'http.server', type: 'string' },
26+
'sentry.sample_rate': { value: 1, type: 'integer' },
27+
'sentry.kind': { value: 'server', type: 'string' },
28+
'http.response.status_code': { value: 200, type: 'integer' },
29+
'url.full': { value: 'http://localhost:3030/test-transaction', type: 'string' },
30+
'url.path': { value: '/test-transaction', type: 'string' },
31+
'server.address': { value: 'localhost', type: 'string' },
32+
'http.request.method': { value: 'GET', type: 'string' },
33+
'url.scheme': { value: 'http', type: 'string' },
34+
'user_agent.original': { value: 'node', type: 'string' },
35+
'client.address': { value: '::1', type: 'string' },
36+
'client.port': { value: expect.any(Number), type: 'integer' },
37+
'network.transport': { value: 'tcp', type: 'string' },
38+
'network.local.address': { value: expect.any(String), type: 'string' },
39+
'network.local.port': { value: expect.any(Number), type: 'integer' },
40+
'network.peer.address': { value: expect.any(String), type: 'string' },
41+
'network.peer.port': { value: expect.any(Number), type: 'integer' },
42+
'network.protocol.name': { value: 'http', type: 'string' },
43+
'network.protocol.version': { value: '1.1', type: 'string' },
44+
'server.port': { value: 3030, type: 'integer' },
45+
'http.response.status_text': { value: 'OK', type: 'string' },
46+
'http.route': { value: '/test-transaction', type: 'string' },
47+
'http.request.header.accept': { value: '*/*', type: 'string' },
48+
'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' },
49+
'http.request.header.accept_language': { value: '*', type: 'string' },
50+
'http.request.header.connection': { value: 'keep-alive', type: 'string' },
51+
'http.request.header.host': { value: expect.any(String), type: 'string' },
52+
'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' },
53+
'http.request.header.user_agent': { value: 'node', type: 'string' },
54+
}),
55+
}),
56+
);
57+
58+
expect(segmentEvent.attributes['http.response.status_code']?.value).toBe(200);
59+
60+
expect(segmentEvent).toEqual(
61+
expect.objectContaining({
62+
name: 'GET /test-transaction',
63+
is_segment: true,
64+
attributes: expect.objectContaining({ 'sentry.segment.name.source': { value: 'route', type: 'string' } }),
65+
}),
66+
);
67+
68+
const spans = segmentEventSpans.filter(
69+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
70+
);
71+
72+
// Manually started span
73+
expect(spans.filter(span => span.name === 'test-span')).toEqual([
74+
expect.objectContaining({
75+
name: 'test-span',
76+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
77+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
78+
start_timestamp: expect.any(Number),
79+
status: 'ok',
80+
end_timestamp: expect.any(Number),
81+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
82+
attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }),
83+
}),
84+
]);
85+
86+
// auto instrumented spans
87+
expect(spans.filter(span => span.name === 'query')).toEqual([
88+
expect.objectContaining({
89+
name: 'query',
90+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
91+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
92+
start_timestamp: expect.any(Number),
93+
status: 'ok',
94+
end_timestamp: expect.any(Number),
95+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
96+
attributes: expect.objectContaining({
97+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
98+
'sentry.op': { value: 'middleware', type: 'string' },
99+
'express.name': { value: 'query', type: 'string' },
100+
'express.type': { value: 'middleware', type: 'string' },
101+
}),
102+
}),
103+
]);
104+
105+
expect(spans.filter(span => span.name === 'expressInit')).toEqual([
106+
expect.objectContaining({
107+
name: 'expressInit',
108+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
109+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
110+
start_timestamp: expect.any(Number),
111+
status: 'ok',
112+
end_timestamp: expect.any(Number),
113+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
114+
attributes: expect.objectContaining({
115+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
116+
'sentry.op': { value: 'middleware', type: 'string' },
117+
'express.name': { value: 'expressInit', type: 'string' },
118+
'express.type': { value: 'middleware', type: 'string' },
119+
}),
120+
}),
121+
]);
122+
123+
expect(spans.filter(span => span.name === '/test-transaction')).toEqual([
124+
expect.objectContaining({
125+
name: '/test-transaction',
126+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
127+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
128+
start_timestamp: expect.any(Number),
129+
status: 'ok',
130+
end_timestamp: expect.any(Number),
131+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
132+
attributes: expect.objectContaining({
133+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
134+
'sentry.op': { value: 'handler', type: 'string' },
135+
'http.route': { value: '/test-transaction', type: 'string' },
136+
'express.name': { value: '/test-transaction', type: 'string' },
137+
'express.type': { value: 'request_handler', type: 'string' },
138+
}),
139+
}),
140+
]);
141+
});
142+
143+
test('Sends an API route span for an errored route', async ({ baseURL }) => {
144+
const segmentEventPromise = collectStreamedSpansUntilSegment(
145+
'node-express',
146+
segment =>
147+
getSpanOp(segment) === 'http.server' &&
148+
segment.name === 'GET /test-exception/:id' &&
149+
segment.attributes['url.full']?.value === 'http://localhost:3030/test-exception/777',
150+
);
151+
152+
await fetch(`${baseURL}/test-exception/777`);
153+
154+
const segmentEventSpans = await segmentEventPromise;
155+
const segmentEvent = segmentEventSpans.find(
156+
segment =>
157+
segment.is_segment &&
158+
getSpanOp(segment) === 'http.server' &&
159+
segment.name === 'GET /test-exception/:id' &&
160+
segment.attributes['url.full']?.value === 'http://localhost:3030/test-exception/777',
161+
)!;
162+
163+
expect(getSpanOp(segmentEvent)).toEqual('http.server');
164+
expect(segmentEvent.name).toEqual('GET /test-exception/:id');
165+
expect(segmentEvent?.status).toEqual('error');
166+
expect(segmentEvent.attributes?.['http.response.status_code']?.value).toEqual(500);
167+
168+
const spans = segmentEventSpans.filter(
169+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
170+
);
171+
172+
expect(spans.filter(span => span.name === 'query')).toEqual([
173+
expect.objectContaining({
174+
name: 'query',
175+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
176+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
177+
start_timestamp: expect.any(Number),
178+
status: 'ok',
179+
end_timestamp: expect.any(Number),
180+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
181+
attributes: expect.objectContaining({
182+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
183+
'sentry.op': { value: 'middleware', type: 'string' },
184+
'express.name': { value: 'query', type: 'string' },
185+
'express.type': { value: 'middleware', type: 'string' },
186+
}),
187+
}),
188+
]);
189+
190+
expect(spans.filter(span => span.name === 'expressInit')).toEqual([
191+
expect.objectContaining({
192+
name: 'expressInit',
193+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
194+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
195+
start_timestamp: expect.any(Number),
196+
status: 'ok',
197+
end_timestamp: expect.any(Number),
198+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
199+
attributes: expect.objectContaining({
200+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
201+
'sentry.op': { value: 'middleware', type: 'string' },
202+
'express.name': { value: 'expressInit', type: 'string' },
203+
'express.type': { value: 'middleware', type: 'string' },
204+
}),
205+
}),
206+
]);
207+
208+
expect(spans.filter(span => span.name === '/test-exception/:id')).toEqual([
209+
expect.objectContaining({
210+
name: '/test-exception/:id',
211+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
212+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
213+
start_timestamp: expect.any(Number),
214+
status: 'error',
215+
end_timestamp: expect.any(Number),
216+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
217+
attributes: expect.objectContaining({
218+
'sentry.origin': { value: 'auto.http.express', type: 'string' },
219+
'sentry.op': { value: 'handler', type: 'string' },
220+
'http.route': { value: '/test-exception/:id', type: 'string' },
221+
'express.name': { value: '/test-exception/:id', type: 'string' },
222+
'express.type': { value: 'request_handler', type: 'string' },
223+
'error.type': { value: 'Error', type: 'string' },
224+
}),
225+
}),
226+
]);
227+
});
228+
229+
test('Outgoing fetch spans include response headers when headersToSpanAttributes is configured', async ({
230+
baseURL,
231+
}) => {
232+
const segmentEventPromise = collectStreamedSpansUntilSegment(
233+
'node-express',
234+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-outgoing-fetch',
235+
);
236+
237+
await fetch(`${baseURL}/test-outgoing-fetch`);
238+
239+
const segmentEventSpans = await segmentEventPromise;
240+
const segmentEvent = segmentEventSpans.find(
241+
segment =>
242+
segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-outgoing-fetch',
243+
)!;
244+
245+
const spans = segmentEventSpans.filter(
246+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
247+
);
248+
249+
// Find the outgoing fetch span (http.client operation from undici instrumentation)
250+
const fetchSpan = spans.find(
251+
span =>
252+
getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'http://localhost:3030/test-success',
253+
);
254+
255+
expect(fetchSpan).toBeDefined();
256+
expect(fetchSpan?.name).toBe('GET localhost');
257+
expect(fetchSpan?.attributes).toEqual(
258+
expect.objectContaining({ 'http.response.header.content-length': { value: [expect.any(String)], type: 'array' } }),
259+
);
260+
});
261+
262+
test('Extracts HTTP request headers as span attributes', async ({ baseURL }) => {
263+
const segmentEventPromise = waitForStreamedSpan(
264+
'node-express',
265+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
266+
);
267+
268+
await fetch(`${baseURL}/test-transaction`, {
269+
headers: {
270+
'User-Agent': 'Custom-Agent/1.0 (Test)',
271+
'Content-Type': 'application/json',
272+
'X-Custom-Header': 'test-value',
273+
Accept: 'application/json, text/plain',
274+
'X-Request-ID': 'req-123',
275+
},
276+
});
277+
278+
const segmentEvent = await segmentEventPromise;
279+
280+
expect(segmentEvent.attributes).toEqual(
281+
expect.objectContaining({
282+
'http.request.header.user_agent': { value: 'Custom-Agent/1.0 (Test)', type: 'string' },
283+
'http.request.header.content_type': { value: 'application/json', type: 'string' },
284+
'http.request.header.x_custom_header': { value: 'test-value', type: 'string' },
285+
'http.request.header.accept': { value: 'application/json, text/plain', type: 'string' },
286+
'http.request.header.x_request_id': { value: 'req-123', type: 'string' },
287+
}),
288+
);
289+
});

0 commit comments

Comments
 (0)