Skip to content

Commit 7a2e787

Browse files
committed
Merge pull request #13 from amplitude/event-limits
Set some limits on queuing and sending
2 parents b1c4d08 + 0aa4b5e commit 7a2e787

File tree

6 files changed

+286
-38
lines changed

6 files changed

+286
-38
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ You can configure Amplitude by passing an object as the third argument to the `i
9393
| option | description | default |
9494
|------------|----------------------------------------------------------------------------------|-----------|
9595
| saveEvents | If `true`, saves events to localStorage and removes them upon successful upload.<br><i>NOTE:</i> Without saving events, events may be lost if the user navigates to another page before events are uploaded. | `true` |
96+
| savedMaxCount | Maximum number of events to save in localStorage. If more events are logged while offline, old events are removed. | 1000 |
97+
| uploadBatchSize | Maximum number of events to send to the server per request. | 100 |
9698
| includeUtm | If `true`, finds utm parameters in the query string or the __utmz cookie, parses, and includes them as user propeties on all events uploaded. | `false` |
9799

98100

amplitude.js

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,18 @@ var log = function(s) {
128128
var API_VERSION = 2;
129129
var DEFAULT_OPTIONS = {
130130
apiEndpoint: 'api.amplitude.com',
131-
cookieName: 'amplitude_id',
132131
cookieExpiration: 365 * 10,
133-
unsentKey: 'amplitude_unsent',
134-
saveEvents: true,
132+
cookieName: 'amplitude_id',
135133
domain: undefined,
136-
sessionTimeout: 30 * 60 * 1000,
137-
platform: 'Web',
138-
language: language.language,
139134
includeUtm: false,
140-
optOut: false
135+
language: language.language,
136+
optOut: false,
137+
platform: 'Web',
138+
savedMaxCount: 1000,
139+
saveEvents: true,
140+
sessionTimeout: 30 * 60 * 1000,
141+
unsentKey: 'amplitude_unsent',
142+
uploadBatchSize: 100,
141143
};
142144
var LocalStorageKeys = {
143145
LAST_EVENT_ID: 'amplitude_lastEventId',
@@ -185,6 +187,8 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
185187
this.options.platform = opt_config.platform || this.options.platform;
186188
this.options.language = opt_config.language || this.options.language;
187189
this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout;
190+
this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize;
191+
this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount;
188192
}
189193

190194
Cookie.options({
@@ -433,19 +437,31 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
433437
}
434438
// country: null
435439
};
440+
441+
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
442+
436443
this._unsentEvents.push(event);
444+
445+
// Remove old events from the beginning of the array if too many
446+
// have accumulated. Don't want to kill memory. Default is 1000 events.
447+
if (this._unsentEvents.length > this.options.savedMaxCount) {
448+
this._unsentEvents.splice(0, this._unsentEvents.length - this.options.savedMaxCount);
449+
}
450+
437451
if (this.options.saveEvents) {
438452
this.saveEvents();
439453
}
440-
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
454+
441455
this.sendEvents();
456+
457+
return eventId;
442458
} catch (e) {
443459
log(e);
444460
}
445461
};
446462

447463
Amplitude.prototype.logEvent = function(eventType, eventProperties) {
448-
this._logEvent(eventType, eventProperties);
464+
return this._logEvent(eventType, eventProperties);
449465
};
450466

451467
// Test that n is a number or a numeric value.
@@ -460,20 +476,39 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) {
460476
return;
461477
}
462478

463-
this._logEvent('revenue_amount', {}, {
479+
return this._logEvent('revenue_amount', {}, {
464480
productId: product,
465481
special: 'revenue_amount',
466482
quantity: quantity || 1,
467483
price: price
468484
});
469485
};
470486

487+
/**
488+
* Remove events in storage with event ids up to and including maxEventId. Does
489+
* a true filter in case events get out of order or old events are removed.
490+
*/
491+
Amplitude.prototype.removeEvents = function (maxEventId) {
492+
var filteredEvents = [];
493+
for (var i = 0; i < this._unsentEvents.length; i++) {
494+
if (this._unsentEvents[i].event_id > maxEventId) {
495+
filteredEvents.push(this._unsentEvents[i]);
496+
}
497+
}
498+
this._unsentEvents = filteredEvents;
499+
};
500+
471501
Amplitude.prototype.sendEvents = function() {
472502
if (!this._sending && !this.options.optOut) {
473503
this._sending = true;
474504
var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' +
475505
this.options.apiEndpoint + '/';
476-
var events = JSON.stringify(this._unsentEvents);
506+
507+
// Determine how many events to send and track the maximum event id sent in this batch.
508+
var numEvents = Math.min(this._unsentEvents.length, this.options.uploadBatchSize);
509+
var maxEventId = this._unsentEvents[numEvents - 1].event_id;
510+
511+
var events = JSON.stringify(this._unsentEvents.slice(0, numEvents));
477512
var uploadTime = new Date().getTime();
478513
var data = {
479514
client: this.options.apiKey,
@@ -482,20 +517,35 @@ Amplitude.prototype.sendEvents = function() {
482517
upload_time: uploadTime,
483518
checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime)
484519
};
485-
var numEvents = this._unsentEvents.length;
520+
486521
var scope = this;
487-
new Request(url, data).send(function(response) {
522+
new Request(url, data).send(function(status, response) {
488523
scope._sending = false;
489524
try {
490-
if (response === 'success') {
525+
if (status === 200 && response === 'success') {
491526
//log('sucessful upload');
492-
scope._unsentEvents.splice(0, numEvents);
527+
scope.removeEvents(maxEventId);
528+
529+
// Update the event cache after the removal of sent events.
493530
if (scope.options.saveEvents) {
494531
scope.saveEvents();
495532
}
533+
534+
// Send more events if any queued during previous send.
496535
if (scope._unsentEvents.length > 0) {
497536
scope.sendEvents();
498537
}
538+
} else if (status === 413) {
539+
//log('request too large');
540+
// Can't even get this one massive event through. Drop it.
541+
if (scope.options.uploadBatchSize === 1) {
542+
scope.removeEvents(maxEventId);
543+
}
544+
545+
// The server complained about the length of the request.
546+
// Backoff and try again.
547+
scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
548+
scope.sendEvents();
499549
}
500550
} catch (e) {
501551
//log('failed upload');
@@ -1462,9 +1512,7 @@ Request.prototype.send = function(callback) {
14621512
xhr.open('POST', this.url, true);
14631513
xhr.onreadystatechange = function() {
14641514
if (xhr.readyState === 4) {
1465-
if (xhr.status === 200) {
1466-
callback(xhr.responseText);
1467-
}
1515+
callback(xhr.status, xhr.responseText);
14681516
}
14691517
};
14701518
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');

amplitude.min.js

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/amplitude.js

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ var log = function(s) {
1616
var API_VERSION = 2;
1717
var DEFAULT_OPTIONS = {
1818
apiEndpoint: 'api.amplitude.com',
19-
cookieName: 'amplitude_id',
2019
cookieExpiration: 365 * 10,
21-
unsentKey: 'amplitude_unsent',
22-
saveEvents: true,
20+
cookieName: 'amplitude_id',
2321
domain: undefined,
24-
sessionTimeout: 30 * 60 * 1000,
25-
platform: 'Web',
26-
language: language.language,
2722
includeUtm: false,
28-
optOut: false
23+
language: language.language,
24+
optOut: false,
25+
platform: 'Web',
26+
savedMaxCount: 1000,
27+
saveEvents: true,
28+
sessionTimeout: 30 * 60 * 1000,
29+
unsentKey: 'amplitude_unsent',
30+
uploadBatchSize: 100,
2931
};
3032
var LocalStorageKeys = {
3133
LAST_EVENT_ID: 'amplitude_lastEventId',
@@ -73,6 +75,8 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
7375
this.options.platform = opt_config.platform || this.options.platform;
7476
this.options.language = opt_config.language || this.options.language;
7577
this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout;
78+
this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize;
79+
this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount;
7680
}
7781

7882
Cookie.options({
@@ -321,19 +325,31 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
321325
}
322326
// country: null
323327
};
328+
329+
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
330+
324331
this._unsentEvents.push(event);
332+
333+
// Remove old events from the beginning of the array if too many
334+
// have accumulated. Don't want to kill memory. Default is 1000 events.
335+
if (this._unsentEvents.length > this.options.savedMaxCount) {
336+
this._unsentEvents.splice(0, this._unsentEvents.length - this.options.savedMaxCount);
337+
}
338+
325339
if (this.options.saveEvents) {
326340
this.saveEvents();
327341
}
328-
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
342+
329343
this.sendEvents();
344+
345+
return eventId;
330346
} catch (e) {
331347
log(e);
332348
}
333349
};
334350

335351
Amplitude.prototype.logEvent = function(eventType, eventProperties) {
336-
this._logEvent(eventType, eventProperties);
352+
return this._logEvent(eventType, eventProperties);
337353
};
338354

339355
// Test that n is a number or a numeric value.
@@ -348,20 +364,39 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) {
348364
return;
349365
}
350366

351-
this._logEvent('revenue_amount', {}, {
367+
return this._logEvent('revenue_amount', {}, {
352368
productId: product,
353369
special: 'revenue_amount',
354370
quantity: quantity || 1,
355371
price: price
356372
});
357373
};
358374

375+
/**
376+
* Remove events in storage with event ids up to and including maxEventId. Does
377+
* a true filter in case events get out of order or old events are removed.
378+
*/
379+
Amplitude.prototype.removeEvents = function (maxEventId) {
380+
var filteredEvents = [];
381+
for (var i = 0; i < this._unsentEvents.length; i++) {
382+
if (this._unsentEvents[i].event_id > maxEventId) {
383+
filteredEvents.push(this._unsentEvents[i]);
384+
}
385+
}
386+
this._unsentEvents = filteredEvents;
387+
};
388+
359389
Amplitude.prototype.sendEvents = function() {
360390
if (!this._sending && !this.options.optOut) {
361391
this._sending = true;
362392
var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' +
363393
this.options.apiEndpoint + '/';
364-
var events = JSON.stringify(this._unsentEvents);
394+
395+
// Determine how many events to send and track the maximum event id sent in this batch.
396+
var numEvents = Math.min(this._unsentEvents.length, this.options.uploadBatchSize);
397+
var maxEventId = this._unsentEvents[numEvents - 1].event_id;
398+
399+
var events = JSON.stringify(this._unsentEvents.slice(0, numEvents));
365400
var uploadTime = new Date().getTime();
366401
var data = {
367402
client: this.options.apiKey,
@@ -370,20 +405,35 @@ Amplitude.prototype.sendEvents = function() {
370405
upload_time: uploadTime,
371406
checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime)
372407
};
373-
var numEvents = this._unsentEvents.length;
408+
374409
var scope = this;
375-
new Request(url, data).send(function(response) {
410+
new Request(url, data).send(function(status, response) {
376411
scope._sending = false;
377412
try {
378-
if (response === 'success') {
413+
if (status === 200 && response === 'success') {
379414
//log('sucessful upload');
380-
scope._unsentEvents.splice(0, numEvents);
415+
scope.removeEvents(maxEventId);
416+
417+
// Update the event cache after the removal of sent events.
381418
if (scope.options.saveEvents) {
382419
scope.saveEvents();
383420
}
421+
422+
// Send more events if any queued during previous send.
384423
if (scope._unsentEvents.length > 0) {
385424
scope.sendEvents();
386425
}
426+
} else if (status === 413) {
427+
//log('request too large');
428+
// Can't even get this one massive event through. Drop it.
429+
if (scope.options.uploadBatchSize === 1) {
430+
scope.removeEvents(maxEventId);
431+
}
432+
433+
// The server complained about the length of the request.
434+
// Backoff and try again.
435+
scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
436+
scope.sendEvents();
387437
}
388438
} catch (e) {
389439
//log('failed upload');

src/xhr.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ Request.prototype.send = function(callback) {
2222
xhr.open('POST', this.url, true);
2323
xhr.onreadystatechange = function() {
2424
if (xhr.readyState === 4) {
25-
if (xhr.status === 200) {
26-
callback(xhr.responseText);
27-
}
25+
callback(xhr.status, xhr.responseText);
2826
}
2927
};
3028
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');

0 commit comments

Comments
 (0)