Skip to content

Commit 12e34d4

Browse files
authored
fix(core): Exclude client reports from offline queuing (#7226)
1 parent fad6c48 commit 12e34d4

File tree

3 files changed

+62
-20
lines changed

3 files changed

+62
-20
lines changed

packages/core/src/transports/offline.ts

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

44
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

8-
function isReplayEnvelope(envelope: Envelope): boolean {
9-
let isReplay = false;
10-
11-
forEachEnvelopeItem(envelope, (_, type) => {
12-
if (type === 'replay_event') {
13-
isReplay = true;
14-
}
15-
});
16-
17-
return isReplay;
18-
}
19-
208
function log(msg: string, error?: Error): void {
219
__DEBUG_BUILD__ && logger.info(`[Offline]: ${msg}`, error);
2210
}
@@ -74,7 +62,8 @@ export function makeOfflineTransport<TO>(
7462
// We don't queue Session Replay envelopes because they are:
7563
// - Ordered and Replay relies on the response status to know when they're successfully sent.
7664
// - Likely to fill the queue quickly and block other events from being sent.
77-
if (isReplayEnvelope(env)) {
65+
// We also want to drop client reports because they can be generated when we retry sending events while offline.
66+
if (envelopeContainsItemType(env, ['replay_event', 'replay_recording', 'client_report'])) {
7867
return false;
7968
}
8069

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
ClientReport,
23
Envelope,
34
EventEnvelope,
45
EventItem,
@@ -9,6 +10,7 @@ import type {
910
TransportMakeRequestResponse,
1011
} from '@sentry/types';
1112
import {
13+
createClientReportEnvelope,
1214
createEnvelope,
1315
createEventEnvelopeHeaders,
1416
dsnFromString,
@@ -54,6 +56,25 @@ const RELAY_ENVELOPE = createEnvelope<ReplayEnvelope>(
5456
],
5557
);
5658

59+
const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [
60+
{
61+
reason: 'before_send',
62+
category: 'error',
63+
quantity: 30,
64+
},
65+
{
66+
reason: 'network_error',
67+
category: 'transaction',
68+
quantity: 23,
69+
},
70+
];
71+
72+
const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
73+
DEFAULT_DISCARDED_EVENTS,
74+
'https://[email protected]/1337',
75+
123456,
76+
);
77+
5778
const transportOptions = {
5879
recordDroppedEvent: () => undefined, // noop
5980
textEncoder: new TextEncoder(),
@@ -288,6 +309,23 @@ describe('makeOfflineTransport', () => {
288309
expect(getCalls()).toEqual([]);
289310
});
290311

312+
it('should not store client report envelopes on send failure', async () => {
313+
const { getCalls, store } = createTestStore();
314+
const { getSendCount, baseTransport } = createTestTransport(new Error());
315+
const queuedCount = 0;
316+
const transport = makeOfflineTransport(baseTransport)({
317+
...transportOptions,
318+
createStore: store,
319+
shouldStore: () => true,
320+
});
321+
const result = transport.send(CLIENT_REPORT_ENVELOPE);
322+
323+
await expect(result).rejects.toBeInstanceOf(Error);
324+
expect(queuedCount).toEqual(0);
325+
expect(getSendCount()).toEqual(0);
326+
expect(getCalls()).toEqual([]);
327+
});
328+
291329
it(
292330
'Follows the Retry-After header',
293331
async () => {

packages/utils/src/envelope.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {
66
DataCategory,
77
DsnComponents,
88
Envelope,
9-
EnvelopeItem,
109
EnvelopeItemType,
1110
Event,
1211
EventEnvelopeHeaders,
@@ -41,16 +40,32 @@ export function addItemToEnvelope<E extends Envelope>(envelope: E, newItem: E[1]
4140
/**
4241
* Convenience function to loop through the items and item types of an envelope.
4342
* (This function was mostly created because working with envelope types is painful at the moment)
43+
*
44+
* If the callback returns true, the rest of the items will be skipped.
4445
*/
4546
export function forEachEnvelopeItem<E extends Envelope>(
4647
envelope: Envelope,
47-
callback: (envelopeItem: E[1][number], envelopeItemType: E[1][number][0]['type']) => void,
48-
): void {
48+
callback: (envelopeItem: E[1][number], envelopeItemType: E[1][number][0]['type']) => boolean | void,
49+
): boolean {
4950
const envelopeItems = envelope[1];
50-
envelopeItems.forEach((envelopeItem: EnvelopeItem) => {
51+
52+
for (const envelopeItem of envelopeItems) {
5153
const envelopeItemType = envelopeItem[0].type;
52-
callback(envelopeItem, envelopeItemType);
53-
});
54+
const result = callback(envelopeItem, envelopeItemType);
55+
56+
if (result) {
57+
return true;
58+
}
59+
}
60+
61+
return false;
62+
}
63+
64+
/**
65+
* Returns true if the envelope contains any of the given envelope item types
66+
*/
67+
export function envelopeContainsItemType(envelope: Envelope, types: EnvelopeItemType[]): boolean {
68+
return forEachEnvelopeItem(envelope, (_, type) => types.includes(type));
5469
}
5570

5671
/**

0 commit comments

Comments
 (0)