mixpanel-browser 2.54.1 → 2.55.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.
@@ -4513,7 +4513,7 @@
4513
4513
 
4514
4514
  var Config = {
4515
4515
  DEBUG: false,
4516
- LIB_VERSION: '2.54.1'
4516
+ LIB_VERSION: '2.55.1'
4517
4517
  };
4518
4518
 
4519
4519
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -4525,7 +4525,7 @@
4525
4525
  hostname: ''
4526
4526
  };
4527
4527
  win = {
4528
- navigator: { userAgent: '' },
4528
+ navigator: { userAgent: '', onLine: true },
4529
4529
  document: {
4530
4530
  location: loc,
4531
4531
  referrer: ''
@@ -4539,6 +4539,8 @@
4539
4539
 
4540
4540
  // Maximum allowed session recording length
4541
4541
  var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
4542
+ // Maximum allowed value for minimum session recording length
4543
+ var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
4542
4544
 
4543
4545
  /*
4544
4546
  * Saved references to long variable names, so that closure compiler can
@@ -5479,7 +5481,7 @@
5479
5481
  _.getQueryParam = function(url, param) {
5480
5482
  // Expects a raw URL
5481
5483
 
5482
- param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
5484
+ param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
5483
5485
  var regexS = '[\\?&]' + param + '=([^&#]*)',
5484
5486
  regex = new RegExp(regexS),
5485
5487
  results = regex.exec(url);
@@ -5936,8 +5938,8 @@
5936
5938
  };
5937
5939
  })();
5938
5940
 
5939
- var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
5940
- var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
5941
+ var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic'];
5942
+ var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
5941
5943
 
5942
5944
  _.info = {
5943
5945
  campaignParams: function(default_value) {
@@ -6219,6 +6221,15 @@
6219
6221
  return matches ? matches[0] : '';
6220
6222
  };
6221
6223
 
6224
+ /**
6225
+ * Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
6226
+ * @returns {boolean}
6227
+ */
6228
+ var isOnline = function() {
6229
+ var onLine = win.navigator['onLine'];
6230
+ return _.isUndefined(onLine) || onLine;
6231
+ };
6232
+
6222
6233
  var JSONStringify = null, JSONParse = null;
6223
6234
  if (typeof JSON !== 'undefined') {
6224
6235
  JSONStringify = JSON.stringify;
@@ -7182,7 +7193,12 @@
7182
7193
  this.flush();
7183
7194
  } else if (
7184
7195
  _.isObject(res) &&
7185
- (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
7196
+ (
7197
+ res.httpStatusCode >= 500
7198
+ || res.httpStatusCode === 429
7199
+ || (res.httpStatusCode <= 0 && !isOnline())
7200
+ || res.error === 'timeout'
7201
+ )
7186
7202
  ) {
7187
7203
  // network or API error, or 429 Too Many Requests, retry
7188
7204
  var retryMS = this.flushInterval * 2;
@@ -7333,6 +7349,7 @@
7333
7349
  this.maxTimeoutId = null;
7334
7350
 
7335
7351
  this.recordMaxMs = MAX_RECORDING_MS;
7352
+ this.recordMinMs = 0;
7336
7353
  this._initBatcher();
7337
7354
  };
7338
7355
 
@@ -7364,16 +7381,24 @@
7364
7381
  logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7365
7382
  }
7366
7383
 
7384
+ this.recordMinMs = this.get_config('record_min_ms');
7385
+ if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7386
+ this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7387
+ logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7388
+ }
7389
+
7367
7390
  this.recEvents = [];
7368
7391
  this.seqNo = 0;
7369
- this.replayStartTime = null;
7392
+ this.replayStartTime = new Date().getTime();
7370
7393
 
7371
7394
  this.replayId = _.UUID();
7372
7395
 
7373
- if (shouldStopBatcher) {
7374
- // this is the case when we're starting recording after a reset
7396
+ if (shouldStopBatcher || this.recordMinMs > 0) {
7397
+ // the primary case for shouldStopBatcher is when we're starting recording after a reset
7375
7398
  // and don't want to send anything over the network until there's
7376
7399
  // actual user activity
7400
+ // this also applies if the minimum recording length has not been hit yet
7401
+ // so that we don't send data until we know the recording will be long enough
7377
7402
  this.batcher.stop();
7378
7403
  } else {
7379
7404
  this.batcher.start();
@@ -7387,11 +7412,16 @@
7387
7412
  }, this), this.get_config('record_idle_timeout_ms'));
7388
7413
  }, this);
7389
7414
 
7415
+ var blockSelector = this.get_config('record_block_selector');
7416
+ if (blockSelector === '' || blockSelector === null) {
7417
+ blockSelector = undefined;
7418
+ }
7419
+
7390
7420
  this._stopRecording = record({
7391
7421
  'emit': _.bind(function (ev) {
7392
7422
  this.batcher.enqueue(ev);
7393
7423
  if (isUserEvent(ev)) {
7394
- if (this.batcher.stopped) {
7424
+ if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
7395
7425
  // start flushing again after user activity
7396
7426
  this.batcher.start();
7397
7427
  }
@@ -7399,7 +7429,7 @@
7399
7429
  }
7400
7430
  }, this),
7401
7431
  'blockClass': this.get_config('record_block_class'),
7402
- 'blockSelector': this.get_config('record_block_selector'),
7432
+ 'blockSelector': blockSelector,
7403
7433
  'collectFonts': this.get_config('record_collect_fonts'),
7404
7434
  'inlineImages': this.get_config('record_inline_images'),
7405
7435
  'maskAllInputs': true,
@@ -7453,14 +7483,14 @@
7453
7483
  }
7454
7484
  };
7455
7485
 
7456
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
7486
+ MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7457
7487
  var onSuccess = _.bind(function (response, responseBody) {
7458
7488
  // Increment sequence counter only if the request was successful to guarantee ordering.
7459
7489
  // RequestBatcher will always flush the next batch after the previous one succeeds.
7460
- if (response.status === 200) {
7490
+ // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
7491
+ if (response.status === 200 && this.replayId === currentReplayId) {
7461
7492
  this.seqNo++;
7462
7493
  }
7463
-
7464
7494
  callback({
7465
7495
  status: 0,
7466
7496
  httpStatusCode: response.status,
@@ -7483,7 +7513,7 @@
7483
7513
  callback({error: error});
7484
7514
  });
7485
7515
  }).catch(function (error) {
7486
- callback({error: error});
7516
+ callback({error: error, httpStatusCode: 0});
7487
7517
  });
7488
7518
  };
7489
7519
 
@@ -7491,9 +7521,15 @@
7491
7521
  const numEvents = data.length;
7492
7522
 
7493
7523
  if (numEvents > 0) {
7524
+ var replayId = this.replayId;
7494
7525
  // each rrweb event has a timestamp - leverage those to get time properties
7495
7526
  var batchStartTime = data[0].timestamp;
7496
- if (this.seqNo === 0) {
7527
+ if (this.seqNo === 0 || !this.replayStartTime) {
7528
+ // extra safety net so that we don't send a null replay start time
7529
+ if (this.seqNo !== 0) {
7530
+ this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
7531
+ }
7532
+
7497
7533
  this.replayStartTime = batchStartTime;
7498
7534
  }
7499
7535
  var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
@@ -7502,7 +7538,7 @@
7502
7538
  'distinct_id': String(this._mixpanel.get_distinct_id()),
7503
7539
  'seq': this.seqNo,
7504
7540
  'batch_start_time': batchStartTime / 1000,
7505
- 'replay_id': this.replayId,
7541
+ 'replay_id': replayId,
7506
7542
  'replay_length_ms': replayLengthMs,
7507
7543
  'replay_start_time': this.replayStartTime / 1000
7508
7544
  };
@@ -7525,11 +7561,11 @@
7525
7561
  .blob()
7526
7562
  .then(_.bind(function(compressedBlob) {
7527
7563
  reqParams['format'] = 'gzip';
7528
- this._sendRequest(reqParams, compressedBlob, callback);
7564
+ this._sendRequest(replayId, reqParams, compressedBlob, callback);
7529
7565
  }, this));
7530
7566
  } else {
7531
7567
  reqParams['format'] = 'body';
7532
- this._sendRequest(reqParams, eventsJson, callback);
7568
+ this._sendRequest(replayId, reqParams, eventsJson, callback);
7533
7569
  }
7534
7570
  }
7535
7571
  });
@@ -9017,6 +9053,7 @@
9017
9053
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
9018
9054
  'record_mask_text_selector': '*',
9019
9055
  'record_max_ms': MAX_RECORDING_MS,
9056
+ 'record_min_ms': 0,
9020
9057
  'record_sessions_percent': 0,
9021
9058
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
9022
9059
  };
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.54.1",
3
+ "version": "2.55.1",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
+ "module": "dist/mixpanel.module.js",
6
7
  "directories": {
7
8
  "test": "tests"
8
9
  },
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.54.1'
3
+ LIB_VERSION: '2.55.1'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -152,6 +152,7 @@ var DEFAULT_CONFIG = {
152
152
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
153
153
  'record_mask_text_selector': '*',
154
154
  'record_max_ms': MAX_RECORDING_MS,
155
+ 'record_min_ms': 0,
155
156
  'record_sessions_percent': 0,
156
157
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
157
158
  };
@@ -1,7 +1,7 @@
1
1
  import { record } from 'rrweb';
2
2
  import { IncrementalSource, EventType } from '@rrweb/types';
3
3
 
4
- import { MAX_RECORDING_MS, console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase
4
+ import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase
5
5
  import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
6
6
  import { RequestBatcher } from '../request-batcher';
7
7
 
@@ -47,6 +47,7 @@ var MixpanelRecorder = function(mixpanelInstance) {
47
47
  this.maxTimeoutId = null;
48
48
 
49
49
  this.recordMaxMs = MAX_RECORDING_MS;
50
+ this.recordMinMs = 0;
50
51
  this._initBatcher();
51
52
  };
52
53
 
@@ -78,16 +79,24 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
78
79
  logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
79
80
  }
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
+
81
88
  this.recEvents = [];
82
89
  this.seqNo = 0;
83
- this.replayStartTime = null;
90
+ this.replayStartTime = new Date().getTime();
84
91
 
85
92
  this.replayId = _.UUID();
86
93
 
87
- if (shouldStopBatcher) {
88
- // this is the case when we're starting recording after a reset
94
+ if (shouldStopBatcher || this.recordMinMs > 0) {
95
+ // the primary case for shouldStopBatcher is when we're starting recording after a reset
89
96
  // and don't want to send anything over the network until there's
90
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
91
100
  this.batcher.stop();
92
101
  } else {
93
102
  this.batcher.start();
@@ -101,11 +110,16 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
101
110
  }, this), this.get_config('record_idle_timeout_ms'));
102
111
  }, this);
103
112
 
113
+ var blockSelector = this.get_config('record_block_selector');
114
+ if (blockSelector === '' || blockSelector === null) {
115
+ blockSelector = undefined;
116
+ }
117
+
104
118
  this._stopRecording = record({
105
119
  'emit': _.bind(function (ev) {
106
120
  this.batcher.enqueue(ev);
107
121
  if (isUserEvent(ev)) {
108
- if (this.batcher.stopped) {
122
+ if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
109
123
  // start flushing again after user activity
110
124
  this.batcher.start();
111
125
  }
@@ -113,7 +127,7 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
113
127
  }
114
128
  }, this),
115
129
  'blockClass': this.get_config('record_block_class'),
116
- 'blockSelector': this.get_config('record_block_selector'),
130
+ 'blockSelector': blockSelector,
117
131
  'collectFonts': this.get_config('record_collect_fonts'),
118
132
  'inlineImages': this.get_config('record_inline_images'),
119
133
  'maskAllInputs': true,
@@ -167,14 +181,14 @@ MixpanelRecorder.prototype._onOptOut = function (code) {
167
181
  }
168
182
  };
169
183
 
170
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
184
+ MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
171
185
  var onSuccess = _.bind(function (response, responseBody) {
172
186
  // Increment sequence counter only if the request was successful to guarantee ordering.
173
187
  // RequestBatcher will always flush the next batch after the previous one succeeds.
174
- if (response.status === 200) {
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) {
175
190
  this.seqNo++;
176
191
  }
177
-
178
192
  callback({
179
193
  status: 0,
180
194
  httpStatusCode: response.status,
@@ -197,7 +211,7 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback)
197
211
  callback({error: error});
198
212
  });
199
213
  }).catch(function (error) {
200
- callback({error: error});
214
+ callback({error: error, httpStatusCode: 0});
201
215
  });
202
216
  };
203
217
 
@@ -205,9 +219,15 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
205
219
  const numEvents = data.length;
206
220
 
207
221
  if (numEvents > 0) {
222
+ var replayId = this.replayId;
208
223
  // each rrweb event has a timestamp - leverage those to get time properties
209
224
  var batchStartTime = data[0].timestamp;
210
- if (this.seqNo === 0) {
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
+
211
231
  this.replayStartTime = batchStartTime;
212
232
  }
213
233
  var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
@@ -216,7 +236,7 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
216
236
  'distinct_id': String(this._mixpanel.get_distinct_id()),
217
237
  'seq': this.seqNo,
218
238
  'batch_start_time': batchStartTime / 1000,
219
- 'replay_id': this.replayId,
239
+ 'replay_id': replayId,
220
240
  'replay_length_ms': replayLengthMs,
221
241
  'replay_start_time': this.replayStartTime / 1000
222
242
  };
@@ -239,11 +259,11 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
239
259
  .blob()
240
260
  .then(_.bind(function(compressedBlob) {
241
261
  reqParams['format'] = 'gzip';
242
- this._sendRequest(reqParams, compressedBlob, callback);
262
+ this._sendRequest(replayId, reqParams, compressedBlob, callback);
243
263
  }, this));
244
264
  } else {
245
265
  reqParams['format'] = 'body';
246
- this._sendRequest(reqParams, eventsJson, callback);
266
+ this._sendRequest(replayId, reqParams, eventsJson, callback);
247
267
  }
248
268
  }
249
269
  });
@@ -1,6 +1,6 @@
1
1
  import Config from './config';
2
2
  import { RequestQueue } from './request-queue';
3
- import { console_with_prefix, _ } from './utils'; // eslint-disable-line camelcase
3
+ import { console_with_prefix, isOnline, _ } from './utils'; // eslint-disable-line camelcase
4
4
 
5
5
  // maximum interval between request retries after exponential backoff
6
6
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
@@ -198,7 +198,12 @@ RequestBatcher.prototype.flush = function(options) {
198
198
  this.flush();
199
199
  } else if (
200
200
  _.isObject(res) &&
201
- (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
201
+ (
202
+ res.httpStatusCode >= 500
203
+ || res.httpStatusCode === 429
204
+ || (res.httpStatusCode <= 0 && !isOnline())
205
+ || res.error === 'timeout'
206
+ )
202
207
  ) {
203
208
  // network or API error, or 429 Too Many Requests, retry
204
209
  var retryMS = this.flushInterval * 2;
package/src/utils.js CHANGED
@@ -8,7 +8,7 @@ if (typeof(window) === 'undefined') {
8
8
  hostname: ''
9
9
  };
10
10
  win = {
11
- navigator: { userAgent: '' },
11
+ navigator: { userAgent: '', onLine: true },
12
12
  document: {
13
13
  location: loc,
14
14
  referrer: ''
@@ -22,6 +22,8 @@ if (typeof(window) === 'undefined') {
22
22
 
23
23
  // Maximum allowed session recording length
24
24
  var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
25
+ // Maximum allowed value for minimum session recording length
26
+ var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
25
27
 
26
28
  /*
27
29
  * Saved references to long variable names, so that closure compiler can
@@ -962,7 +964,7 @@ _.HTTPBuildQuery = function(formdata, arg_separator) {
962
964
  _.getQueryParam = function(url, param) {
963
965
  // Expects a raw URL
964
966
 
965
- param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
967
+ param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
966
968
  var regexS = '[\\?&]' + param + '=([^&#]*)',
967
969
  regex = new RegExp(regexS),
968
970
  results = regex.exec(url);
@@ -1419,8 +1421,8 @@ _.dom_query = (function() {
1419
1421
  };
1420
1422
  })();
1421
1423
 
1422
- var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
1423
- var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
1424
+ var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic'];
1425
+ var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
1424
1426
 
1425
1427
  _.info = {
1426
1428
  campaignParams: function(default_value) {
@@ -1702,6 +1704,15 @@ var extract_domain = function(hostname) {
1702
1704
  return matches ? matches[0] : '';
1703
1705
  };
1704
1706
 
1707
+ /**
1708
+ * Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
1709
+ * @returns {boolean}
1710
+ */
1711
+ var isOnline = function() {
1712
+ var onLine = win.navigator['onLine'];
1713
+ return _.isUndefined(onLine) || onLine;
1714
+ };
1715
+
1705
1716
  var JSONStringify = null, JSONParse = null;
1706
1717
  if (typeof JSON !== 'undefined') {
1707
1718
  JSONStringify = JSON.stringify;
@@ -1724,18 +1735,20 @@ _['info']['browserVersion'] = _.info.browserVersion;
1724
1735
  _['info']['properties'] = _.info.properties;
1725
1736
 
1726
1737
  export {
1727
- MAX_RECORDING_MS,
1728
1738
  _,
1729
- userAgent,
1730
- console,
1731
- win as window,
1732
- document,
1733
- navigator,
1734
1739
  cheap_guid,
1735
1740
  console_with_prefix,
1741
+ console,
1742
+ document,
1736
1743
  extract_domain,
1737
- localStorageSupported,
1738
- JSONStringify,
1739
1744
  JSONParse,
1740
- slice
1745
+ JSONStringify,
1746
+ isOnline,
1747
+ localStorageSupported,
1748
+ MAX_RECORDING_MS,
1749
+ MAX_VALUE_FOR_MIN_RECORDING_MS,
1750
+ navigator,
1751
+ slice,
1752
+ userAgent,
1753
+ win as window
1741
1754
  };