@@ -108,42 +108,43 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
108108
109109 /** @inheritdoc */
110110 public init ( ) : [ InstrumentationNodeModuleDefinition , InstrumentationNodeModuleDefinition ] {
111- const handledRequests = new WeakSet < http . ClientRequest > ( ) ;
112-
113- const handleOutgoingRequestFinishOnce = ( request : http . ClientRequest , response ?: http . IncomingMessage ) : void => {
114- if ( handledRequests . has ( request ) ) {
115- return ;
116- }
117-
118- handledRequests . add ( request ) ;
119- this . _onOutgoingRequestFinish ( request , response ) ;
120- } ;
111+ // We register handlers when either http or https is instrumented
112+ // but we only want to register them once, whichever is instrumented first
113+ let hasRegisteredHandlers = false ;
121114
122115 const onHttpServerRequestStart = ( ( _data : unknown ) => {
123116 const data = _data as { server : http . Server } ;
124117 this . _patchServerEmitOnce ( data . server ) ;
125118 } ) satisfies ChannelListener ;
126119
127- const onHttpsServerRequestStart = ( ( _data : unknown ) => {
128- const data = _data as { server : http . Server } ;
129- this . _patchServerEmitOnce ( data . server ) ;
130- } ) satisfies ChannelListener ;
131-
132120 const onHttpClientResponseFinish = ( ( _data : unknown ) => {
133121 const data = _data as { request : http . ClientRequest ; response : http . IncomingMessage } ;
134- handleOutgoingRequestFinishOnce ( data . request , data . response ) ;
122+ this . _onOutgoingRequestFinish ( data . request , data . response ) ;
135123 } ) satisfies ChannelListener ;
136124
137125 const onHttpClientRequestError = ( ( _data : unknown ) => {
138126 const data = _data as { request : http . ClientRequest } ;
139- handleOutgoingRequestFinishOnce ( data . request , undefined ) ;
127+ this . _onOutgoingRequestFinish ( data . request , undefined ) ;
140128 } ) satisfies ChannelListener ;
141129
130+ /**
131+ * You may be wondering why we register these diagnostics-channel listenrers in such InstrumentationNodeModuleDefinition,
132+ * instead of simply subscribing to the events once in here.
133+ * The reason for this is timing semantics: These functions are called once the http or https module is loaded.
134+ * If we'd subscribe before that, there seem to be conflicts with the OTEL native instrumentation in some scenarios,
135+ * especially the "import-on-top" pattern of setting up ESM applications.
136+ */
142137 return [
143138 new InstrumentationNodeModuleDefinition (
144139 'http' ,
145140 [ '*' ] ,
146141 ( moduleExports : Http ) : Http => {
142+ if ( hasRegisteredHandlers ) {
143+ return moduleExports ;
144+ }
145+
146+ hasRegisteredHandlers = true ;
147+
147148 subscribe ( 'http.server.request.start' , onHttpServerRequestStart ) ;
148149 subscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
149150
@@ -163,7 +164,13 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
163164 'https' ,
164165 [ '*' ] ,
165166 ( moduleExports : Https ) : Https => {
166- subscribe ( 'http.server.request.start' , onHttpsServerRequestStart ) ;
167+ if ( hasRegisteredHandlers ) {
168+ return moduleExports ;
169+ }
170+
171+ hasRegisteredHandlers = true ;
172+
173+ subscribe ( 'http.server.request.start' , onHttpServerRequestStart ) ;
167174 subscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
168175
169176 // When an error happens, we still want to have a breadcrumb
@@ -173,7 +180,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
173180 return moduleExports ;
174181 } ,
175182 ( ) => {
176- unsubscribe ( 'http.server.request.start' , onHttpsServerRequestStart ) ;
183+ unsubscribe ( 'http.server.request.start' , onHttpServerRequestStart ) ;
177184 unsubscribe ( 'http.client.response.finish' , onHttpClientResponseFinish ) ;
178185 unsubscribe ( 'http.client.request.error' , onHttpClientRequestError ) ;
179186 } ,
0 commit comments