Skip to content

Commit 2054753

Browse files
authored
fix(node): Avoid creating breadcrumbs for suppressed requests (#16285)
It was noticed that we were creating breadcrumbs for sentry-internal requests, which is not desired. This was introduced by us moving breadcrumb generation out of the base http/fetch instrumentation into our own, where we did not look at `suppressTracing`. Now, when tracing is suppressed, no fetch/http breadcrumbs will be created.
1 parent 7e79a40 commit 2054753

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';
@@ -132,11 +132,13 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
132132
*/
133133
export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpInstrumentationOptions> {
134134
private _propagationDecisionMap: LRUMap<string, boolean>;
135+
private _ignoreOutgoingRequestsMap: WeakMap<http.ClientRequest, boolean>;
135136

136137
public constructor(config: SentryHttpInstrumentationOptions = {}) {
137138
super(INSTRUMENTATION_NAME, VERSION, config);
138139

139140
this._propagationDecisionMap = new LRUMap<string, boolean>(100);
141+
this._ignoreOutgoingRequestsMap = new WeakMap<http.ClientRequest, boolean>();
140142
}
141143

142144
/** @inheritdoc */
@@ -165,6 +167,37 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
165167
this._onOutgoingRequestCreated(data.request);
166168
}) satisfies ChannelListener;
167169

170+
const wrap = <T extends Http | Https>(moduleExports: T): T => {
171+
if (hasRegisteredHandlers) {
172+
return moduleExports;
173+
}
174+
175+
hasRegisteredHandlers = true;
176+
177+
subscribe('http.server.request.start', onHttpServerRequestStart);
178+
subscribe('http.client.response.finish', onHttpClientResponseFinish);
179+
180+
// When an error happens, we still want to have a breadcrumb
181+
// In this case, `http.client.response.finish` is not triggered
182+
subscribe('http.client.request.error', onHttpClientRequestError);
183+
184+
// NOTE: This channel only exist since Node 22
185+
// Before that, outgoing requests are not patched
186+
// and trace headers are not propagated, sadly.
187+
if (this.getConfig().propagateTraceInOutgoingRequests) {
188+
subscribe('http.client.request.created', onHttpClientRequestCreated);
189+
}
190+
191+
return moduleExports;
192+
};
193+
194+
const unwrap = (): void => {
195+
unsubscribe('http.server.request.start', onHttpServerRequestStart);
196+
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
197+
unsubscribe('http.client.request.error', onHttpClientRequestError);
198+
unsubscribe('http.client.request.created', onHttpClientRequestCreated);
199+
};
200+
168201
/**
169202
* You may be wondering why we register these diagnostics-channel listeners
170203
* in such a convoluted way (as InstrumentationNodeModuleDefinition...)˝,
@@ -174,64 +207,8 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
174207
* especially the "import-on-top" pattern of setting up ESM applications.
175208
*/
176209
return [
177-
new InstrumentationNodeModuleDefinition(
178-
'http',
179-
['*'],
180-
(moduleExports: Http): Http => {
181-
if (hasRegisteredHandlers) {
182-
return moduleExports;
183-
}
184-
185-
hasRegisteredHandlers = true;
186-
187-
subscribe('http.server.request.start', onHttpServerRequestStart);
188-
subscribe('http.client.response.finish', onHttpClientResponseFinish);
189-
190-
// When an error happens, we still want to have a breadcrumb
191-
// In this case, `http.client.response.finish` is not triggered
192-
subscribe('http.client.request.error', onHttpClientRequestError);
193-
194-
// NOTE: This channel only exist since Node 23
195-
// Before that, outgoing requests are not patched
196-
// and trace headers are not propagated, sadly.
197-
if (this.getConfig().propagateTraceInOutgoingRequests) {
198-
subscribe('http.client.request.created', onHttpClientRequestCreated);
199-
}
200-
201-
return moduleExports;
202-
},
203-
() => {
204-
unsubscribe('http.server.request.start', onHttpServerRequestStart);
205-
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
206-
unsubscribe('http.client.request.error', onHttpClientRequestError);
207-
unsubscribe('http.client.request.created', onHttpClientRequestCreated);
208-
},
209-
),
210-
new InstrumentationNodeModuleDefinition(
211-
'https',
212-
['*'],
213-
(moduleExports: Https): Https => {
214-
if (hasRegisteredHandlers) {
215-
return moduleExports;
216-
}
217-
218-
hasRegisteredHandlers = true;
219-
220-
subscribe('http.server.request.start', onHttpServerRequestStart);
221-
subscribe('http.client.response.finish', onHttpClientResponseFinish);
222-
223-
// When an error happens, we still want to have a breadcrumb
224-
// In this case, `http.client.response.finish` is not triggered
225-
subscribe('http.client.request.error', onHttpClientRequestError);
226-
227-
return moduleExports;
228-
},
229-
() => {
230-
unsubscribe('http.server.request.start', onHttpServerRequestStart);
231-
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
232-
unsubscribe('http.client.request.error', onHttpClientRequestError);
233-
},
234-
),
210+
new InstrumentationNodeModuleDefinition('http', ['*'], wrap, unwrap),
211+
new InstrumentationNodeModuleDefinition('https', ['*'], wrap, unwrap),
235212
];
236213
}
237214

@@ -244,13 +221,12 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
244221

245222
const _breadcrumbs = this.getConfig().breadcrumbs;
246223
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs;
247-
const options = getRequestOptions(request);
248224

249-
const _ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
250-
const shouldCreateBreadcrumb =
251-
typeof _ignoreOutgoingRequests === 'function' ? !_ignoreOutgoingRequests(getRequestUrl(request), options) : true;
225+
// Note: We cannot rely on the map being set by `_onOutgoingRequestCreated`, because that is not run in Node <22
226+
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request) ?? this._shouldIgnoreOutgoingRequest(request);
227+
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
252228

253-
if (breadCrumbsEnabled && shouldCreateBreadcrumb) {
229+
if (breadCrumbsEnabled && !shouldIgnore) {
254230
addRequestBreadcrumb(request, response);
255231
}
256232
}
@@ -260,15 +236,16 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
260236
* It has access to the request object, and can mutate it before the request is sent.
261237
*/
262238
private _onOutgoingRequestCreated(request: http.ClientRequest): void {
263-
const url = getRequestUrl(request);
264-
const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
265-
const shouldPropagate =
266-
typeof ignoreOutgoingRequests === 'function' ? !ignoreOutgoingRequests(url, getRequestOptions(request)) : true;
239+
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request) ?? this._shouldIgnoreOutgoingRequest(request);
240+
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
267241

268-
if (!shouldPropagate) {
242+
if (shouldIgnore) {
269243
return;
270244
}
271245

246+
// Add trace propagation headers
247+
const url = getRequestUrl(request);
248+
272249
// Manually add the trace headers, if it applies
273250
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
274251
// Which we do not have in this case
@@ -384,6 +361,25 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
384361

385362
server.emit = newEmit;
386363
}
364+
365+
/**
366+
* Check if the given outgoing request should be ignored.
367+
*/
368+
private _shouldIgnoreOutgoingRequest(request: http.ClientRequest): boolean {
369+
if (isTracingSuppressed(context.active())) {
370+
return true;
371+
}
372+
373+
const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
374+
375+
if (!ignoreOutgoingRequests) {
376+
return false;
377+
}
378+
379+
const options = getRequestOptions(request);
380+
const url = getRequestUrl(request);
381+
return ignoreOutgoingRequests(url, options);
382+
}
387383
}
388384

389385
/** 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)