diff --git a/packages/replay/MIGRATION.md b/packages/replay/MIGRATION.md index 2d9360bafc0a..8bceac5d669e 100644 --- a/packages/replay/MIGRATION.md +++ b/packages/replay/MIGRATION.md @@ -57,3 +57,8 @@ If you only imported from `@sentry/replay`, this will not affect you. It is highly unlikely to affect anybody, but the type `IEventBuffer` was renamed to `EventBuffer` for consistency. Unless you manually imported this and used it somewhere in your codebase, this will not affect you. + +## Session object is now a plain object (https://github.com/getsentry/sentry-javascript/pull/6417) + +The `Session` object exported from Replay is now a plain object, instead of a class. +This should not affect you unless you specifically accessed this class & did custom things with it. diff --git a/packages/replay/src/session/Session.ts b/packages/replay/src/session/Session.ts index 4eb20c057519..2936a8c19dda 100644 --- a/packages/replay/src/session/Session.ts +++ b/packages/replay/src/session/Session.ts @@ -1,11 +1,10 @@ import { uuid4 } from '@sentry/utils'; -import { SampleRates } from '../types'; import { isSampled } from '../util/isSampled'; type Sampled = false | 'session' | 'error'; -interface SessionObject { +export interface Session { id: string; /** @@ -24,59 +23,41 @@ interface SessionObject { segmentId: number; /** - * Is the session sampled? `null` if the sampled, otherwise, `session` or `error` + * The ID of the previous session. + * If this is empty, there was no previous session. */ - sampled: Sampled; -} - -export class Session { - /** - * Session ID - */ - public readonly id: string; - - /** - * Start time of current session - */ - public started: number; - - /** - * Last known activity of the session - */ - public lastActivity: number; - - /** - * Sequence ID specific to replay updates - */ - public segmentId: number; + previousSessionId?: string; /** - * Previous session ID + * Is the session sampled? `false` if not sampled, otherwise, `session` or `error` */ - public previousSessionId: string | undefined; - - /** - * Is the Session sampled? - */ - public readonly sampled: Sampled; + sampled: Sampled; +} - public constructor(session: Partial = {}, { sessionSampleRate, errorSampleRate }: SampleRates) { - const now = new Date().getTime(); - this.id = session.id || uuid4(); - this.started = session.started ?? now; - this.lastActivity = session.lastActivity ?? now; - this.segmentId = session.segmentId ?? 0; - this.sampled = - session.sampled ?? (isSampled(sessionSampleRate) ? 'session' : isSampled(errorSampleRate) ? 'error' : false); - } +/** + * Get a session with defaults & applied sampling. + */ +export function makeSession(session: Partial & { sampled: Sampled }): Session { + const now = new Date().getTime(); + const id = session.id || uuid4(); + // Note that this means we cannot set a started/lastActivity of `0`, but this should not be relevant outside of tests. + const started = session.started || now; + const lastActivity = session.lastActivity || now; + const segmentId = session.segmentId || 0; + const sampled = session.sampled; + + return { + id, + started, + lastActivity, + segmentId, + sampled, + }; +} - toJSON(): SessionObject { - return { - id: this.id, - started: this.started, - lastActivity: this.lastActivity, - segmentId: this.segmentId, - sampled: this.sampled, - } as SessionObject; - } +/** + * Get the sampled status for a session based on sample rates & current sampled status. + */ +export function getSessionSampleType(sessionSampleRate: number, errorSampleRate: number): Sampled { + return isSampled(sessionSampleRate) ? 'session' : isSampled(errorSampleRate) ? 'error' : false; } diff --git a/packages/replay/src/session/createSession.ts b/packages/replay/src/session/createSession.ts index d5d2e1ee98e6..5dcb015fe615 100644 --- a/packages/replay/src/session/createSession.ts +++ b/packages/replay/src/session/createSession.ts @@ -2,7 +2,7 @@ import { logger } from '@sentry/utils'; import { SessionOptions } from '../types'; import { saveSession } from './saveSession'; -import { Session } from './Session'; +import { getSessionSampleType, makeSession, Session } from './Session'; /** * Create a new session, which in its current implementation is a Sentry event @@ -10,9 +10,9 @@ import { Session } from './Session'; * one of these Sentry events per "replay session". */ export function createSession({ sessionSampleRate, errorSampleRate, stickySession = false }: SessionOptions): Session { - const session = new Session(undefined, { - errorSampleRate, - sessionSampleRate, + const sampled = getSessionSampleType(sessionSampleRate, errorSampleRate); + const session = makeSession({ + sampled, }); __DEBUG_BUILD__ && logger.log(`[Replay] Creating new session: ${session.id}`); diff --git a/packages/replay/src/session/fetchSession.ts b/packages/replay/src/session/fetchSession.ts index 1585c4037644..c2dc95c9454c 100644 --- a/packages/replay/src/session/fetchSession.ts +++ b/packages/replay/src/session/fetchSession.ts @@ -1,11 +1,10 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../constants'; -import { SampleRates } from '../types'; -import { Session } from './Session'; +import { makeSession, Session } from './Session'; /** * Fetches a session from storage */ -export function fetchSession({ sessionSampleRate, errorSampleRate }: SampleRates): Session | null { +export function fetchSession(): Session | null { const hasSessionStorage = 'sessionStorage' in WINDOW; if (!hasSessionStorage) { @@ -20,9 +19,9 @@ export function fetchSession({ sessionSampleRate, errorSampleRate }: SampleRates return null; } - const sessionObj = JSON.parse(sessionStringFromStorage); + const sessionObj = JSON.parse(sessionStringFromStorage) as Session; - return new Session(sessionObj, { sessionSampleRate, errorSampleRate }); + return makeSession(sessionObj); } catch { return null; } diff --git a/packages/replay/src/session/getSession.ts b/packages/replay/src/session/getSession.ts index 18689e599317..103008061ad0 100644 --- a/packages/replay/src/session/getSession.ts +++ b/packages/replay/src/session/getSession.ts @@ -29,7 +29,7 @@ export function getSession({ errorSampleRate, }: GetSessionParams): { type: 'new' | 'saved'; session: Session } { // If session exists and is passed, use it instead of always hitting session storage - const session = currentSession || (stickySession && fetchSession({ sessionSampleRate, errorSampleRate })); + const session = currentSession || (stickySession && fetchSession()); if (session) { // If there is a session, check if it is valid (e.g. "last activity" time diff --git a/packages/replay/test/unit/session/Session.test.ts b/packages/replay/test/unit/session/Session.test.ts index b82459c96610..ca34e72b4172 100644 --- a/packages/replay/test/unit/session/Session.test.ts +++ b/packages/replay/test/unit/session/Session.test.ts @@ -26,7 +26,7 @@ jest.mock('@sentry/utils', () => { import * as Sentry from '@sentry/browser'; import { WINDOW } from '../../../src/constants'; -import { Session } from '../../../src/session/Session'; +import { getSessionSampleType, makeSession } from '../../../src/session/Session'; type CaptureEventMockType = jest.MockedFunction; @@ -39,42 +39,33 @@ afterEach(() => { }); it('does not sample', function () { - const newSession = new Session(undefined, { - sessionSampleRate: 0.0, - errorSampleRate: 0.0, + const newSession = makeSession({ + sampled: getSessionSampleType(0, 0), }); expect(newSession.sampled).toBe(false); }); it('samples using `sessionSampleRate`', function () { - const newSession = new Session(undefined, { - sessionSampleRate: 1.0, - errorSampleRate: 0.0, + const newSession = makeSession({ + sampled: getSessionSampleType(1.0, 0), }); expect(newSession.sampled).toBe('session'); }); it('samples using `errorSampleRate`', function () { - const newSession = new Session(undefined, { - sessionSampleRate: 0, - errorSampleRate: 1.0, + const newSession = makeSession({ + sampled: getSessionSampleType(0, 1), }); expect(newSession.sampled).toBe('error'); }); it('does not run sampling function if existing session was sampled', function () { - const newSession = new Session( - { - sampled: 'session', - }, - { - sessionSampleRate: 0, - errorSampleRate: 0, - }, - ); + const newSession = makeSession({ + sampled: 'session', + }); expect(newSession.sampled).toBe('session'); }); diff --git a/packages/replay/test/unit/session/fetchSession.test.ts b/packages/replay/test/unit/session/fetchSession.test.ts index 89cddfb30599..9f7e7694a5e0 100644 --- a/packages/replay/test/unit/session/fetchSession.test.ts +++ b/packages/replay/test/unit/session/fetchSession.test.ts @@ -15,34 +15,44 @@ afterEach(() => { WINDOW.sessionStorage.clear(); }); -const SAMPLE_RATES = { - sessionSampleRate: 1.0, - errorSampleRate: 0.0, -}; - it('fetches a valid and sampled session', function () { WINDOW.sessionStorage.setItem( REPLAY_SESSION_KEY, - '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": true,"started":1648827162630,"lastActivity":1648827162658}', + '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": "session","started":1648827162630,"lastActivity":1648827162658}', + ); + + expect(fetchSession()).toEqual({ + id: 'fd09adfc4117477abc8de643e5a5798a', + lastActivity: 1648827162658, + segmentId: 0, + sampled: 'session', + started: 1648827162630, + }); +}); + +it('fetches an unsampled session', function () { + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": false,"started":1648827162630,"lastActivity":1648827162658}', ); - expect(fetchSession(SAMPLE_RATES)?.toJSON()).toEqual({ + expect(fetchSession()).toEqual({ id: 'fd09adfc4117477abc8de643e5a5798a', lastActivity: 1648827162658, segmentId: 0, - sampled: true, + sampled: false, started: 1648827162630, }); }); it('fetches a session that does not exist', function () { - expect(fetchSession(SAMPLE_RATES)).toBe(null); + expect(fetchSession()).toBe(null); }); it('fetches an invalid session', function () { WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, '{"id":"fd09adfc4117477abc8de643e5a5798a",'); - expect(fetchSession(SAMPLE_RATES)).toBe(null); + expect(fetchSession()).toBe(null); }); it('safely attempts to fetch session when Session Storage is disabled', function () { @@ -55,5 +65,5 @@ it('safely attempts to fetch session when Session Storage is disabled', function }, }); - expect(fetchSession(SAMPLE_RATES)).toEqual(null); + expect(fetchSession()).toEqual(null); }); diff --git a/packages/replay/test/unit/session/getSession.test.ts b/packages/replay/test/unit/session/getSession.test.ts index ddc5e4a6e515..fd1606820c99 100644 --- a/packages/replay/test/unit/session/getSession.test.ts +++ b/packages/replay/test/unit/session/getSession.test.ts @@ -3,7 +3,7 @@ import * as CreateSession from '../../../src/session/createSession'; import * as FetchSession from '../../../src/session/fetchSession'; import { getSession } from '../../../src/session/getSession'; import { saveSession } from '../../../src/session/saveSession'; -import { Session } from '../../../src/session/Session'; +import { makeSession } from '../../../src/session/Session'; jest.mock('@sentry/utils', () => { return { @@ -18,16 +18,13 @@ const SAMPLE_RATES = { }; function createMockSession(when: number = new Date().getTime()) { - return new Session( - { - id: 'test_session_id', - segmentId: 0, - lastActivity: when, - started: when, - sampled: 'session', - }, - { ...SAMPLE_RATES }, - ); + return makeSession({ + id: 'test_session_id', + segmentId: 0, + lastActivity: when, + started: when, + sampled: 'session', + }); } beforeAll(() => { @@ -52,7 +49,7 @@ it('creates a non-sticky session when one does not exist', function () { expect(FetchSession.fetchSession).not.toHaveBeenCalled(); expect(CreateSession.createSession).toHaveBeenCalled(); - expect(session.toJSON()).toEqual({ + expect(session).toEqual({ id: 'test_session_id', segmentId: 0, lastActivity: expect.any(Number), @@ -61,7 +58,7 @@ it('creates a non-sticky session when one does not exist', function () { }); // Should not have anything in storage - expect(FetchSession.fetchSession(SAMPLE_RATES)).toBe(null); + expect(FetchSession.fetchSession()).toBe(null); }); it('creates a non-sticky session, regardless of session existing in sessionStorage', function () { @@ -84,15 +81,13 @@ it('creates a non-sticky session, when one is expired', function () { expiry: 1000, stickySession: false, ...SAMPLE_RATES, - currentSession: new Session( - { - id: 'old_session_id', - lastActivity: new Date().getTime() - 1001, - started: new Date().getTime() - 1001, - segmentId: 0, - }, - { ...SAMPLE_RATES }, - ), + currentSession: makeSession({ + id: 'old_session_id', + lastActivity: new Date().getTime() - 1001, + started: new Date().getTime() - 1001, + segmentId: 0, + sampled: 'session', + }), }); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); @@ -103,7 +98,7 @@ it('creates a non-sticky session, when one is expired', function () { }); it('creates a sticky session when one does not exist', function () { - expect(FetchSession.fetchSession(SAMPLE_RATES)).toBe(null); + expect(FetchSession.fetchSession()).toBe(null); const { session } = getSession({ expiry: 900000, @@ -115,7 +110,7 @@ it('creates a sticky session when one does not exist', function () { expect(FetchSession.fetchSession).toHaveBeenCalled(); expect(CreateSession.createSession).toHaveBeenCalled(); - expect(session.toJSON()).toEqual({ + expect(session).toEqual({ id: 'test_session_id', segmentId: 0, lastActivity: expect.any(Number), @@ -124,7 +119,7 @@ it('creates a sticky session when one does not exist', function () { }); // Should not have anything in storage - expect(FetchSession.fetchSession(SAMPLE_RATES)?.toJSON()).toEqual({ + expect(FetchSession.fetchSession()).toEqual({ id: 'test_session_id', segmentId: 0, lastActivity: expect.any(Number), @@ -147,7 +142,7 @@ it('fetches an existing sticky session', function () { expect(FetchSession.fetchSession).toHaveBeenCalled(); expect(CreateSession.createSession).not.toHaveBeenCalled(); - expect(session.toJSON()).toEqual({ + expect(session).toEqual({ id: 'test_session_id', segmentId: 0, lastActivity: now, @@ -180,15 +175,13 @@ it('fetches a non-expired non-sticky session', function () { expiry: 1000, stickySession: false, ...SAMPLE_RATES, - currentSession: new Session( - { - id: 'test_session_id_2', - lastActivity: +new Date() - 500, - started: +new Date() - 500, - segmentId: 0, - }, - { ...SAMPLE_RATES }, - ), + currentSession: makeSession({ + id: 'test_session_id_2', + lastActivity: +new Date() - 500, + started: +new Date() - 500, + segmentId: 0, + sampled: 'session', + }), }); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); diff --git a/packages/replay/test/unit/session/saveSession.test.ts b/packages/replay/test/unit/session/saveSession.test.ts index 804725f220bd..c1a884bca84f 100644 --- a/packages/replay/test/unit/session/saveSession.test.ts +++ b/packages/replay/test/unit/session/saveSession.test.ts @@ -1,6 +1,6 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../../../src/constants'; import { saveSession } from '../../../src/session/saveSession'; -import { Session } from '../../../src/session/Session'; +import { makeSession } from '../../../src/session/Session'; beforeAll(() => { WINDOW.sessionStorage.clear(); @@ -11,16 +11,13 @@ afterEach(() => { }); it('saves a valid session', function () { - const session = new Session( - { - id: 'fd09adfc4117477abc8de643e5a5798a', - segmentId: 0, - started: 1648827162630, - lastActivity: 1648827162658, - sampled: 'session', - }, - { sessionSampleRate: 1.0, errorSampleRate: 0 }, - ); + const session = makeSession({ + id: 'fd09adfc4117477abc8de643e5a5798a', + segmentId: 0, + started: 1648827162630, + lastActivity: 1648827162658, + sampled: 'session', + }); saveSession(session); expect(WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY)).toEqual(JSON.stringify(session)); diff --git a/packages/replay/test/unit/util/isSessionExpired.test.ts b/packages/replay/test/unit/util/isSessionExpired.test.ts index 6e8c01c939b8..0db371e8e4ef 100644 --- a/packages/replay/test/unit/util/isSessionExpired.test.ts +++ b/packages/replay/test/unit/util/isSessionExpired.test.ts @@ -1,16 +1,15 @@ -import { Session } from '../../../src/session/Session'; +import { makeSession } from '../../../src/session/Session'; import { isSessionExpired } from '../../../src/util/isSessionExpired'; function createSession(extra?: Record) { - return new Session( - { - started: 0, - lastActivity: 0, - segmentId: 0, - ...extra, - }, - { sessionSampleRate: 1.0, errorSampleRate: 0 }, - ); + return makeSession({ + // Setting started/lastActivity to 0 makes it use the default, which is `Date.now()` + started: 1, + lastActivity: 1, + segmentId: 0, + sampled: 'session', + ...extra, + }); } it('session last activity is older than expiry time', function () { @@ -26,5 +25,5 @@ it('session age is not older than max session life', function () { }); it('session age is older than max session life', function () { - expect(isSessionExpired(createSession(), 1_800_000, 1_800_000)).toBe(true); // Session expires at ts >= 1_800_000 + expect(isSessionExpired(createSession(), 1_800_000, 1_800_001)).toBe(true); // Session expires at ts >= 1_800_000 });