1
- import { captureException , flush , getCurrentHub , Handlers , startTransaction , withScope } from '@sentry/node' ;
2
- import { extractTraceparentData , getActiveTransaction , hasTracingEnabled } from '@sentry/tracing' ;
1
+ import { captureException , flush , getCurrentHub , Handlers , startTransaction } from '@sentry/node' ;
2
+ import { extractTraceparentData , hasTracingEnabled } from '@sentry/tracing' ;
3
+ import { Transaction } from '@sentry/types' ;
3
4
import { addExceptionMechanism , isString , logger , stripUrlQueryAndFragment } from '@sentry/utils' ;
4
5
import * as domain from 'domain' ;
5
- import { NextApiHandler } from 'next' ;
6
-
7
- import { addRequestDataToEvent , NextRequest } from './instrumentServer' ;
6
+ import { NextApiHandler , NextApiResponse } from 'next' ;
8
7
9
8
const { parseRequest } = Handlers ;
10
9
11
10
// purely for clarity
12
11
type WrappedNextApiHandler = NextApiHandler ;
13
12
13
+ type AugmentedResponse = NextApiResponse & { __sentryTransaction ?: Transaction } ;
14
+
14
15
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
15
16
export const withSentry = ( handler : NextApiHandler ) : WrappedNextApiHandler => {
16
17
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
17
18
return async ( req , res ) => {
18
- // wrap everything in a domain in order to prevent scope bleed between requests
19
+ // first order of business: monkeypatch `res.end()` so that it will wait for us to send events to sentry before it
20
+ // fires (if we don't do this, the lambda will close too early and events will be either delayed or lost)
21
+ // eslint-disable-next-line @typescript-eslint/unbound-method
22
+ res . end = wrapEndMethod ( res . end ) ;
23
+
24
+ // use a domain in order to prevent scope bleed between requests
19
25
const local = domain . create ( ) ;
20
26
local . add ( req ) ;
21
27
local . add ( res ) ;
@@ -24,73 +30,102 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
24
30
// return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on
25
31
// getting that before it will finish the response.
26
32
const boundHandler = local . bind ( async ( ) => {
27
- try {
28
- const currentScope = getCurrentHub ( ) . getScope ( ) ;
33
+ const currentScope = getCurrentHub ( ) . getScope ( ) ;
29
34
30
- if ( currentScope ) {
31
- currentScope . addEventProcessor ( event => addRequestDataToEvent ( event , req as NextRequest ) ) ;
32
-
33
- if ( hasTracingEnabled ( ) ) {
34
- // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
35
- let traceparentData ;
36
- if ( req . headers && isString ( req . headers [ 'sentry-trace' ] ) ) {
37
- traceparentData = extractTraceparentData ( req . headers [ 'sentry-trace' ] as string ) ;
38
- logger . log ( `[Tracing] Continuing trace ${ traceparentData ?. traceId } .` ) ;
39
- }
35
+ if ( currentScope ) {
36
+ currentScope . addEventProcessor ( event => parseRequest ( event , req ) ) ;
37
+
38
+ if ( hasTracingEnabled ( ) ) {
39
+ // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
40
+ let traceparentData ;
41
+ if ( req . headers && isString ( req . headers [ 'sentry-trace' ] ) ) {
42
+ traceparentData = extractTraceparentData ( req . headers [ 'sentry-trace' ] as string ) ;
43
+ logger . log ( `[Tracing] Continuing trace ${ traceparentData ?. traceId } .` ) ;
44
+ }
40
45
41
- const url = `${ req . url } ` ;
42
- // pull off query string, if any
43
- let reqPath = stripUrlQueryAndFragment ( url ) ;
44
- // Replace with placeholder
45
- if ( req . query ) {
46
- // TODO get this from next if possible, to avoid accidentally replacing non-dynamic parts of the path if
47
- // they match dynamic parts
48
- for ( const [ key , value ] of Object . entries ( req . query ) ) {
49
- reqPath = reqPath . replace ( `${ value } ` , `[${ key } ]` ) ;
50
- }
46
+ const url = `${ req . url } ` ;
47
+ // pull off query string, if any
48
+ let reqPath = stripUrlQueryAndFragment ( url ) ;
49
+ // Replace with placeholder
50
+ if ( req . query ) {
51
+ // TODO get this from next if possible, to avoid accidentally replacing non-dynamic parts of the path if
52
+ // they match dynamic parts
53
+ for ( const [ key , value ] of Object . entries ( req . query ) ) {
54
+ reqPath = reqPath . replace ( `${ value } ` , `[${ key } ]` ) ;
51
55
}
52
- const reqMethod = `${ ( req . method || 'GET' ) . toUpperCase ( ) } ` ;
53
-
54
- const transaction = startTransaction (
55
- {
56
- name : `${ reqMethod } ${ reqPath } ` ,
57
- op : 'http.server' ,
58
- ...traceparentData ,
59
- } ,
60
- // extra context passed to the `tracesSampler`
61
- { request : req } ,
62
- ) ;
63
- currentScope . setSpan ( transaction ) ;
64
56
}
57
+ const reqMethod = `${ ( req . method || 'GET' ) . toUpperCase ( ) } ` ;
58
+
59
+ const transaction = startTransaction (
60
+ {
61
+ name : `${ reqMethod } ${ reqPath } ` ,
62
+ op : 'http.server' ,
63
+ ...traceparentData ,
64
+ } ,
65
+ // extra context passed to the `tracesSampler`
66
+ { request : req } ,
67
+ ) ;
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 it (albeit possibly missing some scope data)
72
+ ( res as AugmentedResponse ) . __sentryTransaction = transaction ;
65
73
}
74
+ }
66
75
76
+ try {
67
77
return await handler ( req , res ) ; // Call original handler
68
78
} catch ( e ) {
69
- withScope ( scope => {
70
- scope . addEventProcessor ( event => {
79
+ if ( currentScope ) {
80
+ currentScope . addEventProcessor ( event => {
71
81
addExceptionMechanism ( event , {
72
82
handled : false ,
73
83
} ) ;
74
- return parseRequest ( event , req ) ;
84
+ return event ;
75
85
} ) ;
76
86
captureException ( e ) ;
77
- } ) ;
78
- throw e ;
79
- } finally {
80
- const transaction = getActiveTransaction ( ) ;
81
- if ( transaction ) {
82
- transaction . setHttpStatus ( res . statusCode ) ;
83
-
84
- transaction . finish ( ) ;
85
- }
86
- try {
87
- await flush ( 2000 ) ;
88
- } catch ( e ) {
89
- // no-empty
90
87
}
88
+ throw e ;
91
89
}
92
90
} ) ;
93
91
94
92
return await boundHandler ( ) ;
95
93
} ;
96
94
} ;
95
+
96
+ type ResponseEndMethod = AugmentedResponse [ 'end' ] ;
97
+ type WrappedResponseEndMethod = AugmentedResponse [ 'end' ] ;
98
+
99
+ function wrapEndMethod ( origEnd : ResponseEndMethod ) : WrappedResponseEndMethod {
100
+ return async function newEnd ( this : AugmentedResponse , ...args : unknown [ ] ) {
101
+ // TODO: if the handler errored, it will have popped us out of the domain, so all of our scope data will be missing
102
+
103
+ const transaction = this . __sentryTransaction ;
104
+
105
+ if ( transaction ) {
106
+ transaction . setHttpStatus ( this . statusCode ) ;
107
+
108
+ // Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
109
+ // transaction closes, and make sure to wait until that's done before flushing events
110
+ const transactionFinished : Promise < void > = new Promise ( resolve => {
111
+ setImmediate ( ( ) => {
112
+ transaction . finish ( ) ;
113
+ resolve ( ) ;
114
+ } ) ;
115
+ } ) ;
116
+ await transactionFinished ;
117
+ }
118
+
119
+ // flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
120
+ // ends
121
+ try {
122
+ logger . log ( 'Flushing events...' ) ;
123
+ await flush ( 2000 ) ;
124
+ logger . log ( 'Done flushing events' ) ;
125
+ } catch ( e ) {
126
+ logger . log ( `Error while flushing events:\n${ e } ` ) ;
127
+ }
128
+
129
+ return origEnd . call ( this , ...args ) ;
130
+ } ;
131
+ }
0 commit comments