mixpanel-browser 2.60.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,6 +1,28 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
+ // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
5
+ var win;
6
+ if (typeof(window) === 'undefined') {
7
+ var loc = {
8
+ hostname: ''
9
+ };
10
+ win = {
11
+ navigator: { userAgent: '', onLine: true },
12
+ document: {
13
+ createElement: function() { return {}; },
14
+ location: loc,
15
+ referrer: ''
16
+ },
17
+ screen: { width: 0, height: 0 },
18
+ location: loc,
19
+ addEventListener: function() {},
20
+ removeEventListener: function() {}
21
+ };
22
+ } else {
23
+ win = window;
24
+ }
25
+
4
26
  var NodeType;
5
27
  (function (NodeType) {
6
28
  NodeType[NodeType["Document"] = 0] = "Document";
@@ -4477,64 +4499,6 @@
4477
4499
  };
4478
4500
  record.mirror = mirror;
4479
4501
 
4480
- var EventType = /* @__PURE__ */ ((EventType2) => {
4481
- EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
4482
- EventType2[EventType2["Load"] = 1] = "Load";
4483
- EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
4484
- EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
4485
- EventType2[EventType2["Meta"] = 4] = "Meta";
4486
- EventType2[EventType2["Custom"] = 5] = "Custom";
4487
- EventType2[EventType2["Plugin"] = 6] = "Plugin";
4488
- return EventType2;
4489
- })(EventType || {});
4490
- var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4491
- IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
4492
- IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
4493
- IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
4494
- IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
4495
- IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
4496
- IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
4497
- IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
4498
- IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
4499
- IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
4500
- IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
4501
- IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
4502
- IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
4503
- IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
4504
- IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
4505
- IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
4506
- IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
4507
- IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
4508
- return IncrementalSource2;
4509
- })(IncrementalSource || {});
4510
-
4511
- var Config = {
4512
- DEBUG: false,
4513
- LIB_VERSION: '2.60.0'
4514
- };
4515
-
4516
- // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
4517
- var win;
4518
- if (typeof(window) === 'undefined') {
4519
- var loc = {
4520
- hostname: ''
4521
- };
4522
- win = {
4523
- navigator: { userAgent: '', onLine: true },
4524
- document: {
4525
- createElement: function() { return {}; },
4526
- location: loc,
4527
- referrer: ''
4528
- },
4529
- screen: { width: 0, height: 0 },
4530
- location: loc,
4531
- addEventListener: function() {},
4532
- removeEventListener: function() {}
4533
- };
4534
- } else {
4535
- win = window;
4536
- }
4537
-
4538
4502
  var setImmediate = win['setImmediate'];
4539
4503
  var builtInProp, cycle, schedulingQueue,
4540
4504
  ToString = Object.prototype.toString,
@@ -4897,6 +4861,42 @@
4897
4861
  PromisePolyfill = NpoPromise;
4898
4862
  }
4899
4863
 
4864
+ var EventType = /* @__PURE__ */ ((EventType2) => {
4865
+ EventType2[EventType2["DomContentLoaded"] = 0] = "DomContentLoaded";
4866
+ EventType2[EventType2["Load"] = 1] = "Load";
4867
+ EventType2[EventType2["FullSnapshot"] = 2] = "FullSnapshot";
4868
+ EventType2[EventType2["IncrementalSnapshot"] = 3] = "IncrementalSnapshot";
4869
+ EventType2[EventType2["Meta"] = 4] = "Meta";
4870
+ EventType2[EventType2["Custom"] = 5] = "Custom";
4871
+ EventType2[EventType2["Plugin"] = 6] = "Plugin";
4872
+ return EventType2;
4873
+ })(EventType || {});
4874
+ var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4875
+ IncrementalSource2[IncrementalSource2["Mutation"] = 0] = "Mutation";
4876
+ IncrementalSource2[IncrementalSource2["MouseMove"] = 1] = "MouseMove";
4877
+ IncrementalSource2[IncrementalSource2["MouseInteraction"] = 2] = "MouseInteraction";
4878
+ IncrementalSource2[IncrementalSource2["Scroll"] = 3] = "Scroll";
4879
+ IncrementalSource2[IncrementalSource2["ViewportResize"] = 4] = "ViewportResize";
4880
+ IncrementalSource2[IncrementalSource2["Input"] = 5] = "Input";
4881
+ IncrementalSource2[IncrementalSource2["TouchMove"] = 6] = "TouchMove";
4882
+ IncrementalSource2[IncrementalSource2["MediaInteraction"] = 7] = "MediaInteraction";
4883
+ IncrementalSource2[IncrementalSource2["StyleSheetRule"] = 8] = "StyleSheetRule";
4884
+ IncrementalSource2[IncrementalSource2["CanvasMutation"] = 9] = "CanvasMutation";
4885
+ IncrementalSource2[IncrementalSource2["Font"] = 10] = "Font";
4886
+ IncrementalSource2[IncrementalSource2["Log"] = 11] = "Log";
4887
+ IncrementalSource2[IncrementalSource2["Drag"] = 12] = "Drag";
4888
+ IncrementalSource2[IncrementalSource2["StyleDeclaration"] = 13] = "StyleDeclaration";
4889
+ IncrementalSource2[IncrementalSource2["Selection"] = 14] = "Selection";
4890
+ IncrementalSource2[IncrementalSource2["AdoptedStyleSheet"] = 15] = "AdoptedStyleSheet";
4891
+ IncrementalSource2[IncrementalSource2["CustomElement"] = 16] = "CustomElement";
4892
+ return IncrementalSource2;
4893
+ })(IncrementalSource || {});
4894
+
4895
+ var Config = {
4896
+ DEBUG: false,
4897
+ LIB_VERSION: '2.61.0'
4898
+ };
4899
+
4900
4900
  /* eslint camelcase: "off", eqeqeq: "off" */
4901
4901
 
4902
4902
  // Maximum allowed session recording length
@@ -5924,15 +5924,9 @@
5924
5924
  }
5925
5925
  };
5926
5926
 
5927
- var _localStorageSupported = null;
5928
- var localStorageSupported = function(storage, forceCheck) {
5929
- if (_localStorageSupported !== null && !forceCheck) {
5930
- return _localStorageSupported;
5931
- }
5932
-
5927
+ var _testStorageSupported = function (storage) {
5933
5928
  var supported = true;
5934
5929
  try {
5935
- storage = storage || win.localStorage;
5936
5930
  var key = '__mplss_' + cheap_guid(8),
5937
5931
  val = 'xyz';
5938
5932
  storage.setItem(key, val);
@@ -5943,59 +5937,74 @@
5943
5937
  } catch (err) {
5944
5938
  supported = false;
5945
5939
  }
5946
-
5947
- _localStorageSupported = supported;
5948
5940
  return supported;
5949
5941
  };
5950
5942
 
5951
- // _.localStorage
5952
- _.localStorage = {
5953
- is_supported: function(force_check) {
5954
- var supported = localStorageSupported(null, force_check);
5955
- if (!supported) {
5956
- console$1.error('localStorage unsupported; falling back to cookie store');
5957
- }
5958
- return supported;
5959
- },
5960
-
5961
- error: function(msg) {
5962
- console$1.error('localStorage error: ' + msg);
5963
- },
5943
+ var _localStorageSupported = null;
5944
+ var localStorageSupported = function(storage, forceCheck) {
5945
+ if (_localStorageSupported !== null && !forceCheck) {
5946
+ return _localStorageSupported;
5947
+ }
5948
+ return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
5949
+ };
5964
5950
 
5965
- get: function(name) {
5966
- try {
5967
- return win.localStorage.getItem(name);
5968
- } catch (err) {
5969
- _.localStorage.error(err);
5970
- }
5971
- return null;
5972
- },
5951
+ var _sessionStorageSupported = null;
5952
+ var sessionStorageSupported = function(storage, forceCheck) {
5953
+ if (_sessionStorageSupported !== null && !forceCheck) {
5954
+ return _sessionStorageSupported;
5955
+ }
5956
+ return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
5957
+ };
5973
5958
 
5974
- parse: function(name) {
5975
- try {
5976
- return _.JSONDecode(_.localStorage.get(name)) || {};
5977
- } catch (err) {
5978
- // noop
5979
- }
5980
- return null;
5981
- },
5959
+ function _storageWrapper(storage, name, is_supported_fn) {
5960
+ var log_error = function(msg) {
5961
+ console$1.error(name + ' error: ' + msg);
5962
+ };
5982
5963
 
5983
- set: function(name, value) {
5984
- try {
5985
- win.localStorage.setItem(name, value);
5986
- } catch (err) {
5987
- _.localStorage.error(err);
5964
+ return {
5965
+ is_supported: function(forceCheck) {
5966
+ var supported = is_supported_fn(storage, forceCheck);
5967
+ if (!supported) {
5968
+ console$1.error(name + ' unsupported');
5969
+ }
5970
+ return supported;
5971
+ },
5972
+ error: log_error,
5973
+ get: function(key) {
5974
+ try {
5975
+ return storage.getItem(key);
5976
+ } catch (err) {
5977
+ log_error(err);
5978
+ }
5979
+ return null;
5980
+ },
5981
+ parse: function(key) {
5982
+ try {
5983
+ return _.JSONDecode(storage.getItem(key)) || {};
5984
+ } catch (err) {
5985
+ // noop
5986
+ }
5987
+ return null;
5988
+ },
5989
+ set: function(key, value) {
5990
+ try {
5991
+ storage.setItem(key, value);
5992
+ } catch (err) {
5993
+ log_error(err);
5994
+ }
5995
+ },
5996
+ remove: function(key) {
5997
+ try {
5998
+ storage.removeItem(key);
5999
+ } catch (err) {
6000
+ log_error(err);
6001
+ }
5988
6002
  }
5989
- },
6003
+ };
6004
+ }
5990
6005
 
5991
- remove: function(name) {
5992
- try {
5993
- win.localStorage.removeItem(name);
5994
- } catch (err) {
5995
- _.localStorage.error(err);
5996
- }
5997
- }
5998
- };
6006
+ _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
6007
+ _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
5999
6008
 
6000
6009
  _.register_event = (function() {
6001
6010
  // written by Dean Edwards, 2005
@@ -6522,6 +6531,31 @@
6522
6531
  }
6523
6532
  };
6524
6533
 
6534
+ /**
6535
+ * Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
6536
+ * Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
6537
+ */
6538
+ var batchedThrottle = function (fn, waitMs) {
6539
+ var timeoutPromise = null;
6540
+ var throttledItems = [];
6541
+ return function (item) {
6542
+ var self = this;
6543
+ throttledItems.push(item);
6544
+
6545
+ if (!timeoutPromise) {
6546
+ timeoutPromise = new PromisePolyfill(function (resolve) {
6547
+ setTimeout(function () {
6548
+ var returnValue = fn.apply(self, [throttledItems]);
6549
+ timeoutPromise = null;
6550
+ throttledItems = [];
6551
+ resolve(returnValue);
6552
+ }, waitMs);
6553
+ });
6554
+ }
6555
+ return timeoutPromise;
6556
+ };
6557
+ };
6558
+
6525
6559
  var cheap_guid = function(maxlen) {
6526
6560
  var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
6527
6561
  return maxlen ? guid.substring(0, maxlen) : guid;
@@ -6564,6 +6598,8 @@
6564
6598
  return _.isUndefined(onLine) || onLine;
6565
6599
  };
6566
6600
 
6601
+ var NOOP_FUNC = function () {};
6602
+
6567
6603
  var JSONStringify = null, JSONParse = null;
6568
6604
  if (typeof JSON !== 'undefined') {
6569
6605
  JSONStringify = JSON.stringify;
@@ -6572,20 +6608,143 @@
6572
6608
  JSONStringify = JSONStringify || _.JSONEncode;
6573
6609
  JSONParse = JSONParse || _.JSONDecode;
6574
6610
 
6575
- // EXPORTS (for closure compiler)
6576
- _['toArray'] = _.toArray;
6577
- _['isObject'] = _.isObject;
6578
- _['JSONEncode'] = _.JSONEncode;
6579
- _['JSONDecode'] = _.JSONDecode;
6580
- _['isBlockedUA'] = _.isBlockedUA;
6581
- _['isEmptyObject'] = _.isEmptyObject;
6611
+ // UNMINIFIED EXPORTS (for closure compiler)
6582
6612
  _['info'] = _.info;
6583
- _['info']['device'] = _.info.device;
6584
6613
  _['info']['browser'] = _.info.browser;
6585
6614
  _['info']['browserVersion'] = _.info.browserVersion;
6615
+ _['info']['device'] = _.info.device;
6586
6616
  _['info']['properties'] = _.info.properties;
6617
+ _['isBlockedUA'] = _.isBlockedUA;
6618
+ _['isEmptyObject'] = _.isEmptyObject;
6619
+ _['isObject'] = _.isObject;
6620
+ _['JSONDecode'] = _.JSONDecode;
6621
+ _['JSONEncode'] = _.JSONEncode;
6622
+ _['toArray'] = _.toArray;
6587
6623
  _['NPO'] = NpoPromise;
6588
6624
 
6625
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
6626
+
6627
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
6628
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
6629
+
6630
+ // note: increment the version number when adding new object stores
6631
+ var DB_VERSION = 1;
6632
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
6633
+
6634
+ /**
6635
+ * @type {import('./wrapper').StorageWrapper}
6636
+ */
6637
+ var IDBStorageWrapper = function (storeName) {
6638
+ /**
6639
+ * @type {Promise<IDBDatabase>|null}
6640
+ */
6641
+ this.dbPromise = null;
6642
+ this.storeName = storeName;
6643
+ };
6644
+
6645
+ IDBStorageWrapper.prototype._openDb = function () {
6646
+ return new PromisePolyfill(function (resolve, reject) {
6647
+ var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
6648
+ openRequest['onerror'] = function () {
6649
+ reject(openRequest.error);
6650
+ };
6651
+
6652
+ openRequest['onsuccess'] = function () {
6653
+ resolve(openRequest.result);
6654
+ };
6655
+
6656
+ openRequest['onupgradeneeded'] = function (ev) {
6657
+ var db = ev.target.result;
6658
+
6659
+ OBJECT_STORES.forEach(function (storeName) {
6660
+ db.createObjectStore(storeName);
6661
+ });
6662
+ };
6663
+ });
6664
+ };
6665
+
6666
+ IDBStorageWrapper.prototype.init = function () {
6667
+ if (!win.indexedDB) {
6668
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
6669
+ }
6670
+
6671
+ if (!this.dbPromise) {
6672
+ this.dbPromise = this._openDb();
6673
+ }
6674
+
6675
+ return this.dbPromise
6676
+ .then(function (dbOrError) {
6677
+ if (dbOrError instanceof win['IDBDatabase']) {
6678
+ return PromisePolyfill.resolve();
6679
+ } else {
6680
+ return PromisePolyfill.reject(dbOrError);
6681
+ }
6682
+ });
6683
+ };
6684
+
6685
+ /**
6686
+ * @param {IDBTransactionMode} mode
6687
+ * @param {function(IDBObjectStore): void} storeCb
6688
+ */
6689
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
6690
+ var storeName = this.storeName;
6691
+ var doTransaction = function (db) {
6692
+ return new PromisePolyfill(function (resolve, reject) {
6693
+ var transaction = db.transaction(storeName, mode);
6694
+ transaction.oncomplete = function () {
6695
+ resolve(transaction);
6696
+ };
6697
+ transaction.onabort = transaction.onerror = function () {
6698
+ reject(transaction.error);
6699
+ };
6700
+
6701
+ storeCb(transaction.objectStore(storeName));
6702
+ });
6703
+ };
6704
+
6705
+ return this.dbPromise
6706
+ .then(doTransaction)
6707
+ .catch(function (err) {
6708
+ if (err['name'] === 'InvalidStateError') {
6709
+ // try reopening the DB if the connection is closed
6710
+ this.dbPromise = this._openDb();
6711
+ return this.dbPromise.then(doTransaction);
6712
+ } else {
6713
+ return PromisePolyfill.reject(err);
6714
+ }
6715
+ }.bind(this));
6716
+ };
6717
+
6718
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
6719
+ return this.makeTransaction('readwrite', function (objectStore) {
6720
+ objectStore.put(value, key);
6721
+ });
6722
+ };
6723
+
6724
+ IDBStorageWrapper.prototype.getItem = function (key) {
6725
+ var req;
6726
+ return this.makeTransaction('readonly', function (objectStore) {
6727
+ req = objectStore.get(key);
6728
+ }).then(function () {
6729
+ return req.result;
6730
+ });
6731
+ };
6732
+
6733
+ IDBStorageWrapper.prototype.removeItem = function (key) {
6734
+ return this.makeTransaction('readwrite', function (objectStore) {
6735
+ objectStore.delete(key);
6736
+ });
6737
+ };
6738
+
6739
+ IDBStorageWrapper.prototype.getAll = function () {
6740
+ var req;
6741
+ return this.makeTransaction('readonly', function (objectStore) {
6742
+ req = objectStore.getAll();
6743
+ }).then(function () {
6744
+ return req.result;
6745
+ });
6746
+ };
6747
+
6589
6748
  /**
6590
6749
  * GDPR utils
6591
6750
  *
@@ -6779,7 +6938,7 @@
6779
6938
  options = options || {};
6780
6939
 
6781
6940
  this.storageKey = key;
6782
- this.storage = options.storage || window.localStorage;
6941
+ this.storage = options.storage || win.localStorage;
6783
6942
  this.pollIntervalMS = options.pollIntervalMS || 100;
6784
6943
  this.timeoutMS = options.timeoutMS || 2000;
6785
6944
 
@@ -6794,7 +6953,6 @@
6794
6953
  return new Promise(_.bind(function (resolve, reject) {
6795
6954
  var i = pid || (new Date().getTime() + '|' + Math.random());
6796
6955
  var startTime = new Date().getTime();
6797
-
6798
6956
  var key = this.storageKey;
6799
6957
  var pollIntervalMS = this.pollIntervalMS;
6800
6958
  var timeoutMS = this.timeoutMS;
@@ -6905,11 +7063,7 @@
6905
7063
  };
6906
7064
 
6907
7065
  /**
6908
- * @typedef {import('./wrapper').StorageWrapper}
6909
- */
6910
-
6911
- /**
6912
- * @type {StorageWrapper}
7066
+ * @type {import('./wrapper').StorageWrapper}
6913
7067
  */
6914
7068
  var LocalStorageWrapper = function (storageOverride) {
6915
7069
  this.storage = storageOverride || localStorage;
@@ -6922,7 +7076,7 @@
6922
7076
  LocalStorageWrapper.prototype.setItem = function (key, value) {
6923
7077
  return new PromisePolyfill(_.bind(function (resolve, reject) {
6924
7078
  try {
6925
- this.storage.setItem(key, value);
7079
+ this.storage.setItem(key, JSONStringify(value));
6926
7080
  } catch (e) {
6927
7081
  reject(e);
6928
7082
  }
@@ -6934,7 +7088,7 @@
6934
7088
  return new PromisePolyfill(_.bind(function (resolve, reject) {
6935
7089
  var item;
6936
7090
  try {
6937
- item = this.storage.getItem(key);
7091
+ item = JSONParse(this.storage.getItem(key));
6938
7092
  } catch (e) {
6939
7093
  reject(e);
6940
7094
  }
@@ -6977,8 +7131,10 @@
6977
7131
  this.usePersistence = options.usePersistence;
6978
7132
  if (this.usePersistence) {
6979
7133
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
6980
- this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
6981
- this.queueStorage.init();
7134
+ this.lock = new SharedLock(storageKey, {
7135
+ storage: options.sharedLockStorage || win.localStorage,
7136
+ timeoutMS: options.sharedLockTimeoutMS,
7137
+ });
6982
7138
  }
6983
7139
  this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
6984
7140
 
@@ -6986,6 +7142,14 @@
6986
7142
 
6987
7143
  this.memQueue = [];
6988
7144
  this.initialized = false;
7145
+
7146
+ if (options.enqueueThrottleMs) {
7147
+ this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
7148
+ } else {
7149
+ this.enqueuePersisted = _.bind(function (queueEntry) {
7150
+ return this._enqueuePersisted([queueEntry]);
7151
+ }, this);
7152
+ }
6989
7153
  };
6990
7154
 
6991
7155
  RequestQueue.prototype.ensureInit = function () {
@@ -7028,36 +7192,39 @@
7028
7192
  this.memQueue.push(queueEntry);
7029
7193
  return PromisePolyfill.resolve(true);
7030
7194
  } else {
7195
+ return this.enqueuePersisted(queueEntry);
7196
+ }
7197
+ };
7031
7198
 
7032
- var enqueueItem = _.bind(function () {
7033
- return this.ensureInit()
7034
- .then(_.bind(function () {
7035
- return this.readFromStorage();
7036
- }, this))
7037
- .then(_.bind(function (storedQueue) {
7038
- storedQueue.push(queueEntry);
7039
- return this.saveToStorage(storedQueue);
7040
- }, this))
7041
- .then(_.bind(function (succeeded) {
7042
- // only add to in-memory queue when storage succeeds
7043
- if (succeeded) {
7044
- this.memQueue.push(queueEntry);
7045
- }
7046
- return succeeded;
7047
- }, this))
7048
- .catch(_.bind(function (err) {
7049
- this.reportError('Error enqueueing item', err, item);
7050
- return false;
7051
- }, this));
7052
- }, this);
7199
+ RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
7200
+ var enqueueItem = _.bind(function () {
7201
+ return this.ensureInit()
7202
+ .then(_.bind(function () {
7203
+ return this.readFromStorage();
7204
+ }, this))
7205
+ .then(_.bind(function (storedQueue) {
7206
+ return this.saveToStorage(storedQueue.concat(queueEntries));
7207
+ }, this))
7208
+ .then(_.bind(function (succeeded) {
7209
+ // only add to in-memory queue when storage succeeds
7210
+ if (succeeded) {
7211
+ this.memQueue = this.memQueue.concat(queueEntries);
7212
+ }
7053
7213
 
7054
- return this.lock
7055
- .withLock(enqueueItem, this.pid)
7214
+ return succeeded;
7215
+ }, this))
7056
7216
  .catch(_.bind(function (err) {
7057
- this.reportError('Error acquiring storage lock', err);
7217
+ this.reportError('Error enqueueing items', err, queueEntries);
7058
7218
  return false;
7059
7219
  }, this));
7060
- }
7220
+ }, this);
7221
+
7222
+ return this.lock
7223
+ .withLock(enqueueItem, this.pid)
7224
+ .catch(_.bind(function (err) {
7225
+ this.reportError('Error acquiring storage lock', err);
7226
+ return false;
7227
+ }, this));
7061
7228
  };
7062
7229
 
7063
7230
  /**
@@ -7078,7 +7245,7 @@
7078
7245
  }, this))
7079
7246
  .then(_.bind(function (storedQueue) {
7080
7247
  if (storedQueue.length) {
7081
- // item IDs already in batch; don't duplicate out of storage
7248
+ // item IDs already in batch; don't duplicate out of storage
7082
7249
  var idsInBatch = {}; // poor man's Set
7083
7250
  _.each(batch, function (item) {
7084
7251
  idsInBatch[item['id']] = true;
@@ -7165,7 +7332,7 @@
7165
7332
  .withLock(removeFromStorage, this.pid)
7166
7333
  .catch(_.bind(function (err) {
7167
7334
  this.reportError('Error acquiring storage lock', err);
7168
- if (!localStorageSupported(this.queueStorage.storage, true)) {
7335
+ if (!localStorageSupported(this.lock.storage, true)) {
7169
7336
  // Looks like localStorage writes have stopped working sometime after
7170
7337
  // initialization (probably full), and so nobody can acquire locks
7171
7338
  // anymore. Consider it temporarily safe to remove items without the
@@ -7253,7 +7420,6 @@
7253
7420
  }, this))
7254
7421
  .then(_.bind(function (storageEntry) {
7255
7422
  if (storageEntry) {
7256
- storageEntry = JSONParse(storageEntry);
7257
7423
  if (!_.isArray(storageEntry)) {
7258
7424
  this.reportError('Invalid storage entry:', storageEntry);
7259
7425
  storageEntry = null;
@@ -7271,16 +7437,9 @@
7271
7437
  * Serialize the given items array to localStorage.
7272
7438
  */
7273
7439
  RequestQueue.prototype.saveToStorage = function (queue) {
7274
- try {
7275
- var serialized = JSONStringify(queue);
7276
- } catch (err) {
7277
- this.reportError('Error serializing queue', err);
7278
- return PromisePolyfill.resolve(false);
7279
- }
7280
-
7281
7440
  return this.ensureInit()
7282
7441
  .then(_.bind(function () {
7283
- return this.queueStorage.setItem(this.storageKey, serialized);
7442
+ return this.queueStorage.setItem(this.storageKey, queue);
7284
7443
  }, this))
7285
7444
  .then(function () {
7286
7445
  return true;
@@ -7324,7 +7483,9 @@
7324
7483
  errorReporter: _.bind(this.reportError, this),
7325
7484
  queueStorage: options.queueStorage,
7326
7485
  sharedLockStorage: options.sharedLockStorage,
7327
- usePersistence: options.usePersistence
7486
+ sharedLockTimeoutMS: options.sharedLockTimeoutMS,
7487
+ usePersistence: options.usePersistence,
7488
+ enqueueThrottleMs: options.enqueueThrottleMs
7328
7489
  });
7329
7490
 
7330
7491
  this.libConfig = options.libConfig;
@@ -7346,6 +7507,8 @@
7346
7507
  // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
7347
7508
  // in a request loop and get ratelimited by the server.
7348
7509
  this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
7510
+
7511
+ this._flushPromise = null;
7349
7512
  };
7350
7513
 
7351
7514
  /**
@@ -7405,7 +7568,7 @@
7405
7568
  if (!this.stopped) { // don't schedule anymore if batching has been stopped
7406
7569
  this.timeoutID = setTimeout(_.bind(function() {
7407
7570
  if (!this.stopped) {
7408
- this.flush();
7571
+ this._flushPromise = this.flush();
7409
7572
  }
7410
7573
  }, this), this.flushInterval);
7411
7574
  }
@@ -7637,6 +7800,17 @@
7637
7800
  }
7638
7801
  };
7639
7802
 
7803
+ /**
7804
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
7805
+ * @returns {boolean}
7806
+ */
7807
+ var isRecordingExpired = function(serializedRecording) {
7808
+ var now = Date.now();
7809
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
7810
+ };
7811
+
7812
+ var RECORD_ENQUEUE_THROTTLE_MS = 250;
7813
+
7640
7814
  var logger$1 = console_with_prefix('recorder');
7641
7815
  var CompressionStream = win['CompressionStream'];
7642
7816
 
@@ -7663,29 +7837,58 @@
7663
7837
  return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
7664
7838
  }
7665
7839
 
7840
+ /**
7841
+ * @typedef {Object} SerializedRecording
7842
+ * @property {number} idleExpires
7843
+ * @property {number} maxExpires
7844
+ * @property {number} replayStartTime
7845
+ * @property {number} seqNo
7846
+ * @property {string} batchStartUrl
7847
+ * @property {string} replayId
7848
+ * @property {string} tabId
7849
+ * @property {string} replayStartUrl
7850
+ */
7851
+
7852
+ /**
7853
+ * @typedef {Object} SessionRecordingOptions
7854
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
7855
+ * @property {String} [options.replayId] - unique uuid for a single replay
7856
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
7857
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
7858
+ * @property {Function} [options.rrwebRecord] - rrweb's `record` function
7859
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
7860
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
7861
+ * optional properties for deserialization:
7862
+ * @property {number} idleExpires
7863
+ * @property {number} maxExpires
7864
+ * @property {number} replayStartTime
7865
+ * @property {number} seqNo
7866
+ * @property {string} batchStartUrl
7867
+ * @property {string} replayStartUrl
7868
+ */
7869
+
7870
+
7666
7871
  /**
7667
7872
  * This class encapsulates a single session recording and its lifecycle.
7668
- * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
7669
- * @param {String} [options.replayId] - unique uuid for a single replay
7670
- * @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
7671
- * @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
7672
- * @param {Function} [options.rrwebRecord] - rrweb's `record` function
7873
+ * @param {SessionRecordingOptions} options
7673
7874
  */
7674
7875
  var SessionRecording = function(options) {
7675
7876
  this._mixpanel = options.mixpanelInstance;
7676
- this._onIdleTimeout = options.onIdleTimeout;
7677
- this._onMaxLengthReached = options.onMaxLengthReached;
7678
- this._rrwebRecord = options.rrwebRecord;
7679
-
7680
- this.replayId = options.replayId;
7877
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
7878
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
7879
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
7880
+ this._rrwebRecord = options.rrwebRecord || null;
7681
7881
 
7682
7882
  // internal rrweb stopRecording function
7683
7883
  this._stopRecording = null;
7884
+ this.replayId = options.replayId;
7684
7885
 
7685
- this.seqNo = 0;
7686
- this.replayStartTime = null;
7687
- this.replayStartUrl = null;
7688
- this.batchStartUrl = null;
7886
+ this.batchStartUrl = options.batchStartUrl || null;
7887
+ this.replayStartUrl = options.replayStartUrl || null;
7888
+ this.idleExpires = options.idleExpires || null;
7889
+ this.maxExpires = options.maxExpires || null;
7890
+ this.replayStartTime = options.replayStartTime || null;
7891
+ this.seqNo = options.seqNo || 0;
7689
7892
 
7690
7893
  this.idleTimeoutId = null;
7691
7894
  this.maxTimeoutId = null;
@@ -7693,18 +7896,40 @@
7693
7896
  this.recordMaxMs = MAX_RECORDING_MS;
7694
7897
  this.recordMinMs = 0;
7695
7898
 
7899
+ // disable persistence if localStorage is not supported
7900
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
7901
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true);
7902
+
7696
7903
  // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
7697
7904
  // this will be important when persistence is introduced
7698
- var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
7699
- this.batcher = new RequestBatcher(batcherKey, {
7700
- errorReporter: _.bind(this.reportError, this),
7905
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
7906
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
7907
+ this.batcher = new RequestBatcher(this.batcherKey, {
7908
+ errorReporter: this.reportError.bind(this),
7701
7909
  flushOnlyOnInterval: true,
7702
7910
  libConfig: RECORDER_BATCHER_LIB_CONFIG,
7703
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
7704
- usePersistence: false
7911
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
7912
+ queueStorage: this.queueStorage,
7913
+ sharedLockStorage: options.sharedLockStorage,
7914
+ usePersistence: usePersistence,
7915
+ stopAllBatchingFunc: this.stopRecording.bind(this),
7916
+
7917
+ // increased throttle and shared lock timeout because recording events are very high frequency.
7918
+ // this will minimize the amount of lock contention between enqueued events.
7919
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
7920
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
7921
+ sharedLockTimeoutMS: 10 * 1000,
7705
7922
  });
7706
7923
  };
7707
7924
 
7925
+ SessionRecording.prototype.unloadPersistedData = function () {
7926
+ this.batcher.stop();
7927
+ return this.batcher.flush()
7928
+ .then(function () {
7929
+ return this.queueStorage.removeItem(this.batcherKey);
7930
+ }.bind(this));
7931
+ };
7932
+
7708
7933
  SessionRecording.prototype.getConfig = function(configVar) {
7709
7934
  return this._mixpanel.get_config(configVar);
7710
7935
  };
@@ -7717,6 +7942,11 @@
7717
7942
  };
7718
7943
 
7719
7944
  SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7945
+ if (this._rrwebRecord === null) {
7946
+ this.reportError('rrweb record function not provided. ');
7947
+ return;
7948
+ }
7949
+
7720
7950
  if (this._stopRecording !== null) {
7721
7951
  logger$1.log('Recording already in progress, skipping startRecording.');
7722
7952
  return;
@@ -7728,15 +7958,21 @@
7728
7958
  logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7729
7959
  }
7730
7960
 
7961
+ if (!this.maxExpires) {
7962
+ this.maxExpires = new Date().getTime() + this.recordMaxMs;
7963
+ }
7964
+
7731
7965
  this.recordMinMs = this.getConfig('record_min_ms');
7732
7966
  if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7733
7967
  this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7734
7968
  logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7735
7969
  }
7736
7970
 
7737
- this.replayStartTime = new Date().getTime();
7738
- this.batchStartUrl = _.info.currentUrl();
7739
- this.replayStartUrl = _.info.currentUrl();
7971
+ if (!this.replayStartTime) {
7972
+ this.replayStartTime = new Date().getTime();
7973
+ this.batchStartUrl = _.info.currentUrl();
7974
+ this.replayStartUrl = _.info.currentUrl();
7975
+ }
7740
7976
 
7741
7977
  if (shouldStopBatcher || this.recordMinMs > 0) {
7742
7978
  // the primary case for shouldStopBatcher is when we're starting recording after a reset
@@ -7749,10 +7985,12 @@
7749
7985
  this.batcher.start();
7750
7986
  }
7751
7987
 
7752
- var resetIdleTimeout = _.bind(function () {
7988
+ var resetIdleTimeout = function () {
7753
7989
  clearTimeout(this.idleTimeoutId);
7754
- this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
7755
- }, this);
7990
+ var idleTimeoutMs = this.getConfig('record_idle_timeout_ms');
7991
+ this.idleTimeoutId = setTimeout(this._onIdleTimeout, idleTimeoutMs);
7992
+ this.idleExpires = new Date().getTime() + idleTimeoutMs;
7993
+ }.bind(this);
7756
7994
 
7757
7995
  var blockSelector = this.getConfig('record_block_selector');
7758
7996
  if (blockSelector === '' || blockSelector === null) {
@@ -7760,8 +7998,7 @@
7760
7998
  }
7761
7999
 
7762
8000
  this._stopRecording = this._rrwebRecord({
7763
- 'emit': _.bind(function (ev) {
7764
- this.batcher.enqueue(ev);
8001
+ 'emit': addOptOutCheckMixpanelLib(function (ev) {
7765
8002
  if (isUserEvent(ev)) {
7766
8003
  if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
7767
8004
  // start flushing again after user activity
@@ -7769,7 +8006,10 @@
7769
8006
  }
7770
8007
  resetIdleTimeout();
7771
8008
  }
7772
- }, this),
8009
+
8010
+ // promise only used to await during tests
8011
+ this.__enqueuePromise = this.batcher.enqueue(ev);
8012
+ }.bind(this)),
7773
8013
  'blockClass': this.getConfig('record_block_class'),
7774
8014
  'blockSelector': blockSelector,
7775
8015
  'collectFonts': this.getConfig('record_collect_fonts'),
@@ -7795,10 +8035,11 @@
7795
8035
 
7796
8036
  resetIdleTimeout();
7797
8037
 
7798
- this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
8038
+ var maxTimeoutMs = this.maxExpires - new Date().getTime();
8039
+ this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
7799
8040
  };
7800
8041
 
7801
- SessionRecording.prototype.stopRecording = function () {
8042
+ SessionRecording.prototype.stopRecording = function (skipFlush) {
7802
8043
  if (!this.isRrwebStopped()) {
7803
8044
  try {
7804
8045
  this._stopRecording();
@@ -7808,17 +8049,19 @@
7808
8049
  this._stopRecording = null;
7809
8050
  }
7810
8051
 
8052
+ var flushPromise;
7811
8053
  if (this.batcher.stopped) {
7812
8054
  // never got user activity to flush after reset, so just clear the batcher
7813
- this.batcher.clear();
7814
- } else {
8055
+ flushPromise = this.batcher.clear();
8056
+ } else if (!skipFlush) {
7815
8057
  // flush any remaining events from running batcher
7816
- this.batcher.flush();
7817
- this.batcher.stop();
8058
+ flushPromise = this.batcher.flush();
7818
8059
  }
8060
+ this.batcher.stop();
7819
8061
 
7820
8062
  clearTimeout(this.idleTimeoutId);
7821
8063
  clearTimeout(this.maxTimeoutId);
8064
+ return flushPromise;
7822
8065
  };
7823
8066
 
7824
8067
  SessionRecording.prototype.isRrwebStopped = function () {
@@ -7830,7 +8073,54 @@
7830
8073
  * we stop recording and dump any queued events if the user has opted out.
7831
8074
  */
7832
8075
  SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
7833
- this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
8076
+ this._flushEvents(data, options, cb, this._onOptOut.bind(this));
8077
+ };
8078
+
8079
+ /**
8080
+ * @returns {SerializedRecording}
8081
+ */
8082
+ SessionRecording.prototype.serialize = function () {
8083
+ // don't break if mixpanel instance was destroyed at some point
8084
+ var tabId;
8085
+ try {
8086
+ tabId = this._mixpanel.get_tab_id();
8087
+ } catch (e) {
8088
+ this.reportError('Error getting tab ID for serialization ', e);
8089
+ tabId = null;
8090
+ }
8091
+
8092
+ return {
8093
+ 'replayId': this.replayId,
8094
+ 'seqNo': this.seqNo,
8095
+ 'replayStartTime': this.replayStartTime,
8096
+ 'batchStartUrl': this.batchStartUrl,
8097
+ 'replayStartUrl': this.replayStartUrl,
8098
+ 'idleExpires': this.idleExpires,
8099
+ 'maxExpires': this.maxExpires,
8100
+ 'tabId': tabId,
8101
+ };
8102
+ };
8103
+
8104
+
8105
+ /**
8106
+ * @static
8107
+ * @param {SerializedRecording} serializedRecording
8108
+ * @param {SessionRecordingOptions} options
8109
+ * @returns {SessionRecording}
8110
+ */
8111
+ SessionRecording.deserialize = function (serializedRecording, options) {
8112
+ var recording = new SessionRecording(_.extend({}, options, {
8113
+ replayId: serializedRecording['replayId'],
8114
+ batchStartUrl: serializedRecording['batchStartUrl'],
8115
+ replayStartUrl: serializedRecording['replayStartUrl'],
8116
+ idleExpires: serializedRecording['idleExpires'],
8117
+ maxExpires: serializedRecording['maxExpires'],
8118
+ replayStartTime: serializedRecording['replayStartTime'],
8119
+ seqNo: serializedRecording['seqNo'],
8120
+ sharedLockStorage: options.sharedLockStorage,
8121
+ }));
8122
+
8123
+ return recording;
7834
8124
  };
7835
8125
 
7836
8126
  SessionRecording.prototype._onOptOut = function (code) {
@@ -7841,7 +8131,7 @@
7841
8131
  };
7842
8132
 
7843
8133
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
7844
- var onSuccess = _.bind(function (response, responseBody) {
8134
+ var onSuccess = function (response, responseBody) {
7845
8135
  // Update batch specific props only if the request was successful to guarantee ordering.
7846
8136
  // RequestBatcher will always flush the next batch after the previous one succeeds.
7847
8137
  // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
@@ -7849,13 +8139,15 @@
7849
8139
  this.seqNo++;
7850
8140
  this.batchStartUrl = _.info.currentUrl();
7851
8141
  }
8142
+
8143
+ this._onBatchSent();
7852
8144
  callback({
7853
8145
  status: 0,
7854
8146
  httpStatusCode: response.status,
7855
8147
  responseBody: responseBody,
7856
8148
  retryAfter: response.headers.get('Retry-After')
7857
8149
  });
7858
- }, this);
8150
+ }.bind(this);
7859
8151
 
7860
8152
  win['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
7861
8153
  'method': 'POST',
@@ -7876,7 +8168,7 @@
7876
8168
  };
7877
8169
 
7878
8170
  SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
7879
- const numEvents = data.length;
8171
+ var numEvents = data.length;
7880
8172
 
7881
8173
  if (numEvents > 0) {
7882
8174
  var replayId = this.replayId;
@@ -7921,10 +8213,10 @@
7921
8213
  var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
7922
8214
  new Response(gzipStream)
7923
8215
  .blob()
7924
- .then(_.bind(function(compressedBlob) {
8216
+ .then(function(compressedBlob) {
7925
8217
  reqParams['format'] = 'gzip';
7926
8218
  this._sendRequest(replayId, reqParams, compressedBlob, callback);
7927
- }, this));
8219
+ }.bind(this));
7928
8220
  } else {
7929
8221
  reqParams['format'] = 'body';
7930
8222
  this._sendRequest(replayId, reqParams, eventsJson, callback);
@@ -7945,54 +8237,208 @@
7945
8237
  }
7946
8238
  };
7947
8239
 
8240
+ /**
8241
+ * Module for handling the storage and retrieval of recording metadata as well as any active recordings.
8242
+ * Makes sure that only one tab can be recording at a time.
8243
+ */
8244
+ var RecordingRegistry = function (options) {
8245
+ this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
8246
+ this.errorReporter = options.errorReporter;
8247
+ this.mixpanelInstance = options.mixpanelInstance;
8248
+ this.sharedLockStorage = options.sharedLockStorage;
8249
+ };
8250
+
8251
+ RecordingRegistry.prototype.handleError = function (err) {
8252
+ this.errorReporter('IndexedDB error: ', err);
8253
+ };
8254
+
8255
+ /**
8256
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
8257
+ */
8258
+ RecordingRegistry.prototype.setActiveRecording = function (serializedRecording) {
8259
+ var tabId = serializedRecording['tabId'];
8260
+ if (!tabId) {
8261
+ console.warn('No tab ID is set, cannot persist recording metadata.');
8262
+ return PromisePolyfill.resolve();
8263
+ }
8264
+
8265
+ return this.idb.init()
8266
+ .then(function () {
8267
+ return this.idb.setItem(tabId, serializedRecording);
8268
+ }.bind(this))
8269
+ .catch(this.handleError.bind(this));
8270
+ };
8271
+
8272
+ /**
8273
+ * @returns {Promise<import('./session-recording').SerializedRecording>}
8274
+ */
8275
+ RecordingRegistry.prototype.getActiveRecording = function () {
8276
+ return this.idb.init()
8277
+ .then(function () {
8278
+ return this.idb.getItem(this.mixpanelInstance.get_tab_id());
8279
+ }.bind(this))
8280
+ .then(function (serializedRecording) {
8281
+ return isRecordingExpired(serializedRecording) ? null : serializedRecording;
8282
+ }.bind(this))
8283
+ .catch(this.handleError.bind(this));
8284
+ };
8285
+
8286
+ RecordingRegistry.prototype.clearActiveRecording = function () {
8287
+ // mark recording as expired instead of deleting it in case the page unloads mid-flush and doesn't make it to ingestion.
8288
+ // this will ensure the next pageload will flush the remaining events, but not try to continue the recording.
8289
+ return this.getActiveRecording()
8290
+ .then(function (serializedRecording) {
8291
+ if (serializedRecording) {
8292
+ serializedRecording['maxExpires'] = 0;
8293
+ return this.setActiveRecording(serializedRecording);
8294
+ }
8295
+ }.bind(this))
8296
+ .catch(this.handleError.bind(this));
8297
+ };
8298
+
8299
+ /**
8300
+ * Flush any inactive recordings from the registry to minimize data loss.
8301
+ * The main idea here is that we can flush remaining rrweb events on the next page load if a tab is closed mid-batch.
8302
+ */
8303
+ RecordingRegistry.prototype.flushInactiveRecordings = function () {
8304
+ return this.idb.init()
8305
+ .then(function() {
8306
+ return this.idb.getAll();
8307
+ }.bind(this))
8308
+ .then(function (serializedRecordings) {
8309
+ // clean up any expired recordings from the registry, non-expired ones may be active in other tabs
8310
+ var unloadPromises = serializedRecordings
8311
+ .filter(function (serializedRecording) {
8312
+ return isRecordingExpired(serializedRecording);
8313
+ })
8314
+ .map(function (serializedRecording) {
8315
+ var sessionRecording = SessionRecording.deserialize(serializedRecording, {
8316
+ mixpanelInstance: this.mixpanelInstance,
8317
+ sharedLockStorage: this.sharedLockStorage
8318
+ });
8319
+ return sessionRecording.unloadPersistedData()
8320
+ .then(function () {
8321
+ // expired recording was successfully flushed, we can clean it up from the registry
8322
+ return this.idb.removeItem(serializedRecording['tabId']);
8323
+ }.bind(this))
8324
+ .catch(this.handleError.bind(this));
8325
+ }.bind(this));
8326
+
8327
+ return PromisePolyfill.all(unloadPromises);
8328
+ }.bind(this))
8329
+ .catch(this.handleError.bind(this));
8330
+ };
8331
+
7948
8332
  var logger = console_with_prefix('recorder');
7949
8333
 
7950
8334
  /**
7951
- * Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
8335
+ * Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
7952
8336
  * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
7953
- */
7954
- var MixpanelRecorder = function(mixpanelInstance) {
7955
- this._mixpanel = mixpanelInstance;
8337
+ */
8338
+ var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage) {
8339
+ this.mixpanelInstance = mixpanelInstance;
8340
+ this.rrwebRecord = rrwebRecord || record;
8341
+ this.sharedLockStorage = sharedLockStorage;
8342
+
8343
+ /**
8344
+ * @member {import('./registry').RecordingRegistry}
8345
+ */
8346
+ this.recordingRegistry = new RecordingRegistry({
8347
+ mixpanelInstance: this.mixpanelInstance,
8348
+ errorReporter: logger.error,
8349
+ sharedLockStorage: sharedLockStorage
8350
+ });
8351
+ this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
8352
+
7956
8353
  this.activeRecording = null;
7957
8354
  };
7958
8355
 
7959
- MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
8356
+ MixpanelRecorder.prototype.startRecording = function(options) {
8357
+ options = options || {};
7960
8358
  if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
7961
8359
  logger.log('Recording already in progress, skipping startRecording.');
7962
8360
  return;
7963
8361
  }
7964
8362
 
7965
- var onIdleTimeout = _.bind(function () {
8363
+ var onIdleTimeout = function () {
7966
8364
  logger.log('Idle timeout reached, restarting recording.');
7967
8365
  this.resetRecording();
7968
- }, this);
8366
+ }.bind(this);
7969
8367
 
7970
- var onMaxLengthReached = _.bind(function () {
8368
+ var onMaxLengthReached = function () {
7971
8369
  logger.log('Max recording length reached, stopping recording.');
7972
8370
  this.resetRecording();
7973
- }, this);
8371
+ }.bind(this);
7974
8372
 
7975
- this.activeRecording = new SessionRecording({
7976
- mixpanelInstance: this._mixpanel,
8373
+ var onBatchSent = function () {
8374
+ this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
8375
+ this['__flushPromise'] = this.activeRecording.batcher._flushPromise;
8376
+ }.bind(this);
8377
+
8378
+ /**
8379
+ * @type {import('./session-recording').SessionRecordingOptions}
8380
+ */
8381
+ var sessionRecordingOptions = {
8382
+ mixpanelInstance: this.mixpanelInstance,
8383
+ onBatchSent: onBatchSent,
7977
8384
  onIdleTimeout: onIdleTimeout,
7978
8385
  onMaxLengthReached: onMaxLengthReached,
7979
8386
  replayId: _.UUID(),
7980
- rrwebRecord: record
7981
- });
8387
+ rrwebRecord: this.rrwebRecord,
8388
+ sharedLockStorage: this.sharedLockStorage
8389
+ };
8390
+
8391
+ if (options.activeSerializedRecording) {
8392
+ this.activeRecording = SessionRecording.deserialize(options.activeSerializedRecording, sessionRecordingOptions);
8393
+ } else {
8394
+ this.activeRecording = new SessionRecording(sessionRecordingOptions);
8395
+ }
7982
8396
 
7983
- this.activeRecording.startRecording(shouldStopBatcher);
8397
+ this.activeRecording.startRecording(options.shouldStopBatcher);
8398
+ return this.recordingRegistry.setActiveRecording(this.activeRecording.serialize());
7984
8399
  };
7985
8400
 
7986
8401
  MixpanelRecorder.prototype.stopRecording = function() {
8402
+ var stopPromise = this._stopCurrentRecording(false);
8403
+ this.recordingRegistry.clearActiveRecording();
8404
+ this.activeRecording = null;
8405
+ return stopPromise;
8406
+ };
8407
+
8408
+ MixpanelRecorder.prototype.pauseRecording = function() {
8409
+ return this._stopCurrentRecording(false);
8410
+ };
8411
+
8412
+ MixpanelRecorder.prototype._stopCurrentRecording = function(skipFlush) {
7987
8413
  if (this.activeRecording) {
7988
- this.activeRecording.stopRecording();
7989
- this.activeRecording = null;
8414
+ return this.activeRecording.stopRecording(skipFlush);
8415
+ }
8416
+ return PromisePolyfill.resolve();
8417
+ };
8418
+
8419
+ MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
8420
+ if (this.activeRecording && this.activeRecording.isRrwebStopped()) {
8421
+ this.activeRecording.startRecording(false);
8422
+ return PromisePolyfill.resolve(null);
7990
8423
  }
8424
+
8425
+ return this.recordingRegistry.getActiveRecording()
8426
+ .then(function (activeSerializedRecording) {
8427
+ if (activeSerializedRecording) {
8428
+ return this.startRecording({activeSerializedRecording: activeSerializedRecording});
8429
+ } else if (startNewIfInactive) {
8430
+ return this.startRecording({shouldStopBatcher: false});
8431
+ } else {
8432
+ logger.log('No resumable recording found.');
8433
+ return null;
8434
+ }
8435
+ }.bind(this));
7991
8436
  };
7992
8437
 
8438
+
7993
8439
  MixpanelRecorder.prototype.resetRecording = function () {
7994
8440
  this.stopRecording();
7995
- this.startRecording(true);
8441
+ this.startRecording({shouldStopBatcher: true});
7996
8442
  };
7997
8443
 
7998
8444
  MixpanelRecorder.prototype.getActiveReplayId = function () {