@statsig/session-replay 3.16.1 → 3.17.0

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.1",
3
+ "version": "3.17.0",
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.1"
13
+ "@statsig/client-core": "3.17.0"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@rrweb/types": "2.0.0-alpha.14"
@@ -3,6 +3,7 @@ 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
5
  const SessionReplayBase_1 = require("./SessionReplayBase");
6
+ const SessionReplayUtils_1 = require("./SessionReplayUtils");
6
7
  class StatsigSessionReplayPlugin {
7
8
  constructor(options) {
8
9
  this.options = options;
@@ -34,11 +35,22 @@ class SessionReplay extends SessionReplayBase_1.SessionReplayBase {
34
35
  }
35
36
  _attemptToStartRecording(force = false) {
36
37
  var _a, _b;
38
+ if (this._totalLogs >= SessionReplayUtils_1.MAX_LOGS) {
39
+ return;
40
+ }
37
41
  const values = this._client.getContext().values;
42
+ if ((values === null || values === void 0 ? void 0 : values.recording_blocked) === true) {
43
+ this._shutdown();
44
+ return;
45
+ }
38
46
  if (!force && (values === null || values === void 0 ? void 0 : values.can_record_session) !== true) {
39
47
  this._shutdown();
40
48
  return;
41
49
  }
50
+ if ((values === null || values === void 0 ? void 0 : values.passes_session_recording_targeting) === false) {
51
+ this._shutdown();
52
+ return;
53
+ }
42
54
  if (this._replayer.isRecording()) {
43
55
  return;
44
56
  }
@@ -15,6 +15,7 @@ export declare abstract class SessionReplayBase {
15
15
  protected _replayer: SessionReplayClient;
16
16
  protected _wasStopped: boolean;
17
17
  protected _currentEventIndex: number;
18
+ protected _totalLogs: number;
18
19
  constructor(client: PrecomputedEvaluationsInterface, options?: SessionReplayOptions);
19
20
  forceStartRecording(): void;
20
21
  stopRecording(): void;
@@ -28,6 +29,7 @@ export declare abstract class SessionReplayBase {
28
29
  protected _getSessionIdFromClient(): string;
29
30
  protected abstract _shutdown(endReason?: EndReason): void;
30
31
  protected _shutdownImpl(endReason?: EndReason): void;
32
+ private _makeEmptySessionData;
31
33
  protected _onRecordingEvent(event: ReplayEvent, data: ReplaySessionData): void;
32
34
  }
33
35
  export {};
@@ -8,13 +8,14 @@ const SizeOf_1 = require("./SizeOf");
8
8
  class SessionReplayBase {
9
9
  constructor(client, options) {
10
10
  this._sessionData = {
11
- startTime: 0,
11
+ startTime: -1,
12
12
  endTime: 0,
13
13
  clickCount: 0,
14
14
  };
15
15
  this._events = [];
16
16
  this._wasStopped = false;
17
17
  this._currentEventIndex = 0;
18
+ this._totalLogs = 0;
18
19
  this._client = client;
19
20
  this._options = options;
20
21
  const { sdkKey, errorBoundary } = this._client.getContext();
@@ -87,6 +88,7 @@ class SessionReplayBase {
87
88
  if (endReason) {
88
89
  event.metadata[endReason] = 'true';
89
90
  }
91
+ this._totalLogs++;
90
92
  this._client.logEvent(event);
91
93
  if (slicedID != null) {
92
94
  this._client.flush().catch((e) => {
@@ -95,6 +97,9 @@ class SessionReplayBase {
95
97
  }
96
98
  }
97
99
  this._events = [];
100
+ if (this._totalLogs > SessionReplayUtils_1.MAX_LOGS) {
101
+ this._shutdown();
102
+ }
98
103
  }
99
104
  _bumpSessionIdleTimerAndLogRecording() {
100
105
  this._getSessionIdFromClient();
@@ -104,15 +109,28 @@ class SessionReplayBase {
104
109
  return this._client.getContext().session.data.sessionID;
105
110
  }
106
111
  _shutdownImpl(endReason) {
107
- this._replayer.stop();
108
- client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
109
- this._currentEventIndex = 0;
112
+ if (this._replayer.isRecording()) {
113
+ this._replayer.stop();
114
+ client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
115
+ }
110
116
  if (this._events.length === 0) {
117
+ // only reset if session expired otherwise we might start recording again
118
+ if (endReason === 'session_expired') {
119
+ this._currentEventIndex = 0;
120
+ this._sessionData = this._makeEmptySessionData();
121
+ }
111
122
  return;
112
123
  }
113
124
  this._logRecording(endReason);
114
- this._sessionData = {
115
- startTime: 0,
125
+ // only reset if session expired otherwise we might start recording again
126
+ if (endReason === 'session_expired') {
127
+ this._currentEventIndex = 0;
128
+ this._sessionData = this._makeEmptySessionData();
129
+ }
130
+ }
131
+ _makeEmptySessionData() {
132
+ return {
133
+ startTime: -1,
116
134
  endTime: 0,
117
135
  clickCount: 0,
118
136
  };
@@ -123,10 +141,17 @@ class SessionReplayBase {
123
141
  this._shutdown('session_expired');
124
142
  return;
125
143
  }
144
+ if (this._totalLogs >= SessionReplayUtils_1.MAX_LOGS) {
145
+ this._shutdown();
146
+ return;
147
+ }
126
148
  event.eventIndex = this._currentEventIndex++;
127
149
  // Update the session data
128
150
  this._sessionData.clickCount += data.clickCount;
129
- this._sessionData.startTime = Math.min(this._sessionData.startTime, data.startTime);
151
+ this._sessionData.startTime =
152
+ this._sessionData.startTime === -1
153
+ ? data.startTime
154
+ : Math.min(this._sessionData.startTime, data.startTime);
130
155
  this._sessionData.endTime = Math.max(this._sessionData.endTime, data.endTime);
131
156
  const eventApproxSize = (0, SizeOf_1._fastApproxSizeOf)(event, SessionReplayUtils_1.MAX_INDIVIDUAL_EVENT_BYTES);
132
157
  if (eventApproxSize > SessionReplayUtils_1.MAX_INDIVIDUAL_EVENT_BYTES) {
@@ -15,7 +15,6 @@ export declare class SessionReplayClient {
15
15
  private _stopCallback?;
16
16
  private _startTimestamp;
17
17
  private _endTimestamp;
18
- private _eventCounter;
19
18
  record(callback: (latest: ReplayEvent, data: ReplaySessionData, isCheckout?: boolean) => void, config: RRWebConfig, stopCallback?: () => void, keepRollingWindow?: boolean): void;
20
19
  stop(): void;
21
20
  isRecording(): boolean;
@@ -4,12 +4,11 @@ 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
+ const CHECKOUT_WINDOW_MS = 1000 * 30; // 30 seconds
8
8
  class SessionReplayClient {
9
9
  constructor() {
10
10
  this._startTimestamp = null;
11
11
  this._endTimestamp = null;
12
- this._eventCounter = 0;
13
12
  }
14
13
  record(callback, config, stopCallback, keepRollingWindow = false) {
15
14
  if ((0, client_core_1._getDocumentSafe)() == null) {
@@ -18,7 +17,6 @@ class SessionReplayClient {
18
17
  // Always reset session id and tracking fields for a new recording
19
18
  this._startTimestamp = null;
20
19
  this._endTimestamp = null;
21
- this._eventCounter = 0;
22
20
  this._stopCallback = stopCallback;
23
21
  if (this._stopFn) {
24
22
  return;
@@ -16,6 +16,7 @@ export type RRWebPayload = {
16
16
  };
17
17
  export declare const REPLAY_ENQUEUE_TRIGGER_BYTES: number;
18
18
  export declare const MAX_INDIVIDUAL_EVENT_BYTES: number;
19
+ export declare const MAX_LOGS = 10000;
19
20
  export declare function _makeLoggableRrwebEvent(slice: string, payload: string, sessionID: string, data: ReplaySessionData): StatsigEvent & {
20
21
  metadata: RRWebPayload;
21
22
  };
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
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;
3
+ exports._appendSlicedMetadata = exports._slicePayload = exports._makeLoggableRrwebEvent = exports.MAX_LOGS = exports.MAX_INDIVIDUAL_EVENT_BYTES = exports.REPLAY_ENQUEUE_TRIGGER_BYTES = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
5
  const REPLAY_SLICE_BYTES = 1024 * 1024; // 1 MB
6
6
  exports.REPLAY_ENQUEUE_TRIGGER_BYTES = 1024 * 10; // 10 KB
7
7
  exports.MAX_INDIVIDUAL_EVENT_BYTES = 1024 * 1024 * 10; // 10 MB
8
+ exports.MAX_LOGS = 10000; // 10K logs
8
9
  function _makeLoggableRrwebEvent(slice, payload, sessionID, data) {
9
10
  const metadata = {
10
11
  session_start_ts: String(data.startTime),
@@ -17,11 +17,18 @@ export declare class StatsigTriggeredSessionReplayPlugin implements StatsigPlugi
17
17
  }
18
18
  export declare function runStatsigSessionReplay(client: PrecomputedEvaluationsInterface, options?: SessionReplayOptions): void;
19
19
  export declare function startRecording(sdkKey: string): void;
20
+ export declare function forceStartRecording(sdkKey: string): void;
20
21
  export declare function stopRecording(sdkKey: string): void;
21
22
  export declare class TriggeredSessionReplay extends SessionReplayBase {
22
23
  private _runningEventData;
23
24
  private _isActiveRecording;
24
25
  constructor(client: PrecomputedEvaluationsInterface, options?: TriggeredSessionReplayOptions);
26
+ private _subscribeToClientEvents;
27
+ private _subscribeToValuesUpdated;
28
+ private _subscribeToLogEventCalled;
29
+ private _subscribeToGateEvaluation;
30
+ private _subscribeToExperimentEvaluation;
31
+ private _tryStartExposureRecording;
25
32
  startRecording(): void;
26
33
  forceStartRecording(): void;
27
34
  stopRecording(): void;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TriggeredSessionReplay = exports.stopRecording = exports.startRecording = exports.runStatsigSessionReplay = exports.StatsigTriggeredSessionReplayPlugin = void 0;
3
+ exports.TriggeredSessionReplay = exports.stopRecording = exports.forceStartRecording = exports.startRecording = exports.runStatsigSessionReplay = exports.StatsigTriggeredSessionReplayPlugin = void 0;
4
4
  const client_core_1 = require("@statsig/client-core");
5
5
  const SessionReplayBase_1 = require("./SessionReplayBase");
6
+ const SessionReplayUtils_1 = require("./SessionReplayUtils");
6
7
  class StatsigTriggeredSessionReplayPlugin {
7
8
  constructor(options) {
8
9
  this.options = options;
@@ -25,6 +26,14 @@ function startRecording(sdkKey) {
25
26
  }
26
27
  }
27
28
  exports.startRecording = startRecording;
29
+ function forceStartRecording(sdkKey) {
30
+ var _a, _b;
31
+ 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];
32
+ if (inst instanceof TriggeredSessionReplay) {
33
+ inst.forceStartRecording();
34
+ }
35
+ }
36
+ exports.forceStartRecording = forceStartRecording;
28
37
  function stopRecording(sdkKey) {
29
38
  var _a, _b;
30
39
  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];
@@ -39,6 +48,21 @@ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
39
48
  super(client, options);
40
49
  this._runningEventData = [];
41
50
  this._isActiveRecording = false;
51
+ this._subscribeToClientEvents(options);
52
+ if (options === null || options === void 0 ? void 0 : options.autoStartRecording) {
53
+ this._attemptToStartRecording((_a = this._options) === null || _a === void 0 ? void 0 : _a.forceRecording);
54
+ }
55
+ else if (options === null || options === void 0 ? void 0 : options.keepRollingWindow) {
56
+ this._attemptToStartRollingWindow();
57
+ }
58
+ }
59
+ _subscribeToClientEvents(options) {
60
+ this._subscribeToValuesUpdated(options);
61
+ this._subscribeToLogEventCalled();
62
+ this._subscribeToGateEvaluation();
63
+ this._subscribeToExperimentEvaluation();
64
+ }
65
+ _subscribeToValuesUpdated(options) {
42
66
  this._client.$on('values_updated', () => {
43
67
  var _a;
44
68
  if (!this._wasStopped) {
@@ -50,11 +74,74 @@ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
50
74
  }
51
75
  }
52
76
  });
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);
77
+ }
78
+ _subscribeToLogEventCalled() {
79
+ this._client.$on('log_event_called', (event) => {
80
+ var _a;
81
+ if (this._wasStopped) {
82
+ return;
83
+ }
84
+ const values = this._client.getContext().values;
85
+ const passedTargeting = values === null || values === void 0 ? void 0 : values.passes_session_recording_targeting;
86
+ if (passedTargeting === false ||
87
+ (values === null || values === void 0 ? void 0 : values.session_recording_event_triggers) == null) {
88
+ return;
89
+ }
90
+ const trigger = values.session_recording_event_triggers[event.event.eventName];
91
+ if (trigger == null) {
92
+ return;
93
+ }
94
+ if (trigger.passes_sampling === false) {
95
+ return;
96
+ }
97
+ const targetValues = trigger.values;
98
+ if (targetValues == null) {
99
+ this._attemptToStartRecording(true);
100
+ return;
101
+ }
102
+ if (targetValues.includes(String((_a = event.event.value) !== null && _a !== void 0 ? _a : ''))) {
103
+ this._attemptToStartRecording(true);
104
+ return;
105
+ }
106
+ });
107
+ }
108
+ _subscribeToGateEvaluation() {
109
+ this._client.$on('gate_evaluation', (event) => {
110
+ this._tryStartExposureRecording(event.gate.name, String(event.gate.value));
111
+ });
112
+ }
113
+ _subscribeToExperimentEvaluation() {
114
+ this._client.$on('experiment_evaluation', (event) => {
115
+ var _a;
116
+ this._tryStartExposureRecording(event.experiment.name, (_a = event.experiment.groupName) !== null && _a !== void 0 ? _a : '');
117
+ });
118
+ }
119
+ _tryStartExposureRecording(name, value) {
120
+ var _a;
121
+ if (this._wasStopped) {
122
+ return;
55
123
  }
56
- else if (options === null || options === void 0 ? void 0 : options.keepRollingWindow) {
57
- this._attemptToStartRollingWindow();
124
+ const values = this._client.getContext().values;
125
+ const passedTargeting = values === null || values === void 0 ? void 0 : values.passes_session_recording_targeting;
126
+ if (passedTargeting === false ||
127
+ (values === null || values === void 0 ? void 0 : values.session_recording_exposure_triggers) == null) {
128
+ return;
129
+ }
130
+ const trigger = (_a = values.session_recording_exposure_triggers[name]) !== null && _a !== void 0 ? _a : values.session_recording_exposure_triggers[(0, client_core_1._DJB2)(name)];
131
+ if (trigger == null) {
132
+ return;
133
+ }
134
+ if (trigger.passes_sampling === false) {
135
+ return;
136
+ }
137
+ const targetValues = trigger.values;
138
+ if (targetValues == null) {
139
+ this._attemptToStartRecording(true);
140
+ return;
141
+ }
142
+ if (targetValues.includes(value)) {
143
+ this._attemptToStartRecording(true);
144
+ return;
58
145
  }
59
146
  }
60
147
  startRecording() {
@@ -79,7 +166,10 @@ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
79
166
  for (let i = 0; i < currentEvents.length; i++) {
80
167
  currentEvents[i].event.eventIndex = i;
81
168
  this._sessionData.clickCount += currentEvents[i].data.clickCount;
82
- this._sessionData.startTime = Math.min(this._sessionData.startTime, currentEvents[i].data.startTime);
169
+ this._sessionData.startTime =
170
+ this._sessionData.startTime === -1
171
+ ? currentEvents[i].data.startTime
172
+ : Math.min(this._sessionData.startTime, currentEvents[i].data.startTime);
83
173
  this._sessionData.endTime = Math.max(this._sessionData.endTime, currentEvents[i].data.endTime);
84
174
  }
85
175
  this._events = currentEvents.map((e) => e.event);
@@ -90,6 +180,8 @@ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
90
180
  else {
91
181
  this._logRecording();
92
182
  }
183
+ // stop recording and since it will be started again
184
+ this._replayer.stop();
93
185
  }
94
186
  _shutdown(endReason) {
95
187
  this._isActiveRecording = false;
@@ -124,7 +216,7 @@ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
124
216
  _attemptToStartRollingWindow() {
125
217
  var _a, _b;
126
218
  const values = this._client.getContext().values;
127
- if ((values === null || values === void 0 ? void 0 : values.can_record_session) !== true) {
219
+ if ((values === null || values === void 0 ? void 0 : values.passes_session_recording_targeting) === false) {
128
220
  this._shutdown();
129
221
  return;
130
222
  }
@@ -137,11 +229,22 @@ class TriggeredSessionReplay extends SessionReplayBase_1.SessionReplayBase {
137
229
  }
138
230
  _attemptToStartRecording(force = false) {
139
231
  var _a, _b;
232
+ if (this._totalLogs >= SessionReplayUtils_1.MAX_LOGS) {
233
+ return;
234
+ }
140
235
  const values = this._client.getContext().values;
236
+ if ((values === null || values === void 0 ? void 0 : values.recording_blocked) === true) {
237
+ this._shutdown();
238
+ return;
239
+ }
141
240
  if (!force && (values === null || values === void 0 ? void 0 : values.can_record_session) !== true) {
142
241
  this._shutdown();
143
242
  return;
144
243
  }
244
+ if ((values === null || values === void 0 ? void 0 : values.passes_session_recording_targeting) === false) {
245
+ this._shutdown();
246
+ return;
247
+ }
145
248
  this._handleStartActiveRecording();
146
249
  this._wasStopped = false;
147
250
  client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'true' });
package/src/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SessionReplay, StatsigSessionReplayPlugin, runStatsigSessionReplay } from './SessionReplay';
2
2
  import { SessionReplayClient } from './SessionReplayClient';
3
- import { StatsigTriggeredSessionReplayPlugin, startRecording, stopRecording } from './TriggeredSessionReplay';
3
+ import { StatsigTriggeredSessionReplayPlugin, forceStartRecording, startRecording, stopRecording } from './TriggeredSessionReplay';
4
4
  export type { ReplaySessionData as ReplayData, ReplayEvent, } from './SessionReplayClient';
5
- export { SessionReplayClient, SessionReplay, runStatsigSessionReplay, StatsigSessionReplayPlugin, StatsigTriggeredSessionReplayPlugin, startRecording, stopRecording, };
5
+ export { SessionReplayClient, SessionReplay, runStatsigSessionReplay, StatsigSessionReplayPlugin, StatsigTriggeredSessionReplayPlugin, startRecording, stopRecording, forceStartRecording, };
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.stopRecording = exports.startRecording = exports.StatsigTriggeredSessionReplayPlugin = exports.StatsigSessionReplayPlugin = exports.runStatsigSessionReplay = exports.SessionReplay = exports.SessionReplayClient = void 0;
3
+ exports.forceStartRecording = 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; } });
@@ -10,6 +10,7 @@ const SessionReplayClient_1 = require("./SessionReplayClient");
10
10
  Object.defineProperty(exports, "SessionReplayClient", { enumerable: true, get: function () { return SessionReplayClient_1.SessionReplayClient; } });
11
11
  const TriggeredSessionReplay_1 = require("./TriggeredSessionReplay");
12
12
  Object.defineProperty(exports, "StatsigTriggeredSessionReplayPlugin", { enumerable: true, get: function () { return TriggeredSessionReplay_1.StatsigTriggeredSessionReplayPlugin; } });
13
+ Object.defineProperty(exports, "forceStartRecording", { enumerable: true, get: function () { return TriggeredSessionReplay_1.forceStartRecording; } });
13
14
  Object.defineProperty(exports, "startRecording", { enumerable: true, get: function () { return TriggeredSessionReplay_1.startRecording; } });
14
15
  Object.defineProperty(exports, "stopRecording", { enumerable: true, get: function () { return TriggeredSessionReplay_1.stopRecording; } });
15
16
  Object.assign((0, client_core_1._getStatsigGlobal)(), {