From 98eeb59b151eae972ee886b7a94b7c1971ead650 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Jul 2023 14:53:33 +0200 Subject: [PATCH] feat(replay): Ensure too short/long sessions are not flushed --- .../suites/replay/bufferMode/init.js | 1 + .../captureReplayFromReplayPackage/init.js | 1 + .../suites/replay/compression/init.js | 1 + .../suites/replay/customEvents/init.js | 1 + .../suites/replay/dsc/init.js | 1 + .../suites/replay/errors/droppedError/init.js | 1 + .../errors/errorModeCustomTransport/init.js | 1 + .../suites/replay/errors/errorNotSent/init.js | 1 + .../replay/errors/errorsInSession/init.js | 1 + .../suites/replay/errors/init.js | 1 + .../fetch/captureRequestBody/init.js | 1 + .../fetch/captureRequestHeaders/init.js | 1 + .../fetch/captureResponseBody/init.js | 1 + .../fetch/captureResponseHeaders/init.js | 1 + .../extendNetworkBreadcrumbs/fetch/init.js | 1 + .../xhr/captureRequestBody/init.js | 1 + .../xhr/captureRequestHeaders/init.js | 1 + .../xhr/captureResponseBody/init.js | 1 + .../xhr/captureResponseHeaders/init.js | 1 + .../extendNetworkBreadcrumbs/xhr/init.js | 1 + .../suites/replay/fileInput/init.js | 1 + .../suites/replay/flushing/init.js | 1 + .../suites/replay/init.js | 1 + .../suites/replay/keyboardEvents/init.js | 1 + .../largeMutations/defaultOptions/init.js | 1 + .../largeMutations/mutationLimit/init.js | 1 + .../suites/replay/minReplayDuration/init.js | 18 ++++++ .../replay/minReplayDuration/template.html | 10 +++ .../suites/replay/minReplayDuration/test.ts | 63 +++++++++++++++++++ .../suites/replay/multiple-pages/init.js | 1 + .../suites/replay/privacyBlock/init.js | 1 + .../suites/replay/privacyDefault/init.js | 1 + .../suites/replay/privacyInput/init.js | 1 + .../suites/replay/privacyInputMaskAll/init.js | 1 + .../suites/replay/replayShim/init.js | 1 + .../suites/replay/requests/init.js | 1 + .../suites/replay/sampling/init.js | 1 + .../suites/replay/sessionExpiry/init.js | 1 + .../suites/replay/sessionInactive/init.js | 1 + .../suites/replay/sessionMaxAge/init.js | 1 + .../suites/replay/slowClick/disable/init.js | 1 + .../suites/replay/slowClick/init.js | 1 + .../suites/replay/throttleBreadcrumbs/init.js | 1 + .../suites/replay/unicode/compressed/init.js | 1 + .../replay/unicode/uncompressed/init.js | 1 + packages/replay/src/constants.ts | 5 ++ packages/replay/src/integration.ts | 9 ++- packages/replay/src/replay.ts | 18 ++++++ packages/replay/src/types/replay.ts | 7 +++ .../test/integration/errorSampleRate.test.ts | 6 ++ .../replay/test/integration/flush.test.ts | 55 ++++++++++++++-- packages/replay/test/mocks/mockSdk.ts | 1 + .../test/unit/session/getSession.test.ts | 1 + .../replay/test/utils/setupReplayContainer.ts | 1 + 54 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 packages/browser-integration-tests/suites/replay/minReplayDuration/init.js create mode 100644 packages/browser-integration-tests/suites/replay/minReplayDuration/template.html create mode 100644 packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/init.js b/packages/browser-integration-tests/suites/replay/bufferMode/init.js index 5691b52d96a3..2453efcfbe1d 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/init.js +++ b/packages/browser-integration-tests/suites/replay/bufferMode/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js index 16b46e3adc54..be0f9cab95d5 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js +++ b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js @@ -5,6 +5,7 @@ window.Sentry = Sentry; window.Replay = new Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/compression/init.js b/packages/browser-integration-tests/suites/replay/compression/init.js index 0c068170f01e..c2dd47ab0c25 100644 --- a/packages/browser-integration-tests/suites/replay/compression/init.js +++ b/packages/browser-integration-tests/suites/replay/compression/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: true, }); diff --git a/packages/browser-integration-tests/suites/replay/customEvents/init.js b/packages/browser-integration-tests/suites/replay/customEvents/init.js index 5ddfef307e9c..f76a1207243b 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, blockAllMedia: false, }); diff --git a/packages/browser-integration-tests/suites/replay/dsc/init.js b/packages/browser-integration-tests/suites/replay/dsc/init.js index 90919aeaeb70..c3c2f62f2c14 100644 --- a/packages/browser-integration-tests/suites/replay/dsc/init.js +++ b/packages/browser-integration-tests/suites/replay/dsc/init.js @@ -5,6 +5,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js b/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js index 1a441494482c..2c9cd8b23147 100644 --- a/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/droppedError/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js b/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js index c925f082c665..acf3b91c0dd3 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js b/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js index 528ca8b3285e..49d938b15060 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js index dc77197ef93f..29486082ff8a 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/errors/init.js b/packages/browser-integration-tests/suites/replay/errors/init.js index 863d143939cf..89c185dacc7f 100644 --- a/packages/browser-integration-tests/suites/replay/errors/init.js +++ b/packages/browser-integration-tests/suites/replay/errors/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js index 2323cd2dda7f..21c548a5e349 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo', 'http://sentry-test.io/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js index a60fcdcfc530..4c3f0a7969c6 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkRequestHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js index 15be2bb2764d..3aa81d299ae2 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js index 241dcc7adc29..ff1e66e53411 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkResponseHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js index 6f80c5e4cb8f..52c219e99dc9 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js index 2323cd2dda7f..21c548a5e349 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo', 'http://sentry-test.io/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js index a60fcdcfc530..4c3f0a7969c6 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkRequestHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js index 15be2bb2764d..3aa81d299ae2 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkCaptureBodies: true, diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js index 241dcc7adc29..ff1e66e53411 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, networkDetailAllowUrls: ['http://localhost:7654/foo'], networkResponseHeaders: ['X-Test-Header'], diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js index 6f80c5e4cb8f..52c219e99dc9 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/fileInput/init.js b/packages/browser-integration-tests/suites/replay/fileInput/init.js index 4081a8b9182d..0e08fdfaa6d0 100644 --- a/packages/browser-integration-tests/suites/replay/fileInput/init.js +++ b/packages/browser-integration-tests/suites/replay/fileInput/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllInputs: false, }); diff --git a/packages/browser-integration-tests/suites/replay/flushing/init.js b/packages/browser-integration-tests/suites/replay/flushing/init.js index 10f4385a1369..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/packages/browser-integration-tests/suites/replay/flushing/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/init.js b/packages/browser-integration-tests/suites/replay/init.js index 7a0337445768..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/init.js +++ b/packages/browser-integration-tests/suites/replay/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js b/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js index 7a0337445768..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/packages/browser-integration-tests/suites/replay/keyboardEvents/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js index 7a0337445768..dac512988b9a 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index ba43582d7816..35c6feed4df7 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, mutationLimit: 250, }); diff --git a/packages/browser-integration-tests/suites/replay/minReplayDuration/init.js b/packages/browser-integration-tests/suites/replay/minReplayDuration/init.js new file mode 100644 index 000000000000..429559c5781a --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/minReplayDuration/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 2000, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + debug: true, + + integrations: [window.Replay], +}); diff --git a/packages/browser-integration-tests/suites/replay/minReplayDuration/template.html b/packages/browser-integration-tests/suites/replay/minReplayDuration/template.html new file mode 100644 index 000000000000..7223a20f82ba --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/minReplayDuration/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts b/packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts new file mode 100644 index 000000000000..b0c19794272e --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts @@ -0,0 +1,63 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates'; +import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; + +const MIN_DURATION = 2000; + +sentryTest('doest not send replay before min. duration', async ({ getLocalTestPath, page, forceFlushReplay }) => { + if (shouldSkipReplayTest()) { + sentryTest.skip(); + } + + let counter = 0; + const reqPromise0 = waitForReplayRequest(page, () => { + counter++; + return true; + }); + + 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 }); + + await page.goto(url); + + // This triggers a page blur, which should trigger a flush + // However, as we are only here too short, this should not actually _send_ anything + await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, +}); +document.dispatchEvent(new Event('visibilitychange'));`); + expect(counter).toBe(0); + + // Now wait for 2s until min duration is reached, and try again + await new Promise(resolve => setTimeout(resolve, MIN_DURATION)); + await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + document.dispatchEvent(new Event('visibilitychange'));`); + await page.evaluate(`Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange'));`); + + const replayEvent0 = getReplayEvent(await reqPromise0); + expect(replayEvent0).toEqual(getExpectedReplayEvent({})); + expect(counter).toBe(1); +}); diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/init.js b/packages/browser-integration-tests/suites/replay/multiple-pages/init.js index 105c0114f2b0..a856a0d13c3e 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/init.js +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/init.js b/packages/browser-integration-tests/suites/replay/privacyBlock/init.js index 6c37e3d85e44..f5360c53561b 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, blockAllMedia: false, block: ['link[rel="icon"]', 'video', '.nested-hide'], diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/init.js b/packages/browser-integration-tests/suites/replay/privacyDefault/init.js index 10f4385a1369..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/init.js b/packages/browser-integration-tests/suites/replay/privacyInput/init.js index 4081a8b9182d..0e08fdfaa6d0 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInput/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyInput/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllInputs: false, }); diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js index cff4c6dab7bd..1657e879ef87 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllInputs: true, }); diff --git a/packages/browser-integration-tests/suites/replay/replayShim/init.js b/packages/browser-integration-tests/suites/replay/replayShim/init.js index b53ada8b5e7c..a89c744e7480 100644 --- a/packages/browser-integration-tests/suites/replay/replayShim/init.js +++ b/packages/browser-integration-tests/suites/replay/replayShim/init.js @@ -6,6 +6,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/requests/init.js b/packages/browser-integration-tests/suites/replay/requests/init.js index 10f4385a1369..db9828fe889e 100644 --- a/packages/browser-integration-tests/suites/replay/requests/init.js +++ b/packages/browser-integration-tests/suites/replay/requests/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/sampling/init.js b/packages/browser-integration-tests/suites/replay/sampling/init.js index 9e99c3536d05..8a98bb9c45de 100644 --- a/packages/browser-integration-tests/suites/replay/sampling/init.js +++ b/packages/browser-integration-tests/suites/replay/sampling/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js index d9008eb4f9d6..a3b9726f3103 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js b/packages/browser-integration-tests/suites/replay/sessionInactive/init.js index 5de219254c61..781e7b583109 100644 --- a/packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionInactive/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js index 4d7fc285d7b7..de8b260647ad 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js b/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js index b7008ced8f6b..aa5be4406824 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/disable/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, slowClickTimeout: 0, }); diff --git a/packages/browser-integration-tests/suites/replay/slowClick/init.js b/packages/browser-integration-tests/suites/replay/slowClick/init.js index 367357937e9a..030b2722d236 100644 --- a/packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, slowClickTimeout: 3100, slowClickIgnoreSelectors: ['.ignore-class', '[ignore-attribute]'], }); diff --git a/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js b/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js index 11207b23752d..3146e64131fd 100644 --- a/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js +++ b/packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 5000, flushMaxDelay: 5000, + minReplayDuration: 0, useCompression: false, }); diff --git a/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js b/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js index 0420898a5b88..2fe6781ee15e 100644 --- a/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js +++ b/packages/browser-integration-tests/suites/replay/unicode/compressed/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: true, maskAllText: false, }); diff --git a/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js b/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js index a4a5a0cbd5c6..956586937a19 100644 --- a/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js +++ b/packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js @@ -4,6 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, + minReplayDuration: 0, useCompression: false, maskAllText: false, }); diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index 86dc228c50bf..0f14d5f00ba9 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -45,3 +45,8 @@ export const SLOW_CLICK_SCROLL_TIMEOUT = 300; /** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */ export const REPLAY_MAX_EVENT_BUFFER_SIZE = 20_000_000; // ~20MB + +/** Replays must be min. 4s long before we send them. */ +export const MIN_REPLAY_DURATION = 4_000; +/* The max. allowed value that the minReplayDuration can be set to. */ +export const MIN_REPLAY_DURATION_MAX = 15_000; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 2e795829894b..008478d81018 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -2,7 +2,12 @@ import { getCurrentHub } from '@sentry/core'; import type { BrowserClientReplayOptions, Integration } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; -import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY } from './constants'; +import { + DEFAULT_FLUSH_MAX_DELAY, + DEFAULT_FLUSH_MIN_DELAY, + MIN_REPLAY_DURATION, + MIN_REPLAY_DURATION_MAX, +} from './constants'; import { ReplayContainer } from './replay'; import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions, SendBufferedReplayOptions } from './types'; import { getPrivacyOptions } from './util/getPrivacyOptions'; @@ -51,6 +56,7 @@ export class Replay implements Integration { public constructor({ flushMinDelay = DEFAULT_FLUSH_MIN_DELAY, flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY, + minReplayDuration = MIN_REPLAY_DURATION, stickySession = true, useCompression = true, _experiments = {}, @@ -127,6 +133,7 @@ export class Replay implements Integration { this._initialOptions = { flushMinDelay, flushMaxDelay, + minReplayDuration: Math.min(minReplayDuration, MIN_REPLAY_DURATION_MAX), stickySession, sessionSampleRate, errorSampleRate, diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index c72306239533..e94f39e4f8b6 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -7,6 +7,7 @@ import { logger } from '@sentry/utils'; import { BUFFER_CHECKOUT_TIME, MAX_SESSION_LIFE, + MIN_REPLAY_DURATION, SESSION_IDLE_EXPIRE_DURATION, SESSION_IDLE_PAUSE_DURATION, SLOW_CLICK_SCROLL_TIMEOUT, @@ -1100,6 +1101,23 @@ export class ReplayContainer implements ReplayContainerInterface { return; } + const start = this._context.initialTimestamp; + const now = Date.now(); + const duration = now - start; + + // If session is too short, or too long (allow some wiggle room over maxSessionLife), do not send it + // This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar + if (duration < this._options.minReplayDuration || duration > this.timeouts.maxSessionLife + 5_000) { + // eslint-disable-next-line no-console + const log = this.getOptions()._experiments.traceInternals ? console.warn : logger.warn; + __DEBUG_BUILD__ && + log( + `[Replay] Session duration (${Math.floor(duration / 1000)}s) is too short or too long, not sending replay.`, + ); + + return; + } + // A flush is about to happen, cancel any queued flushes this._debouncedFlush.cancel(); diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 00cda5e3c6e7..46f1e8f4ef93 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -180,6 +180,13 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { */ slowClickIgnoreSelectors: string[]; + /** + * The min. duration (in ms) a replay has to have before it is sent to Sentry. + * Whenever attempting to flush a session that is shorter than this, it will not actually send it to Sentry. + * Note that this is capped at max. 15s. + */ + minReplayDuration: number; + /** * Callback before adding a custom recording event * diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index fe3049f9704f..f92f66078c97 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -450,6 +450,9 @@ describe('Integration | errorSampleRate', () => { jest.runAllTimers(); await new Promise(process.nextTick); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); + // still no new replay sent expect(replay).not.toHaveLastSentReplay(); @@ -694,6 +697,9 @@ describe('Integration | errorSampleRate', () => { jest.advanceTimersByTime(2 * MAX_SESSION_LIFE); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); + captureException(new Error('testing')); // Flush due to exception diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index cf142ae8c45c..037aa00eb306 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -1,6 +1,6 @@ import * as SentryUtils from '@sentry/utils'; -import { DEFAULT_FLUSH_MIN_DELAY, WINDOW } from '../../src/constants'; +import { DEFAULT_FLUSH_MIN_DELAY, MAX_SESSION_LIFE, WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import type { EventBuffer } from '../../src/types'; @@ -82,6 +82,10 @@ describe('Integration | flush', () => { mockRunFlush.mockClear(); mockAddMemoryEntry.mockClear(); + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + if (replay.eventBuffer) { jest.spyOn(replay.eventBuffer, 'finish'); } @@ -93,9 +97,6 @@ describe('Integration | flush', () => { jest.runAllTimers(); await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); - sessionStorage.clear(); - clearSession(replay); - replay['_loadAndCheckSession'](); mockRecord.takeFullSnapshot.mockClear(); Object.defineProperty(WINDOW, 'location', { value: prevLocation, @@ -258,4 +259,50 @@ describe('Integration | flush', () => { await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); expect(mockFlush).toHaveBeenCalledTimes(1); }); + + it('does not flush is session is too short', async () => { + replay.getOptions().minReplayDuration = 100_000; + + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + + // click happens first + domHandler({ + name: 'click', + }); + + // checkout + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + mockRecord._emitter(TEST_EVENT); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockFlush).toHaveBeenCalledTimes(1); + expect(mockSendReplay).toHaveBeenCalledTimes(0); + }); + + it('does not flush is session is too long', async () => { + replay.timeouts.maxSessionLife = 100_000; + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + + sessionStorage.clear(); + clearSession(replay); + replay['_loadAndCheckSession'](); + + await advanceTimers(120_000); + + // click happens first + domHandler({ + name: 'click', + }); + + // checkout + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + mockRecord._emitter(TEST_EVENT); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(mockFlush).toHaveBeenCalledTimes(1); + expect(mockSendReplay).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/replay/test/mocks/mockSdk.ts b/packages/replay/test/mocks/mockSdk.ts index 4a27b7286d22..f7e268bb49ba 100644 --- a/packages/replay/test/mocks/mockSdk.ts +++ b/packages/replay/test/mocks/mockSdk.ts @@ -76,6 +76,7 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true } const replayIntegration = new TestReplayIntegration({ stickySession: false, + minReplayDuration: 0, ...replayOptions, }); diff --git a/packages/replay/test/unit/session/getSession.test.ts b/packages/replay/test/unit/session/getSession.test.ts index aa3110d114f2..14457f57ade8 100644 --- a/packages/replay/test/unit/session/getSession.test.ts +++ b/packages/replay/test/unit/session/getSession.test.ts @@ -1,5 +1,6 @@ import { MAX_SESSION_LIFE, + MIN_REPLAY_DURATION, SESSION_IDLE_EXPIRE_DURATION, SESSION_IDLE_PAUSE_DURATION, WINDOW, diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts index e2a49052a799..02a965b7d9c2 100644 --- a/packages/replay/test/utils/setupReplayContainer.ts +++ b/packages/replay/test/utils/setupReplayContainer.ts @@ -6,6 +6,7 @@ import type { RecordingOptions, ReplayPluginOptions } from '../../src/types'; const DEFAULT_OPTIONS = { flushMinDelay: 100, flushMaxDelay: 100, + minReplayDuration: 0, stickySession: false, sessionSampleRate: 0, errorSampleRate: 1,