@@ -5,7 +5,7 @@ import type * as http from 'node:http';
5
5
import type * as https from 'node:https' ;
6
6
import type { EventEmitter } from 'node:stream' ;
7
7
import { context , propagation } from '@opentelemetry/api' ;
8
- import { VERSION } from '@opentelemetry/core' ;
8
+ import { isTracingSuppressed , VERSION } from '@opentelemetry/core' ;
9
9
import type { InstrumentationConfig } from '@opentelemetry/instrumentation' ;
10
10
import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
11
11
import type { AggregationCounts , Client , SanitizedRequestData , Scope } from '@sentry/core' ;
@@ -132,11 +132,13 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
132
132
*/
133
133
export class SentryHttpInstrumentation extends InstrumentationBase < SentryHttpInstrumentationOptions > {
134
134
private _propagationDecisionMap : LRUMap < string , boolean > ;
135
+ private _ignoreOutgoingRequestsMap : WeakMap < http . ClientRequest , boolean > ;
135
136
136
137
public constructor ( config : SentryHttpInstrumentationOptions = { } ) {
137
138
super ( INSTRUMENTATION_NAME , VERSION , config ) ;
138
139
139
140
this . _propagationDecisionMap = new LRUMap < string , boolean > ( 100 ) ;
141
+ this . _ignoreOutgoingRequestsMap = new WeakMap < http . ClientRequest , boolean > ( ) ;
140
142
}
141
143
142
144
/** @inheritdoc */
@@ -165,6 +167,37 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
165
167
this . _onOutgoingRequestCreated ( data . request ) ;
166
168
} ) satisfies ChannelListener ;
167
169
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
+
168
201
/**
169
202
* You may be wondering why we register these diagnostics-channel listeners
170
203
* in such a convoluted way (as InstrumentationNodeModuleDefinition...)˝,
@@ -174,64 +207,8 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
174
207
* especially the "import-on-top" pattern of setting up ESM applications.
175
208
*/
176
209
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 ) ,
235
212
] ;
236
213
}
237
214
@@ -244,13 +221,12 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
244
221
245
222
const _breadcrumbs = this . getConfig ( ) . breadcrumbs ;
246
223
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs ;
247
- const options = getRequestOptions ( request ) ;
248
224
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 ) ;
252
228
253
- if ( breadCrumbsEnabled && shouldCreateBreadcrumb ) {
229
+ if ( breadCrumbsEnabled && ! shouldIgnore ) {
254
230
addRequestBreadcrumb ( request , response ) ;
255
231
}
256
232
}
@@ -260,15 +236,16 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
260
236
* It has access to the request object, and can mutate it before the request is sent.
261
237
*/
262
238
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 ) ;
267
241
268
- if ( ! shouldPropagate ) {
242
+ if ( shouldIgnore ) {
269
243
return ;
270
244
}
271
245
246
+ // Add trace propagation headers
247
+ const url = getRequestUrl ( request ) ;
248
+
272
249
// Manually add the trace headers, if it applies
273
250
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
274
251
// Which we do not have in this case
@@ -384,6 +361,25 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
384
361
385
362
server . emit = newEmit ;
386
363
}
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
+ }
387
383
}
388
384
389
385
/** Add a breadcrumb for outgoing requests. */
0 commit comments