1- import { captureException , flush , getCurrentHub , startTransaction } from '@sentry/node' ;
1+ import { captureException , getCurrentHub , startTransaction } from '@sentry/node' ;
22import { extractTraceparentData , hasTracingEnabled } from '@sentry/tracing' ;
33import {
44 addExceptionMechanism ,
@@ -11,14 +11,8 @@ import {
1111import * as domain from 'domain' ;
1212
1313import { formatAsCode , nextLogger } from '../../utils/nextLogger' ;
14- import type {
15- AugmentedNextApiRequest ,
16- AugmentedNextApiResponse ,
17- NextApiHandler ,
18- ResponseEndMethod ,
19- WrappedNextApiHandler ,
20- WrappedResponseEndMethod ,
21- } from './types' ;
14+ import type { AugmentedNextApiRequest , AugmentedNextApiResponse , NextApiHandler , WrappedNextApiHandler } from './types' ;
15+ import { autoEndTransactionOnResponseEnd , finishTransaction , flushQueue } from './utils/responseEnd' ;
2216
2317/**
2418 * Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only
@@ -72,11 +66,6 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
7266 }
7367 req . __withSentry_applied__ = true ;
7468
75- // first order of business: monkeypatch `res.end()` so that it will wait for us to send events to sentry before it
76- // fires (if we don't do this, the lambda will close too early and events will be either delayed or lost)
77- // eslint-disable-next-line @typescript-eslint/unbound-method
78- res . end = wrapEndMethod ( res . end ) ;
79-
8069 // use a domain in order to prevent scope bleed between requests
8170 const local = domain . create ( ) ;
8271 local . add ( req ) ;
@@ -86,6 +75,7 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
8675 // return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on
8776 // getting that before it will finish the response.
8877 const boundHandler = local . bind ( async ( ) => {
78+ let transaction ;
8979 const currentScope = getCurrentHub ( ) . getScope ( ) ;
9080
9181 if ( currentScope ) {
@@ -121,7 +111,7 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
121111
122112 const reqMethod = `${ ( req . method || 'GET' ) . toUpperCase ( ) } ` ;
123113
124- const transaction = startTransaction (
114+ transaction = startTransaction (
125115 {
126116 name : `${ reqMethod } ${ reqPath } ` ,
127117 op : 'http.server' ,
@@ -137,9 +127,7 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
137127 ) ;
138128 currentScope . setSpan ( transaction ) ;
139129
140- // save a link to the transaction on the response, so that even if there's an error (landing us outside of
141- // the domain), we can still finish it (albeit possibly missing some scope data)
142- res . __sentryTransaction = transaction ;
130+ autoEndTransactionOnResponseEnd ( transaction , res ) ;
143131 }
144132 }
145133
@@ -188,8 +176,10 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
188176 // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors
189177 // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the
190178 // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not
191- // deployed serverlessly will run into this cleanup function again in `res.end(), but it'll just no-op.)
192- await finishSentryProcessing ( res ) ;
179+ // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already
180+ // be finished and the queue will already be empty, so effectively it'll just no-op.)
181+ await finishTransaction ( transaction , res ) ;
182+ await flushQueue ( ) ;
193183
194184 // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
195185 // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
@@ -203,57 +193,3 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str
203193 return boundHandler ( ) ;
204194 } ;
205195}
206-
207- /**
208- * Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish.
209- *
210- * Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
211- * things in the right order, in this case it's safe, because the native `.end()` actually *is* async, and its run
212- * actually *is* awaited, just manually so (which reflects the fact that the core of the request/response code in Node
213- * by far predates the introduction of `async`/`await`). When `.end()` is done, it emits the `prefinish` event, and
214- * only once that fires does request processing continue. See
215- * https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7.
216- *
217- * @param origEnd The original `res.end()` method
218- * @returns The wrapped version
219- */
220- function wrapEndMethod ( origEnd : ResponseEndMethod ) : WrappedResponseEndMethod {
221- return async function newEnd ( this : AugmentedNextApiResponse , ...args : unknown [ ] ) {
222- await finishSentryProcessing ( this ) ;
223-
224- return origEnd . call ( this , ...args ) ;
225- } ;
226- }
227-
228- /**
229- * Close the open transaction (if any) and flush events to Sentry.
230- *
231- * @param res The outgoing response for this request, on which the transaction is stored
232- */
233- async function finishSentryProcessing ( res : AugmentedNextApiResponse ) : Promise < void > {
234- const { __sentryTransaction : transaction } = res ;
235-
236- if ( transaction ) {
237- transaction . setHttpStatus ( res . statusCode ) ;
238-
239- // Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
240- // transaction closes, and make sure to wait until that's done before flushing events
241- const transactionFinished : Promise < void > = new Promise ( resolve => {
242- setImmediate ( ( ) => {
243- transaction . finish ( ) ;
244- resolve ( ) ;
245- } ) ;
246- } ) ;
247- await transactionFinished ;
248- }
249-
250- // Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
251- // ends. If there was an error, rethrow it so that the normal exception-handling mechanisms can apply.
252- try {
253- __DEBUG_BUILD__ && logger . log ( 'Flushing events...' ) ;
254- await flush ( 2000 ) ;
255- __DEBUG_BUILD__ && logger . log ( 'Done flushing events' ) ;
256- } catch ( e ) {
257- __DEBUG_BUILD__ && logger . log ( 'Error while flushing events:\n' , e ) ;
258- }
259- }
0 commit comments