Skip to content

feat(replay): Extend session idle time until expire to 15min #7955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Sentry.init({
});

window.Replay._replay.timeouts = {
sessionIdle: 2000, // this is usually 5min, but we want to test this with shorter times
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
maxSessionLife: 3600000, // default: 60min
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Sentry.init({
});

window.Replay._replay.timeouts = {
sessionIdle: 1000, // default: 5min
maxSessionLife: 2000, // this is usually 60min, but we want to test this with shorter times
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
sessionIdleExpire: 900000, // defayult: 15min
maxSessionLife: 3600000, // default: 60min
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
waitForReplayRequest,
} from '../../../utils/replayHelpers';

// Session should expire after 2s - keep in sync with init.js
const SESSION_TIMEOUT = 2000;
// Session should be paused after 2s - keep in sync with init.js
const SESSION_PAUSED = 2000;

sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
Expand Down Expand Up @@ -44,11 +44,8 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>

await page.click('#button1');

// We wait for another segment 0
const reqPromise1 = waitForReplayRequest(page, 0);

// Now we wait for the session timeout, nothing should be sent in the meanwhile
await new Promise(resolve => setTimeout(resolve, SESSION_TIMEOUT));
await new Promise(resolve => setTimeout(resolve, SESSION_PAUSED));

// nothing happened because no activity/inactivity was detected
const replay = await getReplaySnapshot(page);
Expand All @@ -64,17 +61,17 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>
expect(replay2._isEnabled).toEqual(true);
expect(replay2._isPaused).toEqual(true);

// Trigger an action, should re-start the recording
// We wait for next segment to be sent once we resume the session
const reqPromise1 = waitForReplayRequest(page);

// Trigger an action, should resume the recording
await page.click('#button2');
const req1 = await reqPromise1;

const replay3 = await getReplaySnapshot(page);
expect(replay3._isEnabled).toEqual(true);
expect(replay3._isPaused).toEqual(false);

const replayEvent1 = getReplayEvent(req1);
expect(replayEvent1).toEqual(getExpectedReplayEvent({}));

const fullSnapshots1 = getFullRecordingSnapshots(req1);
expect(fullSnapshots1.length).toEqual(1);
const stringifiedSnapshot1 = normalize(fullSnapshots1[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Sentry.init({
});

window.Replay._replay.timeouts = {
sessionIdle: 300000, // default: 5min
sessionIdlePause: 300000, // default: 5min
sessionIdleExpire: 900000, // default: 15min
maxSessionLife: 4000, // this is usually 60min, but we want to test this with shorter times
};
9 changes: 6 additions & 3 deletions packages/replay/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ export const REPLAY_EVENT_NAME = 'replay_event';
export const RECORDING_EVENT_NAME = 'replay_recording';
export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';

// The idle limit for a session
export const SESSION_IDLE_DURATION = 300_000; // 5 minutes in ms
// The idle limit for a session after which recording is paused.
export const SESSION_IDLE_PAUSE_DURATION = 300_000; // 5 minutes in ms

// The idle limit for a session after which the session expires.
export const SESSION_IDLE_EXPIRE_DURATION = 900_000; // 15 minutes in ms

// The maximum length of a session
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes in ms

/** Default flush delays */
export const DEFAULT_FLUSH_MIN_DELAY = 5_000;
Expand Down
17 changes: 12 additions & 5 deletions packages/replay/src/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { captureException, getCurrentHub } from '@sentry/core';
import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types';
import { logger } from '@sentry/utils';

import { ERROR_CHECKOUT_TIME, MAX_SESSION_LIFE, SESSION_IDLE_DURATION, WINDOW } from './constants';
import {
ERROR_CHECKOUT_TIME,
MAX_SESSION_LIFE,
SESSION_IDLE_EXPIRE_DURATION,
SESSION_IDLE_PAUSE_DURATION,
WINDOW,
} from './constants';
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
import { createEventBuffer } from './eventBuffer';
import { getSession } from './session/getSession';
Expand Down Expand Up @@ -61,7 +67,8 @@ export class ReplayContainer implements ReplayContainerInterface {
* @hidden
*/
public readonly timeouts: Timeouts = {
sessionIdle: SESSION_IDLE_DURATION,
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
maxSessionLife: MAX_SESSION_LIFE,
} as const;

Expand Down Expand Up @@ -423,12 +430,12 @@ export class ReplayContainer implements ReplayContainerInterface {
const oldSessionId = this.getSessionId();

// Prevent starting a new session if the last user activity is older than
// SESSION_IDLE_DURATION. Otherwise non-user activity can trigger a new
// SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
// session+recording. This creates noisy replays that do not have much
// content in them.
if (
this._lastActivity &&
isExpired(this._lastActivity, this.timeouts.sessionIdle) &&
isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
this.session &&
this.session.sampled === 'session'
) {
Expand Down Expand Up @@ -638,7 +645,7 @@ export class ReplayContainer implements ReplayContainerInterface {
const isSessionActive = this.checkAndHandleExpiredSession();

if (!isSessionActive) {
// If the user has come back to the page within SESSION_IDLE_DURATION
// If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
// ms, we will re-use the existing session, otherwise create a new
// session
__DEBUG_BUILD__ && logger.log('[Replay] Document has become active, but session has expired');
Expand Down
8 changes: 3 additions & 5 deletions packages/replay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export interface SendReplayData {
}

export interface Timeouts {
sessionIdle: number;
sessionIdlePause: number;
sessionIdleExpire: number;
maxSessionLife: number;
}

Expand Down Expand Up @@ -455,10 +456,7 @@ export interface ReplayContainer {
performanceEvents: AllPerformanceEntry[];
session: Session | undefined;
recordingMode: ReplayRecordingMode;
timeouts: {
sessionIdle: number;
maxSessionLife: number;
};
timeouts: Timeouts;
isEnabled(): boolean;
isPaused(): boolean;
getContext(): InternalEventContext;
Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/util/addEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function addEvent(
// page has been left open and idle for a long period of time and user
// comes back to trigger a new session. The performance entries rely on
// `performance.timeOrigin`, which is when the page first opened.
if (timestampInMs + replay.timeouts.sessionIdle < Date.now()) {
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
return null;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/replay/src/util/isSessionExpired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function isSessionExpired(session: Session, timeouts: Timeouts, targetTim
// First, check that maximum session length has not been exceeded
isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
// check that the idle timeout has not been exceeded (i.e. user has
// performed an action within the last `idleTimeout` ms)
isExpired(session.lastActivity, timeouts.sessionIdle, targetTime)
// performed an action within the last `sessionIdleExpire` ms)
isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
);
}
18 changes: 9 additions & 9 deletions packages/replay/test/integration/errorSampleRate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
ERROR_CHECKOUT_TIME,
MAX_SESSION_LIFE,
REPLAY_SESSION_KEY,
SESSION_IDLE_DURATION,
SESSION_IDLE_EXPIRE_DURATION,
WINDOW,
} from '../../src/constants';
import type { ReplayContainer } from '../../src/replay';
Expand Down Expand Up @@ -252,15 +252,15 @@ describe('Integration | errorSampleRate', () => {
});
});

it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_DURATION]ms', async () => {
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: function () {
return 'visible';
},
});

jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);

document.dispatchEvent(new Event('visibilitychange'));

Expand All @@ -284,8 +284,8 @@ describe('Integration | errorSampleRate', () => {

expect(replay).not.toHaveLastSentReplay();

// User comes back before `SESSION_IDLE_DURATION` elapses
jest.advanceTimersByTime(SESSION_IDLE_DURATION - 100);
// User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100);
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: function () {
Expand Down Expand Up @@ -403,9 +403,9 @@ describe('Integration | errorSampleRate', () => {
});

// Should behave the same as above test
it('stops replay if user has been idle for more than SESSION_IDLE_DURATION and does not start a new session thereafter', async () => {
it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => {
// Idle for 15 minutes
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);

const TEST_EVENT = {
data: { name: 'lost event' },
Expand All @@ -418,7 +418,7 @@ describe('Integration | errorSampleRate', () => {
jest.runAllTimers();
await new Promise(process.nextTick);

// We stop recording after SESSION_IDLE_DURATION of inactivity in error mode
// We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode
expect(replay).not.toHaveLastSentReplay();
expect(replay.isEnabled()).toBe(false);
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
Expand Down Expand Up @@ -544,7 +544,7 @@ describe('Integration | errorSampleRate', () => {
expect(replay).not.toHaveLastSentReplay();

// Go idle
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
await new Promise(process.nextTick);

mockRecord._emitter(TEST_EVENT);
Expand Down
Loading