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' ;
2
4
import type * as http from 'node:http' ;
5
+ import type * as https from 'node:https' ;
3
6
import type { EventEmitter } from 'node:stream' ;
4
7
import { context , propagation } from '@opentelemetry/api' ;
5
8
import { VERSION } from '@opentelemetry/core' ;
6
9
import type { InstrumentationConfig } from '@opentelemetry/instrumentation' ;
7
- import { InstrumentationBase } from '@opentelemetry/instrumentation' ;
10
+ import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
8
11
import type { AggregationCounts , Client , SanitizedRequestData , Scope } from '@sentry/core' ;
9
12
import {
10
13
addBreadcrumb ,
@@ -26,6 +29,9 @@ import { getRequestUrl } from '../../utils/getRequestUrl';
26
29
27
30
const INSTRUMENTATION_NAME = '@sentry/instrumentation-http' ;
28
31
32
+ type Http = typeof http ;
33
+ type Https = typeof https ;
34
+
29
35
export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
30
36
/**
31
37
* Whether breadcrumbs should be recorded for requests.
@@ -101,29 +107,80 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
101
107
}
102
108
103
109
/** @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
113
112
114
- this . _onOutgoingRequestFinish ( request , response ) ;
115
- } ) ;
113
+ const handledRequests = new WeakSet < http . ClientRequest > ( ) ;
116
114
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
+ }
122
119
123
- this . _onOutgoingRequestFinish ( request , undefined ) ;
124
- } ) ;
120
+ handledRequests . add ( request ) ;
121
+ this . _onOutgoingRequestFinish ( request , response ) ;
122
+ } ;
125
123
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
+ ] ;
127
184
}
128
185
129
186
/**
@@ -150,7 +207,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
150
207
* Patch a server.emit function to handle process isolation for incoming requests.
151
208
* This will only patch the emit function if it was not already patched.
152
209
*/
153
- private _patchServerEmit ( server : http . Server ) : void {
210
+ private _patchServerEmitOnce ( server : http . Server ) : void {
154
211
// eslint-disable-next-line @typescript-eslint/unbound-method
155
212
const originalEmit = server . emit ;
156
213
@@ -159,7 +216,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
159
216
return ;
160
217
}
161
218
162
- DEBUG_BUILD && logger . log ( INSTRUMENTATION_NAME , 'Patching server.emit' ) ;
219
+ DEBUG_BUILD && logger . log ( INSTRUMENTATION_NAME , 'Patching server.emit!!! ' ) ;
163
220
164
221
// eslint-disable-next-line @typescript-eslint/no-this-alias
165
222
const instrumentation = this ;
0 commit comments