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.
@@ -20,7 +20,9 @@ var RequestBatcher = function(storageKey, options) {
20
20
  errorReporter: _.bind(this.reportError, this),
21
21
  queueStorage: options.queueStorage,
22
22
  sharedLockStorage: options.sharedLockStorage,
23
- usePersistence: options.usePersistence
23
+ sharedLockTimeoutMS: options.sharedLockTimeoutMS,
24
+ usePersistence: options.usePersistence,
25
+ enqueueThrottleMs: options.enqueueThrottleMs
24
26
  });
25
27
 
26
28
  this.libConfig = options.libConfig;
@@ -42,6 +44,8 @@ var RequestBatcher = function(storageKey, options) {
42
44
  // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
43
45
  // in a request loop and get ratelimited by the server.
44
46
  this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
47
+
48
+ this._flushPromise = null;
45
49
  };
46
50
 
47
51
  /**
@@ -101,7 +105,7 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
101
105
  if (!this.stopped) { // don't schedule anymore if batching has been stopped
102
106
  this.timeoutID = setTimeout(_.bind(function() {
103
107
  if (!this.stopped) {
104
- this.flush();
108
+ this._flushPromise = this.flush();
105
109
  }
106
110
  }, this), this.flushInterval);
107
111
  }
@@ -1,5 +1,6 @@
1
1
  import { SharedLock } from './shared-lock';
2
- import { cheap_guid, console_with_prefix, localStorageSupported, JSONParse, JSONStringify, _ } from './utils'; // eslint-disable-line camelcase
2
+ import { batchedThrottle, cheap_guid, console_with_prefix, localStorageSupported, _ } from './utils'; // eslint-disable-line camelcase
3
+ import { window } from './window';
3
4
  import { LocalStorageWrapper } from './storage/local-storage';
4
5
  import { Promise } from './promise-polyfill';
5
6
 
@@ -27,8 +28,10 @@ var RequestQueue = function (storageKey, options) {
27
28
  this.usePersistence = options.usePersistence;
28
29
  if (this.usePersistence) {
29
30
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
30
- this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
31
- this.queueStorage.init();
31
+ this.lock = new SharedLock(storageKey, {
32
+ storage: options.sharedLockStorage || window.localStorage,
33
+ timeoutMS: options.sharedLockTimeoutMS,
34
+ });
32
35
  }
33
36
  this.reportError = options.errorReporter || _.bind(logger.error, logger);
34
37
 
@@ -36,6 +39,14 @@ var RequestQueue = function (storageKey, options) {
36
39
 
37
40
  this.memQueue = [];
38
41
  this.initialized = false;
42
+
43
+ if (options.enqueueThrottleMs) {
44
+ this.enqueuePersisted = batchedThrottle(_.bind(this._enqueuePersisted, this), options.enqueueThrottleMs);
45
+ } else {
46
+ this.enqueuePersisted = _.bind(function (queueEntry) {
47
+ return this._enqueuePersisted([queueEntry]);
48
+ }, this);
49
+ }
39
50
  };
40
51
 
41
52
  RequestQueue.prototype.ensureInit = function () {
@@ -78,36 +89,39 @@ RequestQueue.prototype.enqueue = function (item, flushInterval) {
78
89
  this.memQueue.push(queueEntry);
79
90
  return Promise.resolve(true);
80
91
  } else {
92
+ return this.enqueuePersisted(queueEntry);
93
+ }
94
+ };
81
95
 
82
- var enqueueItem = _.bind(function () {
83
- return this.ensureInit()
84
- .then(_.bind(function () {
85
- return this.readFromStorage();
86
- }, this))
87
- .then(_.bind(function (storedQueue) {
88
- storedQueue.push(queueEntry);
89
- return this.saveToStorage(storedQueue);
90
- }, this))
91
- .then(_.bind(function (succeeded) {
92
- // only add to in-memory queue when storage succeeds
93
- if (succeeded) {
94
- this.memQueue.push(queueEntry);
95
- }
96
- return succeeded;
97
- }, this))
98
- .catch(_.bind(function (err) {
99
- this.reportError('Error enqueueing item', err, item);
100
- return false;
101
- }, this));
102
- }, this);
96
+ RequestQueue.prototype._enqueuePersisted = function (queueEntries) {
97
+ var enqueueItem = _.bind(function () {
98
+ return this.ensureInit()
99
+ .then(_.bind(function () {
100
+ return this.readFromStorage();
101
+ }, this))
102
+ .then(_.bind(function (storedQueue) {
103
+ return this.saveToStorage(storedQueue.concat(queueEntries));
104
+ }, this))
105
+ .then(_.bind(function (succeeded) {
106
+ // only add to in-memory queue when storage succeeds
107
+ if (succeeded) {
108
+ this.memQueue = this.memQueue.concat(queueEntries);
109
+ }
103
110
 
104
- return this.lock
105
- .withLock(enqueueItem, this.pid)
111
+ return succeeded;
112
+ }, this))
106
113
  .catch(_.bind(function (err) {
107
- this.reportError('Error acquiring storage lock', err);
114
+ this.reportError('Error enqueueing items', err, queueEntries);
108
115
  return false;
109
116
  }, this));
110
- }
117
+ }, this);
118
+
119
+ return this.lock
120
+ .withLock(enqueueItem, this.pid)
121
+ .catch(_.bind(function (err) {
122
+ this.reportError('Error acquiring storage lock', err);
123
+ return false;
124
+ }, this));
111
125
  };
112
126
 
113
127
  /**
@@ -128,7 +142,7 @@ RequestQueue.prototype.fillBatch = function (batchSize) {
128
142
  }, this))
129
143
  .then(_.bind(function (storedQueue) {
130
144
  if (storedQueue.length) {
131
- // item IDs already in batch; don't duplicate out of storage
145
+ // item IDs already in batch; don't duplicate out of storage
132
146
  var idsInBatch = {}; // poor man's Set
133
147
  _.each(batch, function (item) {
134
148
  idsInBatch[item['id']] = true;
@@ -215,7 +229,7 @@ RequestQueue.prototype.removeItemsByID = function (ids) {
215
229
  .withLock(removeFromStorage, this.pid)
216
230
  .catch(_.bind(function (err) {
217
231
  this.reportError('Error acquiring storage lock', err);
218
- if (!localStorageSupported(this.queueStorage.storage, true)) {
232
+ if (!localStorageSupported(this.lock.storage, true)) {
219
233
  // Looks like localStorage writes have stopped working sometime after
220
234
  // initialization (probably full), and so nobody can acquire locks
221
235
  // anymore. Consider it temporarily safe to remove items without the
@@ -303,7 +317,6 @@ RequestQueue.prototype.readFromStorage = function () {
303
317
  }, this))
304
318
  .then(_.bind(function (storageEntry) {
305
319
  if (storageEntry) {
306
- storageEntry = JSONParse(storageEntry);
307
320
  if (!_.isArray(storageEntry)) {
308
321
  this.reportError('Invalid storage entry:', storageEntry);
309
322
  storageEntry = null;
@@ -321,16 +334,9 @@ RequestQueue.prototype.readFromStorage = function () {
321
334
  * Serialize the given items array to localStorage.
322
335
  */
323
336
  RequestQueue.prototype.saveToStorage = function (queue) {
324
- try {
325
- var serialized = JSONStringify(queue);
326
- } catch (err) {
327
- this.reportError('Error serializing queue', err);
328
- return Promise.resolve(false);
329
- }
330
-
331
337
  return this.ensureInit()
332
338
  .then(_.bind(function () {
333
- return this.queueStorage.setItem(this.storageKey, serialized);
339
+ return this.queueStorage.setItem(this.storageKey, queue);
334
340
  }, this))
335
341
  .then(function () {
336
342
  return true;
@@ -1,5 +1,6 @@
1
1
  import { Promise } from './promise-polyfill';
2
2
  import { console_with_prefix, localStorageSupported, _ } from './utils'; // eslint-disable-line camelcase
3
+ import { window } from './window';
3
4
 
4
5
  var logger = console_with_prefix('lock');
5
6
 
@@ -42,7 +43,6 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
42
43
  return new Promise(_.bind(function (resolve, reject) {
43
44
  var i = pid || (new Date().getTime() + '|' + Math.random());
44
45
  var startTime = new Date().getTime();
45
-
46
46
  var key = this.storageKey;
47
47
  var pollIntervalMS = this.pollIntervalMS;
48
48
  var timeoutMS = this.timeoutMS;
@@ -0,0 +1,127 @@
1
+ import { Promise } from '../promise-polyfill';
2
+ import { window } from '../window';
3
+
4
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
5
+
6
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
7
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
8
+
9
+ // note: increment the version number when adding new object stores
10
+ var DB_VERSION = 1;
11
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
12
+
13
+ /**
14
+ * @type {import('./wrapper').StorageWrapper}
15
+ */
16
+ var IDBStorageWrapper = function (storeName) {
17
+ /**
18
+ * @type {Promise<IDBDatabase>|null}
19
+ */
20
+ this.dbPromise = null;
21
+ this.storeName = storeName;
22
+ };
23
+
24
+ IDBStorageWrapper.prototype._openDb = function () {
25
+ return new Promise(function (resolve, reject) {
26
+ var openRequest = window.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
27
+ openRequest['onerror'] = function () {
28
+ reject(openRequest.error);
29
+ };
30
+
31
+ openRequest['onsuccess'] = function () {
32
+ resolve(openRequest.result);
33
+ };
34
+
35
+ openRequest['onupgradeneeded'] = function (ev) {
36
+ var db = ev.target.result;
37
+
38
+ OBJECT_STORES.forEach(function (storeName) {
39
+ db.createObjectStore(storeName);
40
+ });
41
+ };
42
+ });
43
+ };
44
+
45
+ IDBStorageWrapper.prototype.init = function () {
46
+ if (!window.indexedDB) {
47
+ return Promise.reject('indexedDB is not supported in this browser');
48
+ }
49
+
50
+ if (!this.dbPromise) {
51
+ this.dbPromise = this._openDb();
52
+ }
53
+
54
+ return this.dbPromise
55
+ .then(function (dbOrError) {
56
+ if (dbOrError instanceof window['IDBDatabase']) {
57
+ return Promise.resolve();
58
+ } else {
59
+ return Promise.reject(dbOrError);
60
+ }
61
+ });
62
+ };
63
+
64
+ /**
65
+ * @param {IDBTransactionMode} mode
66
+ * @param {function(IDBObjectStore): void} storeCb
67
+ */
68
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
69
+ var storeName = this.storeName;
70
+ var doTransaction = function (db) {
71
+ return new Promise(function (resolve, reject) {
72
+ var transaction = db.transaction(storeName, mode);
73
+ transaction.oncomplete = function () {
74
+ resolve(transaction);
75
+ };
76
+ transaction.onabort = transaction.onerror = function () {
77
+ reject(transaction.error);
78
+ };
79
+
80
+ storeCb(transaction.objectStore(storeName));
81
+ });
82
+ };
83
+
84
+ return this.dbPromise
85
+ .then(doTransaction)
86
+ .catch(function (err) {
87
+ if (err['name'] === 'InvalidStateError') {
88
+ // try reopening the DB if the connection is closed
89
+ this.dbPromise = this._openDb();
90
+ return this.dbPromise.then(doTransaction);
91
+ } else {
92
+ return Promise.reject(err);
93
+ }
94
+ }.bind(this));
95
+ };
96
+
97
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
98
+ return this.makeTransaction('readwrite', function (objectStore) {
99
+ objectStore.put(value, key);
100
+ });
101
+ };
102
+
103
+ IDBStorageWrapper.prototype.getItem = function (key) {
104
+ var req;
105
+ return this.makeTransaction('readonly', function (objectStore) {
106
+ req = objectStore.get(key);
107
+ }).then(function () {
108
+ return req.result;
109
+ });
110
+ };
111
+
112
+ IDBStorageWrapper.prototype.removeItem = function (key) {
113
+ return this.makeTransaction('readwrite', function (objectStore) {
114
+ objectStore.delete(key);
115
+ });
116
+ };
117
+
118
+ IDBStorageWrapper.prototype.getAll = function () {
119
+ var req;
120
+ return this.makeTransaction('readonly', function (objectStore) {
121
+ req = objectStore.getAll();
122
+ }).then(function () {
123
+ return req.result;
124
+ });
125
+ };
126
+
127
+ export { IDBStorageWrapper, RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME };
@@ -1,12 +1,8 @@
1
1
  import { Promise } from '../promise-polyfill';
2
- import { _ } from '../utils'; // eslint-disable-line camelcase
2
+ import { _, JSONParse, JSONStringify } from '../utils'; // eslint-disable-line camelcase
3
3
 
4
4
  /**
5
- * @typedef {import('./wrapper').StorageWrapper}
6
- */
7
-
8
- /**
9
- * @type {StorageWrapper}
5
+ * @type {import('./wrapper').StorageWrapper}
10
6
  */
11
7
  var LocalStorageWrapper = function (storageOverride) {
12
8
  this.storage = storageOverride || localStorage;
@@ -19,7 +15,7 @@ LocalStorageWrapper.prototype.init = function () {
19
15
  LocalStorageWrapper.prototype.setItem = function (key, value) {
20
16
  return new Promise(_.bind(function (resolve, reject) {
21
17
  try {
22
- this.storage.setItem(key, value);
18
+ this.storage.setItem(key, JSONStringify(value));
23
19
  } catch (e) {
24
20
  reject(e);
25
21
  }
@@ -31,7 +27,7 @@ LocalStorageWrapper.prototype.getItem = function (key) {
31
27
  return new Promise(_.bind(function (resolve, reject) {
32
28
  var item;
33
29
  try {
34
- item = this.storage.getItem(key);
30
+ item = JSONParse(this.storage.getItem(key));
35
31
  } catch (e) {
36
32
  reject(e);
37
33
  }
@@ -6,9 +6,9 @@
6
6
  /**
7
7
  * @typedef {Object} StorageWrapper
8
8
  * @property {function():Promise<void>} init - Initializes the wrapper, async storage like IDB needs to create a table and upgrade if needed.
9
- * @property {function(string, string):Promise<void>} setItem - Sets an item in storage.
10
- * @property {function(string):Promise<string>} getItem - Retrieves an item from storage.
11
- * @property {function(string, string):Promise<void>} removeItem - Removes an item from storage.
9
+ * @property {function(string, any):Promise<void>} setItem - Sets an item in storage.
10
+ * @property {function(string):Promise<any>} getItem - Retrieves an item from storage.
11
+ * @property {function(string):Promise<void>} removeItem - Removes an item from storage.
12
12
  */
13
13
 
14
14
  export { };
package/src/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint camelcase: "off", eqeqeq: "off" */
2
2
  import Config from './config';
3
- import { NpoPromise } from './promise-polyfill';
3
+ import { NpoPromise, Promise } from './promise-polyfill';
4
4
  import { window } from './window';
5
5
 
6
6
  // Maximum allowed session recording length
@@ -1080,15 +1080,9 @@ _.cookie = {
1080
1080
  }
1081
1081
  };
1082
1082
 
1083
- var _localStorageSupported = null;
1084
- var localStorageSupported = function(storage, forceCheck) {
1085
- if (_localStorageSupported !== null && !forceCheck) {
1086
- return _localStorageSupported;
1087
- }
1088
-
1083
+ var _testStorageSupported = function (storage) {
1089
1084
  var supported = true;
1090
1085
  try {
1091
- storage = storage || window.localStorage;
1092
1086
  var key = '__mplss_' + cheap_guid(8),
1093
1087
  val = 'xyz';
1094
1088
  storage.setItem(key, val);
@@ -1099,59 +1093,74 @@ var localStorageSupported = function(storage, forceCheck) {
1099
1093
  } catch (err) {
1100
1094
  supported = false;
1101
1095
  }
1102
-
1103
- _localStorageSupported = supported;
1104
1096
  return supported;
1105
1097
  };
1106
1098
 
1107
- // _.localStorage
1108
- _.localStorage = {
1109
- is_supported: function(force_check) {
1110
- var supported = localStorageSupported(null, force_check);
1111
- if (!supported) {
1112
- console.error('localStorage unsupported; falling back to cookie store');
1113
- }
1114
- return supported;
1115
- },
1116
-
1117
- error: function(msg) {
1118
- console.error('localStorage error: ' + msg);
1119
- },
1099
+ var _localStorageSupported = null;
1100
+ var localStorageSupported = function(storage, forceCheck) {
1101
+ if (_localStorageSupported !== null && !forceCheck) {
1102
+ return _localStorageSupported;
1103
+ }
1104
+ return _localStorageSupported = _testStorageSupported(storage || window.localStorage);
1105
+ };
1120
1106
 
1121
- get: function(name) {
1122
- try {
1123
- return window.localStorage.getItem(name);
1124
- } catch (err) {
1125
- _.localStorage.error(err);
1126
- }
1127
- return null;
1128
- },
1107
+ var _sessionStorageSupported = null;
1108
+ var sessionStorageSupported = function(storage, forceCheck) {
1109
+ if (_sessionStorageSupported !== null && !forceCheck) {
1110
+ return _sessionStorageSupported;
1111
+ }
1112
+ return _sessionStorageSupported = _testStorageSupported(storage || window.sessionStorage);
1113
+ };
1129
1114
 
1130
- parse: function(name) {
1131
- try {
1132
- return _.JSONDecode(_.localStorage.get(name)) || {};
1133
- } catch (err) {
1134
- // noop
1135
- }
1136
- return null;
1137
- },
1115
+ function _storageWrapper(storage, name, is_supported_fn) {
1116
+ var log_error = function(msg) {
1117
+ console.error(name + ' error: ' + msg);
1118
+ };
1138
1119
 
1139
- set: function(name, value) {
1140
- try {
1141
- window.localStorage.setItem(name, value);
1142
- } catch (err) {
1143
- _.localStorage.error(err);
1120
+ return {
1121
+ is_supported: function(forceCheck) {
1122
+ var supported = is_supported_fn(storage, forceCheck);
1123
+ if (!supported) {
1124
+ console.error(name + ' unsupported');
1125
+ }
1126
+ return supported;
1127
+ },
1128
+ error: log_error,
1129
+ get: function(key) {
1130
+ try {
1131
+ return storage.getItem(key);
1132
+ } catch (err) {
1133
+ log_error(err);
1134
+ }
1135
+ return null;
1136
+ },
1137
+ parse: function(key) {
1138
+ try {
1139
+ return _.JSONDecode(storage.getItem(key)) || {};
1140
+ } catch (err) {
1141
+ // noop
1142
+ }
1143
+ return null;
1144
+ },
1145
+ set: function(key, value) {
1146
+ try {
1147
+ storage.setItem(key, value);
1148
+ } catch (err) {
1149
+ log_error(err);
1150
+ }
1151
+ },
1152
+ remove: function(key) {
1153
+ try {
1154
+ storage.removeItem(key);
1155
+ } catch (err) {
1156
+ log_error(err);
1157
+ }
1144
1158
  }
1145
- },
1159
+ };
1160
+ }
1146
1161
 
1147
- remove: function(name) {
1148
- try {
1149
- window.localStorage.removeItem(name);
1150
- } catch (err) {
1151
- _.localStorage.error(err);
1152
- }
1153
- }
1154
- };
1162
+ _.localStorage = _storageWrapper(window.localStorage, 'localStorage', localStorageSupported);
1163
+ _.sessionStorage = _storageWrapper(window.sessionStorage, 'sessionStorage', sessionStorageSupported);
1155
1164
 
1156
1165
  _.register_event = (function() {
1157
1166
  // written by Dean Edwards, 2005
@@ -1678,6 +1687,31 @@ _.info = {
1678
1687
  }
1679
1688
  };
1680
1689
 
1690
+ /**
1691
+ * Returns a throttled function that will only run at most every `waitMs` and returns a promise that resolves with the next invocation.
1692
+ * Throttled calls will build up a batch of args and invoke the callback with all args since the last invocation.
1693
+ */
1694
+ var batchedThrottle = function (fn, waitMs) {
1695
+ var timeoutPromise = null;
1696
+ var throttledItems = [];
1697
+ return function (item) {
1698
+ var self = this;
1699
+ throttledItems.push(item);
1700
+
1701
+ if (!timeoutPromise) {
1702
+ timeoutPromise = new Promise(function (resolve) {
1703
+ setTimeout(function () {
1704
+ var returnValue = fn.apply(self, [throttledItems]);
1705
+ timeoutPromise = null;
1706
+ throttledItems = [];
1707
+ resolve(returnValue);
1708
+ }, waitMs);
1709
+ });
1710
+ }
1711
+ return timeoutPromise;
1712
+ };
1713
+ };
1714
+
1681
1715
  var cheap_guid = function(maxlen) {
1682
1716
  var guid = Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
1683
1717
  return maxlen ? guid.substring(0, maxlen) : guid;
@@ -1720,6 +1754,8 @@ var isOnline = function() {
1720
1754
  return _.isUndefined(onLine) || onLine;
1721
1755
  };
1722
1756
 
1757
+ var NOOP_FUNC = function () {};
1758
+
1723
1759
  var JSONStringify = null, JSONParse = null;
1724
1760
  if (typeof JSON !== 'undefined') {
1725
1761
  JSONStringify = JSON.stringify;
@@ -1728,22 +1764,23 @@ if (typeof JSON !== 'undefined') {
1728
1764
  JSONStringify = JSONStringify || _.JSONEncode;
1729
1765
  JSONParse = JSONParse || _.JSONDecode;
1730
1766
 
1731
- // EXPORTS (for closure compiler)
1732
- _['toArray'] = _.toArray;
1733
- _['isObject'] = _.isObject;
1734
- _['JSONEncode'] = _.JSONEncode;
1735
- _['JSONDecode'] = _.JSONDecode;
1736
- _['isBlockedUA'] = _.isBlockedUA;
1737
- _['isEmptyObject'] = _.isEmptyObject;
1767
+ // UNMINIFIED EXPORTS (for closure compiler)
1738
1768
  _['info'] = _.info;
1739
- _['info']['device'] = _.info.device;
1740
1769
  _['info']['browser'] = _.info.browser;
1741
1770
  _['info']['browserVersion'] = _.info.browserVersion;
1771
+ _['info']['device'] = _.info.device;
1742
1772
  _['info']['properties'] = _.info.properties;
1773
+ _['isBlockedUA'] = _.isBlockedUA;
1774
+ _['isEmptyObject'] = _.isEmptyObject;
1775
+ _['isObject'] = _.isObject;
1776
+ _['JSONDecode'] = _.JSONDecode;
1777
+ _['JSONEncode'] = _.JSONEncode;
1778
+ _['toArray'] = _.toArray;
1743
1779
  _['NPO'] = NpoPromise;
1744
1780
 
1745
1781
  export {
1746
1782
  _,
1783
+ batchedThrottle,
1747
1784
  cheap_guid,
1748
1785
  console_with_prefix,
1749
1786
  console,
@@ -1756,6 +1793,7 @@ export {
1756
1793
  MAX_RECORDING_MS,
1757
1794
  MAX_VALUE_FOR_MIN_RECORDING_MS,
1758
1795
  navigator,
1796
+ NOOP_FUNC,
1759
1797
  safewrap,
1760
1798
  safewrapClass,
1761
1799
  slice,