mixpanel-browser 2.43.0 → 2.46.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,3 +1,4 @@
1
+ import Config from './config';
1
2
  import { RequestQueue } from './request-queue';
2
3
  import { console_with_prefix, _ } from './utils'; // eslint-disable-line camelcase
3
4
 
@@ -13,17 +14,26 @@ var logger = console_with_prefix('batch');
13
14
  * @constructor
14
15
  */
15
16
  var RequestBatcher = function(storageKey, options) {
16
- this.queue = new RequestQueue(storageKey, {storage: options.storage});
17
+ this.errorReporter = options.errorReporter;
18
+ this.queue = new RequestQueue(storageKey, {
19
+ errorReporter: _.bind(this.reportError, this),
20
+ storage: options.storage
21
+ });
17
22
 
18
23
  this.libConfig = options.libConfig;
19
24
  this.sendRequest = options.sendRequestFunc;
20
25
  this.beforeSendHook = options.beforeSendHook;
26
+ this.stopAllBatching = options.stopAllBatchingFunc;
21
27
 
22
28
  // seed variable batch size + flush interval with configured values
23
29
  this.batchSize = this.libConfig['batch_size'];
24
30
  this.flushInterval = this.libConfig['batch_flush_interval_ms'];
25
31
 
26
32
  this.stopped = !this.libConfig['batch_autostart'];
33
+ this.consecutiveRemovalFailures = 0;
34
+
35
+ // extra client-side dedupe
36
+ this.itemIdsSentSuccessfully = {};
27
37
  };
28
38
 
29
39
  /**
@@ -39,6 +49,7 @@ RequestBatcher.prototype.enqueue = function(item, cb) {
39
49
  */
40
50
  RequestBatcher.prototype.start = function() {
41
51
  this.stopped = false;
52
+ this.consecutiveRemovalFailures = 0;
42
53
  this.flush();
43
54
  };
44
55
 
@@ -115,7 +126,34 @@ RequestBatcher.prototype.flush = function(options) {
115
126
  payload = this.beforeSendHook(payload);
116
127
  }
117
128
  if (payload) {
118
- dataForRequest.push(payload);
129
+ // mp_sent_by_lib_version prop captures which lib version actually
130
+ // sends each event (regardless of which version originally queued
131
+ // it for sending)
132
+ if (payload['event'] && payload['properties']) {
133
+ payload['properties'] = _.extend(
134
+ {},
135
+ payload['properties'],
136
+ {'mp_sent_by_lib_version': Config.LIB_VERSION}
137
+ );
138
+ }
139
+ var addPayload = true;
140
+ var itemId = item['id'];
141
+ if (itemId) {
142
+ if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
143
+ this.reportError('[dupe] item ID sent too many times, not sending', {
144
+ item: item,
145
+ batchSize: batch.length,
146
+ timesSent: this.itemIdsSentSuccessfully[itemId]
147
+ });
148
+ addPayload = false;
149
+ }
150
+ } else {
151
+ this.reportError('[dupe] found item with no ID', {item: item});
152
+ }
153
+
154
+ if (addPayload) {
155
+ dataForRequest.push(payload);
156
+ }
119
157
  }
120
158
  transformedItems[item['id']] = payload;
121
159
  }, this);
@@ -143,7 +181,7 @@ RequestBatcher.prototype.flush = function(options) {
143
181
  res.error === 'timeout' &&
144
182
  new Date().getTime() - startTime >= timeoutMS
145
183
  ) {
146
- logger.error('Network timeout; retrying');
184
+ this.reportError('Network timeout; retrying');
147
185
  this.flush();
148
186
  } else if (
149
187
  _.isObject(res) &&
@@ -160,17 +198,17 @@ RequestBatcher.prototype.flush = function(options) {
160
198
  }
161
199
  }
162
200
  retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
163
- logger.error('Error; retry in ' + retryMS + ' ms');
201
+ this.reportError('Error; retry in ' + retryMS + ' ms');
164
202
  this.scheduleFlush(retryMS);
165
203
  } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) {
166
204
  // 413 Payload Too Large
167
205
  if (batch.length > 1) {
168
206
  var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
169
207
  this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
170
- logger.error('413 response; reducing batch size to ' + this.batchSize);
208
+ this.reportError('413 response; reducing batch size to ' + this.batchSize);
171
209
  this.resetFlush();
172
210
  } else {
173
- logger.error('Single-event request too large; dropping', batch);
211
+ this.reportError('Single-event request too large; dropping', batch);
174
212
  this.resetBatchSize();
175
213
  removeItemsFromQueue = true;
176
214
  }
@@ -183,12 +221,43 @@ RequestBatcher.prototype.flush = function(options) {
183
221
  if (removeItemsFromQueue) {
184
222
  this.queue.removeItemsByID(
185
223
  _.map(batch, function(item) { return item['id']; }),
186
- _.bind(this.flush, this) // handle next batch if the queue isn't empty
224
+ _.bind(function(succeeded) {
225
+ if (succeeded) {
226
+ this.consecutiveRemovalFailures = 0;
227
+ this.flush(); // handle next batch if the queue isn't empty
228
+ } else {
229
+ this.reportError('Failed to remove items from queue');
230
+ if (++this.consecutiveRemovalFailures > 5) {
231
+ this.reportError('Too many queue failures; disabling batching system.');
232
+ this.stopAllBatching();
233
+ } else {
234
+ this.resetFlush();
235
+ }
236
+ }
237
+ }, this)
187
238
  );
239
+
240
+ // client-side dedupe
241
+ _.each(batch, _.bind(function(item) {
242
+ var itemId = item['id'];
243
+ if (itemId) {
244
+ this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
245
+ this.itemIdsSentSuccessfully[itemId]++;
246
+ if (this.itemIdsSentSuccessfully[itemId] > 5) {
247
+ this.reportError('[dupe] item ID sent too many times', {
248
+ item: item,
249
+ batchSize: batch.length,
250
+ timesSent: this.itemIdsSentSuccessfully[itemId]
251
+ });
252
+ }
253
+ } else {
254
+ this.reportError('[dupe] found item with no ID while removing', {item: item});
255
+ }
256
+ }, this));
188
257
  }
189
258
 
190
259
  } catch(err) {
191
- logger.error('Error handling API response', err);
260
+ this.reportError('Error handling API response', err);
192
261
  this.resetFlush();
193
262
  }
194
263
  }, this);
@@ -205,9 +274,26 @@ RequestBatcher.prototype.flush = function(options) {
205
274
  this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
206
275
 
207
276
  } catch(err) {
208
- logger.error('Error flushing request queue', err);
277
+ this.reportError('Error flushing request queue', err);
209
278
  this.resetFlush();
210
279
  }
211
280
  };
212
281
 
282
+ /**
283
+ * Log error to global logger and optional user-defined logger.
284
+ */
285
+ RequestBatcher.prototype.reportError = function(msg, err) {
286
+ logger.error.apply(logger.error, arguments);
287
+ if (this.errorReporter) {
288
+ try {
289
+ if (!(err instanceof Error)) {
290
+ err = new Error(msg);
291
+ }
292
+ this.errorReporter(msg, err);
293
+ } catch(err) {
294
+ logger.error(err);
295
+ }
296
+ }
297
+ };
298
+
213
299
  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) {