From 1dda0bec7a11422f752d8057402a291d610d71bc Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 13 May 2024 11:58:15 -0230 Subject: [PATCH 1/3] fix(replay/v7): Fix user activity not being updated in `start()` Replays will fail to start recording when using `start()` specifically when manually recording and after the user has been idle for a long period of time. We need to reset the user activity state when we call `start()`, otherwise the session will be [incorrectly] considered to be idle and unable to send any replay events. Closes #11983 --- packages/replay/src/replay.ts | 7 +++ .../replay/test/integration/start.test.ts | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 packages/replay/test/integration/start.test.ts diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 30ccda422544..0d3d40fb94dc 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -27,6 +27,7 @@ import { clearSession } from './session/clearSession'; import { loadOrCreateSession } from './session/loadOrCreateSession'; import { saveSession } from './session/saveSession'; import { shouldRefreshSession } from './session/shouldRefreshSession'; + import type { AddEventResult, AddUpdateCallback, @@ -294,6 +295,12 @@ export class ReplayContainer implements ReplayContainerInterface { logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); + // Required as user activity is initially set in + // constructor, so if `start()` is called after + // session idle expiration, a replay will not be + // created due to an idle timeout. + this._updateUserActivity(); + const session = loadOrCreateSession( { maxReplayDuration: this._options.maxReplayDuration, diff --git a/packages/replay/test/integration/start.test.ts b/packages/replay/test/integration/start.test.ts new file mode 100644 index 000000000000..9e9b8ede0b3f --- /dev/null +++ b/packages/replay/test/integration/start.test.ts @@ -0,0 +1,53 @@ +import { getClient } from '@sentry/core'; +import type { Transport } from '@sentry/types'; + +import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_EXPIRE_DURATION } from '../../src/constants'; +import type { Replay } from '../../src/integration'; +import type { ReplayContainer } from '../../src/replay'; +import { BASE_TIMESTAMP } from '../index'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +describe('Integration | start', () => { + let replay: ReplayContainer; + let integration: Replay; + + beforeEach(async () => { + ({ replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + }, + })); + + const mockTransport = getClient()?.getTransport()?.send as jest.MockedFunction; + mockTransport?.mockClear(); + jest.runAllTimers(); + await new Promise(process.nextTick); + }); + + afterEach(async () => { + integration.stop(); + + jest.runAllTimers(); + await new Promise(process.nextTick); + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + }); + + it('sends replay when calling `start()` after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + + integration.start(); + + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); + + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + }); + }); +}); From cfcded73af20e21637acbe7f3ef5fa442c24876e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 13 May 2024 14:41:50 -0400 Subject: [PATCH 2/3] replayIntegration type --- .../replay/test/integration/beforeAddRecordingEvent.test.ts | 6 ++---- packages/replay/test/integration/start.test.ts | 4 ++-- packages/replay/test/integration/stop.test.ts | 6 ++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts index e6800f4e53d0..035d566def43 100644 --- a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts +++ b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts @@ -2,8 +2,7 @@ import * as SentryCore from '@sentry/core'; import type { Transport } from '@sentry/types'; import * as SentryUtils from '@sentry/utils'; -// eslint-disable-next-line deprecation/deprecation -import type { Replay } from '../../src'; +import type { replayIntegration } from '../../src/integration'; import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import { createPerformanceEntries } from '../../src/util/createPerformanceEntries'; @@ -24,8 +23,7 @@ type MockTransportSend = jest.MockedFunction; describe('Integration | beforeAddRecordingEvent', () => { let replay: ReplayContainer; - // eslint-disable-next-line deprecation/deprecation - let integration: Replay; + let integration: ReturnType; let mockTransportSend: MockTransportSend; let mockSendReplayRequest: jest.SpyInstance; let domHandler: DomHandler; diff --git a/packages/replay/test/integration/start.test.ts b/packages/replay/test/integration/start.test.ts index 9e9b8ede0b3f..7fbef9f8149c 100644 --- a/packages/replay/test/integration/start.test.ts +++ b/packages/replay/test/integration/start.test.ts @@ -2,7 +2,7 @@ import { getClient } from '@sentry/core'; import type { Transport } from '@sentry/types'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_EXPIRE_DURATION } from '../../src/constants'; -import type { Replay } from '../../src/integration'; +import type { replayIntegration } from '../../src/integration'; import type { ReplayContainer } from '../../src/replay'; import { BASE_TIMESTAMP } from '../index'; import { resetSdkMock } from '../mocks/resetSdkMock'; @@ -12,7 +12,7 @@ useFakeTimers(); describe('Integration | start', () => { let replay: ReplayContainer; - let integration: Replay; + let integration: ReturnType; beforeEach(async () => { ({ replay, integration } = await resetSdkMock({ diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index 35bc44f0c80f..f468dd855add 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -1,7 +1,6 @@ import * as SentryUtils from '@sentry/utils'; -// eslint-disable-next-line deprecation/deprecation -import type { Replay } from '../../src'; +import type { replayIntegration } from '../../src/integration'; import { WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; @@ -18,8 +17,7 @@ type MockRunFlush = jest.MockedFunction; describe('Integration | stop', () => { let replay: ReplayContainer; - // eslint-disable-next-line deprecation/deprecation - let integration: Replay; + let integration: ReturnType; const prevLocation = WINDOW.location; const { record: mockRecord } = mockRrweb(); From 11acbfce75d417ca2514b047492f332467a0d269 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 13 May 2024 15:10:27 -0400 Subject: [PATCH 3/3] lint --- packages/replay/test/integration/stop.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index f468dd855add..de0d212de8e9 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 { replayIntegration } from '../../src/integration'; import { WINDOW } from '../../src/constants'; +import type { replayIntegration } from '../../src/integration'; import type { ReplayContainer } from '../../src/replay'; import { clearSession } from '../../src/session/clearSession'; import { addEvent } from '../../src/util/addEvent';