1
- import { extractRequestData , loadModule } from '@sentry/utils' ;
1
+ import { getCurrentHub } from '@sentry/hub' ;
2
+ import { flush } from '@sentry/node' ;
3
+ import { hasTracingEnabled } from '@sentry/tracing' ;
4
+ import { Transaction } from '@sentry/types' ;
5
+ import { extractRequestData , loadModule , logger } from '@sentry/utils' ;
6
+ import * as domain from 'domain' ;
2
7
3
8
import {
4
9
createRoutes ,
@@ -35,20 +40,26 @@ function wrapExpressRequestHandler(
35
40
res : ExpressResponse ,
36
41
next : ExpressNextFunction ,
37
42
) : Promise < void > {
38
- const request = extractRequestData ( req ) ;
43
+ // eslint-disable-next-line @typescript-eslint/unbound-method
44
+ res . end = wrapEndMethod ( res . end ) ;
39
45
40
- if ( ! request . url || ! request . method ) {
41
- return origRequestHandler . call ( this , req , res , next ) ;
42
- }
46
+ const local = domain . create ( ) ;
47
+ local . add ( req ) ;
48
+ local . add ( res ) ;
43
49
44
- const url = new URL ( request . url ) ;
50
+ local . run ( async ( ) => {
51
+ const request = extractRequestData ( req ) ;
52
+ const hub = getCurrentHub ( ) ;
53
+ const options = hub . getClient ( ) ?. getOptions ( ) ;
45
54
46
- const transaction = startRequestHandlerTransaction ( url , request . method , routes , pkg ) ;
55
+ if ( ! options || ! hasTracingEnabled ( options ) || ! request . url || ! request . method ) {
56
+ return origRequestHandler . call ( this , req , res , next ) ;
57
+ }
47
58
48
- await origRequestHandler . call ( this , req , res , next ) ;
49
-
50
- transaction ?. setHttpStatus ( res . statusCode ) ;
51
- transaction ?. finish ( ) ;
59
+ const url = new URL ( request . url ) ;
60
+ startRequestHandlerTransaction ( url , request . method , routes , hub , pkg ) ;
61
+ await origRequestHandler . call ( this , req , res , next ) ;
62
+ } ) ;
52
63
} ;
53
64
}
54
65
@@ -57,11 +68,73 @@ function wrapExpressRequestHandler(
57
68
*/
58
69
export function wrapExpressCreateRequestHandler (
59
70
origCreateRequestHandler : ExpressCreateRequestHandler ,
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
72
) : ( options : any ) => ExpressRequestHandler {
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
74
return function ( this : unknown , options : any ) : ExpressRequestHandler {
62
75
const newBuild = instrumentBuild ( ( options as ExpressCreateRequestHandlerOptions ) . build ) ;
63
76
const requestHandler = origCreateRequestHandler . call ( this , { ...options , build : newBuild } ) ;
64
77
65
78
return wrapExpressRequestHandler ( requestHandler , newBuild ) ;
66
79
} ;
67
80
}
81
+
82
+ export type AugmentedExpressResponse = ExpressResponse & {
83
+ __sentryTransaction ?: Transaction ;
84
+ } ;
85
+
86
+ type ResponseEndMethod = AugmentedExpressResponse [ 'end' ] ;
87
+ type WrappedResponseEndMethod = AugmentedExpressResponse [ 'end' ] ;
88
+
89
+ /**
90
+ * Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish.
91
+ *
92
+ * Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
93
+ * things in the right order, in this case it's safe, because the native `.end()` actually *is* async, and its run
94
+ * actually *is* awaited, just manually so (which reflects the fact that the core of the request/response code in Node
95
+ * by far predates the introduction of `async`/`await`). When `.end()` is done, it emits the `prefinish` event, and
96
+ * only once that fires does request processing continue. See
97
+ * https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7.
98
+ *
99
+ * @param origEnd The original `res.end()` method
100
+ * @returns The wrapped version
101
+ */
102
+ function wrapEndMethod ( origEnd : ResponseEndMethod ) : WrappedResponseEndMethod {
103
+ return async function newEnd ( this : AugmentedExpressResponse , ...args : unknown [ ] ) {
104
+ await finishSentryProcessing ( this ) ;
105
+
106
+ return origEnd . call ( this , ...args ) ;
107
+ } ;
108
+ }
109
+
110
+ /**
111
+ * Close the open transaction (if any) and flush events to Sentry.
112
+ *
113
+ * @param res The outgoing response for this request, on which the transaction is stored
114
+ */
115
+ async function finishSentryProcessing ( res : AugmentedExpressResponse ) : Promise < void > {
116
+ const { __sentryTransaction : transaction } = res ;
117
+
118
+ if ( transaction ) {
119
+ transaction . setHttpStatus ( res . statusCode ) ;
120
+
121
+ // Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
122
+ // transaction closes, and make sure to wait until that's done before flushing events
123
+ await new Promise ( resolve => {
124
+ setImmediate ( ( ) => {
125
+ transaction . finish ( ) ;
126
+ resolve ( ) ;
127
+ } ) ;
128
+ } ) ;
129
+ }
130
+
131
+ // Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
132
+ // ends. If there was an error, rethrow it so that the normal exception-handling mechanisms can apply.
133
+ try {
134
+ __DEBUG_BUILD__ && logger . log ( 'Flushing events...' ) ;
135
+ await flush ( 2000 ) ;
136
+ __DEBUG_BUILD__ && logger . log ( 'Done flushing events' ) ;
137
+ } catch ( e ) {
138
+ __DEBUG_BUILD__ && logger . log ( 'Error while flushing events:\n' , e ) ;
139
+ }
140
+ }
0 commit comments