@@ -8,26 +8,35 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
88import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
99import type { AggregationCounts , Client , RequestEventData , SanitizedRequestData , Scope } from '@sentry/core' ;
1010import {
11+ LRUMap ,
1112 addBreadcrumb ,
1213 generateSpanId ,
1314 getBreadcrumbLogLevelFromHttpStatusCode ,
1415 getClient ,
1516 getIsolationScope ,
1617 getSanitizedUrlString ,
18+ getTraceData ,
1719 httpRequestToRequestData ,
1820 logger ,
1921 parseUrl ,
2022 stripUrlQueryAndFragment ,
2123 withIsolationScope ,
2224 withScope ,
2325} from '@sentry/core' ;
26+ import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry' ;
2427import { DEBUG_BUILD } from '../../debug-build' ;
2528import { getRequestUrl } from '../../utils/getRequestUrl' ;
2629import { getRequestInfo } from './vendor/getRequestInfo' ;
2730
2831type Http = typeof http ;
2932type Https = typeof https ;
3033
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+
3140type SentryHttpInstrumentationOptions = InstrumentationConfig & {
3241 /**
3342 * Whether breadcrumbs should be recorded for requests.
@@ -80,8 +89,11 @@ const MAX_BODY_BYTE_LENGTH = 1024 * 1024;
8089 * https://github.com/open-telemetry/opentelemetry-js/blob/f8ab5592ddea5cba0a3b33bf8d74f27872c0367f/experimental/packages/opentelemetry-instrumentation-http/src/http.ts
8190 */
8291export class SentryHttpInstrumentation extends InstrumentationBase < SentryHttpInstrumentationOptions > {
92+ private _propagationDecisionMap : LRUMap < string , boolean > ;
93+
8394 public constructor ( config : SentryHttpInstrumentationOptions = { } ) {
8495 super ( '@sentry/instrumentation-http' , VERSION , config ) ;
96+ this . _propagationDecisionMap = new LRUMap < string , boolean > ( 100 ) ;
8597 }
8698
8799 /** @inheritdoc */
@@ -208,22 +220,21 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
208220 return function outgoingRequest ( this : unknown , ...args : unknown [ ] ) : http . ClientRequest {
209221 instrumentation . _diag . debug ( 'http instrumentation for outgoing requests' ) ;
210222
211- // Making a copy to avoid mutating the original args array
212223 // We need to access and reconstruct the request options object passed to `ignoreOutgoingRequests`
213224 // so that it matches what Otel instrumentation passes to `ignoreOutgoingRequestHook`.
214225 // @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 ;
218229
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 ) ;
223232
224- const { optionsParsed } = getRequestInfo ( instrumentation . _diag , options , extraOptions ) ;
233+ addSentryHeadersToRequestOptions ( url , optionsParsed , instrumentation . _propagationDecisionMap ) ;
225234
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+ > ;
227238
228239 request . prependListener ( 'response' , ( response : http . IncomingMessage ) => {
229240 const _breadcrumbs = instrumentation . getConfig ( ) . breadcrumbs ;
@@ -457,6 +468,41 @@ function patchRequestToCaptureBody(req: IncomingMessage, isolationScope: Scope):
457468 }
458469}
459470
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+
460506/**
461507 * Starts a session and tracks it in the context of a given isolation scope.
462508 * When the passed response is finished, the session is put into a task and is
@@ -531,3 +577,23 @@ const clientToRequestSessionAggregatesMap = new Map<
531577 Client ,
532578 { [ timestampRoundedToSeconds : string ] : { exited : number ; crashed : number ; errored : number } }
533579> ( ) ;
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