mixpanel-browser 2.53.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 +6 -0
- package/README.md +12 -0
- package/build.sh +2 -0
- package/dist/mixpanel-core.cjs.js +6365 -0
- package/dist/mixpanel-recorder.js +958 -107
- 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/mixpanel-core.js +18 -11
- package/src/recorder/index.js +109 -35
- 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,11 +1,36 @@
|
|
|
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');
|
|
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.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
|
|
@@ -45,12 +80,11 @@ MixpanelRecorder.prototype.startRecording = function () {
|
|
|
45
80
|
|
|
46
81
|
this.recEvents = [];
|
|
47
82
|
this.seqNo = 0;
|
|
48
|
-
this.
|
|
49
|
-
this.replayStartTime = this.startDate.getTime();
|
|
50
|
-
this.batchStartTime = this.replayStartTime;
|
|
83
|
+
this.replayStartTime = null;
|
|
51
84
|
|
|
52
85
|
this.replayId = _.UUID();
|
|
53
|
-
|
|
86
|
+
|
|
87
|
+
this.batcher.start();
|
|
54
88
|
|
|
55
89
|
var resetIdleTimeout = _.bind(function () {
|
|
56
90
|
clearTimeout(this.idleTimeoutId);
|
|
@@ -62,20 +96,22 @@ MixpanelRecorder.prototype.startRecording = function () {
|
|
|
62
96
|
|
|
63
97
|
this._stopRecording = record({
|
|
64
98
|
'emit': _.bind(function (ev) {
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
99
|
+
this.batcher.enqueue(ev);
|
|
100
|
+
if (isUserEvent(ev)) {
|
|
101
|
+
resetIdleTimeout();
|
|
102
|
+
}
|
|
68
103
|
}, this),
|
|
69
|
-
'
|
|
70
|
-
'maskTextSelector': this.get_config('record_mask_text_selector'),
|
|
104
|
+
'blockClass': this.get_config('record_block_class'),
|
|
71
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,
|
|
72
109
|
'maskTextClass': this.get_config('record_mask_text_class'),
|
|
73
|
-
'
|
|
110
|
+
'maskTextSelector': this.get_config('record_mask_text_selector')
|
|
74
111
|
});
|
|
75
112
|
|
|
76
113
|
resetIdleTimeout();
|
|
77
114
|
|
|
78
|
-
this.sendBatchId = setInterval(_.bind(this.flushEventsWithOptOut, this), 10000);
|
|
79
115
|
this.maxTimeoutId = setTimeout(_.bind(this.resetRecording, this), this.recordMaxMs);
|
|
80
116
|
};
|
|
81
117
|
|
|
@@ -90,10 +126,9 @@ MixpanelRecorder.prototype.stopRecording = function () {
|
|
|
90
126
|
this._stopRecording = null;
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
this.
|
|
129
|
+
this.batcher.flush(); // flush any remaining events
|
|
94
130
|
this.replayId = null;
|
|
95
131
|
|
|
96
|
-
clearInterval(this.sendBatchId);
|
|
97
132
|
clearTimeout(this.idleTimeoutId);
|
|
98
133
|
clearTimeout(this.maxTimeoutId);
|
|
99
134
|
};
|
|
@@ -102,8 +137,8 @@ MixpanelRecorder.prototype.stopRecording = function () {
|
|
|
102
137
|
* Flushes the current batch of events to the server, but passes an opt-out callback to make sure
|
|
103
138
|
* we stop recording and dump any queued events if the user has opted out.
|
|
104
139
|
*/
|
|
105
|
-
MixpanelRecorder.prototype.flushEventsWithOptOut = function () {
|
|
106
|
-
this._flushEvents(_.bind(this._onOptOut, this));
|
|
140
|
+
MixpanelRecorder.prototype.flushEventsWithOptOut = function (data, options, cb) {
|
|
141
|
+
this._flushEvents(data, options, cb, _.bind(this._onOptOut, this));
|
|
107
142
|
};
|
|
108
143
|
|
|
109
144
|
MixpanelRecorder.prototype._onOptOut = function (code) {
|
|
@@ -114,33 +149,60 @@ MixpanelRecorder.prototype._onOptOut = function (code) {
|
|
|
114
149
|
}
|
|
115
150
|
};
|
|
116
151
|
|
|
117
|
-
MixpanelRecorder.prototype._sendRequest = function(reqParams, reqBody) {
|
|
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
|
+
|
|
118
168
|
window['fetch'](this.get_config('api_host') + '/' + this.get_config('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
|
|
119
169
|
'method': 'POST',
|
|
120
170
|
'headers': {
|
|
121
171
|
'Authorization': 'Basic ' + btoa(this.get_config('token') + ':'),
|
|
122
172
|
'Content-Type': 'application/octet-stream'
|
|
123
173
|
},
|
|
124
|
-
'body': reqBody
|
|
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});
|
|
125
183
|
});
|
|
126
184
|
};
|
|
127
185
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*/
|
|
132
|
-
MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
|
|
133
|
-
var numEvents = this.recEvents.length;
|
|
186
|
+
MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function (data, options, callback) {
|
|
187
|
+
const numEvents = data.length;
|
|
188
|
+
|
|
134
189
|
if (numEvents > 0) {
|
|
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
|
+
|
|
135
197
|
var reqParams = {
|
|
136
198
|
'distinct_id': String(this._mixpanel.get_distinct_id()),
|
|
137
|
-
'seq': this.seqNo
|
|
138
|
-
'batch_start_time':
|
|
199
|
+
'seq': this.seqNo,
|
|
200
|
+
'batch_start_time': batchStartTime / 1000,
|
|
139
201
|
'replay_id': this.replayId,
|
|
140
|
-
'replay_length_ms':
|
|
202
|
+
'replay_length_ms': replayLengthMs,
|
|
141
203
|
'replay_start_time': this.replayStartTime / 1000
|
|
142
204
|
};
|
|
143
|
-
var eventsJson = _.JSONEncode(
|
|
205
|
+
var eventsJson = _.JSONEncode(data);
|
|
144
206
|
|
|
145
207
|
// send ID management props if they exist
|
|
146
208
|
var deviceId = this._mixpanel.get_property('$device_id');
|
|
@@ -152,8 +214,6 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
|
|
|
152
214
|
reqParams['$user_id'] = userId;
|
|
153
215
|
}
|
|
154
216
|
|
|
155
|
-
this.recEvents = this.recEvents.slice(numEvents);
|
|
156
|
-
this.batchStartTime = new Date().getTime();
|
|
157
217
|
if (CompressionStream) {
|
|
158
218
|
var jsonStream = new Blob([eventsJson], {type: 'application/json'}).stream();
|
|
159
219
|
var gzipStream = jsonStream.pipeThrough(new CompressionStream('gzip'));
|
|
@@ -161,13 +221,27 @@ MixpanelRecorder.prototype._flushEvents = addOptOutCheckMixpanelLib(function() {
|
|
|
161
221
|
.blob()
|
|
162
222
|
.then(_.bind(function(compressedBlob) {
|
|
163
223
|
reqParams['format'] = 'gzip';
|
|
164
|
-
this._sendRequest(reqParams, compressedBlob);
|
|
224
|
+
this._sendRequest(reqParams, compressedBlob, callback);
|
|
165
225
|
}, this));
|
|
166
226
|
} else {
|
|
167
227
|
reqParams['format'] = 'body';
|
|
168
|
-
this._sendRequest(reqParams, eventsJson);
|
|
228
|
+
this._sendRequest(reqParams, eventsJson, callback);
|
|
169
229
|
}
|
|
170
230
|
}
|
|
171
231
|
});
|
|
172
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
|
+
|
|
173
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();
|