@@ -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' ;
@@ -116,11 +116,13 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
116
116
*/
117
117
export class SentryHttpInstrumentation extends InstrumentationBase < SentryHttpInstrumentationOptions > {
118
118
private _propagationDecisionMap : LRUMap < string , boolean > ;
119
+ private _ignoreOutgoingRequestsMap : WeakMap < http . ClientRequest , boolean > ;
119
120
120
121
public constructor ( config : SentryHttpInstrumentationOptions = { } ) {
121
122
super ( INSTRUMENTATION_NAME , VERSION , config ) ;
122
123
123
124
this . _propagationDecisionMap = new LRUMap < string , boolean > ( 100 ) ;
125
+ this . _ignoreOutgoingRequestsMap = new WeakMap < http . ClientRequest , boolean > ( ) ;
124
126
}
125
127
126
128
/** @inheritdoc */
@@ -149,6 +151,37 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
149
151
this . _onOutgoingRequestCreated ( data . request ) ;
150
152
} ) satisfies ChannelListener ;
151
153
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
+
152
185
/**
153
186
* You may be wondering why we register these diagnostics-channel listeners
154
187
* in such a convoluted way (as InstrumentationNodeModuleDefinition...)˝,
@@ -158,64 +191,8 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
158
191
* especially the "import-on-top" pattern of setting up ESM applications.
159
192
*/
160
193
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 ) ,
219
196
] ;
220
197
}
221
198
@@ -228,13 +205,12 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
228
205
229
206
const _breadcrumbs = this . getConfig ( ) . breadcrumbs ;
230
207
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs ;
231
- const options = getRequestOptions ( request ) ;
232
208
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 ) ;
236
212
237
- if ( breadCrumbsEnabled && shouldCreateBreadcrumb ) {
213
+ if ( breadCrumbsEnabled && ! shouldIgnore ) {
238
214
addRequestBreadcrumb ( request , response ) ;
239
215
}
240
216
}
@@ -244,15 +220,16 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
244
220
* It has access to the request object, and can mutate it before the request is sent.
245
221
*/
246
222
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 ) ;
251
225
252
- if ( ! shouldPropagate ) {
226
+ if ( shouldIgnore ) {
253
227
return ;
254
228
}
255
229
230
+ // Add trace propagation headers
231
+ const url = getRequestUrl ( request ) ;
232
+
256
233
// Manually add the trace headers, if it applies
257
234
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
258
235
// Which we do not have in this case
@@ -368,6 +345,25 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
368
345
369
346
server . emit = newEmit ;
370
347
}
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
+ }
371
367
}
372
368
373
369
/** Add a breadcrumb for outgoing requests. */
0 commit comments