Skip to content

Commit 83f7330

Browse files
committed
coalesce events, truncate long string values
1 parent 60b8732 commit 83f7330

File tree

5 files changed

+606
-96
lines changed

5 files changed

+606
-96
lines changed

amplitude.js

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ var log = function(s) {
128128

129129
var IDENTIFY_EVENT = '$identify';
130130
var API_VERSION = 2;
131+
var MAX_STRING_LENGTH = 1024;
131132
var DEFAULT_OPTIONS = {
132133
apiEndpoint: 'api.amplitude.com',
133134
cookieExpiration: 365 * 10,
@@ -149,6 +150,7 @@ var DEFAULT_OPTIONS = {
149150
};
150151
var LocalStorageKeys = {
151152
LAST_EVENT_ID: 'amplitude_lastEventId',
153+
LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId',
152154
LAST_EVENT_TIME: 'amplitude_lastEventTime',
153155
SESSION_ID: 'amplitude_sessionId'
154156
};
@@ -164,11 +166,14 @@ var Amplitude = function() {
164166
};
165167

166168
Amplitude.prototype._eventId = 0;
169+
Amplitude.prototype._identifyId = 0;
167170
Amplitude.prototype._sending = false;
168171
Amplitude.prototype._lastEventTime = null;
169172
Amplitude.prototype._sessionId = null;
170173
Amplitude.prototype._newSession = false;
171174

175+
Amplitude.prototype.Identify = Identify;
176+
172177
/**
173178
* Initializes Amplitude.
174179
* apiKey The API Key for your app
@@ -225,10 +230,16 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
225230

226231
if (this.options.saveEvents) {
227232
var savedUnsentEventsString = localStorage.getItem(this.options.unsentKey);
228-
var savedUnsentIdentifysString = localStorage.getItem(this.options.unsentIdentifyKey);
229233
if (savedUnsentEventsString) {
230234
try {
231235
this._unsentEvents = JSON.parse(savedUnsentEventsString);
236+
} catch (e) {
237+
//log(e);
238+
}
239+
}
240+
var savedUnsentIdentifysString = localStorage.getItem(this.options.unsentIdentifyKey);
241+
if (savedUnsentIdentifysString) {
242+
try {
232243
this._unsentIdentifys = JSON.parse(savedUnsentIdentifysString);
233244
} catch (e) {
234245
//log(e);
@@ -245,6 +256,7 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config) {
245256
this._lastEventTime = parseInt(localStorage.getItem(LocalStorageKeys.LAST_EVENT_TIME)) || null;
246257
this._sessionId = parseInt(localStorage.getItem(LocalStorageKeys.SESSION_ID)) || null;
247258
this._eventId = localStorage.getItem(LocalStorageKeys.LAST_EVENT_ID) || 0;
259+
this._identifyId = localStorage.getItem(LocalStorageKeys.LAST_IDENTIFY_ID) || 0;
248260
var now = new Date().getTime();
249261
if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) {
250262
this._newSession = true;
@@ -267,6 +279,11 @@ Amplitude.prototype.nextEventId = function() {
267279
return this._eventId;
268280
};
269281

282+
Amplitude.prototype.nextIdentifyId = function() {
283+
this._identifyId++;
284+
return this._identifyId;
285+
};
286+
270287
// returns the number of unsent events and identifys
271288
Amplitude.prototype._unsentCount = function() {
272289
return this._unsentEvents.length + this._unsentIdentifys.length;
@@ -444,6 +461,34 @@ Amplitude.prototype.setVersionName = function(versionName) {
444461
}
445462
};
446463

464+
// truncate string values in event and user properties so that request size does not get too large
465+
Amplitude.prototype._truncate = function(value) {
466+
if (typeof(value) === 'object') {
467+
if (Array.isArray(value)) {
468+
for (var i = 0; i < value.length; i++) {
469+
value[i] = this._truncate(value[i]);
470+
}
471+
} else {
472+
for (var key in value) {
473+
if (value.hasOwnProperty(key)) {
474+
value[key] = this._truncate(value[key]);
475+
}
476+
}
477+
}
478+
479+
return value;
480+
}
481+
482+
return _truncateValue(value);
483+
};
484+
485+
var _truncateValue = function(value) {
486+
if (typeof(value) === 'string') {
487+
return value.length > MAX_STRING_LENGTH ? value.substring(0, MAX_STRING_LENGTH) : value;
488+
}
489+
return value;
490+
};
491+
447492
/**
448493
* Private logEvent method. Keeps apiProperties from being publicly exposed.
449494
*/
@@ -459,16 +504,22 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
459504
return;
460505
}
461506
try {
507+
var eventId;
508+
if (eventType === IDENTIFY_EVENT) {
509+
eventId = this.nextIdentifyId();
510+
localStorage.setItem(LocalStorageKeys.LAST_IDENTIFY_ID, eventId);
511+
} else {
512+
eventId = this.nextEventId();
513+
localStorage.setItem(LocalStorageKeys.LAST_EVENT_ID, eventId);
514+
}
462515
var eventTime = new Date().getTime();
463-
var eventId = this.nextEventId();
464516
var ua = this._ua;
465517
if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) {
466518
this._sessionId = eventTime;
467519
localStorage.setItem(LocalStorageKeys.SESSION_ID, this._sessionId);
468520
}
469521
this._lastEventTime = eventTime;
470522
localStorage.setItem(LocalStorageKeys.LAST_EVENT_TIME, this._lastEventTime);
471-
localStorage.setItem(LocalStorageKeys.LAST_EVENT_ID, eventId);
472523

473524
// Add the utm properties, if any, onto the user properties.
474525
userProperties = userProperties || {};
@@ -498,8 +549,8 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
498549
device_model: ua.os.name || null,
499550
language: this.options.language,
500551
api_properties: apiProperties,
501-
event_properties: eventProperties,
502-
user_properties: userProperties,
552+
event_properties: this._truncate(eventProperties),
553+
user_properties: this._truncate(userProperties),
503554
uuid: UUID(),
504555
library: {
505556
name: 'amplitude-js',
@@ -508,14 +559,19 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti
508559
// country: null
509560
};
510561

511-
//log('logged eventType=' + eventType + ', properties=' + JSON.stringify(eventProperties));
512-
513-
this._unsentEvents.push(event);
562+
if (eventType === IDENTIFY_EVENT) {
563+
this._unsentIdentifys.push(event);
564+
if (this._unsentIdentifys.length > this.options.savedMaxCount) {
565+
this._unsentIdentifys.splice(0, this._unsentIdentifys.length - this.options.savedMaxCount);
566+
}
567+
} else {
568+
this._unsentEvents.push(event);
514569

515-
// Remove old events from the beginning of the array if too many
516-
// have accumulated. Don't want to kill memory. Default is 1000 events.
517-
if (this._unsentEvents.length > this.options.savedMaxCount) {
518-
this._unsentEvents.splice(0, this._unsentEvents.length - this.options.savedMaxCount);
570+
// Remove old events from the beginning of the array if too many
571+
// have accumulated. Don't want to kill memory. Default is 1000 events.
572+
if (this._unsentEvents.length > this.options.savedMaxCount) {
573+
this._unsentEvents.splice(0, this._unsentEvents.length - this.options.savedMaxCount);
574+
}
519575
}
520576

521577
if (this.options.saveEvents) {
@@ -560,14 +616,26 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) {
560616
* Remove events in storage with event ids up to and including maxEventId. Does
561617
* a true filter in case events get out of order or old events are removed.
562618
*/
563-
Amplitude.prototype.removeEvents = function (maxEventId) {
564-
var filteredEvents = [];
565-
for (var i = 0; i < this._unsentEvents.length; i++) {
566-
if (this._unsentEvents[i].event_id > maxEventId) {
567-
filteredEvents.push(this._unsentEvents[i]);
619+
Amplitude.prototype.removeEvents = function (maxEventId, maxIdentifyId) {
620+
if (maxEventId) {
621+
var filteredEvents = [];
622+
for (var i = 0; i < this._unsentEvents.length; i++) {
623+
if (this._unsentEvents[i].event_id > maxEventId) {
624+
filteredEvents.push(this._unsentEvents[i]);
625+
}
626+
}
627+
this._unsentEvents = filteredEvents;
628+
}
629+
630+
if (maxIdentifyId) {
631+
var filteredIdentifys = [];
632+
for (var j = 0; j < this._unsentIdentifys.length; j++) {
633+
if (this._unsentIdentifys[j].event_id > maxIdentifyId) {
634+
filteredIdentifys.push(this._unsentIdentifys[j]);
635+
}
568636
}
637+
this._unsentIdentifys = filteredIdentifys;
569638
}
570-
this._unsentEvents = filteredEvents;
571639
};
572640

573641
Amplitude.prototype.sendEvents = function(callback) {
@@ -577,10 +645,42 @@ Amplitude.prototype.sendEvents = function(callback) {
577645
this.options.apiEndpoint + '/';
578646

579647
// Determine how many events to send and track the maximum event id sent in this batch.
580-
var numEvents = Math.min(this._unsentEvents.length, this.options.uploadBatchSize);
581-
var maxEventId = this._unsentEvents[numEvents - 1].event_id;
648+
var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize);
649+
650+
// coalesce events from both queues
651+
var eventsToSend = [];
652+
var eventIndex = 0;
653+
var identifyIndex = 0;
654+
655+
while (eventsToSend.length < numEvents) {
656+
var event;
657+
658+
// case 1: no identifys - grab from events
659+
if (identifyIndex >= this._unsentIdentifys.length) {
660+
event = this._unsentEvents[eventIndex++];
661+
662+
// case 2: no events - grab from identifys
663+
} else if (eventIndex >= this._unsentEvents.length) {
664+
event = this._unsentIdentifys[identifyIndex++];
665+
666+
// case 3: need to compare timestamps
667+
} else {
668+
if (this._unsentIdentifys[identifyIndex].timestamp <= this._unsentEvents[eventIndex].timestamp) {
669+
event = this._unsentIdentifys[identifyIndex++];
670+
} else {
671+
event = this._unsentEvents[eventIndex++];
672+
}
673+
}
674+
675+
eventsToSend.push(event);
676+
}
677+
678+
var maxEventId = eventIndex > 0 && this._unsentEvents.length > 0 ?
679+
this._unsentEvents[eventIndex - 1].event_id : null;
680+
var maxIdentifyId = identifyIndex > 0 && this._unsentIdentifys.length > 0 ?
681+
this._unsentIdentifys[identifyIndex - 1].event_id : null;
682+
var events = JSON.stringify(eventsToSend);
582683

583-
var events = JSON.stringify(this._unsentEvents.slice(0, numEvents));
584684
var uploadTime = new Date().getTime();
585685
var data = {
586686
client: this.options.apiKey,
@@ -596,7 +696,7 @@ Amplitude.prototype.sendEvents = function(callback) {
596696
try {
597697
if (status === 200 && response === 'success') {
598698
//log('sucessful upload');
599-
scope.removeEvents(maxEventId);
699+
scope.removeEvents(maxEventId, maxIdentifyId);
600700

601701
// Update the event cache after the removal of sent events.
602702
if (scope.options.saveEvents) {
@@ -612,7 +712,8 @@ Amplitude.prototype.sendEvents = function(callback) {
612712
//log('request too large');
613713
// Can't even get this one massive event through. Drop it.
614714
if (scope.options.uploadBatchSize === 1) {
615-
scope.removeEvents(maxEventId);
715+
// if massive event is identify, still need to drop it
716+
scope.removeEvents(maxEventId, maxIdentifyId);
616717
}
617718

618719
// The server complained about the length of the request.

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.

0 commit comments

Comments
 (0)