diff --git a/packages/core/test/lib/transports/offline.test.ts b/packages/core/test/lib/transports/offline.test.ts index 3b491753c8bb..d09401367e5d 100644 --- a/packages/core/test/lib/transports/offline.test.ts +++ b/packages/core/test/lib/transports/offline.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, onTestFinished, vi } from 'vitest'; import { createClientReportEnvelope, createEnvelope, @@ -9,7 +9,7 @@ import { parseEnvelope, } from '../../../src'; import type { CreateOfflineStore, OfflineTransportOptions } from '../../../src/transports/offline'; -import { makeOfflineTransport, START_DELAY } from '../../../src/transports/offline'; +import { makeOfflineTransport, MIN_DELAY, START_DELAY } from '../../../src/transports/offline'; import type { ClientReport } from '../../../src/types-hoist/clientreport'; import type { Envelope, EventEnvelope, EventItem, ReplayEnvelope } from '../../../src/types-hoist/envelope'; import type { ReplayEvent } from '../../../src/types-hoist/replay'; @@ -139,23 +139,13 @@ function createTestStore(...popResults: MockResult[]): { }; } -function waitUntil(fn: () => boolean, timeout: number): Promise { - return new Promise(resolve => { - let runtime = 0; - - const interval = setInterval(() => { - runtime += 100; - - if (fn() || runtime >= timeout) { - clearTimeout(interval); - resolve(); - } - }, 100); - }); -} - describe('makeOfflineTransport', () => { it('Sends envelope and checks the store for further envelopes', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + const { getCalls, store } = createTestStore(); const { getSendCount, baseTransport } = createTestTransport({ statusCode: 200 }); let queuedCount = 0; @@ -173,13 +163,18 @@ describe('makeOfflineTransport', () => { expect(queuedCount).toEqual(0); expect(getSendCount()).toEqual(1); - await waitUntil(() => getCalls().length == 1, 1_000); + await vi.advanceTimersByTimeAsync(START_DELAY); // After a successful send, the store should be checked expect(getCalls()).toEqual(['shift']); }); it('Envelopes are added after existing envelopes in the queue', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + const { getCalls, store } = createTestStore(ERROR_ENVELOPE); const { getSendCount, baseTransport } = createTestTransport({ statusCode: 200 }, { statusCode: 200 }); const transport = makeOfflineTransport(baseTransport)({ ...transportOptions, createStore: store }); @@ -187,7 +182,7 @@ describe('makeOfflineTransport', () => { expect(result).toEqual({ statusCode: 200 }); - await waitUntil(() => getCalls().length == 2, 1_000); + await vi.advanceTimersByTimeAsync(START_DELAY); expect(getSendCount()).toEqual(2); // After a successful send from the store, the store should be checked again to ensure it's empty @@ -195,6 +190,11 @@ describe('makeOfflineTransport', () => { }); it('Queues envelope if wrapped transport throws error', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + const { getCalls, store } = createTestStore(); const { getSendCount, baseTransport } = createTestTransport(new Error()); let queuedCount = 0; @@ -210,7 +210,7 @@ describe('makeOfflineTransport', () => { expect(result).toEqual({}); - await waitUntil(() => getCalls().length === 1, 1_000); + await vi.advanceTimersByTimeAsync(1_000); expect(getSendCount()).toEqual(0); expect(queuedCount).toEqual(1); @@ -218,6 +218,11 @@ describe('makeOfflineTransport', () => { }); it('Does not queue envelopes if status code >= 400', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + const { getCalls, store } = createTestStore(); const { getSendCount, baseTransport } = createTestTransport({ statusCode: 500 }); let queuedCount = 0; @@ -233,101 +238,106 @@ describe('makeOfflineTransport', () => { expect(result).toEqual({ statusCode: 500 }); - await waitUntil(() => getSendCount() === 1, 1_000); + await vi.advanceTimersByTimeAsync(1_000); expect(getSendCount()).toEqual(1); expect(queuedCount).toEqual(0); expect(getCalls()).toEqual([]); }); - it( - 'Retries sending envelope after failure', - async () => { - const { getCalls, store } = createTestStore(); - const { getSendCount, baseTransport } = createTestTransport(new Error(), { statusCode: 200 }); - const transport = makeOfflineTransport(baseTransport)({ ...transportOptions, createStore: store }); - const result = await transport.send(ERROR_ENVELOPE); - expect(result).toEqual({}); - expect(getCalls()).toEqual(['push']); + it('Retries sending envelope after failure', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); - await waitUntil(() => getCalls().length === 3 && getSendCount() === 1, START_DELAY * 2); + const { getCalls, store } = createTestStore(); + const { getSendCount, baseTransport } = createTestTransport(new Error(), { statusCode: 200 }); + const transport = makeOfflineTransport(baseTransport)({ ...transportOptions, createStore: store }); + const result = await transport.send(ERROR_ENVELOPE); + expect(result).toEqual({}); + expect(getCalls()).toEqual(['push']); - expect(getSendCount()).toEqual(1); - expect(getCalls()).toEqual(['push', 'shift', 'shift']); - }, - START_DELAY + 2_000, - ); + await vi.advanceTimersByTimeAsync(START_DELAY * 2); - it( - '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 }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _transport = makeOfflineTransport(baseTransport)({ - ...transportOptions, - createStore: store, - flushAtStartup: true, - }); + expect(getSendCount()).toEqual(1); + expect(getCalls()).toEqual(['push', 'shift', 'shift']); + }); - await waitUntil(() => getCalls().length === 3 && getSendCount() === 2, START_DELAY * 2); + it('When flushAtStartup is enabled, sends envelopes found in store shortly after startup', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); - expect(getSendCount()).toEqual(2); - expect(getCalls()).toEqual(['shift', 'shift', 'shift']); - }, - START_DELAY + 2_000, - ); + const { getCalls, store } = createTestStore(ERROR_ENVELOPE, ERROR_ENVELOPE); + const { getSendCount, baseTransport } = createTestTransport({ statusCode: 200 }, { statusCode: 200 }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _transport = makeOfflineTransport(baseTransport)({ + ...transportOptions, + createStore: store, + flushAtStartup: true, + }); - 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 vi.advanceTimersByTimeAsync(START_DELAY * 2); + + expect(getSendCount()).toEqual(2); + expect(getCalls()).toEqual(['shift', 'shift', 'shift']); + }); - await waitUntil(() => getCalls().length === 2, START_DELAY * 2); + it('Unshifts envelopes on retry failure', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); - expect(getSendCount()).toEqual(0); - expect(getCalls()).toEqual(['shift', 'unshift']); - }, - START_DELAY + 2_000, - ); + 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, + }); - it( - 'Updates sent_at envelope header on retry', - async () => { - const testStartTime = new Date(); + await vi.advanceTimersByTimeAsync(START_DELAY * 2); + + expect(getSendCount()).toEqual(0); + expect(getCalls()).toEqual(['shift', 'unshift']); + }); - // 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(); + it('Updates sent_at envelope header on retry', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); - 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, - }); + const testStartTime = new Date(); - await waitUntil(() => getCalls().length >= 1, START_DELAY * 2); - expect(getCalls()).toEqual(['shift']); + // 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(); - // 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); + 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, + }); - expect(sent_at.getTime()).toBeGreaterThan(testStartTime.getTime()); - }, - START_DELAY + 2_000, - ); + await vi.advanceTimersByTimeAsync(START_DELAY); + + 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()); + }); it('shouldStore can stop envelopes from being stored on send failure', async () => { const { getCalls, store } = createTestStore(); @@ -384,51 +394,56 @@ 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, - ); + it('Sends replay envelopes in order', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + + 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 vi.advanceTimersByTimeAsync(START_DELAY + MIN_DELAY * 3); + + 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'); + }); - // eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests - it.skip( + it( 'Follows the Retry-After header', async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + const { getCalls, store } = createTestStore(ERROR_ENVELOPE); const { getSendCount, baseTransport } = createTestTransport( { @@ -454,11 +469,11 @@ describe('makeOfflineTransport', () => { headers: { 'x-sentry-rate-limits': '', 'retry-after': '3' }, }); - await waitUntil(() => getSendCount() === 1, 500); + await vi.advanceTimersByTimeAsync(2_999); expect(getSendCount()).toEqual(1); - await waitUntil(() => getCalls().length === 2, START_DELAY * 2); + await vi.advanceTimersByTimeAsync(START_DELAY); expect(getSendCount()).toEqual(2); expect(queuedCount).toEqual(0);