Skip to content

ref(replay): Make Session a POJO #6417

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 7 commits into from
Dec 6, 2022
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
5 changes: 5 additions & 0 deletions packages/replay/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
83 changes: 32 additions & 51 deletions packages/replay/src/session/Session.ts
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand All @@ -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<SessionObject> = {}, { 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<Session> & { 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;
}
8 changes: 4 additions & 4 deletions packages/replay/src/session/createSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ 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
* that all replays will be saved to as attachments. Currently, we only expect
* 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}`);
Expand Down
9 changes: 4 additions & 5 deletions packages/replay/src/session/fetchSession.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/session/getSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 10 additions & 19 deletions packages/replay/test/unit/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Sentry.captureEvent>;

Expand All @@ -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');
});
32 changes: 21 additions & 11 deletions packages/replay/test/unit/session/fetchSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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);
});
Loading