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