11import { captureException , flush , getCurrentHub , Handlers , startTransaction , withScope } from '@sentry/node' ;
22import { extractTraceparentData , getActiveTransaction , hasTracingEnabled } from '@sentry/tracing' ;
33import { addExceptionMechanism , isString , logger , stripUrlQueryAndFragment } from '@sentry/utils' ;
4+ import * as domain from 'domain' ;
45import { NextApiHandler } from 'next' ;
56
67import { addRequestDataToEvent , NextRequest } from './instrumentServer' ;
@@ -14,70 +15,82 @@ type WrappedNextApiHandler = NextApiHandler;
1415export const withSentry = ( handler : NextApiHandler ) : WrappedNextApiHandler => {
1516 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1617 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 ) ;
1922
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 ( ) ;
2229
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 ) ) ;
3032
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 } .` ) ;
4039 }
41- }
42- const reqMethod = `${ ( req . method || 'GET' ) . toUpperCase ( ) } ` ;
4340
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+ }
5465 }
55- }
5666
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 ) ;
6375 } ) ;
64- return parseRequest ( event , req ) ;
76+ captureException ( e ) ;
6577 } ) ;
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 ) ;
7383
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+ }
8091 }
81- }
92+ } ) ;
93+
94+ return await boundHandler ( ) ;
8295 } ;
8396} ;
0 commit comments