11import { captureException , flush , getCurrentHub , Handlers , startTransaction , withScope } from '@sentry/node' ;
2+ // import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing';
23import { extractTraceparentData , getActiveTransaction , hasTracingEnabled } from '@sentry/tracing' ;
34import { addExceptionMechanism , isString , logger , stripUrlQueryAndFragment } from '@sentry/utils' ;
5+ import { Scope } from '@sentry/hub' ;
46import * as domain from 'domain' ;
57import { NextApiHandler , NextApiResponse } from 'next' ;
68
@@ -11,43 +13,23 @@ const { parseRequest } = Handlers;
1113// purely for clarity
1214type WrappedNextApiHandler = NextApiHandler ;
1315
16+ type ScopedResponse = NextApiResponse & { __sentryScope ?: Scope } ;
17+
1418// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1519export const withSentry = ( handler : NextApiHandler ) : WrappedNextApiHandler => {
1620 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1721 return async ( req , res ) => {
18- const origEnd = res . end ;
19-
20- async function newEnd ( this : NextApiResponse , ...args : any [ ] ) {
21- const transaction = getActiveTransaction ( ) ;
22- console . log ( 'Active transaction:' , transaction ?. name ) ;
23-
24- if ( transaction ) {
25- transaction . setHttpStatus ( res . statusCode ) ;
26-
27- transaction . finish ( ) ;
28- }
29- try {
30- logger . log ( 'Flushing events...' ) ;
31- await flush ( 2000 ) ;
32- } catch ( e ) {
33- logger . log ( `Error while flushing events:\n${ e } ` ) ;
34- } finally {
35- logger . log ( 'Done flushing events' ) ;
36- // res.end();
37- }
38-
39- return origEnd . call ( this , ...args ) ;
40- }
22+ // first order of business: monkeypatch `res.end()` so that it will wait for us to send events to sentry before it
23+ // fires (if we don't do this, the lambda will close too early and events will be either delayed or lost)
24+ res . end = wrapEndMethod ( res . end ) ;
4125
42- res . end = newEnd ;
43-
44- // wrap everything in a domain in order to prevent scope bleed between requests
26+ // use a domain in order to prevent scope bleed between requests
4527 const local = domain . create ( ) ;
4628 local . add ( req ) ;
4729 local . add ( res ) ;
4830
4931 return local . bind ( async ( ) => {
50- try {
32+
5133 const currentScope = getCurrentHub ( ) . getScope ( ) ;
5234
5335 if ( currentScope ) {
@@ -84,11 +66,16 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
8466 { request : req } ,
8567 ) ;
8668 currentScope . setSpan ( transaction ) ;
69+
70+ // save a link to the transaction on the response, so that even if there's an error (landing us outside of
71+ // the domain), we can still finish the transaction and attach the correct data to it
72+ ( res as ScopedResponse ) . __sentryScope = currentScope ;
8773 }
8874 }
89-
75+ try {
9076 return await handler ( req , res ) ; // Call original handler
9177 } catch ( e ) {
78+ // TODO how do we know
9279 withScope ( scope => {
9380 scope . addEventProcessor ( event => {
9481 addExceptionMechanism ( event , {
@@ -119,3 +106,48 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
119106 } ) ( ) ;
120107 } ;
121108} ;
109+
110+ type ResponseEndMethod = ScopedResponse [ 'end' ] ;
111+ type WrappedResponseEndMethod = ScopedResponse [ 'end' ] ;
112+
113+ function wrapEndMethod ( origEnd : ResponseEndMethod ) : WrappedResponseEndMethod {
114+ return async function newEnd ( this : ScopedResponse , ...args : any [ ] ) {
115+ // if the handler errored, it will have popped us out of the domain, so push the domain's scope onto the stack
116+ // just in case (if we *are* still in the domain, this will replace the current scope with a clone of itself,
117+ // which is effectively a no-op as long as we remember to pop it off when we're done)
118+ const currentHub = getCurrentHub ( ) ;
119+ currentHub . pushScope ( this . __sentryScope ) ;
120+
121+ const transaction = getActiveTransaction ( ) ;
122+ console . log ( 'Active transaction:' , transaction ?. name ) ;
123+
124+ if ( transaction ) {
125+ transaction . setHttpStatus ( this . statusCode ) ;
126+
127+ // Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
128+ // transaction closes, and make sure to wait until that's done before flushing events
129+ const transactionFinished : Promise < void > = new Promise ( resolve => {
130+ setImmediate ( ( ) => {
131+ transaction . finish ( ) ;
132+ resolve ( ) ;
133+ } ) ;
134+ } ) ;
135+ await transactionFinished ;
136+ }
137+
138+ // flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
139+ // ends
140+ try {
141+ logger . log ( 'Flushing events...' ) ;
142+ await flush ( 2000 ) ;
143+ } catch ( e ) {
144+ logger . log ( `Error while flushing events:\n${ e } ` ) ;
145+ } finally {
146+ logger . log ( 'Done flushing events' ) ;
147+ }
148+
149+ // now that our work is done, we can pop off the scope and allow the response to end
150+ currentHub . popScope ( ) ;
151+ return origEnd . call ( this , ...args ) ;
152+ } ;
153+ }
0 commit comments