@statsig/session-replay 3.16.0 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statsig/session-replay",
3
- "version": "3.16.0",
3
+ "version": "3.16.1",
4
4
  "license": "ISC",
5
5
  "homepage": "https://github.com/statsig-io/js-client-monorepo",
6
6
  "repository": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "rrweb": "2.0.0-alpha.14",
13
- "@statsig/client-core": "3.16.0"
13
+ "@statsig/client-core": "3.16.1"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@rrweb/types": "2.0.0-alpha.14"
@@ -1,4 +1,5 @@
1
1
  import { PrecomputedEvaluationsInterface, StatsigPlugin } from '@statsig/client-core';
2
+ import { EndReason, SessionReplayBase } from './SessionReplayBase';
2
3
  import { RRWebConfig } from './SessionReplayClient';
3
4
  type SessionReplayOptions = {
4
5
  rrwebConfig?: RRWebConfig;
@@ -11,24 +12,9 @@ export declare class StatsigSessionReplayPlugin implements StatsigPlugin<Precomp
11
12
  bind(client: PrecomputedEvaluationsInterface): void;
12
13
  }
13
14
  export declare function runStatsigSessionReplay(client: PrecomputedEvaluationsInterface, options?: SessionReplayOptions): void;
14
- export declare class SessionReplay {
15
- private _client;
16
- private _options?;
17
- private _replayer;
18
- private _sessionData;
19
- private _events;
20
- private _currentSessionID;
21
- private _errorBoundary;
22
- constructor(_client: PrecomputedEvaluationsInterface, _options?: SessionReplayOptions | undefined);
23
- private _subscribeToVisibilityChanged;
24
- forceStartRecording(): void;
25
- private _onVisibilityChanged;
26
- private _onRecordingEvent;
27
- private _attemptToStartRecording;
28
- private _shutdown;
29
- private _logRecording;
30
- private _logRecordingWithSessionID;
31
- private _bumpSessionIdleTimerAndLogRecording;
32
- private _getSessionIdFromClient;
15
+ export declare class SessionReplay extends SessionReplayBase {
16
+ constructor(client: PrecomputedEvaluationsInterface, options?: SessionReplayOptions);
17
+ protected _shutdown(endReason?: EndReason): void;
18
+ protected _attemptToStartRecording(force?: boolean): void;
33
19
  }
34
20
  export {};
@@ -2,11 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SessionReplay = exports.runStatsigSessionReplay = exports.StatsigSessionReplayPlugin = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
- const SessionReplayClient_1 = require("./SessionReplayClient");
6
- const SizeOf_1 = require("./SizeOf");
7
- const REPLAY_ENQUEUE_TRIGGER_BYTES = 1024 * 10; // 10 KB
8
- const REPLAY_SLICE_BYTES = 1024 * 1024; // 1 MB
9
- const MAX_INDIVIDUAL_EVENT_BYTES = 1024 * 1024 * 10; // 10 MB
5
+ const SessionReplayBase_1 = require("./SessionReplayBase");
10
6
  class StatsigSessionReplayPlugin {
11
7
  constructor(options) {
12
8
  this.options = options;
@@ -21,81 +17,20 @@ function runStatsigSessionReplay(client, options) {
21
17
  new SessionReplay(client, options);
22
18
  }
23
19
  exports.runStatsigSessionReplay = runStatsigSessionReplay;
24
- class SessionReplay {
25
- constructor(_client, _options) {
20
+ class SessionReplay extends SessionReplayBase_1.SessionReplayBase {
21
+ constructor(client, options) {
26
22
  var _a;
27
- this._client = _client;
28
- this._options = _options;
29
- this._sessionData = null;
30
- this._events = [];
31
- const { sdkKey, errorBoundary } = _client.getContext();
32
- this._errorBoundary = errorBoundary;
33
- this._errorBoundary.wrap(this);
34
- if (!(0, client_core_1._isServerEnv)()) {
35
- const statsigGlobal = (0, client_core_1._getStatsigGlobal)();
36
- statsigGlobal.srInstances = Object.assign(Object.assign({}, statsigGlobal.srInstances), { [sdkKey]: this });
37
- }
38
- this._currentSessionID = this._getSessionIdFromClient();
39
- this._replayer = new SessionReplayClient_1.SessionReplayClient();
40
- this._client.$on('pre_shutdown', () => this._shutdown());
41
- this._client.$on('values_updated', () => { var _a; return this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording); });
42
- this._client.on('session_expired', () => {
43
- this._replayer.stop();
44
- client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
45
- this._logRecording('session_expired');
46
- });
47
- this._subscribeToVisibilityChanged();
48
- this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
49
- }
50
- _subscribeToVisibilityChanged() {
51
- // Note: this exists as a separate function to ensure closure scope only contains `sdkKey`
52
- const { sdkKey } = this._client.getContext();
53
- (0, client_core_1._subscribeToVisiblityChanged)((vis) => {
54
- var _a, _b;
55
- const inst = (_b = (_a = (0, client_core_1._getStatsigGlobal)()) === null || _a === void 0 ? void 0 : _a.srInstances) === null || _b === void 0 ? void 0 : _b[sdkKey];
56
- if (inst instanceof SessionReplay) {
57
- inst._onVisibilityChanged(vis);
23
+ super(client, options);
24
+ this._client.$on('values_updated', () => {
25
+ var _a;
26
+ if (!this._wasStopped) {
27
+ this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
58
28
  }
59
29
  });
30
+ this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
60
31
  }
61
- forceStartRecording() {
62
- this._attemptToStartRecording(true);
63
- }
64
- _onVisibilityChanged(visibility) {
65
- if (visibility !== 'background') {
66
- return;
67
- }
68
- this._logRecording();
69
- this._client.flush().catch((e) => {
70
- this._errorBoundary.logError('SR::visibility', e);
71
- });
72
- }
73
- _onRecordingEvent(event, data) {
74
- // The session has expired so we should stop recording
75
- if (this._currentSessionID !== this._getSessionIdFromClient()) {
76
- this._replayer.stop();
77
- client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
78
- this._logRecording('session_expired');
79
- return;
80
- }
81
- this._sessionData = data;
82
- const eventApproxSize = (0, SizeOf_1._fastApproxSizeOf)(event, MAX_INDIVIDUAL_EVENT_BYTES);
83
- if (eventApproxSize > MAX_INDIVIDUAL_EVENT_BYTES) {
84
- client_core_1.Log.warn(`SessionReplay event is too large (~${eventApproxSize} bytes) and will not be logged`, event);
85
- return;
86
- }
87
- const approxArraySizeBefore = (0, SizeOf_1._fastApproxSizeOf)(this._events, REPLAY_ENQUEUE_TRIGGER_BYTES);
88
- this._events.push(event);
89
- if (approxArraySizeBefore + eventApproxSize <
90
- REPLAY_ENQUEUE_TRIGGER_BYTES) {
91
- return;
92
- }
93
- if ((0, client_core_1._isCurrentlyVisible)()) {
94
- this._bumpSessionIdleTimerAndLogRecording();
95
- }
96
- else {
97
- this._logRecording();
98
- }
32
+ _shutdown(endReason) {
33
+ super._shutdownImpl(endReason);
99
34
  }
100
35
  _attemptToStartRecording(force = false) {
101
36
  var _a, _b;
@@ -107,84 +42,11 @@ class SessionReplay {
107
42
  if (this._replayer.isRecording()) {
108
43
  return;
109
44
  }
45
+ this._wasStopped = false;
110
46
  client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'true' });
111
- this._replayer.record((e, d) => this._onRecordingEvent(e, d), (_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.rrwebConfig) !== null && _b !== void 0 ? _b : {});
112
- }
113
- _shutdown() {
114
- this._replayer.stop();
115
- client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
116
- if (this._events.length === 0 || this._sessionData == null) {
117
- return;
118
- }
119
- this._logRecording();
120
- }
121
- _logRecording(endReason) {
122
- if (this._events.length === 0 || this._sessionData == null) {
123
- return;
124
- }
125
- endReason = (0, client_core_1._isUnloading)() ? 'is_leaving_page' : endReason;
126
- this._logRecordingWithSessionID(this._currentSessionID, endReason);
127
- }
128
- _logRecordingWithSessionID(sessionID, endReason) {
129
- const data = this._sessionData;
130
- if (data === null || this._events.length === 0) {
131
- return;
132
- }
133
- const payload = JSON.stringify(this._events);
134
- const parts = _slicePayload(payload);
135
- const slicedID = parts.length > 1 ? (0, client_core_1.getUUID)() : null;
136
- for (let i = 0; i < parts.length; i++) {
137
- const slice = parts[i];
138
- const event = _makeLoggableRrwebEvent(slice, payload, sessionID, data);
139
- if (slicedID != null) {
140
- _appendSlicedMetadata(event.metadata, slicedID, i, parts.length, slice.length);
141
- }
142
- if (endReason) {
143
- event.metadata[endReason] = 'true';
144
- }
145
- this._client.logEvent(event);
146
- if (slicedID != null) {
147
- this._client.flush().catch((e) => {
148
- client_core_1.Log.error(e);
149
- });
150
- }
151
- }
152
- this._events = [];
153
- }
154
- _bumpSessionIdleTimerAndLogRecording() {
155
- this._getSessionIdFromClient();
156
- this._logRecording();
157
- }
158
- _getSessionIdFromClient() {
159
- return this._client.getContext().session.data.sessionID;
47
+ this._replayer.record((e, d) => this._onRecordingEvent(e, d), (_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.rrwebConfig) !== null && _b !== void 0 ? _b : {}, () => {
48
+ this._shutdown();
49
+ });
160
50
  }
161
51
  }
162
52
  exports.SessionReplay = SessionReplay;
163
- function _slicePayload(payload) {
164
- const parts = [];
165
- for (let i = 0; i < payload.length; i += REPLAY_SLICE_BYTES) {
166
- parts.push(payload.slice(i, i + REPLAY_SLICE_BYTES));
167
- }
168
- return parts;
169
- }
170
- function _makeLoggableRrwebEvent(slice, payload, sessionID, data) {
171
- const metadata = {
172
- session_start_ts: String(data.startTime),
173
- session_end_ts: String(data.endTime),
174
- clicks_captured_cumulative: String(data.clickCount),
175
- rrweb_events: slice,
176
- rrweb_payload_size: String(payload.length),
177
- session_replay_sdk_version: client_core_1.SDK_VERSION,
178
- };
179
- return {
180
- eventName: 'statsig::session_recording',
181
- value: sessionID,
182
- metadata,
183
- };
184
- }
185
- function _appendSlicedMetadata(metadata, slicedID, sliceIndex, sliceCount, sliceByteSize) {
186
- metadata.sliced_id = slicedID;
187
- metadata.slice_index = String(sliceIndex);
188
- metadata.slice_count = String(sliceCount);
189
- metadata.slice_byte_size = String(sliceByteSize);
190
- }
@@ -0,0 +1,33 @@
1
+ import { ErrorBoundary, PrecomputedEvaluationsInterface } from '@statsig/client-core';
2
+ import { RRWebConfig, ReplayEvent, ReplaySessionData, SessionReplayClient } from './SessionReplayClient';
3
+ type SessionReplayOptions = {
4
+ rrwebConfig?: RRWebConfig;
5
+ forceRecording?: boolean;
6
+ };
7
+ export type EndReason = 'is_leaving_page' | 'session_expired';
8
+ export declare abstract class SessionReplayBase {
9
+ protected _sessionData: ReplaySessionData;
10
+ protected _events: ReplayEvent[];
11
+ protected _currentSessionID: string;
12
+ protected _errorBoundary: ErrorBoundary;
13
+ protected _client: PrecomputedEvaluationsInterface;
14
+ protected _options?: SessionReplayOptions;
15
+ protected _replayer: SessionReplayClient;
16
+ protected _wasStopped: boolean;
17
+ protected _currentEventIndex: number;
18
+ constructor(client: PrecomputedEvaluationsInterface, options?: SessionReplayOptions);
19
+ forceStartRecording(): void;
20
+ stopRecording(): void;
21
+ isRecording(): boolean;
22
+ protected abstract _attemptToStartRecording(force?: boolean): void;
23
+ protected _logRecording(endReason?: EndReason): void;
24
+ private _onVisibilityChanged;
25
+ protected _subscribeToVisibilityChanged(): void;
26
+ protected _logRecordingWithSessionID(sessionID: string, endReason?: EndReason): void;
27
+ protected _bumpSessionIdleTimerAndLogRecording(): void;
28
+ protected _getSessionIdFromClient(): string;
29
+ protected abstract _shutdown(endReason?: EndReason): void;
30
+ protected _shutdownImpl(endReason?: EndReason): void;
31
+ protected _onRecordingEvent(event: ReplayEvent, data: ReplaySessionData): void;
32
+ }
33
+ export {};
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionReplayBase = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const SessionReplayClient_1 = require("./SessionReplayClient");
6
+ const SessionReplayUtils_1 = require("./SessionReplayUtils");
7
+ const SizeOf_1 = require("./SizeOf");
8
+ class SessionReplayBase {
9
+ constructor(client, options) {
10
+ this._sessionData = {
11
+ startTime: 0,
12
+ endTime: 0,
13
+ clickCount: 0,
14
+ };
15
+ this._events = [];
16
+ this._wasStopped = false;
17
+ this._currentEventIndex = 0;
18
+ this._client = client;
19
+ this._options = options;
20
+ const { sdkKey, errorBoundary } = this._client.getContext();
21
+ this._errorBoundary = errorBoundary;
22
+ this._errorBoundary.wrap(this);
23
+ this._replayer = new SessionReplayClient_1.SessionReplayClient();
24
+ this._client.$on('pre_shutdown', () => this._shutdown());
25
+ this._client.on('session_expired', () => {
26
+ this._shutdown('session_expired');
27
+ });
28
+ if (!(0, client_core_1._isServerEnv)()) {
29
+ const statsigGlobal = (0, client_core_1._getStatsigGlobal)();
30
+ statsigGlobal.srInstances = Object.assign(Object.assign({}, statsigGlobal.srInstances), { [sdkKey]: this });
31
+ }
32
+ this._currentSessionID = this._getSessionIdFromClient();
33
+ this._subscribeToVisibilityChanged();
34
+ }
35
+ forceStartRecording() {
36
+ this._wasStopped = false;
37
+ this._attemptToStartRecording(true);
38
+ }
39
+ stopRecording() {
40
+ this._wasStopped = true;
41
+ this._shutdown();
42
+ }
43
+ isRecording() {
44
+ return this._replayer.isRecording();
45
+ }
46
+ _logRecording(endReason) {
47
+ if (this._events.length === 0) {
48
+ return;
49
+ }
50
+ endReason = (0, client_core_1._isUnloading)() ? 'is_leaving_page' : endReason;
51
+ this._logRecordingWithSessionID(this._currentSessionID, endReason);
52
+ }
53
+ _onVisibilityChanged(visibility) {
54
+ if (visibility !== 'background') {
55
+ return;
56
+ }
57
+ this._logRecording();
58
+ this._client.flush().catch((e) => {
59
+ this._errorBoundary.logError('SR::visibility', e);
60
+ });
61
+ }
62
+ _subscribeToVisibilityChanged() {
63
+ // Note: this exists as a separate function to ensure closure scope only contains `sdkKey`
64
+ const { sdkKey } = this._client.getContext();
65
+ (0, client_core_1._subscribeToVisiblityChanged)((vis) => {
66
+ var _a, _b;
67
+ const inst = (_b = (_a = (0, client_core_1._getStatsigGlobal)()) === null || _a === void 0 ? void 0 : _a.srInstances) === null || _b === void 0 ? void 0 : _b[sdkKey];
68
+ if (inst instanceof SessionReplayBase) {
69
+ inst._onVisibilityChanged(vis);
70
+ }
71
+ });
72
+ }
73
+ _logRecordingWithSessionID(sessionID, endReason) {
74
+ const data = this._sessionData;
75
+ if (this._events.length === 0) {
76
+ return;
77
+ }
78
+ const payload = JSON.stringify(this._events);
79
+ const parts = (0, SessionReplayUtils_1._slicePayload)(payload);
80
+ const slicedID = parts.length > 1 ? (0, client_core_1.getUUID)() : null;
81
+ for (let i = 0; i < parts.length; i++) {
82
+ const slice = parts[i];
83
+ const event = (0, SessionReplayUtils_1._makeLoggableRrwebEvent)(slice, payload, sessionID, data);
84
+ if (slicedID != null) {
85
+ (0, SessionReplayUtils_1._appendSlicedMetadata)(event.metadata, slicedID, i, parts.length, slice.length);
86
+ }
87
+ if (endReason) {
88
+ event.metadata[endReason] = 'true';
89
+ }
90
+ this._client.logEvent(event);
91
+ if (slicedID != null) {
92
+ this._client.flush().catch((e) => {
93
+ client_core_1.Log.error(e);
94
+ });
95
+ }
96
+ }
97
+ this._events = [];
98
+ }
99
+ _bumpSessionIdleTimerAndLogRecording() {
100
+ this._getSessionIdFromClient();
101
+ this._logRecording();
102
+ }
103
+ _getSessionIdFromClient() {
104
+ return this._client.getContext().session.data.sessionID;
105
+ }
106
+ _shutdownImpl(endReason) {
107
+ this._replayer.stop();
108
+ client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
109
+ this._currentEventIndex = 0;
110
+ if (this._events.length === 0) {
111
+ return;
112
+ }
113
+ this._logRecording(endReason);
114
+ this._sessionData = {
115
+ startTime: 0,
116
+ endTime: 0,
117
+ clickCount: 0,
118
+ };
119
+ }
120
+ _onRecordingEvent(event, data) {
121
+ // The session has expired so we should stop recording
122
+ if (this._currentSessionID !== this._getSessionIdFromClient()) {
123
+ this._shutdown('session_expired');
124
+ return;
125
+ }
126
+ event.eventIndex = this._currentEventIndex++;
127
+ // Update the session data
128
+ this._sessionData.clickCount += data.clickCount;
129
+ this._sessionData.startTime = Math.min(this._sessionData.startTime, data.startTime);
130
+ this._sessionData.endTime = Math.max(this._sessionData.endTime, data.endTime);
131
+ const eventApproxSize = (0, SizeOf_1._fastApproxSizeOf)(event, SessionReplayUtils_1.MAX_INDIVIDUAL_EVENT_BYTES);
132
+ if (eventApproxSize > SessionReplayUtils_1.MAX_INDIVIDUAL_EVENT_BYTES) {
133
+ client_core_1.Log.warn(`SessionReplay event is too large (~${eventApproxSize} bytes) and will not be logged`, event);
134
+ return;
135
+ }
136
+ const approxArraySizeBefore = (0, SizeOf_1._fastApproxSizeOf)(this._events, SessionReplayUtils_1.REPLAY_ENQUEUE_TRIGGER_BYTES);
137
+ this._events.push(event);
138
+ if (approxArraySizeBefore + eventApproxSize <
139
+ SessionReplayUtils_1.REPLAY_ENQUEUE_TRIGGER_BYTES) {
140
+ return;
141
+ }
142
+ if ((0, client_core_1._isCurrentlyVisible)()) {
143
+ this._bumpSessionIdleTimerAndLogRecording();
144
+ }
145
+ else {
146
+ this._logRecording();
147
+ }
148
+ }
149
+ }
150
+ exports.SessionReplayBase = SessionReplayBase;
@@ -15,9 +15,8 @@ export declare class SessionReplayClient {
15
15
  private _stopCallback?;
16
16
  private _startTimestamp;
17
17
  private _endTimestamp;
18
- private _clickCount;
19
18
  private _eventCounter;
20
- record(callback: (latest: ReplayEvent, data: ReplaySessionData) => void, config: RRWebConfig, stopCallback?: () => void): void;
19
+ record(callback: (latest: ReplayEvent, data: ReplaySessionData, isCheckout?: boolean) => void, config: RRWebConfig, stopCallback?: () => void, keepRollingWindow?: boolean): void;
21
20
  stop(): void;
22
21
  isRecording(): boolean;
23
22
  }
@@ -4,49 +4,57 @@ exports.SessionReplayClient = void 0;
4
4
  const rrweb = require("rrweb");
5
5
  const client_core_1 = require("@statsig/client-core");
6
6
  const TIMEOUT_MS = 1000 * 60 * 60 * 4; // 4 hours
7
+ const CHECKOUT_WINDOW_MS = 1000 * 60; // 1 minute
7
8
  class SessionReplayClient {
8
9
  constructor() {
9
10
  this._startTimestamp = null;
10
11
  this._endTimestamp = null;
11
- this._clickCount = 0;
12
12
  this._eventCounter = 0;
13
13
  }
14
- record(callback, config, stopCallback) {
14
+ record(callback, config, stopCallback, keepRollingWindow = false) {
15
15
  if ((0, client_core_1._getDocumentSafe)() == null) {
16
16
  return;
17
17
  }
18
18
  // Always reset session id and tracking fields for a new recording
19
19
  this._startTimestamp = null;
20
20
  this._endTimestamp = null;
21
- this._clickCount = 0;
22
21
  this._eventCounter = 0;
23
22
  this._stopCallback = stopCallback;
24
23
  if (this._stopFn) {
25
24
  return;
26
25
  }
27
- const emit = (event) => {
28
- var _a, _b;
29
- // Reset start only for the first event
30
- (_a = this._startTimestamp) !== null && _a !== void 0 ? _a : (this._startTimestamp = event.timestamp);
26
+ const emit = (event, isCheckOut) => {
27
+ var _a, _b, _c;
28
+ if (keepRollingWindow) {
29
+ // Reset start at each checkout
30
+ this._startTimestamp = isCheckOut
31
+ ? event.timestamp
32
+ : (_a = this._startTimestamp) !== null && _a !== void 0 ? _a : event.timestamp;
33
+ }
34
+ else {
35
+ // Reset start only for the first event
36
+ (_b = this._startTimestamp) !== null && _b !== void 0 ? _b : (this._startTimestamp = event.timestamp);
37
+ }
31
38
  // Always keep a running end timestamp
32
39
  this._endTimestamp = event.timestamp;
40
+ let clickCount = 0;
33
41
  // Count clicks only for events representing a click
34
42
  if (_isClickEvent(event)) {
35
- this._clickCount++;
43
+ clickCount++;
36
44
  }
37
- callback(Object.assign(Object.assign({}, event), { eventIndex: this._eventCounter++ }), {
45
+ callback(Object.assign(Object.assign({}, event), { eventIndex: 0 }), {
38
46
  startTime: this._startTimestamp,
39
47
  endTime: this._endTimestamp,
40
- clickCount: this._clickCount,
41
- });
48
+ clickCount,
49
+ }, isCheckOut !== null && isCheckOut !== void 0 ? isCheckOut : false);
42
50
  if (this._endTimestamp - this._startTimestamp > TIMEOUT_MS) {
43
- (_b = this._stopFn) === null || _b === void 0 ? void 0 : _b.call(this);
51
+ (_c = this._stopFn) === null || _c === void 0 ? void 0 : _c.call(this);
44
52
  if (this._stopCallback) {
45
53
  this._stopCallback();
46
54
  }
47
55
  }
48
56
  };
49
- this._stopFn = _minifiedAwareRecord(emit, config);
57
+ this._stopFn = _minifiedAwareRecord(emit, config, keepRollingWindow);
50
58
  }
51
59
  stop() {
52
60
  if (this._stopFn) {
@@ -63,9 +71,14 @@ exports.SessionReplayClient = SessionReplayClient;
63
71
  * We do a simple concat of rrweb during minification.
64
72
  * This function ensures we handle both "npm" and "<script ..>" install options.
65
73
  */
66
- function _minifiedAwareRecord(emit, config) {
74
+ function _minifiedAwareRecord(emit, config, keepRollingWindow) {
67
75
  const record = typeof rrweb === 'function' ? rrweb : rrweb.record;
68
- return record(Object.assign(Object.assign({}, config), { emit }));
76
+ if (keepRollingWindow) {
77
+ return record(Object.assign(Object.assign({}, config), { emit, checkoutEveryNms: CHECKOUT_WINDOW_MS }));
78
+ }
79
+ else {
80
+ return record(Object.assign(Object.assign({}, config), { emit }));
81
+ }
69
82
  }
70
83
  function _isClickEvent(event) {
71
84
  // we use the raw number so we can support the minified rrweb file.
@@ -0,0 +1,23 @@
1
+ import { StatsigEvent } from '@statsig/client-core';
2
+ import { ReplaySessionData } from './SessionReplayClient';
3
+ export type RRWebPayload = {
4
+ session_start_ts: string;
5
+ session_end_ts: string;
6
+ clicks_captured_cumulative: string;
7
+ rrweb_events: string;
8
+ rrweb_payload_size: string;
9
+ session_replay_sdk_version: string;
10
+ sliced_id?: string;
11
+ slice_index?: string;
12
+ slice_count?: string;
13
+ slice_byte_size?: string;
14
+ is_leaving_page?: string;
15
+ session_expired?: string;
16
+ };
17
+ export declare const REPLAY_ENQUEUE_TRIGGER_BYTES: number;
18
+ export declare const MAX_INDIVIDUAL_EVENT_BYTES: number;
19
+ export declare function _makeLoggableRrwebEvent(slice: string, payload: string, sessionID: string, data: ReplaySessionData): StatsigEvent & {
20
+ metadata: RRWebPayload;
21
+ };
22
+ export declare function _slicePayload(payload: string): string[];
23
+ export declare function _appendSlicedMetadata(metadata: RRWebPayload, slicedID: string, sliceIndex: number, sliceCount: number, sliceByteSize: number): void;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._appendSlicedMetadata = exports._slicePayload = exports._makeLoggableRrwebEvent = exports.MAX_INDIVIDUAL_EVENT_BYTES = exports.REPLAY_ENQUEUE_TRIGGER_BYTES = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const REPLAY_SLICE_BYTES = 1024 * 1024; // 1 MB
6
+ exports.REPLAY_ENQUEUE_TRIGGER_BYTES = 1024 * 10; // 10 KB
7
+ exports.MAX_INDIVIDUAL_EVENT_BYTES = 1024 * 1024 * 10; // 10 MB
8
+ function _makeLoggableRrwebEvent(slice, payload, sessionID, data) {
9
+ const metadata = {
10
+ session_start_ts: String(data.startTime),
11
+ session_end_ts: String(data.endTime),
12
+ clicks_captured_cumulative: String(data.clickCount),
13
+ rrweb_events: slice,
14
+ rrweb_payload_size: String(payload.length),
15
+ session_replay_sdk_version: client_core_1.SDK_VERSION,
16
+ };
17
+ return {
18
+ eventName: 'statsig::session_recording',
19
+ value: sessionID,
20
+ metadata,
21
+ };
22
+ }
23
+ exports._makeLoggableRrwebEvent = _makeLoggableRrwebEvent;
24
+ function _slicePayload(payload) {
25
+ const parts = [];
26
+ for (let i = 0; i < payload.length; i += REPLAY_SLICE_BYTES) {
27
+ parts.push(payload.slice(i, i + REPLAY_SLICE_BYTES));
28
+ }
29
+ return parts;
30
+ }
31
+ exports._slicePayload = _slicePayload;
32
+ function _appendSlicedMetadata(metadata, slicedID, sliceIndex, sliceCount, sliceByteSize) {
33
+ metadata.sliced_id = slicedID;
34
+ metadata.slice_index = String(sliceIndex);
35
+ metadata.slice_count = String(sliceCount);
36
+ metadata.slice_byte_size = String(sliceByteSize);
37
+ }
38
+ exports._appendSlicedMetadata = _appendSlicedMetadata;
@@ -0,0 +1,34 @@
1
+ import { PrecomputedEvaluationsInterface, StatsigPlugin } from '@statsig/client-core';
2
+ import { EndReason, SessionReplayBase } from './SessionReplayBase';
3
+ import { RRWebConfig, ReplayEvent, ReplaySessionData } from './SessionReplayClient';
4
+ type SessionReplayOptions = {
5
+ rrwebConfig?: RRWebConfig;
6
+ forceRecording?: boolean;
7
+ };
8
+ export type TriggeredSessionReplayOptions = {
9
+ autoStartRecording?: boolean;
10
+ keepRollingWindow?: boolean;
11
+ } & SessionReplayOptions;
12
+ export declare class StatsigTriggeredSessionReplayPlugin implements StatsigPlugin<PrecomputedEvaluationsInterface> {
13
+ private readonly options?;
14
+ readonly __plugin = "triggered-session-replay";
15
+ constructor(options?: TriggeredSessionReplayOptions | undefined);
16
+ bind(client: PrecomputedEvaluationsInterface): void;
17
+ }
18
+ export declare function runStatsigSessionReplay(client: PrecomputedEvaluationsInterface, options?: SessionReplayOptions): void;
19
+ export declare function startRecording(sdkKey: string): void;
20
+ export declare function stopRecording(sdkKey: string): void;
21
+ export declare class TriggeredSessionReplay extends SessionReplayBase {
22
+ private _runningEventData;
23
+ private _isActiveRecording;
24
+ constructor(client: PrecomputedEvaluationsInterface, options?: TriggeredSessionReplayOptions);
25
+ startRecording(): void;
26
+ forceStartRecording(): void;
27
+ stopRecording(): void;
28
+ private _handleStartActiveRecording;
29
+ protected _shutdown(endReason?: EndReason): void;
30
+ protected _onRecordingEvent(event: ReplayEvent, data: ReplaySessionData, isCheckOut?: boolean): void;
31
+ protected _attemptToStartRollingWindow(): void;
32
+ protected _attemptToStartRecording(force?: boolean): void;
33
+ }
34
+ export {};
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TriggeredSessionReplay = exports.stopRecording = exports.startRecording = exports.runStatsigSessionReplay = exports.StatsigTriggeredSessionReplayPlugin = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const SessionReplayBase_1 = require("./SessionReplayBase");
6
+ class StatsigTriggeredSessionReplayPlugin {
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.__plugin = 'triggered-session-replay';
10
+ }
11
+ bind(client) {
12
+ runStatsigSessionReplay(client, this.options);
13
+ }
14
+ }
15
+ exports.StatsigTriggeredSessionReplayPlugin = StatsigTriggeredSessionReplayPlugin;
16
+ function runStatsigSessionReplay(client, options) {
17
+ new TriggeredSessionReplay(client, options);
18
+ }
19
+ exports.runStatsigSessionReplay = runStatsigSessionReplay;
20
+ function startRecording(sdkKey) {
21
+ var _a, _b;
22
+ const inst = (_b = (_a = (0, client_core_1._getStatsigGlobal)()) === null || _a === void 0 ? void 0 : _a.srInstances) === null || _b === void 0 ? void 0 : _b[sdkKey];
23
+ if (inst instanceof TriggeredSessionReplay) {
24
+ inst.startRecording();
25
+ }
26
+ }
27
+ exports.startRecording = startRecording;
28
+ function stopRecording(sdkKey) {
29
+ var _a, _b;
30
+ const inst = (_b = (_a = (0, client_core_1._getStatsigGlobal)()) === null || _a === void 0 ? void 0 : _a.srInstances) === null || _b === void 0 ? void 0 : _b[sdkKey];
31
+ if (inst instanceof TriggeredSessionReplay) {
32
+ inst.stopRecording();
33
+ }
34
+ }
35
+ exports.stopRecording = stopRecording;
36
+ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
37
+ constructor(client, options) {
38
+ var _a;
39
+ super(client, options);
40
+ this._runningEventData = [];
41
+ this._isActiveRecording = false;
42
+ this._client.$on('values_updated', () => {
43
+ var _a;
44
+ if (!this._wasStopped) {
45
+ if (options === null || options === void 0 ? void 0 : options.autoStartRecording) {
46
+ this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
47
+ }
48
+ else if (options === null || options === void 0 ? void 0 : options.keepRollingWindow) {
49
+ this._attemptToStartRollingWindow();
50
+ }
51
+ }
52
+ });
53
+ if (options === null || options === void 0 ? void 0 : options.autoStartRecording) {
54
+ this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
55
+ }
56
+ else if (options === null || options === void 0 ? void 0 : options.keepRollingWindow) {
57
+ this._attemptToStartRollingWindow();
58
+ }
59
+ }
60
+ startRecording() {
61
+ var _a;
62
+ this._wasStopped = false;
63
+ this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
64
+ }
65
+ forceStartRecording() {
66
+ super.forceStartRecording();
67
+ }
68
+ stopRecording() {
69
+ this._isActiveRecording = false;
70
+ this._runningEventData = [];
71
+ super.stopRecording();
72
+ }
73
+ _handleStartActiveRecording() {
74
+ this._isActiveRecording = true;
75
+ if (this._runningEventData.length === 0) {
76
+ return;
77
+ }
78
+ const currentEvents = this._runningEventData.map((e) => e.events).flat();
79
+ for (let i = 0; i < currentEvents.length; i++) {
80
+ currentEvents[i].event.eventIndex = i;
81
+ this._sessionData.clickCount += currentEvents[i].data.clickCount;
82
+ this._sessionData.startTime = Math.min(this._sessionData.startTime, currentEvents[i].data.startTime);
83
+ this._sessionData.endTime = Math.max(this._sessionData.endTime, currentEvents[i].data.endTime);
84
+ }
85
+ this._events = currentEvents.map((e) => e.event);
86
+ this._currentEventIndex = currentEvents.length;
87
+ if ((0, client_core_1._isCurrentlyVisible)()) {
88
+ this._bumpSessionIdleTimerAndLogRecording();
89
+ }
90
+ else {
91
+ this._logRecording();
92
+ }
93
+ }
94
+ _shutdown(endReason) {
95
+ this._isActiveRecording = false;
96
+ this._runningEventData = [];
97
+ super._shutdownImpl(endReason);
98
+ }
99
+ _onRecordingEvent(event, data, isCheckOut) {
100
+ if (!this._isActiveRecording) {
101
+ // The session has expired so we should clear the current data
102
+ if (this._currentSessionID !== this._getSessionIdFromClient()) {
103
+ this._shutdown('session_expired');
104
+ return;
105
+ }
106
+ if ((isCheckOut && event.type === 4) || // Type 4 and type 2 both show up as checkout events but we only want to start a new entry for type 4
107
+ this._runningEventData.length === 0) {
108
+ // We only want to keep two entries
109
+ if (this._runningEventData.length > 1) {
110
+ this._runningEventData.shift();
111
+ }
112
+ this._runningEventData.push({ events: [{ event, data }] });
113
+ }
114
+ else {
115
+ this._runningEventData[this._runningEventData.length - 1].events.push({
116
+ event,
117
+ data,
118
+ });
119
+ }
120
+ return;
121
+ }
122
+ super._onRecordingEvent(event, data);
123
+ }
124
+ _attemptToStartRollingWindow() {
125
+ var _a, _b;
126
+ const values = this._client.getContext().values;
127
+ if ((values === null || values === void 0 ? void 0 : values.can_record_session) !== true) {
128
+ this._shutdown();
129
+ return;
130
+ }
131
+ if (this._replayer.isRecording()) {
132
+ return;
133
+ }
134
+ this._replayer.record((e, d, isCheckOut) => this._onRecordingEvent(e, d, isCheckOut), (_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.rrwebConfig) !== null && _b !== void 0 ? _b : {}, () => {
135
+ this._shutdown();
136
+ }, true);
137
+ }
138
+ _attemptToStartRecording(force = false) {
139
+ var _a, _b;
140
+ const values = this._client.getContext().values;
141
+ if (!force && (values === null || values === void 0 ? void 0 : values.can_record_session) !== true) {
142
+ this._shutdown();
143
+ return;
144
+ }
145
+ this._handleStartActiveRecording();
146
+ this._wasStopped = false;
147
+ client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'true' });
148
+ if (this._replayer.isRecording()) {
149
+ return;
150
+ }
151
+ this._replayer.record((e, d, isCheckOut) => this._onRecordingEvent(e, d, isCheckOut), (_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.rrwebConfig) !== null && _b !== void 0 ? _b : {}, () => {
152
+ this._shutdown();
153
+ });
154
+ }
155
+ }
156
+ exports.TriggeredSessionReplay = TriggeredSessionReplay;
package/src/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { SessionReplay, StatsigSessionReplayPlugin, runStatsigSessionReplay } from './SessionReplay';
2
2
  import { SessionReplayClient } from './SessionReplayClient';
3
+ import { StatsigTriggeredSessionReplayPlugin, startRecording, stopRecording } from './TriggeredSessionReplay';
3
4
  export type { ReplaySessionData as ReplayData, ReplayEvent, } from './SessionReplayClient';
4
- export { SessionReplayClient, SessionReplay, runStatsigSessionReplay, StatsigSessionReplayPlugin, };
5
+ export { SessionReplayClient, SessionReplay, runStatsigSessionReplay, StatsigSessionReplayPlugin, StatsigTriggeredSessionReplayPlugin, startRecording, stopRecording, };
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StatsigSessionReplayPlugin = exports.runStatsigSessionReplay = exports.SessionReplay = exports.SessionReplayClient = void 0;
3
+ exports.stopRecording = exports.startRecording = exports.StatsigTriggeredSessionReplayPlugin = exports.StatsigSessionReplayPlugin = exports.runStatsigSessionReplay = exports.SessionReplay = exports.SessionReplayClient = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
5
  const SessionReplay_1 = require("./SessionReplay");
6
6
  Object.defineProperty(exports, "SessionReplay", { enumerable: true, get: function () { return SessionReplay_1.SessionReplay; } });
@@ -8,6 +8,10 @@ Object.defineProperty(exports, "StatsigSessionReplayPlugin", { enumerable: true,
8
8
  Object.defineProperty(exports, "runStatsigSessionReplay", { enumerable: true, get: function () { return SessionReplay_1.runStatsigSessionReplay; } });
9
9
  const SessionReplayClient_1 = require("./SessionReplayClient");
10
10
  Object.defineProperty(exports, "SessionReplayClient", { enumerable: true, get: function () { return SessionReplayClient_1.SessionReplayClient; } });
11
+ const TriggeredSessionReplay_1 = require("./TriggeredSessionReplay");
12
+ Object.defineProperty(exports, "StatsigTriggeredSessionReplayPlugin", { enumerable: true, get: function () { return TriggeredSessionReplay_1.StatsigTriggeredSessionReplayPlugin; } });
13
+ Object.defineProperty(exports, "startRecording", { enumerable: true, get: function () { return TriggeredSessionReplay_1.startRecording; } });
14
+ Object.defineProperty(exports, "stopRecording", { enumerable: true, get: function () { return TriggeredSessionReplay_1.stopRecording; } });
11
15
  Object.assign((0, client_core_1._getStatsigGlobal)(), {
12
16
  SessionReplayClient: SessionReplayClient_1.SessionReplayClient,
13
17
  SessionReplay: SessionReplay_1.SessionReplay,