Skip to content

Commit d43375a

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

10 files changed

Lines changed: 708 additions & 641 deletions

File tree

‎dev-packages/e2e-tests/test-applications/node-hapi/src/app.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const Sentry = require('@sentry/node');
22

33
Sentry.init({
4-
traceLifecycle: 'static',
54
environment: 'qa', // dynamic sampling bias to keep transactions
65
dsn: process.env.E2E_TEST_DSN,
76
includeLocalVariables: true,
@@ -31,7 +30,7 @@ const init = async () => {
3130
method: 'GET',
3231
path: '/test-param/{param}',
3332
handler: function (request, h) {
34-
Sentry.setTag(`param-${request.params.param}`, 'yes');
33+
Sentry.setAttribute(`param-${request.params.param}`, 'yes');
3534

3635
return { paramWas: request.params.param };
3736
},

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

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
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('Sends thrown error to Sentry', async ({ baseURL }) => {
55
const errorEventPromise = waitForError('node-hapi', errorEvent => {
66
return errorEvent?.exception?.values?.[0]?.value === 'This is an error';
77
});
88

9-
const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => {
10-
return transactionEvent?.transaction === 'GET /test-failure';
11-
});
9+
const segmentEventPromise = waitForStreamedSpan(
10+
'node-hapi',
11+
segment => segment.is_segment && segment.name === 'GET /test-failure',
12+
);
1213

1314
await fetch(`${baseURL}/test-failure`);
1415

1516
const errorEvent = await errorEventPromise;
16-
const transactionEvent = await transactionEventPromise;
17+
const segmentEvent = await segmentEventPromise;
1718

18-
expect(transactionEvent.transaction).toBe('GET /test-failure');
19-
expect(transactionEvent.contexts?.trace).toMatchObject({
19+
expect(segmentEvent.name).toBe('GET /test-failure');
20+
expect(segmentEvent).toMatchObject({
2021
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
2122
span_id: expect.stringMatching(/[a-f0-9]{16}/),
2223
});
@@ -43,8 +44,8 @@ test('Sends thrown error to Sentry', async ({ baseURL }) => {
4344
span_id: expect.stringMatching(/[a-f0-9]{16}/),
4445
});
4546

46-
expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionEvent.contexts?.trace?.trace_id);
47-
expect(errorEvent.contexts?.trace?.span_id).toBe(transactionEvent.contexts?.trace?.span_id);
47+
expect(errorEvent.contexts?.trace?.trace_id).toBe(segmentEvent?.trace_id);
48+
expect(errorEvent.contexts?.trace?.span_id).toBe(segmentEvent?.span_id);
4849
});
4950

5051
test('sends error with parameterized transaction name', async ({ baseURL }) => {
@@ -71,26 +72,28 @@ test('Does not send errors to Sentry if boom throws in "onPreResponse" after JS
7172
return false; // expects to return a boolean (but not relevant here)
7273
});
7374

74-
const transactionEventPromise4xx = waitForTransaction('node-hapi', transactionEvent => {
75-
return transactionEvent?.transaction === 'GET /test-failure-boom-4xx';
76-
});
75+
const segmentEventPromise4xx = waitForStreamedSpan(
76+
'node-hapi',
77+
segment => segment.is_segment && segment.name === 'GET /test-failure-boom-4xx',
78+
);
7779

78-
const transactionEventPromise5xx = waitForTransaction('node-hapi', transactionEvent => {
79-
return transactionEvent?.transaction === 'GET /test-failure-boom-5xx';
80-
});
80+
const segmentEventPromise5xx = waitForStreamedSpan(
81+
'node-hapi',
82+
segment => segment.is_segment && segment.name === 'GET /test-failure-boom-5xx',
83+
);
8184

8285
const response4xx = await fetch(`${baseURL}/test-failure-boom-4xx`);
8386
const response5xx = await fetch(`${baseURL}/test-failure-boom-5xx`);
8487

8588
expect(response4xx.status).toBe(400);
8689
expect(response5xx.status).toBe(504);
8790

88-
const transactionEvent4xx = await transactionEventPromise4xx;
89-
const transactionEvent5xx = await transactionEventPromise5xx;
91+
const segmentEvent4xx = await segmentEventPromise4xx;
92+
const segmentEvent5xx = await segmentEventPromise5xx;
9093

9194
expect(errorEventOccurred).toBe(false);
92-
expect(transactionEvent4xx.transaction).toBe('GET /test-failure-boom-4xx');
93-
expect(transactionEvent5xx.transaction).toBe('GET /test-failure-boom-5xx');
95+
expect(segmentEvent4xx.name).toBe('GET /test-failure-boom-4xx');
96+
expect(segmentEvent5xx.name).toBe('GET /test-failure-boom-5xx');
9497
});
9598

9699
test('Does not send error to Sentry if error response is overwritten with 2xx in "onPreResponse"', async ({
@@ -105,17 +108,18 @@ test('Does not send error to Sentry if error response is overwritten with 2xx in
105108
return false; // expects to return a boolean (but not relevant here)
106109
});
107110

108-
const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => {
109-
return transactionEvent?.transaction === 'GET /test-failure-2xx-override-onPreResponse';
110-
});
111+
const segmentEventPromise = waitForStreamedSpan(
112+
'node-hapi',
113+
segment => segment.is_segment && segment.name === 'GET /test-failure-2xx-override-onPreResponse',
114+
);
111115

112116
const response = await fetch(`${baseURL}/test-failure-2xx-override-onPreResponse`);
113117

114-
const transactionEvent = await transactionEventPromise;
118+
const segmentEvent = await segmentEventPromise;
115119

116120
expect(response.status).toBe(200);
117121
expect(errorEventOccurred).toBe(false);
118-
expect(transactionEvent.transaction).toBe('GET /test-failure-2xx-override-onPreResponse');
122+
expect(segmentEvent.name).toBe('GET /test-failure-2xx-override-onPreResponse');
119123
});
120124

121125
test('Only sends onPreResponse error to Sentry if JS error is thrown in route handler AND onPreResponse', async ({
@@ -137,18 +141,19 @@ test('Only sends onPreResponse error to Sentry if JS error is thrown in route ha
137141
return false; // expects to return a boolean (but not relevant here)
138142
});
139143

140-
const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => {
141-
return transactionEvent?.transaction === 'GET /test-failure-JS-error-onPreResponse';
142-
});
144+
const segmentEventPromise = waitForStreamedSpan(
145+
'node-hapi',
146+
segment => segment.is_segment && segment.name === 'GET /test-failure-JS-error-onPreResponse',
147+
);
143148

144149
const response = await fetch(`${baseURL}/test-failure-JS-error-onPreResponse`);
145150

146151
expect(response.status).toBe(500);
147152

148153
const errorEvent = await errorEventPromise;
149-
const transactionEvent = await transactionEventPromise;
154+
const segmentEvent = await segmentEventPromise;
150155

151156
expect(routeHandlerErrorOccurred).toBe(false);
152-
expect(transactionEvent.transaction).toBe('GET /test-failure-JS-error-onPreResponse');
157+
expect(segmentEvent.name).toBe('GET /test-failure-JS-error-onPreResponse');
153158
expect(errorEvent.transaction).toEqual('GET /test-failure-JS-error-onPreResponse');
154159
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
3+
4+
test('Sends successful span', async ({ baseURL }) => {
5+
const pageloadSegmentEventPromise = collectStreamedSpansUntilSegment(
6+
'node-hapi',
7+
segment => getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
8+
);
9+
10+
await fetch(`${baseURL}/test-success`);
11+
12+
const segmentEventSpans = await pageloadSegmentEventPromise;
13+
const segmentEvent = segmentEventSpans.find(
14+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-success',
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-success', type: 'string' },
30+
'url.path': { value: '/test-success', 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-success', 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).toEqual(
59+
expect.objectContaining({
60+
name: 'GET /test-success',
61+
is_segment: true,
62+
attributes: expect.objectContaining({ 'sentry.segment.name.source': { value: 'route', type: 'string' } }),
63+
}),
64+
);
65+
66+
const spans = segmentEventSpans.filter(
67+
span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id,
68+
);
69+
70+
spans.forEach(span => {
71+
expect(Object.keys(span.attributes).some(key => key.startsWith('http.request.header.'))).toBe(false);
72+
});
73+
74+
expect(spans).toEqual([
75+
expect.objectContaining({
76+
name: '/test-success',
77+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
78+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
79+
start_timestamp: expect.any(Number),
80+
status: 'ok',
81+
end_timestamp: expect.any(Number),
82+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
83+
attributes: expect.objectContaining({
84+
'hapi.type': { value: 'router', type: 'string' },
85+
'http.request.method': { value: 'GET', type: 'string' },
86+
'http.route': { value: '/test-success', type: 'string' },
87+
'sentry.op': { value: 'router', type: 'string' },
88+
'sentry.origin': { value: 'auto.http.hapi', type: 'string' },
89+
}),
90+
}),
91+
expect.objectContaining({
92+
name: 'ext - onPreResponse',
93+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
94+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
95+
start_timestamp: expect.any(Number),
96+
status: 'ok',
97+
end_timestamp: expect.any(Number),
98+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
99+
attributes: expect.objectContaining({
100+
'hapi.type': { value: 'server.ext', type: 'string' },
101+
'sentry.op': { value: 'middleware', type: 'string' },
102+
'sentry.origin': { value: 'auto.http.hapi', type: 'string' },
103+
'server.ext.type': { value: 'onPreResponse', type: 'string' },
104+
}),
105+
}),
106+
]);
107+
});
108+
109+
test('Sends parameterized spans to Sentry', async ({ baseURL }) => {
110+
const pageloadSegmentEventPromise = waitForStreamedSpan(
111+
'node-hapi',
112+
segment => segment.is_segment && getSpanOp(segment) === 'http.server' && segment.name === 'GET /test-param/{param}',
113+
);
114+
115+
await fetch(`${baseURL}/test-param/123`);
116+
117+
const segmentEvent = await pageloadSegmentEventPromise;
118+
119+
expect(getSpanOp(segmentEvent)).toBe('http.server');
120+
expect(segmentEvent.attributes?.['http.route']?.value).toBe('/test-param/{param}');
121+
expect(segmentEvent.name).toBe('GET /test-param/{param}');
122+
});
123+
124+
test('Isolates requests', async ({ baseURL }) => {
125+
const segment1Promise = waitForStreamedSpan(
126+
'node-hapi',
127+
segment =>
128+
segment.is_segment &&
129+
getSpanOp(segment) === 'http.server' &&
130+
segment.attributes?.['url.path']?.value === '/test-param/888',
131+
);
132+
const segment2Promise = waitForStreamedSpan(
133+
'node-hapi',
134+
segment =>
135+
segment.is_segment &&
136+
getSpanOp(segment) === 'http.server' &&
137+
segment.attributes?.['url.path']?.value === '/test-param/999',
138+
);
139+
140+
await Promise.all([fetch(`${baseURL}/test-param/888`), fetch(`${baseURL}/test-param/999`)]);
141+
142+
const segment1 = await segment1Promise;
143+
const segment2 = await segment2Promise;
144+
145+
expect(segment1.attributes['param-888']?.value).toBe('yes');
146+
expect(segment1.attributes['param-999']).toBeUndefined();
147+
expect(segment2.attributes['param-999']?.value).toBe('yes');
148+
expect(segment2.attributes['param-888']).toBeUndefined();
149+
});

0 commit comments

Comments
 (0)