Skip to content

Commit 2f6c929

Browse files
committed
ref(replay): Extract sticky session handling
1 parent c6f9dfe commit 2f6c929

File tree

10 files changed

+108
-155
lines changed

10 files changed

+108
-155
lines changed

packages/replay/src/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from './session/constants';
2121
import { deleteSession } from './session/deleteSession';
2222
import { getSession } from './session/getSession';
23+
import { saveSession } from './session/saveSession';
2324
import { Session } from './session/Session';
2425
import type {
2526
AllPerformanceEntry,
@@ -637,6 +638,7 @@ export class Replay implements Integration {
637638
// checkout.
638639
if (this.waitForError && this.session && this.context.earliestEvent) {
639640
this.session.started = this.context.earliestEvent;
641+
this._maybeSaveSession();
640642
}
641643

642644
// If the full snapshot is due to an initial load, we will not have
@@ -893,6 +895,7 @@ export class Replay implements Integration {
893895
updateSessionActivity(lastActivity: number = new Date().getTime()): void {
894896
if (this.session) {
895897
this.session.lastActivity = lastActivity;
898+
this._maybeSaveSession();
896899
}
897900
}
898901

@@ -1104,6 +1107,7 @@ export class Replay implements Integration {
11041107
const eventContext = this.popEventContext();
11051108
// Always increment segmentId regardless of outcome of sending replay
11061109
const segmentId = this.session.segmentId++;
1110+
this._maybeSaveSession();
11071111

11081112
await this.sendReplay({
11091113
replayId,
@@ -1342,4 +1346,11 @@ export class Replay implements Integration {
13421346
});
13431347
}
13441348
}
1349+
1350+
/** Save the session, if it is sticky */
1351+
private _maybeSaveSession(): void {
1352+
if (this.session && this.options.stickySession) {
1353+
saveSession(this.session);
1354+
}
1355+
}
13451356
}
+15-80
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { uuid4 } from '@sentry/utils';
22

3-
import { SampleRates, SessionOptions } from '../types';
3+
import { SampleRates } from '../types';
44
import { isSampled } from '../util/isSampled';
5-
import { saveSession } from './saveSession';
6-
7-
type StickyOption = Required<Pick<SessionOptions, 'stickySession'>>;
85

96
type Sampled = false | 'session' | 'error';
107

@@ -33,115 +30,53 @@ interface SessionObject {
3330
}
3431

3532
export class Session {
36-
public readonly options: StickyOption;
37-
3833
/**
3934
* Session ID
4035
*/
41-
private _id: string;
36+
public readonly id: string;
4237

4338
/**
4439
* Start time of current session
4540
*/
46-
private _started: number;
41+
public started: number;
4742

4843
/**
4944
* Last known activity of the session
5045
*/
51-
private _lastActivity: number;
46+
public lastActivity: number;
5247

5348
/**
5449
* Sequence ID specific to replay updates
5550
*/
56-
private _segmentId: number;
51+
public segmentId: number;
5752

5853
/**
5954
* Previous session ID
6055
*/
61-
private _previousSessionId: string | undefined;
56+
public previousSessionId: string | undefined;
6257

6358
/**
6459
* Is the Session sampled?
6560
*/
66-
private _sampled: Sampled;
61+
public readonly sampled: Sampled;
6762

68-
public constructor(
69-
session: Partial<SessionObject> = {},
70-
{ stickySession, sessionSampleRate, errorSampleRate }: StickyOption & SampleRates,
71-
) {
63+
public constructor(session: Partial<SessionObject> = {}, { sessionSampleRate, errorSampleRate }: SampleRates) {
7264
const now = new Date().getTime();
73-
this._id = session.id || uuid4();
74-
this._started = session.started ?? now;
75-
this._lastActivity = session.lastActivity ?? now;
76-
this._segmentId = session.segmentId ?? 0;
77-
this._sampled =
65+
this.id = session.id || uuid4();
66+
this.started = session.started ?? now;
67+
this.lastActivity = session.lastActivity ?? now;
68+
this.segmentId = session.segmentId ?? 0;
69+
this.sampled =
7870
session.sampled ?? (isSampled(sessionSampleRate) ? 'session' : isSampled(errorSampleRate) ? 'error' : false);
79-
80-
this.options = {
81-
stickySession,
82-
};
83-
}
84-
85-
get id(): string {
86-
return this._id;
87-
}
88-
89-
get started(): number {
90-
return this._started;
91-
}
92-
93-
set started(newDate: number) {
94-
this._started = newDate;
95-
if (this.options.stickySession) {
96-
saveSession(this);
97-
}
98-
}
99-
100-
get lastActivity(): number {
101-
return this._lastActivity;
102-
}
103-
104-
set lastActivity(newDate: number) {
105-
this._lastActivity = newDate;
106-
if (this.options.stickySession) {
107-
saveSession(this);
108-
}
109-
}
110-
111-
get segmentId(): number {
112-
return this._segmentId;
113-
}
114-
115-
set segmentId(id: number) {
116-
this._segmentId = id;
117-
if (this.options.stickySession) {
118-
saveSession(this);
119-
}
120-
}
121-
122-
get previousSessionId(): string | undefined {
123-
return this._previousSessionId;
124-
}
125-
126-
set previousSessionId(id: string | undefined) {
127-
this._previousSessionId = id;
128-
}
129-
130-
get sampled(): Sampled {
131-
return this._sampled;
132-
}
133-
134-
set sampled(_isSampled: Sampled) {
135-
throw new Error('Unable to change sampled value');
13671
}
13772

13873
toJSON(): SessionObject {
13974
return {
14075
id: this.id,
14176
started: this.started,
14277
lastActivity: this.lastActivity,
143-
segmentId: this._segmentId,
144-
sampled: this._sampled,
78+
segmentId: this.segmentId,
79+
sampled: this.sampled,
14580
} as SessionObject;
14681
}
14782
}

packages/replay/src/session/createSession.ts

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { Session } from './Session';
1111
*/
1212
export function createSession({ sessionSampleRate, errorSampleRate, stickySession = false }: SessionOptions): Session {
1313
const session = new Session(undefined, {
14-
stickySession,
1514
errorSampleRate,
1615
sessionSampleRate,
1716
});

packages/replay/src/session/fetchSession.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@ export function fetchSession({ sessionSampleRate, errorSampleRate }: SampleRates
2222

2323
const sessionObj = JSON.parse(sessionStringFromStorage);
2424

25-
return new Session(
26-
sessionObj,
27-
// We are assuming that if there is a saved item, then the session is sticky,
28-
// however this could break down if we used a different storage mechanism (e.g. localstorage)
29-
{ stickySession: true, sessionSampleRate, errorSampleRate },
30-
);
25+
return new Session(sessionObj, { sessionSampleRate, errorSampleRate });
3126
} catch {
3227
return null;
3328
}

packages/replay/test/mocks/mockSdk.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class MockTransport implements Transport {
3636

3737
export async function mockSdk({
3838
replayOptions = {
39-
stickySession: true,
39+
stickySession: false,
4040
sessionSampleRate: 1.0,
4141
errorSampleRate: 0.0,
4242
},

packages/replay/test/unit/index.test.ts

+75-18
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
jest.mock('./../../src/util/isInternal', () => ({
22
isInternal: jest.fn(() => true),
33
}));
4+
45
import { BASE_TIMESTAMP, RecordMock } from '@test';
56
import { PerformanceEntryResource } from '@test/fixtures/performanceEntry/resource';
67
import { resetSdkMock } from '@test/mocks';
78
import { DomHandler, MockTransportSend } from '@test/types';
9+
import { EventType } from 'rrweb';
810

911
import { Replay } from '../../src';
1012
import { MAX_SESSION_LIFE, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT } from '../../src/session/constants';
13+
import { RecordingEvent } from '../../src/types';
1114
import { useFakeTimers } from '../utils/use-fake-timers';
1215

1316
useFakeTimers();
@@ -17,6 +20,77 @@ async function advanceTimers(time: number) {
1720
await new Promise(process.nextTick);
1821
}
1922

23+
describe('Replay with custom mock', () => {
24+
afterEach(() => {
25+
jest.clearAllMocks();
26+
});
27+
28+
it('calls rrweb.record with custom options', async () => {
29+
const { mockRecord } = await resetSdkMock({
30+
ignoreClass: 'sentry-test-ignore',
31+
stickySession: false,
32+
sessionSampleRate: 1.0,
33+
errorSampleRate: 0.0,
34+
});
35+
expect(mockRecord.mock.calls[0][0]).toMatchInlineSnapshot(`
36+
Object {
37+
"blockClass": "sentry-block",
38+
"blockSelector": "[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio",
39+
"emit": [Function],
40+
"ignoreClass": "sentry-test-ignore",
41+
"maskAllInputs": true,
42+
"maskTextClass": "sentry-mask",
43+
"maskTextSelector": "*",
44+
}
45+
`);
46+
});
47+
48+
describe('auto save session', () => {
49+
test.each([
50+
['with stickySession=true', true, 1],
51+
['with stickySession=false', false, 0],
52+
])('%s', async (_: string, stickySession: boolean, addSummand: number) => {
53+
let saveSessionSpy;
54+
55+
jest.mock('../../src/session/saveSession', () => {
56+
saveSessionSpy = jest.fn();
57+
58+
return {
59+
saveSession: saveSessionSpy,
60+
};
61+
});
62+
63+
const { replay } = await resetSdkMock({
64+
stickySession,
65+
sessionSampleRate: 1.0,
66+
errorSampleRate: 0.0,
67+
});
68+
69+
// Initially called up to three times: once for start, then once for replay.updateSessionActivity & once for segmentId increase
70+
expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 3);
71+
72+
replay.updateSessionActivity();
73+
74+
expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 4);
75+
76+
// In order for runFlush to actually do something, we need to add an event
77+
const event = {
78+
type: EventType.Custom,
79+
data: {
80+
tag: 'test custom',
81+
},
82+
timestamp: new Date().valueOf(),
83+
} as RecordingEvent;
84+
85+
replay.addEvent(event);
86+
87+
await replay.runFlush();
88+
89+
expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 5);
90+
});
91+
});
92+
});
93+
2094
describe('Replay', () => {
2195
let replay: Replay;
2296
let mockRecord: RecordMock;
@@ -37,7 +111,7 @@ describe('Replay', () => {
37111
({ mockRecord, mockTransportSend, domHandler, replay, spyCaptureException } = await resetSdkMock({
38112
sessionSampleRate: 1.0,
39113
errorSampleRate: 0.0,
40-
stickySession: true,
114+
stickySession: false,
41115
}));
42116

43117
jest.spyOn(replay, 'flush');
@@ -69,23 +143,6 @@ describe('Replay', () => {
69143
replay.stop();
70144
});
71145

72-
it('calls rrweb.record with custom options', async () => {
73-
({ mockRecord, mockTransportSend, domHandler, replay } = await resetSdkMock({
74-
ignoreClass: 'sentry-test-ignore',
75-
}));
76-
expect(mockRecord.mock.calls[0][0]).toMatchInlineSnapshot(`
77-
Object {
78-
"blockClass": "sentry-block",
79-
"blockSelector": "[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio",
80-
"emit": [Function],
81-
"ignoreClass": "sentry-test-ignore",
82-
"maskAllInputs": true,
83-
"maskTextClass": "sentry-mask",
84-
"maskTextSelector": "*",
85-
}
86-
`);
87-
});
88-
89146
it('should have a session after setup', () => {
90147
expect(replay.session).toMatchObject({
91148
lastActivity: BASE_TIMESTAMP,

0 commit comments

Comments
 (0)