mixpanel-browser 2.41.0 → 2.45.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/.github/workflows/tests.yml +25 -0
- package/CHANGELOG.md +18 -0
- package/README.md +4 -2
- package/dist/mixpanel-jslib-snippet.min.js +3 -3
- package/dist/mixpanel-jslib-snippet.min.test.js +3 -3
- package/dist/mixpanel.amd.js +975 -2843
- package/dist/mixpanel.cjs.js +975 -2843
- package/dist/mixpanel.globals.js +975 -2843
- package/dist/mixpanel.min.js +100 -149
- package/dist/mixpanel.umd.js +975 -2843
- package/doc/build-docs.js +16 -0
- package/doc/readme.io/javascript-full-api-reference.md +18 -0
- package/mixpanel-jslib-snippet.js +2 -2
- package/package.json +3 -3
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +76 -129
- package/src/mixpanel-group.js +4 -0
- package/src/mixpanel-persistence.js +0 -21
- package/src/request-batcher.js +47 -10
- package/src/request-queue.js +55 -18
- package/src/utils.js +2 -71
- package/tunnel.log +0 -0
- package/.travis.yml +0 -6
- package/src/mixpanel-notification.js +0 -1309
- package/src/property-filters.js +0 -508
package/src/request-batcher.js
CHANGED
|
@@ -13,17 +13,23 @@ var logger = console_with_prefix('batch');
|
|
|
13
13
|
* @constructor
|
|
14
14
|
*/
|
|
15
15
|
var RequestBatcher = function(storageKey, options) {
|
|
16
|
-
this.
|
|
16
|
+
this.errorReporter = options.errorReporter;
|
|
17
|
+
this.queue = new RequestQueue(storageKey, {
|
|
18
|
+
errorReporter: _.bind(this.reportError, this),
|
|
19
|
+
storage: options.storage
|
|
20
|
+
});
|
|
17
21
|
|
|
18
22
|
this.libConfig = options.libConfig;
|
|
19
23
|
this.sendRequest = options.sendRequestFunc;
|
|
20
24
|
this.beforeSendHook = options.beforeSendHook;
|
|
25
|
+
this.stopAllBatching = options.stopAllBatchingFunc;
|
|
21
26
|
|
|
22
27
|
// seed variable batch size + flush interval with configured values
|
|
23
28
|
this.batchSize = this.libConfig['batch_size'];
|
|
24
29
|
this.flushInterval = this.libConfig['batch_flush_interval_ms'];
|
|
25
30
|
|
|
26
31
|
this.stopped = !this.libConfig['batch_autostart'];
|
|
32
|
+
this.consecutiveRemovalFailures = 0;
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
/**
|
|
@@ -39,6 +45,7 @@ RequestBatcher.prototype.enqueue = function(item, cb) {
|
|
|
39
45
|
*/
|
|
40
46
|
RequestBatcher.prototype.start = function() {
|
|
41
47
|
this.stopped = false;
|
|
48
|
+
this.consecutiveRemovalFailures = 0;
|
|
42
49
|
this.flush();
|
|
43
50
|
};
|
|
44
51
|
|
|
@@ -143,14 +150,14 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
143
150
|
res.error === 'timeout' &&
|
|
144
151
|
new Date().getTime() - startTime >= timeoutMS
|
|
145
152
|
) {
|
|
146
|
-
|
|
153
|
+
this.reportError('Network timeout; retrying');
|
|
147
154
|
this.flush();
|
|
148
155
|
} else if (
|
|
149
156
|
_.isObject(res) &&
|
|
150
157
|
res.xhr_req &&
|
|
151
|
-
(res.xhr_req['status'] >= 500 || res.xhr_req['status']
|
|
158
|
+
(res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
|
|
152
159
|
) {
|
|
153
|
-
// network or API error, retry
|
|
160
|
+
// network or API error, or 429 Too Many Requests, retry
|
|
154
161
|
var retryMS = this.flushInterval * 2;
|
|
155
162
|
var headers = res.xhr_req['responseHeaders'];
|
|
156
163
|
if (headers) {
|
|
@@ -160,17 +167,17 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
160
167
|
}
|
|
161
168
|
}
|
|
162
169
|
retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
|
|
163
|
-
|
|
170
|
+
this.reportError('Error; retry in ' + retryMS + ' ms');
|
|
164
171
|
this.scheduleFlush(retryMS);
|
|
165
172
|
} else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) {
|
|
166
173
|
// 413 Payload Too Large
|
|
167
174
|
if (batch.length > 1) {
|
|
168
175
|
var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
|
|
169
176
|
this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
|
|
170
|
-
|
|
177
|
+
this.reportError('413 response; reducing batch size to ' + this.batchSize);
|
|
171
178
|
this.resetFlush();
|
|
172
179
|
} else {
|
|
173
|
-
|
|
180
|
+
this.reportError('Single-event request too large; dropping', batch);
|
|
174
181
|
this.resetBatchSize();
|
|
175
182
|
removeItemsFromQueue = true;
|
|
176
183
|
}
|
|
@@ -183,12 +190,25 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
183
190
|
if (removeItemsFromQueue) {
|
|
184
191
|
this.queue.removeItemsByID(
|
|
185
192
|
_.map(batch, function(item) { return item['id']; }),
|
|
186
|
-
_.bind(
|
|
193
|
+
_.bind(function(succeeded) {
|
|
194
|
+
if (succeeded) {
|
|
195
|
+
this.consecutiveRemovalFailures = 0;
|
|
196
|
+
this.flush(); // handle next batch if the queue isn't empty
|
|
197
|
+
} else {
|
|
198
|
+
this.reportError('Failed to remove items from queue');
|
|
199
|
+
if (++this.consecutiveRemovalFailures > 5) {
|
|
200
|
+
this.reportError('Too many queue failures; disabling batching system.');
|
|
201
|
+
this.stopAllBatching();
|
|
202
|
+
} else {
|
|
203
|
+
this.resetFlush();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, this)
|
|
187
207
|
);
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
} catch(err) {
|
|
191
|
-
|
|
211
|
+
this.reportError('Error handling API response', err);
|
|
192
212
|
this.resetFlush();
|
|
193
213
|
}
|
|
194
214
|
}, this);
|
|
@@ -205,9 +225,26 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
205
225
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
206
226
|
|
|
207
227
|
} catch(err) {
|
|
208
|
-
|
|
228
|
+
this.reportError('Error flushing request queue', err);
|
|
209
229
|
this.resetFlush();
|
|
210
230
|
}
|
|
211
231
|
};
|
|
212
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Log error to global logger and optional user-defined logger.
|
|
235
|
+
*/
|
|
236
|
+
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
237
|
+
logger.error.apply(logger.error, arguments);
|
|
238
|
+
if (this.errorReporter) {
|
|
239
|
+
try {
|
|
240
|
+
if (!(err instanceof Error)) {
|
|
241
|
+
err = new Error(msg);
|
|
242
|
+
}
|
|
243
|
+
this.errorReporter(msg, err);
|
|
244
|
+
} catch(err) {
|
|
245
|
+
logger.error(err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
213
250
|
export { RequestBatcher };
|
package/src/request-queue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SharedLock } from './shared-lock';
|
|
2
|
-
import { cheap_guid, console_with_prefix, JSONParse, JSONStringify, _ } from './utils'; // eslint-disable-line camelcase
|
|
2
|
+
import { cheap_guid, console_with_prefix, localStorageSupported, JSONParse, JSONStringify, _ } from './utils'; // eslint-disable-line camelcase
|
|
3
3
|
|
|
4
4
|
var logger = console_with_prefix('batch');
|
|
5
5
|
|
|
@@ -23,6 +23,7 @@ var RequestQueue = function(storageKey, options) {
|
|
|
23
23
|
options = options || {};
|
|
24
24
|
this.storageKey = storageKey;
|
|
25
25
|
this.storage = options.storage || window.localStorage;
|
|
26
|
+
this.reportError = options.errorReporter || _.bind(logger.error, logger);
|
|
26
27
|
this.lock = new SharedLock(storageKey, {storage: this.storage});
|
|
27
28
|
|
|
28
29
|
this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
|
|
@@ -60,18 +61,18 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
|
|
|
60
61
|
this.memQueue.push(queueEntry);
|
|
61
62
|
}
|
|
62
63
|
} catch(err) {
|
|
63
|
-
|
|
64
|
+
this.reportError('Error enqueueing item', item);
|
|
64
65
|
succeeded = false;
|
|
65
66
|
}
|
|
66
67
|
if (cb) {
|
|
67
68
|
cb(succeeded);
|
|
68
69
|
}
|
|
69
|
-
}, this), function lockFailure(err) {
|
|
70
|
-
|
|
70
|
+
}, this), _.bind(function lockFailure(err) {
|
|
71
|
+
this.reportError('Error acquiring storage lock', err);
|
|
71
72
|
if (cb) {
|
|
72
73
|
cb(false);
|
|
73
74
|
}
|
|
74
|
-
}, this.pid);
|
|
75
|
+
}, this), this.pid);
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
/**
|
|
@@ -131,25 +132,61 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) {
|
|
|
131
132
|
_.each(ids, function(id) { idSet[id] = true; });
|
|
132
133
|
|
|
133
134
|
this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
|
|
134
|
-
|
|
135
|
+
|
|
136
|
+
var removeFromStorage = _.bind(function() {
|
|
135
137
|
var succeeded;
|
|
136
138
|
try {
|
|
137
139
|
var storedQueue = this.readFromStorage();
|
|
138
140
|
storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
|
|
139
141
|
succeeded = this.saveToStorage(storedQueue);
|
|
142
|
+
|
|
143
|
+
// an extra check: did storage report success but somehow
|
|
144
|
+
// the items are still there?
|
|
145
|
+
if (succeeded) {
|
|
146
|
+
storedQueue = this.readFromStorage();
|
|
147
|
+
for (var i = 0; i < storedQueue.length; i++) {
|
|
148
|
+
var item = storedQueue[i];
|
|
149
|
+
if (item['id'] && !!idSet[item['id']]) {
|
|
150
|
+
this.reportError('Item not removed from storage');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
140
155
|
} catch(err) {
|
|
141
|
-
|
|
156
|
+
this.reportError('Error removing items', ids);
|
|
142
157
|
succeeded = false;
|
|
143
158
|
}
|
|
159
|
+
return succeeded;
|
|
160
|
+
}, this);
|
|
161
|
+
|
|
162
|
+
this.lock.withLock(function lockAcquired() {
|
|
163
|
+
var succeeded = removeFromStorage();
|
|
144
164
|
if (cb) {
|
|
145
165
|
cb(succeeded);
|
|
146
166
|
}
|
|
147
|
-
},
|
|
148
|
-
|
|
167
|
+
}, _.bind(function lockFailure(err) {
|
|
168
|
+
var succeeded = false;
|
|
169
|
+
this.reportError('Error acquiring storage lock', err);
|
|
170
|
+
if (!localStorageSupported(this.storage, true)) {
|
|
171
|
+
// Looks like localStorage writes have stopped working sometime after
|
|
172
|
+
// initialization (probably full), and so nobody can acquire locks
|
|
173
|
+
// anymore. Consider it temporarily safe to remove items without the
|
|
174
|
+
// lock, since nobody's writing successfully anyway.
|
|
175
|
+
succeeded = removeFromStorage();
|
|
176
|
+
if (!succeeded) {
|
|
177
|
+
// OK, we couldn't even write out the smaller queue. Try clearing it
|
|
178
|
+
// entirely.
|
|
179
|
+
try {
|
|
180
|
+
this.storage.removeItem(this.storageKey);
|
|
181
|
+
} catch(err) {
|
|
182
|
+
this.reportError('Error clearing queue', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
149
186
|
if (cb) {
|
|
150
|
-
cb(
|
|
187
|
+
cb(succeeded);
|
|
151
188
|
}
|
|
152
|
-
}, this.pid);
|
|
189
|
+
}, this), this.pid);
|
|
153
190
|
};
|
|
154
191
|
|
|
155
192
|
// internal helper for RequestQueue.updatePayloads
|
|
@@ -184,18 +221,18 @@ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
|
|
|
184
221
|
storedQueue = updatePayloads(storedQueue, itemsToUpdate);
|
|
185
222
|
succeeded = this.saveToStorage(storedQueue);
|
|
186
223
|
} catch(err) {
|
|
187
|
-
|
|
224
|
+
this.reportError('Error updating items', itemsToUpdate);
|
|
188
225
|
succeeded = false;
|
|
189
226
|
}
|
|
190
227
|
if (cb) {
|
|
191
228
|
cb(succeeded);
|
|
192
229
|
}
|
|
193
|
-
}, this), function lockFailure(err) {
|
|
194
|
-
|
|
230
|
+
}, this), _.bind(function lockFailure(err) {
|
|
231
|
+
this.reportError('Error acquiring storage lock', err);
|
|
195
232
|
if (cb) {
|
|
196
233
|
cb(false);
|
|
197
234
|
}
|
|
198
|
-
}, this.pid);
|
|
235
|
+
}, this), this.pid);
|
|
199
236
|
};
|
|
200
237
|
|
|
201
238
|
/**
|
|
@@ -209,12 +246,12 @@ RequestQueue.prototype.readFromStorage = function() {
|
|
|
209
246
|
if (storageEntry) {
|
|
210
247
|
storageEntry = JSONParse(storageEntry);
|
|
211
248
|
if (!_.isArray(storageEntry)) {
|
|
212
|
-
|
|
249
|
+
this.reportError('Invalid storage entry:', storageEntry);
|
|
213
250
|
storageEntry = null;
|
|
214
251
|
}
|
|
215
252
|
}
|
|
216
253
|
} catch (err) {
|
|
217
|
-
|
|
254
|
+
this.reportError('Error retrieving queue', err);
|
|
218
255
|
storageEntry = null;
|
|
219
256
|
}
|
|
220
257
|
return storageEntry || [];
|
|
@@ -228,7 +265,7 @@ RequestQueue.prototype.saveToStorage = function(queue) {
|
|
|
228
265
|
this.storage.setItem(this.storageKey, JSONStringify(queue));
|
|
229
266
|
return true;
|
|
230
267
|
} catch (err) {
|
|
231
|
-
|
|
268
|
+
this.reportError('Error saving queue', err);
|
|
232
269
|
return false;
|
|
233
270
|
}
|
|
234
271
|
};
|
package/src/utils.js
CHANGED
|
@@ -150,14 +150,6 @@ _.bind = function(func, context) {
|
|
|
150
150
|
return bound;
|
|
151
151
|
};
|
|
152
152
|
|
|
153
|
-
_.bind_instance_methods = function(obj) {
|
|
154
|
-
for (var func in obj) {
|
|
155
|
-
if (typeof(obj[func]) === 'function') {
|
|
156
|
-
obj[func] = _.bind(obj[func], obj);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
153
|
/**
|
|
162
154
|
* @param {*=} obj
|
|
163
155
|
* @param {function(...*)=} iterator
|
|
@@ -186,19 +178,6 @@ _.each = function(obj, iterator, context) {
|
|
|
186
178
|
}
|
|
187
179
|
};
|
|
188
180
|
|
|
189
|
-
_.escapeHTML = function(s) {
|
|
190
|
-
var escaped = s;
|
|
191
|
-
if (escaped && _.isString(escaped)) {
|
|
192
|
-
escaped = escaped
|
|
193
|
-
.replace(/&/g, '&')
|
|
194
|
-
.replace(/</g, '<')
|
|
195
|
-
.replace(/>/g, '>')
|
|
196
|
-
.replace(/"/g, '"')
|
|
197
|
-
.replace(/'/g, ''');
|
|
198
|
-
}
|
|
199
|
-
return escaped;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
181
|
_.extend = function(obj) {
|
|
203
182
|
_.each(slice.call(arguments, 1), function(source) {
|
|
204
183
|
for (var prop in source) {
|
|
@@ -374,33 +353,6 @@ _.formatDate = function(d) {
|
|
|
374
353
|
pad(d.getUTCSeconds());
|
|
375
354
|
};
|
|
376
355
|
|
|
377
|
-
_.safewrap = function(f) {
|
|
378
|
-
return function() {
|
|
379
|
-
try {
|
|
380
|
-
return f.apply(this, arguments);
|
|
381
|
-
} catch (e) {
|
|
382
|
-
console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
|
|
383
|
-
if (Config.DEBUG){
|
|
384
|
-
console.critical(e);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
_.safewrap_class = function(klass, functions) {
|
|
391
|
-
for (var i = 0; i < functions.length; i++) {
|
|
392
|
-
klass.prototype[functions[i]] = _.safewrap(klass.prototype[functions[i]]);
|
|
393
|
-
}
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
_.safewrap_instance_methods = function(obj) {
|
|
397
|
-
for (var func in obj) {
|
|
398
|
-
if (typeof(obj[func]) === 'function') {
|
|
399
|
-
obj[func] = _.safewrap(obj[func]);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
|
|
404
356
|
_.strip_empty_properties = function(p) {
|
|
405
357
|
var ret = {};
|
|
406
358
|
_.each(p, function(v, k) {
|
|
@@ -942,10 +894,12 @@ _.UUID = (function() {
|
|
|
942
894
|
// This is to block various web spiders from executing our JS and
|
|
943
895
|
// sending false tracking data
|
|
944
896
|
var BLOCKED_UA_STRS = [
|
|
897
|
+
'ahrefsbot',
|
|
945
898
|
'baiduspider',
|
|
946
899
|
'bingbot',
|
|
947
900
|
'bingpreview',
|
|
948
901
|
'facebookexternal',
|
|
902
|
+
'petalbot',
|
|
949
903
|
'pinterest',
|
|
950
904
|
'screaming frog',
|
|
951
905
|
'yahoo! slurp',
|
|
@@ -1682,28 +1636,6 @@ var cheap_guid = function(maxlen) {
|
|
|
1682
1636
|
return maxlen ? guid.substring(0, maxlen) : guid;
|
|
1683
1637
|
};
|
|
1684
1638
|
|
|
1685
|
-
/**
|
|
1686
|
-
* Check deterministically whether to include or exclude from a feature rollout/test based on the
|
|
1687
|
-
* given string and the desired percentage to include.
|
|
1688
|
-
* @param {String} str - string to run the check against (for instance a project's token)
|
|
1689
|
-
* @param {String} feature - name of feature (for inclusion in hash, to ensure different results
|
|
1690
|
-
* for different features)
|
|
1691
|
-
* @param {Number} percent_allowed - percentage chance that a given string will be included
|
|
1692
|
-
* @returns {Boolean} whether the given string should be included
|
|
1693
|
-
*/
|
|
1694
|
-
var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
|
|
1695
|
-
str = str + feature;
|
|
1696
|
-
|
|
1697
|
-
// Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
|
|
1698
|
-
var hash = 5381;
|
|
1699
|
-
for (var i = 0; i < str.length; i++) {
|
|
1700
|
-
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
1701
|
-
hash = hash & hash;
|
|
1702
|
-
}
|
|
1703
|
-
var dart = (hash >>> 0) % 100;
|
|
1704
|
-
return dart < percent_allowed;
|
|
1705
|
-
});
|
|
1706
|
-
|
|
1707
1639
|
// naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
|
|
1708
1640
|
var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
|
|
1709
1641
|
// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
|
|
@@ -1762,7 +1694,6 @@ export {
|
|
|
1762
1694
|
navigator,
|
|
1763
1695
|
cheap_guid,
|
|
1764
1696
|
console_with_prefix,
|
|
1765
|
-
determine_eligibility,
|
|
1766
1697
|
extract_domain,
|
|
1767
1698
|
localStorageSupported,
|
|
1768
1699
|
JSONStringify,
|
package/tunnel.log
ADDED
|
File without changes
|