11import type { Client , Span } from '@sentry/core' ;
22import { getActiveSpan , getRootSpan , GLOBAL_OBJ } from '@sentry/core' ;
33import type { HttpServerResponse } from '@sentry/core/server' ;
4+ import { _INTERNAL_safeUnref } from '@sentry/core/server' ;
45import { subscribeDiagnosticsChannel } from '@sentry/server-utils' ;
56
67// On Vercel, `http.server.request.start` fires without the request context, while this channel fires with it.
78const HTTP_ON_SERVER_RESPONSE_FINISH = 'http.server.response.finish' ;
89
910// Some frameworks end the request's root span shortly after the response closes.
10- const ROOT_SPAN_END_TIMEOUT_MS = 2000 ;
11+ const REQUEST_END_TIMEOUT_MS = 2000 ;
1112
12- interface VercelRequestContext {
13- waitUntil ?: ( task : Promise < unknown > ) => void ;
13+ interface VercelRequestContextGlobal {
14+ get ? ( ) : { waitUntil ?: ( task : Promise < unknown > ) => void } | undefined ;
1415}
1516
1617/**
@@ -19,7 +20,7 @@ interface VercelRequestContext {
1920 * Vercel can suspend a function as soon as the response is sent, so buffered events, spans, logs and metrics
2021 * (and the timers that flush them) arrive late or never. When a response
2122 * finishes, we register one `waitUntil` that waits for the response to close and the request's root span to
22- * end, and then flushes the client.
23+ * end (2 seconds at most) , and then flushes the client.
2324 */
2425export function setupVercelKeepAlive ( client : Client ) : void {
2526 // Ensure we flush events when vercel functions are ended
@@ -31,9 +32,10 @@ export function setupVercelKeepAlive(client: Client): void {
3132
3233 subscribeDiagnosticsChannel ( HTTP_ON_SERVER_RESPONSE_FINISH , message => {
3334 const { response } = message as { response ?: HttpServerResponse } ;
34- const requestContext = (
35- GLOBAL_OBJ as unknown as Record < symbol , { get ?( ) : VercelRequestContext | undefined } | undefined >
36- ) [ Symbol . for ( '@vercel/request-context' ) ] ?. get ?.( ) ;
35+ const requestContextGlobal : VercelRequestContextGlobal | undefined =
36+ // @ts -expect-error Vercel sets this global, so `GLOBAL_OBJ` does not type it
37+ GLOBAL_OBJ [ Symbol . for ( '@vercel/request-context' ) ] ;
38+ const requestContext = requestContextGlobal ?. get ?.( ) ;
3739 if ( ! response || ! requestContext ?. waitUntil ) {
3840 return ;
3941 }
@@ -42,30 +44,43 @@ export function setupVercelKeepAlive(client: Client): void {
4244 const rootSpan = activeSpan && getRootSpan ( activeSpan ) ;
4345
4446 requestContext . waitUntil (
45- new Promise < void > ( resolve => response . once ( 'close' , resolve ) )
46- . then ( ( ) => rootSpan && waitForSpanEnd ( client , rootSpan , ROOT_SPAN_END_TIMEOUT_MS ) )
47- . then ( ( ) => client . flush ( 2000 ) ) ,
47+ waitForRequestEnd ( client , response , rootSpan , REQUEST_END_TIMEOUT_MS ) . then ( ( ) => client . flush ( 2000 ) ) ,
4848 ) ;
4949 } ) ;
5050}
5151
52- function waitForSpanEnd ( client : Client , span : Span , timeout : number ) : Promise < void > {
53- // Ended and unsampled spans are not recording.
54- if ( ! span . isRecording ( ) ) {
55- return Promise . resolve ( ) ;
56- }
57-
52+ function waitForRequestEnd (
53+ client : Client ,
54+ response : HttpServerResponse ,
55+ rootSpan : Span | undefined ,
56+ timeout : number ,
57+ ) : Promise < void > {
5858 return new Promise ( resolve => {
59+ let responseClosed = false ;
60+ // Ended and unsampled spans are not recording.
61+ let rootSpanEnded = ! rootSpan ?. isRecording ( ) ;
62+
5963 const done = ( ) : void => {
6064 clearTimeout ( timer ) ;
6165 unsubscribe ( ) ;
6266 resolve ( ) ;
6367 } ;
64- const timer = setTimeout ( done , timeout ) ;
65- const unsubscribe = client . on ( 'spanEnd' , endedSpan => {
66- if ( endedSpan === span ) {
68+ const doneIfRequestEnded = ( ) : void => {
69+ if ( responseClosed && rootSpanEnded ) {
6770 done ( ) ;
6871 }
72+ } ;
73+
74+ const timer = _INTERNAL_safeUnref ( setTimeout ( done , timeout ) ) ;
75+ const unsubscribe = client . on ( 'spanEnd' , span => {
76+ if ( span === rootSpan ) {
77+ rootSpanEnded = true ;
78+ doneIfRequestEnded ( ) ;
79+ }
80+ } ) ;
81+ response . once ( 'close' , ( ) => {
82+ responseClosed = true ;
83+ doneIfRequestEnded ( ) ;
6984 } ) ;
7085 } ) ;
7186}
0 commit comments