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.
@@ -4513,7 +4513,7 @@
4513
4513
 
4514
4514
  var Config = {
4515
4515
  DEBUG: false,
4516
- LIB_VERSION: '2.55.0'
4516
+ LIB_VERSION: '2.56.0'
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;
@@ -6539,7 +6550,7 @@
6539
6550
  };
6540
6551
  }
6541
6552
 
6542
- var logger$3 = console_with_prefix('lock');
6553
+ var logger$4 = console_with_prefix('lock');
6543
6554
 
6544
6555
  /**
6545
6556
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -6596,7 +6607,7 @@
6596
6607
 
6597
6608
  var delay = function(cb) {
6598
6609
  if (new Date().getTime() - startTime > timeoutMS) {
6599
- logger$3.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
6610
+ logger$4.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
6600
6611
  storage.removeItem(keyZ);
6601
6612
  storage.removeItem(keyY);
6602
6613
  loop();
@@ -6685,7 +6696,7 @@
6685
6696
  }
6686
6697
  };
6687
6698
 
6688
- var logger$2 = console_with_prefix('batch');
6699
+ var logger$3 = console_with_prefix('batch');
6689
6700
 
6690
6701
  /**
6691
6702
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -6706,11 +6717,13 @@
6706
6717
  var RequestQueue = function(storageKey, options) {
6707
6718
  options = options || {};
6708
6719
  this.storageKey = storageKey;
6709
- this.storage = options.storage || window.localStorage;
6710
- this.reportError = options.errorReporter || _.bind(logger$2.error, logger$2);
6711
- this.lock = new SharedLock(storageKey, {storage: this.storage});
6712
-
6713
6720
  this.usePersistence = options.usePersistence;
6721
+ if (this.usePersistence) {
6722
+ this.storage = options.storage || window.localStorage;
6723
+ this.lock = new SharedLock(storageKey, {storage: this.storage});
6724
+ }
6725
+ this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
6726
+
6714
6727
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
6715
6728
 
6716
6729
  this.memQueue = [];
@@ -6989,7 +7002,7 @@
6989
7002
  // maximum interval between request retries after exponential backoff
6990
7003
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
6991
7004
 
6992
- var logger$1 = console_with_prefix('batch');
7005
+ var logger$2 = console_with_prefix('batch');
6993
7006
 
6994
7007
  /**
6995
7008
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -7103,7 +7116,7 @@
7103
7116
  try {
7104
7117
 
7105
7118
  if (this.requestInProgress) {
7106
- logger$1.log('Flush: Request already in progress');
7119
+ logger$2.log('Flush: Request already in progress');
7107
7120
  return;
7108
7121
  }
7109
7122
 
@@ -7182,7 +7195,12 @@
7182
7195
  this.flush();
7183
7196
  } else if (
7184
7197
  _.isObject(res) &&
7185
- (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
7198
+ (
7199
+ res.httpStatusCode >= 500
7200
+ || res.httpStatusCode === 429
7201
+ || (res.httpStatusCode <= 0 && !isOnline())
7202
+ || res.error === 'timeout'
7203
+ )
7186
7204
  ) {
7187
7205
  // network or API error, or 429 Too Many Requests, retry
7188
7206
  var retryMS = this.flushInterval * 2;
@@ -7266,7 +7284,7 @@
7266
7284
  if (options.unloading) {
7267
7285
  requestOptions.transport = 'sendBeacon';
7268
7286
  }
7269
- logger$1.log('MIXPANEL REQUEST:', dataForRequest);
7287
+ logger$2.log('MIXPANEL REQUEST:', dataForRequest);
7270
7288
  this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
7271
7289
  } catch(err) {
7272
7290
  this.reportError('Error flushing request queue', err);
@@ -7278,7 +7296,7 @@
7278
7296
  * Log error to global logger and optional user-defined logger.
7279
7297
  */
7280
7298
  RequestBatcher.prototype.reportError = function(msg, err) {
7281
- logger$1.error.apply(logger$1.error, arguments);
7299
+ logger$2.error.apply(logger$2.error, arguments);
7282
7300
  if (this.errorReporter) {
7283
7301
  try {
7284
7302
  if (!(err instanceof Error)) {
@@ -7286,12 +7304,12 @@
7286
7304
  }
7287
7305
  this.errorReporter(msg, err);
7288
7306
  } catch(err) {
7289
- logger$1.error(err);
7307
+ logger$2.error(err);
7290
7308
  }
7291
7309
  }
7292
7310
  };
7293
7311
 
7294
- var logger = console_with_prefix('recorder');
7312
+ var logger$1 = console_with_prefix('recorder');
7295
7313
  var CompressionStream = win['CompressionStream'];
7296
7314
 
7297
7315
  var RECORDER_BATCHER_LIB_CONFIG = {
@@ -7317,63 +7335,85 @@
7317
7335
  return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
7318
7336
  }
7319
7337
 
7320
- var MixpanelRecorder = function(mixpanelInstance) {
7321
- this._mixpanel = mixpanelInstance;
7338
+ /**
7339
+ * This class encapsulates a single session recording and its lifecycle.
7340
+ * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
7341
+ * @param {String} [options.replayId] - unique uuid for a single replay
7342
+ * @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
7343
+ * @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
7344
+ * @param {Function} [options.rrwebRecord] - rrweb's `record` function
7345
+ */
7346
+ var SessionRecording = function(options) {
7347
+ this._mixpanel = options.mixpanelInstance;
7348
+ this._onIdleTimeout = options.onIdleTimeout;
7349
+ this._onMaxLengthReached = options.onMaxLengthReached;
7350
+ this._rrwebRecord = options.rrwebRecord;
7351
+
7352
+ this.replayId = options.replayId;
7322
7353
 
7323
7354
  // internal rrweb stopRecording function
7324
7355
  this._stopRecording = null;
7325
7356
 
7326
- this.recEvents = [];
7327
7357
  this.seqNo = 0;
7328
- this.replayId = null;
7329
7358
  this.replayStartTime = null;
7330
- this.sendBatchId = null;
7359
+ this.batchStartUrl = null;
7331
7360
 
7332
7361
  this.idleTimeoutId = null;
7333
7362
  this.maxTimeoutId = null;
7334
7363
 
7335
7364
  this.recordMaxMs = MAX_RECORDING_MS;
7336
- this._initBatcher();
7337
- };
7338
-
7365
+ this.recordMinMs = 0;
7339
7366
 
7340
- MixpanelRecorder.prototype._initBatcher = function () {
7341
- this.batcher = new RequestBatcher('__mprec', {
7342
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
7343
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
7367
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
7368
+ // this will be important when persistence is introduced
7369
+ var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
7370
+ this.batcher = new RequestBatcher(batcherKey, {
7344
7371
  errorReporter: _.bind(this.reportError, this),
7345
7372
  flushOnlyOnInterval: true,
7373
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
7374
+ sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
7346
7375
  usePersistence: false
7347
7376
  });
7348
7377
  };
7349
7378
 
7350
- // eslint-disable-next-line camelcase
7351
- MixpanelRecorder.prototype.get_config = function(configVar) {
7379
+ SessionRecording.prototype.getConfig = function(configVar) {
7352
7380
  return this._mixpanel.get_config(configVar);
7353
7381
  };
7354
7382
 
7355
- MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
7383
+ // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
7384
+ // reaches into this class instance and expects the snake case version of the function.
7385
+ // eslint-disable-next-line camelcase
7386
+ SessionRecording.prototype.get_config = function(configVar) {
7387
+ return this.getConfig(configVar);
7388
+ };
7389
+
7390
+ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7356
7391
  if (this._stopRecording !== null) {
7357
- logger.log('Recording already in progress, skipping startRecording.');
7392
+ logger$1.log('Recording already in progress, skipping startRecording.');
7358
7393
  return;
7359
7394
  }
7360
7395
 
7361
- this.recordMaxMs = this.get_config('record_max_ms');
7396
+ this.recordMaxMs = this.getConfig('record_max_ms');
7362
7397
  if (this.recordMaxMs > MAX_RECORDING_MS) {
7363
7398
  this.recordMaxMs = MAX_RECORDING_MS;
7364
- logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7399
+ logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7365
7400
  }
7366
7401
 
7367
- this.recEvents = [];
7368
- this.seqNo = 0;
7369
- this.replayStartTime = null;
7402
+ this.recordMinMs = this.getConfig('record_min_ms');
7403
+ if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7404
+ this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7405
+ logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7406
+ }
7370
7407
 
7371
- this.replayId = _.UUID();
7408
+ this.replayStartTime = new Date().getTime();
7409
+ this.batchStartUrl = _.info.currentUrl();
7372
7410
 
7373
- if (shouldStopBatcher) {
7374
- // this is the case when we're starting recording after a reset
7411
+ if (shouldStopBatcher || this.recordMinMs > 0) {
7412
+ // the primary case for shouldStopBatcher is when we're starting recording after a reset
7375
7413
  // and don't want to send anything over the network until there's
7376
7414
  // actual user activity
7415
+ // this also applies if the minimum recording length has not been hit yet
7416
+ // so that we don't send data until we know the recording will be long enough
7377
7417
  this.batcher.stop();
7378
7418
  } else {
7379
7419
  this.batcher.start();
@@ -7381,45 +7421,52 @@
7381
7421
 
7382
7422
  var resetIdleTimeout = _.bind(function () {
7383
7423
  clearTimeout(this.idleTimeoutId);
7384
- this.idleTimeoutId = setTimeout(_.bind(function () {
7385
- logger.log('Idle timeout reached, restarting recording.');
7386
- this.resetRecording();
7387
- }, this), this.get_config('record_idle_timeout_ms'));
7424
+ this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
7388
7425
  }, this);
7389
7426
 
7390
- this._stopRecording = record({
7427
+ var blockSelector = this.getConfig('record_block_selector');
7428
+ if (blockSelector === '' || blockSelector === null) {
7429
+ blockSelector = undefined;
7430
+ }
7431
+
7432
+ this._stopRecording = this._rrwebRecord({
7391
7433
  'emit': _.bind(function (ev) {
7392
7434
  this.batcher.enqueue(ev);
7393
7435
  if (isUserEvent(ev)) {
7394
- if (this.batcher.stopped) {
7436
+ if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
7395
7437
  // start flushing again after user activity
7396
7438
  this.batcher.start();
7397
7439
  }
7398
7440
  resetIdleTimeout();
7399
7441
  }
7400
7442
  }, this),
7401
- 'blockClass': this.get_config('record_block_class'),
7402
- 'blockSelector': this.get_config('record_block_selector'),
7403
- 'collectFonts': this.get_config('record_collect_fonts'),
7404
- 'inlineImages': this.get_config('record_inline_images'),
7443
+ 'blockClass': this.getConfig('record_block_class'),
7444
+ 'blockSelector': blockSelector,
7445
+ 'collectFonts': this.getConfig('record_collect_fonts'),
7405
7446
  'maskAllInputs': true,
7406
- 'maskTextClass': this.get_config('record_mask_text_class'),
7407
- 'maskTextSelector': this.get_config('record_mask_text_selector')
7447
+ 'maskTextClass': this.getConfig('record_mask_text_class'),
7448
+ 'maskTextSelector': this.getConfig('record_mask_text_selector')
7408
7449
  });
7409
7450
 
7410
- resetIdleTimeout();
7451
+ if (typeof this._stopRecording !== 'function') {
7452
+ this.reportError('rrweb failed to start, skipping this recording.');
7453
+ this._stopRecording = null;
7454
+ this.stopRecording(); // stop batcher looping and any timeouts
7455
+ return;
7456
+ }
7411
7457
 
7412
- this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
7413
- };
7458
+ resetIdleTimeout();
7414
7459
 
7415
- MixpanelRecorder.prototype.resetRecording = function () {
7416
- this.stopRecording();
7417
- this.startRecording(true);
7460
+ this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
7418
7461
  };
7419
7462
 
7420
- MixpanelRecorder.prototype.stopRecording = function () {
7421
- if (this._stopRecording !== null) {
7422
- this._stopRecording();
7463
+ SessionRecording.prototype.stopRecording = function () {
7464
+ if (!this.isRrwebStopped()) {
7465
+ try {
7466
+ this._stopRecording();
7467
+ } catch (err) {
7468
+ this.reportError('Error with rrweb stopRecording', err);
7469
+ }
7423
7470
  this._stopRecording = null;
7424
7471
  }
7425
7472
 
@@ -7431,36 +7478,39 @@
7431
7478
  this.batcher.flush();
7432
7479
  this.batcher.stop();
7433
7480
  }
7434
- this.replayId = null;
7435
7481
 
7436
7482
  clearTimeout(this.idleTimeoutId);
7437
7483
  clearTimeout(this.maxTimeoutId);
7438
7484
  };
7439
7485
 
7486
+ SessionRecording.prototype.isRrwebStopped = function () {
7487
+ return this._stopRecording === null;
7488
+ };
7489
+
7440
7490
  /**
7441
7491
  * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
7442
7492
  * we stop recording and dump any queued events if the user has opted out.
7443
7493
  */
7444
- MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
7494
+ SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
7445
7495
  this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
7446
7496
  };
7447
7497
 
7448
- MixpanelRecorder.prototype._onOptOut = function (code) {
7498
+ SessionRecording.prototype._onOptOut = function (code) {
7449
7499
  // addOptOutCheckMixpanelLib invokes this function with code=0 when the user has opted out
7450
7500
  if (code === 0) {
7451
- this.recEvents = [];
7452
7501
  this.stopRecording();
7453
7502
  }
7454
7503
  };
7455
7504
 
7456
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
7505
+ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7457
7506
  var onSuccess = _.bind(function (response, responseBody) {
7458
- // Increment sequence counter only if the request was successful to guarantee ordering.
7507
+ // Update batch specific props only if the request was successful to guarantee ordering.
7459
7508
  // RequestBatcher will always flush the next batch after the previous one succeeds.
7460
- if (response.status === 200) {
7509
+ // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
7510
+ if (response.status === 200 && this.replayId === currentReplayId) {
7461
7511
  this.seqNo++;
7512
+ this.batchStartUrl = _.info.currentUrl();
7462
7513
  }
7463
-
7464
7514
  callback({
7465
7515
  status: 0,
7466
7516
  httpStatusCode: response.status,
@@ -7469,10 +7519,10 @@
7469
7519
  });
7470
7520
  }, this);
7471
7521
 
7472
- win['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
7522
+ win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
7473
7523
  'method': 'POST',
7474
7524
  'headers': {
7475
- 'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
7525
+ 'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
7476
7526
  'Content-Type': 'application/octet-stream'
7477
7527
  },
7478
7528
  'body': reqBody,
@@ -7483,28 +7533,37 @@
7483
7533
  callback({error: error});
7484
7534
  });
7485
7535
  }).catch(function (error) {
7486
- callback({error: error});
7536
+ callback({error: error, httpStatusCode: 0});
7487
7537
  });
7488
7538
  };
7489
7539
 
7490
- MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
7540
+ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
7491
7541
  const numEvents = data.length;
7492
7542
 
7493
7543
  if (numEvents > 0) {
7544
+ var replayId = this.replayId;
7494
7545
  // each rrweb event has a timestamp - leverage those to get time properties
7495
7546
  var batchStartTime = data[0].timestamp;
7496
- if (this.seqNo === 0) {
7547
+ if (this.seqNo === 0 || !this.replayStartTime) {
7548
+ // extra safety net so that we don't send a null replay start time
7549
+ if (this.seqNo !== 0) {
7550
+ this.reportError('Replay start time not set but seqNo is not 0. Using current batch start time as a fallback.');
7551
+ }
7552
+
7497
7553
  this.replayStartTime = batchStartTime;
7498
7554
  }
7499
7555
  var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
7500
7556
 
7501
7557
  var reqParams = {
7502
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
7503
- 'seq': this.seqNo,
7558
+ '$current_url': this.batchStartUrl,
7559
+ '$lib_version': Config.LIB_VERSION,
7504
7560
  'batch_start_time': batchStartTime / 1000,
7505
- 'replay_id': this.replayId,
7561
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
7562
+ 'mp_lib': 'web',
7563
+ 'replay_id': replayId,
7506
7564
  'replay_length_ms': replayLengthMs,
7507
- 'replay_start_time': this.replayStartTime / 1000
7565
+ 'replay_start_time': this.replayStartTime / 1000,
7566
+ 'seq': this.seqNo
7508
7567
  };
7509
7568
  var eventsJson = _.JSONEncode(data);
7510
7569
 
@@ -7525,28 +7584,93 @@
7525
7584
  .blob()
7526
7585
  .then(_.bind(function(compressedBlob) {
7527
7586
  reqParams['format'] = 'gzip';
7528
- this._sendRequest(reqParams, compressedBlob, callback);
7587
+ this._sendRequest(replayId, reqParams, compressedBlob, callback);
7529
7588
  }, this));
7530
7589
  } else {
7531
7590
  reqParams['format'] = 'body';
7532
- this._sendRequest(reqParams, eventsJson, callback);
7591
+ this._sendRequest(replayId, reqParams, eventsJson, callback);
7533
7592
  }
7534
7593
  }
7535
7594
  });
7536
7595
 
7537
7596
 
7538
- MixpanelRecorder.prototype.reportError = function(msg, err) {
7539
- logger.error.apply(logger.error, arguments);
7597
+ SessionRecording.prototype.reportError = function(msg, err) {
7598
+ logger$1.error.apply(logger$1.error, arguments);
7540
7599
  try {
7541
7600
  if (!err && !(msg instanceof Error)) {
7542
7601
  msg = new Error(msg);
7543
7602
  }
7544
- this.get_config('error_reporter')(msg, err);
7603
+ this.getConfig('error_reporter')(msg, err);
7545
7604
  } catch(err) {
7546
- logger.error(err);
7605
+ logger$1.error(err);
7547
7606
  }
7548
7607
  };
7549
7608
 
7609
+ var logger = console_with_prefix('recorder');
7610
+
7611
+ /**
7612
+ * Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
7613
+ * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
7614
+ */
7615
+ var MixpanelRecorder = function(mixpanelInstance) {
7616
+ this._mixpanel = mixpanelInstance;
7617
+ this.activeRecording = null;
7618
+ };
7619
+
7620
+ MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
7621
+ if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
7622
+ logger.log('Recording already in progress, skipping startRecording.');
7623
+ return;
7624
+ }
7625
+
7626
+ var onIdleTimeout = _.bind(function () {
7627
+ logger.log('Idle timeout reached, restarting recording.');
7628
+ this.resetRecording();
7629
+ }, this);
7630
+
7631
+ var onMaxLengthReached = _.bind(function () {
7632
+ logger.log('Max recording length reached, stopping recording.');
7633
+ this.resetRecording();
7634
+ }, this);
7635
+
7636
+ this.activeRecording = new SessionRecording({
7637
+ mixpanelInstance: this._mixpanel,
7638
+ onIdleTimeout: onIdleTimeout,
7639
+ onMaxLengthReached: onMaxLengthReached,
7640
+ replayId: _.UUID(),
7641
+ rrwebRecord: record
7642
+ });
7643
+
7644
+ this.activeRecording.startRecording(shouldStopBatcher);
7645
+ };
7646
+
7647
+ MixpanelRecorder.prototype.stopRecording = function() {
7648
+ if (this.activeRecording) {
7649
+ this.activeRecording.stopRecording();
7650
+ this.activeRecording = null;
7651
+ }
7652
+ };
7653
+
7654
+ MixpanelRecorder.prototype.resetRecording = function () {
7655
+ this.stopRecording();
7656
+ this.startRecording(true);
7657
+ };
7658
+
7659
+ MixpanelRecorder.prototype.getActiveReplayId = function () {
7660
+ if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
7661
+ return this.activeRecording.replayId;
7662
+ } else {
7663
+ return null;
7664
+ }
7665
+ };
7666
+
7667
+ // getter so that older mixpanel-core versions can still retrieve the replay ID
7668
+ // when pulling the latest recorder bundle from the CDN
7669
+ Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
7670
+ get: function () {
7671
+ return this.getActiveReplayId();
7672
+ }
7673
+ });
7550
7674
 
7551
7675
  win['__mp_recorder'] = MixpanelRecorder;
7552
7676
 
@@ -9013,10 +9137,10 @@
9013
9137
  'record_block_selector': 'img, video',
9014
9138
  'record_collect_fonts': false,
9015
9139
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
9016
- 'record_inline_images': false,
9017
9140
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
9018
9141
  'record_mask_text_selector': '*',
9019
9142
  'record_max_ms': MAX_RECORDING_MS,
9143
+ 'record_min_ms': 0,
9020
9144
  'record_sessions_percent': 0,
9021
9145
  'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js'
9022
9146
  };
@@ -9265,15 +9389,35 @@
9265
9389
 
9266
9390
  MixpanelLib.prototype.get_session_recording_properties = function () {
9267
9391
  var props = {};
9268
- if (this._recorder) {
9269
- var replay_id = this._recorder['replayId'];
9270
- if (replay_id) {
9271
- props['$mp_replay_id'] = replay_id;
9272
- }
9392
+ var replay_id = this._get_session_replay_id();
9393
+ if (replay_id) {
9394
+ props['$mp_replay_id'] = replay_id;
9273
9395
  }
9274
9396
  return props;
9275
9397
  };
9276
9398
 
9399
+ MixpanelLib.prototype.get_session_replay_url = function () {
9400
+ var replay_url = null;
9401
+ var replay_id = this._get_session_replay_id();
9402
+ if (replay_id) {
9403
+ var query_params = _.HTTPBuildQuery({
9404
+ 'replay_id': replay_id,
9405
+ 'distinct_id': this.get_distinct_id(),
9406
+ 'token': this.get_config('token')
9407
+ });
9408
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
9409
+ }
9410
+ return replay_url;
9411
+ };
9412
+
9413
+ MixpanelLib.prototype._get_session_replay_id = function () {
9414
+ var replay_id = null;
9415
+ if (this._recorder) {
9416
+ replay_id = this._recorder['replayId'];
9417
+ }
9418
+ return replay_id || null;
9419
+ };
9420
+
9277
9421
  // Private methods
9278
9422
 
9279
9423
  MixpanelLib.prototype._loaded = function() {
@@ -11000,6 +11144,7 @@
11000
11144
  MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
11001
11145
  MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
11002
11146
  MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
11147
+ MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
11003
11148
  MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
11004
11149
 
11005
11150
  // MixpanelPersistence Exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.55.0",
3
+ "version": "2.56.0",
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.56.0'
4
4
  };
5
5
 
6
6
  export default Config;