mixpanel-browser 2.56.0 → 2.58.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.
@@ -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
- storage: options.storage,
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, cb) {
49
- this.queue.enqueue(item, this.flushInterval, cb);
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
- try {
132
+ if (this.requestInProgress) {
133
+ logger.log('Flush: Request already in progress');
134
+ return Promise.resolve();
135
+ }
120
136
 
121
- if (this.requestInProgress) {
122
- logger.log('Flush: Request already in progress');
123
- return;
124
- }
137
+ this.requestInProgress = true;
125
138
 
126
- options = options || {};
127
- var timeoutMS = this.libConfig['batch_request_timeout_ms'];
128
- var startTime = new Date().getTime();
129
- var currentBatchSize = this.batchSize;
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
- if (addPayload) {
168
- dataForRequest.push(payload);
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
- this.requestInProgress = true;
179
-
180
- var batchSendCallback = _.bind(function(res) {
181
- this.requestInProgress = false;
182
-
183
- try {
184
-
185
- // handle API response in a try-catch to make sure we can reset the
186
- // flush operation if something goes wrong
187
-
188
- var removeItemsFromQueue = false;
189
- if (options.unloading) {
190
- // update persisted data to include hook transformations
191
- this.queue.updatePayloads(transformedItems);
192
- } else if (
193
- _.isObject(res) &&
194
- res.error === 'timeout' &&
195
- new Date().getTime() - startTime >= timeoutMS
196
- ) {
197
- this.reportError('Network timeout; retrying');
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
- retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
214
- this.reportError('Error; retry in ' + retryMS + ' ms');
215
- this.scheduleFlush(retryMS);
216
- } else if (_.isObject(res) && res.httpStatusCode === 413) {
217
- // 413 Payload Too Large
218
- if (batch.length > 1) {
219
- var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
220
- this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
221
- this.reportError('413 response; reducing batch size to ' + this.batchSize);
222
- this.resetFlush();
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('Single-event request too large; dropping', batch);
225
- this.resetBatchSize();
226
- removeItemsFromQueue = true;
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
- if (removeItemsFromQueue) {
235
- this.queue.removeItemsByID(
236
- _.map(batch, function(item) { return item['id']; }),
237
- _.bind(function(succeeded) {
238
- if (succeeded) {
239
- this.consecutiveRemovalFailures = 0;
240
- if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
241
- this.resetFlush(); // schedule next batch with a delay
242
- } else {
243
- this.flush(); // handle next batch if the queue isn't empty
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('Failed to remove items from queue');
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
- // client-side dedupe
258
- _.each(batch, _.bind(function(item) {
259
- var itemId = item['id'];
260
- if (itemId) {
261
- this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
262
- this.itemIdsSentSuccessfully[itemId]++;
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.reportError('[dupe] found item with no ID while removing', {item: item});
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
- } catch(err) {
277
- this.reportError('Error handling API response', err);
278
- this.resetFlush();
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
- }, this);
281
- var requestOptions = {
282
- method: 'POST',
283
- verbose: true,
284
- ignore_json_errors: true, // eslint-disable-line camelcase
285
- timeout_ms: timeoutMS // eslint-disable-line camelcase
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
  /**