Skip to content

Commit 3c23d1f

Browse files
committed
Limit number of events stored in local storage
1 parent b1c4d08 commit 3c23d1f

File tree

5 files changed

+175
-28
lines changed

5 files changed

+175
-28
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ 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 |
9697
| 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` |
9798

9899

amplitude.js

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,17 @@ 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',
141142
};
142143
var LocalStorageKeys = {
143144
LAST_EVENT_ID: 'amplitude_lastEventId',
@@ -185,6 +186,7 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
185186
this.options.platform = opt_config.platform || this.options.platform;
186187
this.options.language = opt_config.language || this.options.language;
187188
this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout;
189+
this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount;
188190
}
189191

190192
Cookie.options({
@@ -433,19 +435,31 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
433435
}
434436
// country: null
435437
};
438+
439+
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
440+
436441
this._unsentEvents.push(event);
442+
443+
// Remove old events from the beginning of the array if too many
444+
// have accumulated. Don't want to kill memory. Default is 1000 events.
445+
if (this._unsentEvents.length > this.options.savedMaxCount) {
446+
this._unsentEvents.splice(0, this._unsentEvents.length - this.options.savedMaxCount);
447+
}
448+
437449
if (this.options.saveEvents) {
438450
this.saveEvents();
439451
}
440-
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
452+
441453
this.sendEvents();
454+
455+
return eventId;
442456
} catch (e) {
443457
log(e);
444458
}
445459
};
446460

447461
Amplitude.prototype.logEvent = function(eventType, eventProperties) {
448-
this._logEvent(eventType, eventProperties);
462+
return this._logEvent(eventType, eventProperties);
449463
};
450464

451465
// Test that n is a number or a numeric value.
@@ -460,20 +474,38 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) {
460474
return;
461475
}
462476

463-
this._logEvent('revenue_amount', {}, {
477+
return this._logEvent('revenue_amount', {}, {
464478
productId: product,
465479
special: 'revenue_amount',
466480
quantity: quantity || 1,
467481
price: price
468482
});
469483
};
470484

485+
/**
486+
* Remove events in storage with event ids up to and including maxEventId. Does
487+
* a true filter in case events get out of order or old events are removed.
488+
*/
489+
Amplitude.prototype.removeEvents = function (maxEventId) {
490+
var filteredEvents = [];
491+
for (var i = 0; i < this._unsentEvents.length; i++) {
492+
if (this._unsentEvents[i].event_id > maxEventId) {
493+
filteredEvents.push(this._unsentEvents[i]);
494+
}
495+
}
496+
this._unsentEvents = filteredEvents;
497+
};
498+
471499
Amplitude.prototype.sendEvents = function() {
472500
if (!this._sending && !this.options.optOut) {
473501
this._sending = true;
474502
var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' +
475503
this.options.apiEndpoint + '/';
476-
var events = JSON.stringify(this._unsentEvents);
504+
505+
// Determine how many events to send and track the maximum event id sent in this batch.
506+
var maxEventId = this._unsentEvents[this._unsentEvents.length - 1].eventId;
507+
508+
var events = JSON.stringify(this._unsentEvents.slice(0, this._unsentEvents.length));
477509
var uploadTime = new Date().getTime();
478510
var data = {
479511
client: this.options.apiKey,
@@ -482,17 +514,21 @@ Amplitude.prototype.sendEvents = function() {
482514
upload_time: uploadTime,
483515
checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime)
484516
};
485-
var numEvents = this._unsentEvents.length;
517+
486518
var scope = this;
487519
new Request(url, data).send(function(response) {
488520
scope._sending = false;
489521
try {
490522
if (response === 'success') {
491523
//log('sucessful upload');
492-
scope._unsentEvents.splice(0, numEvents);
524+
scope.removeEvents(maxEventId);
525+
526+
// Update the event cache after the removal of sent events.
493527
if (scope.options.saveEvents) {
494528
scope.saveEvents();
495529
}
530+
531+
// Send more events if any queued during previous send.
496532
if (scope._unsentEvents.length > 0) {
497533
scope.sendEvents();
498534
}

amplitude.min.js

Lines changed: 2 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: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ 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',
2930
};
3031
var LocalStorageKeys = {
3132
LAST_EVENT_ID: 'amplitude_lastEventId',
@@ -73,6 +74,7 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
7374
this.options.platform = opt_config.platform || this.options.platform;
7475
this.options.language = opt_config.language || this.options.language;
7576
this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout;
77+
this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount;
7678
}
7779

7880
Cookie.options({
@@ -321,19 +323,31 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
321323
}
322324
// country: null
323325
};
326+
327+
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
328+
324329
this._unsentEvents.push(event);
330+
331+
// Remove old events from the beginning of the array if too many
332+
// have accumulated. Don't want to kill memory. Default is 1000 events.
333+
if (this._unsentEvents.length > this.options.savedMaxCount) {
334+
this._unsentEvents.splice(0, this._unsentEvents.length - this.options.savedMaxCount);
335+
}
336+
325337
if (this.options.saveEvents) {
326338
this.saveEvents();
327339
}
328-
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
340+
329341
this.sendEvents();
342+
343+
return eventId;
330344
} catch (e) {
331345
log(e);
332346
}
333347
};
334348

335349
Amplitude.prototype.logEvent = function(eventType, eventProperties) {
336-
this._logEvent(eventType, eventProperties);
350+
return this._logEvent(eventType, eventProperties);
337351
};
338352

339353
// Test that n is a number or a numeric value.
@@ -348,20 +362,38 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) {
348362
return;
349363
}
350364

351-
this._logEvent('revenue_amount', {}, {
365+
return this._logEvent('revenue_amount', {}, {
352366
productId: product,
353367
special: 'revenue_amount',
354368
quantity: quantity || 1,
355369
price: price
356370
});
357371
};
358372

373+
/**
374+
* Remove events in storage with event ids up to and including maxEventId. Does
375+
* a true filter in case events get out of order or old events are removed.
376+
*/
377+
Amplitude.prototype.removeEvents = function (maxEventId) {
378+
var filteredEvents = [];
379+
for (var i = 0; i < this._unsentEvents.length; i++) {
380+
if (this._unsentEvents[i].event_id > maxEventId) {
381+
filteredEvents.push(this._unsentEvents[i]);
382+
}
383+
}
384+
this._unsentEvents = filteredEvents;
385+
};
386+
359387
Amplitude.prototype.sendEvents = function() {
360388
if (!this._sending && !this.options.optOut) {
361389
this._sending = true;
362390
var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' +
363391
this.options.apiEndpoint + '/';
364-
var events = JSON.stringify(this._unsentEvents);
392+
393+
// Determine how many events to send and track the maximum event id sent in this batch.
394+
var maxEventId = this._unsentEvents[this._unsentEvents.length - 1].eventId;
395+
396+
var events = JSON.stringify(this._unsentEvents.slice(0, this._unsentEvents.length));
365397
var uploadTime = new Date().getTime();
366398
var data = {
367399
client: this.options.apiKey,
@@ -370,17 +402,21 @@ Amplitude.prototype.sendEvents = function() {
370402
upload_time: uploadTime,
371403
checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime)
372404
};
373-
var numEvents = this._unsentEvents.length;
405+
374406
var scope = this;
375407
new Request(url, data).send(function(response) {
376408
scope._sending = false;
377409
try {
378410
if (response === 'success') {
379411
//log('sucessful upload');
380-
scope._unsentEvents.splice(0, numEvents);
412+
scope.removeEvents(maxEventId);
413+
414+
// Update the event cache after the removal of sent events.
381415
if (scope.options.saveEvents) {
382416
scope.saveEvents();
383417
}
418+
419+
// Send more events if any queued during previous send.
384420
if (scope._unsentEvents.length > 0) {
385421
scope.sendEvents();
386422
}

test/amplitude.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,80 @@ describe('Amplitude', function() {
185185
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
186186
assert.deepEqual(events[0].event_properties, {prop: true});
187187
});
188+
189+
it('should queue events', function() {
190+
amplitude._sending = true;
191+
amplitude.logEvent('Event', {index: 1});
192+
amplitude.logEvent('Event', {index: 2});
193+
amplitude.logEvent('Event', {index: 3});
194+
amplitude._sending = false;
195+
196+
amplitude.logEvent('Event', {index: 100});
197+
198+
assert.lengthOf(server.requests, 1);
199+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
200+
assert.lengthOf(events, 4);
201+
assert.deepEqual(events[0].event_properties, {index: 1});
202+
assert.deepEqual(events[3].event_properties, {index: 100});
203+
});
204+
205+
it('should limit events queued', function() {
206+
amplitude.init(apiKey, null, {savedMaxCount: 10});
207+
208+
amplitude._sending = true;
209+
for (var i = 0; i < 15; i++) {
210+
amplitude.logEvent('Event', {index: i});
211+
}
212+
amplitude._sending = false;
213+
214+
amplitude.logEvent('Event', {index: 100});
215+
216+
assert.lengthOf(server.requests, 1);
217+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
218+
assert.lengthOf(events, 10);
219+
assert.deepEqual(events[0].event_properties, {index: 6});
220+
assert.deepEqual(events[9].event_properties, {index: 100});
221+
});
222+
223+
it('should remove only sent events', function() {
224+
amplitude._sending = true;
225+
amplitude.logEvent('Event', {index: 1});
226+
amplitude.logEvent('Event', {index: 2});
227+
amplitude._sending = false;
228+
amplitude.logEvent('Event', {index: 3});
229+
230+
server.respondWith('success');
231+
server.respond();
232+
233+
amplitude.logEvent('Event', {index: 4});
234+
235+
assert.lengthOf(server.requests, 2);
236+
var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e);
237+
assert.lengthOf(events, 1);
238+
assert.deepEqual(events[0].event_properties, {index: 4});
239+
});
240+
241+
it('should save events', function() {
242+
amplitude.init(apiKey, null, {saveEvents: true});
243+
amplitude.logEvent('Event', {index: 1});
244+
amplitude.logEvent('Event', {index: 2});
245+
amplitude.logEvent('Event', {index: 3});
246+
247+
var amplitude2 = new Amplitude();
248+
amplitude2.init(apiKey);
249+
assert.deepEqual(amplitude2._unsentEvents, amplitude._unsentEvents);
250+
});
251+
252+
it('should not save events', function() {
253+
amplitude.init(apiKey, null, {saveEvents: false});
254+
amplitude.logEvent('Event', {index: 1});
255+
amplitude.logEvent('Event', {index: 2});
256+
amplitude.logEvent('Event', {index: 3});
257+
258+
var amplitude2 = new Amplitude();
259+
amplitude2.init(apiKey);
260+
assert.deepEqual(amplitude2._unsentEvents, []);
261+
});
188262
});
189263

190264
describe('optOut', function() {

0 commit comments

Comments
 (0)