mixpanel-browser 2.43.0 → 2.46.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/CHANGELOG.md +11 -1
- package/README.md +1 -1
- package/build.sh +2 -0
- package/dist/mixpanel-jslib-snippet.min.js +3 -3
- package/dist/mixpanel-jslib-snippet.min.test.js +3 -3
- package/dist/mixpanel.amd.js +973 -2788
- package/dist/mixpanel.cjs.js +973 -2788
- package/dist/mixpanel.globals.js +973 -2788
- package/dist/mixpanel.min.js +101 -150
- package/dist/mixpanel.umd.js +973 -2788
- package/doc/readme.io/javascript-full-api-reference.md +3 -55
- package/doc/template.md +1 -1
- package/mixpanel-jslib-snippet.js +2 -2
- package/package.json +5 -5
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +45 -111
- package/src/mixpanel-people.js +29 -27
- package/src/mixpanel-persistence.js +0 -21
- package/src/request-batcher.js +95 -9
- package/src/request-queue.js +55 -18
- package/src/utils.js +0 -48
- package/src/mixpanel-notification.js +0 -1309
- package/src/property-filters.js +0 -508
package/src/request-batcher.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Config from './config';
|
|
1
2
|
import { RequestQueue } from './request-queue';
|
|
2
3
|
import { console_with_prefix, _ } from './utils'; // eslint-disable-line camelcase
|
|
3
4
|
|
|
@@ -13,17 +14,26 @@ var logger = console_with_prefix('batch');
|
|
|
13
14
|
* @constructor
|
|
14
15
|
*/
|
|
15
16
|
var RequestBatcher = function(storageKey, options) {
|
|
16
|
-
this.
|
|
17
|
+
this.errorReporter = options.errorReporter;
|
|
18
|
+
this.queue = new RequestQueue(storageKey, {
|
|
19
|
+
errorReporter: _.bind(this.reportError, this),
|
|
20
|
+
storage: options.storage
|
|
21
|
+
});
|
|
17
22
|
|
|
18
23
|
this.libConfig = options.libConfig;
|
|
19
24
|
this.sendRequest = options.sendRequestFunc;
|
|
20
25
|
this.beforeSendHook = options.beforeSendHook;
|
|
26
|
+
this.stopAllBatching = options.stopAllBatchingFunc;
|
|
21
27
|
|
|
22
28
|
// seed variable batch size + flush interval with configured values
|
|
23
29
|
this.batchSize = this.libConfig['batch_size'];
|
|
24
30
|
this.flushInterval = this.libConfig['batch_flush_interval_ms'];
|
|
25
31
|
|
|
26
32
|
this.stopped = !this.libConfig['batch_autostart'];
|
|
33
|
+
this.consecutiveRemovalFailures = 0;
|
|
34
|
+
|
|
35
|
+
// extra client-side dedupe
|
|
36
|
+
this.itemIdsSentSuccessfully = {};
|
|
27
37
|
};
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -39,6 +49,7 @@ RequestBatcher.prototype.enqueue = function(item, cb) {
|
|
|
39
49
|
*/
|
|
40
50
|
RequestBatcher.prototype.start = function() {
|
|
41
51
|
this.stopped = false;
|
|
52
|
+
this.consecutiveRemovalFailures = 0;
|
|
42
53
|
this.flush();
|
|
43
54
|
};
|
|
44
55
|
|
|
@@ -115,7 +126,34 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
115
126
|
payload = this.beforeSendHook(payload);
|
|
116
127
|
}
|
|
117
128
|
if (payload) {
|
|
118
|
-
|
|
129
|
+
// mp_sent_by_lib_version prop captures which lib version actually
|
|
130
|
+
// sends each event (regardless of which version originally queued
|
|
131
|
+
// it for sending)
|
|
132
|
+
if (payload['event'] && payload['properties']) {
|
|
133
|
+
payload['properties'] = _.extend(
|
|
134
|
+
{},
|
|
135
|
+
payload['properties'],
|
|
136
|
+
{'mp_sent_by_lib_version': Config.LIB_VERSION}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
var addPayload = true;
|
|
140
|
+
var itemId = item['id'];
|
|
141
|
+
if (itemId) {
|
|
142
|
+
if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
|
|
143
|
+
this.reportError('[dupe] item ID sent too many times, not sending', {
|
|
144
|
+
item: item,
|
|
145
|
+
batchSize: batch.length,
|
|
146
|
+
timesSent: this.itemIdsSentSuccessfully[itemId]
|
|
147
|
+
});
|
|
148
|
+
addPayload = false;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
this.reportError('[dupe] found item with no ID', {item: item});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (addPayload) {
|
|
155
|
+
dataForRequest.push(payload);
|
|
156
|
+
}
|
|
119
157
|
}
|
|
120
158
|
transformedItems[item['id']] = payload;
|
|
121
159
|
}, this);
|
|
@@ -143,7 +181,7 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
143
181
|
res.error === 'timeout' &&
|
|
144
182
|
new Date().getTime() - startTime >= timeoutMS
|
|
145
183
|
) {
|
|
146
|
-
|
|
184
|
+
this.reportError('Network timeout; retrying');
|
|
147
185
|
this.flush();
|
|
148
186
|
} else if (
|
|
149
187
|
_.isObject(res) &&
|
|
@@ -160,17 +198,17 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
160
198
|
}
|
|
161
199
|
}
|
|
162
200
|
retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
|
|
163
|
-
|
|
201
|
+
this.reportError('Error; retry in ' + retryMS + ' ms');
|
|
164
202
|
this.scheduleFlush(retryMS);
|
|
165
203
|
} else if (_.isObject(res) && res.xhr_req && res.xhr_req['status'] === 413) {
|
|
166
204
|
// 413 Payload Too Large
|
|
167
205
|
if (batch.length > 1) {
|
|
168
206
|
var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
|
|
169
207
|
this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
|
|
170
|
-
|
|
208
|
+
this.reportError('413 response; reducing batch size to ' + this.batchSize);
|
|
171
209
|
this.resetFlush();
|
|
172
210
|
} else {
|
|
173
|
-
|
|
211
|
+
this.reportError('Single-event request too large; dropping', batch);
|
|
174
212
|
this.resetBatchSize();
|
|
175
213
|
removeItemsFromQueue = true;
|
|
176
214
|
}
|
|
@@ -183,12 +221,43 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
183
221
|
if (removeItemsFromQueue) {
|
|
184
222
|
this.queue.removeItemsByID(
|
|
185
223
|
_.map(batch, function(item) { return item['id']; }),
|
|
186
|
-
_.bind(
|
|
224
|
+
_.bind(function(succeeded) {
|
|
225
|
+
if (succeeded) {
|
|
226
|
+
this.consecutiveRemovalFailures = 0;
|
|
227
|
+
this.flush(); // handle next batch if the queue isn't empty
|
|
228
|
+
} else {
|
|
229
|
+
this.reportError('Failed to remove items from queue');
|
|
230
|
+
if (++this.consecutiveRemovalFailures > 5) {
|
|
231
|
+
this.reportError('Too many queue failures; disabling batching system.');
|
|
232
|
+
this.stopAllBatching();
|
|
233
|
+
} else {
|
|
234
|
+
this.resetFlush();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, this)
|
|
187
238
|
);
|
|
239
|
+
|
|
240
|
+
// client-side dedupe
|
|
241
|
+
_.each(batch, _.bind(function(item) {
|
|
242
|
+
var itemId = item['id'];
|
|
243
|
+
if (itemId) {
|
|
244
|
+
this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
|
|
245
|
+
this.itemIdsSentSuccessfully[itemId]++;
|
|
246
|
+
if (this.itemIdsSentSuccessfully[itemId] > 5) {
|
|
247
|
+
this.reportError('[dupe] item ID sent too many times', {
|
|
248
|
+
item: item,
|
|
249
|
+
batchSize: batch.length,
|
|
250
|
+
timesSent: this.itemIdsSentSuccessfully[itemId]
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
this.reportError('[dupe] found item with no ID while removing', {item: item});
|
|
255
|
+
}
|
|
256
|
+
}, this));
|
|
188
257
|
}
|
|
189
258
|
|
|
190
259
|
} catch(err) {
|
|
191
|
-
|
|
260
|
+
this.reportError('Error handling API response', err);
|
|
192
261
|
this.resetFlush();
|
|
193
262
|
}
|
|
194
263
|
}, this);
|
|
@@ -205,9 +274,26 @@ RequestBatcher.prototype.flush = function(options) {
|
|
|
205
274
|
this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
|
|
206
275
|
|
|
207
276
|
} catch(err) {
|
|
208
|
-
|
|
277
|
+
this.reportError('Error flushing request queue', err);
|
|
209
278
|
this.resetFlush();
|
|
210
279
|
}
|
|
211
280
|
};
|
|
212
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Log error to global logger and optional user-defined logger.
|
|
284
|
+
*/
|
|
285
|
+
RequestBatcher.prototype.reportError = function(msg, err) {
|
|
286
|
+
logger.error.apply(logger.error, arguments);
|
|
287
|
+
if (this.errorReporter) {
|
|
288
|
+
try {
|
|
289
|
+
if (!(err instanceof Error)) {
|
|
290
|
+
err = new Error(msg);
|
|
291
|
+
}
|
|
292
|
+
this.errorReporter(msg, err);
|
|
293
|
+
} catch(err) {
|
|
294
|
+
logger.error(err);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
213
299
|
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) {
|