mixpanel-browser 2.59.0 → 2.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,27 @@
1
1
  'use strict';
2
2
 
3
+ // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
4
+ var win;
5
+ if (typeof(window) === 'undefined') {
6
+ var loc = {
7
+ hostname: ''
8
+ };
9
+ win = {
10
+ navigator: { userAgent: '', onLine: true },
11
+ document: {
12
+ createElement: function() { return {}; },
13
+ location: loc,
14
+ referrer: ''
15
+ },
16
+ screen: { width: 0, height: 0 },
17
+ location: loc,
18
+ addEventListener: function() {},
19
+ removeEventListener: function() {}
20
+ };
21
+ } else {
22
+ win = window;
23
+ }
24
+
3
25
  var NodeType;
4
26
  (function (NodeType) {
5
27
  NodeType[NodeType["Document"] = 0] = "Document";
@@ -4476,64 +4498,6 @@ record.takeFullSnapshot = (isCheckout) => {
4476
4498
  };
4477
4499
  record.mirror = mirror;
4478
4500
 
4479
- var EventType = /* @__PURE__ */ ((EventType2) => {
4480
- EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
4481
- EventType2[EventType2["Load"] = 1] = "Load";
4482
- EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
4483
- EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
4484
- EventType2[EventType2["Meta"] = 4] = "Meta";
4485
- EventType2[EventType2["Custom"] = 5] = "Custom";
4486
- EventType2[EventType2["Plugin"] = 6] = "Plugin";
4487
- return EventType2;
4488
- })(EventType || {});
4489
- var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4490
- IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
4491
- IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
4492
- IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
4493
- IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
4494
- IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
4495
- IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
4496
- IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
4497
- IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
4498
- IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
4499
- IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
4500
- IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
4501
- IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
4502
- IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
4503
- IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
4504
- IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
4505
- IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
4506
- IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
4507
- return IncrementalSource2;
4508
- })(IncrementalSource || {});
4509
-
4510
- var Config = {
4511
- DEBUG: false,
4512
- LIB_VERSION: '2.59.0'
4513
- };
4514
-
4515
- // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
4516
- var win;
4517
- if (typeof(window) === 'undefined') {
4518
- var loc = {
4519
- hostname: ''
4520
- };
4521
- win = {
4522
- navigator: { userAgent: '', onLine: true },
4523
- document: {
4524
- createElement: function() { return {}; },
4525
- location: loc,
4526
- referrer: ''
4527
- },
4528
- screen: { width: 0, height: 0 },
4529
- location: loc,
4530
- addEventListener: function() {},
4531
- removeEventListener: function() {}
4532
- };
4533
- } else {
4534
- win = window;
4535
- }
4536
-
4537
4501
  var setImmediate = win['setImmediate'];
4538
4502
  var builtInProp, cycle, schedulingQueue,
4539
4503
  ToString = Object.prototype.toString,
@@ -4896,6 +4860,42 @@ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]'
4896
4860
  PromisePolyfill = NpoPromise;
4897
4861
  }
4898
4862
 
4863
+ var EventType = /* @__PURE__ */ ((EventType2) => {
4864
+ EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
4865
+ EventType2[EventType2["Load"] = 1] = "Load";
4866
+ EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
4867
+ EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
4868
+ EventType2[EventType2["Meta"] = 4] = "Meta";
4869
+ EventType2[EventType2["Custom"] = 5] = "Custom";
4870
+ EventType2[EventType2["Plugin"] = 6] = "Plugin";
4871
+ return EventType2;
4872
+ })(EventType || {});
4873
+ var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4874
+ IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
4875
+ IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
4876
+ IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
4877
+ IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
4878
+ IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
4879
+ IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
4880
+ IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
4881
+ IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
4882
+ IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
4883
+ IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
4884
+ IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
4885
+ IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
4886
+ IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
4887
+ IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
4888
+ IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
4889
+ IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
4890
+ IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
4891
+ return IncrementalSource2;
4892
+ })(IncrementalSource || {});
4893
+
4894
+ var Config = {
4895
+ DEBUG: false,
4896
+ LIB_VERSION: '2.61.0'
4897
+ };
4898
+
4899
4899
  /* eslint camelcase: "off", eqeqeq: "off" */
4900
4900
 
4901
4901
  // Maximum allowed session recording length
@@ -5975,15 +5975,9 @@ _.cookie = {
5975
5975
  }
5976
5976
  };
5977
5977
 
5978
- var _localStorageSupported = null;
5979
- var localStorageSupported = function(storage, forceCheck) {
5980
- if (_localStorageSupported !== null && !forceCheck) {
5981
- return _localStorageSupported;
5982
- }
5983
-
5978
+ var _testStorageSupported = function (storage) {
5984
5979
  var supported = true;
5985
5980
  try {
5986
- storage = storage || win.localStorage;
5987
5981
  var key = '__mplss_' + cheap_guid(8),
5988
5982
  val = 'xyz';
5989
5983
  storage.setItem(key, val);
@@ -5994,59 +5988,74 @@ var localStorageSupported = function(storage, forceCheck) {
5994
5988
  } catch (err) {
5995
5989
  supported = false;
5996
5990
  }
5997
-
5998
- _localStorageSupported = supported;
5999
5991
  return supported;
6000
5992
  };
6001
5993
 
6002
- // _.localStorage
6003
- _.localStorage = {
6004
- is_supported: function(force_check) {
6005
- var supported = localStorageSupported(null, force_check);
6006
- if (!supported) {
6007
- console$1.error('localStorage unsupported; falling back to cookie store');
6008
- }
6009
- return supported;
6010
- },
6011
-
6012
- error: function(msg) {
6013
- console$1.error('localStorage error: ' + msg);
6014
- },
5994
+ var _localStorageSupported = null;
5995
+ var localStorageSupported = function(storage, forceCheck) {
5996
+ if (_localStorageSupported !== null && !forceCheck) {
5997
+ return _localStorageSupported;
5998
+ }
5999
+ return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
6000
+ };
6015
6001
 
6016
- get: function(name) {
6017
- try {
6018
- return win.localStorage.getItem(name);
6019
- } catch (err) {
6020
- _.localStorage.error(err);
6021
- }
6022
- return null;
6023
- },
6002
+ var _sessionStorageSupported = null;
6003
+ var sessionStorageSupported = function(storage, forceCheck) {
6004
+ if (_sessionStorageSupported !== null && !forceCheck) {
6005
+ return _sessionStorageSupported;
6006
+ }
6007
+ return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
6008
+ };
6024
6009
 
6025
- parse: function(name) {
6026
- try {
6027
- return _.JSONDecode(_.localStorage.get(name)) || {};
6028
- } catch (err) {
6029
- // noop
6030
- }
6031
- return null;
6032
- },
6010
+ function _storageWrapper(storage, name, is_supported_fn) {
6011
+ var log_error = function(msg) {
6012
+ console$1.error(name + ' error: ' + msg);
6013
+ };
6033
6014
 
6034
- set: function(name, value) {
6035
- try {
6036
- win.localStorage.setItem(name, value);
6037
- } catch (err) {
6038
- _.localStorage.error(err);
6015
+ return {
6016
+ is_supported: function(forceCheck) {
6017
+ var supported = is_supported_fn(storage, forceCheck);
6018
+ if (!supported) {
6019
+ console$1.error(name + ' unsupported');
6020
+ }
6021
+ return supported;
6022
+ },
6023
+ error: log_error,
6024
+ get: function(key) {
6025
+ try {
6026
+ return storage.getItem(key);
6027
+ } catch (err) {
6028
+ log_error(err);
6029
+ }
6030
+ return null;
6031
+ },
6032
+ parse: function(key) {
6033
+ try {
6034
+ return _.JSONDecode(storage.getItem(key)) || {};
6035
+ } catch (err) {
6036
+ // noop
6037
+ }
6038
+ return null;
6039
+ },
6040
+ set: function(key, value) {
6041
+ try {
6042
+ storage.setItem(key, value);
6043
+ } catch (err) {
6044
+ log_error(err);
6045
+ }
6046
+ },
6047
+ remove: function(key) {
6048
+ try {
6049
+ storage.removeItem(key);
6050
+ } catch (err) {
6051
+ log_error(err);
6052
+ }
6039
6053
  }
6040
- },
6054
+ };
6055
+ }
6041
6056
 
6042
- remove: function(name) {
6043
- try {
6044
- win.localStorage.removeItem(name);
6045
- } catch (err) {
6046
- _.localStorage.error(err);
6047
- }
6048
- }
6049
- };
6057
+ _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
6058
+ _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
6050
6059
 
6051
6060
  _.register_event = (function() {
6052
6061
  // written by Dean Edwards, 2005
@@ -6573,6 +6582,31 @@ _.info = {
6573
6582
  }
6574
6583
  };
6575
6584
 
6585
+ /**
6586
+ * Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
6587
+ * Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
6588
+ */
6589
+ var batchedThrottle = function (fn, waitMs) {
6590
+ var timeoutPromise = null;
6591
+ var throttledItems = [];
6592
+ return function (item) {
6593
+ var self = this;
6594
+ throttledItems.push(item);
6595
+
6596
+ if (!timeoutPromise) {
6597
+ timeoutPromise = new PromisePolyfill(function (resolve) {
6598
+ setTimeout(function () {
6599
+ var returnValue = fn.apply(self, [throttledItems]);
6600
+ timeoutPromise = null;
6601
+ throttledItems = [];
6602
+ resolve(returnValue);
6603
+ }, waitMs);
6604
+ });
6605
+ }
6606
+ return timeoutPromise;
6607
+ };
6608
+ };
6609
+
6576
6610
  var cheap_guid = function(maxlen) {
6577
6611
  var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
6578
6612
  return maxlen ? guid.substring(0, maxlen) : guid;
@@ -6615,6 +6649,8 @@ var isOnline = function() {
6615
6649
  return _.isUndefined(onLine) || onLine;
6616
6650
  };
6617
6651
 
6652
+ var NOOP_FUNC = function () {};
6653
+
6618
6654
  var JSONStringify = null, JSONParse = null;
6619
6655
  if (typeof JSON !== 'undefined') {
6620
6656
  JSONStringify = JSON.stringify;
@@ -6623,20 +6659,143 @@ if (typeof JSON !== 'undefined') {
6623
6659
  JSONStringify = JSONStringify || _.JSONEncode;
6624
6660
  JSONParse = JSONParse || _.JSONDecode;
6625
6661
 
6626
- // EXPORTS (for closure compiler)
6627
- _['toArray'] = _.toArray;
6628
- _['isObject'] = _.isObject;
6629
- _['JSONEncode'] = _.JSONEncode;
6630
- _['JSONDecode'] = _.JSONDecode;
6631
- _['isBlockedUA'] = _.isBlockedUA;
6632
- _['isEmptyObject'] = _.isEmptyObject;
6662
+ // UNMINIFIED EXPORTS (for closure compiler)
6633
6663
  _['info'] = _.info;
6634
- _['info']['device'] = _.info.device;
6635
6664
  _['info']['browser'] = _.info.browser;
6636
6665
  _['info']['browserVersion'] = _.info.browserVersion;
6666
+ _['info']['device'] = _.info.device;
6637
6667
  _['info']['properties'] = _.info.properties;
6668
+ _['isBlockedUA'] = _.isBlockedUA;
6669
+ _['isEmptyObject'] = _.isEmptyObject;
6670
+ _['isObject'] = _.isObject;
6671
+ _['JSONDecode'] = _.JSONDecode;
6672
+ _['JSONEncode'] = _.JSONEncode;
6673
+ _['toArray'] = _.toArray;
6638
6674
  _['NPO'] = NpoPromise;
6639
6675
 
6676
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
6677
+
6678
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
6679
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
6680
+
6681
+ // note: increment the version number when adding new object stores
6682
+ var DB_VERSION = 1;
6683
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
6684
+
6685
+ /**
6686
+ * @type {import('./wrapper').StorageWrapper}
6687
+ */
6688
+ var IDBStorageWrapper = function (storeName) {
6689
+ /**
6690
+ * @type {Promise<IDBDatabase>|null}
6691
+ */
6692
+ this.dbPromise = null;
6693
+ this.storeName = storeName;
6694
+ };
6695
+
6696
+ IDBStorageWrapper.prototype._openDb = function () {
6697
+ return new PromisePolyfill(function (resolve, reject) {
6698
+ var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
6699
+ openRequest['onerror'] = function () {
6700
+ reject(openRequest.error);
6701
+ };
6702
+
6703
+ openRequest['onsuccess'] = function () {
6704
+ resolve(openRequest.result);
6705
+ };
6706
+
6707
+ openRequest['onupgradeneeded'] = function (ev) {
6708
+ var db = ev.target.result;
6709
+
6710
+ OBJECT_STORES.forEach(function (storeName) {
6711
+ db.createObjectStore(storeName);
6712
+ });
6713
+ };
6714
+ });
6715
+ };
6716
+
6717
+ IDBStorageWrapper.prototype.init = function () {
6718
+ if (!win.indexedDB) {
6719
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
6720
+ }
6721
+
6722
+ if (!this.dbPromise) {
6723
+ this.dbPromise = this._openDb();
6724
+ }
6725
+
6726
+ return this.dbPromise
6727
+ .then(function (dbOrError) {
6728
+ if (dbOrError instanceof win['IDBDatabase']) {
6729
+ return PromisePolyfill.resolve();
6730
+ } else {
6731
+ return PromisePolyfill.reject(dbOrError);
6732
+ }
6733
+ });
6734
+ };
6735
+
6736
+ /**
6737
+ * @param {IDBTransactionMode} mode
6738
+ * @param {function(IDBObjectStore): void} storeCb
6739
+ */
6740
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
6741
+ var storeName = this.storeName;
6742
+ var doTransaction = function (db) {
6743
+ return new PromisePolyfill(function (resolve, reject) {
6744
+ var transaction = db.transaction(storeName, mode);
6745
+ transaction.oncomplete = function () {
6746
+ resolve(transaction);
6747
+ };
6748
+ transaction.onabort = transaction.onerror = function () {
6749
+ reject(transaction.error);
6750
+ };
6751
+
6752
+ storeCb(transaction.objectStore(storeName));
6753
+ });
6754
+ };
6755
+
6756
+ return this.dbPromise
6757
+ .then(doTransaction)
6758
+ .catch(function (err) {
6759
+ if (err['name'] === 'InvalidStateError') {
6760
+ // try reopening the DB if the connection is closed
6761
+ this.dbPromise = this._openDb();
6762
+ return this.dbPromise.then(doTransaction);
6763
+ } else {
6764
+ return PromisePolyfill.reject(err);
6765
+ }
6766
+ }.bind(this));
6767
+ };
6768
+
6769
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
6770
+ return this.makeTransaction('readwrite', function (objectStore) {
6771
+ objectStore.put(value, key);
6772
+ });
6773
+ };
6774
+
6775
+ IDBStorageWrapper.prototype.getItem = function (key) {
6776
+ var req;
6777
+ return this.makeTransaction('readonly', function (objectStore) {
6778
+ req = objectStore.get(key);
6779
+ }).then(function () {
6780
+ return req.result;
6781
+ });
6782
+ };
6783
+
6784
+ IDBStorageWrapper.prototype.removeItem = function (key) {
6785
+ return this.makeTransaction('readwrite', function (objectStore) {
6786
+ objectStore.delete(key);
6787
+ });
6788
+ };
6789
+
6790
+ IDBStorageWrapper.prototype.getAll = function () {
6791
+ var req;
6792
+ return this.makeTransaction('readonly', function (objectStore) {
6793
+ req = objectStore.getAll();
6794
+ }).then(function () {
6795
+ return req.result;
6796
+ });
6797
+ };
6798
+
6640
6799
  /**
6641
6800
  * GDPR utils
6642
6801
  *
@@ -6962,7 +7121,7 @@ var SharedLock = function(key, options) {
6962
7121
  options = options || {};
6963
7122
 
6964
7123
  this.storageKey = key;
6965
- this.storage = options.storage || window.localStorage;
7124
+ this.storage = options.storage || win.localStorage;
6966
7125
  this.pollIntervalMS = options.pollIntervalMS || 100;
6967
7126
  this.timeoutMS = options.timeoutMS || 2000;
6968
7127
 
@@ -6977,7 +7136,6 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
6977
7136
  return new Promise(_.bind(function (resolve, reject) {
6978
7137
  var i = pid || (new Date().getTime() + '|' + Math.random());
6979
7138
  var startTime = new Date().getTime();
6980
-
6981
7139
  var key = this.storageKey;
6982
7140
  var pollIntervalMS = this.pollIntervalMS;
6983
7141
  var timeoutMS = this.timeoutMS;
@@ -7088,11 +7246,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
7088
7246
  };
7089
7247
 
7090
7248
  /**
7091
- * @typedef {import('./wrapper').StorageWrapper}
7092
- */
7093
-
7094
- /**
7095
- * @type {StorageWrapper}
7249
+ * @type {import('./wrapper').StorageWrapper}
7096
7250
  */
7097
7251
  var LocalStorageWrapper = function (storageOverride) {
7098
7252
  this.storage = storageOverride || localStorage;
@@ -7105,7 +7259,7 @@ LocalStorageWrapper.prototype.init = function () {
7105
7259
  LocalStorageWrapper.prototype.setItem = function (key, value) {
7106
7260
  return new PromisePolyfill(_.bind(function (resolve, reject) {
7107
7261
  try {
7108
- this.storage.setItem(key, value);
7262
+ this.storage.setItem(key, JSONStringify(value));
7109
7263
  } catch (e) {
7110
7264
  reject(e);
7111
7265
  }
@@ -7117,7 +7271,7 @@ LocalStorageWrapper.prototype.getItem = function (key) {
7117
7271
  return new PromisePolyfill(_.bind(function (resolve, reject) {
7118
7272
  var item;
7119
7273
  try {
7120
- item = this.storage.getItem(key);
7274
+ item = JSONParse(this.storage.getItem(key));
7121
7275
  } catch (e) {
7122
7276
  reject(e);
7123
7277
  }
@@ -7160,8 +7314,10 @@ var RequestQueue = function (storageKey, options) {
7160
7314
  this.usePersistence = options.usePersistence;
7161
7315
  if (this.usePersistence) {
7162
7316
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
7163
- this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
7164
- this.queueStorage.init();
7317
+ this.lock = new SharedLock(storageKey, {
7318
+ storage: options.sharedLockStorage || win.localStorage,
7319
+ timeoutMS: options.sharedLockTimeoutMS,
7320
+ });
7165
7321
  }
7166
7322
  this.reportError = options.errorReporter || _.bind(logger$4.error, logger$4);
7167
7323
 
@@ -7169,6 +7325,14 @@ var RequestQueue = function (storageKey, options) {
7169
7325
 
7170
7326
  this.memQueue = [];
7171
7327
  this.initialized = false;
7328
+
7329
+ if (options.enqueueThrottleMs) {
7330
+ this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
7331
+ } else {
7332
+ this.enqueuePersisted = _.bind(function (queueEntry) {
7333
+ return this._enqueuePersisted([queueEntry]);
7334
+ }, this);
7335
+ }
7172
7336
  };
7173
7337
 
7174
7338
  RequestQueue.prototype.ensureInit = function () {
@@ -7211,36 +7375,39 @@ RequestQueue.prototype.enqueue = function (item, flushInterval) {
7211
7375
  this.memQueue.push(queueEntry);
7212
7376
  return PromisePolyfill.resolve(true);
7213
7377
  } else {
7378
+ return this.enqueuePersisted(queueEntry);
7379
+ }
7380
+ };
7214
7381
 
7215
- var enqueueItem = _.bind(function () {
7216
- return this.ensureInit()
7217
- .then(_.bind(function () {
7218
- return this.readFromStorage();
7219
- }, this))
7220
- .then(_.bind(function (storedQueue) {
7221
- storedQueue.push(queueEntry);
7222
- return this.saveToStorage(storedQueue);
7223
- }, this))
7224
- .then(_.bind(function (succeeded) {
7225
- // only add to in-memory queue when storage succeeds
7226
- if (succeeded) {
7227
- this.memQueue.push(queueEntry);
7228
- }
7229
- return succeeded;
7230
- }, this))
7231
- .catch(_.bind(function (err) {
7232
- this.reportError('Error enqueueing item', err, item);
7233
- return false;
7234
- }, this));
7235
- }, this);
7382
+ RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
7383
+ var enqueueItem = _.bind(function () {
7384
+ return this.ensureInit()
7385
+ .then(_.bind(function () {
7386
+ return this.readFromStorage();
7387
+ }, this))
7388
+ .then(_.bind(function (storedQueue) {
7389
+ return this.saveToStorage(storedQueue.concat(queueEntries));
7390
+ }, this))
7391
+ .then(_.bind(function (succeeded) {
7392
+ // only add to in-memory queue when storage succeeds
7393
+ if (succeeded) {
7394
+ this.memQueue = this.memQueue.concat(queueEntries);
7395
+ }
7236
7396
 
7237
- return this.lock
7238
- .withLock(enqueueItem, this.pid)
7397
+ return succeeded;
7398
+ }, this))
7239
7399
  .catch(_.bind(function (err) {
7240
- this.reportError('Error acquiring storage lock', err);
7400
+ this.reportError('Error enqueueing items', err, queueEntries);
7241
7401
  return false;
7242
7402
  }, this));
7243
- }
7403
+ }, this);
7404
+
7405
+ return this.lock
7406
+ .withLock(enqueueItem, this.pid)
7407
+ .catch(_.bind(function (err) {
7408
+ this.reportError('Error acquiring storage lock', err);
7409
+ return false;
7410
+ }, this));
7244
7411
  };
7245
7412
 
7246
7413
  /**
@@ -7261,7 +7428,7 @@ RequestQueue.prototype.fillBatch = function (batchSize) {
7261
7428
  }, this))
7262
7429
  .then(_.bind(function (storedQueue) {
7263
7430
  if (storedQueue.length) {
7264
- // item IDs already in batch; don't duplicate out of storage
7431
+ // item IDs already in batch; don't duplicate out of storage
7265
7432
  var idsInBatch = {}; // poor man's Set
7266
7433
  _.each(batch, function (item) {
7267
7434
  idsInBatch[item['id']] = true;
@@ -7348,7 +7515,7 @@ RequestQueue.prototype.removeItemsByID = function (ids) {
7348
7515
  .withLock(removeFromStorage, this.pid)
7349
7516
  .catch(_.bind(function (err) {
7350
7517
  this.reportError('Error acquiring storage lock', err);
7351
- if (!localStorageSupported(this.queueStorage.storage, true)) {
7518
+ if (!localStorageSupported(this.lock.storage, true)) {
7352
7519
  // Looks like localStorage writes have stopped working sometime after
7353
7520
  // initialization (probably full), and so nobody can acquire locks
7354
7521
  // anymore. Consider it temporarily safe to remove items without the
@@ -7436,7 +7603,6 @@ RequestQueue.prototype.readFromStorage = function () {
7436
7603
  }, this))
7437
7604
  .then(_.bind(function (storageEntry) {
7438
7605
  if (storageEntry) {
7439
- storageEntry = JSONParse(storageEntry);
7440
7606
  if (!_.isArray(storageEntry)) {
7441
7607
  this.reportError('Invalid storage entry:', storageEntry);
7442
7608
  storageEntry = null;
@@ -7454,16 +7620,9 @@ RequestQueue.prototype.readFromStorage = function () {
7454
7620
  * Serialize the given items array to localStorage.
7455
7621
  */
7456
7622
  RequestQueue.prototype.saveToStorage = function (queue) {
7457
- try {
7458
- var serialized = JSONStringify(queue);
7459
- } catch (err) {
7460
- this.reportError('Error serializing queue', err);
7461
- return PromisePolyfill.resolve(false);
7462
- }
7463
-
7464
7623
  return this.ensureInit()
7465
7624
  .then(_.bind(function () {
7466
- return this.queueStorage.setItem(this.storageKey, serialized);
7625
+ return this.queueStorage.setItem(this.storageKey, queue);
7467
7626
  }, this))
7468
7627
  .then(function () {
7469
7628
  return true;
@@ -7507,7 +7666,9 @@ var RequestBatcher = function(storageKey, options) {
7507
7666
  errorReporter: _.bind(this.reportError, this),
7508
7667
  queueStorage: options.queueStorage,
7509
7668
  sharedLockStorage: options.sharedLockStorage,
7510
- usePersistence: options.usePersistence
7669
+ sharedLockTimeoutMS: options.sharedLockTimeoutMS,
7670
+ usePersistence: options.usePersistence,
7671
+ enqueueThrottleMs: options.enqueueThrottleMs
7511
7672
  });
7512
7673
 
7513
7674
  this.libConfig = options.libConfig;
@@ -7529,6 +7690,8 @@ var RequestBatcher = function(storageKey, options) {
7529
7690
  // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
7530
7691
  // in a request loop and get ratelimited by the server.
7531
7692
  this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
7693
+
7694
+ this._flushPromise = null;
7532
7695
  };
7533
7696
 
7534
7697
  /**
@@ -7588,7 +7751,7 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
7588
7751
  if (!this.stopped) { // don't schedule anymore if batching has been stopped
7589
7752
  this.timeoutID = setTimeout(_.bind(function() {
7590
7753
  if (!this.stopped) {
7591
- this.flush();
7754
+ this._flushPromise = this.flush();
7592
7755
  }
7593
7756
  }, this), this.flushInterval);
7594
7757
  }
@@ -7820,6 +7983,17 @@ RequestBatcher.prototype.reportError = function(msg, err) {
7820
7983
  }
7821
7984
  };
7822
7985
 
7986
+ /**
7987
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
7988
+ * @returns {boolean}
7989
+ */
7990
+ var isRecordingExpired = function(serializedRecording) {
7991
+ var now = Date.now();
7992
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
7993
+ };
7994
+
7995
+ var RECORD_ENQUEUE_THROTTLE_MS = 250;
7996
+
7823
7997
  var logger$2 = console_with_prefix('recorder');
7824
7998
  var CompressionStream = win['CompressionStream'];
7825
7999
 
@@ -7846,29 +8020,58 @@ function isUserEvent(ev) {
7846
8020
  return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
7847
8021
  }
7848
8022
 
8023
+ /**
8024
+ * @typedef {Object} SerializedRecording
8025
+ * @property {number} idleExpires
8026
+ * @property {number} maxExpires
8027
+ * @property {number} replayStartTime
8028
+ * @property {number} seqNo
8029
+ * @property {string} batchStartUrl
8030
+ * @property {string} replayId
8031
+ * @property {string} tabId
8032
+ * @property {string} replayStartUrl
8033
+ */
8034
+
8035
+ /**
8036
+ * @typedef {Object} SessionRecordingOptions
8037
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
8038
+ * @property {String} [options.replayId] - unique uuid for a single replay
8039
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
8040
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
8041
+ * @property {Function} [options.rrwebRecord] - rrweb's `record` function
8042
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
8043
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
8044
+ * optional properties for deserialization:
8045
+ * @property {number} idleExpires
8046
+ * @property {number} maxExpires
8047
+ * @property {number} replayStartTime
8048
+ * @property {number} seqNo
8049
+ * @property {string} batchStartUrl
8050
+ * @property {string} replayStartUrl
8051
+ */
8052
+
8053
+
7849
8054
  /**
7850
8055
  * This class encapsulates a single session recording and its lifecycle.
7851
- * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
7852
- * @param {String} [options.replayId] - unique uuid for a single replay
7853
- * @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
7854
- * @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
7855
- * @param {Function} [options.rrwebRecord] - rrweb's `record` function
8056
+ * @param {SessionRecordingOptions} options
7856
8057
  */
7857
8058
  var SessionRecording = function(options) {
7858
8059
  this._mixpanel = options.mixpanelInstance;
7859
- this._onIdleTimeout = options.onIdleTimeout;
7860
- this._onMaxLengthReached = options.onMaxLengthReached;
7861
- this._rrwebRecord = options.rrwebRecord;
7862
-
7863
- this.replayId = options.replayId;
8060
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
8061
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
8062
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
8063
+ this._rrwebRecord = options.rrwebRecord || null;
7864
8064
 
7865
8065
  // internal rrweb stopRecording function
7866
8066
  this._stopRecording = null;
8067
+ this.replayId = options.replayId;
7867
8068
 
7868
- this.seqNo = 0;
7869
- this.replayStartTime = null;
7870
- this.replayStartUrl = null;
7871
- this.batchStartUrl = null;
8069
+ this.batchStartUrl = options.batchStartUrl || null;
8070
+ this.replayStartUrl = options.replayStartUrl || null;
8071
+ this.idleExpires = options.idleExpires || null;
8072
+ this.maxExpires = options.maxExpires || null;
8073
+ this.replayStartTime = options.replayStartTime || null;
8074
+ this.seqNo = options.seqNo || 0;
7872
8075
 
7873
8076
  this.idleTimeoutId = null;
7874
8077
  this.maxTimeoutId = null;
@@ -7876,18 +8079,40 @@ var SessionRecording = function(options) {
7876
8079
  this.recordMaxMs = MAX_RECORDING_MS;
7877
8080
  this.recordMinMs = 0;
7878
8081
 
8082
+ // disable persistence if localStorage is not supported
8083
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
8084
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true);
8085
+
7879
8086
  // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
7880
8087
  // this will be important when persistence is introduced
7881
- var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
7882
- this.batcher = new RequestBatcher(batcherKey, {
7883
- errorReporter: _.bind(this.reportError, this),
8088
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
8089
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
8090
+ this.batcher = new RequestBatcher(this.batcherKey, {
8091
+ errorReporter: this.reportError.bind(this),
7884
8092
  flushOnlyOnInterval: true,
7885
8093
  libConfig: RECORDER_BATCHER_LIB_CONFIG,
7886
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
7887
- usePersistence: false
8094
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
8095
+ queueStorage: this.queueStorage,
8096
+ sharedLockStorage: options.sharedLockStorage,
8097
+ usePersistence: usePersistence,
8098
+ stopAllBatchingFunc: this.stopRecording.bind(this),
8099
+
8100
+ // increased throttle and shared lock timeout because recording events are very high frequency.
8101
+ // this will minimize the amount of lock contention between enqueued events.
8102
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
8103
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
8104
+ sharedLockTimeoutMS: 10 * 1000,
7888
8105
  });
7889
8106
  };
7890
8107
 
8108
+ SessionRecording.prototype.unloadPersistedData = function () {
8109
+ this.batcher.stop();
8110
+ return this.batcher.flush()
8111
+ .then(function () {
8112
+ return this.queueStorage.removeItem(this.batcherKey);
8113
+ }.bind(this));
8114
+ };
8115
+
7891
8116
  SessionRecording.prototype.getConfig = function(configVar) {
7892
8117
  return this._mixpanel.get_config(configVar);
7893
8118
  };
@@ -7900,6 +8125,11 @@ SessionRecording.prototype.get_config = function(configVar) {
7900
8125
  };
7901
8126
 
7902
8127
  SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
8128
+ if (this._rrwebRecord === null) {
8129
+ this.reportError('rrweb record function not provided. ');
8130
+ return;
8131
+ }
8132
+
7903
8133
  if (this._stopRecording !== null) {
7904
8134
  logger$2.log('Recording already in progress, skipping startRecording.');
7905
8135
  return;
@@ -7911,15 +8141,21 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7911
8141
  logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7912
8142
  }
7913
8143
 
8144
+ if (!this.maxExpires) {
8145
+ this.maxExpires = new Date().getTime() + this.recordMaxMs;
8146
+ }
8147
+
7914
8148
  this.recordMinMs = this.getConfig('record_min_ms');
7915
8149
  if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7916
8150
  this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7917
8151
  logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7918
8152
  }
7919
8153
 
7920
- this.replayStartTime = new Date().getTime();
7921
- this.batchStartUrl = _.info.currentUrl();
7922
- this.replayStartUrl = _.info.currentUrl();
8154
+ if (!this.replayStartTime) {
8155
+ this.replayStartTime = new Date().getTime();
8156
+ this.batchStartUrl = _.info.currentUrl();
8157
+ this.replayStartUrl = _.info.currentUrl();
8158
+ }
7923
8159
 
7924
8160
  if (shouldStopBatcher || this.recordMinMs > 0) {
7925
8161
  // the primary case for shouldStopBatcher is when we're starting recording after a reset
@@ -7932,10 +8168,12 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7932
8168
  this.batcher.start();
7933
8169
  }
7934
8170
 
7935
- var resetIdleTimeout = _.bind(function () {
8171
+ var resetIdleTimeout = function () {
7936
8172
  clearTimeout(this.idleTimeoutId);
7937
- this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
7938
- }, this);
8173
+ var idleTimeoutMs = this.getConfig('record_idle_timeout_ms');
8174
+ this.idleTimeoutId = setTimeout(this._onIdleTimeout, idleTimeoutMs);
8175
+ this.idleExpires = new Date().getTime() + idleTimeoutMs;
8176
+ }.bind(this);
7939
8177
 
7940
8178
  var blockSelector = this.getConfig('record_block_selector');
7941
8179
  if (blockSelector === '' || blockSelector === null) {
@@ -7943,8 +8181,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7943
8181
  }
7944
8182
 
7945
8183
  this._stopRecording = this._rrwebRecord({
7946
- 'emit': _.bind(function (ev) {
7947
- this.batcher.enqueue(ev);
8184
+ 'emit': addOptOutCheckMixpanelLib(function (ev) {
7948
8185
  if (isUserEvent(ev)) {
7949
8186
  if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
7950
8187
  // start flushing again after user activity
@@ -7952,7 +8189,10 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7952
8189
  }
7953
8190
  resetIdleTimeout();
7954
8191
  }
7955
- }, this),
8192
+
8193
+ // promise only used to await during tests
8194
+ this.__enqueuePromise = this.batcher.enqueue(ev);
8195
+ }.bind(this)),
7956
8196
  'blockClass': this.getConfig('record_block_class'),
7957
8197
  'blockSelector': blockSelector,
7958
8198
  'collectFonts': this.getConfig('record_collect_fonts'),
@@ -7978,10 +8218,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7978
8218
 
7979
8219
  resetIdleTimeout();
7980
8220
 
7981
- this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
8221
+ var maxTimeoutMs = this.maxExpires - new Date().getTime();
8222
+ this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
7982
8223
  };
7983
8224
 
7984
- SessionRecording.prototype.stopRecording = function () {
8225
+ SessionRecording.prototype.stopRecording = function (skipFlush) {
7985
8226
  if (!this.isRrwebStopped()) {
7986
8227
  try {
7987
8228
  this._stopRecording();
@@ -7991,17 +8232,19 @@ SessionRecording.prototype.stopRecording = function () {
7991
8232
  this._stopRecording = null;
7992
8233
  }
7993
8234
 
8235
+ var flushPromise;
7994
8236
  if (this.batcher.stopped) {
7995
8237
  // never got user activity to flush after reset, so just clear the batcher
7996
- this.batcher.clear();
7997
- } else {
8238
+ flushPromise = this.batcher.clear();
8239
+ } else if (!skipFlush) {
7998
8240
  // flush any remaining events from running batcher
7999
- this.batcher.flush();
8000
- this.batcher.stop();
8241
+ flushPromise = this.batcher.flush();
8001
8242
  }
8243
+ this.batcher.stop();
8002
8244
 
8003
8245
  clearTimeout(this.idleTimeoutId);
8004
8246
  clearTimeout(this.maxTimeoutId);
8247
+ return flushPromise;
8005
8248
  };
8006
8249
 
8007
8250
  SessionRecording.prototype.isRrwebStopped = function () {
@@ -8013,7 +8256,54 @@ SessionRecording.prototype.isRrwebStopped = function () {
8013
8256
  * we stop recording and dump any queued events if the user has opted out.
8014
8257
  */
8015
8258
  SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
8016
- this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
8259
+ this._flushEvents(data, options, cb, this._onOptOut.bind(this));
8260
+ };
8261
+
8262
+ /**
8263
+ * @returns {SerializedRecording}
8264
+ */
8265
+ SessionRecording.prototype.serialize = function () {
8266
+ // don't break if mixpanel instance was destroyed at some point
8267
+ var tabId;
8268
+ try {
8269
+ tabId = this._mixpanel.get_tab_id();
8270
+ } catch (e) {
8271
+ this.reportError('Error getting tab ID for serialization ', e);
8272
+ tabId = null;
8273
+ }
8274
+
8275
+ return {
8276
+ 'replayId': this.replayId,
8277
+ 'seqNo': this.seqNo,
8278
+ 'replayStartTime': this.replayStartTime,
8279
+ 'batchStartUrl': this.batchStartUrl,
8280
+ 'replayStartUrl': this.replayStartUrl,
8281
+ 'idleExpires': this.idleExpires,
8282
+ 'maxExpires': this.maxExpires,
8283
+ 'tabId': tabId,
8284
+ };
8285
+ };
8286
+
8287
+
8288
+ /**
8289
+ * @static
8290
+ * @param {SerializedRecording} serializedRecording
8291
+ * @param {SessionRecordingOptions} options
8292
+ * @returns {SessionRecording}
8293
+ */
8294
+ SessionRecording.deserialize = function (serializedRecording, options) {
8295
+ var recording = new SessionRecording(_.extend({}, options, {
8296
+ replayId: serializedRecording['replayId'],
8297
+ batchStartUrl: serializedRecording['batchStartUrl'],
8298
+ replayStartUrl: serializedRecording['replayStartUrl'],
8299
+ idleExpires: serializedRecording['idleExpires'],
8300
+ maxExpires: serializedRecording['maxExpires'],
8301
+ replayStartTime: serializedRecording['replayStartTime'],
8302
+ seqNo: serializedRecording['seqNo'],
8303
+ sharedLockStorage: options.sharedLockStorage,
8304
+ }));
8305
+
8306
+ return recording;
8017
8307
  };
8018
8308
 
8019
8309
  SessionRecording.prototype._onOptOut = function (code) {
@@ -8024,7 +8314,7 @@ SessionRecording.prototype._onOptOut = function (code) {
8024
8314
  };
8025
8315
 
8026
8316
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
8027
- var onSuccess = _.bind(function (response, responseBody) {
8317
+ var onSuccess = function (response, responseBody) {
8028
8318
  // Update batch specific props only if the request was successful to guarantee ordering.
8029
8319
  // RequestBatcher will always flush the next batch after the previous one succeeds.
8030
8320
  // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
@@ -8032,13 +8322,15 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
8032
8322
  this.seqNo++;
8033
8323
  this.batchStartUrl = _.info.currentUrl();
8034
8324
  }
8325
+
8326
+ this._onBatchSent();
8035
8327
  callback({
8036
8328
  status: 0,
8037
8329
  httpStatusCode: response.status,
8038
8330
  responseBody: responseBody,
8039
8331
  retryAfter: response.headers.get('Retry-After')
8040
8332
  });
8041
- }, this);
8333
+ }.bind(this);
8042
8334
 
8043
8335
  win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
8044
8336
  'method': 'POST',
@@ -8059,7 +8351,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
8059
8351
  };
8060
8352
 
8061
8353
  SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
8062
- const numEvents = data.length;
8354
+ var numEvents = data.length;
8063
8355
 
8064
8356
  if (numEvents > 0) {
8065
8357
  var replayId = this.replayId;
@@ -8104,10 +8396,10 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
8104
8396
  var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
8105
8397
  new Response(gzipStream)
8106
8398
  .blob()
8107
- .then(_.bind(function(compressedBlob) {
8399
+ .then(function(compressedBlob) {
8108
8400
  reqParams['format'] = 'gzip';
8109
8401
  this._sendRequest(replayId, reqParams, compressedBlob, callback);
8110
- }, this));
8402
+ }.bind(this));
8111
8403
  } else {
8112
8404
  reqParams['format'] = 'body';
8113
8405
  this._sendRequest(replayId, reqParams, eventsJson, callback);
@@ -8128,54 +8420,208 @@ SessionRecording.prototype.reportError = function(msg, err) {
8128
8420
  }
8129
8421
  };
8130
8422
 
8423
+ /**
8424
+ * Module for handling the storage and retrieval of recording metadata as well as any active recordings.
8425
+ * Makes sure that only one tab can be recording at a time.
8426
+ */
8427
+ var RecordingRegistry = function (options) {
8428
+ this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
8429
+ this.errorReporter = options.errorReporter;
8430
+ this.mixpanelInstance = options.mixpanelInstance;
8431
+ this.sharedLockStorage = options.sharedLockStorage;
8432
+ };
8433
+
8434
+ RecordingRegistry.prototype.handleError = function (err) {
8435
+ this.errorReporter('IndexedDB error: ', err);
8436
+ };
8437
+
8438
+ /**
8439
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
8440
+ */
8441
+ RecordingRegistry.prototype.setActiveRecording = function (serializedRecording) {
8442
+ var tabId = serializedRecording['tabId'];
8443
+ if (!tabId) {
8444
+ console.warn('No tab ID is set, cannot persist recording metadata.');
8445
+ return PromisePolyfill.resolve();
8446
+ }
8447
+
8448
+ return this.idb.init()
8449
+ .then(function () {
8450
+ return this.idb.setItem(tabId, serializedRecording);
8451
+ }.bind(this))
8452
+ .catch(this.handleError.bind(this));
8453
+ };
8454
+
8455
+ /**
8456
+ * @returns {Promise<import('./session-recording').SerializedRecording>}
8457
+ */
8458
+ RecordingRegistry.prototype.getActiveRecording = function () {
8459
+ return this.idb.init()
8460
+ .then(function () {
8461
+ return this.idb.getItem(this.mixpanelInstance.get_tab_id());
8462
+ }.bind(this))
8463
+ .then(function (serializedRecording) {
8464
+ return isRecordingExpired(serializedRecording) ? null : serializedRecording;
8465
+ }.bind(this))
8466
+ .catch(this.handleError.bind(this));
8467
+ };
8468
+
8469
+ RecordingRegistry.prototype.clearActiveRecording = function () {
8470
+ // mark recording as expired instead of deleting it in case the page unloads mid-flush and doesn't make it to ingestion.
8471
+ // this will ensure the next pageload will flush the remaining events, but not try to continue the recording.
8472
+ return this.getActiveRecording()
8473
+ .then(function (serializedRecording) {
8474
+ if (serializedRecording) {
8475
+ serializedRecording['maxExpires'] = 0;
8476
+ return this.setActiveRecording(serializedRecording);
8477
+ }
8478
+ }.bind(this))
8479
+ .catch(this.handleError.bind(this));
8480
+ };
8481
+
8482
+ /**
8483
+ * Flush any inactive recordings from the registry to minimize data loss.
8484
+ * The main idea here is that we can flush remaining rrweb events on the next page load if a tab is closed mid-batch.
8485
+ */
8486
+ RecordingRegistry.prototype.flushInactiveRecordings = function () {
8487
+ return this.idb.init()
8488
+ .then(function() {
8489
+ return this.idb.getAll();
8490
+ }.bind(this))
8491
+ .then(function (serializedRecordings) {
8492
+ // clean up any expired recordings from the registry, non-expired ones may be active in other tabs
8493
+ var unloadPromises = serializedRecordings
8494
+ .filter(function (serializedRecording) {
8495
+ return isRecordingExpired(serializedRecording);
8496
+ })
8497
+ .map(function (serializedRecording) {
8498
+ var sessionRecording = SessionRecording.deserialize(serializedRecording, {
8499
+ mixpanelInstance: this.mixpanelInstance,
8500
+ sharedLockStorage: this.sharedLockStorage
8501
+ });
8502
+ return sessionRecording.unloadPersistedData()
8503
+ .then(function () {
8504
+ // expired recording was successfully flushed, we can clean it up from the registry
8505
+ return this.idb.removeItem(serializedRecording['tabId']);
8506
+ }.bind(this))
8507
+ .catch(this.handleError.bind(this));
8508
+ }.bind(this));
8509
+
8510
+ return PromisePolyfill.all(unloadPromises);
8511
+ }.bind(this))
8512
+ .catch(this.handleError.bind(this));
8513
+ };
8514
+
8131
8515
  var logger$1 = console_with_prefix('recorder');
8132
8516
 
8133
8517
  /**
8134
- * Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
8518
+ * Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
8135
8519
  * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
8136
- */
8137
- var MixpanelRecorder = function(mixpanelInstance) {
8138
- this._mixpanel = mixpanelInstance;
8520
+ */
8521
+ var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage) {
8522
+ this.mixpanelInstance = mixpanelInstance;
8523
+ this.rrwebRecord = rrwebRecord || record;
8524
+ this.sharedLockStorage = sharedLockStorage;
8525
+
8526
+ /**
8527
+ * @member {import('./registry').RecordingRegistry}
8528
+ */
8529
+ this.recordingRegistry = new RecordingRegistry({
8530
+ mixpanelInstance: this.mixpanelInstance,
8531
+ errorReporter: logger$1.error,
8532
+ sharedLockStorage: sharedLockStorage
8533
+ });
8534
+ this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
8535
+
8139
8536
  this.activeRecording = null;
8140
8537
  };
8141
8538
 
8142
- MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
8539
+ MixpanelRecorder.prototype.startRecording = function(options) {
8540
+ options = options || {};
8143
8541
  if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
8144
8542
  logger$1.log('Recording already in progress, skipping startRecording.');
8145
8543
  return;
8146
8544
  }
8147
8545
 
8148
- var onIdleTimeout = _.bind(function () {
8546
+ var onIdleTimeout = function () {
8149
8547
  logger$1.log('Idle timeout reached, restarting recording.');
8150
8548
  this.resetRecording();
8151
- }, this);
8549
+ }.bind(this);
8152
8550
 
8153
- var onMaxLengthReached = _.bind(function () {
8551
+ var onMaxLengthReached = function () {
8154
8552
  logger$1.log('Max recording length reached, stopping recording.');
8155
8553
  this.resetRecording();
8156
- }, this);
8554
+ }.bind(this);
8555
+
8556
+ var onBatchSent = function () {
8557
+ this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
8558
+ this['__flushPromise'] = this.activeRecording.batcher._flushPromise;
8559
+ }.bind(this);
8157
8560
 
8158
- this.activeRecording = new SessionRecording({
8159
- mixpanelInstance: this._mixpanel,
8561
+ /**
8562
+ * @type {import('./session-recording').SessionRecordingOptions}
8563
+ */
8564
+ var sessionRecordingOptions = {
8565
+ mixpanelInstance: this.mixpanelInstance,
8566
+ onBatchSent: onBatchSent,
8160
8567
  onIdleTimeout: onIdleTimeout,
8161
8568
  onMaxLengthReached: onMaxLengthReached,
8162
8569
  replayId: _.UUID(),
8163
- rrwebRecord: record
8164
- });
8570
+ rrwebRecord: this.rrwebRecord,
8571
+ sharedLockStorage: this.sharedLockStorage
8572
+ };
8165
8573
 
8166
- this.activeRecording.startRecording(shouldStopBatcher);
8574
+ if (options.activeSerializedRecording) {
8575
+ this.activeRecording = SessionRecording.deserialize(options.activeSerializedRecording, sessionRecordingOptions);
8576
+ } else {
8577
+ this.activeRecording = new SessionRecording(sessionRecordingOptions);
8578
+ }
8579
+
8580
+ this.activeRecording.startRecording(options.shouldStopBatcher);
8581
+ return this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
8167
8582
  };
8168
8583
 
8169
8584
  MixpanelRecorder.prototype.stopRecording = function() {
8585
+ var stopPromise = this._stopCurrentRecording(false);
8586
+ this.recordingRegistry.clearActiveRecording();
8587
+ this.activeRecording = null;
8588
+ return stopPromise;
8589
+ };
8590
+
8591
+ MixpanelRecorder.prototype.pauseRecording = function() {
8592
+ return this._stopCurrentRecording(false);
8593
+ };
8594
+
8595
+ MixpanelRecorder.prototype._stopCurrentRecording = function(skipFlush) {
8170
8596
  if (this.activeRecording) {
8171
- this.activeRecording.stopRecording();
8172
- this.activeRecording = null;
8597
+ return this.activeRecording.stopRecording(skipFlush);
8598
+ }
8599
+ return PromisePolyfill.resolve();
8600
+ };
8601
+
8602
+ MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
8603
+ if (this.activeRecording && this.activeRecording.isRrwebStopped()) {
8604
+ this.activeRecording.startRecording(false);
8605
+ return PromisePolyfill.resolve(null);
8173
8606
  }
8607
+
8608
+ return this.recordingRegistry.getActiveRecording()
8609
+ .then(function (activeSerializedRecording) {
8610
+ if (activeSerializedRecording) {
8611
+ return this.startRecording({activeSerializedRecording: activeSerializedRecording});
8612
+ } else if (startNewIfInactive) {
8613
+ return this.startRecording({shouldStopBatcher: false});
8614
+ } else {
8615
+ logger$1.log('No resumable recording found.');
8616
+ return null;
8617
+ }
8618
+ }.bind(this));
8174
8619
  };
8175
8620
 
8621
+
8176
8622
  MixpanelRecorder.prototype.resetRecording = function () {
8177
8623
  this.stopRecording();
8178
- this.startRecording(true);
8624
+ this.startRecording({shouldStopBatcher: true});
8179
8625
  };
8180
8626
 
8181
8627
  MixpanelRecorder.prototype.getActiveReplayId = function () {
@@ -8264,7 +8710,7 @@ function getPreviousElementSibling(el) {
8264
8710
  }
8265
8711
  }
8266
8712
 
8267
- function getPropertiesFromElement(el) {
8713
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
8268
8714
  var props = {
8269
8715
  '$classes': getClassName(el).split(' '),
8270
8716
  '$tag_name': el.tagName.toLowerCase()
@@ -8274,9 +8720,9 @@ function getPropertiesFromElement(el) {
8274
8720
  props['$id'] = elId;
8275
8721
  }
8276
8722
 
8277
- if (shouldTrackElement(el)) {
8278
- _.each(TRACKED_ATTRS, function(attr) {
8279
- if (el.hasAttribute(attr)) {
8723
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
8724
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
8725
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
8280
8726
  var attrVal = el.getAttribute(attr);
8281
8727
  if (shouldTrackValue(attrVal)) {
8282
8728
  props['$attr-' + attr] = attrVal;
@@ -8300,8 +8746,21 @@ function getPropertiesFromElement(el) {
8300
8746
  return props;
8301
8747
  }
8302
8748
 
8303
- function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8304
- blockSelectors = blockSelectors || [];
8749
+ function getPropsForDOMEvent(ev, config) {
8750
+ var allowElementCallback = config.allowElementCallback;
8751
+ var allowSelectors = config.allowSelectors || [];
8752
+ var blockAttrs = config.blockAttrs || [];
8753
+ var blockElementCallback = config.blockElementCallback;
8754
+ var blockSelectors = config.blockSelectors || [];
8755
+ var captureTextContent = config.captureTextContent || false;
8756
+ var captureExtraAttrs = config.captureExtraAttrs || [];
8757
+
8758
+ // convert array to set every time, as the config may have changed
8759
+ var blockAttrsSet = {};
8760
+ _.each(blockAttrs, function(attr) {
8761
+ blockAttrsSet[attr] = true;
8762
+ });
8763
+
8305
8764
  var props = null;
8306
8765
 
8307
8766
  var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
@@ -8309,7 +8768,11 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8309
8768
  target = target.parentNode;
8310
8769
  }
8311
8770
 
8312
- if (shouldTrackDomEvent(target, ev)) {
8771
+ if (
8772
+ shouldTrackDomEvent(target, ev) &&
8773
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
8774
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
8775
+ ) {
8313
8776
  var targetElementList = [target];
8314
8777
  var curEl = target;
8315
8778
  while (curEl.parentNode && !isTag(curEl, 'body')) {
@@ -8320,37 +8783,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8320
8783
  var elementsJson = [];
8321
8784
  var href, explicitNoTrack = false;
8322
8785
  _.each(targetElementList, function(el) {
8323
- var shouldTrackEl = shouldTrackElement(el);
8786
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
8324
8787
 
8325
8788
  // if the element or a parent element is an anchor tag
8326
8789
  // include the href as a property
8327
- if (el.tagName.toLowerCase() === 'a') {
8790
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
8328
8791
  href = el.getAttribute('href');
8329
- href = shouldTrackEl && shouldTrackValue(href) && href;
8792
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
8330
8793
  }
8331
8794
 
8332
- // allow users to programmatically prevent tracking of elements by adding classes such as 'mp-no-track'
8333
- var classes = getClasses(el);
8334
- _.each(OPT_OUT_CLASSES, function(cls) {
8335
- if (classes[cls]) {
8336
- explicitNoTrack = true;
8337
- }
8338
- });
8339
-
8340
- if (!explicitNoTrack) {
8341
- // programmatically prevent tracking of elements that match CSS selectors
8342
- _.each(blockSelectors, function(sel) {
8343
- try {
8344
- if (el['matches'](sel)) {
8345
- explicitNoTrack = true;
8346
- }
8347
- } catch (err) {
8348
- logger.critical('Error while checking selector: ' + sel, err);
8349
- }
8350
- });
8795
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
8796
+ explicitNoTrack = true;
8351
8797
  }
8352
8798
 
8353
- elementsJson.push(getPropertiesFromElement(el));
8799
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
8354
8800
  }, this);
8355
8801
 
8356
8802
  if (!explicitNoTrack) {
@@ -8364,9 +8810,17 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8364
8810
  '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
8365
8811
  '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
8366
8812
  };
8813
+ _.each(captureExtraAttrs, function(attr) {
8814
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
8815
+ var attrVal = target.getAttribute(attr);
8816
+ if (shouldTrackValue(attrVal)) {
8817
+ props['$el_attr__' + attr] = attrVal;
8818
+ }
8819
+ }
8820
+ });
8367
8821
 
8368
8822
  if (captureTextContent) {
8369
- elementText = getSafeText(target);
8823
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
8370
8824
  if (elementText && elementText.length) {
8371
8825
  props['$el_text'] = elementText;
8372
8826
  }
@@ -8382,14 +8836,22 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8382
8836
  }
8383
8837
  // prioritize text content from "real" click target if different from original target
8384
8838
  if (captureTextContent) {
8385
- var elementText = getSafeText(target);
8839
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
8386
8840
  if (elementText && elementText.length) {
8387
8841
  props['$el_text'] = elementText;
8388
8842
  }
8389
8843
  }
8390
8844
 
8391
8845
  if (target) {
8392
- var targetProps = getPropertiesFromElement(target);
8846
+ // target may have been recalculated; check allowlists and blocklists again
8847
+ if (
8848
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
8849
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
8850
+ ) {
8851
+ return null;
8852
+ }
8853
+
8854
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
8393
8855
  props['$target'] = targetProps;
8394
8856
  // pull up more props onto main event props
8395
8857
  props['$el_classes'] = targetProps['$classes'];
@@ -8405,19 +8867,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8405
8867
  }
8406
8868
 
8407
8869
 
8408
- /*
8870
+ /**
8409
8871
  * Get the direct text content of an element, protecting against sensitive data collection.
8410
8872
  * Concats textContent of each of the element's text node children; this avoids potential
8411
8873
  * collection of sensitive data that could happen if we used element.textContent and the
8412
8874
  * element had sensitive child elements, since element.textContent includes child content.
8413
8875
  * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
8414
8876
  * @param {Element} el - element to get the text of
8877
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
8415
8878
  * @returns {string} the element's direct text content
8416
8879
  */
8417
- function getSafeText(el) {
8880
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
8418
8881
  var elText = '';
8419
8882
 
8420
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
8883
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
8421
8884
  _.each(el.childNodes, function(child) {
8422
8885
  if (isTextNode(child) && child.textContent) {
8423
8886
  elText += _.trim(child.textContent)
@@ -8456,6 +8919,75 @@ function guessRealClickTarget(ev) {
8456
8919
  return target;
8457
8920
  }
8458
8921
 
8922
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
8923
+ if (allowElementCallback) {
8924
+ try {
8925
+ if (!allowElementCallback(el, ev)) {
8926
+ return false;
8927
+ }
8928
+ } catch (err) {
8929
+ logger.critical('Error while checking element in allowElementCallback', err);
8930
+ return false;
8931
+ }
8932
+ }
8933
+
8934
+ if (!allowSelectors.length) {
8935
+ // no allowlist; all elements are fair game
8936
+ return true;
8937
+ }
8938
+
8939
+ for (var i = 0; i < allowSelectors.length; i++) {
8940
+ var sel = allowSelectors[i];
8941
+ try {
8942
+ if (el['matches'](sel)) {
8943
+ return true;
8944
+ }
8945
+ } catch (err) {
8946
+ logger.critical('Error while checking selector: ' + sel, err);
8947
+ }
8948
+ }
8949
+ return false;
8950
+ }
8951
+
8952
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
8953
+ var i;
8954
+
8955
+ if (blockElementCallback) {
8956
+ try {
8957
+ if (blockElementCallback(el, ev)) {
8958
+ return true;
8959
+ }
8960
+ } catch (err) {
8961
+ logger.critical('Error while checking element in blockElementCallback', err);
8962
+ return true;
8963
+ }
8964
+ }
8965
+
8966
+ if (blockSelectors && blockSelectors.length) {
8967
+ // programmatically prevent tracking of elements that match CSS selectors
8968
+ for (i = 0; i < blockSelectors.length; i++) {
8969
+ var sel = blockSelectors[i];
8970
+ try {
8971
+ if (el['matches'](sel)) {
8972
+ return true;
8973
+ }
8974
+ } catch (err) {
8975
+ logger.critical('Error while checking selector: ' + sel, err);
8976
+ }
8977
+ }
8978
+ }
8979
+
8980
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
8981
+ var classes = getClasses(el);
8982
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
8983
+ if (classes[OPT_OUT_CLASSES[i]]) {
8984
+ return true;
8985
+ }
8986
+ }
8987
+
8988
+ return false;
8989
+ }
8990
+
8459
8991
  /*
8460
8992
  * Check whether a DOM node has nodeType Node.ELEMENT_NODE
8461
8993
  * @param {Node} node - node to check
@@ -8530,11 +9062,16 @@ function shouldTrackDomEvent(el, ev) {
8530
9062
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
8531
9063
  * using a variety of heuristics.
8532
9064
  * @param {Element} el - element to check
9065
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
8533
9066
  * @returns {boolean} whether the element should be tracked
8534
9067
  */
8535
- function shouldTrackElement(el) {
9068
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
8536
9069
  var i;
8537
9070
 
9071
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
9072
+ return false;
9073
+ }
9074
+
8538
9075
  for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
8539
9076
  var classes = getClasses(curEl);
8540
9077
  for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
@@ -8624,9 +9161,17 @@ var PAGEVIEW_OPTION_FULL_URL = 'full-url';
8624
9161
  var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
8625
9162
  var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
8626
9163
 
9164
+ var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
9165
+ var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
9166
+ var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
9167
+ var CONFIG_BLOCK_ATTRS = 'block_attrs';
9168
+ var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
8627
9169
  var CONFIG_BLOCK_SELECTORS = 'block_selectors';
8628
9170
  var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
9171
+ var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
8629
9172
  var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
9173
+ var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
9174
+ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
8630
9175
  var CONFIG_TRACK_CLICK = 'click';
8631
9176
  var CONFIG_TRACK_INPUT = 'input';
8632
9177
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
@@ -8634,7 +9179,16 @@ var CONFIG_TRACK_SCROLL = 'scroll';
8634
9179
  var CONFIG_TRACK_SUBMIT = 'submit';
8635
9180
 
8636
9181
  var CONFIG_DEFAULTS = {};
9182
+ CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
9183
+ CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
9184
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
9185
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
9186
+ CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
9187
+ CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
9188
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
8637
9189
  CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
9190
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
9191
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
8638
9192
  CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
8639
9193
  CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
8640
9194
  CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
@@ -8689,13 +9243,37 @@ Autocapture.prototype.getConfig = function(key) {
8689
9243
  };
8690
9244
 
8691
9245
  Autocapture.prototype.currentUrlBlocked = function() {
9246
+ var i;
9247
+ var currentUrl = _.info.currentUrl();
9248
+
9249
+ var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
9250
+ if (allowUrlRegexes.length) {
9251
+ // we're using an allowlist, only track if current URL matches
9252
+ var allowed = false;
9253
+ for (i = 0; i < allowUrlRegexes.length; i++) {
9254
+ var allowRegex = allowUrlRegexes[i];
9255
+ try {
9256
+ if (currentUrl.match(allowRegex)) {
9257
+ allowed = true;
9258
+ break;
9259
+ }
9260
+ } catch (err) {
9261
+ logger.critical('Error while checking block URL regex: ' + allowRegex, err);
9262
+ return true;
9263
+ }
9264
+ }
9265
+ if (!allowed) {
9266
+ // wasn't allowed by any regex
9267
+ return true;
9268
+ }
9269
+ }
9270
+
8692
9271
  var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
8693
9272
  if (!blockUrlRegexes || !blockUrlRegexes.length) {
8694
9273
  return false;
8695
9274
  }
8696
9275
 
8697
- var currentUrl = _.info.currentUrl();
8698
- for (var i = 0; i < blockUrlRegexes.length; i++) {
9276
+ for (i = 0; i < blockUrlRegexes.length; i++) {
8699
9277
  try {
8700
9278
  if (currentUrl.match(blockUrlRegexes[i])) {
8701
9279
  return true;
@@ -8723,11 +9301,15 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
8723
9301
  return;
8724
9302
  }
8725
9303
 
8726
- var props = getPropsForDOMEvent(
8727
- ev,
8728
- this.getConfig(CONFIG_BLOCK_SELECTORS),
8729
- this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
8730
- );
9304
+ var props = getPropsForDOMEvent(ev, {
9305
+ allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
9306
+ allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
9307
+ blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
9308
+ blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
9309
+ blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
9310
+ captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
9311
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
9312
+ });
8731
9313
  if (props) {
8732
9314
  _.extend(props, DEFAULT_PROPS);
8733
9315
  this.mp.track(mpEventName, props);
@@ -8812,13 +9394,14 @@ Autocapture.prototype.initPageviewTracking = function() {
8812
9394
 
8813
9395
  var currentUrl = _.info.currentUrl();
8814
9396
  var shouldTrack = false;
9397
+ var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
8815
9398
  var trackPageviewOption = this.pageviewTrackingConfig();
8816
9399
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
8817
9400
  shouldTrack = currentUrl !== previousTrackedUrl;
8818
9401
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
8819
9402
  shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
8820
9403
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
8821
- shouldTrack = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
9404
+ shouldTrack = didPathChange;
8822
9405
  }
8823
9406
 
8824
9407
  if (shouldTrack) {
@@ -8826,6 +9409,10 @@ Autocapture.prototype.initPageviewTracking = function() {
8826
9409
  if (tracked) {
8827
9410
  previousTrackedUrl = currentUrl;
8828
9411
  }
9412
+ if (didPathChange) {
9413
+ this.lastScrollCheckpoint = 0;
9414
+ logger.log('Path change: re-initializing scroll depth checkpoints');
9415
+ }
8829
9416
  }
8830
9417
  }.bind(this)));
8831
9418
  };
@@ -8837,6 +9424,7 @@ Autocapture.prototype.initScrollTracking = function() {
8837
9424
  return;
8838
9425
  }
8839
9426
  logger.log('Initializing scroll tracking');
9427
+ this.lastScrollCheckpoint = 0;
8840
9428
 
8841
9429
  this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
8842
9430
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
@@ -8846,6 +9434,11 @@ Autocapture.prototype.initScrollTracking = function() {
8846
9434
  return;
8847
9435
  }
8848
9436
 
9437
+ var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
9438
+ var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
9439
+ .slice()
9440
+ .sort(function(a, b) { return a - b; });
9441
+
8849
9442
  var scrollTop = win.scrollY;
8850
9443
  var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
8851
9444
  try {
@@ -8853,10 +9446,25 @@ Autocapture.prototype.initScrollTracking = function() {
8853
9446
  var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
8854
9447
  props['$scroll_height'] = scrollHeight;
8855
9448
  props['$scroll_percentage'] = scrollPercentage;
9449
+ if (scrollPercentage > this.lastScrollCheckpoint) {
9450
+ for (var i = 0; i < scrollCheckpoints.length; i++) {
9451
+ var checkpoint = scrollCheckpoints[i];
9452
+ if (
9453
+ scrollPercentage >= checkpoint &&
9454
+ this.lastScrollCheckpoint < checkpoint
9455
+ ) {
9456
+ props['$scroll_checkpoint'] = checkpoint;
9457
+ this.lastScrollCheckpoint = checkpoint;
9458
+ shouldTrack = true;
9459
+ }
9460
+ }
9461
+ }
8856
9462
  } catch (err) {
8857
9463
  logger.critical('Error while calculating scroll percentage', err);
8858
9464
  }
8859
- this.mp.track(MP_EV_SCROLL, props);
9465
+ if (shouldTrack) {
9466
+ this.mp.track(MP_EV_SCROLL, props);
9467
+ }
8860
9468
  }.bind(this)));
8861
9469
  };
8862
9470
 
@@ -10125,8 +10733,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
10125
10733
  if (!(k in union_q)) {
10126
10734
  union_q[k] = [];
10127
10735
  }
10128
- // We may send duplicates, the server will dedup them.
10129
- union_q[k] = union_q[k].concat(v);
10736
+ // Prevent duplicate values
10737
+ _.each(v, function(item) {
10738
+ if (!_.include(union_q[k], item)) {
10739
+ union_q[k].push(item);
10740
+ }
10741
+ });
10130
10742
  }
10131
10743
  });
10132
10744
  this._pop_from_people_queue(UNSET_ACTION, q_data);
@@ -10225,11 +10837,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
10225
10837
  * Released under the MIT License.
10226
10838
  */
10227
10839
 
10228
- // ==ClosureCompiler==
10229
- // @compilation_level ADVANCED_OPTIMIZATIONS
10230
- // @output_file_name mixpanel-2.8.min.js
10231
- // ==/ClosureCompiler==
10232
-
10233
10840
  /*
10234
10841
  SIMPLE STYLE GUIDE:
10235
10842
 
@@ -10252,7 +10859,6 @@ var INIT_MODULE = 0;
10252
10859
  var INIT_SNIPPET = 1;
10253
10860
 
10254
10861
  var IDENTITY_FUNC = function(x) {return x;};
10255
- var NOOP_FUNC = function() {};
10256
10862
 
10257
10863
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
10258
10864
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
@@ -10561,34 +11167,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
10561
11167
  this.autocapture = new Autocapture(this);
10562
11168
  this.autocapture.init();
10563
11169
 
10564
- if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
10565
- this.start_session_recording();
11170
+ this._init_tab_id();
11171
+ this._check_and_start_session_recording();
11172
+ };
11173
+
11174
+ /**
11175
+ * Assigns a unique UUID to this tab / window by leveraging sessionStorage.
11176
+ * This is primarily used for session recording, where data must be isolated to the current tab.
11177
+ */
11178
+ MixpanelLib.prototype._init_tab_id = function() {
11179
+ if (_.sessionStorage.is_supported()) {
11180
+ try {
11181
+ var key_suffix = this.get_config('name') + '_' + this.get_config('token');
11182
+ var tab_id_key = 'mp_tab_id_' + key_suffix;
11183
+
11184
+ // A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
11185
+ // This enforces a unique ID in the cases like duplicated tab, window.open(...)
11186
+ var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
11187
+ if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
11188
+ _.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
11189
+ }
11190
+
11191
+ _.sessionStorage.set(should_generate_new_tab_id_key, '1');
11192
+ this.tab_id = _.sessionStorage.get(tab_id_key);
11193
+
11194
+ // Remove the flag when the tab is unloaded to indicate the stored tab ID can be reused. This event is not reliable to detect all page unloads,
11195
+ // but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
11196
+ // If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
11197
+ win.addEventListener('beforeunload', function () {
11198
+ _.sessionStorage.remove(should_generate_new_tab_id_key);
11199
+ });
11200
+ } catch(err) {
11201
+ this.report_error('Error initializing tab id', err);
11202
+ }
11203
+ } else {
11204
+ this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
10566
11205
  }
10567
11206
  };
10568
11207
 
10569
- MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () {
11208
+ MixpanelLib.prototype.get_tab_id = function () {
11209
+ return this.tab_id || null;
11210
+ };
11211
+
11212
+ MixpanelLib.prototype._should_load_recorder = function () {
11213
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
11214
+ var tab_id = this.get_tab_id();
11215
+ return recording_registry_idb.init()
11216
+ .then(function () {
11217
+ return recording_registry_idb.getAll();
11218
+ })
11219
+ .then(function (recordings) {
11220
+ for (var i = 0; i < recordings.length; i++) {
11221
+ // if there are expired recordings in the registry, we should load the recorder to flush them
11222
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
11223
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
11224
+ return true;
11225
+ }
11226
+ }
11227
+ return false;
11228
+ })
11229
+ .catch(_.bind(function (err) {
11230
+ this.report_error('Error checking recording registry', err);
11231
+ }, this));
11232
+ };
11233
+
11234
+ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
10570
11235
  if (!win['MutationObserver']) {
10571
11236
  console$1.critical('Browser does not support MutationObserver; skipping session recording');
10572
11237
  return;
10573
11238
  }
10574
11239
 
10575
- var handleLoadedRecorder = _.bind(function() {
10576
- this._recorder = this._recorder || new win['__mp_recorder'](this);
10577
- this._recorder['startRecording']();
11240
+ var loadRecorder = _.bind(function(startNewIfInactive) {
11241
+ var handleLoadedRecorder = _.bind(function() {
11242
+ this._recorder = this._recorder || new win['__mp_recorder'](this);
11243
+ this._recorder['resumeRecording'](startNewIfInactive);
11244
+ }, this);
11245
+
11246
+ if (_.isUndefined(win['__mp_recorder'])) {
11247
+ load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
11248
+ } else {
11249
+ handleLoadedRecorder();
11250
+ }
10578
11251
  }, this);
10579
11252
 
10580
- if (_.isUndefined(win['__mp_recorder'])) {
10581
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
11253
+ /**
11254
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
11255
+ * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
11256
+ */
11257
+ var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
11258
+ if (force_start || is_sampled) {
11259
+ loadRecorder(true);
10582
11260
  } else {
10583
- handleLoadedRecorder();
11261
+ this._should_load_recorder()
11262
+ .then(function (shouldLoad) {
11263
+ if (shouldLoad) {
11264
+ loadRecorder(false);
11265
+ }
11266
+ });
10584
11267
  }
10585
11268
  });
10586
11269
 
11270
+ MixpanelLib.prototype.start_session_recording = function () {
11271
+ this._check_and_start_session_recording(true);
11272
+ };
11273
+
10587
11274
  MixpanelLib.prototype.stop_session_recording = function () {
10588
11275
  if (this._recorder) {
10589
11276
  this._recorder['stopRecording']();
10590
- } else {
10591
- console$1.critical('Session recorder module not loaded');
11277
+ }
11278
+ };
11279
+
11280
+ MixpanelLib.prototype.pause_session_recording = function () {
11281
+ if (this._recorder) {
11282
+ this._recorder['pauseRecording']();
11283
+ }
11284
+ };
11285
+
11286
+ MixpanelLib.prototype.resume_session_recording = function () {
11287
+ if (this._recorder) {
11288
+ this._recorder['resumeRecording']();
10592
11289
  }
10593
11290
  };
10594
11291
 
@@ -10623,6 +11320,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
10623
11320
  return replay_id || null;
10624
11321
  };
10625
11322
 
11323
+ // "private" public method to reach into the recorder in test cases
11324
+ MixpanelLib.prototype.__get_recorder = function () {
11325
+ return this._recorder;
11326
+ };
11327
+
10626
11328
  // Private methods
10627
11329
 
10628
11330
  MixpanelLib.prototype._loaded = function() {
@@ -10962,7 +11664,8 @@ MixpanelLib.prototype.init_batchers = function() {
10962
11664
  return this._run_hook('before_send_' + attrs.type, item);
10963
11665
  }, this),
10964
11666
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
10965
- usePersistence: true
11667
+ usePersistence: true,
11668
+ enqueueThrottleMs: 10,
10966
11669
  }
10967
11670
  );
10968
11671
  }, this);
@@ -12063,6 +12766,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
12063
12766
 
12064
12767
  if (disabled) {
12065
12768
  this.stop_batch_senders();
12769
+ this.stop_session_recording();
12066
12770
  } else {
12067
12771
  // only start batchers after opt-in if they have previously been started
12068
12772
  // in order to avoid unintentionally starting up batching for the first time
@@ -12303,10 +13007,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
12303
13007
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
12304
13008
  MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
12305
13009
  MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
13010
+ MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
13011
+ MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
12306
13012
  MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
12307
13013
  MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
13014
+ MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
12308
13015
  MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
12309
13016
 
13017
+ // Exports intended only for testing
13018
+ MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
13019
+
12310
13020
  // MixpanelPersistence Exports
12311
13021
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
12312
13022
  MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;