mixpanel-browser 2.41.0 → 2.45.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.
@@ -13,17 +13,23 @@ var logger = console_with_prefix('batch');
13
13
  * @constructor
14
14
  */
15
15
  var RequestBatcher = function(storageKey, options) {
16
- this.queue = new RequestQueue(storageKey, {storage: options.storage});
16
+ this.errorReporter = options.errorReporter;
17
+ this.queue = new RequestQueue(storageKey, {
18
+ errorReporter: _.bind(this.reportError, this),
19
+ storage: options.storage
20
+ });
17
21
 
18
22
  this.libConfig = options.libConfig;
19
23
  this.sendRequest = options.sendRequestFunc;
20
24
  this.beforeSendHook = options.beforeSendHook;
25
+ this.stopAllBatching = options.stopAllBatchingFunc;
21
26
 
22
27
  // seed variable batch size + flush interval with configured values
23
28
  this.batchSize = this.libConfig['batch_size'];
24
29
  this.flushInterval = this.libConfig['batch_flush_interval_ms'];
25
30
 
26
31
  this.stopped = !this.libConfig['batch_autostart'];
32
+ this.consecutiveRemovalFailures = 0;
27
33
  };
28
34
 
29
35
  /**
@@ -39,6 +45,7 @@ RequestBatcher.prototype.enqueue = function(item, cb) {
39
45
  */
40
46
  RequestBatcher.prototype.start = function() {
41
47
  this.stopped = false;
48
+ this.consecutiveRemovalFailures = 0;
42
49
  this.flush();
43
50
  };
44
51
 
@@ -143,14 +150,14 @@ RequestBatcher.prototype.flush = function(options) {
143
150
  res.error === 'timeout' &&
144
151
  new Date().getTime() - startTime >= timeoutMS
145
152
  ) {
146
- logger.error('Network timeout; retrying');
153
+ this.reportError('Network timeout; retrying');
147
154
  this.flush();
148
155
  } else if (
149
156
  _.isObject(res) &&
150
157
  res.xhr_req &&
151
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
158
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
152
159
  ) {
153
- // network or API error, retry
160
+ // network or API error, or 429 Too Many Requests, retry
154
161
  var retryMS = this.flushInterval * 2;
155
162
  var headers = res.xhr_req['responseHeaders'];
156
163
  if (headers) {
@@ -160,17 +167,17 @@ RequestBatcher.prototype.flush = function(options) {
160
167
  }
161
168
  }
162
169
  retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
163
- logger.error('Error; retry in ' + retryMS + ' ms');
170
+ this.reportError('Error; retry in ' + retryMS + ' ms');
164
171
  this.scheduleFlush(retryMS);
165
172
  } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) {
166
173
  // 413 Payload Too Large
167
174
  if (batch.length > 1) {
168
175
  var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
169
176
  this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
170
- logger.error('413 response; reducing batch size to ' + this.batchSize);
177
+ this.reportError('413 response; reducing batch size to ' + this.batchSize);
171
178
  this.resetFlush();
172
179
  } else {
173
- logger.error('Single-event request too large; dropping', batch);
180
+ this.reportError('Single-event request too large; dropping', batch);
174
181
  this.resetBatchSize();
175
182
  removeItemsFromQueue = true;
176
183
  }
@@ -183,12 +190,25 @@ RequestBatcher.prototype.flush = function(options) {
183
190
  if (removeItemsFromQueue) {
184
191
  this.queue.removeItemsByID(
185
192
  _.map(batch, function(item) { return item['id']; }),
186
- _.bind(this.flush, this) // handle next batch if the queue isn't empty
193
+ _.bind(function(succeeded) {
194
+ if (succeeded) {
195
+ this.consecutiveRemovalFailures = 0;
196
+ this.flush(); // handle next batch if the queue isn't empty
197
+ } else {
198
+ this.reportError('Failed to remove items from queue');
199
+ if (++this.consecutiveRemovalFailures > 5) {
200
+ this.reportError('Too many queue failures; disabling batching system.');
201
+ this.stopAllBatching();
202
+ } else {
203
+ this.resetFlush();
204
+ }
205
+ }
206
+ }, this)
187
207
  );
188
208
  }
189
209
 
190
210
  } catch(err) {
191
- logger.error('Error handling API response', err);
211
+ this.reportError('Error handling API response', err);
192
212
  this.resetFlush();
193
213
  }
194
214
  }, this);
@@ -205,9 +225,26 @@ RequestBatcher.prototype.flush = function(options) {
205
225
  this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
206
226
 
207
227
  } catch(err) {
208
- logger.error('Error flushing request queue', err);
228
+ this.reportError('Error flushing request queue', err);
209
229
  this.resetFlush();
210
230
  }
211
231
  };
212
232
 
233
+ /**
234
+ * Log error to global logger and optional user-defined logger.
235
+ */
236
+ RequestBatcher.prototype.reportError = function(msg, err) {
237
+ logger.error.apply(logger.error, arguments);
238
+ if (this.errorReporter) {
239
+ try {
240
+ if (!(err instanceof Error)) {
241
+ err = new Error(msg);
242
+ }
243
+ this.errorReporter(msg, err);
244
+ } catch(err) {
245
+ logger.error(err);
246
+ }
247
+ }
248
+ };
249
+
213
250
  export { RequestBatcher };
@@ -1,5 +1,5 @@
1
1
  import { SharedLock } from './shared-lock';
2
- import { cheap_guid, console_with_prefix, JSONParse, JSONStringify, _ } from './utils'; // eslint-disable-line camelcase
2
+ import { cheap_guid, console_with_prefix, localStorageSupported, JSONParse, JSONStringify, _ } from './utils'; // eslint-disable-line camelcase
3
3
 
4
4
  var logger = console_with_prefix('batch');
5
5
 
@@ -23,6 +23,7 @@ var RequestQueue = function(storageKey, options) {
23
23
  options = options || {};
24
24
  this.storageKey = storageKey;
25
25
  this.storage = options.storage || window.localStorage;
26
+ this.reportError = options.errorReporter || _.bind(logger.error, logger);
26
27
  this.lock = new SharedLock(storageKey, {storage: this.storage});
27
28
 
28
29
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
@@ -60,18 +61,18 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
60
61
  this.memQueue.push(queueEntry);
61
62
  }
62
63
  } catch(err) {
63
- logger.error('Error enqueueing item', item);
64
+ this.reportError('Error enqueueing item', item);
64
65
  succeeded = false;
65
66
  }
66
67
  if (cb) {
67
68
  cb(succeeded);
68
69
  }
69
- }, this), function lockFailure(err) {
70
- logger.error('Error acquiring storage lock', err);
70
+ }, this), _.bind(function lockFailure(err) {
71
+ this.reportError('Error acquiring storage lock', err);
71
72
  if (cb) {
72
73
  cb(false);
73
74
  }
74
- }, this.pid);
75
+ }, this), this.pid);
75
76
  };
76
77
 
77
78
  /**
@@ -131,25 +132,61 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) {
131
132
  _.each(ids, function(id) { idSet[id] = true; });
132
133
 
133
134
  this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
134
- this.lock.withLock(_.bind(function lockAcquired() {
135
+
136
+ var removeFromStorage = _.bind(function() {
135
137
  var succeeded;
136
138
  try {
137
139
  var storedQueue = this.readFromStorage();
138
140
  storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
139
141
  succeeded = this.saveToStorage(storedQueue);
142
+
143
+ // an extra check: did storage report success but somehow
144
+ // the items are still there?
145
+ if (succeeded) {
146
+ storedQueue = this.readFromStorage();
147
+ for (var i = 0; i < storedQueue.length; i++) {
148
+ var item = storedQueue[i];
149
+ if (item['id'] && !!idSet[item['id']]) {
150
+ this.reportError('Item not removed from storage');
151
+ return false;
152
+ }
153
+ }
154
+ }
140
155
  } catch(err) {
141
- logger.error('Error removing items', ids);
156
+ this.reportError('Error removing items', ids);
142
157
  succeeded = false;
143
158
  }
159
+ return succeeded;
160
+ }, this);
161
+
162
+ this.lock.withLock(function lockAcquired() {
163
+ var succeeded = removeFromStorage();
144
164
  if (cb) {
145
165
  cb(succeeded);
146
166
  }
147
- }, this), function lockFailure(err) {
148
- logger.error('Error acquiring storage lock', err);
167
+ }, _.bind(function lockFailure(err) {
168
+ var succeeded = false;
169
+ this.reportError('Error acquiring storage lock', err);
170
+ if (!localStorageSupported(this.storage, true)) {
171
+ // Looks like localStorage writes have stopped working sometime after
172
+ // initialization (probably full), and so nobody can acquire locks
173
+ // anymore. Consider it temporarily safe to remove items without the
174
+ // lock, since nobody's writing successfully anyway.
175
+ succeeded = removeFromStorage();
176
+ if (!succeeded) {
177
+ // OK, we couldn't even write out the smaller queue. Try clearing it
178
+ // entirely.
179
+ try {
180
+ this.storage.removeItem(this.storageKey);
181
+ } catch(err) {
182
+ this.reportError('Error clearing queue', err);
183
+ }
184
+ }
185
+ }
149
186
  if (cb) {
150
- cb(false);
187
+ cb(succeeded);
151
188
  }
152
- }, this.pid);
189
+ }, this), this.pid);
153
190
  };
154
191
 
155
192
  // internal helper for RequestQueue.updatePayloads
@@ -184,18 +221,18 @@ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
184
221
  storedQueue = updatePayloads(storedQueue, itemsToUpdate);
185
222
  succeeded = this.saveToStorage(storedQueue);
186
223
  } catch(err) {
187
- logger.error('Error updating items', itemsToUpdate);
224
+ this.reportError('Error updating items', itemsToUpdate);
188
225
  succeeded = false;
189
226
  }
190
227
  if (cb) {
191
228
  cb(succeeded);
192
229
  }
193
- }, this), function lockFailure(err) {
194
- logger.error('Error acquiring storage lock', err);
230
+ }, this), _.bind(function lockFailure(err) {
231
+ this.reportError('Error acquiring storage lock', err);
195
232
  if (cb) {
196
233
  cb(false);
197
234
  }
198
- }, this.pid);
235
+ }, this), this.pid);
199
236
  };
200
237
 
201
238
  /**
@@ -209,12 +246,12 @@ RequestQueue.prototype.readFromStorage = function() {
209
246
  if (storageEntry) {
210
247
  storageEntry = JSONParse(storageEntry);
211
248
  if (!_.isArray(storageEntry)) {
212
- logger.error('Invalid storage entry:', storageEntry);
249
+ this.reportError('Invalid storage entry:', storageEntry);
213
250
  storageEntry = null;
214
251
  }
215
252
  }
216
253
  } catch (err) {
217
- logger.error('Error retrieving queue', err);
254
+ this.reportError('Error retrieving queue', err);
218
255
  storageEntry = null;
219
256
  }
220
257
  return storageEntry || [];
@@ -228,7 +265,7 @@ RequestQueue.prototype.saveToStorage = function(queue) {
228
265
  this.storage.setItem(this.storageKey, JSONStringify(queue));
229
266
  return true;
230
267
  } catch (err) {
231
- logger.error('Error saving queue', err);
268
+ this.reportError('Error saving queue', err);
232
269
  return false;
233
270
  }
234
271
  };
package/src/utils.js CHANGED
@@ -150,14 +150,6 @@ _.bind = function(func, context) {
150
150
  return bound;
151
151
  };
152
152
 
153
- _.bind_instance_methods = function(obj) {
154
- for (var func in obj) {
155
- if (typeof(obj[func]) === 'function') {
156
- obj[func] = _.bind(obj[func], obj);
157
- }
158
- }
159
- };
160
-
161
153
  /**
162
154
  * @param {*=} obj
163
155
  * @param {function(...*)=} iterator
@@ -186,19 +178,6 @@ _.each = function(obj, iterator, context) {
186
178
  }
187
179
  };
188
180
 
189
- _.escapeHTML = function(s) {
190
- var escaped = s;
191
- if (escaped && _.isString(escaped)) {
192
- escaped = escaped
193
- .replace(/&/g, '&amp;')
194
- .replace(/</g, '&lt;')
195
- .replace(/>/g, '&gt;')
196
- .replace(/"/g, '&quot;')
197
- .replace(/'/g, '&#039;');
198
- }
199
- return escaped;
200
- };
201
-
202
181
  _.extend = function(obj) {
203
182
  _.each(slice.call(arguments, 1), function(source) {
204
183
  for (var prop in source) {
@@ -374,33 +353,6 @@ _.formatDate = function(d) {
374
353
  pad(d.getUTCSeconds());
375
354
  };
376
355
 
377
- _.safewrap = function(f) {
378
- return function() {
379
- try {
380
- return f.apply(this, arguments);
381
- } catch (e) {
382
- console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
383
- if (Config.DEBUG){
384
- console.critical(e);
385
- }
386
- }
387
- };
388
- };
389
-
390
- _.safewrap_class = function(klass, functions) {
391
- for (var i = 0; i < functions.length; i++) {
392
- klass.prototype[functions[i]] = _.safewrap(klass.prototype[functions[i]]);
393
- }
394
- };
395
-
396
- _.safewrap_instance_methods = function(obj) {
397
- for (var func in obj) {
398
- if (typeof(obj[func]) === 'function') {
399
- obj[func] = _.safewrap(obj[func]);
400
- }
401
- }
402
- };
403
-
404
356
  _.strip_empty_properties = function(p) {
405
357
  var ret = {};
406
358
  _.each(p, function(v, k) {
@@ -942,10 +894,12 @@ _.UUID = (function() {
942
894
  // This is to block various web spiders from executing our JS and
943
895
  // sending false tracking data
944
896
  var BLOCKED_UA_STRS = [
897
+ 'ahrefsbot',
945
898
  'baiduspider',
946
899
  'bingbot',
947
900
  'bingpreview',
948
901
  'facebookexternal',
902
+ 'petalbot',
949
903
  'pinterest',
950
904
  'screaming frog',
951
905
  'yahoo! slurp',
@@ -1682,28 +1636,6 @@ var cheap_guid = function(maxlen) {
1682
1636
  return maxlen ? guid.substring(0, maxlen) : guid;
1683
1637
  };
1684
1638
 
1685
- /**
1686
- * Check deterministically whether to include or exclude from a feature rollout/test based on the
1687
- * given string and the desired percentage to include.
1688
- * @param {String} str - string to run the check against (for instance a project's token)
1689
- * @param {String} feature - name of feature (for inclusion in hash, to ensure different results
1690
- * for different features)
1691
- * @param {Number} percent_allowed - percentage chance that a given string will be included
1692
- * @returns {Boolean} whether the given string should be included
1693
- */
1694
- var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
1695
- str = str + feature;
1696
-
1697
- // Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
1698
- var hash = 5381;
1699
- for (var i = 0; i < str.length; i++) {
1700
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
1701
- hash = hash & hash;
1702
- }
1703
- var dart = (hash >>> 0) % 100;
1704
- return dart < percent_allowed;
1705
- });
1706
-
1707
1639
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1708
1640
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1709
1641
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1762,7 +1694,6 @@ export {
1762
1694
  navigator,
1763
1695
  cheap_guid,
1764
1696
  console_with_prefix,
1765
- determine_eligibility,
1766
1697
  extract_domain,
1767
1698
  localStorageSupported,
1768
1699
  JSONStringify,
package/tunnel.log ADDED
File without changes
package/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- node_js:
2
- - '12'
3
- - '14'
4
- language: node_js
5
- script:
6
- - npm test