Skip to content

Commit c8c6e14

Browse files
committed
Merge pull request #50 from amplitude/make_json_serializable
Validate event properties
2 parents c2ef2ef + 03356dc commit c8c6e14

File tree

11 files changed

+429
-100
lines changed

11 files changed

+429
-100
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Add support for logging events to multiple Amplitude apps. **Note this is a major update, and may break backwards compatability.** See [Readme](https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps) for details.
44
* Fix bug where saveReferrer throws exception if sessionStorage is disabled.
55
* Log messages with a try/catch to support IE 8.
6+
* Validate event properties during logEvent and initialization before sending request.
67

78
## 2.9.0 (January 15, 2016)
89

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE');
132132

133133
# Setting Event Properties #
134134

135-
You can attach additional data to any event by passing a Javascript object as the second argument to `logEvent`. The Javascript object should be in the form of key + value pairs that can be JSON serialized (we recommend using String values):
135+
You can attach additional data to any event by passing a Javascript object as the second argument to `logEvent`. The Javascript object should be in the form of key + value pairs that can be JSON serialized. The keys should be string values. The values can be booleans, strings, numbers, arrays of strings/numbers/booleans, nested Javascript objects, and errors (note you cannot nest arrays or Javascript objects inside array values). The SDK will validate the event properties that you set and will log any errors or warnings to console if there are any issues. Here is an example:
136136

137137
```javascript
138138
var eventProperties = {};

amplitude.js

Lines changed: 122 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -262,14 +262,6 @@ var version = require('./version');
262262
var type = require('./type');
263263
var DEFAULT_OPTIONS = require('./options');
264264

265-
var log = function(s) {
266-
try {
267-
console.log('[Amplitude] ' + s);
268-
} catch (e) {
269-
// console logging not available
270-
}
271-
};
272-
273265
var DEFAULT_INSTANCE = '$default_instance';
274266
var IDENTIFY_EVENT = '$identify';
275267
var API_VERSION = 2;
@@ -375,12 +367,19 @@ AmplitudeClient.prototype.init = function(apiKey, opt_userId, opt_config, callba
375367
this._lastEventTime = now;
376368
_saveCookieData(this);
377369

378-
//log('initialized with apiKey=' + apiKey);
379-
//opt_userId !== undefined && opt_userId !== null && log('initialized with userId=' + opt_userId);
370+
//utils.log('initialized with apiKey=' + apiKey);
371+
//opt_userId !== undefined && opt_userId !== null && utils.log('initialized with userId=' + opt_userId);
380372

381373
if (this.options.saveEvents) {
382374
this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey) || this._unsentEvents;
383375
this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey) || this._unsentIdentifys;
376+
377+
// validate event properties for unsent events
378+
for (var i = 0; i < this._unsentEvents.length; i++) {
379+
var eventProperties = this._unsentEvents[i].event_properties;
380+
this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties);
381+
}
382+
384383
this._sendEventsIfReady();
385384
}
386385

@@ -392,7 +391,7 @@ AmplitudeClient.prototype.init = function(apiKey, opt_userId, opt_config, callba
392391
this._saveReferrer(this._getReferrer());
393392
}
394393
} catch (e) {
395-
log(e);
394+
utils.log(e);
396395
}
397396

398397
if (callback && type(callback) === 'function') {
@@ -404,7 +403,7 @@ AmplitudeClient.prototype.Identify = Identify;
404403

405404
AmplitudeClient.prototype._apiKeySet = function(methodName) {
406405
if (!this.options.apiKey) {
407-
log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName);
406+
utils.log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName);
408407
return false;
409408
}
410409
return true;
@@ -426,7 +425,7 @@ AmplitudeClient.prototype._loadSavedUnsentEvents = function(unsentKey) {
426425
try {
427426
return JSON.parse(savedUnsentEventsString);
428427
} catch (e) {
429-
//log(e);
428+
// utils.log(e);
430429
}
431430
}
432431
return null;
@@ -640,7 +639,7 @@ AmplitudeClient.prototype._saveReferrer = function(referrer) {
640639
hasSessionStorage = true;
641640
}
642641
} catch (e) {
643-
// log(e); // sessionStorage disabled
642+
// utils.log(e); // sessionStorage disabled
644643
}
645644

646645
if ((hasSessionStorage && !(this._getFromStorage(sessionStorage, LocalStorageKeys.REFERRER))) || !hasSessionStorage) {
@@ -663,7 +662,7 @@ AmplitudeClient.prototype.saveEvents = function() {
663662
this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents));
664663
this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys));
665664
} catch (e) {
666-
//log(e);
665+
// utils.log(e);
667666
}
668667
};
669668

@@ -679,9 +678,9 @@ AmplitudeClient.prototype.setDomain = function(domain) {
679678
this.options.domain = this.cookieStorage.options().domain;
680679
_loadCookieData(this);
681680
_saveCookieData(this);
682-
//log('set domain=' + domain);
681+
// utils.log('set domain=' + domain);
683682
} catch (e) {
684-
log(e);
683+
utils.log(e);
685684
}
686685
};
687686

@@ -693,9 +692,9 @@ AmplitudeClient.prototype.setUserId = function(userId) {
693692
try {
694693
this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null;
695694
_saveCookieData(this);
696-
//log('set userId=' + userId);
695+
// utils.log('set userId=' + userId);
697696
} catch (e) {
698-
log(e);
697+
utils.log(e);
699698
}
700699
};
701700

@@ -707,9 +706,9 @@ AmplitudeClient.prototype.setOptOut = function(enable) {
707706
try {
708707
this.options.optOut = enable;
709708
_saveCookieData(this);
710-
//log('set optOut=' + enable);
709+
// utils.log('set optOut=' + enable);
711710
} catch (e) {
712-
log(e);
711+
utils.log(e);
713712
}
714713
};
715714

@@ -724,7 +723,7 @@ AmplitudeClient.prototype.setDeviceId = function(deviceId) {
724723
_saveCookieData(this);
725724
}
726725
} catch (e) {
727-
log(e);
726+
utils.log(e);
728727
}
729728
};
730729

@@ -778,9 +777,9 @@ AmplitudeClient.prototype.identify = function(identify) {
778777
AmplitudeClient.prototype.setVersionName = function(versionName) {
779778
try {
780779
this.options.versionName = versionName;
781-
//log('set versionName=' + versionName);
780+
// utils.log('set versionName=' + versionName);
782781
} catch (e) {
783-
log(e);
782+
utils.log(e);
784783
}
785784
};
786785

@@ -862,7 +861,7 @@ AmplitudeClient.prototype._logEvent = function(eventType, eventProperties, apiPr
862861
device_model: ua.os.name || null,
863862
language: this.options.language,
864863
api_properties: apiProperties,
865-
event_properties: this._truncate(eventProperties),
864+
event_properties: this._truncate(utils.validateProperties(eventProperties)),
866865
user_properties: this._truncate(userProperties),
867866
uuid: UUID(),
868867
library: {
@@ -891,7 +890,7 @@ AmplitudeClient.prototype._logEvent = function(eventType, eventProperties, apiPr
891890

892891
return eventId;
893892
} catch (e) {
894-
log(e);
893+
utils.log(e);
895894
}
896895
};
897896

@@ -918,7 +917,7 @@ var _isNumber = function(n) {
918917
AmplitudeClient.prototype.logRevenue = function(price, quantity, product) {
919918
// Test that the parameters are of the right type.
920919
if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || quantity !== undefined && !_isNumber(quantity)) {
921-
// log('Price and quantity arguments to logRevenue must be numbers');
920+
// utils.log('Price and quantity arguments to logRevenue must be numbers');
922921
return -1;
923922
}
924923

@@ -987,7 +986,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) {
987986
scope._sending = false;
988987
try {
989988
if (status === 200 && response === 'success') {
990-
//log('sucessful upload');
989+
// utils.log('sucessful upload');
991990
scope.removeEvents(maxEventId, maxIdentifyId);
992991

993992
// Update the event cache after the removal of sent events.
@@ -1001,7 +1000,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) {
10011000
}
10021001

10031002
} else if (status === 413) {
1004-
//log('request too large');
1003+
// utils.log('request too large');
10051004
// Can't even get this one massive event through. Drop it.
10061005
if (scope.options.uploadBatchSize === 1) {
10071006
// if massive event is identify, still need to drop it
@@ -1017,7 +1016,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) {
10171016
callback(status, response);
10181017
}
10191018
} catch (e) {
1020-
//log('failed upload');
1019+
// utils.log('failed upload');
10211020
}
10221021
});
10231022
} else if (callback) {
@@ -2227,6 +2226,7 @@ module.exports = getUtmData;
22272226
}, {}],
22282227
4: [function(require, module, exports) {
22292228
var type = require('./type');
2229+
var utils = require('./utils');
22302230

22312231
/*
22322232
* Wrapper for a user properties JSON object that supports operations.
@@ -2241,14 +2241,6 @@ var AMP_OP_SET = '$set';
22412241
var AMP_OP_SET_ONCE = '$setOnce';
22422242
var AMP_OP_UNSET = '$unset';
22432243

2244-
var log = function(s) {
2245-
try {
2246-
console.log('[Amplitude] ' + s);
2247-
} catch (e) {
2248-
// console logging not available
2249-
}
2250-
};
2251-
22522244
var Identify = function() {
22532245
this.userPropertiesOperations = {};
22542246
this.properties = []; // keep track of keys that have been added
@@ -2258,7 +2250,7 @@ Identify.prototype.add = function(property, value) {
22582250
if (type(value) === 'number' || type(value) === 'string') {
22592251
this._addOperation(AMP_OP_ADD, property, value);
22602252
} else {
2261-
log('Unsupported type for value: ' + type(value) + ', expecting number or string');
2253+
utils.log('Unsupported type for value: ' + type(value) + ', expecting number or string');
22622254
}
22632255
return this;
22642256
};
@@ -2274,7 +2266,7 @@ Identify.prototype.append = function(property, value) {
22742266
Identify.prototype.clearAll = function() {
22752267
if (Object.keys(this.userPropertiesOperations).length > 0) {
22762268
if (!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) {
2277-
log('Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll');
2269+
utils.log('Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll');
22782270
}
22792271
return this;
22802272
}
@@ -2300,13 +2292,13 @@ Identify.prototype.unset = function(property) {
23002292
Identify.prototype._addOperation = function(operation, property, value) {
23012293
// check that the identify doesn't already contain a clearAll
23022294
if (this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) {
2303-
log('This identify already contains a $clearAll operation, skipping operation ' + operation);
2295+
utils.log('This identify already contains a $clearAll operation, skipping operation ' + operation);
23042296
return;
23052297
}
23062298

23072299
// check that property wasn't already used in this Identify
23082300
if (this.properties.indexOf(property) !== -1) {
2309-
log('User property "' + property + '" already used in this identify, skipping operation ' + operation);
2301+
utils.log('User property "' + property + '" already used in this identify, skipping operation ' + operation);
23102302
return;
23112303
}
23122304

@@ -2319,7 +2311,7 @@ Identify.prototype._addOperation = function(operation, property, value) {
23192311

23202312
module.exports = Identify;
23212313

2322-
}, {"./type":6}],
2314+
}, {"./type":6,"./utils":7}],
23232315
6: [function(require, module, exports) {
23242316
/* Taken from: https://github.com/component/type */
23252317

@@ -2368,6 +2360,93 @@ module.exports = function(val){
23682360
};
23692361

23702362
}, {}],
2363+
7: [function(require, module, exports) {
2364+
var type = require('./type');
2365+
2366+
var log = function(s) {
2367+
try {
2368+
console.log('[Amplitude] ' + s);
2369+
} catch (e) {
2370+
// console logging not available
2371+
}
2372+
};
2373+
2374+
var isEmptyString = function(str) {
2375+
return (!str || str.length === 0);
2376+
};
2377+
2378+
var validateProperties = function(properties) {
2379+
var propsType = type(properties);
2380+
if (propsType !== 'object') {
2381+
log('Error: invalid event properties format. Expecting Javascript object, received ' + propsType + ', ignoring');
2382+
return {};
2383+
}
2384+
2385+
var copy = {}; // create a copy with all of the valid properties
2386+
for (var property in properties) {
2387+
if (!properties.hasOwnProperty(property)) {
2388+
continue;
2389+
}
2390+
2391+
// validate key
2392+
var key = property;
2393+
var keyType = type(key);
2394+
if (keyType !== 'string') {
2395+
log('WARNING: Non-string property key, received type ' + keyType + ', coercing to string "' + key + '"');
2396+
key = String(key);
2397+
}
2398+
2399+
// validate value
2400+
var value = validatePropertyValue(key, properties[property]);
2401+
if (value === null) {
2402+
continue;
2403+
}
2404+
copy[key] = value;
2405+
}
2406+
return copy;
2407+
};
2408+
2409+
var invalidValueTypes = [
2410+
'null', 'nan', 'undefined', 'function', 'arguments', 'regexp', 'element'
2411+
];
2412+
2413+
var validatePropertyValue = function(key, value) {
2414+
var valueType = type(value);
2415+
if (invalidValueTypes.indexOf(valueType) !== -1) {
2416+
log('WARNING: Property key "' + key + '" with invalid value type ' + valueType + ', ignoring');
2417+
value = null;
2418+
}
2419+
else if (valueType === 'error') {
2420+
value = String(value);
2421+
log('WARNING: Property key "' + key + '" with value type error, coercing to ' + value);
2422+
}
2423+
else if (valueType === 'array') {
2424+
// check for nested arrays or objects
2425+
var arrayCopy = [];
2426+
for (var i = 0; i < value.length; i++) {
2427+
var element = value[i];
2428+
var elemType = type(element);
2429+
if (elemType === 'array' || elemType === 'object') {
2430+
log('WARNING: Cannot have ' + elemType + ' nested in an array property value, skipping');
2431+
continue;
2432+
}
2433+
arrayCopy.push(validatePropertyValue(key, element));
2434+
}
2435+
value = arrayCopy;
2436+
}
2437+
else if (valueType === 'object') {
2438+
value = validateProperties(value);
2439+
}
2440+
return value;
2441+
};
2442+
2443+
module.exports = {
2444+
log: log,
2445+
isEmptyString: isEmptyString,
2446+
validateProperties: validateProperties
2447+
};
2448+
2449+
}, {"./type":6}],
23712450
14: [function(require, module, exports) {
23722451
/*
23732452
* JavaScript MD5 1.0.1
@@ -3816,16 +3895,6 @@ function isBuffer(obj) {
38163895

38173896
})(this);
38183897

3819-
}, {}],
3820-
7: [function(require, module, exports) {
3821-
var isEmptyString = function(str) {
3822-
return (!str || str.length === 0);
3823-
};
3824-
3825-
module.exports = {
3826-
isEmptyString: isEmptyString
3827-
};
3828-
38293898
}, {}],
38303899
17: [function(require, module, exports) {
38313900
/* jshint bitwise: false, laxbreak: true */

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)