Skip to content

Commit 3ae4f45

Browse files
authored
fix(replay/v7): Fix user activity not being updated in start() (#12003)
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.
1 parent 75e5501 commit 3ae4f45

File tree

4 files changed

+64
-8
lines changed

4 files changed

+64
-8
lines changed

packages/replay/src/replay.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { clearSession } from './session/clearSession';
2727
import { loadOrCreateSession } from './session/loadOrCreateSession';
2828
import { saveSession } from './session/saveSession';
2929
import { shouldRefreshSession } from './session/shouldRefreshSession';
30+
3031
import type {
3132
AddEventResult,
3233
AddUpdateCallback,
@@ -294,6 +295,12 @@ export class ReplayContainer implements ReplayContainerInterface {
294295

295296
logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
296297

298+
// Required as user activity is initially set in
299+
// constructor, so if `start()` is called after
300+
// session idle expiration, a replay will not be
301+
// created due to an idle timeout.
302+
this._updateUserActivity();
303+
297304
const session = loadOrCreateSession(
298305
{
299306
maxReplayDuration: this._options.maxReplayDuration,

packages/replay/test/integration/beforeAddRecordingEvent.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import * as SentryCore from '@sentry/core';
22
import type { Transport } from '@sentry/types';
33
import * as SentryUtils from '@sentry/utils';
44

5-
// eslint-disable-next-line deprecation/deprecation
6-
import type { Replay } from '../../src';
5+
import type { replayIntegration } from '../../src/integration';
76
import type { ReplayContainer } from '../../src/replay';
87
import { clearSession } from '../../src/session/clearSession';
98
import { createPerformanceEntries } from '../../src/util/createPerformanceEntries';
@@ -24,8 +23,7 @@ type MockTransportSend = jest.MockedFunction<Transport['send']>;
2423

2524
describe('Integration | beforeAddRecordingEvent', () => {
2625
let replay: ReplayContainer;
27-
// eslint-disable-next-line deprecation/deprecation
28-
let integration: Replay;
26+
let integration: ReturnType<typeof replayIntegration>;
2927
let mockTransportSend: MockTransportSend;
3028
let mockSendReplayRequest: jest.SpyInstance<any>;
3129
let domHandler: DomHandler;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { getClient } from '@sentry/core';
2+
import type { Transport } from '@sentry/types';
3+
4+
import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_EXPIRE_DURATION } from '../../src/constants';
5+
import type { replayIntegration } from '../../src/integration';
6+
import type { ReplayContainer } from '../../src/replay';
7+
import { BASE_TIMESTAMP } from '../index';
8+
import { resetSdkMock } from '../mocks/resetSdkMock';
9+
import { useFakeTimers } from '../utils/use-fake-timers';
10+
11+
useFakeTimers();
12+
13+
describe('Integration | start', () => {
14+
let replay: ReplayContainer;
15+
let integration: ReturnType<typeof replayIntegration>;
16+
17+
beforeEach(async () => {
18+
({ replay, integration } = await resetSdkMock({
19+
replayOptions: {
20+
stickySession: false,
21+
},
22+
sentryOptions: {
23+
replaysSessionSampleRate: 0.0,
24+
},
25+
}));
26+
27+
const mockTransport = getClient()?.getTransport()?.send as jest.MockedFunction<Transport['send']>;
28+
mockTransport?.mockClear();
29+
jest.runAllTimers();
30+
await new Promise(process.nextTick);
31+
});
32+
33+
afterEach(async () => {
34+
integration.stop();
35+
36+
jest.runAllTimers();
37+
await new Promise(process.nextTick);
38+
jest.setSystemTime(new Date(BASE_TIMESTAMP));
39+
});
40+
41+
it('sends replay when calling `start()` after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
42+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
43+
44+
integration.start();
45+
46+
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
47+
await new Promise(process.nextTick);
48+
49+
expect(replay).toHaveLastSentReplay({
50+
recordingPayloadHeader: { segment_id: 0 },
51+
});
52+
});
53+
});

packages/replay/test/integration/stop.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as SentryUtils from '@sentry/utils';
22

3-
// eslint-disable-next-line deprecation/deprecation
4-
import type { Replay } from '../../src';
53
import { WINDOW } from '../../src/constants';
4+
import type { replayIntegration } from '../../src/integration';
65
import type { ReplayContainer } from '../../src/replay';
76
import { clearSession } from '../../src/session/clearSession';
87
import { addEvent } from '../../src/util/addEvent';
@@ -18,8 +17,7 @@ type MockRunFlush = jest.MockedFunction<ReplayContainer['_runFlush']>;
1817

1918
describe('Integration | stop', () => {
2019
let replay: ReplayContainer;
21-
// eslint-disable-next-line deprecation/deprecation
22-
let integration: Replay;
20+
let integration: ReturnType<typeof replayIntegration>;
2321
const prevLocation = WINDOW.location;
2422

2523
const { record: mockRecord } = mockRrweb();

0 commit comments

Comments
 (0)