diff --git a/packages/browser-integration-tests/suites/replay/eventBufferError/template.html b/packages/browser-integration-tests/suites/replay/eventBufferError/template.html new file mode 100644 index 000000000000..24fc4828baf1 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/eventBufferError/template.html @@ -0,0 +1,10 @@ + + +
+ + + + + + + diff --git a/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts b/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts new file mode 100644 index 000000000000..10e9ad6f7196 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/eventBufferError/test.ts @@ -0,0 +1,89 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser } from '../../../utils/helpers'; +import { + getDecompressedRecordingEvents, + getReplaySnapshot, + isReplayEvent, + REPLAY_DEFAULT_FLUSH_MAX_DELAY, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../utils/replayHelpers'; + +sentryTest( + 'should stop recording when running into eventBuffer error', + async ({ getLocalTestPath, page, forceFlushReplay }) => { + if (shouldSkipReplayTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + await waitForReplayRequest(page); + const replay = await getReplaySnapshot(page); + expect(replay._isEnabled).toBe(true); + + await forceFlushReplay(); + + let called = 0; + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + const event = envelopeRequestParser(route.request()); + + // We only want to count replays here + if (event && isReplayEvent(event)) { + const events = getDecompressedRecordingEvents(route.request()); + // this makes sure we ignore e.g. mouse move events which can otherwise lead to flakes + if (events.length > 0) { + called++; + } + } + + return route.fulfill({ + status: 200, + }); + }); + + called = 0; + + /** + * We test the following here: + * 1. First click should add an event (so the eventbuffer is not empty) + * 2. Second click should throw an error in eventBuffer (which should lead to stopping the replay) + * 3. Nothing should be sent to API, as we stop the replay due to the eventBuffer error. + */ + await page.evaluate(` +window._count = 0; +window._addEvent = window.Replay._replay.eventBuffer.addEvent.bind(window.Replay._replay.eventBuffer); +window.Replay._replay.eventBuffer.addEvent = (...args) => { + window._count++; + if (window._count === 2) { + throw new Error('provoked error'); + } + window._addEvent(...args); +}; +`); + + void page.click('#button1'); + void page.click('#button2'); + + // Should immediately skip retrying and just cancel, no backoff + // This waitForTimeout call should be okay, as we're not checking for any + // further network requests afterwards. + await page.waitForTimeout(REPLAY_DEFAULT_FLUSH_MAX_DELAY + 100); + + expect(called).toBe(0); + + const replay2 = await getReplaySnapshot(page); + + expect(replay2._isEnabled).toBe(false); + }, +); diff --git a/packages/browser-integration-tests/utils/replayHelpers.ts b/packages/browser-integration-tests/utils/replayHelpers.ts index cb05baffb62b..5ddcf0c46e05 100644 --- a/packages/browser-integration-tests/utils/replayHelpers.ts +++ b/packages/browser-integration-tests/utils/replayHelpers.ts @@ -267,7 +267,7 @@ function getOptionsEvents(replayRequest: Request): CustomRecordingEvent[] { return getAllCustomRrwebRecordingEvents(events).filter(data => data.tag === 'options'); } -function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] { +export function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] { const replayRequest = getRequest(resOrReq); return ( (replayEnvelopeRequestParser(replayRequest, 5) as eventWithTime[]) @@ -302,7 +302,7 @@ const replayEnvelopeRequestParser = (request: Request | null, envelopeIndex = 2) return envelope[envelopeIndex] as Event; }; -const replayEnvelopeParser = (request: Request | null): unknown[] => { +export const replayEnvelopeParser = (request: Request | null): unknown[] => { // https://develop.sentry.dev/sdk/envelopes/ const envelopeBytes = request?.postDataBuffer() || ''; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 15a39391636c..e4c17ea04ba1 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -263,7 +263,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, return Promise.resolve(); } - return this._replay.stop(); + return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' }); } /** diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 4a0a7e91d97d..6bba4eb66a0a 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -368,7 +368,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Currently, this needs to be manually called (e.g. for tests). Sentry SDK * does not support a teardown */ - public async stop(reason?: string): Promise