1
1
import { captureException , flush , getCurrentHub , Handlers , startTransaction , withScope } from '@sentry/node' ;
2
+ // import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing';
2
3
import { extractTraceparentData , getActiveTransaction , hasTracingEnabled } from '@sentry/tracing' ;
3
4
import { addExceptionMechanism , isString , logger , stripUrlQueryAndFragment } from '@sentry/utils' ;
5
+ import { Scope } from '@sentry/hub' ;
4
6
import * as domain from 'domain' ;
5
7
import { NextApiHandler , NextApiResponse } from 'next' ;
6
8
@@ -11,43 +13,23 @@ const { parseRequest } = Handlers;
11
13
// purely for clarity
12
14
type WrappedNextApiHandler = NextApiHandler ;
13
15
16
+ type ScopedResponse = NextApiResponse & { __sentryScope ?: Scope } ;
17
+
14
18
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
15
19
export const withSentry = ( handler : NextApiHandler ) : WrappedNextApiHandler => {
16
20
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
17
21
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 ) ;
41
25
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
45
27
const local = domain . create ( ) ;
46
28
local . add ( req ) ;
47
29
local . add ( res ) ;
48
30
49
31
return local . bind ( async ( ) => {
50
- try {
32
+
51
33
const currentScope = getCurrentHub ( ) . getScope ( ) ;
52
34
53
35
if ( currentScope ) {
@@ -84,11 +66,16 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
84
66
{ request : req } ,
85
67
) ;
86
68
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 ;
87
73
}
88
74
}
89
-
75
+ try {
90
76
return await handler ( req , res ) ; // Call original handler
91
77
} catch ( e ) {
78
+ // TODO how do we know
92
79
withScope ( scope => {
93
80
scope . addEventProcessor ( event => {
94
81
addExceptionMechanism ( event , {
@@ -119,3 +106,48 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
119
106
} ) ( ) ;
120
107
} ;
121
108
} ;
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