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,10 +1,11 @@
1
- import { IncrementalSource, EventType } from '@rrweb/types';
2
-
3
- import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, _} from '../utils'; // eslint-disable-line camelcase
4
1
  import { window } from '../window';
2
+ import { IncrementalSource, EventType } from '@rrweb/types';
3
+ import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, NOOP_FUNC, _, localStorageSupported} from '../utils'; // eslint-disable-line camelcase
4
+ import { IDBStorageWrapper, RECORDING_EVENTS_STORE_NAME } from '../storage/indexed-db';
5
5
  import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
6
6
  import { RequestBatcher } from '../request-batcher';
7
7
  import Config from '../config';
8
+ import { RECORD_ENQUEUE_THROTTLE_MS } from './utils';
8
9
 
9
10
  var logger = console_with_prefix('recorder');
10
11
  var CompressionStream = window['CompressionStream'];
@@ -32,29 +33,58 @@ function isUserEvent(ev) {
32
33
  return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
33
34
  }
34
35
 
36
+ /**
37
+ * @typedef {Object} SerializedRecording
38
+ * @property {number} idleExpires
39
+ * @property {number} maxExpires
40
+ * @property {number} replayStartTime
41
+ * @property {number} seqNo
42
+ * @property {string} batchStartUrl
43
+ * @property {string} replayId
44
+ * @property {string} tabId
45
+ * @property {string} replayStartUrl
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} SessionRecordingOptions
50
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
51
+ * @property {String} [options.replayId] - unique uuid for a single replay
52
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
53
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
54
+ * @property {Function} [options.rrwebRecord] - rrweb's `record` function
55
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
56
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
57
+ * optional properties for deserialization:
58
+ * @property {number} idleExpires
59
+ * @property {number} maxExpires
60
+ * @property {number} replayStartTime
61
+ * @property {number} seqNo
62
+ * @property {string} batchStartUrl
63
+ * @property {string} replayStartUrl
64
+ */
65
+
66
+
35
67
  /**
36
68
  * This class encapsulates a single session recording and its lifecycle.
37
- * @param {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
38
- * @param {String} [options.replayId] - unique uuid for a single replay
39
- * @param {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
40
- * @param {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
41
- * @param {Function} [options.rrwebRecord] - rrweb's `record` function
69
+ * @param {SessionRecordingOptions} options
42
70
  */
43
71
  var SessionRecording = function(options) {
44
72
  this._mixpanel = options.mixpanelInstance;
45
- this._onIdleTimeout = options.onIdleTimeout;
46
- this._onMaxLengthReached = options.onMaxLengthReached;
47
- this._rrwebRecord = options.rrwebRecord;
48
-
49
- this.replayId = options.replayId;
73
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
74
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
75
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
76
+ this._rrwebRecord = options.rrwebRecord || null;
50
77
 
51
78
  // internal rrweb stopRecording function
52
79
  this._stopRecording = null;
80
+ this.replayId = options.replayId;
53
81
 
54
- this.seqNo = 0;
55
- this.replayStartTime = null;
56
- this.replayStartUrl = null;
57
- this.batchStartUrl = null;
82
+ this.batchStartUrl = options.batchStartUrl || null;
83
+ this.replayStartUrl = options.replayStartUrl || null;
84
+ this.idleExpires = options.idleExpires || null;
85
+ this.maxExpires = options.maxExpires || null;
86
+ this.replayStartTime = options.replayStartTime || null;
87
+ this.seqNo = options.seqNo || 0;
58
88
 
59
89
  this.idleTimeoutId = null;
60
90
  this.maxTimeoutId = null;
@@ -62,18 +92,40 @@ var SessionRecording = function(options) {
62
92
  this.recordMaxMs = MAX_RECORDING_MS;
63
93
  this.recordMinMs = 0;
64
94
 
95
+ // disable persistence if localStorage is not supported
96
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
97
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true);
98
+
65
99
  // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
66
100
  // this will be important when persistence is introduced
67
- var batcherKey = '__mprec_' + this.getConfig('token') + '_' + this.replayId;
68
- this.batcher = new RequestBatcher(batcherKey, {
69
- errorReporter: _.bind(this.reportError, this),
101
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
102
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
103
+ this.batcher = new RequestBatcher(this.batcherKey, {
104
+ errorReporter: this.reportError.bind(this),
70
105
  flushOnlyOnInterval: true,
71
106
  libConfig: RECORDER_BATCHER_LIB_CONFIG,
72
- sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
73
- usePersistence: false
107
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
108
+ queueStorage: this.queueStorage,
109
+ sharedLockStorage: options.sharedLockStorage,
110
+ usePersistence: usePersistence,
111
+ stopAllBatchingFunc: this.stopRecording.bind(this),
112
+
113
+ // increased throttle and shared lock timeout because recording events are very high frequency.
114
+ // this will minimize the amount of lock contention between enqueued events.
115
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
116
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
117
+ sharedLockTimeoutMS: 10 * 1000,
74
118
  });
75
119
  };
76
120
 
121
+ SessionRecording.prototype.unloadPersistedData = function () {
122
+ this.batcher.stop();
123
+ return this.batcher.flush()
124
+ .then(function () {
125
+ return this.queueStorage.removeItem(this.batcherKey);
126
+ }.bind(this));
127
+ };
128
+
77
129
  SessionRecording.prototype.getConfig = function(configVar) {
78
130
  return this._mixpanel.get_config(configVar);
79
131
  };
@@ -86,6 +138,11 @@ SessionRecording.prototype.get_config = function(configVar) {
86
138
  };
87
139
 
88
140
  SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
141
+ if (this._rrwebRecord === null) {
142
+ this.reportError('rrweb record function not provided. ');
143
+ return;
144
+ }
145
+
89
146
  if (this._stopRecording !== null) {
90
147
  logger.log('Recording already in progress, skipping startRecording.');
91
148
  return;
@@ -97,15 +154,21 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
97
154
  logger.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
98
155
  }
99
156
 
157
+ if (!this.maxExpires) {
158
+ this.maxExpires = new Date().getTime() + this.recordMaxMs;
159
+ }
160
+
100
161
  this.recordMinMs = this.getConfig('record_min_ms');
101
162
  if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
102
163
  this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
103
164
  logger.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
104
165
  }
105
166
 
106
- this.replayStartTime = new Date().getTime();
107
- this.batchStartUrl = _.info.currentUrl();
108
- this.replayStartUrl = _.info.currentUrl();
167
+ if (!this.replayStartTime) {
168
+ this.replayStartTime = new Date().getTime();
169
+ this.batchStartUrl = _.info.currentUrl();
170
+ this.replayStartUrl = _.info.currentUrl();
171
+ }
109
172
 
110
173
  if (shouldStopBatcher || this.recordMinMs > 0) {
111
174
  // the primary case for shouldStopBatcher is when we're starting recording after a reset
@@ -118,10 +181,12 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
118
181
  this.batcher.start();
119
182
  }
120
183
 
121
- var resetIdleTimeout = _.bind(function () {
184
+ var resetIdleTimeout = function () {
122
185
  clearTimeout(this.idleTimeoutId);
123
- this.idleTimeoutId = setTimeout(this._onIdleTimeout, this.getConfig('record_idle_timeout_ms'));
124
- }, this);
186
+ var idleTimeoutMs = this.getConfig('record_idle_timeout_ms');
187
+ this.idleTimeoutId = setTimeout(this._onIdleTimeout, idleTimeoutMs);
188
+ this.idleExpires = new Date().getTime() + idleTimeoutMs;
189
+ }.bind(this);
125
190
 
126
191
  var blockSelector = this.getConfig('record_block_selector');
127
192
  if (blockSelector === '' || blockSelector === null) {
@@ -129,8 +194,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
129
194
  }
130
195
 
131
196
  this._stopRecording = this._rrwebRecord({
132
- 'emit': _.bind(function (ev) {
133
- this.batcher.enqueue(ev);
197
+ 'emit': addOptOutCheckMixpanelLib(function (ev) {
134
198
  if (isUserEvent(ev)) {
135
199
  if (this.batcher.stopped && new Date().getTime() - this.replayStartTime >= this.recordMinMs) {
136
200
  // start flushing again after user activity
@@ -138,7 +202,10 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
138
202
  }
139
203
  resetIdleTimeout();
140
204
  }
141
- }, this),
205
+
206
+ // promise only used to await during tests
207
+ this.__enqueuePromise = this.batcher.enqueue(ev);
208
+ }.bind(this)),
142
209
  'blockClass': this.getConfig('record_block_class'),
143
210
  'blockSelector': blockSelector,
144
211
  'collectFonts': this.getConfig('record_collect_fonts'),
@@ -164,10 +231,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
164
231
 
165
232
  resetIdleTimeout();
166
233
 
167
- this.maxTimeoutId = setTimeout(_.bind(this._onMaxLengthReached, this), this.recordMaxMs);
234
+ var maxTimeoutMs = this.maxExpires - new Date().getTime();
235
+ this.maxTimeoutId = setTimeout(this._onMaxLengthReached.bind(this), maxTimeoutMs);
168
236
  };
169
237
 
170
- SessionRecording.prototype.stopRecording = function () {
238
+ SessionRecording.prototype.stopRecording = function (skipFlush) {
171
239
  if (!this.isRrwebStopped()) {
172
240
  try {
173
241
  this._stopRecording();
@@ -177,17 +245,19 @@ SessionRecording.prototype.stopRecording = function () {
177
245
  this._stopRecording = null;
178
246
  }
179
247
 
248
+ var flushPromise;
180
249
  if (this.batcher.stopped) {
181
250
  // never got user activity to flush after reset, so just clear the batcher
182
- this.batcher.clear();
183
- } else {
251
+ flushPromise = this.batcher.clear();
252
+ } else if (!skipFlush) {
184
253
  // flush any remaining events from running batcher
185
- this.batcher.flush();
186
- this.batcher.stop();
254
+ flushPromise = this.batcher.flush();
187
255
  }
256
+ this.batcher.stop();
188
257
 
189
258
  clearTimeout(this.idleTimeoutId);
190
259
  clearTimeout(this.maxTimeoutId);
260
+ return flushPromise;
191
261
  };
192
262
 
193
263
  SessionRecording.prototype.isRrwebStopped = function () {
@@ -199,7 +269,54 @@ SessionRecording.prototype.isRrwebStopped = function () {
199
269
  * we stop recording and dump any queued events if the user has opted out.
200
270
  */
201
271
  SessionRecording.prototype.flushEventsWithOptOut = function (data, options, cb) {
202
- this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
272
+ this._flushEvents(data, options, cb, this._onOptOut.bind(this));
273
+ };
274
+
275
+ /**
276
+ * @returns {SerializedRecording}
277
+ */
278
+ SessionRecording.prototype.serialize = function () {
279
+ // don't break if mixpanel instance was destroyed at some point
280
+ var tabId;
281
+ try {
282
+ tabId = this._mixpanel.get_tab_id();
283
+ } catch (e) {
284
+ this.reportError('Error getting tab ID for serialization ', e);
285
+ tabId = null;
286
+ }
287
+
288
+ return {
289
+ 'replayId': this.replayId,
290
+ 'seqNo': this.seqNo,
291
+ 'replayStartTime': this.replayStartTime,
292
+ 'batchStartUrl': this.batchStartUrl,
293
+ 'replayStartUrl': this.replayStartUrl,
294
+ 'idleExpires': this.idleExpires,
295
+ 'maxExpires': this.maxExpires,
296
+ 'tabId': tabId,
297
+ };
298
+ };
299
+
300
+
301
+ /**
302
+ * @static
303
+ * @param {SerializedRecording} serializedRecording
304
+ * @param {SessionRecordingOptions} options
305
+ * @returns {SessionRecording}
306
+ */
307
+ SessionRecording.deserialize = function (serializedRecording, options) {
308
+ var recording = new SessionRecording(_.extend({}, options, {
309
+ replayId: serializedRecording['replayId'],
310
+ batchStartUrl: serializedRecording['batchStartUrl'],
311
+ replayStartUrl: serializedRecording['replayStartUrl'],
312
+ idleExpires: serializedRecording['idleExpires'],
313
+ maxExpires: serializedRecording['maxExpires'],
314
+ replayStartTime: serializedRecording['replayStartTime'],
315
+ seqNo: serializedRecording['seqNo'],
316
+ sharedLockStorage: options.sharedLockStorage,
317
+ }));
318
+
319
+ return recording;
203
320
  };
204
321
 
205
322
  SessionRecording.prototype._onOptOut = function (code) {
@@ -210,7 +327,7 @@ SessionRecording.prototype._onOptOut = function (code) {
210
327
  };
211
328
 
212
329
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
213
- var onSuccess = _.bind(function (response, responseBody) {
330
+ var onSuccess = function (response, responseBody) {
214
331
  // Update batch specific props only if the request was successful to guarantee ordering.
215
332
  // RequestBatcher will always flush the next batch after the previous one succeeds.
216
333
  // extra check to see if the replay ID has changed so that we don't increment the seqNo on the wrong replay
@@ -218,13 +335,15 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
218
335
  this.seqNo++;
219
336
  this.batchStartUrl = _.info.currentUrl();
220
337
  }
338
+
339
+ this._onBatchSent();
221
340
  callback({
222
341
  status: 0,
223
342
  httpStatusCode: response.status,
224
343
  responseBody: responseBody,
225
344
  retryAfter: response.headers.get('Retry-After')
226
345
  });
227
- }, this);
346
+ }.bind(this);
228
347
 
229
348
  window['fetch'](this.getConfig('api_host') + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
230
349
  'method': 'POST',
@@ -245,7 +364,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
245
364
  };
246
365
 
247
366
  SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
248
- const numEvents = data.length;
367
+ var numEvents = data.length;
249
368
 
250
369
  if (numEvents > 0) {
251
370
  var replayId = this.replayId;
@@ -290,10 +409,10 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
290
409
  var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
291
410
  new Response(gzipStream)
292
411
  .blob()
293
- .then(_.bind(function(compressedBlob) {
412
+ .then(function(compressedBlob) {
294
413
  reqParams['format'] = 'gzip';
295
414
  this._sendRequest(replayId, reqParams, compressedBlob, callback);
296
- }, this));
415
+ }.bind(this));
297
416
  } else {
298
417
  reqParams['format'] = 'body';
299
418
  this._sendRequest(replayId, reqParams, eventsJson, callback);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
3
+ * @returns {boolean}
4
+ */
5
+ var isRecordingExpired = function(serializedRecording) {
6
+ var now = Date.now();
7
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
8
+ };
9
+
10
+ var RECORD_ENQUEUE_THROTTLE_MS = 250;
11
+
12
+ export { isRecordingExpired, RECORD_ENQUEUE_THROTTLE_MS};
@@ -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 };