diff --git a/dev-packages/browser-integration-tests/playwright.config.ts b/dev-packages/browser-integration-tests/playwright.config.ts index 78a71108837f..77ed6014d230 100644 --- a/dev-packages/browser-integration-tests/playwright.config.ts +++ b/dev-packages/browser-integration-tests/playwright.config.ts @@ -11,7 +11,7 @@ const config: PlaywrightTestConfig = { testMatch: /test.ts/, use: { - trace: process.env.CI ? 'retry-with-trace' : 'off', + trace: process.env.CI ? 'retain-on-failure' : 'off', }, projects: [ diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js new file mode 100644 index 000000000000..0e1297ae8710 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = Sentry.replayIntegration({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + transport: Sentry.makeBrowserOfflineTransport(), + integrations: [window.Replay], +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/template.html b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts new file mode 100644 index 000000000000..a74a2c891fad --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts @@ -0,0 +1,45 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; + +sentryTest('should capture replays offline', async ({ getLocalTestPath, page }) => { + // makeBrowserOfflineTransport is not included in any CDN bundles + if (shouldSkipReplayTest() || (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle'))) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + // This would be the obvious way to test offline support but it doesn't appear to work! + // await context.setOffline(true); + + // Abort the first envelope request so the event gets queued + await page.route(/ingest\.sentry\.io/, route => route.abort('internetdisconnected'), { times: 1 }); + + await page.goto(url); + + await new Promise(resolve => setTimeout(resolve, 2_000)); + + // Now send a second event which should be queued after the the first one and force flushing the queue + await page.locator('button').click(); + + const replayEvent0 = getReplayEvent(await reqPromise0); + const replayEvent1 = getReplayEvent(await reqPromise1); + + // Check that we received the envelopes in the correct order + expect(replayEvent0.timestamp).toBeGreaterThan(0); + expect(replayEvent1.timestamp).toBeGreaterThan(0); + expect(replayEvent0.timestamp).toBeLessThan(replayEvent1.timestamp || 0); +}); diff --git a/dev-packages/browser-integration-tests/suites/transport/offline/init.js b/dev-packages/browser-integration-tests/suites/transport/offline/init.js index a69f8a32b32a..e102d6a8a5a5 100644 --- a/dev-packages/browser-integration-tests/suites/transport/offline/init.js +++ b/dev-packages/browser-integration-tests/suites/transport/offline/init.js @@ -4,5 +4,5 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - transport: Sentry.makeBrowserOfflineTransport(Sentry.makeFetchTransport), + transport: Sentry.makeBrowserOfflineTransport(), }); diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts index dd7d9c70d929..4a435b2972fa 100644 --- a/packages/browser/src/transports/offline.ts +++ b/packages/browser/src/transports/offline.ts @@ -48,8 +48,8 @@ function keys(store: IDBObjectStore): Promise { return promisifyRequest(store.getAllKeys() as IDBRequest); } -/** Insert into the store */ -export function insert(store: Store, value: Uint8Array | string, maxQueueSize: number): Promise { +/** Insert into the end of the store */ +export function push(store: Store, value: Uint8Array | string, maxQueueSize: number): Promise { return store(store => { return keys(store).then(keys => { if (keys.length >= maxQueueSize) { @@ -63,8 +63,23 @@ export function insert(store: Store, value: Uint8Array | string, maxQueueSize: n }); } +/** Insert into the front of the store */ +export function unshift(store: Store, value: Uint8Array | string, maxQueueSize: number): Promise { + return store(store => { + return keys(store).then(keys => { + if (keys.length >= maxQueueSize) { + return; + } + + // We insert with an decremented key so that the entries are popped in order + store.put(value, Math.min(...keys, 0) - 1); + return promisifyRequest(store.transaction); + }); + }); +} + /** Pop the oldest value from the store */ -export function pop(store: Store): Promise { +export function shift(store: Store): Promise { return store(store => { return keys(store).then(keys => { if (keys.length === 0) { @@ -79,7 +94,7 @@ export function pop(store: Store): Promise { }); } -export interface BrowserOfflineTransportOptions extends OfflineTransportOptions { +export interface BrowserOfflineTransportOptions extends Omit { /** * Name of indexedDb database to store envelopes in * Default: 'sentry-offline' @@ -110,17 +125,25 @@ function createIndexedDbStore(options: BrowserOfflineTransportOptions): OfflineS } return { - insert: async (env: Envelope) => { + push: async (env: Envelope) => { + try { + const serialized = await serializeEnvelope(env); + await push(getStore(), serialized, options.maxQueueSize || 30); + } catch (_) { + // + } + }, + unshift: async (env: Envelope) => { try { const serialized = await serializeEnvelope(env); - await insert(getStore(), serialized, options.maxQueueSize || 30); + await unshift(getStore(), serialized, options.maxQueueSize || 30); } catch (_) { // } }, - pop: async () => { + shift: async () => { try { - const deserialized = await pop(getStore()); + const deserialized = await shift(getStore()); if (deserialized) { return parseEnvelope(deserialized); } diff --git a/packages/browser/test/unit/transports/offline.test.ts b/packages/browser/test/unit/transports/offline.test.ts index ccc206dbdf97..ed4cd770101d 100644 --- a/packages/browser/test/unit/transports/offline.test.ts +++ b/packages/browser/test/unit/transports/offline.test.ts @@ -11,7 +11,7 @@ import type { import { createEnvelope } from '@sentry/utils'; import { MIN_DELAY } from '../../../../core/src/transports/offline'; -import { createStore, insert, makeBrowserOfflineTransport, pop } from '../../../src/transports/offline'; +import { createStore, makeBrowserOfflineTransport, push, shift, unshift } from '../../../src/transports/offline'; function deleteDatabase(name: string): Promise { return new Promise((resolve, reject) => { @@ -63,21 +63,24 @@ describe('makeOfflineTransport', () => { (global as any).TextDecoder = TextDecoder; }); - it('indexedDb wrappers insert and pop', async () => { + it('indexedDb wrappers push, unshift and pop', async () => { const store = createStore('test', 'test'); - const found = await pop(store); + const found = await shift(store); expect(found).toBeUndefined(); - await insert(store, 'test1', 30); - await insert(store, new Uint8Array([1, 2, 3, 4, 5]), 30); + await push(store, 'test1', 30); + await push(store, new Uint8Array([1, 2, 3, 4, 5]), 30); + await unshift(store, 'test2', 30); - const found2 = await pop(store); - expect(found2).toEqual('test1'); - const found3 = await pop(store); - expect(found3).toEqual(new Uint8Array([1, 2, 3, 4, 5])); + const found2 = await shift(store); + expect(found2).toEqual('test2'); + const found3 = await shift(store); + expect(found3).toEqual('test1'); + const found4 = await shift(store); + expect(found4).toEqual(new Uint8Array([1, 2, 3, 4, 5])); - const found4 = await pop(store); - expect(found4).toBeUndefined(); + const found5 = await shift(store); + expect(found5).toBeUndefined(); }); it('Queues and retries envelope if wrapped transport throws error', async () => { @@ -104,7 +107,7 @@ describe('makeOfflineTransport', () => { const result2 = await transport.send(ERROR_ENVELOPE); expect(result2).toEqual({ statusCode: 200 }); - await delay(MIN_DELAY * 2); + await delay(MIN_DELAY * 5); expect(queuedCount).toEqual(1); expect(getSendCount()).toEqual(2); diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index 2e0db450ddfd..9d30b8cb34ec 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -7,13 +7,10 @@ export const MIN_DELAY = 100; // 100 ms export const START_DELAY = 5_000; // 5 seconds const MAX_DELAY = 3.6e6; // 1 hour -function log(msg: string, error?: Error): void { - DEBUG_BUILD && logger.info(`[Offline]: ${msg}`, error); -} - export interface OfflineStore { - insert(env: Envelope): Promise; - pop(): Promise; + push(env: Envelope): Promise; + unshift(env: Envelope): Promise; + shift(): Promise; } export type CreateOfflineStore = (options: OfflineTransportOptions) => OfflineStore; @@ -53,19 +50,25 @@ type Timer = number | { unref?: () => void }; export function makeOfflineTransport( createTransport: (options: TO) => Transport, ): (options: TO & OfflineTransportOptions) => Transport { + function log(...args: unknown[]): void { + DEBUG_BUILD && logger.info('[Offline]:', ...args); + } + return options => { const transport = createTransport(options); - const store = options.createStore ? options.createStore(options) : undefined; + + if (!options.createStore) { + throw new Error('No `createStore` function was provided'); + } + + const store = options.createStore(options); let retryDelay = START_DELAY; let flushTimer: Timer | undefined; function shouldQueue(env: Envelope, error: Error, retryDelay: number): boolean | Promise { - // We don't queue Session Replay envelopes because they are: - // - Ordered and Replay relies on the response status to know when they're successfully sent. - // - Likely to fill the queue quickly and block other events from being sent. - // We also want to drop client reports because they can be generated when we retry sending events while offline. - if (envelopeContainsItemType(env, ['replay_event', 'replay_recording', 'client_report'])) { + // We want to drop client reports because they can be generated when we retry sending events while offline. + if (envelopeContainsItemType(env, ['client_report'])) { return false; } @@ -77,10 +80,6 @@ export function makeOfflineTransport( } function flushIn(delay: number): void { - if (!store) { - return; - } - if (flushTimer) { clearTimeout(flushTimer as ReturnType); } @@ -88,10 +87,14 @@ export function makeOfflineTransport( flushTimer = setTimeout(async () => { flushTimer = undefined; - const found = await store.pop(); + const found = await store.shift(); if (found) { log('Attempting to send previously queued event'); - void send(found).catch(e => { + + // We should to update the sent_at timestamp to the current time. + found[0].sent_at = new Date().toISOString(); + + void send(found, true).catch(e => { log('Failed to retry sending', e); }); } @@ -113,7 +116,15 @@ export function makeOfflineTransport( retryDelay = Math.min(retryDelay * 2, MAX_DELAY); } - async function send(envelope: Envelope): Promise { + async function send(envelope: Envelope, isRetry: boolean = false): Promise { + // We queue all replay envelopes to avoid multiple replay envelopes being sent at the same time. If one fails, we + // need to retry them in order. + if (!isRetry && envelopeContainsItemType(envelope, ['replay_event', 'replay_recording'])) { + await store.push(envelope); + flushIn(MIN_DELAY); + return {}; + } + try { const result = await transport.send(envelope); @@ -123,6 +134,8 @@ export function makeOfflineTransport( // If there's a retry-after header, use that as the next delay. if (result.headers && result.headers['retry-after']) { delay = parseRetryAfterHeader(result.headers['retry-after']); + } else if (result.headers && result.headers['x-sentry-rate-limits']) { + delay = 60_000; // 60 seconds } // If we have a server error, return now so we don't flush the queue. else if ((result.statusCode || 0) >= 400) { return result; @@ -133,10 +146,15 @@ export function makeOfflineTransport( retryDelay = START_DELAY; return result; } catch (e) { - if (store && (await shouldQueue(envelope, e as Error, retryDelay))) { - await store.insert(envelope); + if (await shouldQueue(envelope, e as Error, retryDelay)) { + // If this envelope was a retry, we want to add it to the front of the queue so it's retried again first. + if (isRetry) { + await store.unshift(envelope); + } else { + await store.push(envelope); + } flushWithBackOff(); - log('Error sending. Event queued', e as Error); + log('Error sending. Event queued.', e as Error); return {}; } else { throw e; diff --git a/packages/core/test/lib/transports/offline.test.ts b/packages/core/test/lib/transports/offline.test.ts index 7e87115b9cdb..2368f61d5892 100644 --- a/packages/core/test/lib/transports/offline.test.ts +++ b/packages/core/test/lib/transports/offline.test.ts @@ -6,7 +6,6 @@ import type { InternalBaseTransportOptions, ReplayEnvelope, ReplayEvent, - Transport, TransportMakeRequestResponse, } from '@sentry/types'; import { @@ -15,6 +14,7 @@ import { createEventEnvelopeHeaders, dsnFromString, getSdkMetadataForEnvelopeHeader, + parseEnvelope, } from '@sentry/utils'; import { createTransport } from '../../../src'; @@ -25,34 +25,40 @@ const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4b [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, ]); -const REPLAY_EVENT: ReplayEvent = { - type: 'replay_event', - timestamp: 1670837008.634, - error_ids: ['errorId'], - trace_ids: ['traceId'], - urls: ['https://example.com'], - replay_id: 'MY_REPLAY_ID', - segment_id: 3, - replay_type: 'buffer', -}; +function REPLAY_EVENT(message: string): ReplayEvent { + return { + type: 'replay_event', + timestamp: 1670837008.634, + error_ids: ['errorId'], + trace_ids: ['traceId'], + urls: ['https://example.com'], + replay_id: 'MY_REPLAY_ID', + segment_id: 3, + replay_type: 'buffer', + message, + }; +} const DSN = dsnFromString('https://public@dsn.ingest.sentry.io/1337')!; const DATA = 'nothing'; -const RELAY_ENVELOPE = createEnvelope( - createEventEnvelopeHeaders(REPLAY_EVENT, getSdkMetadataForEnvelopeHeader(REPLAY_EVENT), undefined, DSN), - [ - [{ type: 'replay_event' }, REPLAY_EVENT], +function REPLAY_ENVELOPE(message: string) { + const event = REPLAY_EVENT(message); + return createEnvelope( + createEventEnvelopeHeaders(event, getSdkMetadataForEnvelopeHeader(event), undefined, DSN), [ - { - type: 'replay_recording', - length: DATA.length, - }, - DATA, + [{ type: 'replay_event' }, event], + [ + { + type: 'replay_recording', + length: DATA.length, + }, + DATA, + ], ], - ], -); + ); +} const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [ { @@ -79,22 +85,21 @@ const transportOptions = { type MockResult = T | Error; -const createTestTransport = ( - ...sendResults: MockResult[] -): { getSendCount: () => number; baseTransport: (options: InternalBaseTransportOptions) => Transport } => { - let sendCount = 0; +const createTestTransport = (...sendResults: MockResult[]) => { + const sentEnvelopes: (string | Uint8Array)[] = []; return { - getSendCount: () => sendCount, + getSentEnvelopes: () => sentEnvelopes, + getSendCount: () => sentEnvelopes.length, baseTransport: (options: InternalBaseTransportOptions) => - createTransport(options, () => { + createTransport(options, ({ body }) => { return new Promise((resolve, reject) => { const next = sendResults.shift(); if (next instanceof Error) { reject(next); } else { - sendCount += 1; + sentEnvelopes.push(body); resolve(next as TransportMakeRequestResponse); } }); @@ -102,7 +107,7 @@ const createTestTransport = ( }; }; -type StoreEvents = ('add' | 'pop')[]; +type StoreEvents = ('push' | 'unshift' | 'shift')[]; function createTestStore(...popResults: MockResult[]): { getCalls: () => StoreEvents; @@ -113,14 +118,20 @@ function createTestStore(...popResults: MockResult[]): { return { getCalls: () => calls, store: (_: OfflineTransportOptions) => ({ - insert: async env => { + push: async env => { if (popResults.length < 30) { popResults.push(env); - calls.push('add'); + calls.push('push'); } }, - pop: async () => { - calls.push('pop'); + unshift: async env => { + if (popResults.length < 30) { + popResults.unshift(env); + calls.push('unshift'); + } + }, + shift: async () => { + calls.push('shift'); const next = popResults.shift(); if (next instanceof Error) { @@ -129,6 +140,7 @@ function createTestStore(...popResults: MockResult[]): { return next; }, + count: async () => popResults.length, }), }; } @@ -170,10 +182,10 @@ describe('makeOfflineTransport', () => { await waitUntil(() => getCalls().length == 1, 1_000); // After a successful send, the store should be checked - expect(getCalls()).toEqual(['pop']); + expect(getCalls()).toEqual(['shift']); }); - it('After successfully sending, sends further envelopes found in the store', async () => { + it('Envelopes are added after existing envelopes in the queue', async () => { const { getCalls, store } = createTestStore(ERROR_ENVELOPE); const { getSendCount, baseTransport } = createTestTransport({ statusCode: 200 }, { statusCode: 200 }); const transport = makeOfflineTransport(baseTransport)({ ...transportOptions, createStore: store }); @@ -185,7 +197,7 @@ describe('makeOfflineTransport', () => { expect(getSendCount()).toEqual(2); // After a successful send from the store, the store should be checked again to ensure it's empty - expect(getCalls()).toEqual(['pop', 'pop']); + expect(getCalls()).toEqual(['shift', 'shift']); }); it('Queues envelope if wrapped transport throws error', async () => { @@ -208,7 +220,7 @@ describe('makeOfflineTransport', () => { expect(getSendCount()).toEqual(0); expect(queuedCount).toEqual(1); - expect(getCalls()).toEqual(['add']); + expect(getCalls()).toEqual(['push']); }); it('Does not queue envelopes if status code >= 400', async () => { @@ -242,18 +254,18 @@ describe('makeOfflineTransport', () => { const transport = makeOfflineTransport(baseTransport)({ ...transportOptions, createStore: store }); const result = await transport.send(ERROR_ENVELOPE); expect(result).toEqual({}); - expect(getCalls()).toEqual(['add']); + expect(getCalls()).toEqual(['push']); await waitUntil(() => getCalls().length === 3 && getSendCount() === 1, START_DELAY * 2); expect(getSendCount()).toEqual(1); - expect(getCalls()).toEqual(['add', 'pop', 'pop']); + expect(getCalls()).toEqual(['push', 'shift', 'shift']); }, START_DELAY + 2_000, ); it( - 'When enabled, sends envelopes found in store shortly after startup', + 'When flushAtStartup is enabled, sends envelopes found in store shortly after startup', async () => { const { getCalls, store } = createTestStore(ERROR_ENVELOPE, ERROR_ENVELOPE); const { getSendCount, baseTransport } = createTestTransport({ statusCode: 200 }, { statusCode: 200 }); @@ -267,7 +279,58 @@ describe('makeOfflineTransport', () => { await waitUntil(() => getCalls().length === 3 && getSendCount() === 2, START_DELAY * 2); expect(getSendCount()).toEqual(2); - expect(getCalls()).toEqual(['pop', 'pop', 'pop']); + expect(getCalls()).toEqual(['shift', 'shift', 'shift']); + }, + START_DELAY + 2_000, + ); + + it( + 'Unshifts envelopes on retry failure', + async () => { + const { getCalls, store } = createTestStore(ERROR_ENVELOPE); + const { getSendCount, baseTransport } = createTestTransport(new Error(), { statusCode: 200 }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _transport = makeOfflineTransport(baseTransport)({ + ...transportOptions, + createStore: store, + flushAtStartup: true, + }); + + await waitUntil(() => getCalls().length === 2, START_DELAY * 2); + + expect(getSendCount()).toEqual(0); + expect(getCalls()).toEqual(['shift', 'unshift']); + }, + START_DELAY + 2_000, + ); + + it( + 'Updates sent_at envelope header on retry', + async () => { + const testStartTime = new Date(); + + // Create an envelope with a sent_at header very far in the past + const env: EventEnvelope = [...ERROR_ENVELOPE]; + env[0].sent_at = new Date(2020, 1, 1).toISOString(); + + const { getCalls, store } = createTestStore(ERROR_ENVELOPE); + const { getSentEnvelopes, baseTransport } = createTestTransport({ statusCode: 200 }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _transport = makeOfflineTransport(baseTransport)({ + ...transportOptions, + createStore: store, + flushAtStartup: true, + }); + + await waitUntil(() => getCalls().length >= 1, START_DELAY * 2); + expect(getCalls()).toEqual(['shift']); + + // When it gets shifted out of the store, the sent_at header should be updated + const envelopes = getSentEnvelopes().map(parseEnvelope) as EventEnvelope[]; + expect(envelopes[0][0]).toBeDefined(); + const sent_at = new Date(envelopes[0][0].sent_at); + + expect(sent_at.getTime()).toBeGreaterThan(testStartTime.getTime()); }, START_DELAY + 2_000, ); @@ -289,23 +352,6 @@ describe('makeOfflineTransport', () => { expect(getCalls()).toEqual([]); }); - it('should not store Relay envelopes on send failure', async () => { - const { getCalls, store } = createTestStore(); - const { getSendCount, baseTransport } = createTestTransport(new Error()); - const queuedCount = 0; - const transport = makeOfflineTransport(baseTransport)({ - ...transportOptions, - createStore: store, - shouldStore: () => true, - }); - const result = transport.send(RELAY_ENVELOPE); - - await expect(result).rejects.toBeInstanceOf(Error); - expect(queuedCount).toEqual(0); - expect(getSendCount()).toEqual(0); - expect(getCalls()).toEqual([]); - }); - it('should not store client report envelopes on send failure', async () => { const { getCalls, store } = createTestStore(); const { getSendCount, baseTransport } = createTestTransport(new Error()); @@ -323,6 +369,47 @@ describe('makeOfflineTransport', () => { expect(getCalls()).toEqual([]); }); + it( + 'Sends replay envelopes in order', + async () => { + const { getCalls, store } = createTestStore(REPLAY_ENVELOPE('1'), REPLAY_ENVELOPE('2')); + const { getSendCount, getSentEnvelopes, baseTransport } = createTestTransport( + new Error(), + { statusCode: 200 }, + { statusCode: 200 }, + { statusCode: 200 }, + ); + const transport = makeOfflineTransport(baseTransport)({ ...transportOptions, createStore: store }); + const result = await transport.send(REPLAY_ENVELOPE('3')); + + expect(result).toEqual({}); + expect(getCalls()).toEqual(['push']); + + await waitUntil(() => getCalls().length === 6 && getSendCount() === 3, START_DELAY * 5); + + expect(getSendCount()).toEqual(3); + expect(getCalls()).toEqual([ + // We're sending a replay envelope and they always get queued + 'push', + // The first envelope popped out fails to send so it gets added to the front of the queue + 'shift', + 'unshift', + // The rest of the attempts succeed + 'shift', + 'shift', + 'shift', + ]); + + const envelopes = getSentEnvelopes().map(parseEnvelope); + + // Ensure they're still in the correct order + expect((envelopes[0][1][0][1] as ErrorEvent).message).toEqual('1'); + expect((envelopes[1][1][0][1] as ErrorEvent).message).toEqual('2'); + expect((envelopes[2][1][0][1] as ErrorEvent).message).toEqual('3'); + }, + START_DELAY + 2_000, + ); + // eslint-disable-next-line jest/no-disabled-tests it.skip( 'Follows the Retry-After header', @@ -360,7 +447,7 @@ describe('makeOfflineTransport', () => { expect(getSendCount()).toEqual(2); expect(queuedCount).toEqual(0); - expect(getCalls()).toEqual(['pop', 'pop']); + expect(getCalls()).toEqual(['shift', 'shift']); }, START_DELAY * 3, );