mixpanel-browser 2.55.1 → 2.56.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.
@@ -1,143 +1,51 @@
1
- import { record } from 'rrweb';
2
- import { IncrementalSource, EventType } from '@rrweb/types';
1
+ import {record} from 'rrweb';
3
2
 
4
- import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase
5
- import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
6
- import { RequestBatcher } from '../request-batcher';
3
+ import { SessionRecording } from './session-recording';
4
+ import { console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase
7
5
 
8
6
  var logger = console_with_prefix('recorder');
9
- var CompressionStream = window['CompressionStream'];
10
-
11
- var RECORDER_BATCHER_LIB_CONFIG = {
12
- 'batch_size': 1000,
13
- 'batch_flush_interval_ms': 10 * 1000,
14
- 'batch_request_timeout_ms': 90 * 1000,
15
- 'batch_autostart': true
16
- };
17
-
18
- var ACTIVE_SOURCES = new Set([
19
- IncrementalSource.MouseMove,
20
- IncrementalSource.MouseInteraction,
21
- IncrementalSource.Scroll,
22
- IncrementalSource.ViewportResize,
23
- IncrementalSource.Input,
24
- IncrementalSource.TouchMove,
25
- IncrementalSource.MediaInteraction,
26
- IncrementalSource.Drag,
27
- IncrementalSource.Selection,
28
- ]);
29
-
30
- function isUserEvent(ev) {
31
- return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
32
- }
33
7
 
8
+ /**
9
+ * Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
10
+ * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
11
+ */
34
12
  var MixpanelRecorder = function(mixpanelInstance) {
35
13
  this._mixpanel = mixpanelInstance;
36
-
37
- // internal rrweb stopRecording function
38
- this._stopRecording = null;
39
-
40
- this.recEvents = [];
41
- this.seqNo = 0;
42
- this.replayId = null;
43
- this.replayStartTime = null;
44
- this.sendBatchId = null;
45
-
46
- this.idleTimeoutId = null;
47
- this.maxTimeoutId = null;
48
-
49
- this.recordMaxMs = MAX_RECORDING_MS;
50
- this.recordMinMs = 0;
51
- this._initBatcher();
52
- };
53
-
54
-
55
- MixpanelRecorder.prototype._initBatcher = function () {
56
- this.batcher = new RequestBatcher('__mprec', {
57
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
58
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
59
- errorReporter: _.bind(this.reportError, this),
60
- flushOnlyOnInterval: true,
61
- usePersistence: false
62
- });
14
+ this.activeRecording = null;
63
15
  };
64
16
 
65
- // eslint-disable-next-line camelcase
66
- MixpanelRecorder.prototype.get_config = function(configVar) {
67
- return this._mixpanel.get_config(configVar);
68
- };
69
-
70
- MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
71
- if (this._stopRecording !== null) {
17
+ MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
18
+ if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
72
19
  logger.log('Recording already in progress, skipping startRecording.');
73
20
  return;
74
21
  }
75
22
 
76
- this.recordMaxMs = this.get_config('record_max_ms');
77
- if (this.recordMaxMs > MAX_RECORDING_MS) {
78
- this.recordMaxMs = MAX_RECORDING_MS;
79
- logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
80
- }
81
-
82
- this.recordMinMs = this.get_config('record_min_ms');
83
- if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
84
- this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
85
- logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
86
- }
87
-
88
- this.recEvents = [];
89
- this.seqNo = 0;
90
- this.replayStartTime = new Date().getTime();
91
-
92
- this.replayId = _.UUID();
93
-
94
- if (shouldStopBatcher || this.recordMinMs > 0) {
95
- // the primary case for shouldStopBatcher is when we're starting recording after a reset
96
- // and don't want to send anything over the network until there's
97
- // actual user activity
98
- // this also applies if the minimum recording length has not been hit yet
99
- // so that we don't send data until we know the recording will be long enough
100
- this.batcher.stop();
101
- } else {
102
- this.batcher.start();
103
- }
104
-
105
- var resetIdleTimeout = _.bind(function () {
106
- clearTimeout(this.idleTimeoutId);
107
- this.idleTimeoutId = setTimeout(_.bind(function () {
108
- logger.log('Idle timeout reached, restarting recording.');
109
- this.resetRecording();
110
- }, this), this.get_config('record_idle_timeout_ms'));
23
+ var onIdleTimeout = _.bind(function () {
24
+ logger.log('Idle timeout reached, restarting recording.');
25
+ this.resetRecording();
111
26
  }, this);
112
27
 
113
- var blockSelector = this.get_config('record_block_selector');
114
- if (blockSelector === '' || blockSelector === null) {
115
- blockSelector = undefined;
116
- }
28
+ var onMaxLengthReached = _.bind(function () {
29
+ logger.log('Max recording length reached, stopping recording.');
30
+ this.resetRecording();
31
+ }, this);
117
32
 
118
- this._stopRecording = record({
119
- 'emit': _.bind(function (ev) {
120
- this.batcher.enqueue(ev);
121
- if (isUserEvent(ev)) {
122
- if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
123
- // start flushing again after user activity
124
- this.batcher.start();
125
- }
126
- resetIdleTimeout();
127
- }
128
- }, this),
129
- 'blockClass': this.get_config('record_block_class'),
130
- 'blockSelector': blockSelector,
131
- 'collectFonts': this.get_config('record_collect_fonts'),
132
- 'inlineImages': this.get_config('record_inline_images'),
133
- 'maskAllInputs': true,
134
- 'maskTextClass': this.get_config('record_mask_text_class'),
135
- 'maskTextSelector': this.get_config('record_mask_text_selector')
33
+ this.activeRecording = new SessionRecording({
34
+ mixpanelInstance: this._mixpanel,
35
+ onIdleTimeout: onIdleTimeout,
36
+ onMaxLengthReached: onMaxLengthReached,
37
+ replayId: _.UUID(),
38
+ rrwebRecord: record
136
39
  });
137
40
 
138
- resetIdleTimeout();
41
+ this.activeRecording.startRecording(shouldStopBatcher);
42
+ };
139
43
 
140
- this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
44
+ MixpanelRecorder.prototype.stopRecording = function() {
45
+ if (this.activeRecording) {
46
+ this.activeRecording.stopRecording();
47
+ this.activeRecording = null;
48
+ }
141
49
  };
142
50
 
143
51
  MixpanelRecorder.prototype.resetRecording = function () {
@@ -145,141 +53,20 @@ MixpanelRecorder.prototype.resetRecording = function () {
145
53
  this.startRecording(true);
146
54
  };
147
55
 
148
- MixpanelRecorder.prototype.stopRecording = function () {
149
- if (this._stopRecording !== null) {
150
- this._stopRecording();
151
- this._stopRecording = null;
152
- }
153
-
154
- if (this.batcher.stopped) {
155
- // never got user activity to flush after reset, so just clear the batcher
156
- this.batcher.clear();
56
+ MixpanelRecorder.prototype.getActiveReplayId = function () {
57
+ if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
58
+ return this.activeRecording.replayId;
157
59
  } else {
158
- // flush any remaining events from running batcher
159
- this.batcher.flush();
160
- this.batcher.stop();
60
+ return null;
161
61
  }
162
- this.replayId = null;
163
-
164
- clearTimeout(this.idleTimeoutId);
165
- clearTimeout(this.maxTimeoutId);
166
- };
167
-
168
- /**
169
- * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
170
- * we stop recording and dump any queued events if the user has opted out.
171
- */
172
- MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
173
- this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
174
62
  };
175
63
 
176
- MixpanelRecorder.prototype._onOptOut = function (code) {
177
- // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
178
- if (code === 0) {
179
- this.recEvents = [];
180
- this.stopRecording();
181
- }
182
- };
183
-
184
- MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
185
- var onSuccess = _.bind(function (response, responseBody) {
186
- // Increment sequence counter only if the request was successful to guarantee ordering.
187
- // RequestBatcher will always flush the next batch after the previous one succeeds.
188
- // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
189
- if (response.status === 200 && this.replayId === currentReplayId) {
190
- this.seqNo++;
191
- }
192
- callback({
193
- status: 0,
194
- httpStatusCode: response.status,
195
- responseBody: responseBody,
196
- retryAfter: response.headers.get('Retry-After')
197
- });
198
- }, this);
199
-
200
- window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
201
- 'method': 'POST',
202
- 'headers': {
203
- 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
204
- 'Content-Type': 'application/octet-stream'
205
- },
206
- 'body': reqBody,
207
- }).then(function (response) {
208
- response.json().then(function (responseBody) {
209
- onSuccess(response, responseBody);
210
- }).catch(function (error) {
211
- callback({error: error});
212
- });
213
- }).catch(function (error) {
214
- callback({error: error, httpStatusCode: 0});
215
- });
216
- };
217
-
218
- MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
219
- const numEvents = data.length;
220
-
221
- if (numEvents > 0) {
222
- var replayId = this.replayId;
223
- // each rrweb event has a timestamp - leverage those to get time properties
224
- var batchStartTime = data[0].timestamp;
225
- if (this.seqNo === 0 || !this.replayStartTime) {
226
- // extra safety net so that we don't send a null replay start time
227
- if (this.seqNo !== 0) {
228
- this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
229
- }
230
-
231
- this.replayStartTime = batchStartTime;
232
- }
233
- var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
234
-
235
- var reqParams = {
236
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
237
- 'seq': this.seqNo,
238
- 'batch_start_time': batchStartTime / 1000,
239
- 'replay_id': replayId,
240
- 'replay_length_ms': replayLengthMs,
241
- 'replay_start_time': this.replayStartTime / 1000
242
- };
243
- var eventsJson = _.JSONEncode(data);
244
-
245
- // send ID management props if they exist
246
- var deviceId = this._mixpanel.get_property('$device_id');
247
- if (deviceId) {
248
- reqParams['$device_id'] = deviceId;
249
- }
250
- var userId = this._mixpanel.get_property('$user_id');
251
- if (userId) {
252
- reqParams['$user_id'] = userId;
253
- }
254
-
255
- if (CompressionStream) {
256
- var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
257
- var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
258
- new Response(gzipStream)
259
- .blob()
260
- .then(_.bind(function(compressedBlob) {
261
- reqParams['format'] = 'gzip';
262
- this._sendRequest(replayId, reqParams, compressedBlob, callback);
263
- }, this));
264
- } else {
265
- reqParams['format'] = 'body';
266
- this._sendRequest(replayId, reqParams, eventsJson, callback);
267
- }
64
+ // getter so that older mixpanel-core versions can still retrieve the replay ID
65
+ // when pulling the latest recorder bundle from the CDN
66
+ Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
67
+ get: function () {
68
+ return this.getActiveReplayId();
268
69
  }
269
70
  });
270
71
 
271
-
272
- MixpanelRecorder.prototype.reportError = function(msg, err) {
273
- logger.error.apply(logger.error, arguments);
274
- try {
275
- if (!err && !(msg instanceof Error)) {
276
- msg = new Error(msg);
277
- }
278
- this.get_config('error_reporter')(msg, err);
279
- } catch(err) {
280
- logger.error(err);
281
- }
282
- };
283
-
284
-
285
72
  window['__mp_recorder'] = MixpanelRecorder;
@@ -12,7 +12,8 @@ export default {
12
12
  file: 'build/mixpanel-recorder.min.js',
13
13
  format: 'esm',
14
14
  name: 'version',
15
- plugins: [esbuild({minify: true})]
15
+ plugins: [esbuild({minify: true, sourceMap: true})],
16
+ sourcemap: true,
16
17
  }
17
18
  ],
18
19
  plugins: [nodeResolve({browser: true})],
@@ -0,0 +1,305 @@
1
+ import { IncrementalSource, EventType } from '@rrweb/types';
2
+
3
+ import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase
4
+ import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
5
+ import { RequestBatcher } from '../request-batcher';
6
+ import Config from '../config';
7
+
8
+ var logger = console_with_prefix('recorder');
9
+ var CompressionStream = window['CompressionStream'];
10
+
11
+ var RECORDER_BATCHER_LIB_CONFIG = {
12
+ 'batch_size': 1000,
13
+ 'batch_flush_interval_ms': 10 * 1000,
14
+ 'batch_request_timeout_ms': 90 * 1000,
15
+ 'batch_autostart': true
16
+ };
17
+
18
+ var ACTIVE_SOURCES = new Set([
19
+ IncrementalSource.MouseMove,
20
+ IncrementalSource.MouseInteraction,
21
+ IncrementalSource.Scroll,
22
+ IncrementalSource.ViewportResize,
23
+ IncrementalSource.Input,
24
+ IncrementalSource.TouchMove,
25
+ IncrementalSource.MediaInteraction,
26
+ IncrementalSource.Drag,
27
+ IncrementalSource.Selection,
28
+ ]);
29
+
30
+ function isUserEvent(ev) {
31
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
32
+ }
33
+
34
+ /**
35
+ * This class encapsulates a single session recording and its lifecycle.
36
+ * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
37
+ * @param {String} [options.replayId] - unique uuid for a single replay
38
+ * @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
39
+ * @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
40
+ * @param {Function} [options.rrwebRecord] - rrweb's `record` function
41
+ */
42
+ var SessionRecording = function(options) {
43
+ this._mixpanel = options.mixpanelInstance;
44
+ this._onIdleTimeout = options.onIdleTimeout;
45
+ this._onMaxLengthReached = options.onMaxLengthReached;
46
+ this._rrwebRecord = options.rrwebRecord;
47
+
48
+ this.replayId = options.replayId;
49
+
50
+ // internal rrweb stopRecording function
51
+ this._stopRecording = null;
52
+
53
+ this.seqNo = 0;
54
+ this.replayStartTime = null;
55
+ this.batchStartUrl = null;
56
+
57
+ this.idleTimeoutId = null;
58
+ this.maxTimeoutId = null;
59
+
60
+ this.recordMaxMs = MAX_RECORDING_MS;
61
+ this.recordMinMs = 0;
62
+
63
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
64
+ // this will be important when persistence is introduced
65
+ var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
66
+ this.batcher = new RequestBatcher(batcherKey, {
67
+ errorReporter: _.bind(this.reportError, this),
68
+ flushOnlyOnInterval: true,
69
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
70
+ sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
71
+ usePersistence: false
72
+ });
73
+ };
74
+
75
+ SessionRecording.prototype.getConfig = function(configVar) {
76
+ return this._mixpanel.get_config(configVar);
77
+ };
78
+
79
+ // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
80
+ // reaches into this class instance and expects the snake case version of the function.
81
+ // eslint-disable-next-line camelcase
82
+ SessionRecording.prototype.get_config = function(configVar) {
83
+ return this.getConfig(configVar);
84
+ };
85
+
86
+ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
87
+ if (this._stopRecording !== null) {
88
+ logger.log('Recording already in progress, skipping startRecording.');
89
+ return;
90
+ }
91
+
92
+ this.recordMaxMs = this.getConfig('record_max_ms');
93
+ if (this.recordMaxMs > MAX_RECORDING_MS) {
94
+ this.recordMaxMs = MAX_RECORDING_MS;
95
+ logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
96
+ }
97
+
98
+ this.recordMinMs = this.getConfig('record_min_ms');
99
+ if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
100
+ this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
101
+ logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
102
+ }
103
+
104
+ this.replayStartTime = new Date().getTime();
105
+ this.batchStartUrl = _.info.currentUrl();
106
+
107
+ if (shouldStopBatcher || this.recordMinMs > 0) {
108
+ // the primary case for shouldStopBatcher is when we're starting recording after a reset
109
+ // and don't want to send anything over the network until there's
110
+ // actual user activity
111
+ // this also applies if the minimum recording length has not been hit yet
112
+ // so that we don't send data until we know the recording will be long enough
113
+ this.batcher.stop();
114
+ } else {
115
+ this.batcher.start();
116
+ }
117
+
118
+ var resetIdleTimeout = _.bind(function () {
119
+ clearTimeout(this.idleTimeoutId);
120
+ this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
121
+ }, this);
122
+
123
+ var blockSelector = this.getConfig('record_block_selector');
124
+ if (blockSelector === '' || blockSelector === null) {
125
+ blockSelector = undefined;
126
+ }
127
+
128
+ this._stopRecording = this._rrwebRecord({
129
+ 'emit': _.bind(function (ev) {
130
+ this.batcher.enqueue(ev);
131
+ if (isUserEvent(ev)) {
132
+ if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
133
+ // start flushing again after user activity
134
+ this.batcher.start();
135
+ }
136
+ resetIdleTimeout();
137
+ }
138
+ }, this),
139
+ 'blockClass': this.getConfig('record_block_class'),
140
+ 'blockSelector': blockSelector,
141
+ 'collectFonts': this.getConfig('record_collect_fonts'),
142
+ 'maskAllInputs': true,
143
+ 'maskTextClass': this.getConfig('record_mask_text_class'),
144
+ 'maskTextSelector': this.getConfig('record_mask_text_selector')
145
+ });
146
+
147
+ if (typeof this._stopRecording !== 'function') {
148
+ this.reportError('rrweb failed to start, skipping this recording.');
149
+ this._stopRecording = null;
150
+ this.stopRecording(); // stop batcher looping and any timeouts
151
+ return;
152
+ }
153
+
154
+ resetIdleTimeout();
155
+
156
+ this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
157
+ };
158
+
159
+ SessionRecording.prototype.stopRecording = function () {
160
+ if (!this.isRrwebStopped()) {
161
+ try {
162
+ this._stopRecording();
163
+ } catch (err) {
164
+ this.reportError('Error with rrweb stopRecording', err);
165
+ }
166
+ this._stopRecording = null;
167
+ }
168
+
169
+ if (this.batcher.stopped) {
170
+ // never got user activity to flush after reset, so just clear the batcher
171
+ this.batcher.clear();
172
+ } else {
173
+ // flush any remaining events from running batcher
174
+ this.batcher.flush();
175
+ this.batcher.stop();
176
+ }
177
+
178
+ clearTimeout(this.idleTimeoutId);
179
+ clearTimeout(this.maxTimeoutId);
180
+ };
181
+
182
+ SessionRecording.prototype.isRrwebStopped = function () {
183
+ return this._stopRecording === null;
184
+ };
185
+
186
+ /**
187
+ * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
188
+ * we stop recording and dump any queued events if the user has opted out.
189
+ */
190
+ SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
191
+ this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
192
+ };
193
+
194
+ SessionRecording.prototype._onOptOut = function (code) {
195
+ // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
196
+ if (code === 0) {
197
+ this.stopRecording();
198
+ }
199
+ };
200
+
201
+ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
202
+ var onSuccess = _.bind(function (response, responseBody) {
203
+ // Update batch specific props only if the request was successful to guarantee ordering.
204
+ // RequestBatcher will always flush the next batch after the previous one succeeds.
205
+ // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
206
+ if (response.status === 200 && this.replayId === currentReplayId) {
207
+ this.seqNo++;
208
+ this.batchStartUrl = _.info.currentUrl();
209
+ }
210
+ callback({
211
+ status: 0,
212
+ httpStatusCode: response.status,
213
+ responseBody: responseBody,
214
+ retryAfter: response.headers.get('Retry-After')
215
+ });
216
+ }, this);
217
+
218
+ window['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
219
+ 'method': 'POST',
220
+ 'headers': {
221
+ 'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
222
+ 'Content-Type': 'application/octet-stream'
223
+ },
224
+ 'body': reqBody,
225
+ }).then(function (response) {
226
+ response.json().then(function (responseBody) {
227
+ onSuccess(response, responseBody);
228
+ }).catch(function (error) {
229
+ callback({error: error});
230
+ });
231
+ }).catch(function (error) {
232
+ callback({error: error, httpStatusCode: 0});
233
+ });
234
+ };
235
+
236
+ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
237
+ const numEvents = data.length;
238
+
239
+ if (numEvents > 0) {
240
+ var replayId = this.replayId;
241
+ // each rrweb event has a timestamp - leverage those to get time properties
242
+ var batchStartTime = data[0].timestamp;
243
+ if (this.seqNo === 0 || !this.replayStartTime) {
244
+ // extra safety net so that we don't send a null replay start time
245
+ if (this.seqNo !== 0) {
246
+ this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
247
+ }
248
+
249
+ this.replayStartTime = batchStartTime;
250
+ }
251
+ var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
252
+
253
+ var reqParams = {
254
+ '$current_url': this.batchStartUrl,
255
+ '$lib_version': Config.LIB_VERSION,
256
+ 'batch_start_time': batchStartTime / 1000,
257
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
258
+ 'mp_lib': 'web',
259
+ 'replay_id': replayId,
260
+ 'replay_length_ms': replayLengthMs,
261
+ 'replay_start_time': this.replayStartTime / 1000,
262
+ 'seq': this.seqNo
263
+ };
264
+ var eventsJson = _.JSONEncode(data);
265
+
266
+ // send ID management props if they exist
267
+ var deviceId = this._mixpanel.get_property('$device_id');
268
+ if (deviceId) {
269
+ reqParams['$device_id'] = deviceId;
270
+ }
271
+ var userId = this._mixpanel.get_property('$user_id');
272
+ if (userId) {
273
+ reqParams['$user_id'] = userId;
274
+ }
275
+
276
+ if (CompressionStream) {
277
+ var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
278
+ var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
279
+ new Response(gzipStream)
280
+ .blob()
281
+ .then(_.bind(function(compressedBlob) {
282
+ reqParams['format'] = 'gzip';
283
+ this._sendRequest(replayId, reqParams, compressedBlob, callback);
284
+ }, this));
285
+ } else {
286
+ reqParams['format'] = 'body';
287
+ this._sendRequest(replayId, reqParams, eventsJson, callback);
288
+ }
289
+ }
290
+ });
291
+
292
+
293
+ SessionRecording.prototype.reportError = function(msg, err) {
294
+ logger.error.apply(logger.error, arguments);
295
+ try {
296
+ if (!err && !(msg instanceof Error)) {
297
+ msg = new Error(msg);
298
+ }
299
+ this.getConfig('error_reporter')(msg, err);
300
+ } catch(err) {
301
+ logger.error(err);
302
+ }
303
+ };
304
+
305
+ export { SessionRecording };
@@ -22,11 +22,13 @@ var logger = console_with_prefix('batch');
22
22
  var RequestQueue = function(storageKey, options) {
23
23
  options = options || {};
24
24
  this.storageKey = storageKey;
25
- this.storage = options.storage || window.localStorage;
25
+ this.usePersistence = options.usePersistence;
26
+ if (this.usePersistence) {
27
+ this.storage = options.storage || window.localStorage;
28
+ this.lock = new SharedLock(storageKey, {storage: this.storage});
29
+ }
26
30
  this.reportError = options.errorReporter || _.bind(logger.error, logger);
27
- this.lock = new SharedLock(storageKey, {storage: this.storage});
28
31
 
29
- this.usePersistence = options.usePersistence;
30
32
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
31
33
 
32
34
  this.memQueue = [];