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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.60.0'
5
+ LIB_VERSION: '2.61.0'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -1466,15 +1466,9 @@ _.cookie = {
1466
1466
  }
1467
1467
  };
1468
1468
 
1469
- var _localStorageSupported = null;
1470
- var localStorageSupported = function(storage, forceCheck) {
1471
- if (_localStorageSupported !== null && !forceCheck) {
1472
- return _localStorageSupported;
1473
- }
1474
-
1469
+ var _testStorageSupported = function (storage) {
1475
1470
  var supported = true;
1476
1471
  try {
1477
- storage = storage || win.localStorage;
1478
1472
  var key = '__mplss_' + cheap_guid(8),
1479
1473
  val = 'xyz';
1480
1474
  storage.setItem(key, val);
@@ -1485,59 +1479,74 @@ var localStorageSupported = function(storage, forceCheck) {
1485
1479
  } catch (err) {
1486
1480
  supported = false;
1487
1481
  }
1488
-
1489
- _localStorageSupported = supported;
1490
1482
  return supported;
1491
1483
  };
1492
1484
 
1493
- // _.localStorage
1494
- _.localStorage = {
1495
- is_supported: function(force_check) {
1496
- var supported = localStorageSupported(null, force_check);
1497
- if (!supported) {
1498
- console.error('localStorage unsupported; falling back to cookie store');
1499
- }
1500
- return supported;
1501
- },
1485
+ var _localStorageSupported = null;
1486
+ var localStorageSupported = function(storage, forceCheck) {
1487
+ if (_localStorageSupported !== null && !forceCheck) {
1488
+ return _localStorageSupported;
1489
+ }
1490
+ return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
1491
+ };
1502
1492
 
1503
- error: function(msg) {
1504
- console.error('localStorage error: ' + msg);
1505
- },
1493
+ var _sessionStorageSupported = null;
1494
+ var sessionStorageSupported = function(storage, forceCheck) {
1495
+ if (_sessionStorageSupported !== null && !forceCheck) {
1496
+ return _sessionStorageSupported;
1497
+ }
1498
+ return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
1499
+ };
1506
1500
 
1507
- get: function(name) {
1508
- try {
1509
- return win.localStorage.getItem(name);
1510
- } catch (err) {
1511
- _.localStorage.error(err);
1512
- }
1513
- return null;
1514
- },
1501
+ function _storageWrapper(storage, name, is_supported_fn) {
1502
+ var log_error = function(msg) {
1503
+ console.error(name + ' error: ' + msg);
1504
+ };
1515
1505
 
1516
- parse: function(name) {
1517
- try {
1518
- return _.JSONDecode(_.localStorage.get(name)) || {};
1519
- } catch (err) {
1520
- // noop
1506
+ return {
1507
+ is_supported: function(forceCheck) {
1508
+ var supported = is_supported_fn(storage, forceCheck);
1509
+ if (!supported) {
1510
+ console.error(name + ' unsupported');
1511
+ }
1512
+ return supported;
1513
+ },
1514
+ error: log_error,
1515
+ get: function(key) {
1516
+ try {
1517
+ return storage.getItem(key);
1518
+ } catch (err) {
1519
+ log_error(err);
1520
+ }
1521
+ return null;
1522
+ },
1523
+ parse: function(key) {
1524
+ try {
1525
+ return _.JSONDecode(storage.getItem(key)) || {};
1526
+ } catch (err) {
1527
+ // noop
1528
+ }
1529
+ return null;
1530
+ },
1531
+ set: function(key, value) {
1532
+ try {
1533
+ storage.setItem(key, value);
1534
+ } catch (err) {
1535
+ log_error(err);
1536
+ }
1537
+ },
1538
+ remove: function(key) {
1539
+ try {
1540
+ storage.removeItem(key);
1541
+ } catch (err) {
1542
+ log_error(err);
1543
+ }
1521
1544
  }
1522
- return null;
1523
- },
1545
+ };
1546
+ }
1524
1547
 
1525
- set: function(name, value) {
1526
- try {
1527
- win.localStorage.setItem(name, value);
1528
- } catch (err) {
1529
- _.localStorage.error(err);
1530
- }
1531
- },
1532
-
1533
- remove: function(name) {
1534
- try {
1535
- win.localStorage.removeItem(name);
1536
- } catch (err) {
1537
- _.localStorage.error(err);
1538
- }
1539
- }
1540
- };
1548
+ _.localStorage = _storageWrapper(win.localStorage, 'localStorage', localStorageSupported);
1549
+ _.sessionStorage = _storageWrapper(win.sessionStorage, 'sessionStorage', sessionStorageSupported);
1541
1550
 
1542
1551
  _.register_event = (function() {
1543
1552
  // written by Dean Edwards, 2005
@@ -2064,6 +2073,31 @@ _.info = {
2064
2073
  }
2065
2074
  };
2066
2075
 
2076
+ /**
2077
+ * Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
2078
+ * Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
2079
+ */
2080
+ var batchedThrottle = function (fn, waitMs) {
2081
+ var timeoutPromise = null;
2082
+ var throttledItems = [];
2083
+ return function (item) {
2084
+ var self = this;
2085
+ throttledItems.push(item);
2086
+
2087
+ if (!timeoutPromise) {
2088
+ timeoutPromise = new PromisePolyfill(function (resolve) {
2089
+ setTimeout(function () {
2090
+ var returnValue = fn.apply(self, [throttledItems]);
2091
+ timeoutPromise = null;
2092
+ throttledItems = [];
2093
+ resolve(returnValue);
2094
+ }, waitMs);
2095
+ });
2096
+ }
2097
+ return timeoutPromise;
2098
+ };
2099
+ };
2100
+
2067
2101
  var cheap_guid = function(maxlen) {
2068
2102
  var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
2069
2103
  return maxlen ? guid.substring(0, maxlen) : guid;
@@ -2106,6 +2140,8 @@ var isOnline = function() {
2106
2140
  return _.isUndefined(onLine) || onLine;
2107
2141
  };
2108
2142
 
2143
+ var NOOP_FUNC = function () {};
2144
+
2109
2145
  var JSONStringify = null, JSONParse = null;
2110
2146
  if (typeof JSON !== 'undefined') {
2111
2147
  JSONStringify = JSON.stringify;
@@ -2114,20 +2150,29 @@ if (typeof JSON !== 'undefined') {
2114
2150
  JSONStringify = JSONStringify || _.JSONEncode;
2115
2151
  JSONParse = JSONParse || _.JSONDecode;
2116
2152
 
2117
- // EXPORTS (for closure compiler)
2118
- _['toArray'] = _.toArray;
2119
- _['isObject'] = _.isObject;
2120
- _['JSONEncode'] = _.JSONEncode;
2121
- _['JSONDecode'] = _.JSONDecode;
2122
- _['isBlockedUA'] = _.isBlockedUA;
2123
- _['isEmptyObject'] = _.isEmptyObject;
2153
+ // UNMINIFIED EXPORTS (for closure compiler)
2124
2154
  _['info'] = _.info;
2125
- _['info']['device'] = _.info.device;
2126
2155
  _['info']['browser'] = _.info.browser;
2127
2156
  _['info']['browserVersion'] = _.info.browserVersion;
2157
+ _['info']['device'] = _.info.device;
2128
2158
  _['info']['properties'] = _.info.properties;
2159
+ _['isBlockedUA'] = _.isBlockedUA;
2160
+ _['isEmptyObject'] = _.isEmptyObject;
2161
+ _['isObject'] = _.isObject;
2162
+ _['JSONDecode'] = _.JSONDecode;
2163
+ _['JSONEncode'] = _.JSONEncode;
2164
+ _['toArray'] = _.toArray;
2129
2165
  _['NPO'] = NpoPromise;
2130
2166
 
2167
+ /**
2168
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
2169
+ * @returns {boolean}
2170
+ */
2171
+ var isRecordingExpired = function(serializedRecording) {
2172
+ var now = Date.now();
2173
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
2174
+ };
2175
+
2131
2176
  // stateless utils
2132
2177
 
2133
2178
  var EV_CHANGE = 'change';
@@ -3150,7 +3195,7 @@ var SharedLock = function(key, options) {
3150
3195
  options = options || {};
3151
3196
 
3152
3197
  this.storageKey = key;
3153
- this.storage = options.storage || window.localStorage;
3198
+ this.storage = options.storage || win.localStorage;
3154
3199
  this.pollIntervalMS = options.pollIntervalMS || 100;
3155
3200
  this.timeoutMS = options.timeoutMS || 2000;
3156
3201
 
@@ -3165,7 +3210,6 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
3165
3210
  return new Promise(_.bind(function (resolve, reject) {
3166
3211
  var i = pid || (new Date().getTime() + '|' + Math.random());
3167
3212
  var startTime = new Date().getTime();
3168
-
3169
3213
  var key = this.storageKey;
3170
3214
  var pollIntervalMS = this.pollIntervalMS;
3171
3215
  var timeoutMS = this.timeoutMS;
@@ -3276,11 +3320,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
3276
3320
  };
3277
3321
 
3278
3322
  /**
3279
- * @typedef {import('./wrapper').StorageWrapper}
3280
- */
3281
-
3282
- /**
3283
- * @type {StorageWrapper}
3323
+ * @type {import('./wrapper').StorageWrapper}
3284
3324
  */
3285
3325
  var LocalStorageWrapper = function (storageOverride) {
3286
3326
  this.storage = storageOverride || localStorage;
@@ -3293,7 +3333,7 @@ LocalStorageWrapper.prototype.init = function () {
3293
3333
  LocalStorageWrapper.prototype.setItem = function (key, value) {
3294
3334
  return new PromisePolyfill(_.bind(function (resolve, reject) {
3295
3335
  try {
3296
- this.storage.setItem(key, value);
3336
+ this.storage.setItem(key, JSONStringify(value));
3297
3337
  } catch (e) {
3298
3338
  reject(e);
3299
3339
  }
@@ -3305,7 +3345,7 @@ LocalStorageWrapper.prototype.getItem = function (key) {
3305
3345
  return new PromisePolyfill(_.bind(function (resolve, reject) {
3306
3346
  var item;
3307
3347
  try {
3308
- item = this.storage.getItem(key);
3348
+ item = JSONParse(this.storage.getItem(key));
3309
3349
  } catch (e) {
3310
3350
  reject(e);
3311
3351
  }
@@ -3348,8 +3388,10 @@ var RequestQueue = function (storageKey, options) {
3348
3388
  this.usePersistence = options.usePersistence;
3349
3389
  if (this.usePersistence) {
3350
3390
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
3351
- this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
3352
- this.queueStorage.init();
3391
+ this.lock = new SharedLock(storageKey, {
3392
+ storage: options.sharedLockStorage || win.localStorage,
3393
+ timeoutMS: options.sharedLockTimeoutMS,
3394
+ });
3353
3395
  }
3354
3396
  this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
3355
3397
 
@@ -3357,6 +3399,14 @@ var RequestQueue = function (storageKey, options) {
3357
3399
 
3358
3400
  this.memQueue = [];
3359
3401
  this.initialized = false;
3402
+
3403
+ if (options.enqueueThrottleMs) {
3404
+ this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
3405
+ } else {
3406
+ this.enqueuePersisted = _.bind(function (queueEntry) {
3407
+ return this._enqueuePersisted([queueEntry]);
3408
+ }, this);
3409
+ }
3360
3410
  };
3361
3411
 
3362
3412
  RequestQueue.prototype.ensureInit = function () {
@@ -3399,36 +3449,39 @@ RequestQueue.prototype.enqueue = function (item, flushInterval) {
3399
3449
  this.memQueue.push(queueEntry);
3400
3450
  return PromisePolyfill.resolve(true);
3401
3451
  } else {
3452
+ return this.enqueuePersisted(queueEntry);
3453
+ }
3454
+ };
3402
3455
 
3403
- var enqueueItem = _.bind(function () {
3404
- return this.ensureInit()
3405
- .then(_.bind(function () {
3406
- return this.readFromStorage();
3407
- }, this))
3408
- .then(_.bind(function (storedQueue) {
3409
- storedQueue.push(queueEntry);
3410
- return this.saveToStorage(storedQueue);
3411
- }, this))
3412
- .then(_.bind(function (succeeded) {
3413
- // only add to in-memory queue when storage succeeds
3414
- if (succeeded) {
3415
- this.memQueue.push(queueEntry);
3416
- }
3417
- return succeeded;
3418
- }, this))
3419
- .catch(_.bind(function (err) {
3420
- this.reportError('Error enqueueing item', err, item);
3421
- return false;
3422
- }, this));
3423
- }, this);
3456
+ RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
3457
+ var enqueueItem = _.bind(function () {
3458
+ return this.ensureInit()
3459
+ .then(_.bind(function () {
3460
+ return this.readFromStorage();
3461
+ }, this))
3462
+ .then(_.bind(function (storedQueue) {
3463
+ return this.saveToStorage(storedQueue.concat(queueEntries));
3464
+ }, this))
3465
+ .then(_.bind(function (succeeded) {
3466
+ // only add to in-memory queue when storage succeeds
3467
+ if (succeeded) {
3468
+ this.memQueue = this.memQueue.concat(queueEntries);
3469
+ }
3424
3470
 
3425
- return this.lock
3426
- .withLock(enqueueItem, this.pid)
3471
+ return succeeded;
3472
+ }, this))
3427
3473
  .catch(_.bind(function (err) {
3428
- this.reportError('Error acquiring storage lock', err);
3474
+ this.reportError('Error enqueueing items', err, queueEntries);
3429
3475
  return false;
3430
3476
  }, this));
3431
- }
3477
+ }, this);
3478
+
3479
+ return this.lock
3480
+ .withLock(enqueueItem, this.pid)
3481
+ .catch(_.bind(function (err) {
3482
+ this.reportError('Error acquiring storage lock', err);
3483
+ return false;
3484
+ }, this));
3432
3485
  };
3433
3486
 
3434
3487
  /**
@@ -3449,7 +3502,7 @@ RequestQueue.prototype.fillBatch = function (batchSize) {
3449
3502
  }, this))
3450
3503
  .then(_.bind(function (storedQueue) {
3451
3504
  if (storedQueue.length) {
3452
- // item IDs already in batch; don't duplicate out of storage
3505
+ // item IDs already in batch; don't duplicate out of storage
3453
3506
  var idsInBatch = {}; // poor man's Set
3454
3507
  _.each(batch, function (item) {
3455
3508
  idsInBatch[item['id']] = true;
@@ -3536,7 +3589,7 @@ RequestQueue.prototype.removeItemsByID = function (ids) {
3536
3589
  .withLock(removeFromStorage, this.pid)
3537
3590
  .catch(_.bind(function (err) {
3538
3591
  this.reportError('Error acquiring storage lock', err);
3539
- if (!localStorageSupported(this.queueStorage.storage, true)) {
3592
+ if (!localStorageSupported(this.lock.storage, true)) {
3540
3593
  // Looks like localStorage writes have stopped working sometime after
3541
3594
  // initialization (probably full), and so nobody can acquire locks
3542
3595
  // anymore. Consider it temporarily safe to remove items without the
@@ -3624,7 +3677,6 @@ RequestQueue.prototype.readFromStorage = function () {
3624
3677
  }, this))
3625
3678
  .then(_.bind(function (storageEntry) {
3626
3679
  if (storageEntry) {
3627
- storageEntry = JSONParse(storageEntry);
3628
3680
  if (!_.isArray(storageEntry)) {
3629
3681
  this.reportError('Invalid storage entry:', storageEntry);
3630
3682
  storageEntry = null;
@@ -3642,16 +3694,9 @@ RequestQueue.prototype.readFromStorage = function () {
3642
3694
  * Serialize the given items array to localStorage.
3643
3695
  */
3644
3696
  RequestQueue.prototype.saveToStorage = function (queue) {
3645
- try {
3646
- var serialized = JSONStringify(queue);
3647
- } catch (err) {
3648
- this.reportError('Error serializing queue', err);
3649
- return PromisePolyfill.resolve(false);
3650
- }
3651
-
3652
3697
  return this.ensureInit()
3653
3698
  .then(_.bind(function () {
3654
- return this.queueStorage.setItem(this.storageKey, serialized);
3699
+ return this.queueStorage.setItem(this.storageKey, queue);
3655
3700
  }, this))
3656
3701
  .then(function () {
3657
3702
  return true;
@@ -3695,7 +3740,9 @@ var RequestBatcher = function(storageKey, options) {
3695
3740
  errorReporter: _.bind(this.reportError, this),
3696
3741
  queueStorage: options.queueStorage,
3697
3742
  sharedLockStorage: options.sharedLockStorage,
3698
- usePersistence: options.usePersistence
3743
+ sharedLockTimeoutMS: options.sharedLockTimeoutMS,
3744
+ usePersistence: options.usePersistence,
3745
+ enqueueThrottleMs: options.enqueueThrottleMs
3699
3746
  });
3700
3747
 
3701
3748
  this.libConfig = options.libConfig;
@@ -3717,6 +3764,8 @@ var RequestBatcher = function(storageKey, options) {
3717
3764
  // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
3718
3765
  // in a request loop and get ratelimited by the server.
3719
3766
  this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
3767
+
3768
+ this._flushPromise = null;
3720
3769
  };
3721
3770
 
3722
3771
  /**
@@ -3776,7 +3825,7 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
3776
3825
  if (!this.stopped) { // don't schedule anymore if batching has been stopped
3777
3826
  this.timeoutID = setTimeout(_.bind(function() {
3778
3827
  if (!this.stopped) {
3779
- this.flush();
3828
+ this._flushPromise = this.flush();
3780
3829
  }
3781
3830
  }, this), this.flushInterval);
3782
3831
  }
@@ -5492,6 +5541,129 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
5492
5541
  return timestamp;
5493
5542
  };
5494
5543
 
5544
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
5545
+
5546
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
5547
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
5548
+
5549
+ // note: increment the version number when adding new object stores
5550
+ var DB_VERSION = 1;
5551
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
5552
+
5553
+ /**
5554
+ * @type {import('./wrapper').StorageWrapper}
5555
+ */
5556
+ var IDBStorageWrapper = function (storeName) {
5557
+ /**
5558
+ * @type {Promise<IDBDatabase>|null}
5559
+ */
5560
+ this.dbPromise = null;
5561
+ this.storeName = storeName;
5562
+ };
5563
+
5564
+ IDBStorageWrapper.prototype._openDb = function () {
5565
+ return new PromisePolyfill(function (resolve, reject) {
5566
+ var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
5567
+ openRequest['onerror'] = function () {
5568
+ reject(openRequest.error);
5569
+ };
5570
+
5571
+ openRequest['onsuccess'] = function () {
5572
+ resolve(openRequest.result);
5573
+ };
5574
+
5575
+ openRequest['onupgradeneeded'] = function (ev) {
5576
+ var db = ev.target.result;
5577
+
5578
+ OBJECT_STORES.forEach(function (storeName) {
5579
+ db.createObjectStore(storeName);
5580
+ });
5581
+ };
5582
+ });
5583
+ };
5584
+
5585
+ IDBStorageWrapper.prototype.init = function () {
5586
+ if (!win.indexedDB) {
5587
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
5588
+ }
5589
+
5590
+ if (!this.dbPromise) {
5591
+ this.dbPromise = this._openDb();
5592
+ }
5593
+
5594
+ return this.dbPromise
5595
+ .then(function (dbOrError) {
5596
+ if (dbOrError instanceof win['IDBDatabase']) {
5597
+ return PromisePolyfill.resolve();
5598
+ } else {
5599
+ return PromisePolyfill.reject(dbOrError);
5600
+ }
5601
+ });
5602
+ };
5603
+
5604
+ /**
5605
+ * @param {IDBTransactionMode} mode
5606
+ * @param {function(IDBObjectStore): void} storeCb
5607
+ */
5608
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
5609
+ var storeName = this.storeName;
5610
+ var doTransaction = function (db) {
5611
+ return new PromisePolyfill(function (resolve, reject) {
5612
+ var transaction = db.transaction(storeName, mode);
5613
+ transaction.oncomplete = function () {
5614
+ resolve(transaction);
5615
+ };
5616
+ transaction.onabort = transaction.onerror = function () {
5617
+ reject(transaction.error);
5618
+ };
5619
+
5620
+ storeCb(transaction.objectStore(storeName));
5621
+ });
5622
+ };
5623
+
5624
+ return this.dbPromise
5625
+ .then(doTransaction)
5626
+ .catch(function (err) {
5627
+ if (err['name'] === 'InvalidStateError') {
5628
+ // try reopening the DB if the connection is closed
5629
+ this.dbPromise = this._openDb();
5630
+ return this.dbPromise.then(doTransaction);
5631
+ } else {
5632
+ return PromisePolyfill.reject(err);
5633
+ }
5634
+ }.bind(this));
5635
+ };
5636
+
5637
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
5638
+ return this.makeTransaction('readwrite', function (objectStore) {
5639
+ objectStore.put(value, key);
5640
+ });
5641
+ };
5642
+
5643
+ IDBStorageWrapper.prototype.getItem = function (key) {
5644
+ var req;
5645
+ return this.makeTransaction('readonly', function (objectStore) {
5646
+ req = objectStore.get(key);
5647
+ }).then(function () {
5648
+ return req.result;
5649
+ });
5650
+ };
5651
+
5652
+ IDBStorageWrapper.prototype.removeItem = function (key) {
5653
+ return this.makeTransaction('readwrite', function (objectStore) {
5654
+ objectStore.delete(key);
5655
+ });
5656
+ };
5657
+
5658
+ IDBStorageWrapper.prototype.getAll = function () {
5659
+ var req;
5660
+ return this.makeTransaction('readonly', function (objectStore) {
5661
+ req = objectStore.getAll();
5662
+ }).then(function () {
5663
+ return req.result;
5664
+ });
5665
+ };
5666
+
5495
5667
  /* eslint camelcase: "off" */
5496
5668
 
5497
5669
  /*
@@ -5506,11 +5678,6 @@ MixpanelPersistence.prototype.remove_event_timer = function(event_name) {
5506
5678
  * Released under the MIT License.
5507
5679
  */
5508
5680
 
5509
- // ==ClosureCompiler==
5510
- // @compilation_level ADVANCED_OPTIMIZATIONS
5511
- // @output_file_name mixpanel-2.8.min.js
5512
- // ==/ClosureCompiler==
5513
-
5514
5681
  /*
5515
5682
  SIMPLE STYLE GUIDE:
5516
5683
 
@@ -5533,7 +5700,6 @@ var INIT_MODULE = 0;
5533
5700
  var INIT_SNIPPET = 1;
5534
5701
 
5535
5702
  var IDENTITY_FUNC = function(x) {return x;};
5536
- var NOOP_FUNC = function() {};
5537
5703
 
5538
5704
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
5539
5705
  /** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
@@ -5842,34 +6008,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
5842
6008
  this.autocapture = new Autocapture(this);
5843
6009
  this.autocapture.init();
5844
6010
 
5845
- if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
5846
- this.start_session_recording();
6011
+ this._init_tab_id();
6012
+ this._check_and_start_session_recording();
6013
+ };
6014
+
6015
+ /**
6016
+ * Assigns a unique UUID to this tab / window by leveraging sessionStorage.
6017
+ * This is primarily used for session recording, where data must be isolated to the current tab.
6018
+ */
6019
+ MixpanelLib.prototype._init_tab_id = function() {
6020
+ if (_.sessionStorage.is_supported()) {
6021
+ try {
6022
+ var key_suffix = this.get_config('name') + '_' + this.get_config('token');
6023
+ var tab_id_key = 'mp_tab_id_' + key_suffix;
6024
+
6025
+ // A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
6026
+ // This enforces a unique ID in the cases like duplicated tab, window.open(...)
6027
+ var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
6028
+ if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
6029
+ _.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
6030
+ }
6031
+
6032
+ _.sessionStorage.set(should_generate_new_tab_id_key, '1');
6033
+ this.tab_id = _.sessionStorage.get(tab_id_key);
6034
+
6035
+ // 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,
6036
+ // but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
6037
+ // If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
6038
+ win.addEventListener('beforeunload', function () {
6039
+ _.sessionStorage.remove(should_generate_new_tab_id_key);
6040
+ });
6041
+ } catch(err) {
6042
+ this.report_error('Error initializing tab id', err);
6043
+ }
6044
+ } else {
6045
+ this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
5847
6046
  }
5848
6047
  };
5849
6048
 
5850
- MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(function () {
6049
+ MixpanelLib.prototype.get_tab_id = function () {
6050
+ return this.tab_id || null;
6051
+ };
6052
+
6053
+ MixpanelLib.prototype._should_load_recorder = function () {
6054
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
6055
+ var tab_id = this.get_tab_id();
6056
+ return recording_registry_idb.init()
6057
+ .then(function () {
6058
+ return recording_registry_idb.getAll();
6059
+ })
6060
+ .then(function (recordings) {
6061
+ for (var i = 0; i < recordings.length; i++) {
6062
+ // if there are expired recordings in the registry, we should load the recorder to flush them
6063
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
6064
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
6065
+ return true;
6066
+ }
6067
+ }
6068
+ return false;
6069
+ })
6070
+ .catch(_.bind(function (err) {
6071
+ this.report_error('Error checking recording registry', err);
6072
+ }, this));
6073
+ };
6074
+
6075
+ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
5851
6076
  if (!win['MutationObserver']) {
5852
6077
  console.critical('Browser does not support MutationObserver; skipping session recording');
5853
6078
  return;
5854
6079
  }
5855
6080
 
5856
- var handleLoadedRecorder = _.bind(function() {
5857
- this._recorder = this._recorder || new win['__mp_recorder'](this);
5858
- this._recorder['startRecording']();
6081
+ var loadRecorder = _.bind(function(startNewIfInactive) {
6082
+ var handleLoadedRecorder = _.bind(function() {
6083
+ this._recorder = this._recorder || new win['__mp_recorder'](this);
6084
+ this._recorder['resumeRecording'](startNewIfInactive);
6085
+ }, this);
6086
+
6087
+ if (_.isUndefined(win['__mp_recorder'])) {
6088
+ load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
6089
+ } else {
6090
+ handleLoadedRecorder();
6091
+ }
5859
6092
  }, this);
5860
6093
 
5861
- if (_.isUndefined(win['__mp_recorder'])) {
5862
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
6094
+ /**
6095
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
6096
+ * 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.
6097
+ */
6098
+ var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
6099
+ if (force_start || is_sampled) {
6100
+ loadRecorder(true);
5863
6101
  } else {
5864
- handleLoadedRecorder();
6102
+ this._should_load_recorder()
6103
+ .then(function (shouldLoad) {
6104
+ if (shouldLoad) {
6105
+ loadRecorder(false);
6106
+ }
6107
+ });
5865
6108
  }
5866
6109
  });
5867
6110
 
6111
+ MixpanelLib.prototype.start_session_recording = function () {
6112
+ this._check_and_start_session_recording(true);
6113
+ };
6114
+
5868
6115
  MixpanelLib.prototype.stop_session_recording = function () {
5869
6116
  if (this._recorder) {
5870
6117
  this._recorder['stopRecording']();
5871
- } else {
5872
- console.critical('Session recorder module not loaded');
6118
+ }
6119
+ };
6120
+
6121
+ MixpanelLib.prototype.pause_session_recording = function () {
6122
+ if (this._recorder) {
6123
+ this._recorder['pauseRecording']();
6124
+ }
6125
+ };
6126
+
6127
+ MixpanelLib.prototype.resume_session_recording = function () {
6128
+ if (this._recorder) {
6129
+ this._recorder['resumeRecording']();
5873
6130
  }
5874
6131
  };
5875
6132
 
@@ -5904,6 +6161,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
5904
6161
  return replay_id || null;
5905
6162
  };
5906
6163
 
6164
+ // "private" public method to reach into the recorder in test cases
6165
+ MixpanelLib.prototype.__get_recorder = function () {
6166
+ return this._recorder;
6167
+ };
6168
+
5907
6169
  // Private methods
5908
6170
 
5909
6171
  MixpanelLib.prototype._loaded = function() {
@@ -6243,7 +6505,8 @@ MixpanelLib.prototype.init_batchers = function() {
6243
6505
  return this._run_hook('before_send_' + attrs.type, item);
6244
6506
  }, this),
6245
6507
  stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
6246
- usePersistence: true
6508
+ usePersistence: true,
6509
+ enqueueThrottleMs: 10,
6247
6510
  }
6248
6511
  );
6249
6512
  }, this);
@@ -7344,6 +7607,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
7344
7607
 
7345
7608
  if (disabled) {
7346
7609
  this.stop_batch_senders();
7610
+ this.stop_session_recording();
7347
7611
  } else {
7348
7612
  // only start batchers after opt-in if they have previously been started
7349
7613
  // in order to avoid unintentionally starting up batching for the first time
@@ -7584,10 +7848,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
7584
7848
  MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
7585
7849
  MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
7586
7850
  MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
7851
+ MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
7852
+ MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
7587
7853
  MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
7588
7854
  MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
7855
+ MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
7589
7856
  MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
7590
7857
 
7858
+ // Exports intended only for testing
7859
+ MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
7860
+
7591
7861
  // MixpanelPersistence Exports
7592
7862
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
7593
7863
  MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;