1
1
import type { Envelope , InternalBaseTransportOptions , Transport , TransportMakeRequestResponse } from '@sentry/types' ;
2
- import { logger } from '@sentry/utils' ;
2
+ import { forEachEnvelopeItem , logger } from '@sentry/utils' ;
3
3
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
6
7
const DEFAULT_QUEUE_SIZE = 30 ;
7
8
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 > ;
13
10
14
11
interface OfflineTransportOptions extends InternalBaseTransportOptions {
15
12
/**
16
- * The maximum number of events to keep in the offline queue .
13
+ * The maximum number of events to keep in the offline store .
17
14
*
18
15
* Defaults: 30
19
16
*/
20
17
maxQueueSize ?: number ;
21
18
22
19
/**
23
- * Flush the offline queue shortly after startup.
20
+ * Flush the offline store shortly after startup.
24
21
*
25
22
* Defaults: false
26
23
*/
27
24
flushAtStartup ?: boolean ;
28
25
29
26
/**
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.
36
30
*
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 .
40
34
*/
41
- beforeSend ?: ( request : Envelope ) => BeforeSendResponse | Promise < BeforeSendResponse > ;
35
+ shouldStore ?: ( envelope : Envelope , error : Error , retryDelay : number ) => MaybeAsync < boolean > ;
42
36
}
43
37
44
38
interface OfflineStore {
@@ -48,8 +42,22 @@ interface OfflineStore {
48
42
49
43
export type CreateOfflineStore = ( maxQueueCount : number ) => OfflineStore ;
50
44
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
+
51
59
/**
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.
53
61
*
54
62
* @param createTransport The transport to wrap.
55
63
* @param createStore The store implementation to use.
@@ -59,88 +67,89 @@ export function makeOfflineTransport<TO>(
59
67
createStore : CreateOfflineStore ,
60
68
) : ( options : TO & OfflineTransportOptions ) => Transport {
61
69
return options => {
62
- const baseTransport = createTransport ( options ) ;
70
+ const transport = createTransport ( options ) ;
63
71
const maxQueueSize = options . maxQueueSize === undefined ? DEFAULT_QUEUE_SIZE : options . maxQueueSize ;
64
72
const store = createStore ( maxQueueSize ) ;
65
73
66
74
let retryDelay = START_DELAY ;
75
+ let flushTimer : Timer | undefined ;
67
76
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 ;
71
80
}
81
+
82
+ if ( options . shouldStore ) {
83
+ return options . shouldStore ( env , error , retryDelay ) ;
84
+ }
85
+
86
+ return true ;
72
87
}
73
88
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
+ }
77
97
78
- setTimeout ( ( ) => {
79
- void flushQueue ( ) ;
80
- } , retryDelay ) ;
98
+ const delay = overrideDelay || retryDelay ;
81
99
82
- retryDelay *= 3 ;
100
+ flushTimer = setTimeout ( async ( ) => {
101
+ flushTimer = undefined ;
83
102
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
+ } ) ;
88
109
}
89
- } ) ;
90
- }
110
+ } , delay ) as Timer ;
91
111
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 ( ) ;
98
115
}
99
- }
100
116
101
- async function send ( request : Envelope ) : Promise < void | TransportMakeRequestResponse > {
102
- let action = 'send' ;
117
+ retryDelay *= 2 ;
103
118
104
- if ( options . beforeSend ) {
105
- action = await options . beforeSend ( request ) ;
119
+ if ( retryDelay > MAX_DELAY ) {
120
+ retryDelay = MAX_DELAY ;
106
121
}
122
+ }
107
123
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 ) ;
125
131
}
126
- }
127
132
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
+ }
130
143
}
131
-
132
- return { } ;
133
144
}
134
145
135
146
if ( options . flushAtStartup ) {
136
- setTimeout ( ( ) => {
137
- void flushQueue ( ) ;
138
- } , retryDelay ) ;
147
+ flushLater ( ) ;
139
148
}
140
149
141
150
return {
142
151
send,
143
- flush : ( timeout ?: number ) => baseTransport . flush ( timeout ) ,
152
+ flush : ( timeout ?: number ) => transport . flush ( timeout ) ,
144
153
} ;
145
154
} ;
146
155
}
0 commit comments