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' ;
27
38import {
49 createRoutes ,
@@ -35,20 +40,26 @@ function wrapExpressRequestHandler(
3540 res : ExpressResponse ,
3641 next : ExpressNextFunction ,
3742 ) : Promise < void > {
38- const request = extractRequestData ( req ) ;
43+ // eslint-disable-next-line @typescript-eslint/unbound-method
44+ res . end = wrapEndMethod ( res . end ) ;
3945
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 ) ;
4349
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 ( ) ;
4554
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+ }
4758
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+ } ) ;
5263 } ;
5364}
5465
@@ -57,11 +68,73 @@ function wrapExpressRequestHandler(
5768 */
5869export function wrapExpressCreateRequestHandler (
5970 origCreateRequestHandler : ExpressCreateRequestHandler ,
71+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6072) : ( options : any ) => ExpressRequestHandler {
73+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6174 return function ( this : unknown , options : any ) : ExpressRequestHandler {
6275 const newBuild = instrumentBuild ( ( options as ExpressCreateRequestHandlerOptions ) . build ) ;
6376 const requestHandler = origCreateRequestHandler . call ( this , { ...options , build : newBuild } ) ;
6477
6578 return wrapExpressRequestHandler ( requestHandler , newBuild ) ;
6679 } ;
6780}
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