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