mixpanel-browser 2.55.0 → 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.
@@ -4507,7 +4507,7 @@ var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4507
4507
 
4508
4508
  var Config = {
4509
4509
  DEBUG: false,
4510
- LIB_VERSION: '2.55.0'
4510
+ LIB_VERSION: '2.55.1'
4511
4511
  };
4512
4512
 
4513
4513
  /* eslint camelcase: "off", eqeqeq: "off" */
@@ -4519,7 +4519,7 @@ if (typeof(window) === 'undefined') {
4519
4519
  hostname: ''
4520
4520
  };
4521
4521
  win = {
4522
- navigator: { userAgent: '' },
4522
+ navigator: { userAgent: '', onLine: true },
4523
4523
  document: {
4524
4524
  location: loc,
4525
4525
  referrer: ''
@@ -4533,6 +4533,8 @@ if (typeof(window) === 'undefined') {
4533
4533
 
4534
4534
  // Maximum allowed session recording length
4535
4535
  var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
4536
+ // Maximum allowed value for minimum session recording length
4537
+ var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds
4536
4538
 
4537
4539
  /*
4538
4540
  * Saved references to long variable names, so that closure compiler can
@@ -5473,7 +5475,7 @@ _.HTTPBuildQuery = function(formdata, arg_separator) {
5473
5475
  _.getQueryParam = function(url, param) {
5474
5476
  // Expects a raw URL
5475
5477
 
5476
- param = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
5478
+ param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]');
5477
5479
  var regexS = '[\\?&]' + param + '=([^&#]*)',
5478
5480
  regex = new RegExp(regexS),
5479
5481
  results = regex.exec(url);
@@ -5930,8 +5932,8 @@ _.dom_query = (function() {
5930
5932
  };
5931
5933
  })();
5932
5934
 
5933
- var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
5934
- var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'ttclid', 'twclid', 'wbraid'];
5935
+ 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'];
5936
+ var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid'];
5935
5937
 
5936
5938
  _.info = {
5937
5939
  campaignParams: function(default_value) {
@@ -6213,6 +6215,15 @@ var extract_domain = function(hostname) {
6213
6215
  return matches ? matches[0] : '';
6214
6216
  };
6215
6217
 
6218
+ /**
6219
+ * Check whether we have network connection. default to true for browsers that don't support navigator.onLine (IE)
6220
+ * @returns {boolean}
6221
+ */
6222
+ var isOnline = function() {
6223
+ var onLine = win.navigator['onLine'];
6224
+ return _.isUndefined(onLine) || onLine;
6225
+ };
6226
+
6216
6227
  var JSONStringify = null, JSONParse = null;
6217
6228
  if (typeof JSON !== 'undefined') {
6218
6229
  JSONStringify = JSON.stringify;
@@ -7176,7 +7187,12 @@ RequestBatcher.prototype.flush = function(options) {
7176
7187
  this.flush();
7177
7188
  } else if (
7178
7189
  _.isObject(res) &&
7179
- (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
7190
+ (
7191
+ res.httpStatusCode >= 500
7192
+ || res.httpStatusCode === 429
7193
+ || (res.httpStatusCode <= 0 && !isOnline())
7194
+ || res.error === 'timeout'
7195
+ )
7180
7196
  ) {
7181
7197
  // network or API error, or 429 Too Many Requests, retry
7182
7198
  var retryMS = this.flushInterval * 2;
@@ -7327,6 +7343,7 @@ var MixpanelRecorder = function(mixpanelInstance) {
7327
7343
  this.maxTimeoutId = null;
7328
7344
 
7329
7345
  this.recordMaxMs = MAX_RECORDING_MS;
7346
+ this.recordMinMs = 0;
7330
7347
  this._initBatcher();
7331
7348
  };
7332
7349
 
@@ -7358,16 +7375,24 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
7358
7375
  logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7359
7376
  }
7360
7377
 
7378
+ this.recordMinMs = this.get_config('record_min_ms');
7379
+ if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7380
+ this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7381
+ logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7382
+ }
7383
+
7361
7384
  this.recEvents = [];
7362
7385
  this.seqNo = 0;
7363
- this.replayStartTime = null;
7386
+ this.replayStartTime = new Date().getTime();
7364
7387
 
7365
7388
  this.replayId = _.UUID();
7366
7389
 
7367
- if (shouldStopBatcher) {
7368
- // this is the case when we're starting recording after a reset
7390
+ if (shouldStopBatcher || this.recordMinMs > 0) {
7391
+ // the primary case for shouldStopBatcher is when we're starting recording after a reset
7369
7392
  // and don't want to send anything over the network until there's
7370
7393
  // actual user activity
7394
+ // this also applies if the minimum recording length has not been hit yet
7395
+ // so that we don't send data until we know the recording will be long enough
7371
7396
  this.batcher.stop();
7372
7397
  } else {
7373
7398
  this.batcher.start();
@@ -7381,11 +7406,16 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
7381
7406
  }, this), this.get_config('record_idle_timeout_ms'));
7382
7407
  }, this);
7383
7408
 
7409
+ var blockSelector = this.get_config('record_block_selector');
7410
+ if (blockSelector === '' || blockSelector === null) {
7411
+ blockSelector = undefined;
7412
+ }
7413
+
7384
7414
  this._stopRecording = record({
7385
7415
  'emit': _.bind(function (ev) {
7386
7416
  this.batcher.enqueue(ev);
7387
7417
  if (isUserEvent(ev)) {
7388
- if (this.batcher.stopped) {
7418
+ if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
7389
7419
  // start flushing again after user activity
7390
7420
  this.batcher.start();
7391
7421
  }
@@ -7393,7 +7423,7 @@ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
7393
7423
  }
7394
7424
  }, this),
7395
7425
  'blockClass': this.get_config('record_block_class'),
7396
- 'blockSelector': this.get_config('record_block_selector'),
7426
+ 'blockSelector': blockSelector,
7397
7427
  'collectFonts': this.get_config('record_collect_fonts'),
7398
7428
  'inlineImages': this.get_config('record_inline_images'),
7399
7429
  'maskAllInputs': true,
@@ -7447,14 +7477,14 @@ MixpanelRecorder.prototype._onOptOut = function (code) {
7447
7477
  }
7448
7478
  };
7449
7479
 
7450
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
7480
+ MixpanelRecorder.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7451
7481
  var onSuccess = _.bind(function (response, responseBody) {
7452
7482
  // Increment sequence counter only if the request was successful to guarantee ordering.
7453
7483
  // RequestBatcher will always flush the next batch after the previous one succeeds.
7454
- if (response.status === 200) {
7484
+ // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
7485
+ if (response.status === 200 && this.replayId === currentReplayId) {
7455
7486
  this.seqNo++;
7456
7487
  }
7457
-
7458
7488
  callback({
7459
7489
  status: 0,
7460
7490
  httpStatusCode: response.status,
@@ -7477,7 +7507,7 @@ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback)
7477
7507
  callback({error: error});
7478
7508
  });
7479
7509
  }).catch(function (error) {
7480
- callback({error: error});
7510
+ callback({error: error, httpStatusCode: 0});
7481
7511
  });
7482
7512
  };
7483
7513
 
@@ -7485,9 +7515,15 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
7485
7515
  const numEvents = data.length;
7486
7516
 
7487
7517
  if (numEvents > 0) {
7518
+ var replayId = this.replayId;
7488
7519
  // each rrweb event has a timestamp - leverage those to get time properties
7489
7520
  var batchStartTime = data[0].timestamp;
7490
- if (this.seqNo === 0) {
7521
+ if (this.seqNo === 0 || !this.replayStartTime) {
7522
+ // extra safety net so that we don't send a null replay start time
7523
+ if (this.seqNo !== 0) {
7524
+ this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
7525
+ }
7526
+
7491
7527
  this.replayStartTime = batchStartTime;
7492
7528
  }
7493
7529
  var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
@@ -7496,7 +7532,7 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
7496
7532
  'distinct_id': String(this._mixpanel.get_distinct_id()),
7497
7533
  'seq': this.seqNo,
7498
7534
  'batch_start_time': batchStartTime / 1000,
7499
- 'replay_id': this.replayId,
7535
+ 'replay_id': replayId,
7500
7536
  'replay_length_ms': replayLengthMs,
7501
7537
  'replay_start_time': this.replayStartTime / 1000
7502
7538
  };
@@ -7519,11 +7555,11 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
7519
7555
  .blob()
7520
7556
  .then(_.bind(function(compressedBlob) {
7521
7557
  reqParams['format'] = 'gzip';
7522
- this._sendRequest(reqParams, compressedBlob, callback);
7558
+ this._sendRequest(replayId, reqParams, compressedBlob, callback);
7523
7559
  }, this));
7524
7560
  } else {
7525
7561
  reqParams['format'] = 'body';
7526
- this._sendRequest(reqParams, eventsJson, callback);
7562
+ this._sendRequest(replayId, reqParams, eventsJson, callback);
7527
7563
  }
7528
7564
  }
7529
7565
  });
@@ -9011,6 +9047,7 @@ var DEFAULT_CONFIG = {
9011
9047
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
9012
9048
  'record_mask_text_selector': '*',
9013
9049
  'record_max_ms': MAX_RECORDING_MS,
9050
+ 'record_min_ms': 0,
9014
9051
  'record_sessions_percent': 0,
9015
9052
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
9016
9053
  };
@@ -4513,7 +4513,7 @@
4513
4513
 
4514
4514
  var Config = {
4515
4515
  DEBUG: false,
4516
- LIB_VERSION: '2.55.0'
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,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.55.0",
3
+ "version": "2.55.1",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
6
  "module": "dist/mixpanel.module.js",
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.55.0'
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;