mixpanel-browser 2.55.0 → 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.
@@ -148,10 +148,10 @@ var DEFAULT_CONFIG = {
148
148
  'record_block_selector': 'img, video',
149
149
  'record_collect_fonts': false,
150
150
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
151
- 'record_inline_images': false,
152
151
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
153
152
  'record_mask_text_selector': '*',
154
153
  'record_max_ms': MAX_RECORDING_MS,
154
+ 'record_min_ms': 0,
155
155
  'record_sessions_percent': 0,
156
156
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
157
157
  };
@@ -400,15 +400,35 @@ MixpanelLib.prototype.stop_session_recording = function () {
400
400
 
401
401
  MixpanelLib.prototype.get_session_recording_properties = function () {
402
402
  var props = {};
403
- if (this._recorder) {
404
- var replay_id = this._recorder['replayId'];
405
- if (replay_id) {
406
- props['$mp_replay_id'] = replay_id;
407
- }
403
+ var replay_id = this._get_session_replay_id();
404
+ if (replay_id) {
405
+ props['$mp_replay_id'] = replay_id;
408
406
  }
409
407
  return props;
410
408
  };
411
409
 
410
+ MixpanelLib.prototype.get_session_replay_url = function () {
411
+ var replay_url = null;
412
+ var replay_id = this._get_session_replay_id();
413
+ if (replay_id) {
414
+ var query_params = _.HTTPBuildQuery({
415
+ 'replay_id': replay_id,
416
+ 'distinct_id': this.get_distinct_id(),
417
+ 'token': this.get_config('token')
418
+ });
419
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
420
+ }
421
+ return replay_url;
422
+ };
423
+
424
+ MixpanelLib.prototype._get_session_replay_id = function () {
425
+ var replay_id = null;
426
+ if (this._recorder) {
427
+ replay_id = this._recorder['replayId'];
428
+ }
429
+ return replay_id || null;
430
+ };
431
+
412
432
  // Private methods
413
433
 
414
434
  MixpanelLib.prototype._loaded = function() {
@@ -2135,6 +2155,7 @@ MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.protot
2135
2155
  MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
2136
2156
  MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
2137
2157
  MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
2158
+ MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
2138
2159
  MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
2139
2160
 
2140
2161
  // MixpanelPersistence Exports
@@ -1,129 +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, 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._initBatcher();
14
+ this.activeRecording = null;
51
15
  };
52
16
 
53
-
54
- MixpanelRecorder.prototype._initBatcher = function () {
55
- this.batcher = new RequestBatcher('__mprec', {
56
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
57
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
58
- errorReporter: _.bind(this.reportError, this),
59
- flushOnlyOnInterval: true,
60
- usePersistence: false
61
- });
62
- };
63
-
64
- // eslint-disable-next-line camelcase
65
- MixpanelRecorder.prototype.get_config = function(configVar) {
66
- return this._mixpanel.get_config(configVar);
67
- };
68
-
69
- MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
70
- if (this._stopRecording !== null) {
17
+ MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
18
+ if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
71
19
  logger.log('Recording already in progress, skipping startRecording.');
72
20
  return;
73
21
  }
74
22
 
75
- this.recordMaxMs = this.get_config('record_max_ms');
76
- if (this.recordMaxMs > MAX_RECORDING_MS) {
77
- this.recordMaxMs = MAX_RECORDING_MS;
78
- logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
79
- }
80
-
81
- this.recEvents = [];
82
- this.seqNo = 0;
83
- this.replayStartTime = null;
84
-
85
- this.replayId = _.UUID();
86
-
87
- if (shouldStopBatcher) {
88
- // this is the case when we're starting recording after a reset
89
- // and don't want to send anything over the network until there's
90
- // actual user activity
91
- this.batcher.stop();
92
- } else {
93
- this.batcher.start();
94
- }
23
+ var onIdleTimeout = _.bind(function () {
24
+ logger.log('Idle timeout reached, restarting recording.');
25
+ this.resetRecording();
26
+ }, this);
95
27
 
96
- var resetIdleTimeout = _.bind(function () {
97
- clearTimeout(this.idleTimeoutId);
98
- this.idleTimeoutId = setTimeout(_.bind(function () {
99
- logger.log('Idle timeout reached, restarting recording.');
100
- this.resetRecording();
101
- }, this), this.get_config('record_idle_timeout_ms'));
28
+ var onMaxLengthReached = _.bind(function () {
29
+ logger.log('Max recording length reached, stopping recording.');
30
+ this.resetRecording();
102
31
  }, this);
103
32
 
104
- this._stopRecording = record({
105
- 'emit': _.bind(function (ev) {
106
- this.batcher.enqueue(ev);
107
- if (isUserEvent(ev)) {
108
- if (this.batcher.stopped) {
109
- // start flushing again after user activity
110
- this.batcher.start();
111
- }
112
- resetIdleTimeout();
113
- }
114
- }, this),
115
- 'blockClass': this.get_config('record_block_class'),
116
- 'blockSelector': this.get_config('record_block_selector'),
117
- 'collectFonts': this.get_config('record_collect_fonts'),
118
- 'inlineImages': this.get_config('record_inline_images'),
119
- 'maskAllInputs': true,
120
- 'maskTextClass': this.get_config('record_mask_text_class'),
121
- '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
122
39
  });
123
40
 
124
- resetIdleTimeout();
41
+ this.activeRecording.startRecording(shouldStopBatcher);
42
+ };
125
43
 
126
- 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
+ }
127
49
  };
128
50
 
129
51
  MixpanelRecorder.prototype.resetRecording = function () {
@@ -131,135 +53,20 @@ MixpanelRecorder.prototype.resetRecording = function () {
131
53
  this.startRecording(true);
132
54
  };
133
55
 
134
- MixpanelRecorder.prototype.stopRecording = function () {
135
- if (this._stopRecording !== null) {
136
- this._stopRecording();
137
- this._stopRecording = null;
138
- }
139
-
140
- if (this.batcher.stopped) {
141
- // never got user activity to flush after reset, so just clear the batcher
142
- this.batcher.clear();
56
+ MixpanelRecorder.prototype.getActiveReplayId = function () {
57
+ if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
58
+ return this.activeRecording.replayId;
143
59
  } else {
144
- // flush any remaining events from running batcher
145
- this.batcher.flush();
146
- this.batcher.stop();
60
+ return null;
147
61
  }
148
- this.replayId = null;
149
-
150
- clearTimeout(this.idleTimeoutId);
151
- clearTimeout(this.maxTimeoutId);
152
- };
153
-
154
- /**
155
- * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
156
- * we stop recording and dump any queued events if the user has opted out.
157
- */
158
- MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
159
- this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
160
62
  };
161
63
 
162
- MixpanelRecorder.prototype._onOptOut = function (code) {
163
- // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
164
- if (code === 0) {
165
- this.recEvents = [];
166
- this.stopRecording();
167
- }
168
- };
169
-
170
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
171
- var onSuccess = _.bind(function (response, responseBody) {
172
- // Increment sequence counter only if the request was successful to guarantee ordering.
173
- // RequestBatcher will always flush the next batch after the previous one succeeds.
174
- if (response.status === 200) {
175
- this.seqNo++;
176
- }
177
-
178
- callback({
179
- status: 0,
180
- httpStatusCode: response.status,
181
- responseBody: responseBody,
182
- retryAfter: response.headers.get('Retry-After')
183
- });
184
- }, this);
185
-
186
- window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
187
- 'method': 'POST',
188
- 'headers': {
189
- 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
190
- 'Content-Type': 'application/octet-stream'
191
- },
192
- 'body': reqBody,
193
- }).then(function (response) {
194
- response.json().then(function (responseBody) {
195
- onSuccess(response, responseBody);
196
- }).catch(function (error) {
197
- callback({error: error});
198
- });
199
- }).catch(function (error) {
200
- callback({error: error});
201
- });
202
- };
203
-
204
- MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
205
- const numEvents = data.length;
206
-
207
- if (numEvents > 0) {
208
- // each rrweb event has a timestamp - leverage those to get time properties
209
- var batchStartTime = data[0].timestamp;
210
- if (this.seqNo === 0) {
211
- this.replayStartTime = batchStartTime;
212
- }
213
- var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
214
-
215
- var reqParams = {
216
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
217
- 'seq': this.seqNo,
218
- 'batch_start_time': batchStartTime / 1000,
219
- 'replay_id': this.replayId,
220
- 'replay_length_ms': replayLengthMs,
221
- 'replay_start_time': this.replayStartTime / 1000
222
- };
223
- var eventsJson = _.JSONEncode(data);
224
-
225
- // send ID management props if they exist
226
- var deviceId = this._mixpanel.get_property('$device_id');
227
- if (deviceId) {
228
- reqParams['$device_id'] = deviceId;
229
- }
230
- var userId = this._mixpanel.get_property('$user_id');
231
- if (userId) {
232
- reqParams['$user_id'] = userId;
233
- }
234
-
235
- if (CompressionStream) {
236
- var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
237
- var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
238
- new Response(gzipStream)
239
- .blob()
240
- .then(_.bind(function(compressedBlob) {
241
- reqParams['format'] = 'gzip';
242
- this._sendRequest(reqParams, compressedBlob, callback);
243
- }, this));
244
- } else {
245
- reqParams['format'] = 'body';
246
- this._sendRequest(reqParams, eventsJson, callback);
247
- }
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();
248
69
  }
249
70
  });
250
71
 
251
-
252
- MixpanelRecorder.prototype.reportError = function(msg, err) {
253
- logger.error.apply(logger.error, arguments);
254
- try {
255
- if (!err && !(msg instanceof Error)) {
256
- msg = new Error(msg);
257
- }
258
- this.get_config('error_reporter')(msg, err);
259
- } catch(err) {
260
- logger.error(err);
261
- }
262
- };
263
-
264
-
265
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 };