Skip to content

Commit b5fc755

Browse files
cursoragentclaude
andcommitted
test(node): Add integration tests for diagnostics channel HTTP spans
This adds integration tests for the new outgoing request span creation feature using diagnostics channels (Node 22.12+). The tests verify: - Spans are created for outgoing HTTP requests - Trace propagation uses the outgoing span's ID, not the parent - The 'spans' option correctly disables span creation when false - Span attributes include proper sentry.origin values The tests are conditionally run only on Node 22.12+, 23.2+, or 24+ where the http.client.request.created diagnostics channel is available. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9cb0bcf commit b5fc755

File tree

6 files changed

+172
-0
lines changed

6 files changed

+172
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
integrations: [Sentry.httpIntegration({ spans: false })],
9+
transport: loggingTransport,
10+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as http from 'http';
3+
4+
Sentry.startSpan({ name: 'test_span' }, async () => {
5+
await makeHttpRequest(`${process.env.SERVER_URL}/api/test`);
6+
});
7+
8+
function makeHttpRequest(url) {
9+
return new Promise((resolve, reject) => {
10+
http
11+
.request(url, httpRes => {
12+
httpRes.on('data', () => {
13+
// we don't care about data
14+
});
15+
httpRes.on('end', () => {
16+
resolve();
17+
});
18+
httpRes.on('error', reject);
19+
})
20+
.on('error', reject)
21+
.end();
22+
});
23+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createTestServer } from '@sentry-internal/test-utils';
2+
import { parseSemver } from '@sentry/core';
3+
import { describe, expect } from 'vitest';
4+
import { createEsmAndCjsTests } from '../../../../utils/runner';
5+
6+
const NODE_VERSION = parseSemver(process.versions.node);
7+
8+
const supportsHttpDiagnosticsChannel =
9+
(NODE_VERSION.major === 22 && NODE_VERSION.minor >= 12) ||
10+
(NODE_VERSION.major === 23 && NODE_VERSION.minor >= 2) ||
11+
NODE_VERSION.major >= 24;
12+
13+
const testIfSupported = supportsHttpDiagnosticsChannel ? describe : describe.skip;
14+
15+
testIfSupported('outgoing http with diagnostics channel spans disabled', () => {
16+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
17+
test('does not create spans when spans option is false', async () => {
18+
expect.assertions(3);
19+
20+
const [SERVER_URL, closeTestServer] = await createTestServer()
21+
.get('/api/test', headers => {
22+
expect(headers['sentry-trace']).toEqual(expect.any(String));
23+
})
24+
.start();
25+
26+
await createRunner()
27+
.withEnv({ SERVER_URL })
28+
.expect({
29+
transaction: event => {
30+
expect(event.transaction).toBe('test_span');
31+
32+
const httpClientSpans = event.spans?.filter(span => span.op === 'http.client');
33+
expect(httpClientSpans).toHaveLength(0);
34+
},
35+
})
36+
.start()
37+
.completed();
38+
closeTestServer();
39+
});
40+
});
41+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
integrations: [],
9+
transport: loggingTransport,
10+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/node';
2+
import * as http from 'http';
3+
4+
Sentry.startSpan({ name: 'test_span' }, async () => {
5+
await makeHttpRequest(`${process.env.SERVER_URL}/api/test`);
6+
});
7+
8+
function makeHttpRequest(url) {
9+
return new Promise((resolve, reject) => {
10+
http
11+
.request(url, httpRes => {
12+
httpRes.on('data', () => {
13+
// we don't care about data
14+
});
15+
httpRes.on('end', () => {
16+
resolve();
17+
});
18+
httpRes.on('error', reject);
19+
})
20+
.on('error', reject)
21+
.end();
22+
});
23+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { createTestServer } from '@sentry-internal/test-utils';
2+
import { parseSemver } from '@sentry/core';
3+
import { describe, expect } from 'vitest';
4+
import { createEsmAndCjsTests } from '../../../../utils/runner';
5+
6+
const NODE_VERSION = parseSemver(process.versions.node);
7+
8+
// The `http.client.request.created` diagnostics channel was added in Node 22.12.0 and 23.2.0
9+
const supportsHttpDiagnosticsChannel =
10+
(NODE_VERSION.major === 22 && NODE_VERSION.minor >= 12) ||
11+
(NODE_VERSION.major === 23 && NODE_VERSION.minor >= 2) ||
12+
NODE_VERSION.major >= 24;
13+
14+
const testIfSupported = supportsHttpDiagnosticsChannel ? describe : describe.skip;
15+
16+
testIfSupported('outgoing http with diagnostics channel spans', () => {
17+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
18+
test('creates spans for outgoing requests and propagates trace context within span', async () => {
19+
expect.assertions(8);
20+
21+
let outgoingSpanId: string | undefined;
22+
23+
const [SERVER_URL, closeTestServer] = await createTestServer()
24+
.get('/api/test', headers => {
25+
// Verify trace propagation headers are present
26+
expect(headers['baggage']).toEqual(expect.any(String));
27+
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/));
28+
29+
// Extract the span ID from the sentry-trace header
30+
const sentryTrace = headers['sentry-trace'] as string;
31+
outgoingSpanId = sentryTrace.split('-')[1];
32+
33+
// Verify we're not propagating all-zero trace IDs
34+
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1');
35+
})
36+
.start();
37+
38+
await createRunner()
39+
.withEnv({ SERVER_URL })
40+
.expect({
41+
transaction: event => {
42+
expect(event.transaction).toBe('test_span');
43+
44+
// Verify that an http.client span was created
45+
const httpClientSpans = event.spans?.filter(span => span.op === 'http.client');
46+
expect(httpClientSpans).toHaveLength(1);
47+
48+
const httpSpan = httpClientSpans![0];
49+
expect(httpSpan?.description).toMatch(/^GET /);
50+
51+
// Verify the propagated span ID matches the created span
52+
if (outgoingSpanId) {
53+
expect(httpSpan?.span_id).toBe(outgoingSpanId);
54+
}
55+
56+
// Verify span attributes include sentry.origin
57+
expect(httpSpan?.data?.['sentry.origin']).toBe('auto.http.otel.http');
58+
},
59+
})
60+
.start()
61+
.completed();
62+
closeTestServer();
63+
});
64+
});
65+
});

0 commit comments

Comments
 (0)