1
1
import { captureException , flush , getCurrentHub , Handlers , startTransaction , withScope } from '@sentry/node' ;
2
2
import { extractTraceparentData , getActiveTransaction , hasTracingEnabled } from '@sentry/tracing' ;
3
3
import { addExceptionMechanism , isString , logger , stripUrlQueryAndFragment } from '@sentry/utils' ;
4
+ import * as domain from 'domain' ;
4
5
import { NextApiHandler } from 'next' ;
5
6
6
7
import { addRequestDataToEvent , NextRequest } from './instrumentServer' ;
@@ -14,70 +15,82 @@ type WrappedNextApiHandler = NextApiHandler;
14
15
export const withSentry = ( handler : NextApiHandler ) : WrappedNextApiHandler => {
15
16
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
16
17
return async ( req , res ) => {
17
- try {
18
- const currentScope = getCurrentHub ( ) . getScope ( ) ;
18
+ // wrap everything in a domain in order to prevent scope bleed between requests
19
+ const local = domain . create ( ) ;
20
+ local . add ( req ) ;
21
+ local . add ( res ) ;
19
22
20
- if ( currentScope ) {
21
- currentScope . addEventProcessor ( event => addRequestDataToEvent ( event , req as NextRequest ) ) ;
23
+ // `local.bind` causes everything to run inside a domain, just like `local.run` does, but it also lets the callback
24
+ // return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on
25
+ // getting that before it will finish the response.
26
+ const boundHandler = local . bind ( async ( ) => {
27
+ try {
28
+ const currentScope = getCurrentHub ( ) . getScope ( ) ;
22
29
23
- if ( hasTracingEnabled ( ) ) {
24
- // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
25
- let traceparentData ;
26
- if ( req . headers && isString ( req . headers [ 'sentry-trace' ] ) ) {
27
- traceparentData = extractTraceparentData ( req . headers [ 'sentry-trace' ] as string ) ;
28
- logger . log ( `[Tracing] Continuing trace ${ traceparentData ?. traceId } .` ) ;
29
- }
30
+ if ( currentScope ) {
31
+ currentScope . addEventProcessor ( event => addRequestDataToEvent ( event , req as NextRequest ) ) ;
30
32
31
- const url = `${ req . url } ` ;
32
- // pull off query string, if any
33
- let reqPath = stripUrlQueryAndFragment ( url ) ;
34
- // Replace with placeholder
35
- if ( req . query ) {
36
- // TODO get this from next if possible, to avoid accidentally replacing non-dynamic parts of the path if
37
- // they match dynamic parts
38
- for ( const [ key , value ] of Object . entries ( req . query ) ) {
39
- reqPath = reqPath . replace ( `${ value } ` , `[${ key } ]` ) ;
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 } .` ) ;
40
39
}
41
- }
42
- const reqMethod = `${ ( req . method || 'GET' ) . toUpperCase ( ) } ` ;
43
40
44
- const transaction = startTransaction (
45
- {
46
- name : `${ reqMethod } ${ reqPath } ` ,
47
- op : 'http.server' ,
48
- ...traceparentData ,
49
- } ,
50
- // extra context passed to the `tracesSampler`
51
- { request : req } ,
52
- ) ;
53
- currentScope . setSpan ( transaction ) ;
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
+ }
51
+ }
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
+ }
54
65
}
55
- }
56
66
57
- return await handler ( req , res ) ; // Call original handler
58
- } catch ( e ) {
59
- withScope ( scope => {
60
- scope . addEventProcessor ( event => {
61
- addExceptionMechanism ( event , {
62
- handled : false ,
67
+ return await handler ( req , res ) ; // Call original handler
68
+ } catch ( e ) {
69
+ withScope ( scope => {
70
+ scope . addEventProcessor ( event => {
71
+ addExceptionMechanism ( event , {
72
+ handled : false ,
73
+ } ) ;
74
+ return parseRequest ( event , req ) ;
63
75
} ) ;
64
- return parseRequest ( event , req ) ;
76
+ captureException ( e ) ;
65
77
} ) ;
66
- captureException ( e ) ;
67
- } ) ;
68
- throw e ;
69
- } finally {
70
- const transaction = getActiveTransaction ( ) ;
71
- if ( transaction ) {
72
- transaction . setHttpStatus ( res . statusCode ) ;
78
+ throw e ;
79
+ } finally {
80
+ const transaction = getActiveTransaction ( ) ;
81
+ if ( transaction ) {
82
+ transaction . setHttpStatus ( res . statusCode ) ;
73
83
74
- transaction . finish ( ) ;
75
- }
76
- try {
77
- await flush ( 2000 ) ;
78
- } catch ( e ) {
79
- // no-empty
84
+ transaction . finish ( ) ;
85
+ }
86
+ try {
87
+ await flush ( 2000 ) ;
88
+ } catch ( e ) {
89
+ // no-empty
90
+ }
80
91
}
81
- }
92
+ } ) ;
93
+
94
+ return await boundHandler ( ) ;
82
95
} ;
83
96
} ;
0 commit comments