Skip to content

Commit 008d74d

Browse files
committed
validate properties
1 parent c2ef2ef commit 008d74d

File tree

9 files changed

+335
-99
lines changed

9 files changed

+335
-99
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

amplitude.js

Lines changed: 117 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,8 +367,8 @@ 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;
@@ -392,7 +384,7 @@ AmplitudeClient.prototype.init = function(apiKey, opt_userId, opt_config, callba
392384
this._saveReferrer(this._getReferrer());
393385
}
394386
} catch (e) {
395-
log(e);
387+
utils.log(e);
396388
}
397389

398390
if (callback && type(callback) === 'function') {
@@ -404,7 +396,7 @@ AmplitudeClient.prototype.Identify = Identify;
404396

405397
AmplitudeClient.prototype._apiKeySet = function(methodName) {
406398
if (!this.options.apiKey) {
407-
log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName);
399+
utils.log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName);
408400
return false;
409401
}
410402
return true;
@@ -426,7 +418,7 @@ AmplitudeClient.prototype._loadSavedUnsentEvents = function(unsentKey) {
426418
try {
427419
return JSON.parse(savedUnsentEventsString);
428420
} catch (e) {
429-
//log(e);
421+
//utils.log(e);
430422
}
431423
}
432424
return null;
@@ -640,7 +632,7 @@ AmplitudeClient.prototype._saveReferrer = function(referrer) {
640632
hasSessionStorage = true;
641633
}
642634
} catch (e) {
643-
// log(e); // sessionStorage disabled
635+
// utils.log(e); // sessionStorage disabled
644636
}
645637

646638
if ((hasSessionStorage && !(this._getFromStorage(sessionStorage, LocalStorageKeys.REFERRER))) || !hasSessionStorage) {
@@ -663,7 +655,7 @@ AmplitudeClient.prototype.saveEvents = function() {
663655
this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents));
664656
this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys));
665657
} catch (e) {
666-
//log(e);
658+
// utils.log(e);
667659
}
668660
};
669661

@@ -679,9 +671,9 @@ AmplitudeClient.prototype.setDomain = function(domain) {
679671
this.options.domain = this.cookieStorage.options().domain;
680672
_loadCookieData(this);
681673
_saveCookieData(this);
682-
//log('set domain=' + domain);
674+
// utils.log('set domain=' + domain);
683675
} catch (e) {
684-
log(e);
676+
utils.log(e);
685677
}
686678
};
687679

@@ -693,9 +685,9 @@ AmplitudeClient.prototype.setUserId = function(userId) {
693685
try {
694686
this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null;
695687
_saveCookieData(this);
696-
//log('set userId=' + userId);
688+
// utils.log('set userId=' + userId);
697689
} catch (e) {
698-
log(e);
690+
utils.log(e);
699691
}
700692
};
701693

@@ -707,9 +699,9 @@ AmplitudeClient.prototype.setOptOut = function(enable) {
707699
try {
708700
this.options.optOut = enable;
709701
_saveCookieData(this);
710-
//log('set optOut=' + enable);
702+
// utils.log('set optOut=' + enable);
711703
} catch (e) {
712-
log(e);
704+
utils.log(e);
713705
}
714706
};
715707

@@ -724,7 +716,7 @@ AmplitudeClient.prototype.setDeviceId = function(deviceId) {
724716
_saveCookieData(this);
725717
}
726718
} catch (e) {
727-
log(e);
719+
utils.log(e);
728720
}
729721
};
730722

@@ -778,9 +770,9 @@ AmplitudeClient.prototype.identify = function(identify) {
778770
AmplitudeClient.prototype.setVersionName = function(versionName) {
779771
try {
780772
this.options.versionName = versionName;
781-
//log('set versionName=' + versionName);
773+
// utils.log('set versionName=' + versionName);
782774
} catch (e) {
783-
log(e);
775+
utils.log(e);
784776
}
785777
};
786778

@@ -847,7 +839,7 @@ AmplitudeClient.prototype._logEvent = function(eventType, eventProperties, apiPr
847839
}
848840

849841
apiProperties = apiProperties || {};
850-
eventProperties = eventProperties || {};
842+
eventProperties = utils.validateProperties(eventProperties) || {};
851843
var event = {
852844
device_id: this.options.deviceId,
853845
user_id: this.options.userId || this.options.deviceId,
@@ -891,7 +883,7 @@ AmplitudeClient.prototype._logEvent = function(eventType, eventProperties, apiPr
891883

892884
return eventId;
893885
} catch (e) {
894-
log(e);
886+
utils.log(e);
895887
}
896888
};
897889

@@ -918,7 +910,7 @@ var _isNumber = function(n) {
918910
AmplitudeClient.prototype.logRevenue = function(price, quantity, product) {
919911
// Test that the parameters are of the right type.
920912
if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || quantity !== undefined && !_isNumber(quantity)) {
921-
// log('Price and quantity arguments to logRevenue must be numbers');
913+
// utils.log('Price and quantity arguments to logRevenue must be numbers');
922914
return -1;
923915
}
924916

@@ -987,7 +979,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) {
987979
scope._sending = false;
988980
try {
989981
if (status === 200 && response === 'success') {
990-
//log('sucessful upload');
982+
// utils.log('sucessful upload');
991983
scope.removeEvents(maxEventId, maxIdentifyId);
992984

993985
// Update the event cache after the removal of sent events.
@@ -1001,7 +993,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) {
1001993
}
1002994

1003995
} else if (status === 413) {
1004-
//log('request too large');
996+
// utils.log('request too large');
1005997
// Can't even get this one massive event through. Drop it.
1006998
if (scope.options.uploadBatchSize === 1) {
1007999
// if massive event is identify, still need to drop it
@@ -1017,7 +1009,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) {
10171009
callback(status, response);
10181010
}
10191011
} catch (e) {
1020-
//log('failed upload');
1012+
// utils.log('failed upload');
10211013
}
10221014
});
10231015
} else if (callback) {
@@ -2227,6 +2219,7 @@ module.exports = getUtmData;
22272219
}, {}],
22282220
4: [function(require, module, exports) {
22292221
var type = require('./type');
2222+
var utils = require('./utils');
22302223

22312224
/*
22322225
* Wrapper for a user properties JSON object that supports operations.
@@ -2241,14 +2234,6 @@ var AMP_OP_SET = '$set';
22412234
var AMP_OP_SET_ONCE = '$setOnce';
22422235
var AMP_OP_UNSET = '$unset';
22432236

2244-
var log = function(s) {
2245-
try {
2246-
console.log('[Amplitude] ' + s);
2247-
} catch (e) {
2248-
// console logging not available
2249-
}
2250-
};
2251-
22522237
var Identify = function() {
22532238
this.userPropertiesOperations = {};
22542239
this.properties = []; // keep track of keys that have been added
@@ -2258,7 +2243,7 @@ Identify.prototype.add = function(property, value) {
22582243
if (type(value) === 'number' || type(value) === 'string') {
22592244
this._addOperation(AMP_OP_ADD, property, value);
22602245
} else {
2261-
log('Unsupported type for value: ' + type(value) + ', expecting number or string');
2246+
utils.log('Unsupported type for value: ' + type(value) + ', expecting number or string');
22622247
}
22632248
return this;
22642249
};
@@ -2274,7 +2259,7 @@ Identify.prototype.append = function(property, value) {
22742259
Identify.prototype.clearAll = function() {
22752260
if (Object.keys(this.userPropertiesOperations).length > 0) {
22762261
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');
2262+
utils.log('Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll');
22782263
}
22792264
return this;
22802265
}
@@ -2300,13 +2285,13 @@ Identify.prototype.unset = function(property) {
23002285
Identify.prototype._addOperation = function(operation, property, value) {
23012286
// check that the identify doesn't already contain a clearAll
23022287
if (this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) {
2303-
log('This identify already contains a $clearAll operation, skipping operation ' + operation);
2288+
utils.log('This identify already contains a $clearAll operation, skipping operation ' + operation);
23042289
return;
23052290
}
23062291

23072292
// check that property wasn't already used in this Identify
23082293
if (this.properties.indexOf(property) !== -1) {
2309-
log('User property "' + property + '" already used in this identify, skipping operation ' + operation);
2294+
utils.log('User property "' + property + '" already used in this identify, skipping operation ' + operation);
23102295
return;
23112296
}
23122297

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

23202305
module.exports = Identify;
23212306

2322-
}, {"./type":6}],
2307+
}, {"./type":6,"./utils":7}],
23232308
6: [function(require, module, exports) {
23242309
/* Taken from: https://github.com/component/type */
23252310

@@ -2368,6 +2353,95 @@ module.exports = function(val){
23682353
};
23692354

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

38173891
})(this);
38183892

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-
38293893
}, {}],
38303894
17: [function(require, module, exports) {
38313895
/* 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)