11import type { Envelope , InternalBaseTransportOptions , Transport , TransportMakeRequestResponse } from '@sentry/types' ;
2- import { logger } from '@sentry/utils' ;
2+ import { forEachEnvelopeItem , logger } from '@sentry/utils' ;
33
4- export const START_DELAY = 5_000 ;
5- const MAX_DELAY = 2_000_000_000 ;
4+ export const BETWEEN_DELAY = 100 ; // 100 ms
5+ export const START_DELAY = 5_000 ; // 5 seconds
6+ const MAX_DELAY = 3.6e6 ; // 1 hour
67const DEFAULT_QUEUE_SIZE = 30 ;
78
8- function wasRateLimited ( result : TransportMakeRequestResponse ) : boolean {
9- return ! ! ( result . headers && result . headers [ 'x-sentry-rate-limits' ] ) ;
10- }
11-
12- type BeforeSendResponse = 'send' | 'queue' | 'drop' ;
9+ type MaybeAsync < T > = T | Promise < T > ;
1310
1411interface OfflineTransportOptions extends InternalBaseTransportOptions {
1512 /**
16- * The maximum number of events to keep in the offline queue .
13+ * The maximum number of events to keep in the offline store .
1714 *
1815 * Defaults: 30
1916 */
2017 maxQueueSize ?: number ;
2118
2219 /**
23- * Flush the offline queue shortly after startup.
20+ * Flush the offline store shortly after startup.
2421 *
2522 * Defaults: false
2623 */
2724 flushAtStartup ?: boolean ;
2825
2926 /**
30- * Called when an event is queued .
31- */
32- eventQueued ?: ( ) => void ;
33-
34- /**
35- * Called before attempting to send an event to Sentry.
27+ * Called before an event is stored.
28+ *
29+ * Return false to drop the envelope rather than store it.
3630 *
37- * Return 'send' to attempt to send the event .
38- * Return 'queue' to queue the event for sending later .
39- * Return 'drop' to drop the event .
31+ * @param envelope The envelope that failed to send .
32+ * @param error The error that occurred .
33+ * @param retryDelay The current retry delay .
4034 */
41- beforeSend ?: ( request : Envelope ) => BeforeSendResponse | Promise < BeforeSendResponse > ;
35+ shouldStore ?: ( envelope : Envelope , error : Error , retryDelay : number ) => MaybeAsync < boolean > ;
4236}
4337
4438interface OfflineStore {
@@ -48,8 +42,22 @@ interface OfflineStore {
4842
4943export type CreateOfflineStore = ( maxQueueCount : number ) => OfflineStore ;
5044
45+ type Timer = number | { unref ?: ( ) => void } ;
46+
47+ function isReplayEnvelope ( envelope : Envelope ) : boolean {
48+ let isReplay = false ;
49+
50+ forEachEnvelopeItem ( envelope , ( _ , type ) => {
51+ if ( type === 'replay_event' ) {
52+ isReplay = true ;
53+ }
54+ } ) ;
55+
56+ return isReplay ;
57+ }
58+
5159/**
52- * Wraps a transport and queues events when envelopes fail to send.
60+ * Wraps a transport and stores and retries events when they fail to send.
5361 *
5462 * @param createTransport The transport to wrap.
5563 * @param createStore The store implementation to use.
@@ -59,88 +67,89 @@ export function makeOfflineTransport<TO>(
5967 createStore : CreateOfflineStore ,
6068) : ( options : TO & OfflineTransportOptions ) => Transport {
6169 return options => {
62- const baseTransport = createTransport ( options ) ;
70+ const transport = createTransport ( options ) ;
6371 const maxQueueSize = options . maxQueueSize === undefined ? DEFAULT_QUEUE_SIZE : options . maxQueueSize ;
6472 const store = createStore ( maxQueueSize ) ;
6573
6674 let retryDelay = START_DELAY ;
75+ let flushTimer : Timer | undefined ;
6776
68- function queued ( ) : void {
69- if ( options . eventQueued ) {
70- options . eventQueued ( ) ;
77+ function shouldQueue ( env : Envelope , error : Error , retryDelay : number ) : MaybeAsync < boolean > {
78+ if ( isReplayEnvelope ( env ) ) {
79+ return false ;
7180 }
81+
82+ if ( options . shouldStore ) {
83+ return options . shouldStore ( env , error , retryDelay ) ;
84+ }
85+
86+ return true ;
7287 }
7388
74- function queueRequest ( envelope : Envelope ) : Promise < void > {
75- return store . insert ( envelope ) . then ( ( ) => {
76- queued ( ) ;
89+ function flushLater ( overrideDelay ?: number ) : void {
90+ if ( flushTimer ) {
91+ if ( overrideDelay ) {
92+ clearTimeout ( flushTimer as ReturnType < typeof setTimeout > ) ;
93+ } else {
94+ return ;
95+ }
96+ }
7797
78- setTimeout ( ( ) => {
79- void flushQueue ( ) ;
80- } , retryDelay ) ;
98+ const delay = overrideDelay || retryDelay ;
8199
82- retryDelay *= 3 ;
100+ flushTimer = setTimeout ( async ( ) => {
101+ flushTimer = undefined ;
83102
84- // If the delay is bigger than 2^31 (max signed 32-bit int), setTimeout throws
85- // an error on node.js and falls back to 1 which can cause a huge number of requests.
86- if ( retryDelay > MAX_DELAY ) {
87- retryDelay = MAX_DELAY ;
103+ const found = await store . pop ( ) ;
104+ if ( found ) {
105+ __DEBUG_BUILD__ && logger . info ( '[Offline]: Attempting to send previously queued event' ) ;
106+ void send ( found ) . catch ( e => {
107+ __DEBUG_BUILD__ && logger . info ( '[Offline]: Failed to send when retrying' , e ) ;
108+ } ) ;
88109 }
89- } ) ;
90- }
110+ } , delay ) as Timer ;
91111
92- async function flushQueue ( ) : Promise < void > {
93- const found = await store . pop ( ) ;
94-
95- if ( found ) {
96- __DEBUG_BUILD__ && logger . info ( '[Offline]: Attempting to send previously queued event' ) ;
97- void send ( found ) ;
112+ // We need to unref the timer in node.js, otherwise the node process never exit.
113+ if ( typeof flushTimer !== 'number' && typeof flushTimer . unref === 'function' ) {
114+ flushTimer . unref ( ) ;
98115 }
99- }
100116
101- async function send ( request : Envelope ) : Promise < void | TransportMakeRequestResponse > {
102- let action = 'send' ;
117+ retryDelay *= 2 ;
103118
104- if ( options . beforeSend ) {
105- action = await options . beforeSend ( request ) ;
119+ if ( retryDelay > MAX_DELAY ) {
120+ retryDelay = MAX_DELAY ;
106121 }
122+ }
107123
108- if ( action === 'send' ) {
109- try {
110- const result = await baseTransport . send ( request ) ;
111- if ( wasRateLimited ( result || { } ) ) {
112- __DEBUG_BUILD__ && logger . info ( '[Offline]: Event queued due to rate limiting' ) ;
113- action = 'queue' ;
114- } else {
115- // Envelope was successfully sent
116- // Reset the retry delay
117- retryDelay = START_DELAY ;
118- // Check if there are any more in the queue
119- void flushQueue ( ) ;
120- return result ;
121- }
122- } catch ( e ) {
123- __DEBUG_BUILD__ && logger . info ( '[Offline]: Event queued due to error' , e ) ;
124- action = 'queue' ;
124+ async function send ( envelope : Envelope ) : Promise < void | TransportMakeRequestResponse > {
125+ try {
126+ const result = await transport . send ( envelope ) ;
127+ // If the status code wasn't a server error, reset retryDelay and flush
128+ if ( result && ( result . statusCode || 500 ) < 400 ) {
129+ retryDelay = START_DELAY ;
130+ flushLater ( BETWEEN_DELAY ) ;
125131 }
126- }
127132
128- if ( action == 'queue' ) {
129- void queueRequest ( request ) ;
133+ return result ;
134+ } catch ( e ) {
135+ if ( await shouldQueue ( envelope , e , retryDelay ) ) {
136+ await store . insert ( envelope ) ;
137+ flushLater ( ) ;
138+ __DEBUG_BUILD__ && logger . info ( '[Offline]: Event queued' , e ) ;
139+ return { } ;
140+ } else {
141+ throw e ;
142+ }
130143 }
131-
132- return { } ;
133144 }
134145
135146 if ( options . flushAtStartup ) {
136- setTimeout ( ( ) => {
137- void flushQueue ( ) ;
138- } , retryDelay ) ;
147+ flushLater ( ) ;
139148 }
140149
141150 return {
142151 send,
143- flush : ( timeout ?: number ) => baseTransport . flush ( timeout ) ,
152+ flush : ( timeout ?: number ) => transport . flush ( timeout ) ,
144153 } ;
145154 } ;
146155}
0 commit comments