diff --git a/packages/integration-tests/suites/replay/errors/errorMode/test.ts b/packages/integration-tests/suites/replay/errors/errorMode/test.ts index 839a5e1ffa46..b4da8aa4d3e4 100644 --- a/packages/integration-tests/suites/replay/errors/errorMode/test.ts +++ b/packages/integration-tests/suites/replay/errors/errorMode/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; import { expectedClickBreadcrumb, expectedConsoleBreadcrumb, @@ -10,6 +10,7 @@ import { import { getReplayEvent, getReplayRecordingContent, + isReplayEvent, shouldSkipReplayTest, waitForReplayRequest, } from '../../../../utils/replayHelpers'; @@ -26,6 +27,7 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); const reqPromise2 = waitForReplayRequest(page, 2); + const reqErrorPromise = waitForErrorRequest(page); await page.route('https://dsn.ingest.sentry.io/**/*', route => { const event = envelopeRequestParser(route.request()); @@ -33,7 +35,10 @@ sentryTest( if (event && !event.type && event.event_id) { errorEventId = event.event_id; } - callsToSentry++; + // We only want to count errors & replays here + if (event && (!event.type || isReplayEvent(event))) { + callsToSentry++; + } return route.fulfill({ status: 200, @@ -46,6 +51,8 @@ sentryTest( await page.goto(url); await page.click('#go-background'); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(callsToSentry).toEqual(0); await page.click('#error'); @@ -53,6 +60,7 @@ sentryTest( await page.click('#go-background'); const req1 = await reqPromise1; + await reqErrorPromise; expect(callsToSentry).toEqual(3); // 1 error, 2 replay events @@ -69,11 +77,12 @@ sentryTest( const event2 = getReplayEvent(req2); const content2 = getReplayRecordingContent(req2); + expect(callsToSentry).toBe(4); // 1 error, 3 replay events + expect(event0).toEqual( getExpectedReplayEvent({ contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, - // @ts-ignore this is fine - error_ids: [errorEventId], + error_ids: [errorEventId!], replay_type: 'error', }), ); @@ -97,7 +106,6 @@ sentryTest( expect(event1).toEqual( getExpectedReplayEvent({ contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, - // @ts-ignore this is fine replay_type: 'error', // although we're in session mode, we still send 'error' as replay_type replay_start_timestamp: undefined, segment_id: 1, @@ -108,14 +116,12 @@ sentryTest( // Also the second snapshot should have a full snapshot, as we switched from error to session // mode which triggers another checkout expect(content1.fullSnapshots).toHaveLength(1); - expect(content1.incrementalSnapshots).toHaveLength(0); // The next event should just be a normal replay event as we're now in session mode and // we continue recording everything expect(event2).toEqual( getExpectedReplayEvent({ contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, - // @ts-ignore this is fine replay_type: 'error', replay_start_timestamp: undefined, segment_id: 2, diff --git a/packages/integration-tests/suites/replay/errors/errorsInSession/test.ts b/packages/integration-tests/suites/replay/errors/errorsInSession/test.ts index d948875cf2d8..79835eef5771 100644 --- a/packages/integration-tests/suites/replay/errors/errorsInSession/test.ts +++ b/packages/integration-tests/suites/replay/errors/errorsInSession/test.ts @@ -39,7 +39,6 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await page.click('#go-background'); const req0 = await reqPromise0; await page.click('#error'); @@ -57,7 +56,6 @@ sentryTest( getExpectedReplayEvent({ replay_start_timestamp: undefined, segment_id: 1, - // @ts-ignore this is fine error_ids: [errorEventId], urls: [], }), diff --git a/packages/integration-tests/suites/replay/errors/init.js b/packages/integration-tests/suites/replay/errors/init.js index d34480954ef5..cd21267f1cc7 100644 --- a/packages/integration-tests/suites/replay/errors/init.js +++ b/packages/integration-tests/suites/replay/errors/init.js @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; window.Replay = new Sentry.Replay({ - flushMinDelay: 500, - flushMaxDelay: 500, + flushMinDelay: 1000, + flushMaxDelay: 1000, }); Sentry.init({ diff --git a/packages/integration-tests/utils/helpers.ts b/packages/integration-tests/utils/helpers.ts index 82ed49435681..25630ceba2fd 100644 --- a/packages/integration-tests/utils/helpers.ts +++ b/packages/integration-tests/utils/helpers.ts @@ -108,6 +108,23 @@ async function getSentryEvents(page: Page, url?: string): Promise> return eventsHandle.jsonValue(); } +export function waitForErrorRequest(page: Page): Promise { + return page.waitForRequest(req => { + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + const event = envelopeRequestParser(req); + + return !event.type; + } catch { + return false; + } + }); +} + /** * Waits until a number of requests matching urlRgx at the given URL arrive. * If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured diff --git a/packages/integration-tests/utils/replayHelpers.ts b/packages/integration-tests/utils/replayHelpers.ts index d1fe2b6b07cb..236431a75ee6 100644 --- a/packages/integration-tests/utils/replayHelpers.ts +++ b/packages/integration-tests/utils/replayHelpers.ts @@ -64,7 +64,7 @@ export function waitForReplayRequest(page: Page, segmentId?: number): Promise { }); }); - it('does not send a replay when triggering a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', async () => { + it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_DURATION]ms', async () => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { @@ -162,7 +162,7 @@ describe('Integration | errorSampleRate', () => { }, }); - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1); document.dispatchEvent(new Event('visibilitychange')); @@ -186,8 +186,8 @@ describe('Integration | errorSampleRate', () => { expect(replay).not.toHaveLastSentReplay(); - // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 100); + // User comes back before `SESSION_IDLE_DURATION` elapses + jest.advanceTimersByTime(SESSION_IDLE_DURATION - 100); Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts index 45938fbc7fa6..57c49ba1e245 100644 --- a/packages/replay/test/integration/events.test.ts +++ b/packages/replay/test/integration/events.test.ts @@ -40,7 +40,7 @@ describe('Integration | events', () => { // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run clearSession(replay); - replay['_loadAndCheckSession'](0); + replay['_loadAndCheckSession'](); mockTransportSend.mockClear(); }); @@ -93,7 +93,7 @@ describe('Integration | events', () => { it('has correct timestamps when there are events earlier than initial timestamp', async function () { clearSession(replay); - replay['_loadAndCheckSession'](0); + replay['_loadAndCheckSession'](); mockTransportSend.mockClear(); Object.defineProperty(document, 'visibilityState', { configurable: true, diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index 2398c22e2f8a..5d91edf483a3 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, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; +import { DEFAULT_FLUSH_MIN_DELAY, WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import type { EventBuffer } from '../../src/types'; import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; @@ -95,7 +95,7 @@ describe('Integration | flush', () => { jest.setSystemTime(new Date(BASE_TIMESTAMP)); sessionStorage.clear(); clearSession(replay); - replay['_loadAndCheckSession'](SESSION_IDLE_DURATION); + replay['_loadAndCheckSession'](); mockRecord.takeFullSnapshot.mockClear(); Object.defineProperty(WINDOW, 'location', { value: prevLocation, diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts index ad0d6cfbdb22..fcd170b31784 100644 --- a/packages/replay/test/integration/rateLimiting.test.ts +++ b/packages/replay/test/integration/rateLimiting.test.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/core'; import type { Transport } from '@sentry/types'; -import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION } from '../../src/constants'; +import { DEFAULT_FLUSH_MIN_DELAY } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import * as SendReplayRequest from '../../src/util/sendReplayRequest'; import { BASE_TIMESTAMP, mockSdk } from '../index'; @@ -46,7 +46,7 @@ describe('Integration | rate-limiting behaviour', () => { // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run clearSession(replay); - replay['_loadAndCheckSession'](0); + replay['_loadAndCheckSession'](); mockSendReplayRequest.mockClear(); }); @@ -57,7 +57,7 @@ describe('Integration | rate-limiting behaviour', () => { jest.setSystemTime(new Date(BASE_TIMESTAMP)); clearSession(replay); jest.clearAllMocks(); - replay['_loadAndCheckSession'](SESSION_IDLE_DURATION); + replay['_loadAndCheckSession'](); }); afterAll(() => { diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts index 3263e1a09772..4f17644a241e 100644 --- a/packages/replay/test/integration/sendReplayEvent.test.ts +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -2,7 +2,7 @@ import * as SentryCore from '@sentry/core'; import type { Transport } from '@sentry/types'; import * as SentryUtils from '@sentry/utils'; -import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; +import { DEFAULT_FLUSH_MIN_DELAY, WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; import * as SendReplayRequest from '../../src/util/sendReplayRequest'; @@ -59,7 +59,7 @@ describe('Integration | sendReplayEvent', () => { // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run clearSession(replay); - replay['_loadAndCheckSession'](0); + replay['_loadAndCheckSession'](); mockSendReplayRequest.mockClear(); }); @@ -69,7 +69,7 @@ describe('Integration | sendReplayEvent', () => { await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); clearSession(replay); - replay['_loadAndCheckSession'](SESSION_IDLE_DURATION); + replay['_loadAndCheckSession'](); }); afterAll(() => { diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index dc54503e922d..d6820db61da7 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -5,7 +5,7 @@ import { DEFAULT_FLUSH_MIN_DELAY, MAX_SESSION_LIFE, REPLAY_SESSION_KEY, - VISIBILITY_CHANGE_TIMEOUT, + SESSION_IDLE_DURATION, WINDOW, } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; @@ -55,7 +55,7 @@ describe('Integration | session', () => { }); }); - it('creates a new session and triggers a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { + it('creates a new session and triggers a full dom snapshot when document becomes visible after [SESSION_IDLE_DURATION]ms', () => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { @@ -65,7 +65,7 @@ describe('Integration | session', () => { const initialSession = replay.session; - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1); document.dispatchEvent(new Event('visibilitychange')); @@ -75,7 +75,7 @@ describe('Integration | session', () => { expect(replay).not.toHaveSameSession(initialSession); }); - it('does not create a new session if user hides the tab and comes back within [VISIBILITY_CHANGE_TIMEOUT] seconds', () => { + it('does not create a new session if user hides the tab and comes back within [SESSION_IDLE_DURATION] seconds', () => { const initialSession = replay.session; Object.defineProperty(document, 'visibilityState', { @@ -88,8 +88,8 @@ describe('Integration | session', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).toHaveSameSession(initialSession); - // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 1); + // User comes back before `SESSION_IDLE_DURATION` elapses + jest.advanceTimersByTime(SESSION_IDLE_DURATION - 1); Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { @@ -184,7 +184,7 @@ describe('Integration | session', () => { expect(replay.session).toBe(undefined); }); - it('creates a new session and triggers a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { + it('creates a new session and triggers a full dom snapshot when document becomes visible after [SESSION_IDLE_DURATION]ms', () => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { @@ -194,7 +194,7 @@ describe('Integration | session', () => { const initialSession = replay.session; - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1); document.dispatchEvent(new Event('visibilitychange')); @@ -204,7 +204,7 @@ describe('Integration | session', () => { expect(replay).not.toHaveSameSession(initialSession); }); - it('creates a new session and triggers a full dom snapshot when document becomes focused after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { + it('creates a new session and triggers a full dom snapshot when document becomes focused after [SESSION_IDLE_DURATION]ms', () => { Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { @@ -214,7 +214,7 @@ describe('Integration | session', () => { const initialSession = replay.session; - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1); WINDOW.dispatchEvent(new Event('focus')); @@ -224,7 +224,7 @@ describe('Integration | session', () => { expect(replay).not.toHaveSameSession(initialSession); }); - it('does not create a new session if user hides the tab and comes back within [VISIBILITY_CHANGE_TIMEOUT] seconds', () => { + it('does not create a new session if user hides the tab and comes back within [SESSION_IDLE_DURATION] seconds', () => { const initialSession = replay.session; Object.defineProperty(document, 'visibilityState', { @@ -237,8 +237,8 @@ describe('Integration | session', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).toHaveSameSession(initialSession); - // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 1); + // User comes back before `SESSION_IDLE_DURATION` elapses + jest.advanceTimersByTime(SESSION_IDLE_DURATION - 1); Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { @@ -451,7 +451,7 @@ describe('Integration | session', () => { it('increases segment id after each event', async () => { clearSession(replay); - replay['_loadAndCheckSession'](0); + replay['_loadAndCheckSession'](); Object.defineProperty(document, 'visibilityState', { configurable: true, diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index 7e091bc7b49b..05772ecab6c7 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -1,7 +1,7 @@ import * as SentryUtils from '@sentry/utils'; import type { Replay } from '../../src'; -import { SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; +import { WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; // mock functions need to be imported first @@ -44,7 +44,7 @@ describe('Integration | stop', () => { jest.setSystemTime(new Date(BASE_TIMESTAMP)); sessionStorage.clear(); clearSession(replay); - replay['_loadAndCheckSession'](SESSION_IDLE_DURATION); + replay['_loadAndCheckSession'](); mockRecord.takeFullSnapshot.mockClear(); mockAddInstrumentationHandler.mockClear(); Object.defineProperty(WINDOW, 'location', { diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts index 15eaa47d5736..9a9455a3728a 100644 --- a/packages/replay/test/utils/setupReplayContainer.ts +++ b/packages/replay/test/utils/setupReplayContainer.ts @@ -1,4 +1,3 @@ -import { SESSION_IDLE_DURATION } from '../../src/constants'; import { createEventBuffer } from '../../src/eventBuffer'; import { ReplayContainer } from '../../src/replay'; import type { RecordingOptions, ReplayPluginOptions } from '../../src/types'; @@ -28,7 +27,7 @@ export function setupReplayContainer({ clearSession(replay); replay['_setInitialState'](); - replay['_loadAndCheckSession'](SESSION_IDLE_DURATION); + replay['_loadAndCheckSession'](); replay['_isEnabled'] = true; replay.eventBuffer = createEventBuffer({ useCompression: options?.useCompression || false,