mixpanel-browser 2.56.0 → 2.57.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/CHANGELOG.md +6 -0
- package/LICENSE +1 -15
- package/build.sh +3 -3
- package/dist/mixpanel-core.cjs.js +898 -410
- package/dist/mixpanel-recorder.js +897 -409
- package/dist/mixpanel-recorder.min.js +10 -10
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +898 -410
- package/dist/mixpanel.amd.js +898 -410
- package/dist/mixpanel.cjs.js +898 -410
- package/dist/mixpanel.globals.js +898 -410
- package/dist/mixpanel.min.js +122 -112
- package/dist/mixpanel.module.js +898 -410
- package/dist/mixpanel.umd.js +898 -410
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/externs.js +14 -0
- package/src/gdpr-utils.js +2 -1
- package/src/mixpanel-core.js +3 -2
- package/src/promise-polyfill.js +379 -0
- package/src/recorder/index.js +2 -1
- package/src/recorder/session-recording.js +2 -1
- package/src/request-batcher.js +185 -164
- package/src/request-queue.js +200 -147
- package/src/shared-lock.js +104 -98
- package/src/storage/local-storage.js +53 -0
- package/src/storage/wrapper.js +14 -0
- package/src/utils.js +16 -33
- package/src/window.js +20 -0
- package/dist/mixpanel.min.js.map +0 -8
package/src/request-batcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Config from './config';
|
|
2
|
+
import { Promise } from './promise-polyfill';
|
|
2
3
|
import { RequestQueue } from './request-queue';
|
|
3
4
|
import { console_with_prefix, isOnline, _ } from './utils'; // eslint-disable-line camelcase
|
|
4
5
|
|
|
@@ -17,7 +18,8 @@ var RequestBatcher = function(storageKey, options) {
|
|
|
17
18
|
this.errorReporter = options.errorReporter;
|
|
18
19
|
this.queue = new RequestQueue(storageKey, {
|
|
19
20
|
errorReporter: _.bind(this.reportError, this),
|
|
20
|
-
|
|
21
|
+
queueStorage: options.queueStorage,
|
|
22
|
+
sharedLockStorage: options.sharedLockStorage,
|
|
21
23
|
usePersistence: options.usePersistence
|
|
22
24
|
});
|
|
23
25
|
|
|
@@ -45,8 +47,8 @@ var RequestBatcher = function(storageKey, options) {
|
|
|
45
47
|
/**
|
|
46
48
|
* Add one item to queue.
|
|
47
49
|
*/
|
|
48
|
-
RequestBatcher.prototype.enqueue = function(item
|
|
49
|
-
this.queue.enqueue(item, this.flushInterval
|
|
50
|
+
RequestBatcher.prototype.enqueue = function(item) {
|
|
51
|
+
return this.queue.enqueue(item, this.flushInterval);
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -56,7 +58,7 @@ RequestBatcher.prototype.enqueue = function(item, cb) {
|
|
|
56
58
|
RequestBatcher.prototype.start = function() {
|
|
57
59
|
this.stopped = false;
|
|
58
60
|
this.consecutiveRemovalFailures = 0;
|
|
59
|
-
this.flush();
|
|
61
|
+
return this.flush();
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
/**
|
|
@@ -74,7 +76,7 @@ RequestBatcher.prototype.stop = function() {
|
|
|
74
76
|
* Clear out queue.
|
|
75
77
|
*/
|
|
76
78
|
RequestBatcher.prototype.clear = function() {
|
|
77
|
-
this.queue.clear();
|
|
79
|
+
return this.queue.clear();
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -105,6 +107,17 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
|
|
|
105
107
|
}
|
|
106
108
|
};
|
|
107
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Send a request using the sendRequest callback, but promisified.
|
|
112
|
+
* TODO: sendRequest should be promisified in the first place.
|
|
113
|
+
*/
|
|
114
|
+
RequestBatcher.prototype.sendRequestPromise = function(data, options) {
|
|
115
|
+
return new Promise(_.bind(function(resolve) {
|
|
116
|
+
this.sendRequest(data, options, resolve);
|
|
117
|
+
}, this));
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
|
|
108
121
|
/**
|
|
109
122
|
* Flush one batch to network. Depending on success/failure modes, it will either
|
|
110
123
|
* remove the batch from the queue or leave it in for retry, and schedule the next
|
|
@@ -116,183 +129,191 @@ RequestBatcher.prototype.scheduleFlush = function(flushMS) {
|
|
|
116
129
|
* sendBeacon offers no callbacks or status indications)
|
|
117
130
|
*/
|
|
118
131
|
RequestBatcher.prototype.flush = function(options) {
|
|
119
|
-
|
|
132
|
+
if (this.requestInProgress) {
|
|
133
|
+
logger.log('Flush: Request already in progress');
|
|
134
|
+
return Promise.resolve();
|
|
135
|
+
}
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
logger.log('Flush: Request already in progress');
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
137
|
+
this.requestInProgress = true;
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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;
|
|
134
|
-
var dataForRequest = [];
|
|
135
|
-
var transformedItems = {};
|
|
136
|
-
_.each(batch, function(item) {
|
|
137
|
-
var payload = item['payload'];
|
|
138
|
-
if (this.beforeSendHook && !item.orphaned) {
|
|
139
|
-
payload = this.beforeSendHook(payload);
|
|
140
|
-
}
|
|
141
|
-
if (payload) {
|
|
142
|
-
// mp_sent_by_lib_version prop captures which lib version actually
|
|
143
|
-
// sends each event (regardless of which version originally queued
|
|
144
|
-
// it for sending)
|
|
145
|
-
if (payload['event'] && payload['properties']) {
|
|
146
|
-
payload['properties'] = _.extend(
|
|
147
|
-
{},
|
|
148
|
-
payload['properties'],
|
|
149
|
-
{'mp_sent_by_lib_version': Config.LIB_VERSION}
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
var addPayload = true;
|
|
153
|
-
var itemId = item['id'];
|
|
154
|
-
if (itemId) {
|
|
155
|
-
if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
|
|
156
|
-
this.reportError('[dupe] item ID sent too many times, not sending', {
|
|
157
|
-
item: item,
|
|
158
|
-
batchSize: batch.length,
|
|
159
|
-
timesSent: this.itemIdsSentSuccessfully[itemId]
|
|
160
|
-
});
|
|
161
|
-
addPayload = false;
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
this.reportError('[dupe] found item with no ID', {item: item});
|
|
165
|
-
}
|
|
139
|
+
options = options || {};
|
|
140
|
+
var timeoutMS = this.libConfig['batch_request_timeout_ms'];
|
|
141
|
+
var startTime = new Date().getTime();
|
|
142
|
+
var currentBatchSize = this.batchSize;
|
|
166
143
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
transformedItems[item['id']] = payload;
|
|
172
|
-
}, this);
|
|
173
|
-
if (dataForRequest.length < 1) {
|
|
174
|
-
this.resetFlush();
|
|
175
|
-
return; // nothing to do
|
|
176
|
-
}
|
|
144
|
+
return this.queue.fillBatch(currentBatchSize)
|
|
145
|
+
.then(_.bind(function(batch) {
|
|
177
146
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.flush();
|
|
199
|
-
} else if (
|
|
200
|
-
_.isObject(res) &&
|
|
201
|
-
(
|
|
202
|
-
res.httpStatusCode >= 500
|
|
203
|
-
|| res.httpStatusCode === 429
|
|
204
|
-
|| (res.httpStatusCode <= 0 && !isOnline())
|
|
205
|
-
|| res.error === 'timeout'
|
|
206
|
-
)
|
|
207
|
-
) {
|
|
208
|
-
// network or API error, or 429 Too Many Requests, retry
|
|
209
|
-
var retryMS = this.flushInterval * 2;
|
|
210
|
-
if (res.retryAfter) {
|
|
211
|
-
retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
|
|
147
|
+
// if there's more items in the queue than the batch size, attempt
|
|
148
|
+
// to flush again after the current batch is done.
|
|
149
|
+
var attemptSecondaryFlush = batch.length === currentBatchSize;
|
|
150
|
+
var dataForRequest = [];
|
|
151
|
+
var transformedItems = {};
|
|
152
|
+
_.each(batch, function(item) {
|
|
153
|
+
var payload = item['payload'];
|
|
154
|
+
if (this.beforeSendHook && !item.orphaned) {
|
|
155
|
+
payload = this.beforeSendHook(payload);
|
|
156
|
+
}
|
|
157
|
+
if (payload) {
|
|
158
|
+
// mp_sent_by_lib_version prop captures which lib version actually
|
|
159
|
+
// sends each event (regardless of which version originally queued
|
|
160
|
+
// it for sending)
|
|
161
|
+
if (payload['event'] && payload['properties']) {
|
|
162
|
+
payload['properties'] = _.extend(
|
|
163
|
+
{},
|
|
164
|
+
payload['properties'],
|
|
165
|
+
{'mp_sent_by_lib_version': Config.LIB_VERSION}
|
|
166
|
+
);
|
|
212
167
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
168
|
+
var addPayload = true;
|
|
169
|
+
var itemId = item['id'];
|
|
170
|
+
if (itemId) {
|
|
171
|
+
if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
|
|
172
|
+
this.reportError('[dupe] item ID sent too many times, not sending', {
|
|
173
|
+
item: item,
|
|
174
|
+
batchSize: batch.length,
|
|
175
|
+
timesSent: this.itemIdsSentSuccessfully[itemId]
|
|
176
|
+
});
|
|
177
|
+
addPayload = false;
|
|
178
|
+
}
|
|
223
179
|
} else {
|
|
224
|
-
this.reportError('
|
|
225
|
-
|
|
226
|
-
|
|
180
|
+
this.reportError('[dupe] found item with no ID', {item: item});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (addPayload) {
|
|
184
|
+
dataForRequest.push(payload);
|
|
227
185
|
}
|
|
228
|
-
} else {
|
|
229
|
-
// successful network request+response; remove each item in batch from queue
|
|
230
|
-
// (even if it was e.g. a 400, in which case retrying won't help)
|
|
231
|
-
removeItemsFromQueue = true;
|
|
232
186
|
}
|
|
187
|
+
transformedItems[item['id']] = payload;
|
|
188
|
+
}, this);
|
|
233
189
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
190
|
+
if (dataForRequest.length < 1) {
|
|
191
|
+
this.requestInProgress = false;
|
|
192
|
+
this.resetFlush();
|
|
193
|
+
return Promise.resolve(); // nothing to do
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
var removeItemsFromQueue = _.bind(function () {
|
|
197
|
+
return this.queue
|
|
198
|
+
.removeItemsByID(
|
|
199
|
+
_.map(batch, function (item) {
|
|
200
|
+
return item['id'];
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
.then(_.bind(function (succeeded) {
|
|
204
|
+
// client-side dedupe
|
|
205
|
+
_.each(batch, _.bind(function(item) {
|
|
206
|
+
var itemId = item['id'];
|
|
207
|
+
if (itemId) {
|
|
208
|
+
this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
|
|
209
|
+
this.itemIdsSentSuccessfully[itemId]++;
|
|
210
|
+
if (this.itemIdsSentSuccessfully[itemId] > 5) {
|
|
211
|
+
this.reportError('[dupe] item ID sent too many times', {
|
|
212
|
+
item: item,
|
|
213
|
+
batchSize: batch.length,
|
|
214
|
+
timesSent: this.itemIdsSentSuccessfully[itemId]
|
|
215
|
+
});
|
|
244
216
|
}
|
|
245
217
|
} else {
|
|
246
|
-
this.reportError('
|
|
247
|
-
if (++this.consecutiveRemovalFailures > 5) {
|
|
248
|
-
this.reportError('Too many queue failures; disabling batching system.');
|
|
249
|
-
this.stopAllBatching();
|
|
250
|
-
} else {
|
|
251
|
-
this.resetFlush();
|
|
252
|
-
}
|
|
218
|
+
this.reportError('[dupe] found item with no ID while removing', {item: item});
|
|
253
219
|
}
|
|
254
|
-
}, this)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (this.itemIdsSentSuccessfully[itemId] > 5) {
|
|
264
|
-
this.reportError('[dupe] item ID sent too many times', {
|
|
265
|
-
item: item,
|
|
266
|
-
batchSize: batch.length,
|
|
267
|
-
timesSent: this.itemIdsSentSuccessfully[itemId]
|
|
268
|
-
});
|
|
220
|
+
}, this));
|
|
221
|
+
|
|
222
|
+
if (succeeded) {
|
|
223
|
+
this.consecutiveRemovalFailures = 0;
|
|
224
|
+
if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
|
|
225
|
+
this.resetFlush(); // schedule next batch with a delay
|
|
226
|
+
return Promise.resolve();
|
|
227
|
+
} else {
|
|
228
|
+
return this.flush(); // handle next batch if the queue isn't empty
|
|
269
229
|
}
|
|
270
230
|
} else {
|
|
271
|
-
this.
|
|
231
|
+
if (++this.consecutiveRemovalFailures > 5) {
|
|
232
|
+
this.reportError('Too many queue failures; disabling batching system.');
|
|
233
|
+
this.stopAllBatching();
|
|
234
|
+
} else {
|
|
235
|
+
this.resetFlush();
|
|
236
|
+
}
|
|
237
|
+
return Promise.resolve();
|
|
272
238
|
}
|
|
273
239
|
}, this));
|
|
274
|
-
|
|
240
|
+
}, this);
|
|
275
241
|
|
|
276
|
-
|
|
277
|
-
this.
|
|
278
|
-
|
|
242
|
+
var batchSendCallback = _.bind(function(res) {
|
|
243
|
+
this.requestInProgress = false;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
|
|
247
|
+
// handle API response in a try-catch to make sure we can reset the
|
|
248
|
+
// flush operation if something goes wrong
|
|
249
|
+
|
|
250
|
+
if (options.unloading) {
|
|
251
|
+
// update persisted data to include hook transformations
|
|
252
|
+
return this.queue.updatePayloads(transformedItems);
|
|
253
|
+
} else if (
|
|
254
|
+
_.isObject(res) &&
|
|
255
|
+
res.error === 'timeout' &&
|
|
256
|
+
new Date().getTime() - startTime >= timeoutMS
|
|
257
|
+
) {
|
|
258
|
+
this.reportError('Network timeout; retrying');
|
|
259
|
+
return this.flush();
|
|
260
|
+
} else if (
|
|
261
|
+
_.isObject(res) &&
|
|
262
|
+
(
|
|
263
|
+
res.httpStatusCode >= 500
|
|
264
|
+
|| res.httpStatusCode === 429
|
|
265
|
+
|| (res.httpStatusCode <= 0 && !isOnline())
|
|
266
|
+
|| res.error === 'timeout'
|
|
267
|
+
)
|
|
268
|
+
) {
|
|
269
|
+
// network or API error, or 429 Too Many Requests, retry
|
|
270
|
+
var retryMS = this.flushInterval * 2;
|
|
271
|
+
if (res.retryAfter) {
|
|
272
|
+
retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
|
|
273
|
+
}
|
|
274
|
+
retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
|
|
275
|
+
this.reportError('Error; retry in ' + retryMS + ' ms');
|
|
276
|
+
this.scheduleFlush(retryMS);
|
|
277
|
+
return Promise.resolve();
|
|
278
|
+
} else if (_.isObject(res) && res.httpStatusCode === 413) {
|
|
279
|
+
// 413 Payload Too Large
|
|
280
|
+
if (batch.length > 1) {
|
|
281
|
+
var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
|
|
282
|
+
this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
|
|
283
|
+
this.reportError('413 response; reducing batch size to ' + this.batchSize);
|
|
284
|
+
this.resetFlush();
|
|
285
|
+
return Promise.resolve();
|
|
286
|
+
} else {
|
|
287
|
+
this.reportError('Single-event request too large; dropping', batch);
|
|
288
|
+
this.resetBatchSize();
|
|
289
|
+
return removeItemsFromQueue();
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// successful network request+response; remove each item in batch from queue
|
|
293
|
+
// (even if it was e.g. a 400, in which case retrying won't help)
|
|
294
|
+
return removeItemsFromQueue();
|
|
295
|
+
}
|
|
296
|
+
} catch(err) {
|
|
297
|
+
this.reportError('Error handling API response', err);
|
|
298
|
+
this.resetFlush();
|
|
299
|
+
}
|
|
300
|
+
}, this);
|
|
301
|
+
var requestOptions = {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
verbose: true,
|
|
304
|
+
ignore_json_errors: true, // eslint-disable-line camelcase
|
|
305
|
+
timeout_ms: timeoutMS // eslint-disable-line camelcase
|
|
306
|
+
};
|
|
307
|
+
if (options.unloading) {
|
|
308
|
+
requestOptions.transport = 'sendBeacon';
|
|
279
309
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
};
|
|
287
|
-
if (options.unloading) {
|
|
288
|
-
requestOptions.transport = 'sendBeacon';
|
|
289
|
-
}
|
|
290
|
-
logger.log('MIXPANEL REQUEST:', dataForRequest);
|
|
291
|
-
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
292
|
-
} catch(err) {
|
|
293
|
-
this.reportError('Error flushing request queue', err);
|
|
294
|
-
this.resetFlush();
|
|
295
|
-
}
|
|
310
|
+
logger.log('MIXPANEL REQUEST:', dataForRequest);
|
|
311
|
+
return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
|
|
312
|
+
}, this))
|
|
313
|
+
.catch(_.bind(function(err) {
|
|
314
|
+
this.reportError('Error flushing request queue', err);
|
|
315
|
+
this.resetFlush();
|
|
316
|
+
}, this));
|
|
296
317
|
};
|
|
297
318
|
|
|
298
319
|
/**
|