Skip to content

Commit 85a4d11

Browse files
msonnbcodex
andauthored
test(e2e): Migrate Elysia to span streaming (#24162)
Exercise Elysia on Node with span streaming, including its low-cardinality handler names and error correlation. part of #23800 Co-authored-by: GPT-6 <codex@openai.com>
1 parent c5fce74 commit 85a4d11

5 files changed

Lines changed: 252 additions & 250 deletions

File tree

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Elysia } from 'elysia';
33
import { node } from '@elysiajs/node';
44

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

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

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

44
test('Captures an error thrown in a route handler', async ({ baseURL, request }) => {
55
const errorEventPromise = waitForError('elysia-node', event => {
@@ -47,13 +47,14 @@ test('Error event includes request metadata', async ({ baseURL, request }) => {
4747
});
4848

4949
test('Does not capture errors for 4xx responses', async ({ baseURL, request }) => {
50-
const transactionPromise = waitForTransaction('elysia-node', transactionEvent => {
51-
return transactionEvent?.transaction === 'GET /test-4xx';
52-
});
50+
const segmentPromise = waitForStreamedSpan(
51+
'elysia-node',
52+
segment => segment.is_segment && segment.name === 'GET /test-4xx',
53+
);
5354

5455
const response = await request.get(`${baseURL}/test-4xx`);
55-
// Wait for the transaction to ensure the request was processed
56-
await transactionPromise;
56+
// Wait for the segment to ensure the request was processed
57+
await segmentPromise;
5758

5859
expect(response.status()).toBe(400);
5960
});

‎dev-packages/e2e-tests/test-applications/elysia-node/tests/propagation.test.ts‎

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'node:crypto';
22
import { expect, test } from '@playwright/test';
3-
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
import { waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils';
44

55
test('Includes sentry-trace and baggage in response headers', async ({ baseURL }) => {
66
const response = await fetch(`${baseURL}/test-success`);
@@ -21,27 +21,23 @@ test('Includes sentry-trace and baggage in response headers', async ({ baseURL }
2121
test.fixme('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
2222
const id = randomUUID();
2323

24-
const inboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
25-
return (
26-
transactionEvent.contexts?.trace?.op === 'http.server' &&
27-
transactionEvent.transaction === 'GET /test-inbound-headers/:id'
28-
);
24+
const inboundSegmentPromise = waitForStreamedSpan('elysia-node', segmentEvent => {
25+
if (!segmentEvent.is_segment) return false;
26+
return getSpanOp(segmentEvent!) === 'http.server' && segmentEvent.name === 'GET /test-inbound-headers/:id';
2927
});
3028

31-
const outboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
32-
return (
33-
transactionEvent.contexts?.trace?.op === 'http.server' &&
34-
transactionEvent.transaction === 'GET /test-outgoing-fetch/:id'
35-
);
29+
const outboundSegmentPromise = waitForStreamedSpan('elysia-node', segmentEvent => {
30+
if (!segmentEvent.is_segment) return false;
31+
return getSpanOp(segmentEvent!) === 'http.server' && segmentEvent.name === 'GET /test-outgoing-fetch/:id';
3632
});
3733

3834
const response = await fetch(`${baseURL}/test-outgoing-fetch/${id}`);
3935
const data = await response.json();
4036

41-
const inboundTransaction = await inboundTransactionPromise;
42-
const outboundTransaction = await outboundTransactionPromise;
37+
const inboundSegment = await inboundSegmentPromise;
38+
const outboundSegment = await outboundSegmentPromise;
4339

44-
const traceId = outboundTransaction?.contexts?.trace?.trace_id;
40+
const traceId = outboundSegment?.trace_id;
4541
expect(traceId).toEqual(expect.any(String));
4642

4743
// Verify sentry-trace header was propagated to the inbound request
@@ -61,22 +57,22 @@ test.fixme('Propagates trace for outgoing fetch requests', async ({ baseURL }) =
6157
);
6258

6359
// Both transactions should share the same trace ID
64-
expect(inboundTransaction.contexts?.trace?.trace_id).toBe(traceId);
60+
expect(inboundSegment?.trace_id).toBe(traceId);
6561
});
6662

6763
test.fixme('Propagates trace for outgoing fetch to external allowed URL', async ({ baseURL }) => {
68-
const inboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
64+
const inboundSegmentPromise = waitForStreamedSpan('elysia-node', segmentEvent => {
65+
if (!segmentEvent.is_segment) return false;
6966
return (
70-
transactionEvent.contexts?.trace?.op === 'http.server' &&
71-
transactionEvent.transaction === 'GET /test-outgoing-fetch-external-allowed'
67+
getSpanOp(segmentEvent!) === 'http.server' && segmentEvent.name === 'GET /test-outgoing-fetch-external-allowed'
7268
);
7369
});
7470

7571
const response = await fetch(`${baseURL}/test-outgoing-fetch-external-allowed`);
7672
const data = await response.json();
7773

78-
const inboundTransaction = await inboundTransactionPromise;
79-
const traceId = inboundTransaction?.contexts?.trace?.trace_id;
74+
const inboundSegment = await inboundSegmentPromise;
75+
const traceId = inboundSegment?.trace_id;
8076

8177
expect(traceId).toEqual(expect.any(String));
8278

@@ -95,17 +91,18 @@ test.fixme('Propagates trace for outgoing fetch to external allowed URL', async
9591
});
9692

9793
test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => {
98-
const inboundTransactionPromise = waitForTransaction('elysia-node', transactionEvent => {
99-
return (
100-
transactionEvent.contexts?.trace?.op === 'http.server' &&
101-
transactionEvent.transaction === 'GET /test-outgoing-fetch-external-disallowed'
102-
);
103-
});
94+
const inboundSegmentPromise = waitForStreamedSpan(
95+
'elysia-node',
96+
segment =>
97+
segment.is_segment &&
98+
getSpanOp(segment) === 'http.server' &&
99+
segment.name === 'GET /test-outgoing-fetch-external-disallowed',
100+
);
104101

105102
const response = await fetch(`${baseURL}/test-outgoing-fetch-external-disallowed`);
106103
const data = await response.json();
107104

108-
await inboundTransactionPromise;
105+
await inboundSegmentPromise;
109106

110107
expect(data.route).toBe('/external-disallowed');
111108
expect(data.headers?.['sentry-trace']).toBeUndefined();
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
3+
4+
test('Sends a span for a successful route', async ({ baseURL, request }) => {
5+
const segmentEventPromise = waitForStreamedSpan(
6+
'elysia-node',
7+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
8+
);
9+
10+
await request.get(`${baseURL}/test-success`);
11+
12+
const segmentEvent = await segmentEventPromise;
13+
14+
expect(segmentEvent).toEqual(
15+
expect.objectContaining({
16+
name: 'GET /test-success',
17+
is_segment: true,
18+
attributes: expect.objectContaining({ 'sentry.segment.name.source': { value: 'route', type: 'string' } }),
19+
}),
20+
);
21+
22+
expect(segmentEvent).toEqual(
23+
expect.objectContaining({
24+
status: 'ok',
25+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
26+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
27+
attributes: expect.objectContaining({ 'sentry.op': { value: 'http.server', type: 'string' } }),
28+
}),
29+
);
30+
});
31+
32+
test('Sends a span with parameterized route name', async ({ baseURL, request }) => {
33+
const segmentEventPromise = waitForStreamedSpan(
34+
'elysia-node',
35+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-param/:param',
36+
);
37+
38+
await request.get(`${baseURL}/test-param/123`);
39+
40+
const segmentEvent = await segmentEventPromise;
41+
42+
expect(segmentEvent.name).toBe('GET /test-param/:param');
43+
expect(segmentEvent.attributes['sentry.segment.name.source']?.value).toBe('route');
44+
});
45+
46+
test('Sends a span with multiple parameterized segments', async ({ baseURL, request }) => {
47+
const segmentEventPromise = waitForStreamedSpan(
48+
'elysia-node',
49+
segment =>
50+
segment.is_segment &&
51+
getSpanOp(segment) === 'http.server' &&
52+
segment.name === 'GET /test-multi-param/:param1/:param2',
53+
);
54+
55+
await request.get(`${baseURL}/test-multi-param/foo/bar`);
56+
57+
const segmentEvent = await segmentEventPromise;
58+
59+
expect(segmentEvent.name).toBe('GET /test-multi-param/:param1/:param2');
60+
expect(segmentEvent.attributes['sentry.segment.name.source']?.value).toBe('route');
61+
});
62+
63+
test('Sends a span for an errored route', async ({ baseURL, request }) => {
64+
const segmentEventPromise = waitForStreamedSpan(
65+
'elysia-node',
66+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-exception/:id',
67+
);
68+
69+
await request.get(`${baseURL}/test-exception/777`);
70+
71+
const segmentEvent = await segmentEventPromise;
72+
73+
expect(segmentEvent.name).toBe('GET /test-exception/:id');
74+
expect(segmentEvent?.status).toBe('error');
75+
});
76+
77+
test('Includes manually started spans with parent-child relationship', async ({ baseURL, request }) => {
78+
const segmentEventPromise = collectStreamedSpansUntilSegment(
79+
'elysia-node',
80+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
81+
);
82+
83+
await request.get(`${baseURL}/test-transaction`);
84+
85+
const segmentEventSpans = await segmentEventPromise;
86+
const segmentEvent = segmentEventSpans.find(
87+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-transaction',
88+
)!;
89+
const spans = segmentEventSpans.filter(
90+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
91+
);
92+
93+
const testSpan = spans.find(span => span.name === 'test-span');
94+
const childSpan = spans.find(span => span.name === 'child-span');
95+
96+
expect(testSpan).toEqual(
97+
expect.objectContaining({
98+
name: 'test-span',
99+
attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }),
100+
}),
101+
);
102+
103+
expect(childSpan).toEqual(
104+
expect.objectContaining({
105+
name: 'child-span',
106+
parent_span_id: testSpan?.span_id,
107+
attributes: expect.objectContaining({ 'sentry.origin': { value: 'manual', type: 'string' } }),
108+
}),
109+
);
110+
});
111+
112+
test('Creates lifecycle spans for Elysia hooks', async ({ baseURL, request }) => {
113+
const segmentEventPromise = collectStreamedSpansUntilSegment(
114+
'elysia-node',
115+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
116+
);
117+
118+
await request.get(`${baseURL}/test-success`);
119+
120+
const segmentEventSpans = await segmentEventPromise;
121+
const segmentEvent = segmentEventSpans.find(
122+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
123+
)!;
124+
const spans = segmentEventSpans.filter(
125+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
126+
);
127+
128+
// Elysia should produce lifecycle spans enriched with sentry attributes
129+
const elysiaSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.http.elysia');
130+
expect(elysiaSpans.length).toBeGreaterThan(0);
131+
132+
expect(spans.filter(span => span.name === '/test-success')).toEqual([
133+
expect.objectContaining({
134+
name: '/test-success',
135+
attributes: expect.objectContaining({
136+
'sentry.op': { value: 'handler', type: 'string' },
137+
'sentry.origin': { value: 'auto.http.elysia', type: 'string' },
138+
}),
139+
}),
140+
]);
141+
});
142+
143+
test('Names middleware "anonymous" and handlers after their route', async ({ baseURL, request }) => {
144+
const segmentEventPromise = collectStreamedSpansUntilSegment(
145+
'elysia-node',
146+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
147+
);
148+
149+
await request.get(`${baseURL}/with-middleware/test`);
150+
151+
const segmentEventSpans = await segmentEventPromise;
152+
const segmentEvent = segmentEventSpans.find(
153+
segment =>
154+
segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
155+
)!;
156+
const spans = segmentEventSpans.filter(
157+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
158+
);
159+
160+
const unknownSpans = spans.filter(span => span.name === '<unknown>');
161+
expect(unknownSpans).toHaveLength(0);
162+
163+
const elysiaSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.http.elysia');
164+
const anonymousMiddlewareSpans = elysiaSpans.filter(
165+
span => getSpanOp(span) === 'middleware' && span.name === 'anonymous',
166+
);
167+
expect(anonymousMiddlewareSpans).toHaveLength(1);
168+
169+
const handlerSpans = elysiaSpans.filter(span => getSpanOp(span) === 'handler');
170+
expect(handlerSpans.map(span => span.name)).toEqual(['/with-middleware/test']);
171+
});
172+
173+
test('Creates lifecycle spans for route-specific middleware', async ({ baseURL, request }) => {
174+
const segmentEventPromise = collectStreamedSpansUntilSegment(
175+
'elysia-node',
176+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
177+
);
178+
179+
await request.get(`${baseURL}/with-middleware/test`);
180+
181+
const segmentEventSpans = await segmentEventPromise;
182+
const segmentEvent = segmentEventSpans.find(
183+
segment =>
184+
segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /with-middleware/test',
185+
)!;
186+
const spans = segmentEventSpans.filter(
187+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
188+
);
189+
190+
// BeforeHandle span should be present from the route-specific middleware
191+
expect(spans.filter(span => span.name === 'BeforeHandle')).toEqual([
192+
expect.objectContaining({
193+
name: 'BeforeHandle',
194+
attributes: expect.objectContaining({
195+
'sentry.op': { value: 'middleware', type: 'string' },
196+
'sentry.origin': { value: 'auto.http.elysia', type: 'string' },
197+
}),
198+
}),
199+
]);
200+
});
201+
202+
test('Captures request metadata for POST requests', async ({ baseURL, request }) => {
203+
const segmentEventPromise = waitForStreamedSpan(
204+
'elysia-node',
205+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'POST /test-post',
206+
);
207+
208+
const response = await request.post(`${baseURL}/test-post`, {
209+
data: { foo: 'bar', other: 1 },
210+
headers: { 'Content-Type': 'application/json' },
211+
});
212+
const resBody = await response.json();
213+
214+
expect(resBody).toEqual({ status: 'ok', body: { foo: 'bar', other: 1 } });
215+
216+
const segmentEvent = await segmentEventPromise;
217+
218+
expect(segmentEvent.attributes['http.request.method']?.value).toEqual('POST');
219+
expect(segmentEvent.attributes['url.full']?.value).toEqual(expect.stringContaining('/test-post'));
220+
expect(segmentEvent.attributes['http.request.header.content_type']?.value).toEqual('application/json');
221+
});

0 commit comments

Comments
 (0)