mixpanel-browser 2.56.0 → 2.58.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,11 +2,9 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.56.0'
5
+ LIB_VERSION: '2.58.0'
6
6
  };
7
7
 
8
- /* eslint camelcase: "off", eqeqeq: "off" */
9
-
10
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
11
9
  var win;
12
10
  if (typeof(window) === 'undefined') {
@@ -26,6 +24,370 @@ if (typeof(window) === 'undefined') {
26
24
  win = window;
27
25
  }
28
26
 
27
+ var setImmediate = win['setImmediate'];
28
+ var builtInProp, cycle, schedulingQueue,
29
+ ToString = Object.prototype.toString,
30
+ timer = (typeof setImmediate !== 'undefined') ?
31
+ function timer(fn) { return setImmediate(fn); } :
32
+ setTimeout;
33
+
34
+ // dammit, IE8.
35
+ try {
36
+ Object.defineProperty({},'x',{});
37
+ builtInProp = function builtInProp(obj,name,val,config) {
38
+ return Object.defineProperty(obj,name,{
39
+ value: val,
40
+ writable: true,
41
+ configurable: config !== false
42
+ });
43
+ };
44
+ }
45
+ catch (err) {
46
+ builtInProp = function builtInProp(obj,name,val) {
47
+ obj[name] = val;
48
+ return obj;
49
+ };
50
+ }
51
+
52
+ // Note: using a queue instead of array for efficiency
53
+ schedulingQueue = (function Queue() {
54
+ var first, last, item;
55
+
56
+ function Item(fn,self) {
57
+ this.fn = fn;
58
+ this.self = self;
59
+ this.next = void 0;
60
+ }
61
+
62
+ return {
63
+ add: function add(fn,self) {
64
+ item = new Item(fn,self);
65
+ if (last) {
66
+ last.next = item;
67
+ }
68
+ else {
69
+ first = item;
70
+ }
71
+ last = item;
72
+ item = void 0;
73
+ },
74
+ drain: function drain() {
75
+ var f = first;
76
+ first = last = cycle = void 0;
77
+
78
+ while (f) {
79
+ f.fn.call(f.self);
80
+ f = f.next;
81
+ }
82
+ }
83
+ };
84
+ })();
85
+
86
+ function schedule(fn,self) {
87
+ schedulingQueue.add(fn,self);
88
+ if (!cycle) {
89
+ cycle = timer(schedulingQueue.drain);
90
+ }
91
+ }
92
+
93
+ // promise duck typing
94
+ function isThenable(o) {
95
+ var _then, oType = typeof o;
96
+
97
+ if (o !== null && (oType === 'object' || oType === 'function')) {
98
+ _then = o.then;
99
+ }
100
+ return typeof _then === 'function' ? _then : false;
101
+ }
102
+
103
+ function notify() {
104
+ for (var i=0; i<this.chain.length; i++) {
105
+ notifyIsolated(
106
+ this,
107
+ (this.state === 1) ? this.chain[i].success : this.chain[i].failure,
108
+ this.chain[i]
109
+ );
110
+ }
111
+ this.chain.length = 0;
112
+ }
113
+
114
+ // NOTE: This is a separate function to isolate
115
+ // the `try..catch` so that other code can be
116
+ // optimized better
117
+ function notifyIsolated(self,cb,chain) {
118
+ var ret, _then;
119
+ try {
120
+ if (cb === false) {
121
+ chain.reject(self.msg);
122
+ }
123
+ else {
124
+ if (cb === true) {
125
+ ret = self.msg;
126
+ }
127
+ else {
128
+ ret = cb.call(void 0,self.msg);
129
+ }
130
+
131
+ if (ret === chain.promise) {
132
+ chain.reject(TypeError('Promise-chain cycle'));
133
+ }
134
+ // eslint-disable-next-line no-cond-assign
135
+ else if (_then = isThenable(ret)) {
136
+ _then.call(ret,chain.resolve,chain.reject);
137
+ }
138
+ else {
139
+ chain.resolve(ret);
140
+ }
141
+ }
142
+ }
143
+ catch (err) {
144
+ chain.reject(err);
145
+ }
146
+ }
147
+
148
+ function resolve(msg) {
149
+ var _then, self = this;
150
+
151
+ // already triggered?
152
+ if (self.triggered) { return; }
153
+
154
+ self.triggered = true;
155
+
156
+ // unwrap
157
+ if (self.def) {
158
+ self = self.def;
159
+ }
160
+
161
+ try {
162
+ // eslint-disable-next-line no-cond-assign
163
+ if (_then = isThenable(msg)) {
164
+ schedule(function(){
165
+ var defWrapper = new MakeDefWrapper(self);
166
+ try {
167
+ _then.call(msg,
168
+ function $resolve$(){ resolve.apply(defWrapper,arguments); },
169
+ function $reject$(){ reject.apply(defWrapper,arguments); }
170
+ );
171
+ }
172
+ catch (err) {
173
+ reject.call(defWrapper,err);
174
+ }
175
+ });
176
+ }
177
+ else {
178
+ self.msg = msg;
179
+ self.state = 1;
180
+ if (self.chain.length > 0) {
181
+ schedule(notify,self);
182
+ }
183
+ }
184
+ }
185
+ catch (err) {
186
+ reject.call(new MakeDefWrapper(self),err);
187
+ }
188
+ }
189
+
190
+ function reject(msg) {
191
+ var self = this;
192
+
193
+ // already triggered?
194
+ if (self.triggered) { return; }
195
+
196
+ self.triggered = true;
197
+
198
+ // unwrap
199
+ if (self.def) {
200
+ self = self.def;
201
+ }
202
+
203
+ self.msg = msg;
204
+ self.state = 2;
205
+ if (self.chain.length > 0) {
206
+ schedule(notify,self);
207
+ }
208
+ }
209
+
210
+ function iteratePromises(Constructor,arr,resolver,rejecter) {
211
+ for (var idx=0; idx<arr.length; idx++) {
212
+ (function IIFE(idx){
213
+ Constructor.resolve(arr[idx])
214
+ .then(
215
+ function $resolver$(msg){
216
+ resolver(idx,msg);
217
+ },
218
+ rejecter
219
+ );
220
+ })(idx);
221
+ }
222
+ }
223
+
224
+ function MakeDefWrapper(self) {
225
+ this.def = self;
226
+ this.triggered = false;
227
+ }
228
+
229
+ function MakeDef(self) {
230
+ this.promise = self;
231
+ this.state = 0;
232
+ this.triggered = false;
233
+ this.chain = [];
234
+ this.msg = void 0;
235
+ }
236
+
237
+ function NpoPromise(executor) {
238
+ if (typeof executor !== 'function') {
239
+ throw TypeError('Not a function');
240
+ }
241
+
242
+ if (this['__NPO__'] !== 0) {
243
+ throw TypeError('Not a promise');
244
+ }
245
+
246
+ // instance shadowing the inherited "brand"
247
+ // to signal an already "initialized" promise
248
+ this['__NPO__'] = 1;
249
+
250
+ var def = new MakeDef(this);
251
+
252
+ this['then'] = function then(success,failure) {
253
+ var o = {
254
+ success: typeof success === 'function' ? success : true,
255
+ failure: typeof failure === 'function' ? failure : false
256
+ };
257
+ // Note: `then(..)` itself can be borrowed to be used against
258
+ // a different promise constructor for making the chained promise,
259
+ // by substituting a different `this` binding.
260
+ o.promise = new this.constructor(function extractChain(resolve,reject) {
261
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
262
+ throw TypeError('Not a function');
263
+ }
264
+
265
+ o.resolve = resolve;
266
+ o.reject = reject;
267
+ });
268
+ def.chain.push(o);
269
+
270
+ if (def.state !== 0) {
271
+ schedule(notify,def);
272
+ }
273
+
274
+ return o.promise;
275
+ };
276
+ this['catch'] = function $catch$(failure) {
277
+ return this.then(void 0,failure);
278
+ };
279
+
280
+ try {
281
+ executor.call(
282
+ void 0,
283
+ function publicResolve(msg){
284
+ resolve.call(def,msg);
285
+ },
286
+ function publicReject(msg) {
287
+ reject.call(def,msg);
288
+ }
289
+ );
290
+ }
291
+ catch (err) {
292
+ reject.call(def,err);
293
+ }
294
+ }
295
+
296
+ var PromisePrototype = builtInProp({},'constructor',NpoPromise,
297
+ /*configurable=*/false
298
+ );
299
+
300
+ // Note: Android 4 cannot use `Object.defineProperty(..)` here
301
+ NpoPromise.prototype = PromisePrototype;
302
+
303
+ // built-in "brand" to signal an "uninitialized" promise
304
+ builtInProp(PromisePrototype,'__NPO__',0,
305
+ /*configurable=*/false
306
+ );
307
+
308
+ builtInProp(NpoPromise,'resolve',function Promise$resolve(msg) {
309
+ var Constructor = this;
310
+
311
+ // spec mandated checks
312
+ // note: best "isPromise" check that's practical for now
313
+ if (msg && typeof msg === 'object' && msg['__NPO__'] === 1) {
314
+ return msg;
315
+ }
316
+
317
+ return new Constructor(function executor(resolve,reject){
318
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
319
+ throw TypeError('Not a function');
320
+ }
321
+
322
+ resolve(msg);
323
+ });
324
+ });
325
+
326
+ builtInProp(NpoPromise,'reject',function Promise$reject(msg) {
327
+ return new this(function executor(resolve,reject){
328
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
329
+ throw TypeError('Not a function');
330
+ }
331
+
332
+ reject(msg);
333
+ });
334
+ });
335
+
336
+ builtInProp(NpoPromise,'all',function Promise$all(arr) {
337
+ var Constructor = this;
338
+
339
+ // spec mandated checks
340
+ if (ToString.call(arr) !== '[object Array]') {
341
+ return Constructor.reject(TypeError('Not an array'));
342
+ }
343
+ if (arr.length === 0) {
344
+ return Constructor.resolve([]);
345
+ }
346
+
347
+ return new Constructor(function executor(resolve,reject){
348
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
349
+ throw TypeError('Not a function');
350
+ }
351
+
352
+ var len = arr.length, msgs = Array(len), count = 0;
353
+
354
+ iteratePromises(Constructor,arr,function resolver(idx,msg) {
355
+ msgs[idx] = msg;
356
+ if (++count === len) {
357
+ resolve(msgs);
358
+ }
359
+ },reject);
360
+ });
361
+ });
362
+
363
+ builtInProp(NpoPromise,'race',function Promise$race(arr) {
364
+ var Constructor = this;
365
+
366
+ // spec mandated checks
367
+ if (ToString.call(arr) !== '[object Array]') {
368
+ return Constructor.reject(TypeError('Not an array'));
369
+ }
370
+
371
+ return new Constructor(function executor(resolve,reject){
372
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
373
+ throw TypeError('Not a function');
374
+ }
375
+
376
+ iteratePromises(Constructor,arr,function resolver(idx,msg){
377
+ resolve(msg);
378
+ },reject);
379
+ });
380
+ });
381
+
382
+ var PromisePolyfill;
383
+ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) {
384
+ PromisePolyfill = Promise;
385
+ } else {
386
+ PromisePolyfill = NpoPromise;
387
+ }
388
+
389
+ /* eslint camelcase: "off", eqeqeq: "off" */
390
+
29
391
  // Maximum allowed session recording length
30
392
  var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
31
393
 
@@ -1085,7 +1447,7 @@ var localStorageSupported = function(storage, forceCheck) {
1085
1447
 
1086
1448
  var supported = true;
1087
1449
  try {
1088
- storage = storage || window.localStorage;
1450
+ storage = storage || win.localStorage;
1089
1451
  var key = '__mplss_' + cheap_guid(8),
1090
1452
  val = 'xyz';
1091
1453
  storage.setItem(key, val);
@@ -1117,7 +1479,7 @@ _.localStorage = {
1117
1479
 
1118
1480
  get: function(name) {
1119
1481
  try {
1120
- return window.localStorage.getItem(name);
1482
+ return win.localStorage.getItem(name);
1121
1483
  } catch (err) {
1122
1484
  _.localStorage.error(err);
1123
1485
  }
@@ -1135,7 +1497,7 @@ _.localStorage = {
1135
1497
 
1136
1498
  set: function(name, value) {
1137
1499
  try {
1138
- window.localStorage.setItem(name, value);
1500
+ win.localStorage.setItem(name, value);
1139
1501
  } catch (err) {
1140
1502
  _.localStorage.error(err);
1141
1503
  }
@@ -1143,7 +1505,7 @@ _.localStorage = {
1143
1505
 
1144
1506
  remove: function(name) {
1145
1507
  try {
1146
- window.localStorage.removeItem(name);
1508
+ win.localStorage.removeItem(name);
1147
1509
  } catch (err) {
1148
1510
  _.localStorage.error(err);
1149
1511
  }
@@ -1182,7 +1544,7 @@ _.register_event = (function() {
1182
1544
 
1183
1545
  function makeHandler(element, new_handler, old_handlers) {
1184
1546
  var handler = function(event) {
1185
- event = event || fixEvent(window.event);
1547
+ event = event || fixEvent(win.event);
1186
1548
 
1187
1549
  // this basically happens in firefox whenever another script
1188
1550
  // overwrites the onload callback and doesn't pass the event
@@ -1737,6 +2099,7 @@ _['info']['device'] = _.info.device;
1737
2099
  _['info']['browser'] = _.info.browser;
1738
2100
  _['info']['browserVersion'] = _.info.browserVersion;
1739
2101
  _['info']['properties'] = _.info.properties;
2102
+ _['NPO'] = NpoPromise;
1740
2103
 
1741
2104
  /* eslint camelcase: "off" */
1742
2105
 
@@ -1918,121 +2281,175 @@ var SharedLock = function(key, options) {
1918
2281
  this.storage = options.storage || window.localStorage;
1919
2282
  this.pollIntervalMS = options.pollIntervalMS || 100;
1920
2283
  this.timeoutMS = options.timeoutMS || 2000;
2284
+
2285
+ // dependency-inject promise implementation for testing purposes
2286
+ this.promiseImpl = options.promiseImpl || PromisePolyfill;
1921
2287
  };
1922
2288
 
1923
2289
  // pass in a specific pid to test contention scenarios; otherwise
1924
2290
  // it is chosen randomly for each acquisition attempt
1925
- SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) {
1926
- if (!pid && typeof errorCB !== 'function') {
1927
- pid = errorCB;
1928
- errorCB = null;
1929
- }
1930
-
1931
- var i = pid || (new Date().getTime() + '|' + Math.random());
1932
- var startTime = new Date().getTime();
2291
+ SharedLock.prototype.withLock = function(lockedCB, pid) {
2292
+ var Promise = this.promiseImpl;
2293
+ return new Promise(_.bind(function (resolve, reject) {
2294
+ var i = pid || (new Date().getTime() + '|' + Math.random());
2295
+ var startTime = new Date().getTime();
1933
2296
 
1934
- var key = this.storageKey;
1935
- var pollIntervalMS = this.pollIntervalMS;
1936
- var timeoutMS = this.timeoutMS;
1937
- var storage = this.storage;
2297
+ var key = this.storageKey;
2298
+ var pollIntervalMS = this.pollIntervalMS;
2299
+ var timeoutMS = this.timeoutMS;
2300
+ var storage = this.storage;
1938
2301
 
1939
- var keyX = key + ':X';
1940
- var keyY = key + ':Y';
1941
- var keyZ = key + ':Z';
2302
+ var keyX = key + ':X';
2303
+ var keyY = key + ':Y';
2304
+ var keyZ = key + ':Z';
1942
2305
 
1943
- var reportError = function(err) {
1944
- errorCB && errorCB(err);
1945
- };
2306
+ var delay = function(cb) {
2307
+ if (new Date().getTime() - startTime > timeoutMS) {
2308
+ logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
2309
+ storage.removeItem(keyZ);
2310
+ storage.removeItem(keyY);
2311
+ loop();
2312
+ return;
2313
+ }
2314
+ setTimeout(function() {
2315
+ try {
2316
+ cb();
2317
+ } catch(err) {
2318
+ reject(err);
2319
+ }
2320
+ }, pollIntervalMS * (Math.random() + 0.1));
2321
+ };
1946
2322
 
1947
- var delay = function(cb) {
1948
- if (new Date().getTime() - startTime > timeoutMS) {
1949
- logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
1950
- storage.removeItem(keyZ);
1951
- storage.removeItem(keyY);
1952
- loop();
1953
- return;
1954
- }
1955
- setTimeout(function() {
1956
- try {
2323
+ var waitFor = function(predicate, cb) {
2324
+ if (predicate()) {
1957
2325
  cb();
1958
- } catch(err) {
1959
- reportError(err);
2326
+ } else {
2327
+ delay(function() {
2328
+ waitFor(predicate, cb);
2329
+ });
1960
2330
  }
1961
- }, pollIntervalMS * (Math.random() + 0.1));
1962
- };
1963
-
1964
- var waitFor = function(predicate, cb) {
1965
- if (predicate()) {
1966
- cb();
1967
- } else {
1968
- delay(function() {
1969
- waitFor(predicate, cb);
1970
- });
1971
- }
1972
- };
2331
+ };
1973
2332
 
1974
- var getSetY = function() {
1975
- var valY = storage.getItem(keyY);
1976
- if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
1977
- return false;
1978
- } else {
1979
- storage.setItem(keyY, i);
1980
- if (storage.getItem(keyY) === i) {
1981
- return true;
2333
+ var getSetY = function() {
2334
+ var valY = storage.getItem(keyY);
2335
+ if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
2336
+ return false;
1982
2337
  } else {
1983
- if (!localStorageSupported(storage, true)) {
1984
- throw new Error('localStorage support dropped while acquiring lock');
2338
+ storage.setItem(keyY, i);
2339
+ if (storage.getItem(keyY) === i) {
2340
+ return true;
2341
+ } else {
2342
+ if (!localStorageSupported(storage, true)) {
2343
+ reject(new Error('localStorage support dropped while acquiring lock'));
2344
+ }
2345
+ return false;
1985
2346
  }
1986
- return false;
1987
2347
  }
1988
- }
1989
- };
1990
-
1991
- var loop = function() {
1992
- storage.setItem(keyX, i);
2348
+ };
1993
2349
 
1994
- waitFor(getSetY, function() {
1995
- if (storage.getItem(keyX) === i) {
1996
- criticalSection();
1997
- return;
1998
- }
2350
+ var loop = function() {
2351
+ storage.setItem(keyX, i);
1999
2352
 
2000
- delay(function() {
2001
- if (storage.getItem(keyY) !== i) {
2002
- loop();
2353
+ waitFor(getSetY, function() {
2354
+ if (storage.getItem(keyX) === i) {
2355
+ criticalSection();
2003
2356
  return;
2004
2357
  }
2005
- waitFor(function() {
2006
- return !storage.getItem(keyZ);
2007
- }, criticalSection);
2358
+
2359
+ delay(function() {
2360
+ if (storage.getItem(keyY) !== i) {
2361
+ loop();
2362
+ return;
2363
+ }
2364
+ waitFor(function() {
2365
+ return !storage.getItem(keyZ);
2366
+ }, criticalSection);
2367
+ });
2008
2368
  });
2009
- });
2010
- };
2369
+ };
2370
+
2371
+ var criticalSection = function() {
2372
+ storage.setItem(keyZ, '1');
2373
+ var removeLock = function () {
2374
+ storage.removeItem(keyZ);
2375
+ if (storage.getItem(keyY) === i) {
2376
+ storage.removeItem(keyY);
2377
+ }
2378
+ if (storage.getItem(keyX) === i) {
2379
+ storage.removeItem(keyX);
2380
+ }
2381
+ };
2382
+
2383
+ lockedCB()
2384
+ .then(function (ret) {
2385
+ removeLock();
2386
+ resolve(ret);
2387
+ })
2388
+ .catch(function (err) {
2389
+ removeLock();
2390
+ reject(err);
2391
+ });
2392
+ };
2011
2393
 
2012
- var criticalSection = function() {
2013
- storage.setItem(keyZ, '1');
2014
2394
  try {
2015
- lockedCB();
2016
- } finally {
2017
- storage.removeItem(keyZ);
2018
- if (storage.getItem(keyY) === i) {
2019
- storage.removeItem(keyY);
2020
- }
2021
- if (storage.getItem(keyX) === i) {
2022
- storage.removeItem(keyX);
2395
+ if (localStorageSupported(storage, true)) {
2396
+ loop();
2397
+ } else {
2398
+ throw new Error('localStorage support check failed');
2023
2399
  }
2400
+ } catch(err) {
2401
+ reject(err);
2024
2402
  }
2025
- };
2403
+ }, this));
2404
+ };
2026
2405
 
2027
- try {
2028
- if (localStorageSupported(storage, true)) {
2029
- loop();
2030
- } else {
2031
- throw new Error('localStorage support check failed');
2406
+ /**
2407
+ * @typedef {import('./wrapper').StorageWrapper}
2408
+ */
2409
+
2410
+ /**
2411
+ * @type {StorageWrapper}
2412
+ */
2413
+ var LocalStorageWrapper = function (storageOverride) {
2414
+ this.storage = storageOverride || localStorage;
2415
+ };
2416
+
2417
+ LocalStorageWrapper.prototype.init = function () {
2418
+ return PromisePolyfill.resolve();
2419
+ };
2420
+
2421
+ LocalStorageWrapper.prototype.setItem = function (key, value) {
2422
+ return new PromisePolyfill(_.bind(function (resolve, reject) {
2423
+ try {
2424
+ this.storage.setItem(key, value);
2425
+ } catch (e) {
2426
+ reject(e);
2032
2427
  }
2033
- } catch(err) {
2034
- reportError(err);
2035
- }
2428
+ resolve();
2429
+ }, this));
2430
+ };
2431
+
2432
+ LocalStorageWrapper.prototype.getItem = function (key) {
2433
+ return new PromisePolyfill(_.bind(function (resolve, reject) {
2434
+ var item;
2435
+ try {
2436
+ item = this.storage.getItem(key);
2437
+ } catch (e) {
2438
+ reject(e);
2439
+ }
2440
+ resolve(item);
2441
+ }, this));
2442
+ };
2443
+
2444
+ LocalStorageWrapper.prototype.removeItem = function (key) {
2445
+ return new PromisePolyfill(_.bind(function (resolve, reject) {
2446
+ try {
2447
+ this.storage.removeItem(key);
2448
+ } catch (e) {
2449
+ reject(e);
2450
+ }
2451
+ resolve();
2452
+ }, this));
2036
2453
  };
2037
2454
 
2038
2455
  var logger$1 = console_with_prefix('batch');
@@ -2053,19 +2470,38 @@ var logger$1 = console_with_prefix('batch');
2053
2470
  * to data loss in some situations).
2054
2471
  * @constructor
2055
2472
  */
2056
- var RequestQueue = function(storageKey, options) {
2473
+ var RequestQueue = function (storageKey, options) {
2057
2474
  options = options || {};
2058
2475
  this.storageKey = storageKey;
2059
2476
  this.usePersistence = options.usePersistence;
2060
2477
  if (this.usePersistence) {
2061
- this.storage = options.storage || window.localStorage;
2062
- this.lock = new SharedLock(storageKey, {storage: this.storage});
2478
+ this.queueStorage = options.queueStorage || new LocalStorageWrapper();
2479
+ this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
2480
+ this.queueStorage.init();
2063
2481
  }
2064
2482
  this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
2065
2483
 
2066
2484
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
2067
2485
 
2068
2486
  this.memQueue = [];
2487
+ this.initialized = false;
2488
+ };
2489
+
2490
+ RequestQueue.prototype.ensureInit = function () {
2491
+ if (this.initialized) {
2492
+ return PromisePolyfill.resolve();
2493
+ }
2494
+
2495
+ return this.queueStorage
2496
+ .init()
2497
+ .then(_.bind(function () {
2498
+ this.initialized = true;
2499
+ }, this))
2500
+ .catch(_.bind(function (err) {
2501
+ this.reportError('Error initializing queue persistence. Disabling persistence', err);
2502
+ this.initialized = true;
2503
+ this.usePersistence = false;
2504
+ }, this));
2069
2505
  };
2070
2506
 
2071
2507
  /**
@@ -2080,7 +2516,7 @@ var RequestQueue = function(storageKey, options) {
2080
2516
  * failure of the enqueue operation; it is asynchronous because the localStorage
2081
2517
  * lock is asynchronous.
2082
2518
  */
2083
- RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
2519
+ RequestQueue.prototype.enqueue = function (item, flushInterval) {
2084
2520
  var queueEntry = {
2085
2521
  'id': cheap_guid(),
2086
2522
  'flushAfter': new Date().getTime() + flushInterval * 2,
@@ -2089,33 +2525,37 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
2089
2525
 
2090
2526
  if (!this.usePersistence) {
2091
2527
  this.memQueue.push(queueEntry);
2092
- if (cb) {
2093
- cb(true);
2094
- }
2528
+ return PromisePolyfill.resolve(true);
2095
2529
  } else {
2096
- this.lock.withLock(_.bind(function lockAcquired() {
2097
- var succeeded;
2098
- try {
2099
- var storedQueue = this.readFromStorage();
2100
- storedQueue.push(queueEntry);
2101
- succeeded = this.saveToStorage(storedQueue);
2102
- if (succeeded) {
2530
+
2531
+ var enqueueItem = _.bind(function () {
2532
+ return this.ensureInit()
2533
+ .then(_.bind(function () {
2534
+ return this.readFromStorage();
2535
+ }, this))
2536
+ .then(_.bind(function (storedQueue) {
2537
+ storedQueue.push(queueEntry);
2538
+ return this.saveToStorage(storedQueue);
2539
+ }, this))
2540
+ .then(_.bind(function (succeeded) {
2103
2541
  // only add to in-memory queue when storage succeeds
2104
- this.memQueue.push(queueEntry);
2105
- }
2106
- } catch(err) {
2107
- this.reportError('Error enqueueing item', item);
2108
- succeeded = false;
2109
- }
2110
- if (cb) {
2111
- cb(succeeded);
2112
- }
2113
- }, this), _.bind(function lockFailure(err) {
2114
- this.reportError('Error acquiring storage lock', err);
2115
- if (cb) {
2116
- cb(false);
2117
- }
2118
- }, this), this.pid);
2542
+ if (succeeded) {
2543
+ this.memQueue.push(queueEntry);
2544
+ }
2545
+ return succeeded;
2546
+ }, this))
2547
+ .catch(_.bind(function (err) {
2548
+ this.reportError('Error enqueueing item', err, item);
2549
+ return false;
2550
+ }, this));
2551
+ }, this);
2552
+
2553
+ return this.lock
2554
+ .withLock(enqueueItem, this.pid)
2555
+ .catch(_.bind(function (err) {
2556
+ this.reportError('Error acquiring storage lock', err);
2557
+ return false;
2558
+ }, this));
2119
2559
  }
2120
2560
  };
2121
2561
 
@@ -2125,31 +2565,41 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
2125
2565
  * in the persisted queue (items where the 'flushAfter' time has
2126
2566
  * already passed).
2127
2567
  */
2128
- RequestQueue.prototype.fillBatch = function(batchSize) {
2568
+ RequestQueue.prototype.fillBatch = function (batchSize) {
2129
2569
  var batch = this.memQueue.slice(0, batchSize);
2130
2570
  if (this.usePersistence && batch.length < batchSize) {
2131
2571
  // don't need lock just to read events; localStorage is thread-safe
2132
2572
  // and the worst that could happen is a duplicate send of some
2133
2573
  // orphaned events, which will be deduplicated on the server side
2134
- var storedQueue = this.readFromStorage();
2135
- if (storedQueue.length) {
2136
- // item IDs already in batch; don't duplicate out of storage
2137
- var idsInBatch = {}; // poor man's Set
2138
- _.each(batch, function(item) { idsInBatch[item['id']] = true; });
2139
-
2140
- for (var i = 0; i < storedQueue.length; i++) {
2141
- var item = storedQueue[i];
2142
- if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
2143
- item.orphaned = true;
2144
- batch.push(item);
2145
- if (batch.length >= batchSize) {
2146
- break;
2574
+ return this.ensureInit()
2575
+ .then(_.bind(function () {
2576
+ return this.readFromStorage();
2577
+ }, this))
2578
+ .then(_.bind(function (storedQueue) {
2579
+ if (storedQueue.length) {
2580
+ // item IDs already in batch; don't duplicate out of storage
2581
+ var idsInBatch = {}; // poor man's Set
2582
+ _.each(batch, function (item) {
2583
+ idsInBatch[item['id']] = true;
2584
+ });
2585
+
2586
+ for (var i = 0; i < storedQueue.length; i++) {
2587
+ var item = storedQueue[i];
2588
+ if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
2589
+ item.orphaned = true;
2590
+ batch.push(item);
2591
+ if (batch.length >= batchSize) {
2592
+ break;
2593
+ }
2594
+ }
2147
2595
  }
2148
2596
  }
2149
- }
2150
- }
2597
+
2598
+ return batch;
2599
+ }, this));
2600
+ } else {
2601
+ return PromisePolyfill.resolve(batch);
2151
2602
  }
2152
- return batch;
2153
2603
  };
2154
2604
 
2155
2605
  /**
@@ -2157,9 +2607,9 @@ RequestQueue.prototype.fillBatch = function(batchSize) {
2157
2607
  * also remove any item without a valid id (e.g., malformed
2158
2608
  * storage entries).
2159
2609
  */
2160
- var filterOutIDsAndInvalid = function(items, idSet) {
2610
+ var filterOutIDsAndInvalid = function (items, idSet) {
2161
2611
  var filteredItems = [];
2162
- _.each(items, function(item) {
2612
+ _.each(items, function (item) {
2163
2613
  if (item['id'] && !idSet[item['id']]) {
2164
2614
  filteredItems.push(item);
2165
2615
  }
@@ -2171,78 +2621,80 @@ var filterOutIDsAndInvalid = function(items, idSet) {
2171
2621
  * Remove items with matching IDs from both in-memory queue
2172
2622
  * and persisted queue
2173
2623
  */
2174
- RequestQueue.prototype.removeItemsByID = function(ids, cb) {
2624
+ RequestQueue.prototype.removeItemsByID = function (ids) {
2175
2625
  var idSet = {}; // poor man's Set
2176
- _.each(ids, function(id) { idSet[id] = true; });
2626
+ _.each(ids, function (id) {
2627
+ idSet[id] = true;
2628
+ });
2177
2629
 
2178
2630
  this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
2179
2631
  if (!this.usePersistence) {
2180
- if (cb) {
2181
- cb(true);
2182
- }
2632
+ return PromisePolyfill.resolve(true);
2183
2633
  } else {
2184
- var removeFromStorage = _.bind(function() {
2185
- var succeeded;
2186
- try {
2187
- var storedQueue = this.readFromStorage();
2188
- storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
2189
- succeeded = this.saveToStorage(storedQueue);
2190
-
2191
- // an extra check: did storage report success but somehow
2192
- // the items are still there?
2193
- if (succeeded) {
2194
- storedQueue = this.readFromStorage();
2634
+ var removeFromStorage = _.bind(function () {
2635
+ return this.ensureInit()
2636
+ .then(_.bind(function () {
2637
+ return this.readFromStorage();
2638
+ }, this))
2639
+ .then(_.bind(function (storedQueue) {
2640
+ storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
2641
+ return this.saveToStorage(storedQueue);
2642
+ }, this))
2643
+ .then(_.bind(function () {
2644
+ return this.readFromStorage();
2645
+ }, this))
2646
+ .then(_.bind(function (storedQueue) {
2647
+ // an extra check: did storage report success but somehow
2648
+ // the items are still there?
2195
2649
  for (var i = 0; i < storedQueue.length; i++) {
2196
2650
  var item = storedQueue[i];
2197
2651
  if (item['id'] && !!idSet[item['id']]) {
2198
- this.reportError('Item not removed from storage');
2199
- return false;
2652
+ throw new Error('Item not removed from storage');
2200
2653
  }
2201
2654
  }
2202
- }
2203
- } catch(err) {
2204
- this.reportError('Error removing items', ids);
2205
- succeeded = false;
2206
- }
2207
- return succeeded;
2655
+ return true;
2656
+ }, this))
2657
+ .catch(_.bind(function (err) {
2658
+ this.reportError('Error removing items', err, ids);
2659
+ return false;
2660
+ }, this));
2208
2661
  }, this);
2209
2662
 
2210
- this.lock.withLock(function lockAcquired() {
2211
- var succeeded = removeFromStorage();
2212
- if (cb) {
2213
- cb(succeeded);
2214
- }
2215
- }, _.bind(function lockFailure(err) {
2216
- var succeeded = false;
2217
- this.reportError('Error acquiring storage lock', err);
2218
- if (!localStorageSupported(this.storage, true)) {
2219
- // Looks like localStorage writes have stopped working sometime after
2220
- // initialization (probably full), and so nobody can acquire locks
2221
- // anymore. Consider it temporarily safe to remove items without the
2222
- // lock, since nobody's writing successfully anyway.
2223
- succeeded = removeFromStorage();
2224
- if (!succeeded) {
2225
- // OK, we couldn't even write out the smaller queue. Try clearing it
2226
- // entirely.
2227
- try {
2228
- this.storage.removeItem(this.storageKey);
2229
- } catch(err) {
2230
- this.reportError('Error clearing queue', err);
2231
- }
2663
+ return this.lock
2664
+ .withLock(removeFromStorage, this.pid)
2665
+ .catch(_.bind(function (err) {
2666
+ this.reportError('Error acquiring storage lock', err);
2667
+ if (!localStorageSupported(this.queueStorage.storage, true)) {
2668
+ // Looks like localStorage writes have stopped working sometime after
2669
+ // initialization (probably full), and so nobody can acquire locks
2670
+ // anymore. Consider it temporarily safe to remove items without the
2671
+ // lock, since nobody's writing successfully anyway.
2672
+ return removeFromStorage()
2673
+ .then(_.bind(function (success) {
2674
+ if (!success) {
2675
+ // OK, we couldn't even write out the smaller queue. Try clearing it
2676
+ // entirely.
2677
+ return this.queueStorage.removeItem(this.storageKey).then(function () {
2678
+ return success;
2679
+ });
2680
+ }
2681
+ return success;
2682
+ }, this))
2683
+ .catch(_.bind(function (err) {
2684
+ this.reportError('Error clearing queue', err);
2685
+ return false;
2686
+ }, this));
2687
+ } else {
2688
+ return false;
2232
2689
  }
2233
- }
2234
- if (cb) {
2235
- cb(succeeded);
2236
- }
2237
- }, this), this.pid);
2690
+ }, this));
2238
2691
  }
2239
-
2240
2692
  };
2241
2693
 
2242
2694
  // internal helper for RequestQueue.updatePayloads
2243
- var updatePayloads = function(existingItems, itemsToUpdate) {
2695
+ var updatePayloads = function (existingItems, itemsToUpdate) {
2244
2696
  var newItems = [];
2245
- _.each(existingItems, function(item) {
2697
+ _.each(existingItems, function (item) {
2246
2698
  var id = item['id'];
2247
2699
  if (id in itemsToUpdate) {
2248
2700
  var newPayload = itemsToUpdate[id];
@@ -2262,79 +2714,95 @@ var updatePayloads = function(existingItems, itemsToUpdate) {
2262
2714
  * Update payloads of given items in both in-memory queue and
2263
2715
  * persisted queue. Items set to null are removed from queues.
2264
2716
  */
2265
- RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
2717
+ RequestQueue.prototype.updatePayloads = function (itemsToUpdate) {
2266
2718
  this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
2267
2719
  if (!this.usePersistence) {
2268
- if (cb) {
2269
- cb(true);
2270
- }
2720
+ return PromisePolyfill.resolve(true);
2271
2721
  } else {
2272
- this.lock.withLock(_.bind(function lockAcquired() {
2273
- var succeeded;
2274
- try {
2275
- var storedQueue = this.readFromStorage();
2276
- storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2277
- succeeded = this.saveToStorage(storedQueue);
2278
- } catch(err) {
2279
- this.reportError('Error updating items', itemsToUpdate);
2280
- succeeded = false;
2281
- }
2282
- if (cb) {
2283
- cb(succeeded);
2284
- }
2285
- }, this), _.bind(function lockFailure(err) {
2286
- this.reportError('Error acquiring storage lock', err);
2287
- if (cb) {
2288
- cb(false);
2289
- }
2290
- }, this), this.pid);
2722
+ return this.lock
2723
+ .withLock(_.bind(function lockAcquired() {
2724
+ return this.ensureInit()
2725
+ .then(_.bind(function () {
2726
+ return this.readFromStorage();
2727
+ }, this))
2728
+ .then(_.bind(function (storedQueue) {
2729
+ storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2730
+ return this.saveToStorage(storedQueue);
2731
+ }, this))
2732
+ .catch(_.bind(function (err) {
2733
+ this.reportError('Error updating items', itemsToUpdate, err);
2734
+ return false;
2735
+ }, this));
2736
+ }, this), this.pid)
2737
+ .catch(_.bind(function (err) {
2738
+ this.reportError('Error acquiring storage lock', err);
2739
+ return false;
2740
+ }, this));
2291
2741
  }
2292
-
2293
2742
  };
2294
2743
 
2295
2744
  /**
2296
2745
  * Read and parse items array from localStorage entry, handling
2297
2746
  * malformed/missing data if necessary.
2298
2747
  */
2299
- RequestQueue.prototype.readFromStorage = function() {
2300
- var storageEntry;
2301
- try {
2302
- storageEntry = this.storage.getItem(this.storageKey);
2303
- if (storageEntry) {
2304
- storageEntry = JSONParse(storageEntry);
2305
- if (!_.isArray(storageEntry)) {
2306
- this.reportError('Invalid storage entry:', storageEntry);
2307
- storageEntry = null;
2748
+ RequestQueue.prototype.readFromStorage = function () {
2749
+ return this.ensureInit()
2750
+ .then(_.bind(function () {
2751
+ return this.queueStorage.getItem(this.storageKey);
2752
+ }, this))
2753
+ .then(_.bind(function (storageEntry) {
2754
+ if (storageEntry) {
2755
+ storageEntry = JSONParse(storageEntry);
2756
+ if (!_.isArray(storageEntry)) {
2757
+ this.reportError('Invalid storage entry:', storageEntry);
2758
+ storageEntry = null;
2759
+ }
2308
2760
  }
2309
- }
2310
- } catch (err) {
2311
- this.reportError('Error retrieving queue', err);
2312
- storageEntry = null;
2313
- }
2314
- return storageEntry || [];
2761
+ return storageEntry || [];
2762
+ }, this))
2763
+ .catch(_.bind(function (err) {
2764
+ this.reportError('Error retrieving queue', err);
2765
+ return [];
2766
+ }, this));
2315
2767
  };
2316
2768
 
2317
2769
  /**
2318
2770
  * Serialize the given items array to localStorage.
2319
2771
  */
2320
- RequestQueue.prototype.saveToStorage = function(queue) {
2772
+ RequestQueue.prototype.saveToStorage = function (queue) {
2321
2773
  try {
2322
- this.storage.setItem(this.storageKey, JSONStringify(queue));
2323
- return true;
2774
+ var serialized = JSONStringify(queue);
2324
2775
  } catch (err) {
2325
- this.reportError('Error saving queue', err);
2326
- return false;
2776
+ this.reportError('Error serializing queue', err);
2777
+ return PromisePolyfill.resolve(false);
2327
2778
  }
2779
+
2780
+ return this.ensureInit()
2781
+ .then(_.bind(function () {
2782
+ return this.queueStorage.setItem(this.storageKey, serialized);
2783
+ }, this))
2784
+ .then(function () {
2785
+ return true;
2786
+ })
2787
+ .catch(_.bind(function (err) {
2788
+ this.reportError('Error saving queue', err);
2789
+ return false;
2790
+ }, this));
2328
2791
  };
2329
2792
 
2330
2793
  /**
2331
2794
  * Clear out queues (memory and localStorage).
2332
2795
  */
2333
- RequestQueue.prototype.clear = function() {
2796
+ RequestQueue.prototype.clear = function () {
2334
2797
  this.memQueue = [];
2335
2798
 
2336
2799
  if (this.usePersistence) {
2337
- this.storage.removeItem(this.storageKey);
2800
+ return this.ensureInit()
2801
+ .then(_.bind(function () {
2802
+ return this.queueStorage.removeItem(this.storageKey);
2803
+ }, this));
2804
+ } else {
2805
+ return PromisePolyfill.resolve();
2338
2806
  }
2339
2807
  };
2340
2808
 
@@ -2353,7 +2821,8 @@ var RequestBatcher = function(storageKey, options) {
2353
2821
  this.errorReporter = options.errorReporter;
2354
2822
  this.queue = new RequestQueue(storageKey, {
2355
2823
  errorReporter: _.bind(this.reportError, this),
2356
- storage: options.storage,
2824
+ queueStorage: options.queueStorage,
2825
+ sharedLockStorage: options.sharedLockStorage,
2357
2826
  usePersistence: options.usePersistence
2358
2827
  });
2359
2828
 
@@ -2381,8 +2850,8 @@ var RequestBatcher = function(storageKey, options) {
2381
2850
  /**
2382
2851
  * Add one item to queue.
2383
2852
  */
2384
- RequestBatcher.prototype.enqueue = function(item, cb) {
2385
- this.queue.enqueue(item, this.flushInterval, cb);
2853
+ RequestBatcher.prototype.enqueue = function(item) {
2854
+ return this.queue.enqueue(item, this.flushInterval);
2386
2855
  };
2387
2856
 
2388
2857
  /**
@@ -2392,7 +2861,7 @@ RequestBatcher.prototype.enqueue = function(item, cb) {
2392
2861
  RequestBatcher.prototype.start = function() {
2393
2862
  this.stopped = false;
2394
2863
  this.consecutiveRemovalFailures = 0;
2395
- this.flush();
2864
+ return this.flush();
2396
2865
  };
2397
2866
 
2398
2867
  /**
@@ -2410,7 +2879,7 @@ RequestBatcher.prototype.stop = function() {
2410
2879
  * Clear out queue.
2411
2880
  */
2412
2881
  RequestBatcher.prototype.clear = function() {
2413
- this.queue.clear();
2882
+ return this.queue.clear();
2414
2883
  };
2415
2884
 
2416
2885
  /**
@@ -2441,6 +2910,17 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
2441
2910
  }
2442
2911
  };
2443
2912
 
2913
+ /**
2914
+ * Send a request using the sendRequest callback, but promisified.
2915
+ * TODO: sendRequest should be promisified in the first place.
2916
+ */
2917
+ RequestBatcher.prototype.sendRequestPromise = function(data, options) {
2918
+ return new PromisePolyfill(_.bind(function(resolve) {
2919
+ this.sendRequest(data, options, resolve);
2920
+ }, this));
2921
+ };
2922
+
2923
+
2444
2924
  /**
2445
2925
  * Flush one batch to network. Depending on success/failure modes, it will either
2446
2926
  * remove the batch from the queue or leave it in for retry, and schedule the next
@@ -2452,183 +2932,191 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
2452
2932
  * sendBeacon offers no callbacks or status indications)
2453
2933
  */
2454
2934
  RequestBatcher.prototype.flush = function(options) {
2455
- try {
2935
+ if (this.requestInProgress) {
2936
+ logger.log('Flush: Request already in progress');
2937
+ return PromisePolyfill.resolve();
2938
+ }
2456
2939
 
2457
- if (this.requestInProgress) {
2458
- logger.log('Flush: Request already in progress');
2459
- return;
2460
- }
2940
+ this.requestInProgress = true;
2461
2941
 
2462
- options = options || {};
2463
- var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2464
- var startTime = new Date().getTime();
2465
- var currentBatchSize = this.batchSize;
2466
- var batch = this.queue.fillBatch(currentBatchSize);
2467
- // if there's more items in the queue than the batch size, attempt
2468
- // to flush again after the current batch is done.
2469
- var attemptSecondaryFlush = batch.length === currentBatchSize;
2470
- var dataForRequest = [];
2471
- var transformedItems = {};
2472
- _.each(batch, function(item) {
2473
- var payload = item['payload'];
2474
- if (this.beforeSendHook && !item.orphaned) {
2475
- payload = this.beforeSendHook(payload);
2476
- }
2477
- if (payload) {
2478
- // mp_sent_by_lib_version prop captures which lib version actually
2479
- // sends each event (regardless of which version originally queued
2480
- // it for sending)
2481
- if (payload['event'] && payload['properties']) {
2482
- payload['properties'] = _.extend(
2483
- {},
2484
- payload['properties'],
2485
- {'mp_sent_by_lib_version': Config.LIB_VERSION}
2486
- );
2942
+ options = options || {};
2943
+ var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2944
+ var startTime = new Date().getTime();
2945
+ var currentBatchSize = this.batchSize;
2946
+
2947
+ return this.queue.fillBatch(currentBatchSize)
2948
+ .then(_.bind(function(batch) {
2949
+
2950
+ // if there's more items in the queue than the batch size, attempt
2951
+ // to flush again after the current batch is done.
2952
+ var attemptSecondaryFlush = batch.length === currentBatchSize;
2953
+ var dataForRequest = [];
2954
+ var transformedItems = {};
2955
+ _.each(batch, function(item) {
2956
+ var payload = item['payload'];
2957
+ if (this.beforeSendHook && !item.orphaned) {
2958
+ payload = this.beforeSendHook(payload);
2487
2959
  }
2488
- var addPayload = true;
2489
- var itemId = item['id'];
2490
- if (itemId) {
2491
- if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
2492
- this.reportError('[dupe] item ID sent too many times, not sending', {
2493
- item: item,
2494
- batchSize: batch.length,
2495
- timesSent: this.itemIdsSentSuccessfully[itemId]
2496
- });
2497
- addPayload = false;
2960
+ if (payload) {
2961
+ // mp_sent_by_lib_version prop captures which lib version actually
2962
+ // sends each event (regardless of which version originally queued
2963
+ // it for sending)
2964
+ if (payload['event'] && payload['properties']) {
2965
+ payload['properties'] = _.extend(
2966
+ {},
2967
+ payload['properties'],
2968
+ {'mp_sent_by_lib_version': Config.LIB_VERSION}
2969
+ );
2970
+ }
2971
+ var addPayload = true;
2972
+ var itemId = item['id'];
2973
+ if (itemId) {
2974
+ if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
2975
+ this.reportError('[dupe] item ID sent too many times, not sending', {
2976
+ item: item,
2977
+ batchSize: batch.length,
2978
+ timesSent: this.itemIdsSentSuccessfully[itemId]
2979
+ });
2980
+ addPayload = false;
2981
+ }
2982
+ } else {
2983
+ this.reportError('[dupe] found item with no ID', {item: item});
2498
2984
  }
2499
- } else {
2500
- this.reportError('[dupe] found item with no ID', {item: item});
2501
- }
2502
2985
 
2503
- if (addPayload) {
2504
- dataForRequest.push(payload);
2986
+ if (addPayload) {
2987
+ dataForRequest.push(payload);
2988
+ }
2505
2989
  }
2506
- }
2507
- transformedItems[item['id']] = payload;
2508
- }, this);
2509
- if (dataForRequest.length < 1) {
2510
- this.resetFlush();
2511
- return; // nothing to do
2512
- }
2513
-
2514
- this.requestInProgress = true;
2515
-
2516
- var batchSendCallback = _.bind(function(res) {
2517
- this.requestInProgress = false;
2990
+ transformedItems[item['id']] = payload;
2991
+ }, this);
2518
2992
 
2519
- try {
2993
+ if (dataForRequest.length < 1) {
2994
+ this.requestInProgress = false;
2995
+ this.resetFlush();
2996
+ return PromisePolyfill.resolve(); // nothing to do
2997
+ }
2520
2998
 
2521
- // handle API response in a try-catch to make sure we can reset the
2522
- // flush operation if something goes wrong
2523
-
2524
- var removeItemsFromQueue = false;
2525
- if (options.unloading) {
2526
- // update persisted data to include hook transformations
2527
- this.queue.updatePayloads(transformedItems);
2528
- } else if (
2529
- _.isObject(res) &&
2530
- res.error === 'timeout' &&
2531
- new Date().getTime() - startTime >= timeoutMS
2532
- ) {
2533
- this.reportError('Network timeout; retrying');
2534
- this.flush();
2535
- } else if (
2536
- _.isObject(res) &&
2537
- (
2538
- res.httpStatusCode >= 500
2539
- || res.httpStatusCode === 429
2540
- || (res.httpStatusCode <= 0 && !isOnline())
2541
- || res.error === 'timeout'
2999
+ var removeItemsFromQueue = _.bind(function () {
3000
+ return this.queue
3001
+ .removeItemsByID(
3002
+ _.map(batch, function (item) {
3003
+ return item['id'];
3004
+ })
2542
3005
  )
2543
- ) {
2544
- // network or API error, or 429 Too Many Requests, retry
2545
- var retryMS = this.flushInterval * 2;
2546
- if (res.retryAfter) {
2547
- retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
2548
- }
2549
- retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
2550
- this.reportError('Error; retry in ' + retryMS + ' ms');
2551
- this.scheduleFlush(retryMS);
2552
- } else if (_.isObject(res) && res.httpStatusCode === 413) {
2553
- // 413 Payload Too Large
2554
- if (batch.length > 1) {
2555
- var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
2556
- this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
2557
- this.reportError('413 response; reducing batch size to ' + this.batchSize);
2558
- this.resetFlush();
2559
- } else {
2560
- this.reportError('Single-event request too large; dropping', batch);
2561
- this.resetBatchSize();
2562
- removeItemsFromQueue = true;
2563
- }
2564
- } else {
2565
- // successful network request+response; remove each item in batch from queue
2566
- // (even if it was e.g. a 400, in which case retrying won't help)
2567
- removeItemsFromQueue = true;
2568
- }
2569
-
2570
- if (removeItemsFromQueue) {
2571
- this.queue.removeItemsByID(
2572
- _.map(batch, function(item) { return item['id']; }),
2573
- _.bind(function(succeeded) {
2574
- if (succeeded) {
2575
- this.consecutiveRemovalFailures = 0;
2576
- if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
2577
- this.resetFlush(); // schedule next batch with a delay
2578
- } else {
2579
- this.flush(); // handle next batch if the queue isn't empty
3006
+ .then(_.bind(function (succeeded) {
3007
+ // client-side dedupe
3008
+ _.each(batch, _.bind(function(item) {
3009
+ var itemId = item['id'];
3010
+ if (itemId) {
3011
+ this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
3012
+ this.itemIdsSentSuccessfully[itemId]++;
3013
+ if (this.itemIdsSentSuccessfully[itemId] > 5) {
3014
+ this.reportError('[dupe] item ID sent too many times', {
3015
+ item: item,
3016
+ batchSize: batch.length,
3017
+ timesSent: this.itemIdsSentSuccessfully[itemId]
3018
+ });
2580
3019
  }
2581
3020
  } else {
2582
- this.reportError('Failed to remove items from queue');
2583
- if (++this.consecutiveRemovalFailures > 5) {
2584
- this.reportError('Too many queue failures; disabling batching system.');
2585
- this.stopAllBatching();
2586
- } else {
2587
- this.resetFlush();
2588
- }
3021
+ this.reportError('[dupe] found item with no ID while removing', {item: item});
2589
3022
  }
2590
- }, this)
2591
- );
3023
+ }, this));
2592
3024
 
2593
- // client-side dedupe
2594
- _.each(batch, _.bind(function(item) {
2595
- var itemId = item['id'];
2596
- if (itemId) {
2597
- this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
2598
- this.itemIdsSentSuccessfully[itemId]++;
2599
- if (this.itemIdsSentSuccessfully[itemId] > 5) {
2600
- this.reportError('[dupe] item ID sent too many times', {
2601
- item: item,
2602
- batchSize: batch.length,
2603
- timesSent: this.itemIdsSentSuccessfully[itemId]
2604
- });
3025
+ if (succeeded) {
3026
+ this.consecutiveRemovalFailures = 0;
3027
+ if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
3028
+ this.resetFlush(); // schedule next batch with a delay
3029
+ return PromisePolyfill.resolve();
3030
+ } else {
3031
+ return this.flush(); // handle next batch if the queue isn't empty
2605
3032
  }
2606
3033
  } else {
2607
- this.reportError('[dupe] found item with no ID while removing', {item: item});
3034
+ if (++this.consecutiveRemovalFailures > 5) {
3035
+ this.reportError('Too many queue failures; disabling batching system.');
3036
+ this.stopAllBatching();
3037
+ } else {
3038
+ this.resetFlush();
3039
+ }
3040
+ return PromisePolyfill.resolve();
2608
3041
  }
2609
3042
  }, this));
2610
- }
3043
+ }, this);
2611
3044
 
2612
- } catch(err) {
2613
- this.reportError('Error handling API response', err);
2614
- this.resetFlush();
3045
+ var batchSendCallback = _.bind(function(res) {
3046
+ this.requestInProgress = false;
3047
+
3048
+ try {
3049
+
3050
+ // handle API response in a try-catch to make sure we can reset the
3051
+ // flush operation if something goes wrong
3052
+
3053
+ if (options.unloading) {
3054
+ // update persisted data to include hook transformations
3055
+ return this.queue.updatePayloads(transformedItems);
3056
+ } else if (
3057
+ _.isObject(res) &&
3058
+ res.error === 'timeout' &&
3059
+ new Date().getTime() - startTime >= timeoutMS
3060
+ ) {
3061
+ this.reportError('Network timeout; retrying');
3062
+ return this.flush();
3063
+ } else if (
3064
+ _.isObject(res) &&
3065
+ (
3066
+ res.httpStatusCode >= 500
3067
+ || res.httpStatusCode === 429
3068
+ || (res.httpStatusCode <= 0 && !isOnline())
3069
+ || res.error === 'timeout'
3070
+ )
3071
+ ) {
3072
+ // network or API error, or 429 Too Many Requests, retry
3073
+ var retryMS = this.flushInterval * 2;
3074
+ if (res.retryAfter) {
3075
+ retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
3076
+ }
3077
+ retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
3078
+ this.reportError('Error; retry in ' + retryMS + ' ms');
3079
+ this.scheduleFlush(retryMS);
3080
+ return PromisePolyfill.resolve();
3081
+ } else if (_.isObject(res) && res.httpStatusCode === 413) {
3082
+ // 413 Payload Too Large
3083
+ if (batch.length > 1) {
3084
+ var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
3085
+ this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
3086
+ this.reportError('413 response; reducing batch size to ' + this.batchSize);
3087
+ this.resetFlush();
3088
+ return PromisePolyfill.resolve();
3089
+ } else {
3090
+ this.reportError('Single-event request too large; dropping', batch);
3091
+ this.resetBatchSize();
3092
+ return removeItemsFromQueue();
3093
+ }
3094
+ } else {
3095
+ // successful network request+response; remove each item in batch from queue
3096
+ // (even if it was e.g. a 400, in which case retrying won't help)
3097
+ return removeItemsFromQueue();
3098
+ }
3099
+ } catch(err) {
3100
+ this.reportError('Error handling API response', err);
3101
+ this.resetFlush();
3102
+ }
3103
+ }, this);
3104
+ var requestOptions = {
3105
+ method: 'POST',
3106
+ verbose: true,
3107
+ ignore_json_errors: true, // eslint-disable-line camelcase
3108
+ timeout_ms: timeoutMS // eslint-disable-line camelcase
3109
+ };
3110
+ if (options.unloading) {
3111
+ requestOptions.transport = 'sendBeacon';
2615
3112
  }
2616
- }, this);
2617
- var requestOptions = {
2618
- method: 'POST',
2619
- verbose: true,
2620
- ignore_json_errors: true, // eslint-disable-line camelcase
2621
- timeout_ms: timeoutMS // eslint-disable-line camelcase
2622
- };
2623
- if (options.unloading) {
2624
- requestOptions.transport = 'sendBeacon';
2625
- }
2626
- logger.log('MIXPANEL REQUEST:', dataForRequest);
2627
- this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
2628
- } catch(err) {
2629
- this.reportError('Error flushing request queue', err);
2630
- this.resetFlush();
2631
- }
3113
+ logger.log('MIXPANEL REQUEST:', dataForRequest);
3114
+ return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
3115
+ }, this))
3116
+ .catch(_.bind(function(err) {
3117
+ this.reportError('Error flushing request queue', err);
3118
+ this.resetFlush();
3119
+ }, this));
2632
3120
  };
2633
3121
 
2634
3122
  /**
@@ -4257,6 +4745,7 @@ var DEFAULT_CONFIG = {
4257
4745
  'hooks': {},
4258
4746
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
4259
4747
  'record_block_selector': 'img, video',
4748
+ 'record_canvas': false,
4260
4749
  'record_collect_fonts': false,
4261
4750
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
4262
4751
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
@@ -5034,7 +5523,7 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
5034
5523
  }, this);
5035
5524
 
5036
5525
  if (this._batch_requests && !should_send_immediately) {
5037
- batcher.enqueue(truncated_data, function(succeeded) {
5526
+ batcher.enqueue(truncated_data).then(function(succeeded) {
5038
5527
  if (succeeded) {
5039
5528
  callback(1, truncated_data);
5040
5529
  } else {