mixpanel-browser 2.53.0 → 2.54.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.53.0",
3
+ "version": "2.54.1",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
6
  "directories": {
@@ -54,7 +54,6 @@
54
54
  "request": "2.88.0",
55
55
  "rollup": "2.79.1",
56
56
  "rollup-plugin-esbuild": "4.10.3",
57
- "rollup-plugin-npm": "1.4.0",
58
57
  "sinon": "8.1.1",
59
58
  "sinon-chai": "3.5.0",
60
59
  "webpack": "1.12.2"
package/rollup.config.js CHANGED
@@ -1,11 +1,11 @@
1
- import npm from 'rollup-plugin-npm';
1
+ import nodeResolve from '@rollup/plugin-node-resolve';
2
2
 
3
3
  export default {
4
4
  plugins: [
5
- npm({
5
+ nodeResolve({
6
6
  browser: true,
7
7
  main: true,
8
8
  jsnext: true,
9
9
  })
10
10
  ]
11
- }
11
+ };
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.53.0'
3
+ LIB_VERSION: '2.54.1'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -0,0 +1,22 @@
1
+ // For loading separate bundles asynchronously via script tag
2
+ // so that we don't load them until they are needed at runtime.
3
+ export function loadAsync (src, onload) {
4
+ var scriptEl = document.createElement('script');
5
+ scriptEl.type = 'text/javascript';
6
+ scriptEl.async = true;
7
+ scriptEl.onload = onload;
8
+ scriptEl.src = src;
9
+ document.head.appendChild(scriptEl);
10
+ }
11
+
12
+ // For builds that have everything in one bundle, no extra work.
13
+ export function loadNoop (_src, onload) {
14
+ onload();
15
+ }
16
+
17
+ // For builds that do NOT want any extra bundles (e.g. session recorder)
18
+ // and just the main SDK, throw an error when trying to load a separate bundle.
19
+ // eslint-disable-next-line no-unused-vars
20
+ export function loadThrowError (src, _onload) {
21
+ throw new Error('This build of Mixpanel only includes core SDK functionality, could not load ' + src);
22
+ }
@@ -1,4 +1,5 @@
1
1
  /* eslint camelcase: "off" */
2
2
  import { init_from_snippet } from '../mixpanel-core';
3
+ import {loadAsync} from './bundle-loaders';
3
4
 
4
- init_from_snippet();
5
+ init_from_snippet(loadAsync);
@@ -0,0 +1,7 @@
1
+ /* eslint camelcase: "off" */
2
+ import {init_as_module} from '../mixpanel-core.js';
3
+ import {loadThrowError} from './bundle-loaders.js';
4
+
5
+ var mixpanel = init_as_module(loadThrowError);
6
+
7
+ export default mixpanel;
@@ -0,0 +1,7 @@
1
+ /* eslint camelcase: "off" */
2
+ import {init_as_module} from '../mixpanel-core.js';
3
+ import {loadAsync} from './bundle-loaders.js';
4
+
5
+ var mixpanel = init_as_module(loadAsync);
6
+
7
+ export default mixpanel;
@@ -1,6 +1,9 @@
1
1
  /* eslint camelcase: "off" */
2
+ import '../recorder';
3
+
2
4
  import { init_as_module } from '../mixpanel-core';
5
+ import { loadNoop } from './bundle-loaders';
3
6
 
4
- var mixpanel = init_as_module();
7
+ var mixpanel = init_as_module(loadNoop);
5
8
 
6
9
  export default mixpanel;
@@ -47,6 +47,12 @@ Globals should be all caps
47
47
  */
48
48
 
49
49
  var init_type; // MODULE or SNIPPET loader
50
+ // allow bundlers to specify how extra code (recorder bundle) should be loaded
51
+ // eslint-disable-next-line no-unused-vars
52
+ var load_extra_bundle = function(src, _onload) {
53
+ throw new Error(src + ' not available in this build.');
54
+ };
55
+
50
56
  var mixpanel_master; // main mixpanel instance / object
51
57
  var INIT_MODULE = 0;
52
58
  var INIT_SNIPPET = 1;
@@ -140,7 +146,9 @@ var DEFAULT_CONFIG = {
140
146
  'hooks': {},
141
147
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
142
148
  'record_block_selector': 'img, video',
149
+ 'record_collect_fonts': false,
143
150
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
151
+ 'record_inline_images': false,
144
152
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
145
153
  'record_mask_text_selector': '*',
146
154
  'record_max_ms': MAX_RECORDING_MS,
@@ -376,12 +384,7 @@ MixpanelLib.prototype.start_session_recording = addOptOutCheckMixpanelLib(functi
376
384
  }, this);
377
385
 
378
386
  if (_.isUndefined(window['__mp_recorder'])) {
379
- var scriptEl = document.createElement('script');
380
- scriptEl.type = 'text/javascript';
381
- scriptEl.async = true;
382
- scriptEl.onload = handleLoadedRecorder;
383
- scriptEl.src = this.get_config('recorder_src');
384
- document.head.appendChild(scriptEl);
387
+ load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
385
388
  } else {
386
389
  handleLoadedRecorder();
387
390
  }
@@ -680,7 +683,8 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
680
683
  lib.report_error(error);
681
684
  if (callback) {
682
685
  if (verbose_mode) {
683
- callback({status: 0, error: error, xhr_req: req});
686
+ var response_headers = req['responseHeaders'] || {};
687
+ callback({status: 0, httpStatusCode: req['status'], error: error, retryAfter: response_headers['Retry-After']});
684
688
  } else {
685
689
  callback(0);
686
690
  }
@@ -780,6 +784,7 @@ MixpanelLib.prototype.init_batchers = function() {
780
784
  attrs.queue_key,
781
785
  {
782
786
  libConfig: this['config'],
787
+ errorReporter: this.get_config('error_reporter'),
783
788
  sendRequestFunc: _.bind(function(data, options, cb) {
784
789
  this._send_request(
785
790
  this.get_config('api_host') + attrs.endpoint,
@@ -791,8 +796,8 @@ MixpanelLib.prototype.init_batchers = function() {
791
796
  beforeSendHook: _.bind(function(item) {
792
797
  return this._run_hook('before_send_' + attrs.type, item);
793
798
  }, this),
794
- errorReporter: this.get_config('error_reporter'),
795
- stopAllBatchingFunc: _.bind(this.stop_batch_senders, this)
799
+ stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
800
+ usePersistence: true
796
801
  }
797
802
  );
798
803
  }, this);
@@ -2241,7 +2246,8 @@ var add_dom_loaded_handler = function() {
2241
2246
  _.register_event(window, 'load', dom_loaded_handler, true);
2242
2247
  };
2243
2248
 
2244
- export function init_from_snippet() {
2249
+ export function init_from_snippet(bundle_loader) {
2250
+ load_extra_bundle = bundle_loader;
2245
2251
  init_type = INIT_SNIPPET;
2246
2252
  mixpanel_master = window[PRIMARY_INSTANCE_NAME];
2247
2253
 
@@ -2281,7 +2287,8 @@ export function init_from_snippet() {
2281
2287
  add_dom_loaded_handler();
2282
2288
  }
2283
2289
 
2284
- export function init_as_module() {
2290
+ export function init_as_module(bundle_loader) {
2291
+ load_extra_bundle = bundle_loader;
2285
2292
  init_type = INIT_MODULE;
2286
2293
  mixpanel_master = new MixpanelLib();
2287
2294
 
@@ -1,11 +1,36 @@
1
- import {default as record} from 'rrweb/es/rrweb/packages/rrweb/src/record/index.js';
1
+ import { record } from 'rrweb';
2
+ import { IncrementalSource, EventType } from '@rrweb/types';
2
3
 
3
- import { MAX_RECORDING_MS, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase
4
+ import { MAX_RECORDING_MS, console_with_prefix, _, window} from '../utils'; // eslint-disable-line camelcase
4
5
  import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
6
+ import { RequestBatcher } from '../request-batcher';
5
7
 
6
8
  var logger = console_with_prefix('recorder');
7
9
  var CompressionStream = window['CompressionStream'];
8
10
 
11
+ var RECORDER_BATCHER_LIB_CONFIG = {
12
+ 'batch_size': 1000,
13
+ 'batch_flush_interval_ms': 10 * 1000,
14
+ 'batch_request_timeout_ms': 90 * 1000,
15
+ 'batch_autostart': true
16
+ };
17
+
18
+ var ACTIVE_SOURCES = new Set([
19
+ IncrementalSource.MouseMove,
20
+ IncrementalSource.MouseInteraction,
21
+ IncrementalSource.Scroll,
22
+ IncrementalSource.ViewportResize,
23
+ IncrementalSource.Input,
24
+ IncrementalSource.TouchMove,
25
+ IncrementalSource.MediaInteraction,
26
+ IncrementalSource.Drag,
27
+ IncrementalSource.Selection,
28
+ ]);
29
+
30
+ function isUserEvent(ev) {
31
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
32
+ }
33
+
9
34
  var MixpanelRecorder = function(mixpanelInstance) {
10
35
  this._mixpanel = mixpanelInstance;
11
36
 
@@ -16,14 +41,24 @@ var MixpanelRecorder = function(mixpanelInstance) {
16
41
  this.seqNo = 0;
17
42
  this.replayId = null;
18
43
  this.replayStartTime = null;
19
- this.batchStartTime = null;
20
- this.replayLengthMs = 0;
21
44
  this.sendBatchId = null;
22
45
 
23
46
  this.idleTimeoutId = null;
24
47
  this.maxTimeoutId = null;
25
48
 
26
49
  this.recordMaxMs = MAX_RECORDING_MS;
50
+ this._initBatcher();
51
+ };
52
+
53
+
54
+ MixpanelRecorder.prototype._initBatcher = function () {
55
+ this.batcher = new RequestBatcher('__mprec', {
56
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
57
+ sendRequestFunc: _.bind(this.flushEventsWithOptOut, this),
58
+ errorReporter: _.bind(this.reportError, this),
59
+ flushOnlyOnInterval: true,
60
+ usePersistence: false
61
+ });
27
62
  };
28
63
 
29
64
  // eslint-disable-next-line camelcase
@@ -31,7 +66,7 @@ MixpanelRecorder.prototype.get_config = function(configVar) {
31
66
  return this._mixpanel.get_config(configVar);
32
67
  };
33
68
 
34
- MixpanelRecorder.prototype.startRecording = function () {
69
+ MixpanelRecorder.prototype.startRecording = function (shouldStopBatcher) {
35
70
  if (this._stopRecording !== null) {
36
71
  logger.log('Recording already in progress, skipping startRecording.');
37
72
  return;
@@ -45,12 +80,18 @@ MixpanelRecorder.prototype.startRecording = function () {
45
80
 
46
81
  this.recEvents = [];
47
82
  this.seqNo = 0;
48
- this.startDate = new Date();
49
- this.replayStartTime = this.startDate.getTime();
50
- this.batchStartTime = this.replayStartTime;
83
+ this.replayStartTime = null;
51
84
 
52
85
  this.replayId = _.UUID();
53
- this.replayLengthMs = 0;
86
+
87
+ if (shouldStopBatcher) {
88
+ // this is the case when we're starting recording after a reset
89
+ // and don't want to send anything over the network until there's
90
+ // actual user activity
91
+ this.batcher.stop();
92
+ } else {
93
+ this.batcher.start();
94
+ }
54
95
 
55
96
  var resetIdleTimeout = _.bind(function () {
56
97
  clearTimeout(this.idleTimeoutId);
@@ -62,26 +103,32 @@ MixpanelRecorder.prototype.startRecording = function () {
62
103
 
63
104
  this._stopRecording = record({
64
105
  'emit': _.bind(function (ev) {
65
- this.recEvents.push(ev);
66
- this.replayLengthMs = new Date().getTime() - this.replayStartTime;
67
- resetIdleTimeout();
106
+ this.batcher.enqueue(ev);
107
+ if (isUserEvent(ev)) {
108
+ if (this.batcher.stopped) {
109
+ // start flushing again after user activity
110
+ this.batcher.start();
111
+ }
112
+ resetIdleTimeout();
113
+ }
68
114
  }, this),
69
- 'maskAllInputs': true,
70
- 'maskTextSelector': this.get_config('record_mask_text_selector'),
115
+ 'blockClass': this.get_config('record_block_class'),
71
116
  'blockSelector': this.get_config('record_block_selector'),
117
+ 'collectFonts': this.get_config('record_collect_fonts'),
118
+ 'inlineImages': this.get_config('record_inline_images'),
119
+ 'maskAllInputs': true,
72
120
  'maskTextClass': this.get_config('record_mask_text_class'),
73
- 'blockClass': this.get_config('record_block_class'),
121
+ 'maskTextSelector': this.get_config('record_mask_text_selector')
74
122
  });
75
123
 
76
124
  resetIdleTimeout();
77
125
 
78
- this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000);
79
126
  this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
80
127
  };
81
128
 
82
129
  MixpanelRecorder.prototype.resetRecording = function () {
83
130
  this.stopRecording();
84
- this.startRecording();
131
+ this.startRecording(true);
85
132
  };
86
133
 
87
134
  MixpanelRecorder.prototype.stopRecording = function () {
@@ -90,10 +137,16 @@ MixpanelRecorder.prototype.stopRecording = function () {
90
137
  this._stopRecording = null;
91
138
  }
92
139
 
93
- this._flushEvents(); // flush any remaining events
140
+ if (this.batcher.stopped) {
141
+ // never got user activity to flush after reset, so just clear the batcher
142
+ this.batcher.clear();
143
+ } else {
144
+ // flush any remaining events from running batcher
145
+ this.batcher.flush();
146
+ this.batcher.stop();
147
+ }
94
148
  this.replayId = null;
95
149
 
96
- clearInterval(this.sendBatchId);
97
150
  clearTimeout(this.idleTimeoutId);
98
151
  clearTimeout(this.maxTimeoutId);
99
152
  };
@@ -102,8 +155,8 @@ MixpanelRecorder.prototype.stopRecording = function () {
102
155
  * Flushes the current batch of events to the server, but passes an opt-out callback to make sure
103
156
  * we stop recording and dump any queued events if the user has opted out.
104
157
  */
105
- MixpanelRecorder.prototype.flushEventsWithOptOut = function () {
106
- this._flushEvents(_.bind(this._onOptOut, this));
158
+ MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
159
+ this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
107
160
  };
108
161
 
109
162
  MixpanelRecorder.prototype._onOptOut = function (code) {
@@ -114,33 +167,60 @@ MixpanelRecorder.prototype._onOptOut = function (code) {
114
167
  }
115
168
  };
116
169
 
117
- MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) {
170
+ MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
171
+ var onSuccess = _.bind(function (response, responseBody) {
172
+ // Increment sequence counter only if the request was successful to guarantee ordering.
173
+ // RequestBatcher will always flush the next batch after the previous one succeeds.
174
+ if (response.status === 200) {
175
+ this.seqNo++;
176
+ }
177
+
178
+ callback({
179
+ status: 0,
180
+ httpStatusCode: response.status,
181
+ responseBody: responseBody,
182
+ retryAfter: response.headers.get('Retry-After')
183
+ });
184
+ }, this);
185
+
118
186
  window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
119
187
  'method': 'POST',
120
188
  'headers': {
121
189
  'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
122
190
  'Content-Type': 'application/octet-stream'
123
191
  },
124
- 'body': reqBody
192
+ 'body': reqBody,
193
+ }).then(function (response) {
194
+ response.json().then(function (responseBody) {
195
+ onSuccess(response, responseBody);
196
+ }).catch(function (error) {
197
+ callback({error: error});
198
+ });
199
+ }).catch(function (error) {
200
+ callback({error: error});
125
201
  });
126
202
  };
127
203
 
128
- /**
129
- * @api private
130
- * Private method, flushes the current batch of events to the server.
131
- */
132
- MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
133
- var numEvents = this.recEvents.length;
204
+ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
205
+ const numEvents = data.length;
206
+
134
207
  if (numEvents > 0) {
208
+ // each rrweb event has a timestamp - leverage those to get time properties
209
+ var batchStartTime = data[0].timestamp;
210
+ if (this.seqNo === 0) {
211
+ this.replayStartTime = batchStartTime;
212
+ }
213
+ var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
214
+
135
215
  var reqParams = {
136
216
  'distinct_id': String(this._mixpanel.get_distinct_id()),
137
- 'seq': this.seqNo++,
138
- 'batch_start_time': this.batchStartTime / 1000,
217
+ 'seq': this.seqNo,
218
+ 'batch_start_time': batchStartTime / 1000,
139
219
  'replay_id': this.replayId,
140
- 'replay_length_ms': this.replayLengthMs,
220
+ 'replay_length_ms': replayLengthMs,
141
221
  'replay_start_time': this.replayStartTime / 1000
142
222
  };
143
- var eventsJson = _.JSONEncode(this.recEvents);
223
+ var eventsJson = _.JSONEncode(data);
144
224
 
145
225
  // send ID management props if they exist
146
226
  var deviceId = this._mixpanel.get_property('$device_id');
@@ -152,8 +232,6 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
152
232
  reqParams['$user_id'] = userId;
153
233
  }
154
234
 
155
- this.recEvents = this.recEvents.slice(numEvents);
156
- this.batchStartTime = new Date().getTime();
157
235
  if (CompressionStream) {
158
236
  var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
159
237
  var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
@@ -161,13 +239,27 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
161
239
  .blob()
162
240
  .then(_.bind(function(compressedBlob) {
163
241
  reqParams['format'] = 'gzip';
164
- this._sendRequest(reqParams, compressedBlob);
242
+ this._sendRequest(reqParams, compressedBlob, callback);
165
243
  }, this));
166
244
  } else {
167
245
  reqParams['format'] = 'body';
168
- this._sendRequest(reqParams, eventsJson);
246
+ this._sendRequest(reqParams, eventsJson, callback);
169
247
  }
170
248
  }
171
249
  });
172
250
 
251
+
252
+ MixpanelRecorder.prototype.reportError = function(msg, err) {
253
+ logger.error.apply(logger.error, arguments);
254
+ try {
255
+ if (!err && !(msg instanceof Error)) {
256
+ msg = new Error(msg);
257
+ }
258
+ this.get_config('error_reporter')(msg, err);
259
+ } catch(err) {
260
+ logger.error(err);
261
+ }
262
+ };
263
+
264
+
173
265
  window['__mp_recorder'] = MixpanelRecorder;
@@ -17,7 +17,8 @@ var RequestBatcher = function(storageKey, options) {
17
17
  this.errorReporter = options.errorReporter;
18
18
  this.queue = new RequestQueue(storageKey, {
19
19
  errorReporter: _.bind(this.reportError, this),
20
- storage: options.storage
20
+ storage: options.storage,
21
+ usePersistence: options.usePersistence
21
22
  });
22
23
 
23
24
  this.libConfig = options.libConfig;
@@ -34,6 +35,11 @@ var RequestBatcher = function(storageKey, options) {
34
35
 
35
36
  // extra client-side dedupe
36
37
  this.itemIdsSentSuccessfully = {};
38
+
39
+ // Make the flush occur at the interval specified by flushIntervalMs, default behavior will attempt consecutive flushes
40
+ // as long as the queue is not empty. This is useful for high-frequency events like Session Replay where we might end up
41
+ // in a request loop and get ratelimited by the server.
42
+ this.flushOnlyOnInterval = options.flushOnlyOnInterval || false;
37
43
  };
38
44
 
39
45
  /**
@@ -91,7 +97,11 @@ RequestBatcher.prototype.resetFlush = function() {
91
97
  RequestBatcher.prototype.scheduleFlush = function(flushMS) {
92
98
  this.flushInterval = flushMS;
93
99
  if (!this.stopped) { // don't schedule anymore if batching has been stopped
94
- this.timeoutID = setTimeout(_.bind(this.flush, this), this.flushInterval);
100
+ this.timeoutID = setTimeout(_.bind(function() {
101
+ if (!this.stopped) {
102
+ this.flush();
103
+ }
104
+ }, this), this.flushInterval);
95
105
  }
96
106
  };
97
107
 
@@ -118,6 +128,9 @@ RequestBatcher.prototype.flush = function(options) {
118
128
  var startTime = new Date().getTime();
119
129
  var currentBatchSize = this.batchSize;
120
130
  var batch = this.queue.fillBatch(currentBatchSize);
131
+ // if there's more items in the queue than the batch size, attempt
132
+ // to flush again after the current batch is done.
133
+ var attemptSecondaryFlush = batch.length === currentBatchSize;
121
134
  var dataForRequest = [];
122
135
  var transformedItems = {};
123
136
  _.each(batch, function(item) {
@@ -185,22 +198,17 @@ RequestBatcher.prototype.flush = function(options) {
185
198
  this.flush();
186
199
  } else if (
187
200
  _.isObject(res) &&
188
- res.xhr_req &&
189
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
201
+ (res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
190
202
  ) {
191
203
  // network or API error, or 429 Too Many Requests, retry
192
204
  var retryMS = this.flushInterval * 2;
193
- var headers = res.xhr_req['responseHeaders'];
194
- if (headers) {
195
- var retryAfter = headers['Retry-After'];
196
- if (retryAfter) {
197
- retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS;
198
- }
205
+ if (res.retryAfter) {
206
+ retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
199
207
  }
200
208
  retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
201
209
  this.reportError('Error; retry in ' + retryMS + ' ms');
202
210
  this.scheduleFlush(retryMS);
203
- } else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) {
211
+ } else if (_.isObject(res) && res.httpStatusCode === 413) {
204
212
  // 413 Payload Too Large
205
213
  if (batch.length > 1) {
206
214
  var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
@@ -224,7 +232,11 @@ RequestBatcher.prototype.flush = function(options) {
224
232
  _.bind(function(succeeded) {
225
233
  if (succeeded) {
226
234
  this.consecutiveRemovalFailures = 0;
227
- this.flush(); // handle next batch if the queue isn't empty
235
+ if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
236
+ this.resetFlush(); // schedule next batch with a delay
237
+ } else {
238
+ this.flush(); // handle next batch if the queue isn't empty
239
+ }
228
240
  } else {
229
241
  this.reportError('Failed to remove items from queue');
230
242
  if (++this.consecutiveRemovalFailures > 5) {
@@ -272,7 +284,6 @@ RequestBatcher.prototype.flush = function(options) {
272
284
  }
273
285
  logger.log('MIXPANEL REQUEST:', dataForRequest);
274
286
  this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
275
-
276
287
  } catch(err) {
277
288
  this.reportError('Error flushing request queue', err);
278
289
  this.resetFlush();