mixpanel-browser 2.39.0 → 2.42.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,7 +1,6 @@
1
1
  /* eslint camelcase: "off" */
2
2
  import Config from './config';
3
- import { _, console, userAgent, window, document, navigator, determine_eligibility } from './utils';
4
- import { autotrack } from './autotrack';
3
+ import { _, console, userAgent, window, document, navigator, slice } from './utils';
5
4
  import { FormTracker, LinkTracker } from './dom-trackers';
6
5
  import { RequestBatcher } from './request-batcher';
7
6
  import { MixpanelGroup } from './mixpanel-group';
@@ -53,6 +52,9 @@ var mixpanel_master; // main mixpanel instance / object
53
52
  var INIT_MODULE = 0;
54
53
  var INIT_SNIPPET = 1;
55
54
 
55
+ var IDENTITY_FUNC = function(x) {return x;};
56
+ var NOOP_FUNC = function() {};
57
+
56
58
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
57
59
 
58
60
 
@@ -85,7 +87,6 @@ var DEFAULT_CONFIG = {
85
87
  'api_method': 'POST',
86
88
  'api_transport': 'XHR',
87
89
  'app_host': 'https://mixpanel.com',
88
- 'autotrack': true,
89
90
  'cdn': 'https://cdn.mxpnl.com',
90
91
  'cross_site_cookie': false,
91
92
  'cross_subdomain_cookie': true,
@@ -93,7 +94,7 @@ var DEFAULT_CONFIG = {
93
94
  'persistence_name': '',
94
95
  'cookie_domain': '',
95
96
  'cookie_name': '',
96
- 'loaded': function() {},
97
+ 'loaded': NOOP_FUNC,
97
98
  'store_google': true,
98
99
  'save_referrer': true,
99
100
  'test': false,
@@ -116,10 +117,12 @@ var DEFAULT_CONFIG = {
116
117
  'inapp_protocol': '//',
117
118
  'inapp_link_new_window': false,
118
119
  'ignore_dnt': false,
119
- 'batch_requests': false, // for now
120
+ 'batch_requests': true,
120
121
  'batch_size': 50,
121
122
  'batch_flush_interval_ms': 5000,
122
- 'batch_request_timeout_ms': 90000
123
+ 'batch_request_timeout_ms': 90000,
124
+ 'batch_autostart': true,
125
+ 'hooks': {}
123
126
  };
124
127
 
125
128
  var DOM_LOADED = false;
@@ -166,21 +169,6 @@ var create_mplib = function(token, config, name) {
166
169
  // global debug to be true
167
170
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
168
171
 
169
- instance['__autotrack_enabled'] = instance.get_config('autotrack');
170
- if (instance.get_config('autotrack')) {
171
- var num_buckets = 100;
172
- var num_enabled_buckets = 100;
173
- if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
174
- instance['__autotrack_enabled'] = false;
175
- console.log('Not in active bucket: disabling Automatic Event Collection.');
176
- } else if (!autotrack.isBrowserSupported()) {
177
- instance['__autotrack_enabled'] = false;
178
- console.log('Disabling Automatic Event Collection because this browser is not supported');
179
- } else {
180
- autotrack.init(instance);
181
- }
182
- }
183
-
184
172
  // if target is not defined, we called init after the lib already
185
173
  // loaded, so there won't be an array of things to execute
186
174
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -248,23 +236,13 @@ MixpanelLib.prototype._init = function(token, config, name) {
248
236
  this['config'] = {};
249
237
  this['_triggered_notifs'] = [];
250
238
 
251
- // rollout: enable batch_requests by default for 30% of projects
252
- // (only if they have not specified a value in their init config
253
- // and they aren't using a custom API host)
254
- var variable_features = {};
255
- var api_host = config['api_host'];
256
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
257
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 30)) {
258
- variable_features['batch_requests'] = true;
259
- }
260
-
261
- this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
239
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
262
240
  'name': name,
263
241
  'token': token,
264
242
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
265
243
  }));
266
244
 
267
- this['_jsc'] = function() {};
245
+ this['_jsc'] = NOOP_FUNC;
268
246
 
269
247
  this.__dom_loaded_queue = [];
270
248
  this.__request_queue = [];
@@ -282,20 +260,40 @@ MixpanelLib.prototype._init = function(token, config, name) {
282
260
  this._batch_requests = false;
283
261
  console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
284
262
  } else {
285
- this.start_batch_requests();
263
+ this.init_batchers();
286
264
  if (sendBeacon && window.addEventListener) {
287
- window.addEventListener('unload', _.bind(function() {
288
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
289
- // Since sendBeacon doesn't report success/failure, events will not be removed from
290
- // the persistent store; if the site is loaded again, the events will be flushed again
291
- // on startup and deduplicated on the Mixpanel server side.
292
- this.request_batchers.events.flush({sendBeacon: true});
293
- }, this));
265
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
266
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
267
+ // events will not be removed from the persistent store; if the site is loaded again,
268
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
269
+ // side.
270
+ // There is no reliable way to capture only page close events, so we lean on the
271
+ // visibilitychange and pagehide events as recommended at
272
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
273
+ // These events fire when the user clicks away from the current page/tab, so will occur
274
+ // more frequently than page unload, but are the only mechanism currently for capturing
275
+ // this scenario somewhat reliably.
276
+ var flush_on_unload = _.bind(function() {
277
+ if (!this.request_batchers.events.stopped) {
278
+ this.request_batchers.events.flush({unloading: true});
279
+ }
280
+ }, this);
281
+ window.addEventListener('pagehide', function(ev) {
282
+ if (ev['persisted']) {
283
+ flush_on_unload();
284
+ }
285
+ });
286
+ window.addEventListener('visibilitychange', function() {
287
+ if (document['visibilityState'] === 'hidden') {
288
+ flush_on_unload();
289
+ }
290
+ });
294
291
  }
295
292
  }
296
293
  }
297
294
 
298
295
  this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']);
296
+ this.unpersisted_superprops = {};
299
297
  this._gdpr_init();
300
298
 
301
299
  var uuid = _.UUID();
@@ -458,6 +456,13 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
458
456
  console.error(e);
459
457
  succeeded = false;
460
458
  }
459
+ try {
460
+ if (callback) {
461
+ callback(succeeded ? 1 : 0);
462
+ }
463
+ } catch (e) {
464
+ console.error(e);
465
+ }
461
466
  } else if (USE_XHR) {
462
467
  try {
463
468
  var req = new XMLHttpRequest();
@@ -592,32 +597,53 @@ MixpanelLib.prototype._execute_array = function(array) {
592
597
 
593
598
  // request queueing utils
594
599
 
595
- MixpanelLib.prototype.start_batch_requests = function() {
600
+ MixpanelLib.prototype.are_batchers_initialized = function() {
601
+ return !!this.request_batchers.events;
602
+ };
603
+
604
+ MixpanelLib.prototype.init_batchers = function() {
596
605
  var token = this.get_config('token');
597
- if (!this.request_batchers.events) { // no batchers initialized yet
598
- var batcher_config = {
599
- libConfig: this['config'],
600
- sendRequestFunc: _.bind(function(endpoint, data, options, cb) {
601
- this._send_request(
602
- this.get_config('api_host') + endpoint,
603
- encode_data_for_request(data),
604
- options,
605
- this._prepare_callback(cb, data)
606
- );
607
- }, this)
608
- };
606
+ if (!this.are_batchers_initialized()) {
607
+ var batcher_for = _.bind(function(attrs) {
608
+ return new RequestBatcher(
609
+ '__mpq_' + token + attrs.queue_suffix,
610
+ {
611
+ libConfig: this['config'],
612
+ sendRequestFunc: _.bind(function(data, options, cb) {
613
+ this._send_request(
614
+ this.get_config('api_host') + attrs.endpoint,
615
+ encode_data_for_request(data),
616
+ options,
617
+ this._prepare_callback(cb, data)
618
+ );
619
+ }, this),
620
+ beforeSendHook: _.bind(function(item) {
621
+ return this._run_hook('before_send_' + attrs.type, item);
622
+ }, this)
623
+ }
624
+ );
625
+ }, this);
609
626
  this.request_batchers = {
610
- events: new RequestBatcher('__mpq_' + token + '_ev', '/track/', batcher_config),
611
- people: new RequestBatcher('__mpq_' + token + '_pp', '/engage/', batcher_config),
612
- groups: new RequestBatcher('__mpq_' + token + '_gr', '/groups/', batcher_config)
627
+ events: batcher_for({type: 'events', endpoint: '/track/', queue_suffix: '_ev'}),
628
+ people: batcher_for({type: 'people', endpoint: '/engage/', queue_suffix: '_pp'}),
629
+ groups: batcher_for({type: 'groups', endpoint: '/groups/', queue_suffix: '_gr'})
613
630
  };
614
631
  }
615
- _.each(this.request_batchers, function(batcher) {
616
- batcher.start();
617
- });
632
+ if (this.get_config('batch_autostart')) {
633
+ this.start_batch_senders();
634
+ }
618
635
  };
619
636
 
620
- MixpanelLib.prototype.stop_batch_requests = function() {
637
+ MixpanelLib.prototype.start_batch_senders = function() {
638
+ if (this.are_batchers_initialized()) {
639
+ this._batch_requests = true;
640
+ _.each(this.request_batchers, function(batcher) {
641
+ batcher.start();
642
+ });
643
+ }
644
+ };
645
+
646
+ MixpanelLib.prototype.stop_batch_senders = function() {
621
647
  this._batch_requests = false;
622
648
  _.each(this.request_batchers, function(batcher) {
623
649
  batcher.stop();
@@ -662,23 +688,30 @@ MixpanelLib.prototype.disable = function(events) {
662
688
 
663
689
  // internal method for handling track vs batch-enqueue logic
664
690
  MixpanelLib.prototype._track_or_batch = function(options, callback) {
665
- var truncated_data = options.truncated_data;
691
+ var truncated_data = _.truncate(options.data, 255);
666
692
  var endpoint = options.endpoint;
667
693
  var batcher = options.batcher;
668
694
  var should_send_immediately = options.should_send_immediately;
669
695
  var send_request_options = options.send_request_options || {};
670
- callback = callback || function() {};
696
+ callback = callback || NOOP_FUNC;
671
697
 
672
698
  var request_enqueued_or_initiated = true;
673
699
  var send_request_immediately = _.bind(function() {
674
- console.log('MIXPANEL REQUEST:');
675
- console.log(truncated_data);
676
- return this._send_request(
677
- endpoint,
678
- encode_data_for_request(truncated_data),
679
- send_request_options,
680
- this._prepare_callback(callback, truncated_data)
681
- );
700
+ if (!send_request_options.skip_hooks) {
701
+ truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
702
+ }
703
+ if (truncated_data) {
704
+ console.log('MIXPANEL REQUEST:');
705
+ console.log(truncated_data);
706
+ return this._send_request(
707
+ endpoint,
708
+ encode_data_for_request(truncated_data),
709
+ send_request_options,
710
+ this._prepare_callback(callback, truncated_data)
711
+ );
712
+ } else {
713
+ return null;
714
+ }
682
715
  }, this);
683
716
 
684
717
  if (this._batch_requests && !should_send_immediately) {
@@ -731,7 +764,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
731
764
  }
732
765
  var should_send_immediately = options['send_immediately'];
733
766
  if (typeof callback !== 'function') {
734
- callback = function() {};
767
+ callback = NOOP_FUNC;
735
768
  }
736
769
 
737
770
  if (_.isUndefined(event_name)) {
@@ -766,6 +799,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
766
799
  {},
767
800
  _.info.properties(),
768
801
  this['persistence'].properties(),
802
+ this.unpersisted_superprops,
769
803
  properties
770
804
  );
771
805
 
@@ -783,7 +817,8 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
783
817
  'properties': properties
784
818
  };
785
819
  var ret = this._track_or_batch({
786
- truncated_data: _.truncate(data, 255),
820
+ type: 'events',
821
+ data: data,
787
822
  endpoint: this.get_config('api_host') + '/track/',
788
823
  batcher: this.request_batchers.events,
789
824
  should_send_immediately: should_send_immediately,
@@ -1030,6 +1065,27 @@ MixpanelLib.prototype.time_event = function(event_name) {
1030
1065
  this['persistence'].set_event_timer(event_name, new Date().getTime());
1031
1066
  };
1032
1067
 
1068
+ var REGISTER_DEFAULTS = {
1069
+ 'persistent': true
1070
+ };
1071
+ /**
1072
+ * Helper to parse options param for register methods, maintaining
1073
+ * legacy support for plain "days" param instead of options object
1074
+ * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods
1075
+ * @returns {Object} options object
1076
+ */
1077
+ var options_for_register = function(days_or_options) {
1078
+ var options;
1079
+ if (_.isObject(days_or_options)) {
1080
+ options = days_or_options;
1081
+ } else if (!_.isUndefined(days_or_options)) {
1082
+ options = {'days': days_or_options};
1083
+ } else {
1084
+ options = {};
1085
+ }
1086
+ return _.extend({}, REGISTER_DEFAULTS, options);
1087
+ };
1088
+
1033
1089
  /**
1034
1090
  * Register a set of super properties, which are included with all
1035
1091
  * events. This will overwrite previous super property values.
@@ -1045,11 +1101,21 @@ MixpanelLib.prototype.time_event = function(event_name) {
1045
1101
  * 'Account Type': 'Free'
1046
1102
  * });
1047
1103
  *
1104
+ * // register only for the current pageload
1105
+ * mixpanel.register({'Name': 'Pat'}, {persistent: false});
1106
+ *
1048
1107
  * @param {Object} properties An associative array of properties to store about the user
1049
- * @param {Number} [days] How many days since the user's last visit to store the super properties
1108
+ * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props)
1109
+ * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props)
1110
+ * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
1050
1111
  */
1051
- MixpanelLib.prototype.register = function(props, days) {
1052
- this['persistence'].register(props, days);
1112
+ MixpanelLib.prototype.register = function(props, days_or_options) {
1113
+ var options = options_for_register(days_or_options);
1114
+ if (options['persistent']) {
1115
+ this['persistence'].register(props, options['days']);
1116
+ } else {
1117
+ _.extend(this.unpersisted_superprops, props);
1118
+ }
1053
1119
  };
1054
1120
 
1055
1121
  /**
@@ -1063,6 +1129,11 @@ MixpanelLib.prototype.register = function(props, days) {
1063
1129
  * 'First Login Date': new Date().toISOString()
1064
1130
  * });
1065
1131
  *
1132
+ * // register once, only for the current pageload
1133
+ * mixpanel.register_once({
1134
+ * 'First interaction time': new Date().toISOString()
1135
+ * }, 'None', {persistent: false});
1136
+ *
1066
1137
  * ### Notes:
1067
1138
  *
1068
1139
  * If default_value is specified, current super properties
@@ -1070,19 +1141,40 @@ MixpanelLib.prototype.register = function(props, days) {
1070
1141
  *
1071
1142
  * @param {Object} properties An associative array of properties to store about the user
1072
1143
  * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
1073
- * @param {Number} [days] How many days since the users last visit to store the super properties
1144
+ * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props)
1145
+ * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props)
1146
+ * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
1074
1147
  */
1075
- MixpanelLib.prototype.register_once = function(props, default_value, days) {
1076
- this['persistence'].register_once(props, default_value, days);
1148
+ MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
1149
+ var options = options_for_register(days_or_options);
1150
+ if (options['persistent']) {
1151
+ this['persistence'].register_once(props, default_value, options['days']);
1152
+ } else {
1153
+ if (typeof(default_value) === 'undefined') {
1154
+ default_value = 'None';
1155
+ }
1156
+ _.each(props, function(val, prop) {
1157
+ if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) {
1158
+ this.unpersisted_superprops[prop] = val;
1159
+ }
1160
+ }, this);
1161
+ }
1077
1162
  };
1078
1163
 
1079
1164
  /**
1080
1165
  * Delete a super property stored with the current user.
1081
1166
  *
1082
1167
  * @param {String} property The name of the super property to remove
1168
+ * @param {Object} [options]
1169
+ * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
1083
1170
  */
1084
- MixpanelLib.prototype.unregister = function(property) {
1085
- this['persistence'].unregister(property);
1171
+ MixpanelLib.prototype.unregister = function(property, options) {
1172
+ options = options_for_register(options);
1173
+ if (options['persistent']) {
1174
+ this['persistence'].unregister(property);
1175
+ } else {
1176
+ delete this.unpersisted_superprops[property];
1177
+ }
1086
1178
  };
1087
1179
 
1088
1180
  MixpanelLib.prototype._register_single = function(prop, value) {
@@ -1153,7 +1245,10 @@ MixpanelLib.prototype.identify = function(
1153
1245
  // send an $identify event any time the distinct_id is changing - logic on the server
1154
1246
  // will determine whether or not to do anything with it.
1155
1247
  if (new_distinct_id !== previous_distinct_id) {
1156
- this.track('$identify', { 'distinct_id': new_distinct_id, '$anon_distinct_id': previous_distinct_id });
1248
+ this.track('$identify', {
1249
+ 'distinct_id': new_distinct_id,
1250
+ '$anon_distinct_id': previous_distinct_id
1251
+ }, {skip_hooks: true});
1157
1252
  }
1158
1253
  };
1159
1254
 
@@ -1242,7 +1337,12 @@ MixpanelLib.prototype.alias = function(alias, original) {
1242
1337
  }
1243
1338
  if (alias !== original) {
1244
1339
  this._register_single(ALIAS_ID_KEY, alias);
1245
- return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() {
1340
+ return this.track('$create_alias', {
1341
+ 'alias': alias,
1342
+ 'distinct_id': original
1343
+ }, {
1344
+ skip_hooks: true
1345
+ }, function() {
1246
1346
  // Flush the people queue
1247
1347
  _this.identify(alias);
1248
1348
  });
@@ -1421,6 +1521,21 @@ MixpanelLib.prototype.get_config = function(prop_name) {
1421
1521
  return this['config'][prop_name];
1422
1522
  };
1423
1523
 
1524
+ /**
1525
+ * Fetch a hook function from config, with safe default, and run it
1526
+ * against the given arguments
1527
+ * @param {string} hook_name which hook to retrieve
1528
+ * @returns {any|null} return value of user-provided hook, or null if nothing was returned
1529
+ */
1530
+ MixpanelLib.prototype._run_hook = function(hook_name) {
1531
+ var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
1532
+ if (typeof ret === 'undefined') {
1533
+ console.error(hook_name + ' hook did not return a value');
1534
+ ret = null;
1535
+ }
1536
+ return ret;
1537
+ };
1538
+
1424
1539
  /**
1425
1540
  * Returns the value of the super property named property_name. If no such
1426
1541
  * property is set, get_property() will return the undefined value.
@@ -1806,7 +1921,8 @@ MixpanelLib.prototype['set_group'] = MixpanelLib.protot
1806
1921
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
1807
1922
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
1808
1923
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
1809
- MixpanelLib.prototype['stop_batch_requests'] = MixpanelLib.prototype.stop_batch_requests;
1924
+ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
1925
+ MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
1810
1926
 
1811
1927
  // MixpanelPersistence Exports
1812
1928
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
@@ -1931,7 +2047,7 @@ export function init_from_snippet() {
1931
2047
  }
1932
2048
  if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) {
1933
2049
  // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
1934
- console.error('Mixpanel library has already been downloaded at least once.');
2050
+ console.critical('The Mixpanel library has already been downloaded at least once. Ensure that the Mixpanel code snippet only appears once on the page (and is not double-loaded by a tag manager) in order to avoid errors.');
1935
2051
  return;
1936
2052
  }
1937
2053
  var snippet_version = mixpanel_master['__SV'] || 0;
@@ -110,9 +110,13 @@ MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name,
110
110
  * Permanently delete a group.
111
111
  *
112
112
  * ### Usage:
113
+ *
113
114
  * mixpanel.get_group('company', 'mixpanel').delete();
115
+ *
116
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
114
117
  */
115
118
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
119
+ // bracket notation above prevents a minification error related to reserved words
116
120
  var data = this.delete_action();
117
121
  return this._send_request(data, callback);
118
122
  });
@@ -140,7 +144,8 @@ MixpanelGroup.prototype._send_request = function(data, callback) {
140
144
 
141
145
  var date_encoded_data = _.encodeDates(data);
142
146
  return this._mixpanel._track_or_batch({
143
- truncated_data: _.truncate(date_encoded_data, 255),
147
+ type: 'groups',
148
+ data: date_encoded_data,
144
149
  endpoint: this._get_config('api_host') + '/groups/',
145
150
  batcher: this._mixpanel.request_batchers.groups
146
151
  }, callback);
@@ -330,7 +330,6 @@ MixpanelPeople.prototype._send_request = function(data, callback) {
330
330
  }
331
331
 
332
332
  var date_encoded_data = _.encodeDates(data);
333
- var truncated_data = _.truncate(date_encoded_data, 255);
334
333
 
335
334
  if (!this._identify_called()) {
336
335
  this._enqueue(data);
@@ -341,11 +340,12 @@ MixpanelPeople.prototype._send_request = function(data, callback) {
341
340
  callback(-1);
342
341
  }
343
342
  }
344
- return truncated_data;
343
+ return _.truncate(date_encoded_data, 255);
345
344
  }
346
345
 
347
346
  return this._mixpanel._track_or_batch({
348
- truncated_data: truncated_data,
347
+ type: 'people',
348
+ data: date_encoded_data,
349
349
  endpoint: this._get_config('api_host') + '/engage/',
350
350
  batcher: this._mixpanel.request_batchers.people
351
351
  }, callback);
@@ -12,18 +12,18 @@ var logger = console_with_prefix('batch');
12
12
  * Uses RequestQueue to manage the backing store.
13
13
  * @constructor
14
14
  */
15
- var RequestBatcher = function(storageKey, endpoint, options) {
15
+ var RequestBatcher = function(storageKey, options) {
16
16
  this.queue = new RequestQueue(storageKey, {storage: options.storage});
17
- this.endpoint = endpoint;
18
17
 
19
18
  this.libConfig = options.libConfig;
20
19
  this.sendRequest = options.sendRequestFunc;
20
+ this.beforeSendHook = options.beforeSendHook;
21
21
 
22
22
  // seed variable batch size + flush interval with configured values
23
23
  this.batchSize = this.libConfig['batch_size'];
24
24
  this.flushInterval = this.libConfig['batch_flush_interval_ms'];
25
25
 
26
- this.stopped = false;
26
+ this.stopped = !this.libConfig['batch_autostart'];
27
27
  };
28
28
 
29
29
  /**
@@ -103,18 +103,29 @@ RequestBatcher.prototype.flush = function(options) {
103
103
  }
104
104
 
105
105
  options = options || {};
106
+ var timeoutMS = this.libConfig['batch_request_timeout_ms'];
107
+ var startTime = new Date().getTime();
106
108
  var currentBatchSize = this.batchSize;
107
109
  var batch = this.queue.fillBatch(currentBatchSize);
108
- if (batch.length < 1) {
110
+ var dataForRequest = [];
111
+ var transformedItems = {};
112
+ _.each(batch, function(item) {
113
+ var payload = item['payload'];
114
+ if (this.beforeSendHook && !item.orphaned) {
115
+ payload = this.beforeSendHook(payload);
116
+ }
117
+ if (payload) {
118
+ dataForRequest.push(payload);
119
+ }
120
+ transformedItems[item['id']] = payload;
121
+ }, this);
122
+ if (dataForRequest.length < 1) {
109
123
  this.resetFlush();
110
124
  return; // nothing to do
111
125
  }
112
126
 
113
127
  this.requestInProgress = true;
114
128
 
115
- var timeoutMS = this.libConfig['batch_request_timeout_ms'];
116
- var startTime = new Date().getTime();
117
- var dataForRequest = _.map(batch, function(item) { return item['payload']; });
118
129
  var batchSendCallback = _.bind(function(res) {
119
130
  this.requestInProgress = false;
120
131
 
@@ -124,7 +135,10 @@ RequestBatcher.prototype.flush = function(options) {
124
135
  // flush operation if something goes wrong
125
136
 
126
137
  var removeItemsFromQueue = false;
127
- if (
138
+ if (options.unloading) {
139
+ // update persisted data to include hook transformations
140
+ this.queue.updatePayloads(transformedItems);
141
+ } else if (
128
142
  _.isObject(res) &&
129
143
  res.error === 'timeout' &&
130
144
  new Date().getTime() - startTime >= timeoutMS
@@ -134,9 +148,9 @@ RequestBatcher.prototype.flush = function(options) {
134
148
  } else if (
135
149
  _.isObject(res) &&
136
150
  res.xhr_req &&
137
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
151
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
138
152
  ) {
139
- // network or API error, retry
153
+ // network or API error, or 429 Too Many Requests, retry
140
154
  var retryMS = this.flushInterval * 2;
141
155
  var headers = res.xhr_req['responseHeaders'];
142
156
  if (headers) {
@@ -184,11 +198,11 @@ RequestBatcher.prototype.flush = function(options) {
184
198
  ignore_json_errors: true, // eslint-disable-line camelcase
185
199
  timeout_ms: timeoutMS // eslint-disable-line camelcase
186
200
  };
187
- if (options.sendBeacon) {
201
+ if (options.unloading) {
188
202
  requestOptions.transport = 'sendBeacon';
189
203
  }
190
- logger.log('MIXPANEL REQUEST:', this.endpoint, dataForRequest);
191
- this.sendRequest(this.endpoint, dataForRequest, requestOptions, batchSendCallback);
204
+ logger.log('MIXPANEL REQUEST:', dataForRequest);
205
+ this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
192
206
 
193
207
  } catch(err) {
194
208
  logger.error('Error flushing request queue', err);