Skip to content

Commit b362612

Browse files
committed
Improve
1 parent 9a252ca commit b362612

File tree

2 files changed

+209
-92
lines changed

2 files changed

+209
-92
lines changed
Lines changed: 84 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,38 @@
11
import 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
67
const 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

1411
interface 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

4438
interface OfflineStore {
@@ -48,8 +42,22 @@ interface OfflineStore {
4842

4943
export 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

Comments
 (0)