1- import { subscribe } from 'node:diagnostics_channel' ;
1+ /* eslint-disable max-lines */
2+ import type { ChannelListener } from 'node:diagnostics_channel' ;
3+ import { subscribe , unsubscribe } from 'node:diagnostics_channel' ;
24import type * as http from 'node:http' ;
5+ import type * as https from 'node:https' ;
36import type { EventEmitter } from 'node:stream' ;
47import { context , propagation } from '@opentelemetry/api' ;
58import { VERSION } from '@opentelemetry/core' ;
69import type { InstrumentationConfig } from '@opentelemetry/instrumentation' ;
7- import { InstrumentationBase } from '@opentelemetry/instrumentation' ;
10+ import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
811import type { AggregationCounts , Client , SanitizedRequestData , Scope } from '@sentry/core' ;
912import {
1013 addBreadcrumb ,
@@ -26,6 +29,9 @@ import { getRequestUrl } from '../../utils/getRequestUrl';
2629
2730const INSTRUMENTATION_NAME = '@sentry/instrumentation-http' ;
2831
32+ type Http = typeof http ;
33+ type Https = typeof https ;
34+
2935export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
3036 /**
3137 * Whether breadcrumbs should be recorded for requests.
@@ -101,29 +107,80 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
101107 }
102108
103109 /** @inheritdoc */
104- public init ( ) : [ ] {
105- subscribe ( 'http.server.request.start' , data => {
106- const server = ( data as { server : http . Server } ) . server ;
107- this . _patchServerEmit ( server ) ;
108- } ) ;
109-
110- subscribe ( 'http.client.response.finish' , data => {
111- const request = ( data as { request : http . ClientRequest } ) . request ;
112- const response = ( data as { response : http . IncomingMessage } ) . response ;
110+ public init ( ) : [ InstrumentationNodeModuleDefinition , InstrumentationNodeModuleDefinition ] {
111+ // Nothing to do here
113112
114- this . _onOutgoingRequestFinish ( request , response ) ;
115- } ) ;
113+ const handledRequests = new WeakSet < http . ClientRequest > ( ) ;
116114
117- // When an error happens, we still want to have a breadcrumb
118- // In this case, `http.client.response.finish` is not triggered
119- subscribe ( 'http.client.request.error' , data => {
120- const request = ( data as { request : http . ClientRequest } ) . request ;
121- // there is no response object here, we only have access to request & error :(
115+ const handleOutgoingRequestFinishOnce = ( request : http . ClientRequest , response ?: http . IncomingMessage ) : void => {
116+ if ( handledRequests . has ( request ) ) {
117+ return ;
118+ }
122119
123- this . _onOutgoingRequestFinish ( request , undefined ) ;
124- } ) ;
120+ handledRequests . add ( request ) ;
121+ this . _onOutgoingRequestFinish ( request , response ) ;
122+ } ;
125123
126- return [ ] ;
124+ const onHttpServerRequestStart = ( ( _data : unknown ) => {
125+ const data = _data as { server : http . Server } ;
126+ this . _patchServerEmitOnce ( data . server ) ;
127+ } ) satisfies ChannelListener ;
128+
129+ const onHttpsServerRequestStart = ( ( _data : unknown ) => {
130+ const data = _data as { server : http . Server } ;
131+ this . _patchServerEmitOnce ( data . server ) ;
132+ } ) satisfies ChannelListener ;
133+
134+ const onHttpClientResponseFinish = ( ( _data : unknown ) => {
135+ const data = _data as { request : http . ClientRequest ; response : http . IncomingMessage } ;
136+ handleOutgoingRequestFinishOnce ( data . request , data . response ) ;
137+ } ) satisfies ChannelListener ;
138+
139+ const onHttpClientRequestError = ( ( _data : unknown ) => {
140+ const data = _data as { request : http . ClientRequest } ;
141+ handleOutgoingRequestFinishOnce ( data . request , undefined ) ;
142+ } ) satisfies ChannelListener ;
143+
144+ return [
145+ new InstrumentationNodeModuleDefinition (
146+ 'http' ,
147+ [ '*' ] ,
148+ ( moduleExports : Http ) : Http => {
149+ subscribe ( 'http.server.request.start' , onHttpServerRequestStart ) ;
150+ subscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
151+
152+ // When an error happens, we still want to have a breadcrumb
153+ // In this case, `http.client.response.finish` is not triggered
154+ subscribe ( 'http.client.request.error' , onHttpClientRequestError ) ;
155+
156+ return moduleExports ;
157+ } ,
158+ ( ) => {
159+ unsubscribe ( 'http.server.request.start' , onHttpServerRequestStart ) ;
160+ unsubscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
161+ unsubscribe ( 'http.client.request.error' , onHttpClientRequestError ) ;
162+ } ,
163+ ) ,
164+ new InstrumentationNodeModuleDefinition (
165+ 'https' ,
166+ [ '*' ] ,
167+ ( moduleExports : Https ) : Https => {
168+ subscribe ( 'http.server.request.start' , onHttpsServerRequestStart ) ;
169+ subscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
170+
171+ // When an error happens, we still want to have a breadcrumb
172+ // In this case, `http.client.response.finish` is not triggered
173+ subscribe ( 'http.client.request.error' , onHttpClientRequestError ) ;
174+
175+ return moduleExports ;
176+ } ,
177+ ( ) => {
178+ unsubscribe ( 'http.server.request.start' , onHttpsServerRequestStart ) ;
179+ unsubscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
180+ unsubscribe ( 'http.client.request.error' , onHttpClientRequestError ) ;
181+ } ,
182+ ) ,
183+ ] ;
127184 }
128185
129186 /**
@@ -150,7 +207,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
150207 * Patch a server.emit function to handle process isolation for incoming requests.
151208 * This will only patch the emit function if it was not already patched.
152209 */
153- private _patchServerEmit ( server : http . Server ) : void {
210+ private _patchServerEmitOnce ( server : http . Server ) : void {
154211 // eslint-disable-next-line @typescript-eslint/unbound-method
155212 const originalEmit = server . emit ;
156213
@@ -159,7 +216,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
159216 return ;
160217 }
161218
162- DEBUG_BUILD && logger . log ( INSTRUMENTATION_NAME , 'Patching server.emit' ) ;
219+ DEBUG_BUILD && logger . log ( INSTRUMENTATION_NAME , 'Patching server.emit!!! ' ) ;
163220
164221 // eslint-disable-next-line @typescript-eslint/no-this-alias
165222 const instrumentation = this ;
0 commit comments