mixpanel-browser 2.43.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.
@@ -68,8 +68,8 @@ var MIXPANEL_LIB_URL = '//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js';
68
68
 
69
69
  function _set_and_defer_chained(fn_name) {
70
70
  mock_group[fn_name] = function() {
71
- call2_args = arguments;
72
- call2 = [fn_name].concat(Array.prototype.slice.call(call2_args, 0));
71
+ var call2_args = arguments;
72
+ var call2 = [fn_name].concat(Array.prototype.slice.call(call2_args, 0));
73
73
  target.push([call1, call2]);
74
74
  };
75
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.43.0",
3
+ "version": "2.45.0",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
6
  "directories": {
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.43.0'
3
+ LIB_VERSION: '2.45.0'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -4,7 +4,6 @@ import { _, console, userAgent, window, document, navigator, slice } from './uti
4
4
  import { FormTracker, LinkTracker } from './dom-trackers';
5
5
  import { RequestBatcher } from './request-batcher';
6
6
  import { MixpanelGroup } from './mixpanel-group';
7
- import { MixpanelNotification } from './mixpanel-notification';
8
7
  import { MixpanelPeople } from './mixpanel-people';
9
8
  import {
10
9
  MixpanelPersistence,
@@ -93,6 +92,7 @@ var DEFAULT_CONFIG = {
93
92
  'cdn': 'https://cdn.mxpnl.com',
94
93
  'cross_site_cookie': false,
95
94
  'cross_subdomain_cookie': true,
95
+ 'error_reporter': NOOP_FUNC,
96
96
  'persistence': 'cookie',
97
97
  'persistence_name': '',
98
98
  'cookie_domain': '',
@@ -117,8 +117,6 @@ var DEFAULT_CONFIG = {
117
117
  'opt_out_tracking_cookie_prefix': null,
118
118
  'property_blacklist': [],
119
119
  'xhr_headers': {}, // { header: value, header2: value }
120
- 'inapp_protocol': '//',
121
- 'inapp_link_new_window': false,
122
120
  'ignore_dnt': false,
123
121
  'batch_requests': true,
124
122
  'batch_size': 50,
@@ -160,8 +158,6 @@ var create_mplib = function(token, config, name) {
160
158
  }
161
159
 
162
160
  instance._cached_groups = {}; // cache groups in a pool
163
- instance._user_decide_check_complete = false;
164
- instance._events_tracked_before_user_decide_check_complete = [];
165
161
 
166
162
  instance._init(token, config, name);
167
163
 
@@ -204,11 +200,11 @@ var create_mplib = function(token, config, name) {
204
200
  */
205
201
  MixpanelLib.prototype.init = function (token, config, name) {
206
202
  if (_.isUndefined(name)) {
207
- console.error('You must name your new library: init(token, config, name)');
203
+ this.report_error('You must name your new library: init(token, config, name)');
208
204
  return;
209
205
  }
210
206
  if (name === PRIMARY_INSTANCE_NAME) {
211
- console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
207
+ this.report_error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
212
208
  return;
213
209
  }
214
210
 
@@ -231,7 +227,6 @@ MixpanelLib.prototype._init = function(token, config, name) {
231
227
 
232
228
  this['__loaded'] = true;
233
229
  this['config'] = {};
234
- this['_triggered_notifs'] = [];
235
230
 
236
231
  var variable_features = {};
237
232
 
@@ -350,7 +345,7 @@ MixpanelLib.prototype._dom_loaded = function() {
350
345
 
351
346
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
352
347
  if (this.get_config('img')) {
353
- console.error('You can\'t use DOM tracking functions with img = true.');
348
+ this.report_error('You can\'t use DOM tracking functions with img = true.');
354
349
  return false;
355
350
  }
356
351
 
@@ -452,6 +447,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
452
447
 
453
448
  url += '?' + _.HTTPBuildQuery(data);
454
449
 
450
+ var lib = this;
455
451
  if ('img' in data) {
456
452
  var img = document.createElement('img');
457
453
  img.src = url;
@@ -460,7 +456,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
460
456
  try {
461
457
  succeeded = sendBeacon(url, body_data);
462
458
  } catch (e) {
463
- console.error(e);
459
+ lib.report_error(e);
464
460
  succeeded = false;
465
461
  }
466
462
  try {
@@ -468,7 +464,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
468
464
  callback(succeeded ? 1 : 0);
469
465
  }
470
466
  } catch (e) {
471
- console.error(e);
467
+ lib.report_error(e);
472
468
  }
473
469
  } else if (USE_XHR) {
474
470
  try {
@@ -500,7 +496,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
500
496
  try {
501
497
  response = _.JSONDecode(req.responseText);
502
498
  } catch (e) {
503
- console.error(e);
499
+ lib.report_error(e);
504
500
  if (options.ignore_json_errors) {
505
501
  response = req.responseText;
506
502
  } else {
@@ -523,7 +519,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
523
519
  } else {
524
520
  error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
525
521
  }
526
- console.error(error);
522
+ lib.report_error(error);
527
523
  if (callback) {
528
524
  if (verbose_mode) {
529
525
  callback({status: 0, error: error, xhr_req: req});
@@ -536,7 +532,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
536
532
  };
537
533
  req.send(body_data);
538
534
  } catch (e) {
539
- console.error(e);
535
+ lib.report_error(e);
540
536
  succeeded = false;
541
537
  }
542
538
  } else {
@@ -626,7 +622,9 @@ MixpanelLib.prototype.init_batchers = function() {
626
622
  }, this),
627
623
  beforeSendHook: _.bind(function(item) {
628
624
  return this._run_hook('before_send_' + attrs.type, item);
629
- }, this)
625
+ }, this),
626
+ errorReporter: this.get_config('error_reporter'),
627
+ stopAllBatchingFunc: _.bind(this.stop_batch_senders, this)
630
628
  }
631
629
  );
632
630
  }, this);
@@ -783,7 +781,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
783
781
  }
784
782
 
785
783
  if (_.isUndefined(event_name)) {
786
- console.error('No event name provided to mixpanel.track');
784
+ this.report_error('No event name provided to mixpanel.track');
787
785
  return;
788
786
  }
789
787
 
@@ -824,7 +822,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
824
822
  delete properties[blacklisted_prop];
825
823
  });
826
824
  } else {
827
- console.error('Invalid value for property_blacklist config: ' + property_blacklist);
825
+ this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
828
826
  }
829
827
 
830
828
  var data = {
@@ -840,8 +838,6 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
840
838
  send_request_options: options
841
839
  }, callback);
842
840
 
843
- this._check_and_handle_triggered_notifications(data);
844
-
845
841
  return ret;
846
842
  });
847
843
 
@@ -1069,7 +1065,7 @@ MixpanelLib.prototype.track_forms = function() {
1069
1065
  */
1070
1066
  MixpanelLib.prototype.time_event = function(event_name) {
1071
1067
  if (_.isUndefined(event_name)) {
1072
- console.error('No event name provided to mixpanel.time_event');
1068
+ this.report_error('No event name provided to mixpanel.time_event');
1073
1069
  return;
1074
1070
  }
1075
1071
 
@@ -1252,7 +1248,6 @@ MixpanelLib.prototype.identify = function(
1252
1248
  this.unregister(ALIAS_ID_KEY);
1253
1249
  this.register({'distinct_id': new_distinct_id});
1254
1250
  }
1255
- this._check_and_handle_notifications(this.get_distinct_id());
1256
1251
  this._flags.identify_called = true;
1257
1252
  // Flush any queued up people requests
1258
1253
  this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback, _remove_callback);
@@ -1342,7 +1337,7 @@ MixpanelLib.prototype.alias = function(alias, original) {
1342
1337
  // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
1343
1338
  // this ID, as it will duplicate users.
1344
1339
  if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
1345
- console.critical('Attempting to create alias for existing People user - aborting.');
1340
+ this.report_error('Attempting to create alias for existing People user - aborting.');
1346
1341
  return -2;
1347
1342
  }
1348
1343
 
@@ -1362,7 +1357,7 @@ MixpanelLib.prototype.alias = function(alias, original) {
1362
1357
  _this.identify(alias);
1363
1358
  });
1364
1359
  } else {
1365
- console.error('alias matches current distinct_id - skipping api call.');
1360
+ this.report_error('alias matches current distinct_id - skipping api call.');
1366
1361
  this.identify(alias);
1367
1362
  return -1;
1368
1363
  }
@@ -1489,14 +1484,6 @@ MixpanelLib.prototype.name_tag = function(name_tag) {
1489
1484
  * // the format {'Header-Name': value}
1490
1485
  * xhr_headers: {}
1491
1486
  *
1492
- * // protocol for fetching in-app message resources, e.g.
1493
- * // 'https://' or 'http://'; defaults to '//' (which defers to the
1494
- * // current page's protocol)
1495
- * inapp_protocol: '//'
1496
- *
1497
- * // whether to open in-app message link in new tab/window
1498
- * inapp_link_new_window: false
1499
- *
1500
1487
  * // whether to ignore or respect the web browser's Do Not Track setting
1501
1488
  * ignore_dnt: false
1502
1489
  * }
@@ -1545,7 +1532,7 @@ MixpanelLib.prototype.get_config = function(prop_name) {
1545
1532
  MixpanelLib.prototype._run_hook = function(hook_name) {
1546
1533
  var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
1547
1534
  if (typeof ret === 'undefined') {
1548
- console.error(hook_name + ' hook did not return a value');
1535
+ this.report_error(hook_name + ' hook did not return a value');
1549
1536
  ret = null;
1550
1537
  }
1551
1538
  return ret;
@@ -1587,75 +1574,6 @@ MixpanelLib.prototype._event_is_disabled = function(event_name) {
1587
1574
  _.include(this.__disabled_events, event_name);
1588
1575
  };
1589
1576
 
1590
- MixpanelLib.prototype._check_and_handle_triggered_notifications = addOptOutCheckMixpanelLib(function(event_data) {
1591
- if (!this._user_decide_check_complete) {
1592
- this._events_tracked_before_user_decide_check_complete.push(event_data);
1593
- } else {
1594
- var arr = this['_triggered_notifs'];
1595
- for (var i = 0; i < arr.length; i++) {
1596
- var notif = new MixpanelNotification(arr[i], this);
1597
- if (notif._matches_event_data(event_data)) {
1598
- this._show_notification(arr[i]);
1599
- return;
1600
- }
1601
- }
1602
- }
1603
- });
1604
-
1605
- MixpanelLib.prototype._check_and_handle_notifications = addOptOutCheckMixpanelLib(function(distinct_id) {
1606
- if (
1607
- !distinct_id ||
1608
- this._flags.identify_called ||
1609
- this.get_config('disable_notifications')
1610
- ) {
1611
- return;
1612
- }
1613
-
1614
- console.log('MIXPANEL NOTIFICATION CHECK');
1615
-
1616
- var data = {
1617
- 'verbose': true,
1618
- 'version': '3',
1619
- 'lib': 'web',
1620
- 'token': this.get_config('token'),
1621
- 'distinct_id': distinct_id
1622
- };
1623
- this._send_request(
1624
- this.get_config('api_host') + '/decide/',
1625
- data,
1626
- {method: 'GET', transport: 'XHR'},
1627
- this._prepare_callback(_.bind(function(result) {
1628
- if (result['notifications'] && result['notifications'].length > 0) {
1629
- this['_triggered_notifs'] = [];
1630
- var notifications = [];
1631
- _.each(result['notifications'], function(notif) {
1632
- (notif['display_triggers'] && notif['display_triggers'].length > 0 ? this['_triggered_notifs'] : notifications).push(notif);
1633
- }, this);
1634
- if (notifications.length > 0) {
1635
- this._show_notification.call(this, notifications[0]);
1636
- }
1637
- }
1638
- this._handle_user_decide_check_complete();
1639
- }, this))
1640
- );
1641
- });
1642
-
1643
- MixpanelLib.prototype._handle_user_decide_check_complete = function() {
1644
- this._user_decide_check_complete = true;
1645
-
1646
- // check notifications against events that were tracked before decide call completed
1647
- var events = this._events_tracked_before_user_decide_check_complete;
1648
- while (events.length > 0) {
1649
- var data = events.shift(); // replay in the same order they came in
1650
- this._check_and_handle_triggered_notifications(data);
1651
- }
1652
- };
1653
-
1654
- MixpanelLib.prototype._show_notification = function(notif_data) {
1655
- var notification = new MixpanelNotification(notif_data, this);
1656
- notification.show();
1657
- };
1658
-
1659
1577
  // perform some housekeeping around GDPR opt-in/out state
1660
1578
  MixpanelLib.prototype._gdpr_init = function() {
1661
1579
  var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage';
@@ -1901,6 +1819,18 @@ MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) {
1901
1819
  this._gdpr_update_persistence(options);
1902
1820
  };
1903
1821
 
1822
+ MixpanelLib.prototype.report_error = function(msg, err) {
1823
+ console.error.apply(console.error, arguments);
1824
+ try {
1825
+ if (!err && !(msg instanceof Error)) {
1826
+ msg = new Error(msg);
1827
+ }
1828
+ this.get_config('error_reporter')(msg, err);
1829
+ } catch(err) {
1830
+ console.error(err);
1831
+ }
1832
+ };
1833
+
1904
1834
  // EXPORTS (for closure compiler)
1905
1835
 
1906
1836
  // MixpanelLib Exports
@@ -1923,9 +1853,6 @@ MixpanelLib.prototype['get_config'] = MixpanelLib.protot
1923
1853
  MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property;
1924
1854
  MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id;
1925
1855
  MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString;
1926
- MixpanelLib.prototype['_check_and_handle_notifications'] = MixpanelLib.prototype._check_and_handle_notifications;
1927
- MixpanelLib.prototype['_handle_user_decide_check_complete'] = MixpanelLib.prototype._handle_user_decide_check_complete;
1928
- MixpanelLib.prototype['_show_notification'] = MixpanelLib.prototype._show_notification;
1929
1856
  MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking;
1930
1857
  MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking;
1931
1858
  MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking;
@@ -1946,8 +1873,6 @@ MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.pro
1946
1873
  MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain;
1947
1874
  MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear;
1948
1875
 
1949
- _.safewrap_class(MixpanelLib, ['identify', '_check_and_handle_notifications', '_show_notification']);
1950
-
1951
1876
 
1952
1877
  var instances = {};
1953
1878
  var extend_mp = function() {
@@ -9,7 +9,6 @@ import {
9
9
  REMOVE_ACTION,
10
10
  UNION_ACTION
11
11
  } from './api-actions';
12
- import Config from './config';
13
12
  import { _, console } from './utils';
14
13
 
15
14
  /*
@@ -25,7 +24,6 @@ import { _, console } from './utils';
25
24
  // This key is deprecated, but we want to check for it to see whether aliasing is allowed.
26
25
  /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id';
27
26
  /** @const */ var ALIAS_ID_KEY = '__alias';
28
- /** @const */ var CAMPAIGN_IDS_KEY = '__cmpns';
29
27
  /** @const */ var EVENT_TIMERS_KEY = '__timers';
30
28
  /** @const */ var RESERVED_PROPERTIES = [
31
29
  SET_QUEUE_KEY,
@@ -37,7 +35,6 @@ import { _, console } from './utils';
37
35
  UNION_QUEUE_KEY,
38
36
  PEOPLE_DISTINCT_ID_KEY,
39
37
  ALIAS_ID_KEY,
40
- CAMPAIGN_IDS_KEY,
41
38
  EVENT_TIMERS_KEY
42
39
  ];
43
40
 
@@ -151,7 +148,6 @@ MixpanelPersistence.prototype.upgrade = function(config) {
151
148
 
152
149
  MixpanelPersistence.prototype.save = function() {
153
150
  if (this.disabled) { return; }
154
- this._expire_notification_campaigns();
155
151
  this.storage.set(
156
152
  this.name,
157
153
  _.JSONEncode(this['props']),
@@ -223,22 +219,6 @@ MixpanelPersistence.prototype.unregister = function(prop) {
223
219
  }
224
220
  };
225
221
 
226
- MixpanelPersistence.prototype._expire_notification_campaigns = _.safewrap(function() {
227
- var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY],
228
- EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN)
229
- if (!campaigns_shown) {
230
- return;
231
- }
232
- for (var campaign_id in campaigns_shown) {
233
- if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) {
234
- delete campaigns_shown[campaign_id];
235
- }
236
- }
237
- if (_.isEmptyObject(campaigns_shown)) {
238
- delete this['props'][CAMPAIGN_IDS_KEY];
239
- }
240
- });
241
-
242
222
  MixpanelPersistence.prototype.update_campaign_params = function() {
243
223
  if (!this.campaign_params_saved) {
244
224
  this.register_once(_.info.campaignParams());
@@ -501,6 +481,5 @@ export {
501
481
  UNION_QUEUE_KEY,
502
482
  PEOPLE_DISTINCT_ID_KEY,
503
483
  ALIAS_ID_KEY,
504
- CAMPAIGN_IDS_KEY,
505
484
  EVENT_TIMERS_KEY
506
485
  };
@@ -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,7 +150,7 @@ 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) &&
@@ -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
  };