1
- /* eslint-disable max-lines */
1
+ import { subscribe } from 'node:diagnostics_channel' ;
2
2
import type * as http from 'node:http' ;
3
- import type { IncomingMessage , RequestOptions } from 'node:http' ;
4
- import type * as https from 'node:https' ;
5
3
import type { EventEmitter } from 'node:stream' ;
6
4
import { context , propagation } from '@opentelemetry/api' ;
7
5
import { VERSION } from '@opentelemetry/core' ;
8
6
import type { InstrumentationConfig } from '@opentelemetry/instrumentation' ;
9
- import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
7
+ import { InstrumentationBase } from '@opentelemetry/instrumentation' ;
10
8
import type { AggregationCounts , Client , SanitizedRequestData , Scope } from '@sentry/core' ;
11
9
import {
12
10
addBreadcrumb ,
11
+ addNonEnumerableProperty ,
13
12
generateSpanId ,
14
13
getBreadcrumbLogLevelFromHttpStatusCode ,
15
14
getClient ,
@@ -24,11 +23,6 @@ import {
24
23
} from '@sentry/core' ;
25
24
import { DEBUG_BUILD } from '../../debug-build' ;
26
25
import { getRequestUrl } from '../../utils/getRequestUrl' ;
27
- import { stealthWrap } from './utils' ;
28
- import { getRequestInfo } from './vendor/getRequestInfo' ;
29
-
30
- type Http = typeof http ;
31
- type Https = typeof https ;
32
26
33
27
const INSTRUMENTATION_NAME = '@sentry/instrumentation-http' ;
34
28
@@ -58,7 +52,7 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
58
52
* @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request.
59
53
* @param request Contains the {@type RequestOptions} object used to make the outgoing request.
60
54
*/
61
- ignoreOutgoingRequests ?: ( url : string , request : RequestOptions ) => boolean ;
55
+ ignoreOutgoingRequests ?: ( url : string , request : http . RequestOptions ) => boolean ;
62
56
63
57
/**
64
58
* Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
@@ -67,7 +61,7 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
67
61
* @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request.
68
62
* @param request Contains the {@type RequestOptions} object used to make the outgoing request.
69
63
*/
70
- ignoreIncomingRequestBody ?: ( url : string , request : RequestOptions ) => boolean ;
64
+ ignoreIncomingRequestBody ?: ( url : string , request : http . RequestOptions ) => boolean ;
71
65
72
66
/**
73
67
* Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry.
@@ -107,72 +101,65 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
107
101
}
108
102
109
103
/** @inheritdoc */
110
- public init ( ) : [ InstrumentationNodeModuleDefinition , InstrumentationNodeModuleDefinition ] {
111
- return [ this . _getHttpsInstrumentation ( ) , this . _getHttpInstrumentation ( ) ] ;
112
- }
104
+ public init ( ) : [ ] {
105
+ subscribe ( 'http.server.request.start' , data => {
106
+ const server = ( data as { server : http . Server } ) . server ;
107
+ this . _patchServerEmit ( server ) ;
108
+ } ) ;
113
109
114
- /** Get the instrumentation for the http module. */
115
- private _getHttpInstrumentation ( ) : InstrumentationNodeModuleDefinition {
116
- return new InstrumentationNodeModuleDefinition (
117
- 'http' ,
118
- [ '*' ] ,
119
- ( moduleExports : Http ) : Http => {
120
- // Patch incoming requests for request isolation
121
- stealthWrap ( moduleExports . Server . prototype , 'emit' , this . _getPatchIncomingRequestFunction ( ) ) ;
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 ;
122
113
123
- // Patch outgoing requests for breadcrumbs
124
- const patchedRequest = stealthWrap ( moduleExports , 'request' , this . _getPatchOutgoingRequestFunction ( ) ) ;
125
- stealthWrap ( moduleExports , 'get' , this . _getPatchOutgoingGetFunction ( patchedRequest ) ) ;
114
+ this . _onOutgoingRequestFinish ( request , response ) ;
115
+ } ) ;
126
116
127
- return moduleExports ;
128
- } ,
129
- ( ) => {
130
- // no unwrap here
131
- } ,
132
- ) ;
117
+ return [ ] ;
133
118
}
134
119
135
- /** Get the instrumentation for the https module. */
136
- private _getHttpsInstrumentation ( ) : InstrumentationNodeModuleDefinition {
137
- return new InstrumentationNodeModuleDefinition (
138
- 'https' ,
139
- [ '*' ] ,
140
- ( moduleExports : Https ) : Https => {
141
- // Patch incoming requests for request isolation
142
- stealthWrap ( moduleExports . Server . prototype , 'emit' , this . _getPatchIncomingRequestFunction ( ) ) ;
120
+ /**
121
+ * This is triggered when an outgoing request finishes.
122
+ * It has access to the final request and response objects.
123
+ */
124
+ private _onOutgoingRequestFinish ( request : http . ClientRequest , response : http . IncomingMessage ) : void {
125
+ const _breadcrumbs = this . getConfig ( ) . breadcrumbs ;
126
+ const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs ;
127
+ const options = getRequestOptions ( request ) ;
143
128
144
- // Patch outgoing requests for breadcrumbs
145
- const patchedRequest = stealthWrap ( moduleExports , 'request' , this . _getPatchOutgoingRequestFunction ( ) ) ;
146
- stealthWrap ( moduleExports , 'get' , this . _getPatchOutgoingGetFunction ( patchedRequest ) ) ;
129
+ const _ignoreOutgoingRequests = this . getConfig ( ) . ignoreOutgoingRequests ;
130
+ const shouldCreateBreadcrumb =
131
+ typeof _ignoreOutgoingRequests === 'function' ? ! _ignoreOutgoingRequests ( getRequestUrl ( request ) , options ) : true ;
147
132
148
- return moduleExports ;
149
- } ,
150
- ( ) => {
151
- // no unwrap here
152
- } ,
153
- ) ;
133
+ if ( breadCrumbsEnabled && shouldCreateBreadcrumb ) {
134
+ addRequestBreadcrumb ( request , response ) ;
135
+ }
154
136
}
155
137
156
138
/**
157
- * Patch the incoming request function for request isolation.
139
+ * Patch a server.emit function to handle process isolation for incoming requests.
140
+ * This will only patch the emit function if it was not already patched.
158
141
*/
159
- private _getPatchIncomingRequestFunction ( ) : (
160
- original : ( event : string , ...args : unknown [ ] ) => boolean ,
161
- ) => ( this : unknown , event : string , ...args : unknown [ ] ) => boolean {
142
+ private _patchServerEmit ( server : http . Server ) : void {
143
+ // eslint-disable-next-line @typescript-eslint/unbound-method
144
+ const originalEmit = server . emit ;
145
+
146
+ // This means it was already patched, do nothing
147
+ if ( ( originalEmit as { __sentry_patched__ ?: boolean } ) . __sentry_patched__ ) {
148
+ return ;
149
+ }
150
+
162
151
// eslint-disable-next-line @typescript-eslint/no-this-alias
163
152
const instrumentation = this ;
164
153
const { ignoreIncomingRequestBody } = instrumentation . getConfig ( ) ;
165
154
166
- return (
167
- original : ( event : string , ...args : unknown [ ] ) => boolean ,
168
- ) : ( ( this : unknown , event : string , ...args : unknown [ ] ) => boolean ) => {
169
- return function incomingRequest ( this : unknown , ...args : [ event : string , ...args : unknown [ ] ] ) : boolean {
155
+ const newEmit = new Proxy ( originalEmit , {
156
+ apply ( target , thisArg , args : [ event : string , ...args : unknown [ ] ] ) {
170
157
// Only traces request events
171
158
if ( args [ 0 ] !== 'request' ) {
172
- return original . apply ( this , args ) ;
159
+ return target . apply ( thisArg , args ) ;
173
160
}
174
161
175
- instrumentation . _diag . debug ( 'http instrumentation for incoming request' ) ;
162
+ DEBUG_BUILD && logger . log ( 'http instrumentation for incoming request' ) ;
176
163
177
164
const isolationScope = getIsolationScope ( ) . clone ( ) ;
178
165
const request = args [ 1 ] as http . IncomingMessage ;
@@ -217,89 +204,20 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
217
204
218
205
// If we don't want to extract the trace from the header, we can skip this
219
206
if ( ! instrumentation . getConfig ( ) . extractIncomingTraceFromHeader ) {
220
- return original . apply ( this , args ) ;
207
+ return target . apply ( thisArg , args ) ;
221
208
}
222
209
223
210
const ctx = propagation . extract ( context . active ( ) , normalizedRequest . headers ) ;
224
211
return context . with ( ctx , ( ) => {
225
- return original . apply ( this , args ) ;
212
+ return target . apply ( thisArg , args ) ;
226
213
} ) ;
227
214
} ) ;
228
- } ;
229
- } ;
230
- }
231
-
232
- /**
233
- * Patch the outgoing request function for breadcrumbs.
234
- */
235
- private _getPatchOutgoingRequestFunction ( ) : (
236
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
237
- original : ( ...args : any [ ] ) => http . ClientRequest ,
238
- ) => ( options : URL | http . RequestOptions | string , ...args : unknown [ ] ) => http . ClientRequest {
239
- // eslint-disable-next-line @typescript-eslint/no-this-alias
240
- const instrumentation = this ;
241
-
242
- return ( original : ( ...args : unknown [ ] ) => http . ClientRequest ) : ( ( ...args : unknown [ ] ) => http . ClientRequest ) => {
243
- return function outgoingRequest ( this : unknown , ...args : unknown [ ] ) : http . ClientRequest {
244
- instrumentation . _diag . debug ( 'http instrumentation for outgoing requests' ) ;
245
-
246
- // Making a copy to avoid mutating the original args array
247
- // We need to access and reconstruct the request options object passed to `ignoreOutgoingRequests`
248
- // so that it matches what Otel instrumentation passes to `ignoreOutgoingRequestHook`.
249
- // @see https://github.com/open-telemetry/opentelemetry-js/blob/7293e69c1e55ca62e15d0724d22605e61bd58952/experimental/packages/opentelemetry-instrumentation-http/src/http.ts#L756-L789
250
- const argsCopy = [ ...args ] ;
251
-
252
- const options = argsCopy . shift ( ) as URL | http . RequestOptions | string ;
253
-
254
- const extraOptions =
255
- typeof argsCopy [ 0 ] === 'object' && ( typeof options === 'string' || options instanceof URL )
256
- ? ( argsCopy . shift ( ) as http . RequestOptions )
257
- : undefined ;
258
-
259
- const { optionsParsed } = getRequestInfo ( instrumentation . _diag , options , extraOptions ) ;
260
-
261
- const request = original . apply ( this , args ) as ReturnType < typeof http . request > ;
262
-
263
- request . prependListener ( 'response' , ( response : http . IncomingMessage ) => {
264
- const _breadcrumbs = instrumentation . getConfig ( ) . breadcrumbs ;
265
- const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs ;
266
-
267
- const _ignoreOutgoingRequests = instrumentation . getConfig ( ) . ignoreOutgoingRequests ;
268
- const shouldCreateBreadcrumb =
269
- typeof _ignoreOutgoingRequests === 'function'
270
- ? ! _ignoreOutgoingRequests ( getRequestUrl ( request ) , optionsParsed )
271
- : true ;
272
-
273
- if ( breadCrumbsEnabled && shouldCreateBreadcrumb ) {
274
- addRequestBreadcrumb ( request , response ) ;
275
- }
276
- } ) ;
215
+ } ,
216
+ } ) ;
277
217
278
- return request ;
279
- } ;
280
- } ;
281
- }
218
+ addNonEnumerableProperty ( newEmit , '__sentry_patched__' , true ) ;
282
219
283
- /** Path the outgoing get function for breadcrumbs. */
284
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
- private _getPatchOutgoingGetFunction ( clientRequest : ( ...args : any [ ] ) => http . ClientRequest ) {
286
- return ( _original : unknown ) : ( ( ...args : unknown [ ] ) => http . ClientRequest ) => {
287
- // Re-implement http.get. This needs to be done (instead of using
288
- // getPatchOutgoingRequestFunction to patch it) because we need to
289
- // set the trace context header before the returned http.ClientRequest is
290
- // ended. The Node.js docs state that the only differences between
291
- // request and get are that (1) get defaults to the HTTP GET method and
292
- // (2) the returned request object is ended immediately. The former is
293
- // already true (at least in supported Node versions up to v10), so we
294
- // simply follow the latter. Ref:
295
- // https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback
296
- // https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-http.ts#L198
297
- return function outgoingGetRequest ( ...args : unknown [ ] ) : http . ClientRequest {
298
- const req = clientRequest ( ...args ) ;
299
- req . end ( ) ;
300
- return req ;
301
- } ;
302
- } ;
220
+ server . emit = newEmit ;
303
221
}
304
222
}
305
223
@@ -359,7 +277,7 @@ function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedReques
359
277
* we monkey patch `req.on('data')` to intercept the body chunks.
360
278
* This way, we only read the body if the user also consumes the body, ensuring we do not change any behavior in unexpected ways.
361
279
*/
362
- function patchRequestToCaptureBody ( req : IncomingMessage , isolationScope : Scope ) : void {
280
+ function patchRequestToCaptureBody ( req : http . IncomingMessage , isolationScope : Scope ) : void {
363
281
let bodyByteLength = 0 ;
364
282
const chunks : Buffer [ ] = [ ] ;
365
283
@@ -451,6 +369,17 @@ function patchRequestToCaptureBody(req: IncomingMessage, isolationScope: Scope):
451
369
}
452
370
}
453
371
372
+ function getRequestOptions ( request : http . ClientRequest ) : http . RequestOptions {
373
+ return {
374
+ method : request . method ,
375
+ protocol : request . protocol ,
376
+ host : request . host ,
377
+ hostname : request . host ,
378
+ path : request . path ,
379
+ headers : request . getHeaders ( ) ,
380
+ } ;
381
+ }
382
+
454
383
/**
455
384
* Starts a session and tracks it in the context of a given isolation scope.
456
385
* When the passed response is finished, the session is put into a task and is
0 commit comments