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/.vscode/launch.json +23 -0
- package/CHANGELOG.md +9 -0
- package/README.md +12 -0
- package/build.sh +2 -0
- package/dist/mixpanel-core.cjs.js +6369 -0
- package/dist/mixpanel-recorder.js +982 -109
- package/dist/mixpanel-recorder.min.js +9 -9
- package/dist/mixpanel-with-async-recorder.cjs.js +6371 -0
- package/dist/mixpanel.amd.js +5333 -519
- package/dist/mixpanel.cjs.js +5333 -519
- package/dist/mixpanel.globals.js +166 -114
- package/dist/mixpanel.min.js +108 -108
- package/dist/mixpanel.umd.js +5333 -519
- 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 +129 -37
- package/src/request-batcher.js +24 -13
- 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.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
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.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.
|
|
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
|
+
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.
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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.
|
|
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
|
-
|
|
130
|
-
|
|
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':
|
|
217
|
+
'seq': this.seqNo,
|
|
218
|
+
'batch_start_time': batchStartTime / 1000,
|
|
139
219
|
'replay_id': this.replayId,
|
|
140
|
-
'replay_length_ms':
|
|
220
|
+
'replay_length_ms': replayLengthMs,
|
|
141
221
|
'replay_start_time': this.replayStartTime / 1000
|
|
142
222
|
};
|
|
143
|
-
var eventsJson = _.JSONEncode(
|
|
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;
|
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
|
/**
|
|
@@ -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(
|
|
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.
|
|
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
|
-
|
|
194
|
-
|
|
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.
|
|
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.
|
|
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();
|