@@ -8,26 +8,35 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
8
8
import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
9
9
import type { AggregationCounts , Client , RequestEventData , SanitizedRequestData , Scope } from '@sentry/core' ;
10
10
import {
11
+ LRUMap ,
11
12
addBreadcrumb ,
12
13
generateSpanId ,
13
14
getBreadcrumbLogLevelFromHttpStatusCode ,
14
15
getClient ,
15
16
getIsolationScope ,
16
17
getSanitizedUrlString ,
18
+ getTraceData ,
17
19
httpRequestToRequestData ,
18
20
logger ,
19
21
parseUrl ,
20
22
stripUrlQueryAndFragment ,
21
23
withIsolationScope ,
22
24
withScope ,
23
25
} from '@sentry/core' ;
26
+ import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry' ;
24
27
import { DEBUG_BUILD } from '../../debug-build' ;
25
28
import { getRequestUrl } from '../../utils/getRequestUrl' ;
26
29
import { getRequestInfo } from './vendor/getRequestInfo' ;
27
30
28
31
type Http = typeof http ;
29
32
type Https = typeof https ;
30
33
34
+ type RequestArgs =
35
+ // eslint-disable-next-line @typescript-eslint/ban-types
36
+ | [ url : string | URL , options ?: RequestOptions , callback ?: Function ]
37
+ // eslint-disable-next-line @typescript-eslint/ban-types
38
+ | [ options : RequestOptions , callback ?: Function ] ;
39
+
31
40
type SentryHttpInstrumentationOptions = InstrumentationConfig & {
32
41
/**
33
42
* Whether breadcrumbs should be recorded for requests.
@@ -80,8 +89,11 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
80
89
* https://github.com/open-telemetry/opentelemetry-js/blob/f8ab5592ddea5cba0a3b33bf8d74f27872c0367f/experimental/packages/opentelemetry-instrumentation-http/src/http.ts
81
90
*/
82
91
export class SentryHttpInstrumentation extends InstrumentationBase < SentryHttpInstrumentationOptions > {
92
+ private _propagationDecisionMap : LRUMap < string , boolean > ;
93
+
83
94
public constructor ( config : SentryHttpInstrumentationOptions = { } ) {
84
95
super ( '@sentry/instrumentation-http' , VERSION , config ) ;
96
+ this . _propagationDecisionMap = new LRUMap < string , boolean > ( 100 ) ;
85
97
}
86
98
87
99
/** @inheritdoc */
@@ -208,22 +220,21 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
208
220
return function outgoingRequest ( this : unknown , ...args : unknown [ ] ) : http . ClientRequest {
209
221
instrumentation . _diag . debug ( 'http instrumentation for outgoing requests' ) ;
210
222
211
- // Making a copy to avoid mutating the original args array
212
223
// We need to access and reconstruct the request options object passed to `ignoreOutgoingRequests`
213
224
// so that it matches what Otel instrumentation passes to `ignoreOutgoingRequestHook`.
214
225
// @see https://github.com/open-telemetry/opentelemetry-js/blob/7293e69c1e55ca62e15d0724d22605e61bd58952/experimental/packages/opentelemetry-instrumentation-http/src/http.ts#L756-L789
215
- const argsCopy = [ ...args ] ;
216
-
217
- const options = argsCopy . shift ( ) as URL | http . RequestOptions | string ;
226
+ const requestArgs = [ ...args ] as RequestArgs ;
227
+ const options = requestArgs [ 0 ] ;
228
+ const extraOptions = typeof requestArgs [ 1 ] === 'object' ? requestArgs [ 1 ] : undefined ;
218
229
219
- const extraOptions =
220
- typeof argsCopy [ 0 ] === 'object' && ( typeof options === 'string' || options instanceof URL )
221
- ? ( argsCopy . shift ( ) as http . RequestOptions )
222
- : undefined ;
230
+ const { optionsParsed, origin, pathname } = getRequestInfo ( instrumentation . _diag , options , extraOptions ) ;
231
+ const url = getAbsoluteUrl ( origin , pathname ) ;
223
232
224
- const { optionsParsed } = getRequestInfo ( instrumentation . _diag , options , extraOptions ) ;
233
+ addSentryHeadersToRequestOptions ( url , optionsParsed , instrumentation . _propagationDecisionMap ) ;
225
234
226
- const request = original . apply ( this , args ) as ReturnType < typeof http . request > ;
235
+ const request = original . apply ( this , [ optionsParsed , ...requestArgs . slice ( 1 ) ] ) as ReturnType <
236
+ typeof http . request
237
+ > ;
227
238
228
239
request . prependListener ( 'response' , ( response : http . IncomingMessage ) => {
229
240
const _breadcrumbs = instrumentation . getConfig ( ) . breadcrumbs ;
@@ -457,6 +468,41 @@ function patchRequestToCaptureBody(req: IncomingMessage, isolationScope: Scope):
457
468
}
458
469
}
459
470
471
+ /**
472
+ * Mutates the passed in `options` and adds `sentry-trace` / `baggage` headers, if they are not already set.
473
+ */
474
+ function addSentryHeadersToRequestOptions (
475
+ url : string ,
476
+ options : RequestOptions ,
477
+ propagationDecisionMap : LRUMap < string , boolean > ,
478
+ ) : void {
479
+ // Manually add the trace headers, if it applies
480
+ // Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
481
+ // Which we do not have in this case
482
+ const tracePropagationTargets = getClient ( ) ?. getOptions ( ) . tracePropagationTargets ;
483
+ const addedHeaders = shouldPropagateTraceForUrl ( url , tracePropagationTargets , propagationDecisionMap )
484
+ ? getTraceData ( )
485
+ : undefined ;
486
+
487
+ if ( ! addedHeaders ) {
488
+ return ;
489
+ }
490
+
491
+ if ( ! options . headers ) {
492
+ options . headers = { } ;
493
+ }
494
+ const headers = options . headers ;
495
+
496
+ Object . entries ( addedHeaders ) . forEach ( ( [ k , v ] ) => {
497
+ // We do not want to overwrite existing headers here
498
+ // If the core UndiciInstrumentation is registered, it will already have set the headers
499
+ // We do not want to add any then
500
+ if ( ! headers [ k ] ) {
501
+ headers [ k ] = v ;
502
+ }
503
+ } ) ;
504
+ }
505
+
460
506
/**
461
507
* Starts a session and tracks it in the context of a given isolation scope.
462
508
* When the passed response is finished, the session is put into a task and is
@@ -531,3 +577,23 @@ const clientToRequestSessionAggregatesMap = new Map<
531
577
Client ,
532
578
{ [ timestampRoundedToSeconds : string ] : { exited : number ; crashed : number ; errored : number } }
533
579
> ( ) ;
580
+
581
+ function getAbsoluteUrl ( origin : string , path : string = '/' ) : string {
582
+ try {
583
+ const url = new URL ( path , origin ) ;
584
+ return url . toString ( ) ;
585
+ } catch {
586
+ // fallback: Construct it on our own
587
+ const url = `${ origin } ` ;
588
+
589
+ if ( url . endsWith ( '/' ) && path . startsWith ( '/' ) ) {
590
+ return `${ url } ${ path . slice ( 1 ) } ` ;
591
+ }
592
+
593
+ if ( ! url . endsWith ( '/' ) && ! path . startsWith ( '/' ) ) {
594
+ return `${ url } /${ path . slice ( 1 ) } ` ;
595
+ }
596
+
597
+ return `${ url } ${ path } ` ;
598
+ }
599
+ }
0 commit comments