Skip to content

Commit fd74808

Browse files
committed
Merge pull request #23 from amplitude/batch_events
Add option to batch events
2 parents 65eef18 + 603ea1d commit fd74808

File tree

6 files changed

+164
-22
lines changed

6 files changed

+164
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Unreleased
22

3+
* Add option to batch events into a single request.
4+
35
## 2.2.1 (Aug 13, 2015)
46

57
* Fix bug where multi-byte unicode characters were hashed improperly.

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ You can configure Amplitude by passing an object as the third argument to the `i
8787
// optional configuration options
8888
saveEvents: true,
8989
includeUtm: true,
90-
includeReferrer: true
90+
includeReferrer: true,
91+
batchEvents: true,
92+
eventUploadThreshold: 50
9193
})
9294

9395
| option | description | default |
@@ -97,6 +99,9 @@ You can configure Amplitude by passing an object as the third argument to the `i
9799
| uploadBatchSize | Maximum number of events to send to the server per request. | 100 |
98100
| 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` |
99101
| includeReferrer | If `true`, includes `referrer` and `referring_domain` as user propeties on all events uploaded. | `false` |
102+
| batchEvents | If `true`, events are batched together and uploaded only when the number of unsent events is greater than or equal to `eventUploadThreshold` or after `eventUploadPeriodMillis` milliseconds have passed since the first unsent event was logged. | `false` |
103+
| eventUploadThreshold | Minimum number of events to batch together per request if `batchEvents` is `true`. | 30 |
104+
| eventUploadPeriodMillis | Amount of time in milliseconds that the SDK waits before uploading events if `batchEvents` is `true`. | 30*1000 |
100105

101106

102107
# Advanced #

amplitude.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ var DEFAULT_OPTIONS = {
139139
saveEvents: true,
140140
sessionTimeout: 30 * 60 * 1000,
141141
unsentKey: 'amplitude_unsent',
142-
uploadBatchSize: 100
142+
uploadBatchSize: 100,
143+
batchEvents: false,
144+
eventUploadThreshold: 30,
145+
eventUploadPeriodMillis: 30 * 1000 // 30s
143146
};
144147
var LocalStorageKeys = {
145148
LAST_EVENT_ID: 'amplitude_lastEventId',
@@ -188,11 +191,16 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
188191
if (opt_config.includeReferrer !== undefined) {
189192
this.options.includeReferrer = !!opt_config.includeReferrer;
190193
}
194+
if (opt_config.batchEvents !== undefined) {
195+
this.options.batchEvents = !!opt_config.batchEvents;
196+
}
191197
this.options.platform = opt_config.platform || this.options.platform;
192198
this.options.language = opt_config.language || this.options.language;
193199
this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout;
194200
this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize;
201+
this.options.eventUploadThreshold = opt_config.eventUploadThreshold || this.options.eventUploadThreshold;
195202
this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount;
203+
this.options.eventUploadPeriodMillis = opt_config.eventUploadPeriodMillis || this.options.eventUploadPeriodMillis;
196204
}
197205

198206
Cookie.options({
@@ -222,9 +230,8 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
222230
}
223231
}
224232
}
225-
if (this._unsentEvents.length > 0) {
226-
this.sendEvents();
227-
}
233+
234+
this._sendEventsIfReady();
228235

229236
if (this.options.includeUtm) {
230237
this._initUtmData();
@@ -255,6 +262,23 @@ Amplitude.prototype.nextEventId = function() {
255262
return this._eventId;
256263
};
257264

265+
Amplitude.prototype._sendEventsIfReady = function() {
266+
if (this._unsentEvents.length === 0) {
267+
return;
268+
}
269+
270+
if (!this.options.batchEvents) {
271+
this.sendEvents();
272+
return;
273+
}
274+
275+
if (this._unsentEvents.length >= this.options.eventUploadThreshold) {
276+
this.sendEvents();
277+
} else {
278+
setTimeout(this.sendEvents.bind(this), this.options.eventUploadPeriodMillis);
279+
}
280+
};
281+
258282
var _loadCookieData = function(scope) {
259283
var cookieData = Cookie.get(scope.options.cookieName);
260284
if (cookieData) {
@@ -477,7 +501,7 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
477501
this.saveEvents();
478502
}
479503

480-
this.sendEvents();
504+
this._sendEventsIfReady();
481505

482506
return eventId;
483507
} catch (e) {
@@ -524,7 +548,7 @@ Amplitude.prototype.removeEvents = function (maxEventId) {
524548
};
525549

526550
Amplitude.prototype.sendEvents = function() {
527-
if (!this._sending && !this.options.optOut) {
551+
if (!this._sending && !this.options.optOut && this._unsentEvents.length > 0) {
528552
this._sending = true;
529553
var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' +
530554
this.options.apiEndpoint + '/';
@@ -557,9 +581,8 @@ Amplitude.prototype.sendEvents = function() {
557581
}
558582

559583
// Send more events if any queued during previous send.
560-
if (scope._unsentEvents.length > 0) {
561-
scope.sendEvents();
562-
}
584+
scope._sendEventsIfReady();
585+
563586
} else if (status === 413) {
564587
//log('request too large');
565588
// Can't even get this one massive event through. Drop it.

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: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ var DEFAULT_OPTIONS = {
2727
saveEvents: true,
2828
sessionTimeout: 30 * 60 * 1000,
2929
unsentKey: 'amplitude_unsent',
30-
uploadBatchSize: 100
30+
uploadBatchSize: 100,
31+
batchEvents: false,
32+
eventUploadThreshold: 30,
33+
eventUploadPeriodMillis: 30 * 1000 // 30s
3134
};
3235
var LocalStorageKeys = {
3336
LAST_EVENT_ID: 'amplitude_lastEventId',
@@ -76,11 +79,16 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
7679
if (opt_config.includeReferrer !== undefined) {
7780
this.options.includeReferrer = !!opt_config.includeReferrer;
7881
}
82+
if (opt_config.batchEvents !== undefined) {
83+
this.options.batchEvents = !!opt_config.batchEvents;
84+
}
7985
this.options.platform = opt_config.platform || this.options.platform;
8086
this.options.language = opt_config.language || this.options.language;
8187
this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout;
8288
this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize;
89+
this.options.eventUploadThreshold = opt_config.eventUploadThreshold || this.options.eventUploadThreshold;
8390
this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount;
91+
this.options.eventUploadPeriodMillis = opt_config.eventUploadPeriodMillis || this.options.eventUploadPeriodMillis;
8492
}
8593

8694
Cookie.options({
@@ -110,9 +118,8 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
110118
}
111119
}
112120
}
113-
if (this._unsentEvents.length > 0) {
114-
this.sendEvents();
115-
}
121+
122+
this._sendEventsIfReady();
116123

117124
if (this.options.includeUtm) {
118125
this._initUtmData();
@@ -143,6 +150,23 @@ Amplitude.prototype.nextEventId = function() {
143150
return this._eventId;
144151
};
145152

153+
Amplitude.prototype._sendEventsIfReady = function() {
154+
if (this._unsentEvents.length === 0) {
155+
return;
156+
}
157+
158+
if (!this.options.batchEvents) {
159+
this.sendEvents();
160+
return;
161+
}
162+
163+
if (this._unsentEvents.length >= this.options.eventUploadThreshold) {
164+
this.sendEvents();
165+
} else {
166+
setTimeout(this.sendEvents.bind(this), this.options.eventUploadPeriodMillis);
167+
}
168+
};
169+
146170
var _loadCookieData = function(scope) {
147171
var cookieData = Cookie.get(scope.options.cookieName);
148172
if (cookieData) {
@@ -365,7 +389,7 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
365389
this.saveEvents();
366390
}
367391

368-
this.sendEvents();
392+
this._sendEventsIfReady();
369393

370394
return eventId;
371395
} catch (e) {
@@ -412,7 +436,7 @@ Amplitude.prototype.removeEvents = function (maxEventId) {
412436
};
413437

414438
Amplitude.prototype.sendEvents = function() {
415-
if (!this._sending && !this.options.optOut) {
439+
if (!this._sending && !this.options.optOut && this._unsentEvents.length > 0) {
416440
this._sending = true;
417441
var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' +
418442
this.options.apiEndpoint + '/';
@@ -445,9 +469,8 @@ Amplitude.prototype.sendEvents = function() {
445469
}
446470

447471
// Send more events if any queued during previous send.
448-
if (scope._unsentEvents.length > 0) {
449-
scope.sendEvents();
450-
}
472+
scope._sendEventsIfReady();
473+
451474
} else if (status === 413) {
452475
//log('request too large');
453476
// Can't even get this one massive event through. Drop it.

test/amplitude.js

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,16 @@ describe('Amplitude', function() {
130130

131131
describe('logEvent', function() {
132132

133+
var clock;
134+
133135
beforeEach(function() {
136+
clock = sinon.useFakeTimers();
134137
amplitude.init(apiKey);
135138
});
136139

137140
afterEach(function() {
138141
reset();
142+
clock.restore();
139143
});
140144

141145
it('should send request', function() {
@@ -260,7 +264,7 @@ describe('Amplitude', function() {
260264
assert.deepEqual(amplitude2._unsentEvents, []);
261265
});
262266

263-
it('should batch events sent', function() {
267+
it('should limit events sent', function() {
264268
amplitude.init(apiKey, null, {uploadBatchSize: 10});
265269

266270
amplitude._sending = true;
@@ -287,6 +291,91 @@ describe('Amplitude', function() {
287291
assert.deepEqual(events[5].event_properties, {index: 100});
288292
});
289293

294+
it('should batch events sent', function() {
295+
var eventUploadPeriodMillis = 10*1000;
296+
amplitude.init(apiKey, null, {
297+
batchEvents: true,
298+
eventUploadThreshold: 10,
299+
eventUploadPeriodMillis: eventUploadPeriodMillis
300+
});
301+
302+
for (var i = 0; i < 15; i++) {
303+
amplitude.logEvent('Event', {index: i});
304+
}
305+
306+
assert.lengthOf(server.requests, 1);
307+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
308+
assert.lengthOf(events, 10);
309+
assert.deepEqual(events[0].event_properties, {index: 0});
310+
assert.deepEqual(events[9].event_properties, {index: 9});
311+
312+
server.respondWith('success');
313+
server.respond();
314+
315+
assert.lengthOf(server.requests, 1);
316+
var unsentEvents = amplitude._unsentEvents;
317+
assert.lengthOf(unsentEvents, 5);
318+
assert.deepEqual(unsentEvents[4].event_properties, {index: 14});
319+
320+
// remaining 5 events should be sent by the delayed sendEvent call
321+
clock.tick(eventUploadPeriodMillis);
322+
assert.lengthOf(server.requests, 2);
323+
server.respondWith('success');
324+
server.respond();
325+
assert.lengthOf(amplitude._unsentEvents, 0);
326+
var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e);
327+
assert.lengthOf(events, 5);
328+
assert.deepEqual(events[4].event_properties, {index: 14});
329+
});
330+
331+
it('should send events after a delay', function() {
332+
var eventUploadPeriodMillis = 10*1000;
333+
amplitude.init(apiKey, null, {
334+
batchEvents: true,
335+
eventUploadThreshold: 2,
336+
eventUploadPeriodMillis: eventUploadPeriodMillis
337+
});
338+
amplitude.logEvent('Event');
339+
340+
// saveEvent should not have been called yet
341+
assert.lengthOf(amplitude._unsentEvents, 1);
342+
assert.lengthOf(server.requests, 0);
343+
344+
// saveEvent should be called after delay
345+
clock.tick(eventUploadPeriodMillis);
346+
assert.lengthOf(server.requests, 1);
347+
server.respondWith('success');
348+
server.respond();
349+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
350+
assert.lengthOf(events, 1);
351+
assert.deepEqual(events[0].event_type, 'Event');
352+
});
353+
354+
it('should not send events after a delay if no events to send', function() {
355+
var eventUploadPeriodMillis = 10*1000;
356+
amplitude.init(apiKey, null, {
357+
batchEvents: true,
358+
eventUploadThreshold: 2,
359+
eventUploadPeriodMillis: eventUploadPeriodMillis
360+
});
361+
amplitude.logEvent('Event1');
362+
amplitude.logEvent('Event2');
363+
364+
// saveEvent triggered by 2 event batch threshold
365+
assert.lengthOf(amplitude._unsentEvents, 2);
366+
assert.lengthOf(server.requests, 1);
367+
server.respondWith('success');
368+
server.respond();
369+
var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e);
370+
assert.lengthOf(events, 2);
371+
assert.deepEqual(events[1].event_type, 'Event2');
372+
373+
// saveEvent should be called after delay, but no request made
374+
assert.lengthOf(amplitude._unsentEvents, 0);
375+
clock.tick(eventUploadPeriodMillis);
376+
assert.lengthOf(server.requests, 1);
377+
})
378+
290379
it('should back off on 413 status', function() {
291380
amplitude.init(apiKey, null, {uploadBatchSize: 10});
292381

0 commit comments

Comments
 (0)