Skip to content

Commit a9750be

Browse files
authored
fix(node): Ensure node-fetch does not emit spans without tracing (#13765)
Found this while working on #13763. Oops, the node-otel-without-tracing E2E test was not running on CI, we forgot to add it there - and it was actually failing since we switched to the new undici instrumentation :O This PR ensures to add it to CI, and also fixes it. The main change for this is to ensure we do not emit any spans when tracing is disabled, while still ensuring that trace propagation works as expected for this case. I also pulled some general changes into this, which ensure that we patch both `http.get` and `http.request` properly.
1 parent 5fffd1b commit a9750be

File tree

9 files changed

+193
-111
lines changed

9 files changed

+193
-111
lines changed

.github/workflows/build.yml

+1
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,7 @@ jobs:
895895
'node-express-cjs-preload',
896896
'node-otel-sdk-node',
897897
'node-otel-custom-sampler',
898+
'node-otel-without-tracing',
898899
'ember-classic',
899900
'ember-embroider',
900901
'nextjs-app-dir',

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ local.log
4343
.rpt2_cache
4444

4545
lint-results.json
46+
trace.zip
4647

4748
# legacy
4849
tmp.js

dev-packages/e2e-tests/test-applications/node-otel-without-tracing/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"test:assert": "pnpm test"
1212
},
1313
"dependencies": {
14-
"@opentelemetry/sdk-trace-node": "1.25.1",
15-
"@opentelemetry/exporter-trace-otlp-http": "0.52.1",
16-
"@opentelemetry/instrumentation-undici": "0.4.0",
17-
"@opentelemetry/instrumentation": "0.52.1",
14+
"@opentelemetry/sdk-trace-node": "1.26.0",
15+
"@opentelemetry/exporter-trace-otlp-http": "0.53.0",
16+
"@opentelemetry/instrumentation-undici": "0.6.0",
17+
"@opentelemetry/instrumentation": "0.53.0",
1818
"@sentry/core": "latest || *",
1919
"@sentry/node": "latest || *",
2020
"@sentry/opentelemetry": "latest || *",

dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/instrument.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const { SentrySpanProcessor, SentryPropagator } = require('@sentry/opentelemetry
55
const { UndiciInstrumentation } = require('@opentelemetry/instrumentation-undici');
66
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
77

8-
const sentryClient = Sentry.init({
8+
Sentry.init({
99
environment: 'qa', // dynamic sampling bias to keep transactions
1010
dsn:
1111
process.env.E2E_TEST_DSN ||

dev-packages/e2e-tests/test-applications/node-otel-without-tracing/tests/transactions.test.ts

+57-101
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
1212

1313
const scopeSpans = json.resourceSpans?.[0]?.scopeSpans;
1414

15-
const httpScope = scopeSpans?.find(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http');
15+
const httpScope = scopeSpans?.find(
16+
scopeSpan => scopeSpan.scope.name === '@opentelemetry_sentry-patched/instrumentation-http',
17+
);
1618

1719
return (
1820
httpScope &&
@@ -22,7 +24,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
2224
);
2325
});
2426

25-
await fetch(`${baseURL}/test-transaction`);
27+
fetch(`${baseURL}/test-transaction`);
2628

2729
const otelData = await otelPromise;
2830

@@ -38,7 +40,9 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
3840
// But our default node-fetch spans are not emitted
3941
expect(scopeSpans.length).toEqual(2);
4042

41-
const httpScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http');
43+
const httpScopes = scopeSpans?.filter(
44+
scopeSpan => scopeSpan.scope.name === '@opentelemetry_sentry-patched/instrumentation-http',
45+
);
4246
const undiciScopes = scopeSpans?.filter(
4347
scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-undici',
4448
);
@@ -49,6 +53,38 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
4953
expect(undiciScopes.length).toBe(1);
5054
expect(undiciScopes[0].spans.length).toBe(1);
5155

56+
expect(undiciScopes[0].spans).toEqual([
57+
{
58+
traceId: expect.any(String),
59+
spanId: expect.any(String),
60+
name: 'GET',
61+
kind: 3,
62+
startTimeUnixNano: expect.any(String),
63+
endTimeUnixNano: expect.any(String),
64+
attributes: expect.arrayContaining([
65+
{ key: 'http.request.method', value: { stringValue: 'GET' } },
66+
{ key: 'http.request.method_original', value: { stringValue: 'GET' } },
67+
{ key: 'url.full', value: { stringValue: 'http://localhost:3030/test-success' } },
68+
{ key: 'url.path', value: { stringValue: '/test-success' } },
69+
{ key: 'url.query', value: { stringValue: '' } },
70+
{ key: 'url.scheme', value: { stringValue: 'http' } },
71+
{ key: 'server.address', value: { stringValue: 'localhost' } },
72+
{ key: 'server.port', value: { intValue: 3030 } },
73+
{ key: 'user_agent.original', value: { stringValue: 'node' } },
74+
{ key: 'network.peer.address', value: { stringValue: expect.any(String) } },
75+
{ key: 'network.peer.port', value: { intValue: 3030 } },
76+
{ key: 'http.response.status_code', value: { intValue: 200 } },
77+
{ key: 'http.response.header.content-length', value: { intValue: 16 } },
78+
]),
79+
droppedAttributesCount: 0,
80+
events: [],
81+
droppedEventsCount: 0,
82+
status: { code: 0 },
83+
links: [],
84+
droppedLinksCount: 0,
85+
},
86+
]);
87+
5288
// There may be another span from another request, we can ignore that
5389
const httpSpans = httpScopes[0].spans.filter(span =>
5490
span.attributes.some(attr => attr.key === 'http.target' && attr.value?.stringValue === '/test-transaction'),
@@ -62,104 +98,24 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
6298
kind: 2,
6399
startTimeUnixNano: expect.any(String),
64100
endTimeUnixNano: expect.any(String),
65-
attributes: [
66-
{
67-
key: 'http.url',
68-
value: {
69-
stringValue: 'http://localhost:3030/test-transaction',
70-
},
71-
},
72-
{
73-
key: 'http.host',
74-
value: {
75-
stringValue: 'localhost:3030',
76-
},
77-
},
78-
{
79-
key: 'net.host.name',
80-
value: {
81-
stringValue: 'localhost',
82-
},
83-
},
84-
{
85-
key: 'http.method',
86-
value: {
87-
stringValue: 'GET',
88-
},
89-
},
90-
{
91-
key: 'http.scheme',
92-
value: {
93-
stringValue: 'http',
94-
},
95-
},
96-
{
97-
key: 'http.target',
98-
value: {
99-
stringValue: '/test-transaction',
100-
},
101-
},
102-
{
103-
key: 'http.user_agent',
104-
value: {
105-
stringValue: 'node',
106-
},
107-
},
108-
{
109-
key: 'http.flavor',
110-
value: {
111-
stringValue: '1.1',
112-
},
113-
},
114-
{
115-
key: 'net.transport',
116-
value: {
117-
stringValue: 'ip_tcp',
118-
},
119-
},
120-
{
121-
key: 'sentry.origin',
122-
value: {
123-
stringValue: 'auto.http.otel.http',
124-
},
125-
},
126-
{
127-
key: 'net.host.ip',
128-
value: {
129-
stringValue: expect.any(String),
130-
},
131-
},
132-
{
133-
key: 'net.host.port',
134-
value: {
135-
intValue: 3030,
136-
},
137-
},
138-
{
139-
key: 'net.peer.ip',
140-
value: {
141-
stringValue: expect.any(String),
142-
},
143-
},
144-
{
145-
key: 'net.peer.port',
146-
value: {
147-
intValue: expect.any(Number),
148-
},
149-
},
150-
{
151-
key: 'http.status_code',
152-
value: {
153-
intValue: 200,
154-
},
155-
},
156-
{
157-
key: 'http.status_text',
158-
value: {
159-
stringValue: 'OK',
160-
},
161-
},
162-
],
101+
attributes: expect.arrayContaining([
102+
{ key: 'http.url', value: { stringValue: 'http://localhost:3030/test-transaction' } },
103+
{ key: 'http.host', value: { stringValue: 'localhost:3030' } },
104+
{ key: 'net.host.name', value: { stringValue: 'localhost' } },
105+
{ key: 'http.method', value: { stringValue: 'GET' } },
106+
{ key: 'http.scheme', value: { stringValue: 'http' } },
107+
{ key: 'http.target', value: { stringValue: '/test-transaction' } },
108+
{ key: 'http.user_agent', value: { stringValue: 'node' } },
109+
{ key: 'http.flavor', value: { stringValue: '1.1' } },
110+
{ key: 'net.transport', value: { stringValue: 'ip_tcp' } },
111+
{ key: 'net.host.ip', value: { stringValue: expect.any(String) } },
112+
{ key: 'net.host.port', value: { intValue: 3030 } },
113+
{ key: 'net.peer.ip', value: { stringValue: expect.any(String) } },
114+
{ key: 'net.peer.port', value: { intValue: expect.any(Number) } },
115+
{ key: 'http.status_code', value: { intValue: 200 } },
116+
{ key: 'http.status_text', value: { stringValue: 'OK' } },
117+
{ key: 'sentry.origin', value: { stringValue: 'auto.http.otel.http' } },
118+
]),
163119
droppedAttributesCount: 0,
164120
events: [],
165121
droppedEventsCount: 0,

dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function run(): Promise<void> {
2222
Sentry.addBreadcrumb({ message: 'manual breadcrumb' });
2323

2424
await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
25-
await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`);
25+
await makeHttpGet(`${process.env.SERVER_URL}/api/v1`);
2626
await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
2727
await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
2828

@@ -46,3 +46,16 @@ function makeHttpRequest(url: string): Promise<void> {
4646
.end();
4747
});
4848
}
49+
50+
function makeHttpGet(url: string): Promise<void> {
51+
return new Promise<void>(resolve => {
52+
http.get(url, httpRes => {
53+
httpRes.on('data', () => {
54+
// we don't care about data
55+
});
56+
httpRes.on('end', () => {
57+
resolve();
58+
});
59+
});
60+
});
61+
}

dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/scenario.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as http from 'http';
1313

1414
async function run(): Promise<void> {
1515
await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`);
16-
await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`);
16+
await makeHttpGet(`${process.env.SERVER_URL}/api/v1`);
1717
await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
1818
await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
1919

@@ -37,3 +37,16 @@ function makeHttpRequest(url: string): Promise<void> {
3737
.end();
3838
});
3939
}
40+
41+
function makeHttpGet(url: string): Promise<void> {
42+
return new Promise<void>(resolve => {
43+
http.get(url, httpRes => {
44+
httpRes.on('data', () => {
45+
// we don't care about data
46+
});
47+
httpRes.on('end', () => {
48+
resolve();
49+
});
50+
});
51+
});
52+
}

dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/test.ts

+50
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,56 @@ test('outgoing http requests are correctly instrumented with tracing disabled',
3838
},
3939
],
4040
},
41+
breadcrumbs: [
42+
{
43+
message: 'manual breadcrumb',
44+
timestamp: expect.any(Number),
45+
},
46+
{
47+
category: 'http',
48+
data: {
49+
'http.method': 'GET',
50+
url: `${SERVER_URL}/api/v0`,
51+
status_code: 404,
52+
ADDED_PATH: '/api/v0',
53+
},
54+
timestamp: expect.any(Number),
55+
type: 'http',
56+
},
57+
{
58+
category: 'http',
59+
data: {
60+
'http.method': 'GET',
61+
url: `${SERVER_URL}/api/v1`,
62+
status_code: 404,
63+
ADDED_PATH: '/api/v1',
64+
},
65+
timestamp: expect.any(Number),
66+
type: 'http',
67+
},
68+
{
69+
category: 'http',
70+
data: {
71+
'http.method': 'GET',
72+
url: `${SERVER_URL}/api/v2`,
73+
status_code: 404,
74+
ADDED_PATH: '/api/v2',
75+
},
76+
timestamp: expect.any(Number),
77+
type: 'http',
78+
},
79+
{
80+
category: 'http',
81+
data: {
82+
'http.method': 'GET',
83+
url: `${SERVER_URL}/api/v3`,
84+
status_code: 404,
85+
ADDED_PATH: '/api/v3',
86+
},
87+
timestamp: expect.any(Number),
88+
type: 'http',
89+
},
90+
],
4191
},
4292
})
4393
.start(closeTestServer);

0 commit comments

Comments
 (0)