Skip to content

Commit 79b26f6

Browse files
committed
cater for retry-after
1 parent b362612 commit 79b26f6

File tree

2 files changed

+91
-39
lines changed

2 files changed

+91
-39
lines changed

packages/core/src/transports/offline.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import type { Envelope, InternalBaseTransportOptions, Transport, TransportMakeRequestResponse } from '@sentry/types';
2-
import { forEachEnvelopeItem, logger } from '@sentry/utils';
2+
import { forEachEnvelopeItem, logger, parseRetryAfterHeader } from '@sentry/utils';
33

4-
export const BETWEEN_DELAY = 100; // 100 ms
4+
export const MIN_DELAY = 100; // 100 ms
55
export const START_DELAY = 5_000; // 5 seconds
66
const MAX_DELAY = 3.6e6; // 1 hour
77
const DEFAULT_QUEUE_SIZE = 30;
88

9-
type MaybeAsync<T> = T | Promise<T>;
9+
function isReplayEnvelope(envelope: Envelope): boolean {
10+
let isReplay = false;
11+
12+
forEachEnvelopeItem(envelope, (_, type) => {
13+
if (type === 'replay_event') {
14+
isReplay = true;
15+
}
16+
});
17+
18+
return isReplay;
19+
}
1020

1121
interface OfflineTransportOptions extends InternalBaseTransportOptions {
1222
/**
@@ -30,9 +40,9 @@ interface OfflineTransportOptions extends InternalBaseTransportOptions {
3040
*
3141
* @param envelope The envelope that failed to send.
3242
* @param error The error that occurred.
33-
* @param retryDelay The current retry delay.
43+
* @param retryDelay The current retry delay in milliseconds.
3444
*/
35-
shouldStore?: (envelope: Envelope, error: Error, retryDelay: number) => MaybeAsync<boolean>;
45+
shouldStore?: (envelope: Envelope, error: Error, retryDelay: number) => boolean | Promise<boolean>;
3646
}
3747

3848
interface OfflineStore {
@@ -44,18 +54,6 @@ export type CreateOfflineStore = (maxQueueCount: number) => OfflineStore;
4454

4555
type Timer = number | { unref?: () => void };
4656

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-
5957
/**
6058
* Wraps a transport and stores and retries events when they fail to send.
6159
*
@@ -74,6 +72,10 @@ export function makeOfflineTransport<TO>(
7472
let retryDelay = START_DELAY;
7573
let flushTimer: Timer | undefined;
7674

75+
function log(msg: string, error?: Error): void {
76+
__DEBUG_BUILD__ && logger.info(`[Offline]: ${msg}`, error);
77+
}
78+
7779
function shouldQueue(env: Envelope, error: Error, retryDelay: number): MaybeAsync<boolean> {
7880
if (isReplayEnvelope(env)) {
7981
return false;
@@ -86,25 +88,19 @@ export function makeOfflineTransport<TO>(
8688
return true;
8789
}
8890

89-
function flushLater(overrideDelay?: number): void {
91+
function flushIn(delay: number): void {
9092
if (flushTimer) {
91-
if (overrideDelay) {
92-
clearTimeout(flushTimer as ReturnType<typeof setTimeout>);
93-
} else {
94-
return;
95-
}
93+
clearTimeout(flushTimer as ReturnType<typeof setTimeout>);
9694
}
9795

98-
const delay = overrideDelay || retryDelay;
99-
10096
flushTimer = setTimeout(async () => {
10197
flushTimer = undefined;
10298

10399
const found = await store.pop();
104100
if (found) {
105-
__DEBUG_BUILD__ && logger.info('[Offline]: Attempting to send previously queued event');
101+
log('Attempting to send previously queued event');
106102
void send(found).catch(e => {
107-
__DEBUG_BUILD__ && logger.info('[Offline]: Failed to send when retrying', e);
103+
log('Failed to retry sending', e);
108104
});
109105
}
110106
}, delay) as Timer;
@@ -113,6 +109,14 @@ export function makeOfflineTransport<TO>(
113109
if (typeof flushTimer !== 'number' && typeof flushTimer.unref === 'function') {
114110
flushTimer.unref();
115111
}
112+
}
113+
114+
function flushWithBackOff(): void {
115+
if (flushTimer) {
116+
return;
117+
}
118+
119+
flushIn(retryDelay);
116120

117121
retryDelay *= 2;
118122

@@ -124,18 +128,27 @@ export function makeOfflineTransport<TO>(
124128
async function send(envelope: Envelope): Promise<void | TransportMakeRequestResponse> {
125129
try {
126130
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);
131+
132+
let delay = MIN_DELAY;
133+
134+
if (result) {
135+
// If there's a retry-after header, use that as the next delay.
136+
if (result.headers && result.headers['retry-after']) {
137+
delay = parseRetryAfterHeader(result.headers['retry-after']);
138+
} // If we have a server error, return now so we don't flush the queue.
139+
else if ((result.statusCode || 0) >= 400) {
140+
return result;
141+
}
131142
}
132143

144+
flushIn(delay);
145+
retryDelay = START_DELAY;
133146
return result;
134147
} catch (e) {
135148
if (await shouldQueue(envelope, e, retryDelay)) {
136149
await store.insert(envelope);
137-
flushLater();
138-
__DEBUG_BUILD__ && logger.info('[Offline]: Event queued', e);
150+
flushWithBackOff();
151+
log('Error sending. Event queued', e);
139152
return {};
140153
} else {
141154
throw e;
@@ -144,7 +157,7 @@ export function makeOfflineTransport<TO>(
144157
}
145158

146159
if (options.flushAtStartup) {
147-
flushLater();
160+
flushWithBackOff();
148161
}
149162

150163
return {

packages/core/test/lib/transports/offline.test.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { TextEncoder } from 'util';
1818

1919
import { createTransport } from '../../../src';
2020
import type { CreateOfflineStore } from '../../../src/transports/offline';
21-
import { BETWEEN_DELAY, makeOfflineTransport, START_DELAY } from '../../../src/transports/offline';
21+
import { makeOfflineTransport, MIN_DELAY, START_DELAY } from '../../../src/transports/offline';
2222

2323
const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [
2424
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
@@ -140,7 +140,7 @@ describe('makeOfflineTransport', () => {
140140
expect(queuedCount).toEqual(0);
141141
expect(getSendCount()).toEqual(1);
142142

143-
await delay(BETWEEN_DELAY * 2);
143+
await delay(MIN_DELAY * 2);
144144

145145
// After a successful send, the store should be checked
146146
expect(getCalls()).toEqual(['pop']);
@@ -154,7 +154,7 @@ describe('makeOfflineTransport', () => {
154154

155155
expect(result).toEqual({ statusCode: 200 });
156156

157-
await delay(BETWEEN_DELAY * 3);
157+
await delay(MIN_DELAY * 3);
158158

159159
expect(getSendCount()).toEqual(2);
160160
// After a successful send from the store, the store should be checked again to ensure it's empty
@@ -179,7 +179,7 @@ describe('makeOfflineTransport', () => {
179179

180180
expect(result).toEqual({});
181181

182-
await delay(BETWEEN_DELAY * 2);
182+
await delay(MIN_DELAY * 2);
183183

184184
expect(getSendCount()).toEqual(0);
185185
expect(queuedCount).toEqual(1);
@@ -204,7 +204,7 @@ describe('makeOfflineTransport', () => {
204204

205205
expect(result).toEqual({ statusCode: 500 });
206206

207-
await delay(BETWEEN_DELAY * 2);
207+
await delay(MIN_DELAY * 2);
208208

209209
expect(getSendCount()).toEqual(1);
210210
expect(queuedCount).toEqual(0);
@@ -282,4 +282,43 @@ describe('makeOfflineTransport', () => {
282282
expect(getSendCount()).toEqual(0);
283283
expect(getCalls()).toEqual([]);
284284
});
285+
286+
it('Follows the Retry-After header', async () => {
287+
const { getCalls, store } = createTestStore(ERROR_ENVELOPE);
288+
const { getSendCount, baseTransport } = createTestTransport(
289+
{
290+
statusCode: 429,
291+
headers: { 'x-sentry-rate-limits': '', 'retry-after': '3' },
292+
},
293+
{ statusCode: 200 },
294+
);
295+
296+
let queuedCount = 0;
297+
const transport = makeOfflineTransport(
298+
baseTransport,
299+
store,
300+
)({
301+
...transportOptions,
302+
shouldStore: () => {
303+
queuedCount += 1;
304+
return true;
305+
},
306+
});
307+
const result = await transport.send(ERROR_ENVELOPE);
308+
309+
expect(result).toEqual({
310+
statusCode: 429,
311+
headers: { 'x-sentry-rate-limits': '', 'retry-after': '3' },
312+
});
313+
314+
await delay(MIN_DELAY * 2);
315+
316+
expect(getSendCount()).toEqual(1);
317+
318+
await delay(4_000);
319+
320+
expect(getSendCount()).toEqual(2);
321+
expect(queuedCount).toEqual(0);
322+
expect(getCalls()).toEqual(['pop', 'pop']);
323+
}, 7_000);
285324
});

0 commit comments

Comments
 (0)