1
- import { getCurrentHub } from '@sentry/core' ;
1
+ import { getCurrentHub , Span } from '@sentry/core' ;
2
2
import { Integration } from '@sentry/types' ;
3
3
import { fill } from '@sentry/utils' ;
4
4
import * as http from 'http' ;
5
- import * as util from 'util' ;
6
-
7
- let lastResponse : http . ServerResponse | undefined ;
8
-
9
- /**
10
- * Request interface which can carry around unified url
11
- * independently of used framework
12
- */
13
- interface SentryRequest extends http . IncomingMessage {
14
- __ravenBreadcrumbUrl ?: string ;
15
- }
5
+ import * as https from 'https' ;
16
6
17
7
/** http module integration */
18
8
export class Http implements Integration {
@@ -25,19 +15,118 @@ export class Http implements Integration {
25
15
*/
26
16
public static id : string = 'Http' ;
27
17
18
+ /**
19
+ * @inheritDoc
20
+ */
21
+ private readonly _breadcrumbs : boolean ;
22
+
23
+ /**
24
+ * @inheritDoc
25
+ */
26
+ private readonly _tracing : boolean ;
27
+
28
+ /**
29
+ * @inheritDoc
30
+ */
31
+ public constructor ( options : { breadcrumbs ?: boolean ; tracing ?: boolean } = { } ) {
32
+ this . _breadcrumbs = typeof options . breadcrumbs === 'undefined' ? true : options . breadcrumbs ;
33
+ this . _tracing = typeof options . tracing === 'undefined' ? false : options . tracing ;
34
+ }
35
+
28
36
/**
29
37
* @inheritDoc
30
38
*/
31
39
public setupOnce ( ) : void {
32
- const nativeModule = require ( 'module' ) ;
33
- fill ( nativeModule , '_load' , loadWrapper ( nativeModule ) ) ;
34
- // observation: when the https module does its own require('http'), it *does not* hit our hooked require to instrument http on the fly
35
- // but if we've previously instrumented http, https *does* get our already-instrumented version
36
- // this is because raven's transports are required before this instrumentation takes place, which loads https (and http)
37
- // so module cache will have uninstrumented http; proactively loading it here ensures instrumented version is in module cache
38
- // alternatively we could refactor to load our transports later, but this is easier and doesn't have much drawback
39
- require ( 'http' ) ;
40
+ // No need to instrument if we don't want to track anything
41
+ if ( ! this . _breadcrumbs && ! this . _tracing ) {
42
+ return ;
43
+ }
44
+
45
+ const handlerWrapper = createHandlerWrapper ( this . _breadcrumbs , this . _tracing ) ;
46
+
47
+ const httpModule = require ( 'http' ) ;
48
+ fill ( httpModule , 'get' , handlerWrapper ) ;
49
+ fill ( httpModule , 'request' , handlerWrapper ) ;
50
+
51
+ const httpsModule = require ( 'https' ) ;
52
+ fill ( httpsModule , 'get' , handlerWrapper ) ;
53
+ fill ( httpsModule , 'request' , handlerWrapper ) ;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Wrapper function for internal `request` and `get` calls within `http` and `https` modules
59
+ */
60
+ function createHandlerWrapper (
61
+ breadcrumbsEnabled : boolean ,
62
+ tracingEnabled : boolean ,
63
+ ) : ( originalHandler : ( ) => http . ClientRequest ) => ( options : string | http . ClientRequestArgs ) => http . ClientRequest {
64
+ return function handlerWrapper (
65
+ originalHandler : ( ) => http . ClientRequest ,
66
+ ) : ( options : string | http . ClientRequestArgs ) => http . ClientRequest {
67
+ return function ( this : typeof http | typeof https , options : string | http . ClientRequestArgs ) : http . ClientRequest {
68
+ const requestUrl = extractUrl ( options ) ;
69
+
70
+ if ( isSentryRequest ( requestUrl ) ) {
71
+ return originalHandler . apply ( this , arguments ) ;
72
+ }
73
+
74
+ let span : Span ;
75
+ if ( tracingEnabled ) {
76
+ span = getCurrentHub ( ) . startSpan ( {
77
+ description : `${ typeof options === 'string' ? 'GET' : options . method } |${ requestUrl } ` ,
78
+ op : 'request' ,
79
+ } ) ;
80
+ }
81
+
82
+ return originalHandler
83
+ . apply ( this , arguments )
84
+ . once ( 'response' , function ( this : http . IncomingMessage , res : http . ServerResponse ) : void {
85
+ if ( breadcrumbsEnabled ) {
86
+ addRequestBreadcrumb ( 'response' , requestUrl , this , res ) ;
87
+ }
88
+ if ( tracingEnabled ) {
89
+ span . setSuccess ( ) ;
90
+ span . finish ( ) ;
91
+ }
92
+ } )
93
+ . once ( 'error' , function ( this : http . IncomingMessage ) : void {
94
+ if ( breadcrumbsEnabled ) {
95
+ addRequestBreadcrumb ( 'error' , requestUrl , this ) ;
96
+ }
97
+ if ( tracingEnabled ) {
98
+ span . setFailure ( ) ;
99
+ span . finish ( ) ;
100
+ }
101
+ } ) ;
102
+ } ;
103
+ } ;
104
+ }
105
+
106
+ /**
107
+ * Captures Breadcrumb based on provided request/response pair
108
+ */
109
+ function addRequestBreadcrumb ( event : string , url : string , req : http . IncomingMessage , res ?: http . ServerResponse ) : void {
110
+ if ( ! getCurrentHub ( ) . getIntegration ( Http ) ) {
111
+ return ;
40
112
}
113
+
114
+ getCurrentHub ( ) . addBreadcrumb (
115
+ {
116
+ category : 'http' ,
117
+ data : {
118
+ method : req . method ,
119
+ status_code : res && res . statusCode ,
120
+ url,
121
+ } ,
122
+ type : 'http' ,
123
+ } ,
124
+ {
125
+ event,
126
+ request : req ,
127
+ response : res ,
128
+ } ,
129
+ ) ;
41
130
}
42
131
43
132
/**
@@ -46,10 +135,7 @@ export class Http implements Integration {
46
135
* @param options url that should be returned or an object containing it's parts.
47
136
* @returns constructed url
48
137
*/
49
- function createBreadcrumbUrl ( options : string | http . ClientRequestArgs ) : string {
50
- // We could just always reconstruct this from this.agent, this._headers, this.path, etc
51
- // but certain other http-instrumenting libraries (like nock, which we use for tests) fail to
52
- // maintain the guarantee that after calling origClientRequest, those fields will be populated
138
+ function extractUrl ( options : string | http . ClientRequestArgs ) : string {
53
139
if ( typeof options === 'string' ) {
54
140
return options ;
55
141
}
@@ -62,108 +148,19 @@ function createBreadcrumbUrl(options: string | http.ClientRequestArgs): string {
62
148
}
63
149
64
150
/**
65
- * Wrapper function for internal _load calls within `require`
66
- */
67
- function loadWrapper ( nativeModule : any ) : any {
68
- // We need to use some functional-style currying to pass values around
69
- // as we cannot rely on `bind`, because this has to preserve correct
70
- // context for native calls
71
- return function ( originalLoad : ( ) => any ) : any {
72
- return function ( this : SentryRequest , moduleId : string ) : any {
73
- const originalModule = originalLoad . apply ( nativeModule , arguments ) ;
74
-
75
- if ( moduleId !== 'http' || originalModule . __sentry__ ) {
76
- return originalModule ;
77
- }
78
-
79
- const origClientRequest = originalModule . ClientRequest ;
80
- const clientRequest = function (
81
- this : SentryRequest ,
82
- options : http . ClientRequestArgs | string ,
83
- callback : ( ) => void ,
84
- ) : any {
85
- // Note: this won't capture a breadcrumb if a response never comes
86
- // It would be useful to know if that was the case, though, so
87
- // TODO: revisit to see if we can capture sth indicating response never came
88
- // possibility: capture one breadcrumb for "req sent" and one for "res recvd"
89
- // seems excessive but solves the problem and *is* strictly more information
90
- // could be useful for weird response sequencing bug scenarios
91
-
92
- origClientRequest . call ( this , options , callback ) ;
93
- this . __ravenBreadcrumbUrl = createBreadcrumbUrl ( options ) ;
94
- } ;
95
-
96
- util . inherits ( clientRequest , origClientRequest ) ;
97
-
98
- fill ( clientRequest . prototype , 'emit' , emitWrapper ) ;
99
-
100
- fill ( originalModule , 'ClientRequest' , function ( ) : any {
101
- return clientRequest ;
102
- } ) ;
103
-
104
- // http.request orig refs module-internal ClientRequest, not exported one, so
105
- // it still points at orig ClientRequest after our monkeypatch; these reimpls
106
- // just get that reference updated to use our new ClientRequest
107
- fill ( originalModule , 'request' , function ( ) : any {
108
- return function ( options : http . ClientRequestArgs , callback : ( ) => void ) : any {
109
- return new originalModule . ClientRequest ( options , callback ) as http . IncomingMessage ;
110
- } ;
111
- } ) ;
112
-
113
- fill ( originalModule , 'get' , function ( ) : any {
114
- return function ( options : http . ClientRequestArgs , callback : ( ) => void ) : any {
115
- const req = originalModule . request ( options , callback ) ;
116
- req . end ( ) ;
117
- return req ;
118
- } ;
119
- } ) ;
120
-
121
- originalModule . __sentry__ = true ;
122
- return originalModule ;
123
- } ;
124
- } ;
125
- }
126
-
127
- /**
128
- * Wrapper function for request's `emit` calls
151
+ * Checks whether given url points to Sentry server
152
+ * @param url url to verify
129
153
*/
130
- function emitWrapper ( origEmit : EventListener ) : ( event : string , response : http . ServerResponse ) => EventListener {
131
- return function ( this : SentryRequest , event : string , response : http . ServerResponse ) : any {
132
- // I'm not sure why but Node.js (at least in v8.X)
133
- // is emitting all events twice :|
134
- if ( lastResponse === undefined || lastResponse !== response ) {
135
- lastResponse = response ;
136
- } else {
137
- return origEmit . apply ( this , arguments ) ;
138
- }
154
+ function isSentryRequest ( url : string ) : boolean {
155
+ const client = getCurrentHub ( ) . getClient ( ) ;
156
+ if ( ! url || ! client ) {
157
+ return false ;
158
+ }
139
159
140
- const client = getCurrentHub ( ) . getClient ( ) ;
141
- if ( client ) {
142
- const dsn = client . getDsn ( ) ;
143
-
144
- const isInterestingEvent = event === 'response' || event === 'error' ;
145
- const isNotSentryRequest = dsn && this . __ravenBreadcrumbUrl && this . __ravenBreadcrumbUrl . indexOf ( dsn . host ) === - 1 ;
146
-
147
- if ( isInterestingEvent && isNotSentryRequest && getCurrentHub ( ) . getIntegration ( Http ) ) {
148
- getCurrentHub ( ) . addBreadcrumb (
149
- {
150
- category : 'http' ,
151
- data : {
152
- method : this . method ,
153
- status_code : response . statusCode ,
154
- url : this . __ravenBreadcrumbUrl ,
155
- } ,
156
- type : 'http' ,
157
- } ,
158
- {
159
- event,
160
- request : this ,
161
- response,
162
- } ,
163
- ) ;
164
- }
165
- }
160
+ const dsn = client . getDsn ( ) ;
161
+ if ( ! dsn ) {
162
+ return false ;
163
+ }
166
164
167
- return origEmit . apply ( this , arguments ) ;
168
- } ;
165
+ return url . indexOf ( dsn . host ) !== - 1 ;
169
166
}
0 commit comments