@@ -16,6 +16,9 @@ export interface CloudflareTransportOptions extends BaseTransportOptions {
1616 */
1717const DEFAULT_TRANSPORT_BUFFER_SIZE = 256 ;
1818
19+ type TaskProducer = ( ) => PromiseLike < TransportMakeRequestResponse > ;
20+ type RunTask = ( taskProducer : TaskProducer , signal : AbortSignal ) => PromiseLike < TransportMakeRequestResponse > ;
21+
1922/**
2023 * This is a modified promise buffer that collects tasks until drain is called.
2124 * We need this in the edge runtime because edge function invocations may not share I/O objects, like fetch requests
@@ -29,20 +32,23 @@ export class IsolatedPromiseBuffer {
2932 // If we ever remove it from the interface we should also remove it here.
3033 public $ : Array < PromiseLike < TransportMakeRequestResponse > > ;
3134
32- private _taskProducers : ( ( ) => PromiseLike < TransportMakeRequestResponse > ) [ ] ;
35+ private _taskProducers : TaskProducer [ ] ;
3336
3437 private readonly _bufferSize : number ;
3538
36- public constructor ( _bufferSize = DEFAULT_TRANSPORT_BUFFER_SIZE ) {
39+ private readonly _runTask : RunTask ;
40+
41+ public constructor ( _bufferSize = DEFAULT_TRANSPORT_BUFFER_SIZE , _runTask : RunTask = taskProducer => taskProducer ( ) ) {
3742 this . $ = [ ] ;
3843 this . _taskProducers = [ ] ;
3944 this . _bufferSize = _bufferSize ;
45+ this . _runTask = _runTask ;
4046 }
4147
4248 /**
4349 * @inheritdoc
4450 */
45- public add ( taskProducer : ( ) => PromiseLike < TransportMakeRequestResponse > ) : PromiseLike < TransportMakeRequestResponse > {
51+ public add ( taskProducer : TaskProducer ) : PromiseLike < TransportMakeRequestResponse > {
4652 if ( this . _taskProducers . length >= this . _bufferSize ) {
4753 return Promise . reject ( SENTRY_BUFFER_FULL_ERROR ) ;
4854 }
@@ -57,19 +63,22 @@ export class IsolatedPromiseBuffer {
5763 public drain ( timeout ?: number ) : PromiseLike < boolean > {
5864 const oldTaskProducers = [ ...this . _taskProducers ] ;
5965 this . _taskProducers = [ ] ;
66+ const drainController = new AbortController ( ) ;
67+ const tasks = oldTaskProducers . map ( taskProducer => this . _runTask ( taskProducer , drainController . signal ) ) ;
6068
6169 return new Promise ( resolve => {
6270 const timer = setTimeout ( ( ) => {
6371 if ( timeout && timeout > 0 ) {
72+ drainController . abort ( ) ;
6473 resolve ( false ) ;
6574 }
6675 } , timeout ) ;
6776
6877 // This cannot reject
6978 // eslint-disable-next-line @typescript-eslint/no-floating-promises
7079 Promise . all (
71- oldTaskProducers . map ( taskProducer =>
72- taskProducer ( ) . then ( null , ( ) => {
80+ tasks . map ( task =>
81+ task . then ( null , ( ) => {
7382 // catch all failed requests
7483 } ) ,
7584 ) ,
@@ -86,15 +95,36 @@ export class IsolatedPromiseBuffer {
8695 * Creates a Transport that uses the native fetch API to send events to Sentry.
8796 */
8897export function makeCloudflareTransport ( options : CloudflareTransportOptions ) : Transport {
98+ let activeDrainSignal : AbortSignal | undefined ;
99+
89100 function makeRequest ( request : TransportRequest ) : PromiseLike < TransportMakeRequestResponse > {
101+ const controller = new AbortController ( ) ;
102+ const callerSignal = options . fetchOptions ?. signal ;
103+ const drainSignal = activeDrainSignal ;
104+ const abortFromCallerSignal = ( ) : void => controller . abort ( ) ;
105+ const abortFromDrainSignal = ( ) : void => controller . abort ( ) ;
106+
107+ if ( callerSignal ?. aborted ) {
108+ controller . abort ( ) ;
109+ } else {
110+ callerSignal ?. addEventListener ( 'abort' , abortFromCallerSignal , { once : true } ) ;
111+ }
112+
113+ if ( drainSignal ?. aborted ) {
114+ controller . abort ( ) ;
115+ } else {
116+ drainSignal ?. addEventListener ( 'abort' , abortFromDrainSignal , { once : true } ) ;
117+ }
118+
90119 const requestOptions : RequestInit = {
91120 body : request . body as BodyInit ,
92121 method : 'POST' ,
93122 headers : options . headers ,
94123 ...options . fetchOptions ,
124+ signal : controller . signal ,
95125 } ;
96126
97- return suppressTracing ( ( ) => {
127+ const requestPromise = suppressTracing ( ( ) => {
98128 return ( options . fetch ?? fetch ) ( options . url , requestOptions ) . then ( async response => {
99129 // Consume the response body to satisfy Cloudflare Workers' fetch requirements.
100130 // The runtime requires all fetch response bodies to be read or explicitly canceled
@@ -116,7 +146,24 @@ export function makeCloudflareTransport(options: CloudflareTransportOptions): Tr
116146 } ;
117147 } ) ;
118148 } ) ;
149+
150+ return Promise . resolve ( requestPromise ) . finally ( ( ) => {
151+ callerSignal ?. removeEventListener ( 'abort' , abortFromCallerSignal ) ;
152+ drainSignal ?. removeEventListener ( 'abort' , abortFromDrainSignal ) ;
153+ } ) ;
154+ }
155+
156+ function runTaskWithinDrain (
157+ taskProducer : TaskProducer ,
158+ signal : AbortSignal ,
159+ ) : PromiseLike < TransportMakeRequestResponse > {
160+ activeDrainSignal = signal ;
161+ try {
162+ return taskProducer ( ) ;
163+ } finally {
164+ activeDrainSignal = undefined ;
165+ }
119166 }
120167
121- return createTransport ( options , makeRequest , new IsolatedPromiseBuffer ( options . bufferSize ) ) ;
168+ return createTransport ( options , makeRequest , new IsolatedPromiseBuffer ( options . bufferSize , runTaskWithinDrain ) ) ;
122169}
0 commit comments