Skip to content

Commit ce2822d

Browse files
committed
fix(node): Avoid creating breadcrumbs for suppressed requests
1 parent c64c4fd commit ce2822d

File tree

5 files changed

+101
-81
lines changed

5 files changed

+101
-81
lines changed

dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ async function run() {
88
await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
99
await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
1010

11+
await Sentry.suppressTracing(() => fetch(`${process.env.SERVER_URL}/api/v4`).then(res => res.text()));
12+
1113
Sentry.captureException(new Error('foo'));
1214
}
1315

dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createTestServer } from '../../../../utils/server';
44

55
describe('outgoing fetch', () => {
66
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
7-
test('outgoing fetch requests create breadcrumbs xxx', async () => {
7+
test('outgoing fetch requests create breadcrumbs', async () => {
88
const [SERVER_URL, closeTestServer] = await createTestServer().start();
99

1010
await createRunner()

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ async function run() {
99
await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
1010
await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
1111

12+
await Sentry.suppressTracing(() => makeHttpRequest(`${process.env.SERVER_URL}/api/v4`));
13+
1214
Sentry.captureException(new Error('foo'));
1315
}
1416

packages/node/src/integrations/http/SentryHttpInstrumentation.ts

Lines changed: 65 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type * as http from 'node:http';
55
import type * as https from 'node:https';
66
import type { EventEmitter } from 'node:stream';
77
import { context, propagation } from '@opentelemetry/api';
8-
import { VERSION } from '@opentelemetry/core';
8+
import { isTracingSuppressed, VERSION } from '@opentelemetry/core';
99
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
1010
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
1111
import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core';
@@ -116,11 +116,13 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
116116
*/
117117
export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpInstrumentationOptions> {
118118
private _propagationDecisionMap: LRUMap<string, boolean>;
119+
private _ignoreOutgoingRequestsMap: WeakMap<http.ClientRequest, boolean>;
119120

120121
public constructor(config: SentryHttpInstrumentationOptions = {}) {
121122
super(INSTRUMENTATION_NAME, VERSION, config);
122123

123124
this._propagationDecisionMap = new LRUMap<string, boolean>(100);
125+
this._ignoreOutgoingRequestsMap = new WeakMap<http.ClientRequest, boolean>();
124126
}
125127

126128
/** @inheritdoc */
@@ -149,6 +151,37 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
149151
this._onOutgoingRequestCreated(data.request);
150152
}) satisfies ChannelListener;
151153

154+
const wrap = <T extends Http | Https>(moduleExports: T): T => {
155+
if (hasRegisteredHandlers) {
156+
return moduleExports;
157+
}
158+
159+
hasRegisteredHandlers = true;
160+
161+
subscribe('http.server.request.start', onHttpServerRequestStart);
162+
subscribe('http.client.response.finish', onHttpClientResponseFinish);
163+
164+
// When an error happens, we still want to have a breadcrumb
165+
// In this case, `http.client.response.finish` is not triggered
166+
subscribe('http.client.request.error', onHttpClientRequestError);
167+
168+
// NOTE: This channel only exist since Node 22
169+
// Before that, outgoing requests are not patched
170+
// and trace headers are not propagated, sadly.
171+
if (this.getConfig().propagateTraceInOutgoingRequests) {
172+
subscribe('http.client.request.created', onHttpClientRequestCreated);
173+
}
174+
175+
return moduleExports;
176+
};
177+
178+
const unwrap = (): void => {
179+
unsubscribe('http.server.request.start', onHttpServerRequestStart);
180+
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
181+
unsubscribe('http.client.request.error', onHttpClientRequestError);
182+
unsubscribe('http.client.request.created', onHttpClientRequestCreated);
183+
};
184+
152185
/**
153186
* You may be wondering why we register these diagnostics-channel listeners
154187
* in such a convoluted way (as InstrumentationNodeModuleDefinition...)˝,
@@ -158,64 +191,8 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
158191
* especially the "import-on-top" pattern of setting up ESM applications.
159192
*/
160193
return [
161-
new InstrumentationNodeModuleDefinition(
162-
'http',
163-
['*'],
164-
(moduleExports: Http): Http => {
165-
if (hasRegisteredHandlers) {
166-
return moduleExports;
167-
}
168-
169-
hasRegisteredHandlers = true;
170-
171-
subscribe('http.server.request.start', onHttpServerRequestStart);
172-
subscribe('http.client.response.finish', onHttpClientResponseFinish);
173-
174-
// When an error happens, we still want to have a breadcrumb
175-
// In this case, `http.client.response.finish` is not triggered
176-
subscribe('http.client.request.error', onHttpClientRequestError);
177-
178-
// NOTE: This channel only exist since Node 23
179-
// Before that, outgoing requests are not patched
180-
// and trace headers are not propagated, sadly.
181-
if (this.getConfig().propagateTraceInOutgoingRequests) {
182-
subscribe('http.client.request.created', onHttpClientRequestCreated);
183-
}
184-
185-
return moduleExports;
186-
},
187-
() => {
188-
unsubscribe('http.server.request.start', onHttpServerRequestStart);
189-
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
190-
unsubscribe('http.client.request.error', onHttpClientRequestError);
191-
unsubscribe('http.client.request.created', onHttpClientRequestCreated);
192-
},
193-
),
194-
new InstrumentationNodeModuleDefinition(
195-
'https',
196-
['*'],
197-
(moduleExports: Https): Https => {
198-
if (hasRegisteredHandlers) {
199-
return moduleExports;
200-
}
201-
202-
hasRegisteredHandlers = true;
203-
204-
subscribe('http.server.request.start', onHttpServerRequestStart);
205-
subscribe('http.client.response.finish', onHttpClientResponseFinish);
206-
207-
// When an error happens, we still want to have a breadcrumb
208-
// In this case, `http.client.response.finish` is not triggered
209-
subscribe('http.client.request.error', onHttpClientRequestError);
210-
211-
return moduleExports;
212-
},
213-
() => {
214-
unsubscribe('http.server.request.start', onHttpServerRequestStart);
215-
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
216-
unsubscribe('http.client.request.error', onHttpClientRequestError);
217-
},
218-
),
194+
new InstrumentationNodeModuleDefinition('http', ['*'], wrap, unwrap),
195+
new InstrumentationNodeModuleDefinition('https', ['*'], wrap, unwrap),
219196
];
220197
}
221198

@@ -228,13 +205,12 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
228205

229206
const _breadcrumbs = this.getConfig().breadcrumbs;
230207
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs;
231-
const options = getRequestOptions(request);
232208

233-
const _ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
234-
const shouldCreateBreadcrumb =
235-
typeof _ignoreOutgoingRequests === 'function' ? !_ignoreOutgoingRequests(getRequestUrl(request), options) : true;
209+
// Note: We cannot rely on the map being set by `_onOutgoingRequestCreated`, because that is not run in Node <22
210+
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request) ?? this._shouldIgnoreOutgoingRequest(request);
211+
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
236212

237-
if (breadCrumbsEnabled && shouldCreateBreadcrumb) {
213+
if (breadCrumbsEnabled && !shouldIgnore) {
238214
addRequestBreadcrumb(request, response);
239215
}
240216
}
@@ -244,15 +220,16 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
244220
* It has access to the request object, and can mutate it before the request is sent.
245221
*/
246222
private _onOutgoingRequestCreated(request: http.ClientRequest): void {
247-
const url = getRequestUrl(request);
248-
const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
249-
const shouldPropagate =
250-
typeof ignoreOutgoingRequests === 'function' ? !ignoreOutgoingRequests(url, getRequestOptions(request)) : true;
223+
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request) ?? this._shouldIgnoreOutgoingRequest(request);
224+
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
251225

252-
if (!shouldPropagate) {
226+
if (shouldIgnore) {
253227
return;
254228
}
255229

230+
// Add trace propagation headers
231+
const url = getRequestUrl(request);
232+
256233
// Manually add the trace headers, if it applies
257234
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
258235
// Which we do not have in this case
@@ -368,6 +345,25 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
368345

369346
server.emit = newEmit;
370347
}
348+
349+
/**
350+
* Check if the given outgoing request should be ignored.
351+
*/
352+
private _shouldIgnoreOutgoingRequest(request: http.ClientRequest): boolean {
353+
if (isTracingSuppressed(context.active())) {
354+
return true;
355+
}
356+
357+
const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
358+
359+
if (!ignoreOutgoingRequests) {
360+
return false;
361+
}
362+
363+
const options = getRequestOptions(request);
364+
const url = getRequestUrl(request);
365+
return ignoreOutgoingRequests(url, options);
366+
}
371367
}
372368

373369
/** Add a breadcrumb for outgoing requests. */

packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { VERSION } from '@opentelemetry/core';
1+
import { context } from '@opentelemetry/api';
2+
import { isTracingSuppressed, VERSION } from '@opentelemetry/core';
23
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
34
import { InstrumentationBase } from '@opentelemetry/instrumentation';
45
import type { SanitizedRequestData } from '@sentry/core';
@@ -61,11 +62,13 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
6162
// unsubscribing.
6263
private _channelSubs: Array<ListenerRecord>;
6364
private _propagationDecisionMap: LRUMap<string, boolean>;
65+
private _ignoreOutgoingRequestsMap: WeakMap<UndiciRequest, boolean>;
6466

6567
public constructor(config: SentryNodeFetchInstrumentationOptions = {}) {
6668
super('@sentry/instrumentation-node-fetch', VERSION, config);
6769
this._channelSubs = [];
6870
this._propagationDecisionMap = new LRUMap<string, boolean>(100);
71+
this._ignoreOutgoingRequestsMap = new WeakMap<UndiciRequest, boolean>();
6972
}
7073

7174
/** No need to instrument files/modules. */
@@ -118,15 +121,17 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
118121
return;
119122
}
120123

121-
// Add trace propagation headers
122-
const url = getAbsoluteUrl(request.origin, request.path);
123-
const _ignoreOutgoingRequests = config.ignoreOutgoingRequests;
124-
const shouldIgnore = _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url);
124+
const shouldIgnore = this._shouldIgnoreOutgoingRequest(request);
125+
// We store this decisision for later so we do not need to re-evaluate it
126+
// Additionally, the active context is not correct in _onResponseHeaders, so we need to make sure it is evaluated here
127+
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
125128

126129
if (shouldIgnore) {
127130
return;
128131
}
129132

133+
const url = getAbsoluteUrl(request.origin, request.path);
134+
130135
// Manually add the trace headers, if it applies
131136
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
132137
// Which we do not have in this case
@@ -197,13 +202,9 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
197202
const _breadcrumbs = config.breadcrumbs;
198203
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs;
199204

200-
const _ignoreOutgoingRequests = config.ignoreOutgoingRequests;
201-
const shouldCreateBreadcrumb =
202-
typeof _ignoreOutgoingRequests === 'function'
203-
? !_ignoreOutgoingRequests(getAbsoluteUrl(request.origin, request.path))
204-
: true;
205+
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request);
205206

206-
if (breadCrumbsEnabled && shouldCreateBreadcrumb) {
207+
if (breadCrumbsEnabled && !shouldIgnore) {
207208
addRequestBreadcrumb(request, response);
208209
}
209210
}
@@ -232,6 +233,25 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
232233
unsubscribe,
233234
});
234235
}
236+
237+
/**
238+
* Check if the given outgoing request should be ignored.
239+
*/
240+
private _shouldIgnoreOutgoingRequest(request: UndiciRequest): boolean {
241+
if (isTracingSuppressed(context.active())) {
242+
return true;
243+
}
244+
245+
// Add trace propagation headers
246+
const url = getAbsoluteUrl(request.origin, request.path);
247+
const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
248+
249+
if (typeof ignoreOutgoingRequests !== 'function' || !url) {
250+
return false;
251+
}
252+
253+
return ignoreOutgoingRequests(url);
254+
}
235255
}
236256

237257
/** Add a breadcrumb for outgoing requests. */

0 commit comments

Comments
 (0)