Skip to content

Commit 3c8ff89

Browse files
msonnbcodex
andauthored
test(deno): Port integration tests to span streaming (#24870)
Deno integration suites still pin static tracing and miss the SDK's default span-streaming behavior. Exercise streaming with span-envelope capture and streamed span assertions, while retaining the GraphQL suite as static lifecycle coverage. Fixes #24004 Fixes #24133 Co-authored-by: GPT-6 <codex@openai.com>
1 parent fe68d49 commit 3c8ff89

27 files changed

Lines changed: 567 additions & 429 deletions

File tree

‎dev-packages/deno-integration-tests/src/index.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export function transactionSink(): {
7272
};
7373
}
7474

75+
export function getSpanOp(span: SerializedStreamedSpan): string | undefined {
76+
const attribute = span.attributes['sentry.op'];
77+
return attribute?.type === 'string' ? attribute.value : undefined;
78+
}
79+
7580
/**
7681
* A `beforeSend` hook that records every error and lets a test
7782
* `await` the first one matching a predicate. `waitFor` resolves immediately if
@@ -98,18 +103,22 @@ export function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise
98103
* (returning null is disallowed), so intercepting them needs a transport rather than a hook.
99104
*/
100105
export function spanSink(): {
106+
spans: SerializedStreamedSpan[];
101107
waitFor: (predicate: (span: SerializedStreamedSpan) => boolean) => Promise<SerializedStreamedSpan>;
102108
transport: () => Transport;
103109
} {
110+
const spans: SerializedStreamedSpan[] = [];
104111
const sink = eventSink<SerializedStreamedSpan>();
105112

106113
return {
114+
spans,
107115
waitFor: sink.waitFor,
108116
transport: () => ({
109117
send: (envelope: Envelope) => {
110118
for (const [header, payload] of envelope[1]) {
111119
if (header.type === 'span') {
112120
for (const span of (payload as SerializedStreamedSpanContainer).items) {
121+
spans.push(span);
113122
sink.beforeSend(span);
114123
}
115124
}
Lines changed: 84 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
// <reference lib="deno.ns" />
22

3-
import { denoServeIntegration, init } from '@sentry/deno';
3+
import { denoServeIntegration, flush, init } from '@sentry/deno';
44
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
5-
import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
5+
import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
66

77
Deno.test('Deno.serve captures incoming request bodies by default', async () => {
88
resetGlobals();
9-
const sink = transactionSink();
9+
const sink = spanSink();
1010

1111
init({
12-
traceLifecycle: 'static',
1312
dsn: 'https://username@domain/123',
1413
tracesSampleRate: 1,
15-
beforeSendTransaction: sink.beforeSendTransaction,
14+
transport: sink.transport,
1615
});
1716

1817
const requestBody = 'captured-by-default';
@@ -25,39 +24,45 @@ Deno.test('Deno.serve captures incoming request bodies by default', async () =>
2524
});
2625
await listening;
2726

28-
const transactionPromise = withTimeout(
29-
sink.waitFor(event => event.request?.url?.endsWith('/default') === true),
30-
5_000,
31-
'transaction for /default',
32-
);
33-
const response = await fetch(`http://localhost:${server.addr.port}/default`, {
34-
method: 'POST',
35-
headers: { 'content-type': 'text/plain' },
36-
body: requestBody,
37-
});
38-
assertEquals(await response.text(), 'OK');
39-
40-
const transaction = await transactionPromise;
41-
assertEquals(transaction.request?.data, requestBody);
42-
43-
abortController.abort();
44-
await server.finished;
27+
try {
28+
const spanPromise = withTimeout(
29+
sink.waitFor(
30+
span =>
31+
span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/default',
32+
),
33+
5_000,
34+
'segment span for /default',
35+
);
36+
const response = await fetch(`http://localhost:${server.addr.port}/default`, {
37+
method: 'POST',
38+
headers: { 'content-type': 'text/plain' },
39+
body: requestBody,
40+
});
41+
assertEquals(await response.text(), 'OK');
42+
43+
await flush();
44+
const span = await spanPromise;
45+
assertEquals(span.name, 'POST');
46+
assertEquals(span.attributes['http.request.body.data']?.value, requestBody);
47+
} finally {
48+
abortController.abort();
49+
await server.finished;
50+
}
4551
});
4652

4753
Deno.test('Deno.serve explicit small overrides disabled incoming request body collection', async () => {
4854
resetGlobals();
49-
const sink = transactionSink();
55+
const sink = spanSink();
5056

5157
init({
52-
traceLifecycle: 'static',
5358
dsn: 'https://username@domain/123',
5459
tracesSampleRate: 1,
5560
dataCollection: { httpBodies: [] },
5661
integrations: integrations => [
5762
...integrations.filter(integration => integration.name !== 'DenoServe'),
5863
denoServeIntegration({ maxRequestBodySize: 'small' }),
5964
],
60-
beforeSendTransaction: sink.beforeSendTransaction,
65+
transport: sink.transport,
6166
});
6267

6368
const requestBody = 'a'.repeat(1_001);
@@ -71,39 +76,47 @@ Deno.test('Deno.serve explicit small overrides disabled incoming request body co
7176
});
7277
await listening;
7378

74-
const transactionPromise = withTimeout(
75-
sink.waitFor(event => event.request?.url?.endsWith('/explicit-small') === true),
76-
5_000,
77-
'transaction for /explicit-small',
78-
);
79-
const response = await fetch(`http://localhost:${server.addr.port}/explicit-small`, {
80-
method: 'POST',
81-
headers: { 'content-type': 'text/plain' },
82-
body: requestBody,
83-
});
84-
assertEquals(await response.text(), 'OK');
85-
86-
const transaction = await transactionPromise;
87-
assertEquals(transaction.request?.data, expectedBody);
88-
89-
abortController.abort();
90-
await server.finished;
79+
try {
80+
const spanPromise = withTimeout(
81+
sink.waitFor(
82+
span =>
83+
span.is_segment &&
84+
getSpanOp(span) === 'http.server' &&
85+
span.attributes['url.path']?.value === '/explicit-small',
86+
),
87+
5_000,
88+
'segment span for /explicit-small',
89+
);
90+
const response = await fetch(`http://localhost:${server.addr.port}/explicit-small`, {
91+
method: 'POST',
92+
headers: { 'content-type': 'text/plain' },
93+
body: requestBody,
94+
});
95+
assertEquals(await response.text(), 'OK');
96+
97+
await flush();
98+
const span = await spanPromise;
99+
assertEquals(span.name, 'POST');
100+
assertEquals(span.attributes['http.request.body.data']?.value, expectedBody);
101+
} finally {
102+
abortController.abort();
103+
await server.finished;
104+
}
91105
});
92106

93107
Deno.test('Deno.serve explicit none overrides enabled incoming request body collection', async () => {
94108
resetGlobals();
95-
const sink = transactionSink();
109+
const sink = spanSink();
96110

97111
init({
98-
traceLifecycle: 'static',
99112
dsn: 'https://username@domain/123',
100113
tracesSampleRate: 1,
101114
dataCollection: { httpBodies: ['incomingRequest'] },
102115
integrations: integrations => [
103116
...integrations.filter(integration => integration.name !== 'DenoServe'),
104117
denoServeIntegration({ maxRequestBodySize: 'none' }),
105118
],
106-
beforeSendTransaction: sink.beforeSendTransaction,
119+
transport: sink.transport,
107120
});
108121

109122
const abortController = new AbortController();
@@ -115,21 +128,30 @@ Deno.test('Deno.serve explicit none overrides enabled incoming request body coll
115128
});
116129
await listening;
117130

118-
const transactionPromise = withTimeout(
119-
sink.waitFor(event => event.request?.url?.endsWith('/explicit-none') === true),
120-
5_000,
121-
'transaction for /explicit-none',
122-
);
123-
const response = await fetch(`http://localhost:${server.addr.port}/explicit-none`, {
124-
method: 'POST',
125-
headers: { 'content-type': 'text/plain' },
126-
body: 'do-not-capture',
127-
});
128-
assertEquals(await response.text(), 'OK');
129-
130-
const transaction = await transactionPromise;
131-
assertEquals(transaction.request?.data, undefined);
132-
133-
abortController.abort();
134-
await server.finished;
131+
try {
132+
const spanPromise = withTimeout(
133+
sink.waitFor(
134+
span =>
135+
span.is_segment &&
136+
getSpanOp(span) === 'http.server' &&
137+
span.attributes['url.path']?.value === '/explicit-none',
138+
),
139+
5_000,
140+
'segment span for /explicit-none',
141+
);
142+
const response = await fetch(`http://localhost:${server.addr.port}/explicit-none`, {
143+
method: 'POST',
144+
headers: { 'content-type': 'text/plain' },
145+
body: 'do-not-capture',
146+
});
147+
assertEquals(await response.text(), 'OK');
148+
149+
await flush();
150+
const span = await spanPromise;
151+
assertEquals(span.name, 'POST');
152+
assertEquals(span.attributes['http.request.body.data']?.value, undefined);
153+
} finally {
154+
abortController.abort();
155+
await server.finished;
156+
}
135157
});

‎dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,24 @@ import { nodeStackLineParser } from '@sentry/core/server';
1212
import { DenoClient, getCurrentScope, getDefaultIntegrations, startSpan } from '@sentry/deno';
1313
import { tracingChannel } from 'node:diagnostics_channel';
1414

15-
let nested = false;
15+
const spans = [];
1616

1717
const client = new DenoClient({
1818
dsn: 'https://username@domain/123',
1919
tracesSampleRate: 1,
20-
traceLifecycle: 'static',
2120
integrations: getDefaultIntegrations({ tracesSampleRate: 1 }),
2221
stackParser: createStackParser(nodeStackLineParser()),
23-
beforeSendTransaction(event) {
24-
const spans = event.spans ?? [];
25-
if (spans.some(s => s.op === 'db' && s.data?.['sentry.origin'] === 'auto.db.mysql')) {
26-
nested = true;
27-
}
28-
return null;
29-
},
30-
transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }),
22+
transport: () => ({
23+
send(envelope) {
24+
for (const [header, payload] of envelope[1]) {
25+
if (header.type === 'span') {
26+
spans.push(...payload.items);
27+
}
28+
}
29+
return Promise.resolve({});
30+
},
31+
flush: () => Promise.resolve(true),
32+
}),
3133
});
3234

3335
client.init();
@@ -50,5 +52,14 @@ startSpan({ name: 'parent', op: 'test' }, () => {
5052

5153
await client.flush(2000);
5254

55+
const parent = spans.find(span => span.is_segment && span.name === 'parent');
56+
const nested = spans.some(
57+
span =>
58+
span.attributes['sentry.op']?.value === 'db' &&
59+
span.attributes['sentry.origin']?.value === 'auto.db.mysql' &&
60+
span.parent_span_id === parent?.span_id &&
61+
span.trace_id === parent?.trace_id,
62+
);
63+
5364
// eslint-disable-next-line no-console
5465
console.log(`SCENARIO nested=${nested}`);

‎dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22

33
import { tracingChannel } from 'node:diagnostics_channel';
44
import type { DenoClient } from '@sentry/deno';
5-
import { init, startSpan } from '@sentry/deno';
5+
import { flush, init, startSpan } from '@sentry/deno';
66
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
77
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
88
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
9-
import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
9+
import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
1010

1111
Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0+)', () => {
1212
resetGlobals();
1313
const client = init({
14-
traceLifecycle: 'static',
1514
dsn: 'https://username@domain/123',
1615
tracesSampleRate: 1,
1716
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0
2120

2221
Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces a nested queue.publish span', async () => {
2322
resetGlobals();
24-
const sink = transactionSink();
23+
const sink = spanSink();
2524
init({
26-
traceLifecycle: 'static',
2725
dsn: 'https://username@domain/123',
2826
tracesSampleRate: 1,
29-
beforeSendTransaction: sink.beforeSendTransaction,
27+
transport: sink.transport,
3028
});
3129

3230
const channel = tracingChannel('orchestrion:amqplib:publish');
@@ -47,16 +45,25 @@ Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces
4745
});
4846
});
4947

48+
await flush();
49+
5050
const parent = await withTimeout(
51-
sink.waitFor(t => t.transaction === 'parent'),
51+
sink.waitFor(span => span.is_segment && span.name === 'parent'),
5252
5000,
53-
"'parent' transaction",
53+
"'parent' segment span",
5454
);
55+
const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
56+
assertEquals(children.length, 1);
57+
assertEquals(children[0]!.trace_id, parent.trace_id);
58+
assertEquals(children[0]!.is_segment, false);
5559

56-
const publishSpan = parent.spans?.find(s => s.op === 'queue.publish');
57-
assertExists(publishSpan, `expected a queue.publish child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
58-
assertEquals(publishSpan!.description, 'publish my-exchange');
59-
assertEquals(publishSpan!.data?.['messaging.destination.name'], 'my-exchange');
60-
assertEquals(publishSpan!.data?.['messaging.system'], 'rabbitmq');
61-
assertEquals(publishSpan!.data?.['sentry.origin'], 'auto.amqplib.publisher');
60+
const publishSpan = children.find(s => getSpanOp(s) === 'queue.publish');
61+
assertExists(
62+
publishSpan,
63+
`expected a queue.publish child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
64+
);
65+
assertEquals(publishSpan.name, 'send my-exchange');
66+
assertEquals(publishSpan.attributes['messaging.destination.name']?.value, 'my-exchange');
67+
assertEquals(publishSpan.attributes['messaging.system']?.value, 'rabbitmq');
68+
assertEquals(publishSpan.attributes['sentry.origin']?.value, 'auto.amqplib.publisher');
6269
});

0 commit comments

Comments
 (0)