mixpanel-browser 2.52.0 → 2.54.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.
- package/.vscode/launch.json +23 -0
- package/CHANGELOG.md +22 -0
- package/README.md +12 -0
- package/build.sh +2 -0
- package/dist/mixpanel-core.cjs.js +6365 -0
- package/dist/mixpanel-js-wrapper.js +2 -0
- package/dist/mixpanel-js-wrapper.min.js +1 -1
- package/dist/mixpanel-recorder.js +980 -112
- package/dist/mixpanel-recorder.min.js +9 -9
- package/dist/mixpanel-with-async-recorder.cjs.js +6367 -0
- package/dist/mixpanel.amd.js +5310 -518
- package/dist/mixpanel.cjs.js +5310 -518
- package/dist/mixpanel.globals.js +161 -113
- package/dist/mixpanel.min.js +106 -106
- package/dist/mixpanel.umd.js +5310 -518
- package/package.json +1 -2
- package/rollup.config.js +3 -3
- package/src/config.js +1 -1
- package/src/loaders/bundle-loaders.js +22 -0
- package/src/loaders/loader-globals.js +2 -1
- package/src/loaders/loader-module-core.js +7 -0
- package/src/loaders/loader-module-with-async-recorder.js +7 -0
- package/src/loaders/loader-module.js +4 -1
- package/src/loaders/mixpanel-js-wrapper.js +2 -0
- package/src/mixpanel-core.js +18 -11
- package/src/recorder/index.js +133 -42
- package/src/request-batcher.js +19 -12
- package/src/request-queue.js +112 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixpanel-browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.54.0",
|
|
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
package/src/config.js
CHANGED
|
@@ -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,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;
|
package/src/mixpanel-core.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
|
package/src/recorder/index.js
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
import {
|
|
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');
|
|
9
|
+
var CompressionStream = window['CompressionStream'];
|
|
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.source);
|
|
32
|
+
}
|
|
7
33
|
|
|
8
34
|
var MixpanelRecorder = function(mixpanelInstance) {
|
|
9
35
|
this._mixpanel = mixpanelInstance;
|
|
@@ -15,14 +41,24 @@ var MixpanelRecorder = function(mixpanelInstance) {
|
|
|
15
41
|
this.seqNo = 0;
|
|
16
42
|
this.replayId = null;
|
|
17
43
|
this.replayStartTime = null;
|
|
18
|
-
this.batchStartTime = null;
|
|
19
|
-
this.replayLengthMs = 0;
|
|
20
44
|
this.sendBatchId = null;
|
|
21
45
|
|
|
22
46
|
this.idleTimeoutId = null;
|
|
23
47
|
this.maxTimeoutId = null;
|
|
24
48
|
|
|
25
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
|
+
});
|
|
26
62
|
};
|
|
27
63
|
|
|
28
64
|
// eslint-disable-next-line camelcase
|
|
@@ -44,12 +80,11 @@ MixpanelRecorder.prototype.startRecording = function () {
|
|
|
44
80
|
|
|
45
81
|
this.recEvents = [];
|
|
46
82
|
this.seqNo = 0;
|
|
47
|
-
this.
|
|
48
|
-
this.replayStartTime = this.startDate.getTime();
|
|
49
|
-
this.batchStartTime = this.replayStartTime;
|
|
83
|
+
this.replayStartTime = null;
|
|
50
84
|
|
|
51
85
|
this.replayId = _.UUID();
|
|
52
|
-
|
|
86
|
+
|
|
87
|
+
this.batcher.start();
|
|
53
88
|
|
|
54
89
|
var resetIdleTimeout = _.bind(function () {
|
|
55
90
|
clearTimeout(this.idleTimeoutId);
|
|
@@ -61,20 +96,22 @@ MixpanelRecorder.prototype.startRecording = function () {
|
|
|
61
96
|
|
|
62
97
|
this._stopRecording = record({
|
|
63
98
|
'emit': _.bind(function (ev) {
|
|
64
|
-
this.
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
this.batcher.enqueue(ev);
|
|
100
|
+
if (isUserEvent(ev)) {
|
|
101
|
+
resetIdleTimeout();
|
|
102
|
+
}
|
|
67
103
|
}, this),
|
|
68
|
-
'
|
|
69
|
-
'maskTextSelector': this.get_config('record_mask_text_selector'),
|
|
104
|
+
'blockClass': this.get_config('record_block_class'),
|
|
70
105
|
'blockSelector': this.get_config('record_block_selector'),
|
|
106
|
+
'collectFonts': this.get_config('record_collect_fonts'),
|
|
107
|
+
'inlineImages': this.get_config('record_inline_images'),
|
|
108
|
+
'maskAllInputs': true,
|
|
71
109
|
'maskTextClass': this.get_config('record_mask_text_class'),
|
|
72
|
-
'
|
|
110
|
+
'maskTextSelector': this.get_config('record_mask_text_selector')
|
|
73
111
|
});
|
|
74
112
|
|
|
75
113
|
resetIdleTimeout();
|
|
76
114
|
|
|
77
|
-
this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000);
|
|
78
115
|
this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
|
|
79
116
|
};
|
|
80
117
|
|
|
@@ -89,10 +126,9 @@ MixpanelRecorder.prototype.stopRecording = function () {
|
|
|
89
126
|
this._stopRecording = null;
|
|
90
127
|
}
|
|
91
128
|
|
|
92
|
-
this.
|
|
129
|
+
this.batcher.flush(); // flush any remaining events
|
|
93
130
|
this.replayId = null;
|
|
94
131
|
|
|
95
|
-
clearInterval(this.sendBatchId);
|
|
96
132
|
clearTimeout(this.idleTimeoutId);
|
|
97
133
|
clearTimeout(this.maxTimeoutId);
|
|
98
134
|
};
|
|
@@ -101,8 +137,8 @@ MixpanelRecorder.prototype.stopRecording = function () {
|
|
|
101
137
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
102
138
|
* we stop recording and dump any queued events if the user has opted out.
|
|
103
139
|
*/
|
|
104
|
-
MixpanelRecorder.prototype.flushEventsWithOptOut = function () {
|
|
105
|
-
this._flushEvents(_.bind(this._onOptOut, this));
|
|
140
|
+
MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
141
|
+
this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
|
|
106
142
|
};
|
|
107
143
|
|
|
108
144
|
MixpanelRecorder.prototype._onOptOut = function (code) {
|
|
@@ -113,44 +149,99 @@ MixpanelRecorder.prototype._onOptOut = function (code) {
|
|
|
113
149
|
}
|
|
114
150
|
};
|
|
115
151
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody, callback) {
|
|
153
|
+
var onSuccess = _.bind(function (response, responseBody) {
|
|
154
|
+
// Increment sequence counter only if the request was successful to guarantee ordering.
|
|
155
|
+
// RequestBatcher will always flush the next batch after the previous one succeeds.
|
|
156
|
+
if (response.status === 200) {
|
|
157
|
+
this.seqNo++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
callback({
|
|
161
|
+
status: 0,
|
|
162
|
+
httpStatusCode: response.status,
|
|
163
|
+
responseBody: responseBody,
|
|
164
|
+
retryAfter: response.headers.get('Retry-After')
|
|
165
|
+
});
|
|
166
|
+
}, this);
|
|
167
|
+
|
|
168
|
+
window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
169
|
+
'method': 'POST',
|
|
170
|
+
'headers': {
|
|
171
|
+
'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
|
|
172
|
+
'Content-Type': 'application/octet-stream'
|
|
173
|
+
},
|
|
174
|
+
'body': reqBody,
|
|
175
|
+
}).then(function (response) {
|
|
176
|
+
response.json().then(function (responseBody) {
|
|
177
|
+
onSuccess(response, responseBody);
|
|
178
|
+
}).catch(function (error) {
|
|
179
|
+
callback({error: error});
|
|
180
|
+
});
|
|
181
|
+
}).catch(function (error) {
|
|
182
|
+
callback({error: error});
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
187
|
+
const numEvents = data.length;
|
|
188
|
+
|
|
122
189
|
if (numEvents > 0) {
|
|
123
|
-
|
|
190
|
+
// each rrweb event has a timestamp - leverage those to get time properties
|
|
191
|
+
var batchStartTime = data[0].timestamp;
|
|
192
|
+
if (this.seqNo === 0) {
|
|
193
|
+
this.replayStartTime = batchStartTime;
|
|
194
|
+
}
|
|
195
|
+
var replayLengthMs = data[numEvents - 1].timestamp - this.replayStartTime;
|
|
196
|
+
|
|
197
|
+
var reqParams = {
|
|
124
198
|
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'batch_start_time': this.batchStartTime / 1000,
|
|
199
|
+
'seq': this.seqNo,
|
|
200
|
+
'batch_start_time': batchStartTime / 1000,
|
|
128
201
|
'replay_id': this.replayId,
|
|
129
|
-
'replay_length_ms':
|
|
202
|
+
'replay_length_ms': replayLengthMs,
|
|
130
203
|
'replay_start_time': this.replayStartTime / 1000
|
|
131
204
|
};
|
|
205
|
+
var eventsJson = _.JSONEncode(data);
|
|
132
206
|
|
|
133
207
|
// send ID management props if they exist
|
|
134
208
|
var deviceId = this._mixpanel.get_property('$device_id');
|
|
135
209
|
if (deviceId) {
|
|
136
|
-
|
|
210
|
+
reqParams['$device_id'] = deviceId;
|
|
137
211
|
}
|
|
138
212
|
var userId = this._mixpanel.get_property('$user_id');
|
|
139
213
|
if (userId) {
|
|
140
|
-
|
|
214
|
+
reqParams['$user_id'] = userId;
|
|
141
215
|
}
|
|
142
216
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
'
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
217
|
+
if (CompressionStream) {
|
|
218
|
+
var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
|
|
219
|
+
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
220
|
+
new Response(gzipStream)
|
|
221
|
+
.blob()
|
|
222
|
+
.then(_.bind(function(compressedBlob) {
|
|
223
|
+
reqParams['format'] = 'gzip';
|
|
224
|
+
this._sendRequest(reqParams, compressedBlob, callback);
|
|
225
|
+
}, this));
|
|
226
|
+
} else {
|
|
227
|
+
reqParams['format'] = 'body';
|
|
228
|
+
this._sendRequest(reqParams, eventsJson, callback);
|
|
229
|
+
}
|
|
153
230
|
}
|
|
154
231
|
});
|
|
155
232
|
|
|
233
|
+
|
|
234
|
+
MixpanelRecorder.prototype.reportError = function(msg, err) {
|
|
235
|
+
logger.error.apply(logger.error, arguments);
|
|
236
|
+
try {
|
|
237
|
+
if (!err && !(msg instanceof Error)) {
|
|
238
|
+
msg = new Error(msg);
|
|
239
|
+
}
|
|
240
|
+
this.get_config('error_reporter')(msg, err);
|
|
241
|
+
} catch(err) {
|
|
242
|
+
logger.error(err);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
|
|
156
247
|
window['__mp_recorder'] = MixpanelRecorder;
|
package/src/request-batcher.js
CHANGED
|
@@ -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
|
/**
|
|
@@ -118,6 +124,9 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
118
124
|
var startTime = new Date().getTime();
|
|
119
125
|
var currentBatchSize = this.batchSize;
|
|
120
126
|
var batch = this.queue.fillBatch(currentBatchSize);
|
|
127
|
+
// if there's more items in the queue than the batch size, attempt
|
|
128
|
+
// to flush again after the current batch is done.
|
|
129
|
+
var attemptSecondaryFlush = batch.length === currentBatchSize;
|
|
121
130
|
var dataForRequest = [];
|
|
122
131
|
var transformedItems = {};
|
|
123
132
|
_.each(batch, function(item) {
|
|
@@ -185,22 +194,17 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
185
194
|
this.flush();
|
|
186
195
|
} else if (
|
|
187
196
|
_.isObject(res) &&
|
|
188
|
-
res.
|
|
189
|
-
(res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
|
|
197
|
+
(res.httpStatusCode >= 500 || res.httpStatusCode === 429 || res.error === 'timeout')
|
|
190
198
|
) {
|
|
191
199
|
// network or API error, or 429 Too Many Requests, retry
|
|
192
200
|
var retryMS = this.flushInterval * 2;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
var retryAfter = headers['Retry-After'];
|
|
196
|
-
if (retryAfter) {
|
|
197
|
-
retryMS = (parseInt(retryAfter, 10) * 1000) || retryMS;
|
|
198
|
-
}
|
|
201
|
+
if (res.retryAfter) {
|
|
202
|
+
retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
|
|
199
203
|
}
|
|
200
204
|
retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
|
|
201
205
|
this.reportError('Error; retry in ' + retryMS + ' ms');
|
|
202
206
|
this.scheduleFlush(retryMS);
|
|
203
|
-
} else if (_.isObject(res) && res.
|
|
207
|
+
} else if (_.isObject(res) && res.httpStatusCode === 413) {
|
|
204
208
|
// 413 Payload Too Large
|
|
205
209
|
if (batch.length > 1) {
|
|
206
210
|
var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
|
|
@@ -224,7 +228,11 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
224
228
|
_.bind(function(succeeded) {
|
|
225
229
|
if (succeeded) {
|
|
226
230
|
this.consecutiveRemovalFailures = 0;
|
|
227
|
-
this.
|
|
231
|
+
if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
|
|
232
|
+
this.resetFlush(); // schedule next batch with a delay
|
|
233
|
+
} else {
|
|
234
|
+
this.flush(); // handle next batch if the queue isn't empty
|
|
235
|
+
}
|
|
228
236
|
} else {
|
|
229
237
|
this.reportError('Failed to remove items from queue');
|
|
230
238
|
if (++this.consecutiveRemovalFailures > 5) {
|
|
@@ -272,7 +280,6 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
272
280
|
}
|
|
273
281
|
logger.log('MIXPANEL REQUEST:', dataForRequest);
|
|
274
282
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
275
|
-
|
|
276
283
|
} catch(err) {
|
|
277
284
|
this.reportError('Error flushing request queue', err);
|
|
278
285
|
this.resetFlush();
|