diff --git a/CHANGELOG.md b/CHANGELOG.md index fd73a52b..76574ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +* Add tracking of each user's initial_utm parameters (which is captured as a set once operation). Utm parameters are now sent only once per user session. +* Add documentation for SDK functions. You can take a look [here](https://rawgit.com/amplitude/Amplitude-Javascript/defensive_cleanup/documentation/Amplitude.html). A link has also been added to the Readme. + ### 2.10.0 (March 30, 2016) * Identify function now accepts a callback function as an argument. The callback will be run after the identify event is sent. diff --git a/Makefile b/Makefile index b55269ec..b062263f 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ TESTS = $(wildcard test/*.js) BINS = node_modules/.bin DUO = $(BINS)/duo MINIFY = $(BINS)/uglifyjs +JSDOC = $(BINS)/jsdoc JSHINT = $(BINS)/jshint BUILD_DIR = build PROJECT = amplitude @@ -86,6 +87,9 @@ build: $(TESTS) $(OUT) $(SNIPPET_OUT) $(SEGMENT_SNIPPET_OUT) README.md @$(DUO) --development test/tests.js > build/tests.js @$(DUO) --development test/snippet-tests.js > build/snippet-tests.js +docs: + @$(JSDOC) -d ./documentation/ src/*.js + # # Target for release. # diff --git a/README.md b/README.md index 7ea631c6..7608127f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Amplitude-Javascript ==================== +This Readme will guide you through using Amplitude's Javascript SDK to track users and events. You can also take a look at the [SDK documentation](https://rawgit.com/amplitude/Amplitude-Javascript/defensive_cleanup/documentation/Amplitude.html). + # Setup # 1. If you haven't already, go to http://amplitude.com and register for an account. You will receive an API Key. 2. On every page that uses analytics, paste the following Javascript code between the `` and `` tags: @@ -224,18 +226,25 @@ amplitude.init('YOUR_API_KEY_HERE', null, { }); ``` -| option | description | default | +| option | type | description | default | |------------|----------------------------------------------------------------------------------|-----------| -| saveEvents | If `true`, saves events to localStorage and removes them upon successful upload.
NOTE: Without saving events, events may be lost if the user navigates to another page before events are uploaded. | `true` | -| savedMaxCount | Maximum number of events to save in localStorage. If more events are logged while offline, old events are removed. | 1000 | -| uploadBatchSize | Maximum number of events to send to the server per request. | 100 | -| 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` | -| includeReferrer | If `true`, captures the `referrer` and `referring_domain` for each session, as well as the user's `initial_referrer` and `initial_referring_domain` via a set once operation. | `false` | -| 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` | -| eventUploadThreshold | Minimum number of events to batch together per request if `batchEvents` is `true`. | 30 | -| eventUploadPeriodMillis | Amount of time in milliseconds that the SDK waits before uploading events if `batchEvents` is `true`. | 30\*1000 (30 sec) | -| deviceId | Custom device ID to set | Randomly generated UUID | -| sessionTimeout | Time between logged events before a new session starts in milliseconds | 30\*60\*1000 (30 min) | +| batchEvents | Boolean | 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 | `false` | +| cookieExpiration | Number | The number of days after which the Amplitude cookie will expire | 365\*10 (10 years) | +| cookieName | String | Custom name for the Amplitude cookie | `'amplitude_id'` | +| deviceId | String | Custom device ID to set. Note this is not recommended unless you really know what you are doing (like if you have your own system for tracking user devices) | Randomly generated UUID | +| domain | String | Custom cookie domain | The top domain of the current page's url | +| eventUploadPeriodMillis | Number | Amount of time in milliseconds that the SDK waits before uploading events if `batchEvents` is `true` | 30\*1000 (30 sec) | +| eventUploadThreshold | Number | Minimum number of events to batch together per request if `batchEvents` is `true` | 30 | +| includeReferrer | Boolean | If `true`, captures the `referrer` and `referring_domain` for each session, as well as the user's `initial_referrer` and `initial_referring_domain` via a set once operation | `false` | +| includeUtm | Boolean | 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` | +| language | String | Custom language to set | Language determined by browser | +| optOut | Boolean | Whether to opt the current user out of tracking | `false` | +| platform | String | Custom platform to set | `'Web'` | +| savedMaxCount | Number | Maximum number of events to save in localStorage. If more events are logged while offline, old events are removed. | 1000 | +| saveEvents | Boolean | If `true`, saves events to localStorage and removes them upon successful upload.
NOTE: Without saving events, events may be lost if the user navigates to another page before events are uploaded. | `true` | +| sessionTimeout | Number | Time between logged events before a new session starts in milliseconds | 30\*60\*1000 (30 min) | +| uploadBatchSize | Number | Maximum number of events to send to the server per request. | 100 | + # Advanced # This SDK automatically grabs useful data about the browser, including browser type and operating system version. diff --git a/amplitude.js b/amplitude.js index 8656ede5..8a74b8d0 100644 --- a/amplitude.js +++ b/amplitude.js @@ -104,6 +104,7 @@ module.exports = instance; }, {"./amplitude":2}], 2: [function(require, module, exports) { +var Constants = require('./constants'); var cookieStorage = require('./cookiestorage'); var getUtmData = require('./utm'); var Identify = require('./identify'); @@ -119,83 +120,53 @@ var UUID = require('./uuid'); var version = require('./version'); var DEFAULT_OPTIONS = require('./options'); -var IDENTIFY_EVENT = '$identify'; -var API_VERSION = 2; -var MAX_STRING_LENGTH = 1024; -var LocalStorageKeys = { - LAST_EVENT_ID: 'amplitude_lastEventId', - LAST_EVENT_TIME: 'amplitude_lastEventTime', - LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', - LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', - REFERRER: 'amplitude_referrer', - SESSION_ID: 'amplitude_sessionId', - - // Used in cookie as well - DEVICE_ID: 'amplitude_deviceId', - OPT_OUT: 'amplitude_optOut', - USER_ID: 'amplitude_userId' -}; - -/* - * Amplitude API +/** + * Amplitude SDK API - instance constructor. + * @constructor Amplitude + * @public + * @example var amplitude = new Amplitude(); */ -var Amplitude = function() { +var Amplitude = function Amplitude() { this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); this.options = object.merge({}, DEFAULT_OPTIONS); this.cookieStorage = new cookieStorage().getStorage(); this._q = []; // queue for proxied functions before script load + this._sending = false; + this._updateScheduled = false; + + // event meta data + this._eventId = 0; + this._identifyId = 0; + this._lastEventTime = null; + this._newSession = false; + this._sequenceNumber = 0; + this._sessionId = null; }; -Amplitude.prototype._eventId = 0; -Amplitude.prototype._identifyId = 0; -Amplitude.prototype._sequenceNumber = 0; -Amplitude.prototype._sending = false; -Amplitude.prototype._lastEventTime = null; -Amplitude.prototype._sessionId = null; -Amplitude.prototype._newSession = false; -Amplitude.prototype._updateScheduled = false; - Amplitude.prototype.Identify = Identify; /** - * Initializes Amplitude. - * apiKey The API Key for your app - * opt_userId An identifier for this user - * opt_config Configuration options - * - saveEvents (boolean) Whether to save events to local storage. Defaults to true. - * - includeUtm (boolean) Whether to send utm parameters with events. Defaults to false. - * - includeReferrer (boolean) Whether to send referrer info with events. Defaults to false. + * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. + * This is required before any other methods can be called. + * @public + * @param {string} apiKey - The API key for your app. + * @param {string} opt_userId - (optional) An identifier for this user. + * @param {object} opt_config - (optional) Configuration options. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#configuration-options} for list of options and default values. + * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete. + * @example amplitude.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); */ -Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { +Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { + if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { + utils.log('Invalid apiKey. Please re-initialize with a valid apiKey'); + return; + } + try { this.options.apiKey = apiKey; - if (opt_config) { - if (opt_config.saveEvents !== undefined) { - this.options.saveEvents = !!opt_config.saveEvents; - } - if (opt_config.domain !== undefined) { - this.options.domain = opt_config.domain; - } - if (opt_config.includeUtm !== undefined) { - this.options.includeUtm = !!opt_config.includeUtm; - } - if (opt_config.includeReferrer !== undefined) { - this.options.includeReferrer = !!opt_config.includeReferrer; - } - if (opt_config.batchEvents !== undefined) { - this.options.batchEvents = !!opt_config.batchEvents; - } - this.options.platform = opt_config.platform || this.options.platform; - this.options.language = opt_config.language || this.options.language; - this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout; - this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize; - this.options.eventUploadThreshold = opt_config.eventUploadThreshold || this.options.eventUploadThreshold; - this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount; - this.options.eventUploadPeriodMillis = opt_config.eventUploadPeriodMillis || this.options.eventUploadPeriodMillis; - } - + _parseConfig(this.options, opt_config); this.cookieStorage.options({ expirationDays: this.options.cookieExpiration, domain: this.options.domain @@ -205,10 +176,11 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { _upgradeCookeData(this); _loadCookieData(this); - this.options.deviceId = (opt_config && opt_config.deviceId !== undefined && - opt_config.deviceId !== null && opt_config.deviceId) || - this.options.deviceId || UUID(); - this.options.userId = (opt_userId !== undefined && opt_userId !== null && opt_userId) || this.options.userId || null; + // load deviceId and userId from input, or try to fetch existing value from cookie + this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' && + !utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R'; + this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) || + this.options.userId || null; var now = new Date().getTime(); if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) { @@ -218,12 +190,9 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { this._lastEventTime = now; _saveCookieData(this); - //utils.log('initialized with apiKey=' + apiKey); - //opt_userId !== undefined && opt_userId !== null && utils.log('initialized with userId=' + opt_userId); - if (this.options.saveEvents) { - this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey) || this._unsentEvents; - this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey) || this._unsentIdentifys; + this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey); + this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey); // validate event properties for unsent events for (var i = 0; i < this._unsentEvents.length; i++) { @@ -231,7 +200,13 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); } - this._sendEventsIfReady(); + // validate user properties for unsent identifys + for (var j = 0; j < this._unsentIdentifys.length; j++) { + var userProperties = this._unsentIdentifys[j].user_properties; + this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); + } + + this._sendEventsIfReady(); // try sending unsent events } if (this.options.includeUtm) { @@ -243,146 +218,239 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { } } catch (e) { utils.log(e); + } finally { + if (type(opt_callback) === 'function') { + opt_callback(); + } } +}; - if (callback && type(callback) === 'function') { - callback(); +/** + * Parse and validate user specified config values and overwrite existing option value + * DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values + * @private + */ +var _parseConfig = function _parseConfig(options, config) { + if (type(config) !== 'object') { + return; } + + // validates config value is defined, is the correct type, and some additional value sanity checks + var parseValidateAndLoad = function parseValidateAndLoad(key) { + if (!DEFAULT_OPTIONS.hasOwnProperty(key)) { + return; // skip bogus config values + } + + var inputValue = config[key]; + var expectedType = type(DEFAULT_OPTIONS[key]); + if (!utils.validateInput(inputValue, key + ' option', expectedType)) { + return; + } + if (expectedType === 'boolean') { + options[key] = !!inputValue; + } else if ((expectedType === 'string' && !utils.isEmptyString(inputValue)) || + (expectedType === 'number' && inputValue > 0)) { + options[key] = inputValue; + } + }; + + for (var key in config) { + if (config.hasOwnProperty(key)) { + parseValidateAndLoad(key); + } + } }; +/** + * Run functions queued up by proxy loading snippet + * @private + */ Amplitude.prototype.runQueuedFunctions = function () { for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; - if (fn && type(fn) === 'function') { + if (type(fn) === 'function') { fn.apply(this, this._q[i].slice(1)); } } this._q = []; // clear function queue after running }; -Amplitude.prototype._apiKeySet = function(methodName) { - if (!this.options.apiKey) { - utils.log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName); +/** + * Check that the apiKey is set before calling a function. Logs a warning message if not set. + * @private + */ +Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { + if (utils.isEmptyString(this.options.apiKey)) { + utils.log('Invalid apiKey. Please set a valid apiKey with init() before calling ' + methodName); return false; } return true; }; -Amplitude.prototype._loadSavedUnsentEvents = function(unsentKey) { +/** + * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. + * @private + */ +Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); - if (savedUnsentEventsString) { + if (utils.isEmptyString(savedUnsentEventsString)) { + return []; // new app, does not have any saved events + } + + if (type(savedUnsentEventsString) === 'string') { try { - return JSON.parse(savedUnsentEventsString); - } catch (e) { - // utils.log(e); - } + var events = JSON.parse(savedUnsentEventsString); + if (type(events) === 'array') { // handle case where JSON dumping of unsent events is corrupted + return events; + } + } catch (e) {} } - return null; + utils.log('Unable to load ' + unsentKey + ' events. Restart with a new empty queue.'); + return []; }; -Amplitude.prototype.isNewSession = function() { +/** + * Returns true if a new session was created during initialization, otherwise false. + * @public + * @return {boolean} Whether a new session was created during initialization. + */ +Amplitude.prototype.isNewSession = function isNewSession() { return this._newSession; }; -Amplitude.prototype.getSessionId = function() { +/** + * Returns the id of the current session. + * @public + * @return {number} Id of the current session. + */ +Amplitude.prototype.getSessionId = function getSessionId() { return this._sessionId; }; -Amplitude.prototype.nextEventId = function() { +/** + * Increments the eventId and returns it. + * @private + */ +Amplitude.prototype.nextEventId = function nextEventId() { this._eventId++; return this._eventId; }; -Amplitude.prototype.nextIdentifyId = function() { +/** + * Increments the identifyId and returns it. + * @private + */ +Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { this._identifyId++; return this._identifyId; }; -Amplitude.prototype.nextSequenceNumber = function() { +/** + * Increments the sequenceNumber and returns it. + * @private + */ +Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { this._sequenceNumber++; return this._sequenceNumber; }; -// returns the number of unsent events and identifys -Amplitude.prototype._unsentCount = function() { +/** + * Returns the total count of unsent events and identifys + * @private + */ +Amplitude.prototype._unsentCount = function _unsentCount() { return this._unsentEvents.length + this._unsentIdentifys.length; }; -// returns true if sendEvents called immediately -Amplitude.prototype._sendEventsIfReady = function(callback) { +/** + * Send events if ready. Returns true if events are sent. + * @private + */ +Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { if (this._unsentCount() === 0) { return false; } + // if batching disabled, send any unsent events immediately if (!this.options.batchEvents) { this.sendEvents(callback); return true; } + // if batching enabled, check if min threshold met for batch size if (this._unsentCount() >= this.options.eventUploadThreshold) { this.sendEvents(callback); return true; } - if (!this._updateScheduled) { + // otherwise schedule an upload after 30s + if (!this._updateScheduled) { // make sure we only schedule 1 upload this._updateScheduled = true; - setTimeout( - function() { + setTimeout(function() { this._updateScheduled = false; this.sendEvents(); }.bind(this), this.options.eventUploadPeriodMillis ); } - return false; + return false; // an upload was scheduled, no events were uploaded }; -// storage argument allows for localStorage and sessionStorage -Amplitude.prototype._getFromStorage = function(storage, key) { +/** + * Helper function to fetch values from storage + * Storage argument allows for localStoraoge and sessionStoraoge + * @private + */ +Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { return storage.getItem(key); }; -// storage argument allows for localStorage and sessionStorage -Amplitude.prototype._setInStorage = function(storage, key, value) { +/** + * Helper function to set values in storage + * Storage argument allows for localStoraoge and sessionStoraoge + * @private + */ +Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) { storage.setItem(key, value); }; -/* +/** * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber) * can be stored in many different places (localStorage, cookie, etc). - * Need to unify all sources into one place with a one-time upgrade/migration for the defaultInstance. + * Need to unify all sources into one place with a one-time upgrade/migration. + * @private */ -var _upgradeCookeData = function(scope) { +var _upgradeCookeData = function _upgradeCookeData(scope) { // skip if migration already happened var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (cookieData && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { + if (type(cookieData) === 'object' && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { return; } - var _getAndRemoveFromLocalStorage = function(key) { + var _getAndRemoveFromLocalStorage = function _getAndRemoveFromLocalStorage(key) { var value = localStorage.getItem(key); localStorage.removeItem(key); return value; }; // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey - var apiKeySuffix = '_' + scope.options.apiKey.slice(0, 6); - var localStorageDeviceId = _getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID + apiKeySuffix); - var localStorageUserId = _getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID + apiKeySuffix); - var localStorageOptOut = _getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT + apiKeySuffix); + var apiKeySuffix = (type(scope.options.apiKey) === 'string' && ('_' + scope.options.apiKey.slice(0, 6))) || ''; + var localStorageDeviceId = _getAndRemoveFromLocalStorage(Constants.DEVICE_ID + apiKeySuffix); + var localStorageUserId = _getAndRemoveFromLocalStorage(Constants.USER_ID + apiKeySuffix); + var localStorageOptOut = _getAndRemoveFromLocalStorage(Constants.OPT_OUT + apiKeySuffix); if (localStorageOptOut !== null && localStorageOptOut !== undefined) { localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean } // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support - var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID)); - var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME)); - var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID)); - var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID)); - var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER)); - - var _getFromCookie = function(key) { - return cookieData && cookieData[key]; + var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID)); + var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME)); + var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID)); + var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID)); + var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER)); + + var _getFromCookie = function _getFromCookie(key) { + return type(cookieData) === 'object' && cookieData[key]; }; scope.options.deviceId = _getFromCookie('deviceId') || localStorageDeviceId; scope.options.userId = _getFromCookie('userId') || localStorageUserId; @@ -401,9 +469,13 @@ var _upgradeCookeData = function(scope) { _saveCookieData(scope); }; -var _loadCookieData = function(scope) { +/** + * Fetches deviceId, userId, event meta data from amplitude cookie + * @private + */ +var _loadCookieData = function _loadCookieData(scope) { var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (cookieData) { + if (type(cookieData) === 'object') { if (cookieData.deviceId) { scope.options.deviceId = cookieData.deviceId; } @@ -431,7 +503,11 @@ var _loadCookieData = function(scope) { } }; -var _saveCookieData = function(scope) { +/** + * Saves deviceId, userId, event meta data to amplitude cookie + * @private + */ +var _saveCookieData = function _saveCookieData(scope) { scope.cookieStorage.set(scope.options.cookieName, { deviceId: scope.options.deviceId, userId: scope.options.userId, @@ -446,75 +522,110 @@ var _saveCookieData = function(scope) { /** * Parse the utm properties out of cookies and query for adding to user properties. + * @private */ -Amplitude.prototype._initUtmData = function(queryParams, cookieParams) { +Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { queryParams = queryParams || location.search; cookieParams = cookieParams || this.cookieStorage.get('__utmz'); - this._utmProperties = getUtmData(cookieParams, queryParams); + var utmProperties = getUtmData(cookieParams, queryParams); + _sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties); }; -Amplitude.prototype._getReferrer = function() { - return document.referrer; -}; - -Amplitude.prototype._getReferringDomain = function(referrer) { - if (referrer === null || referrer === undefined || referrer === '') { - return null; - } - var parts = referrer.split('/'); - if (parts.length >= 3) { - return parts[2]; - } - return null; -}; - -// since user properties are propagated on the server, only send once per session, don't need to send with every event -Amplitude.prototype._saveReferrer = function(referrer) { - if (referrer === null || referrer === undefined || referrer === '') { +/** + * Since user properties are propagated on server, only send once per session, don't need to send with every event + * @private + */ +var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) { + if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) { return; } - // always setOnce initial referrer - var referring_domain = this._getReferringDomain(referrer); - var identify = new Identify().setOnce('initial_referrer', referrer); - identify.setOnce('initial_referring_domain', referring_domain); - - // only save referrer if not already in session storage or if storage disabled - var hasSessionStorage = false; - try { - if (window.sessionStorage) { - hasSessionStorage = true; + // setOnce the initial user properties + var identify = new Identify(); + for (var key in userProperties) { + if (userProperties.hasOwnProperty(key)) { + identify.setOnce('initial_' + key, userProperties[key]); } - } catch (e) { - // utils.log(e); // sessionStorage disabled } - if ((hasSessionStorage && !(this._getFromStorage(sessionStorage, LocalStorageKeys.REFERRER))) || !hasSessionStorage) { - identify.set('referrer', referrer).set('referring_domain', referring_domain); + // only save userProperties if not already in sessionStorage under key or if storage disabled + var hasSessionStorage = utils.sessionStorageEnabled(); + if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) { + for (var property in userProperties) { + if (userProperties.hasOwnProperty(property)) { + identify.set(property, userProperties[property]); + } + } if (hasSessionStorage) { - this._setInStorage(sessionStorage, LocalStorageKeys.REFERRER, referrer); + scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties)); } } - this.identify(identify); + scope.identify(identify); +}; + +/** + * @private + */ +Amplitude.prototype._getReferrer = function _getReferrer() { + return document.referrer; }; -Amplitude.prototype.saveEvents = function() { - if (!this._apiKeySet('saveEvents()')) { +/** + * Parse the domain from referrer info + * @private + */ +Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) { + if (utils.isEmptyString(referrer)) { + return null; + } + var parts = referrer.split('/'); + if (parts.length >= 3) { + return parts[2]; + } + return null; +}; + +/** + * Fetch the referrer information, parse the domain and send. + * Since user properties are propagated on the server, only send once per session, don't need to send with every event + * @private + */ +Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { + if (utils.isEmptyString(referrer)) { return; } + var referrerInfo = { + 'referrer': referrer, + 'referring_domain': this._getReferringDomain(referrer) + }; + _sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo); +}; +/** + * Saves unsent events and identifies to localStorage. JSON stringifies event queues before saving. + * Note: this is called automatically every time events are logged, unless you explicitly set option saveEvents to false. + * @private + */ +Amplitude.prototype.saveEvents = function saveEvents() { try { this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); + } catch (e) {} + + try { this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); - } catch (e) { - // utils.log(e); - } + } catch (e) {} }; -Amplitude.prototype.setDomain = function(domain) { - if (!this._apiKeySet('setDomain()')) { +/** + * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. + * @public + * @param {string} domain to set. + * @example amplitude.setDomain('.amplitude.com'); + */ +Amplitude.prototype.setDomain = function setDomain(domain) { + if (!utils.validateInput(domain, 'domain', 'string')) { return; } @@ -525,47 +636,60 @@ Amplitude.prototype.setDomain = function(domain) { this.options.domain = this.cookieStorage.options().domain; _loadCookieData(this); _saveCookieData(this); - // utils.log('set domain=' + domain); } catch (e) { utils.log(e); } }; -Amplitude.prototype.setUserId = function(userId) { - if (!this._apiKeySet('setUserId()')) { - return; - } - +/** + * Sets an identifier for the current user. + * @public + * @param {string} userId - identifier to set. Can be null. + * @example amplitude.setUserId('joe@gmail.com'); + */ +Amplitude.prototype.setUserId = function setUserId(userId) { try { this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; _saveCookieData(this); - // utils.log('set userId=' + userId); } catch (e) { utils.log(e); } }; -Amplitude.prototype.setOptOut = function(enable) { - if (!this._apiKeySet('setOptOut()')) { +/** + * Sets whether to opt current user out of tracking. + * @public + * @param {boolean} enable - if true then no events will be logged or sent. + * @example: amplitude.setOptOut(true); + */ +Amplitude.prototype.setOptOut = function setOptOut(enable) { + if (!utils.validateInput(enable, 'enable', 'boolean')) { return; } try { this.options.optOut = enable; _saveCookieData(this); - // utils.log('set optOut=' + enable); } catch (e) { utils.log(e); } }; -Amplitude.prototype.setDeviceId = function(deviceId) { - if (!this._apiKeySet('setDeviceId()')) { +/** + * Sets a custom deviceId for current user. Note: this is not recommended unless you know what you are doing + * (like if you have your own system for managing deviceIds). Make sure the deviceId you set is sufficiently unique + * (we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system. + * @public + * @param {string} deviceId - custom deviceId for current user. + * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); + */ +Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { + if (!utils.validateInput(deviceId, 'deviceId', 'string')) { return; } try { - if (deviceId) { + if (!utils.isEmptyString(deviceId)) { this.options.deviceId = ('' + deviceId); _saveCookieData(this); } @@ -574,8 +698,16 @@ Amplitude.prototype.setDeviceId = function(deviceId) { } }; -Amplitude.prototype.setUserProperties = function(userProperties) { - if (!this._apiKeySet('setUserProperties()')) { +/** + * Sets user properties for the current user. + * @public + * @param {object} - object with string keys and values for the user properties to set. + * @param {boolean} - DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in + * memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated. + * @example amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true}) + */ +Amplitude.prototype.setUserProperties = function setUserProperties(userProperties) { + if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { return; } // convert userProperties into an identify call @@ -588,8 +720,12 @@ Amplitude.prototype.setUserProperties = function(userProperties) { this.identify(identify); }; -// Clearing user properties is irreversible! -Amplitude.prototype.clearUserProperties = function(){ +/** + * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! + * @public + * @example amplitude.clearUserProperties(); + */ +Amplitude.prototype.clearUserProperties = function clearUserProperties(){ if (!this._apiKeySet('clearUserProperties()')) { return; } @@ -599,93 +735,86 @@ Amplitude.prototype.clearUserProperties = function(){ this.identify(identify); }; -Amplitude.prototype.identify = function(identify, callback) { +/** + * Send an identify call containing user property operations to Amplitude servers. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations} + * for more information on the Identify API and user property operations. + * @param {Identify} identify_obj - the Identify object containing the user property operations to send. + * @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent. + * Note: the server response code and response body from the identify event upload are passed to the callback function. + * @example + * var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31'); + * amplitude.identify(identify); + */ +Amplitude.prototype.identify = function(identify_obj, opt_callback) { if (!this._apiKeySet('identify()')) { - if (callback && type(callback) === 'function') { - callback(0, 'No request sent'); + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); } return; } - if (type(identify) === 'object' && '_q' in identify) { + // if identify input is a proxied object created by the async loading snippet, convert it into an identify object + if (type(identify_obj) === 'object' && identify_obj.hasOwnProperty('_q')) { var instance = new Identify(); - // Apply the queued commands - for (var i = 0; i < identify._q.length; i++) { - var fn = instance[identify._q[i][0]]; - if (fn && type(fn) === 'function') { - fn.apply(instance, identify._q[i].slice(1)); + for (var i = 0; i < identify_obj._q.length; i++) { + var fn = instance[identify_obj._q[i][0]]; + if (type(fn) === 'function') { + fn.apply(instance, identify_obj._q[i].slice(1)); } } - identify = instance; + identify_obj = instance; } - if (identify instanceof Identify && Object.keys(identify.userPropertiesOperations).length > 0) { - this._logEvent(IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, callback); - } else if (callback && type(callback) === 'function') { - callback(0, 'No request sent'); - } -}; - -Amplitude.prototype.setVersionName = function(versionName) { - try { - this.options.versionName = versionName; - // utils.log('set versionName=' + versionName); - } catch (e) { - utils.log(e); - } -}; - -// truncate string values in event and user properties so that request size does not get too large -Amplitude.prototype._truncate = function(value) { - if (type(value) === 'array') { - for (var i = 0; i < value.length; i++) { - value[i] = this._truncate(value[i]); - } - } else if (type(value) === 'object') { - for (var key in value) { - if (value.hasOwnProperty(key)) { - value[key] = this._truncate(value[key]); - } + if (identify_obj instanceof Identify) { + // only send if there are operations + if (Object.keys(identify_obj.userPropertiesOperations).length > 0) { + return this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, opt_callback); } } else { - value = _truncateValue(value); + utils.log('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj)); } - return value; + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } }; -var _truncateValue = function(value) { - if (type(value) === 'string') { - return value.length > MAX_STRING_LENGTH ? value.substring(0, MAX_STRING_LENGTH) : value; +/** + * Set a versionName for your application. + * @public + * @param {string} versionName + * @example amplitude.setVersionName('1.12.3'); + */ +Amplitude.prototype.setVersionName = function setVersionName(versionName) { + if (!utils.validateInput(versionName, 'versionName', 'string')) { + return; } - return value; + this.options.versionName = versionName; }; /** * Private logEvent method. Keeps apiProperties from being publicly exposed. + * @private */ -Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperties, userProperties, callback) { - if (type(callback) !== 'function') { - callback = null; - } - - _loadCookieData(this); +Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, callback) { + _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs if (!eventType || this.options.optOut) { - if (callback) { + if (type(callback) === 'function') { callback(0, 'No request sent'); } return; } + try { var eventId; - if (eventType === IDENTIFY_EVENT) { + if (eventType === Constants.IDENTIFY_EVENT) { eventId = this.nextIdentifyId(); } else { eventId = this.nextEventId(); } var sequenceNumber = this.nextSequenceNumber(); var eventTime = new Date().getTime(); - var ua = this._ua; if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { this._sessionId = eventTime; } @@ -693,29 +822,24 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti _saveCookieData(this); userProperties = userProperties || {}; - // Only add utm properties to user properties for events - if (eventType !== IDENTIFY_EVENT) { - object.merge(userProperties, this._utmProperties); - } - apiProperties = apiProperties || {}; eventProperties = eventProperties || {}; var event = { device_id: this.options.deviceId, - user_id: this.options.userId || this.options.deviceId, + user_id: this.options.userId, timestamp: eventTime, event_id: eventId, session_id: this._sessionId || -1, event_type: eventType, version_name: this.options.versionName || null, platform: this.options.platform, - os_name: ua.browser.name || null, - os_version: ua.browser.major || null, - device_model: ua.os.name || null, + os_name: this._ua.browser.name || null, + os_version: this._ua.browser.major || null, + device_model: this._ua.os.name || null, language: this.options.language, api_properties: apiProperties, - event_properties: this._truncate(utils.validateProperties(eventProperties)), - user_properties: this._truncate(userProperties), + event_properties: utils.truncate(utils.validateProperties(eventProperties)), + user_properties: utils.truncate(utils.validateProperties(userProperties)), uuid: UUID(), library: { name: 'amplitude-js', @@ -725,7 +849,7 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti // country: null }; - if (eventType === IDENTIFY_EVENT) { + if (eventType === Constants.IDENTIFY_EVENT) { this._unsentIdentifys.push(event); this._limitEventsQueued(this._unsentIdentifys); } else { @@ -737,7 +861,7 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti this.saveEvents(); } - if (!this._sendEventsIfReady(callback) && callback) { + if (!this._sendEventsIfReady(callback) && type(callback) === 'function') { callback(0, 'No request sent'); } @@ -747,32 +871,63 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti } }; -// Remove old events from the beginning of the array if too many -// have accumulated. Don't want to kill memory. Default is 1000 events. -Amplitude.prototype._limitEventsQueued = function(queue) { +/** + * Remove old events from the beginning of the array if too many have accumulated. Default limit is 1000 events. + * @private + */ +Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { if (queue.length > this.options.savedMaxCount) { queue.splice(0, queue.length - this.options.savedMaxCount); } }; -Amplitude.prototype.logEvent = function(eventType, eventProperties, callback) { - if (!this._apiKeySet('logEvent()')) { - if (callback && type(callback) === 'function') { - callback(0, 'No request sent'); +/** + * This is the callback for logEvent and identify calls. It gets called after the event/identify is uploaded, + * and the server response code and response body from the upload request are passed to the callback function. + * @callback Amplitude~eventCallback + * @param {number} responseCode - Server response code for the event / identify upload request. + * @param {string} responseBody - Server response body for the event / identify upload request. + */ + +/** + * Log an event with eventType and eventProperties + * @public + * @param {string} eventType - name of event + * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. + * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. + * Note: the server response code and response body from the event upload are passed to the callback function. + * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); + */ +Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { + if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || + utils.isEmptyString(eventType)) { + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); } return -1; } - return this._logEvent(eventType, eventProperties, null, null, callback); + return this._logEvent(eventType, eventProperties, null, null, opt_callback); }; -// Test that n is a number or a numeric value. -var _isNumber = function(n) { +/** + * Test that n is a number or a numeric value. + * @private + */ +var _isNumber = function _isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; -Amplitude.prototype.logRevenue = function(price, quantity, product) { +/** + * Log revenue event with a price, quantity, and product identifier. + * @public + * @param {number} price - price of revenue event + * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1. + * @param {string} product - (optional) product identifier + * @example amplitude.logRevenue(3.99, 1, 'product_1234'); + */ +Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { // Test that the parameters are of the right type. - if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || quantity !== undefined && !_isNumber(quantity)) { + if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { // utils.log('Price and quantity arguments to logRevenue must be numbers'); return -1; } @@ -786,104 +941,110 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) { }; /** - * Remove events in storage with event ids up to and including maxEventId. Does - * a true filter in case events get out of order or old events are removed. + * Remove events in storage with event ids up to and including maxEventId. + * @private */ -Amplitude.prototype.removeEvents = function (maxEventId, maxIdentifyId) { - if (maxEventId >= 0) { - var filteredEvents = []; - for (var i = 0; i < this._unsentEvents.length; i++) { - if (this._unsentEvents[i].event_id > maxEventId) { - filteredEvents.push(this._unsentEvents[i]); - } - } - this._unsentEvents = filteredEvents; +Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { + _removeEvents(this, '_unsentEvents', maxEventId); + _removeEvents(this, '_unsentIdentifys', maxIdentifyId); +}; + +/** + * Helper function to remove events up to maxId from a single queue. + * Does a true filter in case events get out of order or old events are removed. + * @private + */ +var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { + if (maxId < 0) { + return; } - if (maxIdentifyId >= 0) { - var filteredIdentifys = []; - for (var j = 0; j < this._unsentIdentifys.length; j++) { - if (this._unsentIdentifys[j].event_id > maxIdentifyId) { - filteredIdentifys.push(this._unsentIdentifys[j]); - } + var filteredEvents = []; + for (var i = 0; i < scope[eventQueue].length || 0; i++) { + if (scope[eventQueue][i].event_id > maxId) { + filteredEvents.push(scope[eventQueue][i]); } - this._unsentIdentifys = filteredIdentifys; } + scope[eventQueue] = filteredEvents; }; -Amplitude.prototype.sendEvents = function(callback) { - if (!this._apiKeySet('sendEvents()')) { - if (callback && type(callback) === 'function') { +/** + * Send unsent events. Note: this is called automatically after events are logged if option batchEvents is false. + * If batchEvents is true, then events are only sent when batch criterias are met. + * @private + * @param {Amplitude~eventCallback} callback - (optional) callback to run after events are sent. + * Note the server response code and response body are passed to the callback as input arguments. + */ +Amplitude.prototype.sendEvents = function sendEvents(callback) { + if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) { + if (type(callback) === 'function') { callback(0, 'No request sent'); } return; } - if (!this._sending && !this.options.optOut && this._unsentCount() > 0) { - this._sending = true; - var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + - this.options.apiEndpoint + '/'; - - // fetch events to send - var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); - var mergedEvents = this._mergeEventsAndIdentifys(numEvents); - var maxEventId = mergedEvents.maxEventId; - var maxIdentifyId = mergedEvents.maxIdentifyId; - var events = JSON.stringify(mergedEvents.eventsToSend); - - var uploadTime = new Date().getTime(); - var data = { - client: this.options.apiKey, - e: events, - v: API_VERSION, - upload_time: uploadTime, - checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime) - }; + this._sending = true; + var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + this.options.apiEndpoint + '/'; + + // fetch events to send + var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); + var mergedEvents = this._mergeEventsAndIdentifys(numEvents); + var maxEventId = mergedEvents.maxEventId; + var maxIdentifyId = mergedEvents.maxIdentifyId; + var events = JSON.stringify(mergedEvents.eventsToSend); + var uploadTime = new Date().getTime(); + + var data = { + client: this.options.apiKey, + e: events, + v: Constants.API_VERSION, + upload_time: uploadTime, + checksum: md5(Constants.API_VERSION + this.options.apiKey + events + uploadTime) + }; - var scope = this; - new Request(url, data).send(function(status, response) { - scope._sending = false; - try { - if (status === 200 && response === 'success') { - // utils.log('sucessful upload'); - scope.removeEvents(maxEventId, maxIdentifyId); + var scope = this; + new Request(url, data).send(function(status, response) { + scope._sending = false; + try { + if (status === 200 && response === 'success') { + scope.removeEvents(maxEventId, maxIdentifyId); - // Update the event cache after the removal of sent events. - if (scope.options.saveEvents) { - scope.saveEvents(); - } + // Update the event cache after the removal of sent events. + if (scope.options.saveEvents) { + scope.saveEvents(); + } - // Send more events if any queued during previous send. - if (!scope._sendEventsIfReady(callback) && callback) { - callback(status, response); - } + // Send more events if any queued during previous send. + if (!scope._sendEventsIfReady(callback) && type(callback) === 'function') { + callback(status, response); + } - } else if (status === 413) { - // utils.log('request too large'); - // Can't even get this one massive event through. Drop it. - if (scope.options.uploadBatchSize === 1) { - // if massive event is identify, still need to drop it - scope.removeEvents(maxEventId, maxIdentifyId); - } + // handle payload too large + } else if (status === 413) { + // utils.log('request too large'); + // Can't even get this one massive event through. Drop it, even if it is an identify. + if (scope.options.uploadBatchSize === 1) { + scope.removeEvents(maxEventId, maxIdentifyId); + } - // The server complained about the length of the request. - // Backoff and try again. - scope.options.uploadBatchSize = Math.ceil(numEvents / 2); - scope.sendEvents(callback); + // The server complained about the length of the request. Backoff and try again. + scope.options.uploadBatchSize = Math.ceil(numEvents / 2); + scope.sendEvents(callback); - } else if (callback) { // If server turns something like a 400 - callback(status, response); - } - } catch (e) { - // utils.log('failed upload'); + } else if (type(callback) === 'function') { // If server turns something like a 400 + callback(status, response); } - }); - } else if (callback) { - callback(0, 'No request sent'); - } + } catch (e) { + // utils.log('failed upload'); + } + }); }; -Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { +/** + * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. + * @private + */ +Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { // coalesce events from both queues var eventsToSend = []; var eventIndex = 0; @@ -893,14 +1054,23 @@ Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { while (eventsToSend.length < numEvents) { var event; + var noIdentifys = identifyIndex >= this._unsentIdentifys.length; + var noEvents = eventIndex >= this._unsentEvents.length; + + // case 0: no events or identifys left + // note this should not happen, this means we have less events and identifys than expected + if (noEvents && noIdentifys) { + utils.log('Merging Events and Identifys, less events and identifys than expected'); + break; + } // case 1: no identifys - grab from events - if (identifyIndex >= this._unsentIdentifys.length) { + else if (noIdentifys) { event = this._unsentEvents[eventIndex++]; maxEventId = event.event_id; // case 2: no events - grab from identifys - } else if (eventIndex >= this._unsentEvents.length) { + } else if (noEvents) { event = this._unsentIdentifys[identifyIndex++]; maxIdentifyId = event.event_id; @@ -929,16 +1099,48 @@ Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { }; /** - * @deprecated + * Set global user properties. Note this is deprecated, and we recommend using setUserProperties + * @public + * @deprecated */ -Amplitude.prototype.setGlobalUserProperties = Amplitude.prototype.setUserProperties; +Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { + this.setUserProperties(userProperties); +}; +/** + * Get the current version of Amplitude's Javascript SDK. + * @public + * @returns {number} version number + * @example var amplitudeVersion = amplitude.__VERSION__; + */ Amplitude.prototype.__VERSION__ = version; module.exports = Amplitude; -}, {"./cookiestorage":3,"./utm":4,"./identify":5,"json":6,"./localstorage":7,"JavaScript-MD5":8,"object":9,"./xhr":10,"./type":11,"ua-parser-js":12,"./utils":13,"./uuid":14,"./version":15,"./options":16}], +}, {"./constants":3,"./cookiestorage":4,"./utm":5,"./identify":6,"json":7,"./localstorage":8,"JavaScript-MD5":9,"object":10,"./xhr":11,"./type":12,"ua-parser-js":13,"./utils":14,"./uuid":15,"./version":16,"./options":17}], 3: [function(require, module, exports) { +module.exports = { + API_VERSION: 2, + MAX_STRING_LENGTH: 4096, + IDENTIFY_EVENT: '$identify', + + // localStorageKeys + LAST_EVENT_ID: 'amplitude_lastEventId', + LAST_EVENT_TIME: 'amplitude_lastEventTime', + LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', + LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', + REFERRER: 'amplitude_referrer', + SESSION_ID: 'amplitude_sessionId', + UTM_PROPERTIES: 'amplitude_utm_properties', + + // Used in cookie as well + DEVICE_ID: 'amplitude_deviceId', + OPT_OUT: 'amplitude_optOut', + USER_ID: 'amplitude_userId' +}; + +}, {}], +4: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -1031,8 +1233,8 @@ cookieStorage.prototype.getStorage = function() { module.exports = cookieStorage; -}, {"./cookie":17,"json":6,"./localstorage":7}], -17: [function(require, module, exports) { +}, {"./cookie":18,"json":7,"./localstorage":8}], +18: [function(require, module, exports) { /* * Cookie data */ @@ -1040,6 +1242,7 @@ module.exports = cookieStorage; var Base64 = require('./base64'); var JSON = require('json'); // jshint ignore:line var topDomain = require('top-domain'); +var utils = require('./utils'); var _options = { @@ -1065,7 +1268,7 @@ var options = function(opts) { _options.expirationDays = opts.expirationDays; - var domain = (opts.domain !== undefined) ? opts.domain : '.' + topDomain(window.location.href); + var domain = (!utils.isEmptyString(opts.domain)) ? opts.domain : '.' + topDomain(window.location.href); var token = Math.random(); _options.domain = domain; set('amplitude_test', token); @@ -1161,8 +1364,8 @@ module.exports = { }; -}, {"./base64":18,"json":6,"top-domain":19}], -18: [function(require, module, exports) { +}, {"./base64":19,"json":7,"top-domain":20,"./utils":14}], +19: [function(require, module, exports) { /* jshint bitwise: false */ /* global escape, unescape */ @@ -1261,8 +1464,8 @@ var Base64 = { module.exports = Base64; -}, {"./utf8":20}], -20: [function(require, module, exports) { +}, {"./utf8":21}], +21: [function(require, module, exports) { /* jshint bitwise: false */ /* @@ -1322,7 +1525,7 @@ var UTF8 = { module.exports = UTF8; }, {}], -6: [function(require, module, exports) { +7: [function(require, module, exports) { var json = window.JSON || {}; var stringify = json.stringify; @@ -1332,8 +1535,8 @@ module.exports = parse && stringify ? JSON : require('json-fallback'); -}, {"json-fallback":21}], -21: [function(require, module, exports) { +}, {"json-fallback":22}], +22: [function(require, module, exports) { /* json2.js 2014-02-04 @@ -1823,7 +2026,7 @@ module.exports = parse && stringify }()); }, {}], -19: [function(require, module, exports) { +20: [function(require, module, exports) { /** * Module dependencies. @@ -1871,8 +2074,8 @@ function domain(url){ return match ? match[0] : ''; }; -}, {"url":22}], -22: [function(require, module, exports) { +}, {"url":23}], +23: [function(require, module, exports) { /** * Parse the given `url`. @@ -1898,66 +2101,244 @@ exports.parse = function(url){ }; }; -/** - * Check if `url` is absolute. - * - * @param {String} url - * @return {Boolean} - * @api public - */ - -exports.isAbsolute = function(url){ - return 0 == url.indexOf('//') || !!~url.indexOf('://'); +/** + * Check if `url` is absolute. + * + * @param {String} url + * @return {Boolean} + * @api public + */ + +exports.isAbsolute = function(url){ + return 0 == url.indexOf('//') || !!~url.indexOf('://'); +}; + +/** + * Check if `url` is relative. + * + * @param {String} url + * @return {Boolean} + * @api public + */ + +exports.isRelative = function(url){ + return !exports.isAbsolute(url); +}; + +/** + * Check if `url` is cross domain. + * + * @param {String} url + * @return {Boolean} + * @api public + */ + +exports.isCrossDomain = function(url){ + url = exports.parse(url); + var location = exports.parse(window.location.href); + return url.hostname !== location.hostname + || url.port !== location.port + || url.protocol !== location.protocol; +}; + +/** + * Return default port for `protocol`. + * + * @param {String} protocol + * @return {String} + * @api private + */ +function port (protocol){ + switch (protocol) { + case 'http:': + return 80; + case 'https:': + return 443; + default: + return location.port; + } +} + +}, {}], +14: [function(require, module, exports) { +var constants = require('./constants'); +var type = require('./type'); + +var log = function log(s) { + try { + console.log('[Amplitude] ' + s); + } catch (e) { + // console logging not available + } +}; + +var isEmptyString = function isEmptyString(str) { + return (!str || str.length === 0); +}; + +var sessionStorageEnabled = function sessionStorageEnabled() { + try { + if (window.sessionStorage) { + return true; + } + } catch (e) {} // sessionStorage disabled + return false; +}; + +// truncate string values in event and user properties so that request size does not get too large +var truncate = function truncate(value) { + if (type(value) === 'array') { + for (var i = 0; i < value.length; i++) { + value[i] = truncate(value[i]); + } + } else if (type(value) === 'object') { + for (var key in value) { + if (value.hasOwnProperty(key)) { + value[key] = truncate(value[key]); + } + } + } else { + value = _truncateValue(value); + } + + return value; +}; + +var _truncateValue = function _truncateValue(value) { + if (type(value) === 'string') { + return value.length > constants.MAX_STRING_LENGTH ? value.substring(0, constants.MAX_STRING_LENGTH) : value; + } + return value; +}; + +var validateInput = function validateInput(input, name, expectedType) { + if (type(input) !== expectedType) { + log('Invalid ' + name + ' input type. Expected ' + expectedType + ' but received ' + type(input)); + return false; + } + return true; +}; + +var validateProperties = function validateProperties(properties) { + var propsType = type(properties); + if (propsType !== 'object') { + log('Error: invalid event properties format. Expecting Javascript object, received ' + propsType + ', ignoring'); + return {}; + } + + var copy = {}; // create a copy with all of the valid properties + for (var property in properties) { + if (!properties.hasOwnProperty(property)) { + continue; + } + + // validate key + var key = property; + var keyType = type(key); + if (keyType !== 'string') { + log('WARNING: Non-string property key, received type ' + keyType + ', coercing to string "' + key + '"'); + key = String(key); + } + + // validate value + var value = validatePropertyValue(key, properties[property]); + if (value === null) { + continue; + } + copy[key] = value; + } + return copy; +}; + +var invalidValueTypes = [ + 'null', 'nan', 'undefined', 'function', 'arguments', 'regexp', 'element' +]; + +var validatePropertyValue = function validatePropertyValue(key, value) { + var valueType = type(value); + if (invalidValueTypes.indexOf(valueType) !== -1) { + log('WARNING: Property key "' + key + '" with invalid value type ' + valueType + ', ignoring'); + value = null; + } else if (valueType === 'error') { + value = String(value); + log('WARNING: Property key "' + key + '" with value type error, coercing to ' + value); + } else if (valueType === 'array') { + // check for nested arrays or objects + var arrayCopy = []; + for (var i = 0; i < value.length; i++) { + var element = value[i]; + var elemType = type(element); + if (elemType === 'array' || elemType === 'object') { + log('WARNING: Cannot have ' + elemType + ' nested in an array property value, skipping'); + continue; + } + arrayCopy.push(validatePropertyValue(key, element)); + } + value = arrayCopy; + } else if (valueType === 'object') { + value = validateProperties(value); + } + return value; +}; + +module.exports = { + log: log, + isEmptyString: isEmptyString, + sessionStorageEnabled: sessionStorageEnabled, + truncate: truncate, + validateInput: validateInput, + validateProperties: validateProperties }; +}, {"./constants":3,"./type":12}], +12: [function(require, module, exports) { /** - * Check if `url` is relative. - * - * @param {String} url - * @return {Boolean} - * @api public + * toString ref. + * @private */ -exports.isRelative = function(url){ - return !exports.isAbsolute(url); -}; +var toString = Object.prototype.toString; /** - * Check if `url` is cross domain. - * - * @param {String} url - * @return {Boolean} + * Return the type of `val`. + * @private + * @param {Mixed} val + * @return {String} * @api public */ -exports.isCrossDomain = function(url){ - url = exports.parse(url); - var location = exports.parse(window.location.href); - return url.hostname !== location.hostname - || url.port !== location.port - || url.protocol !== location.protocol; -}; +module.exports = function(val){ + switch (toString.call(val)) { + case '[object Date]': return 'date'; + case '[object RegExp]': return 'regexp'; + case '[object Arguments]': return 'arguments'; + case '[object Array]': return 'array'; + case '[object Error]': return 'error'; + } -/** - * Return default port for `protocol`. - * - * @param {String} protocol - * @return {String} - * @api private - */ -function port (protocol){ - switch (protocol) { - case 'http:': - return 80; - case 'https:': - return 443; - default: - return location.port; + if (val === null) { + return 'null'; } -} + if (val === undefined) { + return 'undefined'; + } + if (val !== val) { + return 'nan'; + } + if (val && val.nodeType === 1) { + return 'element'; + } + + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(val)) { + return 'buffer'; + } + + val = val.valueOf ? val.valueOf() : Object.prototype.valueOf.apply(val); + return typeof val; +}; }, {}], -7: [function(require, module, exports) { +8: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -2061,7 +2442,7 @@ if (!localStorage) { module.exports = localStorage; }, {}], -4: [function(require, module, exports) { +5: [function(require, module, exports) { var getUtmParam = function(name, query) { name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); @@ -2090,7 +2471,7 @@ var getUtmData = function(rawCookie, query) { module.exports = getUtmData; }, {}], -5: [function(require, module, exports) { +6: [function(require, module, exports) { var type = require('./type'); var utils = require('./utils'); @@ -2108,11 +2489,33 @@ var AMP_OP_SET = '$set'; var AMP_OP_SET_ONCE = '$setOnce'; var AMP_OP_UNSET = '$unset'; +/** + * Identify API - instance constructor. Identify objects are a wrapper for user property operations. + * Each method adds a user property operation to the Identify object, and returns the same Identify object, + * allowing you to chain multiple method calls together. + * Note: if the same user property is used in multiple operations on a single Identify object, + * only the first operation on that property will be saved, and the rest will be ignored. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations} + * for more information on the Identify API and user property operations. + * @constructor Identify + * @public + * @example var identify = new amplitude.Identify(); + */ var Identify = function() { this.userPropertiesOperations = {}; this.properties = []; // keep track of keys that have been added }; +/** + * Increment a user property by a given value (can also be negative to decrement). + * If the user property does not have a value set yet, it will be initialized to 0 before being incremented. + * @public + * @param {string} property - The user property key. + * @param {number|string} value - The amount by which to increment the user property. Allows numbers as strings (ex: '123'). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().add('karma', 1).add('friends', 1); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.add = function(property, value) { if (type(value) === 'number' || type(value) === 'string') { this._addOperation(AMP_OP_ADD, property, value); @@ -2122,14 +2525,33 @@ Identify.prototype.add = function(property, value) { return this; }; +/** + * Append a value or values to a user property. + * If the user property does not have a value set yet, + * it will be initialized to an empty list before the new values are appended. + * If the user property has an existing value and it is not a list, + * the existing value will be converted into a list with the new values appended. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to append. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().append('ab-tests', 'new-user-tests'); + * identify.append('some_list', [1, 2, 3, 4, 'values']); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.append = function(property, value) { this._addOperation(AMP_OP_APPEND, property, value); return this; }; -// clearAll should be sent on its own Identify object -// If there are already other operations, then don't add clearAll -// If clearAll already in Identify, don't add other operations +/** + * Clear all user properties for the current user. + * SDK user should instead call amplitude.clearUserProperties() instead of using this. + * $clearAll needs to be sent on its own Identify object. If there are already other operations, then don't add $clearAll. + * If $clearAll already in an Identify object, don't allow other operations to be added. + * @private + */ Identify.prototype.clearAll = function() { if (Object.keys(this.userPropertiesOperations).length > 0) { if (!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) { @@ -2141,26 +2563,78 @@ Identify.prototype.clearAll = function() { return this; }; +/** + * Prepend a value or values to a user property. + * Prepend means inserting the value or values at the front of a list. + * If the user property does not have a value set yet, + * it will be initialized to an empty list before the new values are prepended. + * If the user property has an existing value and it is not a list, + * the existing value will be converted into a list with the new values prepended. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to prepend. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-tests'); + * identify.prepend('some_list', [1, 2, 3, 4, 'values']); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.prepend = function(property, value) { this._addOperation(AMP_OP_PREPEND, property, value); return this; }; +/** + * Sets the value of a given user property. If a value already exists, it will be overwriten with the new value. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to set. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().set('user_type', 'beta'); + * identify.set('name', {'first': 'John', 'last': 'Doe'}); // dict is flattened and becomes name.first: John, name.last: Doe + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.set = function(property, value) { this._addOperation(AMP_OP_SET, property, value); return this; }; +/** + * Sets the value of a given user property only once. Subsequent setOnce operations on that user property will be ignored; + * however, that user property can still be modified through any of the other operations. + * Useful for capturing properties such as 'initial_signup_date', 'initial_referrer', etc. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to set once. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().setOnce('sign_up_date', '2016-04-01'); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.setOnce = function(property, value) { this._addOperation(AMP_OP_SET_ONCE, property, value); return this; }; +/** + * Unset and remove a user property. This user property will no longer show up in a user's profile. + * @public + * @param {string} property - The user property key. + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().unset('user_type').unset('age'); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.unset = function(property) { this._addOperation(AMP_OP_UNSET, property, '-'); return this; }; +/** + * Helper function that adds operation to the Identify's object + * Handle's filtering of duplicate user property keys, and filtering for clearAll. + * @private + */ Identify.prototype._addOperation = function(operation, property, value) { // check that the identify doesn't already contain a clearAll if (this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) { @@ -2183,140 +2657,8 @@ Identify.prototype._addOperation = function(operation, property, value) { module.exports = Identify; -}, {"./type":11,"./utils":13}], -11: [function(require, module, exports) { -/* Taken from: https://github.com/component/type */ - -/** - * toString ref. - */ - -var toString = Object.prototype.toString; - -/** - * Return the type of `val`. - * - * @param {Mixed} val - * @return {String} - * @api public - */ - -module.exports = function(val){ - switch (toString.call(val)) { - case '[object Date]': return 'date'; - case '[object RegExp]': return 'regexp'; - case '[object Arguments]': return 'arguments'; - case '[object Array]': return 'array'; - case '[object Error]': return 'error'; - } - - if (val === null) { - return 'null'; - } - if (val === undefined) { - return 'undefined'; - } - if (val !== val) { - return 'nan'; - } - if (val && val.nodeType === 1) { - return 'element'; - } - - if (typeof Buffer !== 'undefined' && Buffer.isBuffer(val)) { - return 'buffer'; - } - - val = val.valueOf ? val.valueOf() : Object.prototype.valueOf.apply(val); - return typeof val; -}; - -}, {}], -13: [function(require, module, exports) { -var type = require('./type'); - -var log = function(s) { - try { - console.log('[Amplitude] ' + s); - } catch (e) { - // console logging not available - } -}; - -var isEmptyString = function(str) { - return (!str || str.length === 0); -}; - -var validateProperties = function(properties) { - var propsType = type(properties); - if (propsType !== 'object') { - log('Error: invalid event properties format. Expecting Javascript object, received ' + propsType + ', ignoring'); - return {}; - } - - var copy = {}; // create a copy with all of the valid properties - for (var property in properties) { - if (!properties.hasOwnProperty(property)) { - continue; - } - - // validate key - var key = property; - var keyType = type(key); - if (keyType !== 'string') { - log('WARNING: Non-string property key, received type ' + keyType + ', coercing to string "' + key + '"'); - key = String(key); - } - - // validate value - var value = validatePropertyValue(key, properties[property]); - if (value === null) { - continue; - } - copy[key] = value; - } - return copy; -}; - -var invalidValueTypes = [ - 'null', 'nan', 'undefined', 'function', 'arguments', 'regexp', 'element' -]; - -var validatePropertyValue = function(key, value) { - var valueType = type(value); - if (invalidValueTypes.indexOf(valueType) !== -1) { - log('WARNING: Property key "' + key + '" with invalid value type ' + valueType + ', ignoring'); - value = null; - } else if (valueType === 'error') { - value = String(value); - log('WARNING: Property key "' + key + '" with value type error, coercing to ' + value); - } else if (valueType === 'array') { - // check for nested arrays or objects - var arrayCopy = []; - for (var i = 0; i < value.length; i++) { - var element = value[i]; - var elemType = type(element); - if (elemType === 'array' || elemType === 'object') { - log('WARNING: Cannot have ' + elemType + ' nested in an array property value, skipping'); - continue; - } - arrayCopy.push(validatePropertyValue(key, element)); - } - value = arrayCopy; - } else if (valueType === 'object') { - value = validateProperties(value); - } - return value; -}; - -module.exports = { - log: log, - isEmptyString: isEmptyString, - validateProperties: validateProperties -}; - -}, {"./type":11}], -8: [function(require, module, exports) { +}, {"./type":12,"./utils":14}], +9: [function(require, module, exports) { /* * JavaScript MD5 1.0.1 * https://github.com/blueimp/JavaScript-MD5 @@ -2604,7 +2946,7 @@ module.exports = { }(this)); }, {}], -9: [function(require, module, exports) { +10: [function(require, module, exports) { /** * HOP ref. @@ -2690,7 +3032,7 @@ exports.isEmpty = function(obj){ return 0 == exports.length(obj); }; }, {}], -10: [function(require, module, exports) { +11: [function(require, module, exports) { var querystring = require('querystring'); /* @@ -2736,8 +3078,8 @@ Request.prototype.send = function(callback) { module.exports = Request; -}, {"querystring":23}], -23: [function(require, module, exports) { +}, {"querystring":24}], +24: [function(require, module, exports) { /** * Module dependencies. @@ -2812,8 +3154,8 @@ exports.stringify = function(obj){ return pairs.join('&'); }; -}, {"trim":24,"type":25}], -24: [function(require, module, exports) { +}, {"trim":25,"type":26}], +25: [function(require, module, exports) { exports = module.exports = trim; @@ -2833,7 +3175,7 @@ exports.right = function(str){ }; }, {}], -25: [function(require, module, exports) { +26: [function(require, module, exports) { /** * toString ref. */ @@ -2882,7 +3224,7 @@ function isBuffer(obj) { } }, {}], -12: [function(require, module, exports) { +13: [function(require, module, exports) { /* jshint eqeqeq: false, forin: false */ /* global define */ @@ -3765,17 +4107,17 @@ function isBuffer(obj) { })(this); }, {}], -14: [function(require, module, exports) { +15: [function(require, module, exports) { /* jshint bitwise: false, laxbreak: true */ /** - * Taken straight from jed's gist: https://gist.github.com/982883 - * + * Source: [jed's gist]{@link https://gist.github.com/982883}. * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, * where each x is replaced with a random hexadecimal digit from 0 to f, and * y is replaced with a random hexadecimal digit from 8 to b. + * Used to generate UUIDs for deviceIds. + * @private */ - var uuid = function(a) { return a // if the placeholder was passed, return ? ( // a random number from 0 to 15 @@ -3799,11 +4141,11 @@ var uuid = function(a) { module.exports = uuid; }, {}], -15: [function(require, module, exports) { +16: [function(require, module, exports) { module.exports = '2.10.0'; }, {}], -16: [function(require, module, exports) { +17: [function(require, module, exports) { var language = require('./language'); // default options @@ -3811,7 +4153,8 @@ module.exports = { apiEndpoint: 'api.amplitude.com', cookieExpiration: 365 * 10, cookieName: 'amplitude_id', - domain: undefined, + domain: '', + includeReferrer: false, includeUtm: false, language: language.language, optOut: false, @@ -3827,8 +4170,8 @@ module.exports = { eventUploadPeriodMillis: 30 * 1000, // 30s }; -}, {"./language":26}], -26: [function(require, module, exports) { +}, {"./language":27}], +27: [function(require, module, exports) { var getLanguage = function() { return (navigator && ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage)) || undefined; diff --git a/amplitude.min.js b/amplitude.min.js index a143bbd1..5cadd1a9 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,2 +1,2 @@ -(function umd(require){if("object"==typeof exports){module.exports=require("1")}else if("function"==typeof define&&define.amd){define(function(){return require("1")})}else{this["amplitude"]=require("1")}})(function outer(modules,cache,entries){var global=function(){return this}();function require(name,jumped){if(cache[name])return cache[name].exports;if(modules[name])return call(name,require);throw new Error('cannot find module "'+name+'"')}function call(id,require){var m=cache[id]={exports:{}};var mod=modules[id];var name=mod[2];var fn=mod[0];fn.call(m.exports,function(req){var dep=modules[id][1][req];return require(dep?dep:req)},m,m.exports,outer,modules,cache,entries);if(name)cache[name]=cache[id];return cache[id].exports}for(var id in entries){if(entries[id]){global[entries[id]]=require(id)}else{require(id)}}require.duo=true;require.cache=cache;require.modules=modules;return require}({1:[function(require,module,exports){var Amplitude=require("./amplitude");var old=window.amplitude||{};var instance=new Amplitude;instance._q=old._q||[];module.exports=instance},{"./amplitude":2}],2:[function(require,module,exports){var cookieStorage=require("./cookiestorage");var getUtmData=require("./utm");var Identify=require("./identify");var JSON=require("json");var localStorage=require("./localstorage");var md5=require("JavaScript-MD5");var object=require("object");var Request=require("./xhr");var type=require("./type");var UAParser=require("ua-parser-js");var utils=require("./utils");var UUID=require("./uuid");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var IDENTIFY_EVENT="$identify";var API_VERSION=2;var MAX_STRING_LENGTH=1024;var LocalStorageKeys={LAST_EVENT_ID:"amplitude_lastEventId",LAST_EVENT_TIME:"amplitude_lastEventTime",LAST_IDENTIFY_ID:"amplitude_lastIdentifyId",LAST_SEQUENCE_NUMBER:"amplitude_lastSequenceNumber",REFERRER:"amplitude_referrer",SESSION_ID:"amplitude_sessionId",DEVICE_ID:"amplitude_deviceId",OPT_OUT:"amplitude_optOut",USER_ID:"amplitude_userId"};var Amplitude=function(){this._unsentEvents=[];this._unsentIdentifys=[];this._ua=new UAParser(navigator.userAgent).getResult();this.options=object.merge({},DEFAULT_OPTIONS);this.cookieStorage=(new cookieStorage).getStorage();this._q=[]};Amplitude.prototype._eventId=0;Amplitude.prototype._identifyId=0;Amplitude.prototype._sequenceNumber=0;Amplitude.prototype._sending=false;Amplitude.prototype._lastEventTime=null;Amplitude.prototype._sessionId=null;Amplitude.prototype._newSession=false;Amplitude.prototype._updateScheduled=false;Amplitude.prototype.Identify=Identify;Amplitude.prototype.init=function(apiKey,opt_userId,opt_config,callback){try{this.options.apiKey=apiKey;if(opt_config){if(opt_config.saveEvents!==undefined){this.options.saveEvents=!!opt_config.saveEvents}if(opt_config.domain!==undefined){this.options.domain=opt_config.domain}if(opt_config.includeUtm!==undefined){this.options.includeUtm=!!opt_config.includeUtm}if(opt_config.includeReferrer!==undefined){this.options.includeReferrer=!!opt_config.includeReferrer}if(opt_config.batchEvents!==undefined){this.options.batchEvents=!!opt_config.batchEvents}this.options.platform=opt_config.platform||this.options.platform;this.options.language=opt_config.language||this.options.language;this.options.sessionTimeout=opt_config.sessionTimeout||this.options.sessionTimeout;this.options.uploadBatchSize=opt_config.uploadBatchSize||this.options.uploadBatchSize;this.options.eventUploadThreshold=opt_config.eventUploadThreshold||this.options.eventUploadThreshold;this.options.savedMaxCount=opt_config.savedMaxCount||this.options.savedMaxCount;this.options.eventUploadPeriodMillis=opt_config.eventUploadPeriodMillis||this.options.eventUploadPeriodMillis}this.cookieStorage.options({expirationDays:this.options.cookieExpiration,domain:this.options.domain});this.options.domain=this.cookieStorage.options().domain;_upgradeCookeData(this);_loadCookieData(this);this.options.deviceId=opt_config&&opt_config.deviceId!==undefined&&opt_config.deviceId!==null&&opt_config.deviceId||this.options.deviceId||UUID();this.options.userId=opt_userId!==undefined&&opt_userId!==null&&opt_userId||this.options.userId||null;var now=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||now-this._lastEventTime>this.options.sessionTimeout){this._newSession=true;this._sessionId=now}this._lastEventTime=now;_saveCookieData(this);if(this.options.saveEvents){this._unsentEvents=this._loadSavedUnsentEvents(this.options.unsentKey)||this._unsentEvents;this._unsentIdentifys=this._loadSavedUnsentEvents(this.options.unsentIdentifyKey)||this._unsentIdentifys;for(var i=0;i=this.options.eventUploadThreshold){this.sendEvents(callback);return true}if(!this._updateScheduled){this._updateScheduled=true;setTimeout(function(){this._updateScheduled=false;this.sendEvents()}.bind(this),this.options.eventUploadPeriodMillis)}return false};Amplitude.prototype._getFromStorage=function(storage,key){return storage.getItem(key)};Amplitude.prototype._setInStorage=function(storage,key,value){storage.setItem(key,value)};var _upgradeCookeData=function(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(cookieData&&cookieData.deviceId&&cookieData.sessionId&&cookieData.lastEventTime){return}var _getAndRemoveFromLocalStorage=function(key){var value=localStorage.getItem(key);localStorage.removeItem(key);return value};var apiKeySuffix="_"+scope.options.apiKey.slice(0,6);var localStorageDeviceId=_getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID+apiKeySuffix);var localStorageUserId=_getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID+apiKeySuffix);var localStorageOptOut=_getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT+apiKeySuffix);if(localStorageOptOut!==null&&localStorageOptOut!==undefined){localStorageOptOut=String(localStorageOptOut)==="true"}var localStorageSessionId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID));var localStorageLastEventTime=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME));var localStorageEventId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID));var localStorageIdentifyId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID));var localStorageSequenceNumber=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER));var _getFromCookie=function(key){return cookieData&&cookieData[key]};scope.options.deviceId=_getFromCookie("deviceId")||localStorageDeviceId;scope.options.userId=_getFromCookie("userId")||localStorageUserId;scope._sessionId=_getFromCookie("sessionId")||localStorageSessionId||scope._sessionId;scope._lastEventTime=_getFromCookie("lastEventTime")||localStorageLastEventTime||scope._lastEventTime;scope._eventId=_getFromCookie("eventId")||localStorageEventId||scope._eventId;scope._identifyId=_getFromCookie("identifyId")||localStorageIdentifyId||scope._identifyId;scope._sequenceNumber=_getFromCookie("sequenceNumber")||localStorageSequenceNumber||scope._sequenceNumber;scope.options.optOut=localStorageOptOut||false;if(cookieData&&cookieData.optOut!==undefined&&cookieData.optOut!==null){scope.options.optOut=String(cookieData.optOut)==="true"}_saveCookieData(scope)};var _loadCookieData=function(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(cookieData){if(cookieData.deviceId){scope.options.deviceId=cookieData.deviceId}if(cookieData.userId){scope.options.userId=cookieData.userId}if(cookieData.optOut!==null&&cookieData.optOut!==undefined){scope.options.optOut=cookieData.optOut}if(cookieData.sessionId){scope._sessionId=parseInt(cookieData.sessionId)}if(cookieData.lastEventTime){scope._lastEventTime=parseInt(cookieData.lastEventTime)}if(cookieData.eventId){scope._eventId=parseInt(cookieData.eventId)}if(cookieData.identifyId){scope._identifyId=parseInt(cookieData.identifyId)}if(cookieData.sequenceNumber){scope._sequenceNumber=parseInt(cookieData.sequenceNumber)}}};var _saveCookieData=function(scope){scope.cookieStorage.set(scope.options.cookieName,{deviceId:scope.options.deviceId,userId:scope.options.userId,optOut:scope.options.optOut,sessionId:scope._sessionId,lastEventTime:scope._lastEventTime,eventId:scope._eventId,identifyId:scope._identifyId,sequenceNumber:scope._sequenceNumber})};Amplitude.prototype._initUtmData=function(queryParams,cookieParams){queryParams=queryParams||location.search;cookieParams=cookieParams||this.cookieStorage.get("__utmz");this._utmProperties=getUtmData(cookieParams,queryParams)};Amplitude.prototype._getReferrer=function(){return document.referrer};Amplitude.prototype._getReferringDomain=function(referrer){if(referrer===null||referrer===undefined||referrer===""){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};Amplitude.prototype._saveReferrer=function(referrer){if(referrer===null||referrer===undefined||referrer===""){return}var referring_domain=this._getReferringDomain(referrer);var identify=(new Identify).setOnce("initial_referrer",referrer);identify.setOnce("initial_referring_domain",referring_domain);var hasSessionStorage=false;try{if(window.sessionStorage){hasSessionStorage=true}}catch(e){}if(hasSessionStorage&&!this._getFromStorage(sessionStorage,LocalStorageKeys.REFERRER)||!hasSessionStorage){identify.set("referrer",referrer).set("referring_domain",referring_domain);if(hasSessionStorage){this._setInStorage(sessionStorage,LocalStorageKeys.REFERRER,referrer)}}this.identify(identify)};Amplitude.prototype.saveEvents=function(){if(!this._apiKeySet("saveEvents()")){return}try{this._setInStorage(localStorage,this.options.unsentKey,JSON.stringify(this._unsentEvents));this._setInStorage(localStorage,this.options.unsentIdentifyKey,JSON.stringify(this._unsentIdentifys))}catch(e){}};Amplitude.prototype.setDomain=function(domain){if(!this._apiKeySet("setDomain()")){return}try{this.cookieStorage.options({domain:domain});this.options.domain=this.cookieStorage.options().domain;_loadCookieData(this);_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setUserId=function(userId){if(!this._apiKeySet("setUserId()")){return}try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setOptOut=function(enable){if(!this._apiKeySet("setOptOut()")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setDeviceId=function(deviceId){if(!this._apiKeySet("setDeviceId()")){return}try{if(deviceId){this.options.deviceId=""+deviceId;_saveCookieData(this)}}catch(e){utils.log(e)}};Amplitude.prototype.setUserProperties=function(userProperties){if(!this._apiKeySet("setUserProperties()")){return}var identify=new Identify;for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}this.identify(identify)};Amplitude.prototype.clearUserProperties=function(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};Amplitude.prototype.identify=function(identify,callback){if(!this._apiKeySet("identify()")){if(callback&&type(callback)==="function"){callback(0,"No request sent")}return}if(type(identify)==="object"&&"_q"in identify){var instance=new Identify;for(var i=0;i0){this._logEvent(IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,callback)}else if(callback&&type(callback)==="function"){callback(0,"No request sent")}};Amplitude.prototype.setVersionName=function(versionName){try{this.options.versionName=versionName}catch(e){utils.log(e)}};Amplitude.prototype._truncate=function(value){if(type(value)==="array"){for(var i=0;iMAX_STRING_LENGTH?value.substring(0,MAX_STRING_LENGTH):value}return value};Amplitude.prototype._logEvent=function(eventType,eventProperties,apiProperties,userProperties,callback){if(type(callback)!=="function"){callback=null}_loadCookieData(this);if(!eventType||this.options.optOut){if(callback){callback(0,"No request sent")}return}try{var eventId;if(eventType===IDENTIFY_EVENT){eventId=this.nextIdentifyId()}else{eventId=this.nextEventId()}var sequenceNumber=this.nextSequenceNumber();var eventTime=(new Date).getTime();var ua=this._ua;if(!this._sessionId||!this._lastEventTime||eventTime-this._lastEventTime>this.options.sessionTimeout){this._sessionId=eventTime}this._lastEventTime=eventTime;_saveCookieData(this);userProperties=userProperties||{};if(eventType!==IDENTIFY_EVENT){object.merge(userProperties,this._utmProperties)}apiProperties=apiProperties||{};eventProperties=eventProperties||{};var event={device_id:this.options.deviceId,user_id:this.options.userId||this.options.deviceId,timestamp:eventTime,event_id:eventId,session_id:this._sessionId||-1,event_type:eventType,version_name:this.options.versionName||null,platform:this.options.platform,os_name:ua.browser.name||null,os_version:ua.browser.major||null,device_model:ua.os.name||null,language:this.options.language,api_properties:apiProperties,event_properties:this._truncate(utils.validateProperties(eventProperties)),user_properties:this._truncate(userProperties),uuid:UUID(),library:{name:"amplitude-js",version:version},sequence_number:sequenceNumber};if(eventType===IDENTIFY_EVENT){this._unsentIdentifys.push(event);this._limitEventsQueued(this._unsentIdentifys)}else{this._unsentEvents.push(event);this._limitEventsQueued(this._unsentEvents)}if(this.options.saveEvents){this.saveEvents()}if(!this._sendEventsIfReady(callback)&&callback){callback(0,"No request sent")}return eventId}catch(e){utils.log(e)}};Amplitude.prototype._limitEventsQueued=function(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};Amplitude.prototype.logEvent=function(eventType,eventProperties,callback){if(!this._apiKeySet("logEvent()")){if(callback&&type(callback)==="function"){callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,callback)};var _isNumber=function(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenue=function(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent("revenue_amount",{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price})};Amplitude.prototype.removeEvents=function(maxEventId,maxIdentifyId){if(maxEventId>=0){var filteredEvents=[];for(var i=0;imaxEventId){filteredEvents.push(this._unsentEvents[i])}}this._unsentEvents=filteredEvents}if(maxIdentifyId>=0){var filteredIdentifys=[];for(var j=0;jmaxIdentifyId){filteredIdentifys.push(this._unsentIdentifys[j])}}this._unsentIdentifys=filteredIdentifys}};Amplitude.prototype.sendEvents=function(callback){if(!this._apiKeySet("sendEvents()")){if(callback&&type(callback)==="function"){callback(0,"No request sent")}return}if(!this._sending&&!this.options.optOut&&this._unsentCount()>0){this._sending=true;var url=("https:"===window.location.protocol?"https":"http")+"://"+this.options.apiEndpoint+"/";var numEvents=Math.min(this._unsentCount(),this.options.uploadBatchSize);var mergedEvents=this._mergeEventsAndIdentifys(numEvents);var maxEventId=mergedEvents.maxEventId;var maxIdentifyId=mergedEvents.maxIdentifyId;var events=JSON.stringify(mergedEvents.eventsToSend);var uploadTime=(new Date).getTime();var data={client:this.options.apiKey,e:events,v:API_VERSION,upload_time:uploadTime,checksum:md5(API_VERSION+this.options.apiKey+events+uploadTime)};var scope=this;new Request(url,data).send(function(status,response){scope._sending=false;try{if(status===200&&response==="success"){scope.removeEvents(maxEventId,maxIdentifyId);if(scope.options.saveEvents){scope.saveEvents()}if(!scope._sendEventsIfReady(callback)&&callback){callback(status,response)}}else if(status===413){if(scope.options.uploadBatchSize===1){scope.removeEvents(maxEventId,maxIdentifyId)}scope.options.uploadBatchSize=Math.ceil(numEvents/2);scope.sendEvents(callback)}else if(callback){callback(status,response)}}catch(e){}})}else if(callback){callback(0,"No request sent")}};Amplitude.prototype._mergeEventsAndIdentifys=function(numEvents){var eventsToSend=[];var eventIndex=0;var maxEventId=-1;var identifyIndex=0;var maxIdentifyId=-1;while(eventsToSend.length=this._unsentIdentifys.length){event=this._unsentEvents[eventIndex++];maxEventId=event.event_id}else if(eventIndex>=this._unsentEvents.length){event=this._unsentIdentifys[identifyIndex++];maxIdentifyId=event.event_id}else{if(!("sequence_number"in this._unsentEvents[eventIndex])||this._unsentEvents[eventIndex].sequence_number>2;enc2=(chr1&3)<<4|chr2>>4;enc3=(chr2&15)<<2|chr3>>6;enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else if(isNaN(chr3)){enc4=64}output=output+Base64._keyStr.charAt(enc1)+Base64._keyStr.charAt(enc2)+Base64._keyStr.charAt(enc3)+Base64._keyStr.charAt(enc4)}return output},decode:function(input){try{if(window.btoa&&window.atob){return decodeURIComponent(escape(window.atob(input)))}}catch(e){}return Base64._decode(input)},_decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(i>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}output=UTF8.decode(output);return output}};module.exports=Base64},{"./utf8":20}],20:[function(require,module,exports){var UTF8={encode:function(s){var utftext="";for(var n=0;n127&&c<2048){utftext+=String.fromCharCode(c>>6|192);utftext+=String.fromCharCode(c&63|128)}else{utftext+=String.fromCharCode(c>>12|224);utftext+=String.fromCharCode(c>>6&63|128);utftext+=String.fromCharCode(c&63|128)}}return utftext},decode:function(utftext){var s="";var i=0;var c=0,c1=0,c2=0;while(i191&&c<224){c1=utftext.charCodeAt(i+1);s+=String.fromCharCode((c&31)<<6|c1&63);i+=2}else{c1=utftext.charCodeAt(i+1);c2=utftext.charCodeAt(i+2);s+=String.fromCharCode((c&15)<<12|(c1&63)<<6|c2&63);i+=3}}return s}};module.exports=UTF8},{}],6:[function(require,module,exports){var json=window.JSON||{};var stringify=json.stringify;var parse=json.parse;module.exports=parse&&stringify?JSON:require("json-fallback")},{"json-fallback":21}],21:[function(require,module,exports){(function(){"use strict";var JSON=module.exports={};function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx,escapable,gap,indent,meta,rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i0){if(!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll")}return this}this.userPropertiesOperations[AMP_OP_CLEAR_ALL]="-";return this};Identify.prototype.prepend=function(property,value){this._addOperation(AMP_OP_PREPEND,property,value);return this};Identify.prototype.set=function(property,value){this._addOperation(AMP_OP_SET,property,value);return this};Identify.prototype.setOnce=function(property,value){this._addOperation(AMP_OP_SET_ONCE,property,value);return this};Identify.prototype.unset=function(property){this._addOperation(AMP_OP_UNSET,property,"-");return this};Identify.prototype._addOperation=function(operation,property,value){if(this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("This identify already contains a $clearAll operation, skipping operation "+operation);return}if(this.properties.indexOf(property)!==-1){utils.log('User property "'+property+'" already used in this identify, skipping operation '+operation);return}if(!this.userPropertiesOperations.hasOwnProperty(operation)){this.userPropertiesOperations[operation]={}}this.userPropertiesOperations[operation][property]=value;this.properties.push(property)};module.exports=Identify},{"./type":11,"./utils":13}],11:[function(require,module,exports){var toString=Object.prototype.toString;module.exports=function(val){switch(toString.call(val)){case"[object Date]":return"date";case"[object RegExp]":return"regexp";case"[object Arguments]":return"arguments";case"[object Array]":return"array";case"[object Error]":return"error"}if(val===null){return"null"}if(val===undefined){return"undefined"}if(val!==val){return"nan"}if(val&&val.nodeType===1){return"element"}if(typeof Buffer!=="undefined"&&Buffer.isBuffer(val)){return"buffer"}val=val.valueOf?val.valueOf():Object.prototype.valueOf.apply(val);return typeof val}},{}],13:[function(require,module,exports){var type=require("./type");var log=function(s){try{console.log("[Amplitude] "+s)}catch(e){}};var isEmptyString=function(str){return!str||str.length===0};var validateProperties=function(properties){var propsType=type(properties);if(propsType!=="object"){log("Error: invalid event properties format. Expecting Javascript object, received "+propsType+", ignoring");return{}}var copy={};for(var property in properties){if(!properties.hasOwnProperty(property)){continue}var key=property;var keyType=type(key);if(keyType!=="string"){log("WARNING: Non-string property key, received type "+keyType+', coercing to string "'+key+'"');key=String(key)}var value=validatePropertyValue(key,properties[property]);if(value===null){continue}copy[key]=value}return copy};var invalidValueTypes=["null","nan","undefined","function","arguments","regexp","element"];var validatePropertyValue=function(key,value){var valueType=type(value);if(invalidValueTypes.indexOf(valueType)!==-1){log('WARNING: Property key "'+key+'" with invalid value type '+valueType+", ignoring");value=null}else if(valueType==="error"){value=String(value);log('WARNING: Property key "'+key+'" with value type error, coercing to '+value)}else if(valueType==="array"){var arrayCopy=[];for(var i=0;i>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}function bit_rol(num,cnt){return num<>>32-cnt}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn(b&c|~b&d,a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn(b&d|c&~d,a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|~d),a,b,x,s,t)}function binl_md5(x,len){x[len>>5]|=128<>>9<<4)+14]=len;var i,olda,oldb,oldc,oldd,a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(i=0;i>5]>>>i%32&255)}return output}function rstr2binl(input){var i,output=[];output[(input.length>>2)-1]=undefined;for(i=0;i>5]|=(input.charCodeAt(i/8)&255)<16){bkey=binl_md5(bkey,key.length*8)}for(i=0;i<16;i+=1){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}hash=binl_md5(ipad.concat(rstr2binl(data)),512+data.length*8);return binl2rstr(binl_md5(opad.concat(hash),512+128))}function rstr2hex(input){var hex_tab="0123456789abcdef",output="",x,i;for(i=0;i>>4&15)+hex_tab.charAt(x&15)}return output}function str2rstr_utf8(input){return unescape(encodeURIComponent(input))}function raw_md5(s){return rstr_md5(str2rstr_utf8(s))}function hex_md5(s){return rstr2hex(raw_md5(s))}function raw_hmac_md5(k,d){return rstr_hmac_md5(str2rstr_utf8(k),str2rstr_utf8(d))}function hex_hmac_md5(k,d){return rstr2hex(raw_hmac_md5(k,d))}function md5(string,key,raw){if(!key){if(!raw){return hex_md5(string)}return raw_md5(string)}if(!raw){return hex_hmac_md5(key,string)}return raw_hmac_md5(key,string)}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=md5}exports.md5=md5}else{if(typeof define==="function"&&define.amd){define(function(){return md5})}else{$.md5=md5}}})(this)},{}],9:[function(require,module,exports){var has=Object.prototype.hasOwnProperty;exports.keys=Object.keys||function(obj){var keys=[];for(var key in obj){if(has.call(obj,key)){keys.push(key)}}return keys};exports.values=function(obj){var vals=[];for(var key in obj){if(has.call(obj,key)){vals.push(obj[key])}}return vals};exports.merge=function(a,b){for(var key in b){if(has.call(b,key)){a[key]=b[key]}}return a};exports.length=function(obj){return exports.keys(obj).length};exports.isEmpty=function(obj){return 0==exports.length(obj)}},{}],10:[function(require,module,exports){var querystring=require("querystring");var Request=function(url,data){this.url=url;this.data=data||{}};Request.prototype.send=function(callback){var isIE=window.XDomainRequest?true:false;if(isIE){var xdr=new window.XDomainRequest;xdr.open("POST",this.url,true);xdr.onload=function(){callback(200,xdr.responseText)};xdr.onerror=function(){if(xdr.responseText==="Request Entity Too Large"){callback(413,xdr.responseText)}else{callback(500,xdr.responseText)}};xdr.ontimeout=function(){};xdr.onprogress=function(){};xdr.send(querystring.stringify(this.data))}else{var xhr=new XMLHttpRequest;xhr.open("POST",this.url,true);xhr.onreadystatechange=function(){if(xhr.readyState===4){callback(xhr.status,xhr.responseText)}};xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");xhr.send(querystring.stringify(this.data))}};module.exports=Request},{querystring:23}],23:[function(require,module,exports){var encode=encodeURIComponent;var decode=decodeURIComponent;var trim=require("trim");var type=require("type");exports.parse=function(str){if("string"!=typeof str)return{};str=trim(str);if(""==str)return{};if("?"==str.charAt(0))str=str.slice(1);var obj={};var pairs=str.split("&");for(var i=0;i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)};module.exports=uuid},{}],15:[function(require,module,exports){module.exports="2.10.0"},{}],16:[function(require,module,exports){var language=require("./language");module.exports={apiEndpoint:"api.amplitude.com",cookieExpiration:365*10,cookieName:"amplitude_id",domain:undefined,includeUtm:false,language:language.language,optOut:false,platform:"Web",savedMaxCount:1e3,saveEvents:true,sessionTimeout:30*60*1e3,unsentKey:"amplitude_unsent",unsentIdentifyKey:"amplitude_unsent_identify",uploadBatchSize:100,batchEvents:false,eventUploadThreshold:30,eventUploadPeriodMillis:30*1e3}},{"./language":26}],26:[function(require,module,exports){var getLanguage=function(){return navigator&&(navigator.languages&&navigator.languages[0]||navigator.language||navigator.userLanguage)||undefined};module.exports={language:getLanguage()}},{}]},{},{1:""})); \ No newline at end of file +(function umd(require){if("object"==typeof exports){module.exports=require("1")}else if("function"==typeof define&&define.amd){define(function(){return require("1")})}else{this["amplitude"]=require("1")}})(function outer(modules,cache,entries){var global=function(){return this}();function require(name,jumped){if(cache[name])return cache[name].exports;if(modules[name])return call(name,require);throw new Error('cannot find module "'+name+'"')}function call(id,require){var m=cache[id]={exports:{}};var mod=modules[id];var name=mod[2];var fn=mod[0];fn.call(m.exports,function(req){var dep=modules[id][1][req];return require(dep?dep:req)},m,m.exports,outer,modules,cache,entries);if(name)cache[name]=cache[id];return cache[id].exports}for(var id in entries){if(entries[id]){global[entries[id]]=require(id)}else{require(id)}}require.duo=true;require.cache=cache;require.modules=modules;return require}({1:[function(require,module,exports){var Amplitude=require("./amplitude");var old=window.amplitude||{};var instance=new Amplitude;instance._q=old._q||[];module.exports=instance},{"./amplitude":2}],2:[function(require,module,exports){var Constants=require("./constants");var cookieStorage=require("./cookiestorage");var getUtmData=require("./utm");var Identify=require("./identify");var JSON=require("json");var localStorage=require("./localstorage");var md5=require("JavaScript-MD5");var object=require("object");var Request=require("./xhr");var type=require("./type");var UAParser=require("ua-parser-js");var utils=require("./utils");var UUID=require("./uuid");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this._unsentEvents=[];this._unsentIdentifys=[];this._ua=new UAParser(navigator.userAgent).getResult();this.options=object.merge({},DEFAULT_OPTIONS);this.cookieStorage=(new cookieStorage).getStorage();this._q=[];this._sending=false;this._updateScheduled=false;this._eventId=0;this._identifyId=0;this._lastEventTime=null;this._newSession=false;this._sequenceNumber=0;this._sessionId=null};Amplitude.prototype.Identify=Identify;Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){if(type(apiKey)!=="string"||utils.isEmptyString(apiKey)){utils.log("Invalid apiKey. Please re-initialize with a valid apiKey");return}try{this.options.apiKey=apiKey;_parseConfig(this.options,opt_config);this.cookieStorage.options({expirationDays:this.options.cookieExpiration,domain:this.options.domain});this.options.domain=this.cookieStorage.options().domain;_upgradeCookeData(this);_loadCookieData(this);this.options.deviceId=type(opt_config)==="object"&&type(opt_config.deviceId)==="string"&&!utils.isEmptyString(opt_config.deviceId)&&opt_config.deviceId||this.options.deviceId||UUID()+"R";this.options.userId=type(opt_userId)==="string"&&!utils.isEmptyString(opt_userId)&&opt_userId||this.options.userId||null;var now=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||now-this._lastEventTime>this.options.sessionTimeout){this._newSession=true;this._sessionId=now}this._lastEventTime=now;_saveCookieData(this);if(this.options.saveEvents){this._unsentEvents=this._loadSavedUnsentEvents(this.options.unsentKey);this._unsentIdentifys=this._loadSavedUnsentEvents(this.options.unsentIdentifyKey);for(var i=0;i0){options[key]=inputValue}};for(var key in config){if(config.hasOwnProperty(key)){parseValidateAndLoad(key)}}};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;i=this.options.eventUploadThreshold){this.sendEvents(callback);return true}if(!this._updateScheduled){this._updateScheduled=true;setTimeout(function(){this._updateScheduled=false;this.sendEvents()}.bind(this),this.options.eventUploadPeriodMillis)}return false};Amplitude.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key)};Amplitude.prototype._setInStorage=function _setInStorage(storage,key,value){storage.setItem(key,value)};var _upgradeCookeData=function _upgradeCookeData(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(type(cookieData)==="object"&&cookieData.deviceId&&cookieData.sessionId&&cookieData.lastEventTime){return}var _getAndRemoveFromLocalStorage=function _getAndRemoveFromLocalStorage(key){var value=localStorage.getItem(key);localStorage.removeItem(key);return value};var apiKeySuffix=type(scope.options.apiKey)==="string"&&"_"+scope.options.apiKey.slice(0,6)||"";var localStorageDeviceId=_getAndRemoveFromLocalStorage(Constants.DEVICE_ID+apiKeySuffix);var localStorageUserId=_getAndRemoveFromLocalStorage(Constants.USER_ID+apiKeySuffix);var localStorageOptOut=_getAndRemoveFromLocalStorage(Constants.OPT_OUT+apiKeySuffix);if(localStorageOptOut!==null&&localStorageOptOut!==undefined){localStorageOptOut=String(localStorageOptOut)==="true"}var localStorageSessionId=parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID));var localStorageLastEventTime=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME));var localStorageEventId=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID));var localStorageIdentifyId=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID));var localStorageSequenceNumber=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER));var _getFromCookie=function _getFromCookie(key){return type(cookieData)==="object"&&cookieData[key]};scope.options.deviceId=_getFromCookie("deviceId")||localStorageDeviceId;scope.options.userId=_getFromCookie("userId")||localStorageUserId;scope._sessionId=_getFromCookie("sessionId")||localStorageSessionId||scope._sessionId;scope._lastEventTime=_getFromCookie("lastEventTime")||localStorageLastEventTime||scope._lastEventTime;scope._eventId=_getFromCookie("eventId")||localStorageEventId||scope._eventId;scope._identifyId=_getFromCookie("identifyId")||localStorageIdentifyId||scope._identifyId;scope._sequenceNumber=_getFromCookie("sequenceNumber")||localStorageSequenceNumber||scope._sequenceNumber;scope.options.optOut=localStorageOptOut||false;if(cookieData&&cookieData.optOut!==undefined&&cookieData.optOut!==null){scope.options.optOut=String(cookieData.optOut)==="true"}_saveCookieData(scope)};var _loadCookieData=function _loadCookieData(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(type(cookieData)==="object"){if(cookieData.deviceId){scope.options.deviceId=cookieData.deviceId}if(cookieData.userId){scope.options.userId=cookieData.userId}if(cookieData.optOut!==null&&cookieData.optOut!==undefined){scope.options.optOut=cookieData.optOut}if(cookieData.sessionId){scope._sessionId=parseInt(cookieData.sessionId)}if(cookieData.lastEventTime){scope._lastEventTime=parseInt(cookieData.lastEventTime)}if(cookieData.eventId){scope._eventId=parseInt(cookieData.eventId)}if(cookieData.identifyId){scope._identifyId=parseInt(cookieData.identifyId)}if(cookieData.sequenceNumber){scope._sequenceNumber=parseInt(cookieData.sequenceNumber)}}};var _saveCookieData=function _saveCookieData(scope){scope.cookieStorage.set(scope.options.cookieName,{deviceId:scope.options.deviceId,userId:scope.options.userId,optOut:scope.options.optOut,sessionId:scope._sessionId,lastEventTime:scope._lastEventTime,eventId:scope._eventId,identifyId:scope._identifyId,sequenceNumber:scope._sequenceNumber})};Amplitude.prototype._initUtmData=function _initUtmData(queryParams,cookieParams){queryParams=queryParams||location.search;cookieParams=cookieParams||this.cookieStorage.get("__utmz");var utmProperties=getUtmData(cookieParams,queryParams);_sendUserPropertiesOncePerSession(this,Constants.UTM_PROPERTIES,utmProperties)};var _sendUserPropertiesOncePerSession=function _sendUserPropertiesOncePerSession(scope,storageKey,userProperties){if(type(userProperties)!=="object"||Object.keys(userProperties).length===0){return}var identify=new Identify;for(var key in userProperties){if(userProperties.hasOwnProperty(key)){identify.setOnce("initial_"+key,userProperties[key])}}var hasSessionStorage=utils.sessionStorageEnabled();if(hasSessionStorage&&!scope._getFromStorage(sessionStorage,storageKey)||!hasSessionStorage){for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}if(hasSessionStorage){scope._setInStorage(sessionStorage,storageKey,JSON.stringify(userProperties))}}scope.identify(identify)};Amplitude.prototype._getReferrer=function _getReferrer(){return document.referrer};Amplitude.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};Amplitude.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};Amplitude.prototype.saveEvents=function saveEvents(){try{this._setInStorage(localStorage,this.options.unsentKey,JSON.stringify(this._unsentEvents))}catch(e){}try{this._setInStorage(localStorage,this.options.unsentIdentifyKey,JSON.stringify(this._unsentIdentifys))}catch(e){}};Amplitude.prototype.setDomain=function setDomain(domain){if(!utils.validateInput(domain,"domain","string")){return}try{this.cookieStorage.options({domain:domain});this.options.domain=this.cookieStorage.options().domain;_loadCookieData(this);_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setDeviceId=function setDeviceId(deviceId){if(!utils.validateInput(deviceId,"deviceId","string")){return}try{if(!utils.isEmptyString(deviceId)){this.options.deviceId=""+deviceId;_saveCookieData(this)}}catch(e){utils.log(e)}};Amplitude.prototype.setUserProperties=function setUserProperties(userProperties){if(!this._apiKeySet("setUserProperties()")||!utils.validateInput(userProperties,"userProperties","object")){return}var identify=new Identify;for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}this.identify(identify)};Amplitude.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};Amplitude.prototype.identify=function(identify_obj,opt_callback){if(!this._apiKeySet("identify()")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return}if(type(identify_obj)==="object"&&identify_obj.hasOwnProperty("_q")){var instance=new Identify;for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,opt_callback)}}else{utils.log("Invalid identify input type. Expected Identify object but saw "+type(identify_obj))}if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}};Amplitude.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};Amplitude.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,callback){_loadCookieData(this);if(!eventType||this.options.optOut){if(type(callback)==="function"){callback(0,"No request sent")}return}try{var eventId;if(eventType===Constants.IDENTIFY_EVENT){eventId=this.nextIdentifyId()}else{eventId=this.nextEventId()}var sequenceNumber=this.nextSequenceNumber();var eventTime=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||eventTime-this._lastEventTime>this.options.sessionTimeout){this._sessionId=eventTime}this._lastEventTime=eventTime;_saveCookieData(this);userProperties=userProperties||{};apiProperties=apiProperties||{};eventProperties=eventProperties||{};var event={device_id:this.options.deviceId,user_id:this.options.userId,timestamp:eventTime,event_id:eventId,session_id:this._sessionId||-1,event_type:eventType,version_name:this.options.versionName||null,platform:this.options.platform,os_name:this._ua.browser.name||null,os_version:this._ua.browser.major||null,device_model:this._ua.os.name||null,language:this.options.language,api_properties:apiProperties,event_properties:utils.truncate(utils.validateProperties(eventProperties)),user_properties:utils.truncate(utils.validateProperties(userProperties)),uuid:UUID(),library:{name:"amplitude-js",version:version},sequence_number:sequenceNumber};if(eventType===Constants.IDENTIFY_EVENT){this._unsentIdentifys.push(event);this._limitEventsQueued(this._unsentIdentifys)}else{this._unsentEvents.push(event);this._limitEventsQueued(this._unsentEvents)}if(this.options.saveEvents){this.saveEvents()}if(!this._sendEventsIfReady(callback)&&type(callback)==="function"){callback(0,"No request sent")}return eventId}catch(e){utils.log(e)}};Amplitude.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};Amplitude.prototype.logEvent=function logEvent(eventType,eventProperties,opt_callback){if(!this._apiKeySet("logEvent()")||!utils.validateInput(eventType,"eventType","string")||utils.isEmptyString(eventType)){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent("revenue_amount",{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price})};Amplitude.prototype.removeEvents=function removeEvents(maxEventId,maxIdentifyId){_removeEvents(this,"_unsentEvents",maxEventId);_removeEvents(this,"_unsentIdentifys",maxIdentifyId)};var _removeEvents=function _removeEvents(scope,eventQueue,maxId){if(maxId<0){return}var filteredEvents=[];for(var i=0;imaxId){filteredEvents.push(scope[eventQueue][i])}}scope[eventQueue]=filteredEvents};Amplitude.prototype.sendEvents=function sendEvents(callback){if(!this._apiKeySet("sendEvents()")||this._sending||this.options.optOut||this._unsentCount()===0){if(type(callback)==="function"){callback(0,"No request sent")}return}this._sending=true;var url=("https:"===window.location.protocol?"https":"http")+"://"+this.options.apiEndpoint+"/";var numEvents=Math.min(this._unsentCount(),this.options.uploadBatchSize);var mergedEvents=this._mergeEventsAndIdentifys(numEvents);var maxEventId=mergedEvents.maxEventId;var maxIdentifyId=mergedEvents.maxIdentifyId;var events=JSON.stringify(mergedEvents.eventsToSend);var uploadTime=(new Date).getTime();var data={client:this.options.apiKey,e:events,v:Constants.API_VERSION,upload_time:uploadTime,checksum:md5(Constants.API_VERSION+this.options.apiKey+events+uploadTime)};var scope=this;new Request(url,data).send(function(status,response){scope._sending=false;try{if(status===200&&response==="success"){scope.removeEvents(maxEventId,maxIdentifyId);if(scope.options.saveEvents){scope.saveEvents()}if(!scope._sendEventsIfReady(callback)&&type(callback)==="function"){callback(status,response)}}else if(status===413){if(scope.options.uploadBatchSize===1){scope.removeEvents(maxEventId,maxIdentifyId)}scope.options.uploadBatchSize=Math.ceil(numEvents/2);scope.sendEvents(callback)}else if(type(callback)==="function"){callback(status,response)}}catch(e){}})};Amplitude.prototype._mergeEventsAndIdentifys=function _mergeEventsAndIdentifys(numEvents){var eventsToSend=[];var eventIndex=0;var maxEventId=-1;var identifyIndex=0;var maxIdentifyId=-1;while(eventsToSend.length=this._unsentIdentifys.length;var noEvents=eventIndex>=this._unsentEvents.length;if(noEvents&&noIdentifys){utils.log("Merging Events and Identifys, less events and identifys than expected");break}else if(noIdentifys){event=this._unsentEvents[eventIndex++];maxEventId=event.event_id}else if(noEvents){event=this._unsentIdentifys[identifyIndex++];maxIdentifyId=event.event_id}else{if(!("sequence_number"in this._unsentEvents[eventIndex])||this._unsentEvents[eventIndex].sequence_number>2;enc2=(chr1&3)<<4|chr2>>4;enc3=(chr2&15)<<2|chr3>>6;enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else if(isNaN(chr3)){enc4=64}output=output+Base64._keyStr.charAt(enc1)+Base64._keyStr.charAt(enc2)+Base64._keyStr.charAt(enc3)+Base64._keyStr.charAt(enc4)}return output},decode:function(input){try{if(window.btoa&&window.atob){return decodeURIComponent(escape(window.atob(input)))}}catch(e){}return Base64._decode(input)},_decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(i>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}output=UTF8.decode(output);return output}};module.exports=Base64},{"./utf8":21}],21:[function(require,module,exports){var UTF8={encode:function(s){var utftext="";for(var n=0;n127&&c<2048){utftext+=String.fromCharCode(c>>6|192);utftext+=String.fromCharCode(c&63|128)}else{utftext+=String.fromCharCode(c>>12|224);utftext+=String.fromCharCode(c>>6&63|128);utftext+=String.fromCharCode(c&63|128)}}return utftext},decode:function(utftext){var s="";var i=0;var c=0,c1=0,c2=0;while(i191&&c<224){c1=utftext.charCodeAt(i+1);s+=String.fromCharCode((c&31)<<6|c1&63);i+=2}else{c1=utftext.charCodeAt(i+1);c2=utftext.charCodeAt(i+2);s+=String.fromCharCode((c&15)<<12|(c1&63)<<6|c2&63);i+=3}}return s}};module.exports=UTF8},{}],7:[function(require,module,exports){var json=window.JSON||{};var stringify=json.stringify;var parse=json.parse;module.exports=parse&&stringify?JSON:require("json-fallback")},{"json-fallback":22}],22:[function(require,module,exports){(function(){"use strict";var JSON=module.exports={};function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx,escapable,gap,indent,meta,rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;iconstants.MAX_STRING_LENGTH?value.substring(0,constants.MAX_STRING_LENGTH):value}return value};var validateInput=function validateInput(input,name,expectedType){if(type(input)!==expectedType){log("Invalid "+name+" input type. Expected "+expectedType+" but received "+type(input));return false}return true};var validateProperties=function validateProperties(properties){var propsType=type(properties);if(propsType!=="object"){log("Error: invalid event properties format. Expecting Javascript object, received "+propsType+", ignoring");return{}}var copy={};for(var property in properties){if(!properties.hasOwnProperty(property)){continue}var key=property;var keyType=type(key);if(keyType!=="string"){log("WARNING: Non-string property key, received type "+keyType+', coercing to string "'+key+'"');key=String(key)}var value=validatePropertyValue(key,properties[property]);if(value===null){continue}copy[key]=value}return copy};var invalidValueTypes=["null","nan","undefined","function","arguments","regexp","element"];var validatePropertyValue=function validatePropertyValue(key,value){var valueType=type(value);if(invalidValueTypes.indexOf(valueType)!==-1){log('WARNING: Property key "'+key+'" with invalid value type '+valueType+", ignoring");value=null}else if(valueType==="error"){value=String(value);log('WARNING: Property key "'+key+'" with value type error, coercing to '+value)}else if(valueType==="array"){var arrayCopy=[];for(var i=0;i0){if(!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll")}return this}this.userPropertiesOperations[AMP_OP_CLEAR_ALL]="-";return this};Identify.prototype.prepend=function(property,value){this._addOperation(AMP_OP_PREPEND,property,value);return this};Identify.prototype.set=function(property,value){this._addOperation(AMP_OP_SET,property,value);return this};Identify.prototype.setOnce=function(property,value){this._addOperation(AMP_OP_SET_ONCE,property,value);return this};Identify.prototype.unset=function(property){this._addOperation(AMP_OP_UNSET,property,"-");return this};Identify.prototype._addOperation=function(operation,property,value){if(this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("This identify already contains a $clearAll operation, skipping operation "+operation);return}if(this.properties.indexOf(property)!==-1){utils.log('User property "'+property+'" already used in this identify, skipping operation '+operation);return}if(!this.userPropertiesOperations.hasOwnProperty(operation)){this.userPropertiesOperations[operation]={}}this.userPropertiesOperations[operation][property]=value;this.properties.push(property)};module.exports=Identify},{"./type":12,"./utils":14}],9:[function(require,module,exports){(function($){"use strict";function safe_add(x,y){var lsw=(x&65535)+(y&65535),msw=(x>>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}function bit_rol(num,cnt){return num<>>32-cnt}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn(b&c|~b&d,a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn(b&d|c&~d,a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|~d),a,b,x,s,t)}function binl_md5(x,len){x[len>>5]|=128<>>9<<4)+14]=len;var i,olda,oldb,oldc,oldd,a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(i=0;i>5]>>>i%32&255)}return output}function rstr2binl(input){var i,output=[];output[(input.length>>2)-1]=undefined;for(i=0;i>5]|=(input.charCodeAt(i/8)&255)<16){bkey=binl_md5(bkey,key.length*8)}for(i=0;i<16;i+=1){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}hash=binl_md5(ipad.concat(rstr2binl(data)),512+data.length*8);return binl2rstr(binl_md5(opad.concat(hash),512+128))}function rstr2hex(input){var hex_tab="0123456789abcdef",output="",x,i;for(i=0;i>>4&15)+hex_tab.charAt(x&15)}return output}function str2rstr_utf8(input){return unescape(encodeURIComponent(input))}function raw_md5(s){return rstr_md5(str2rstr_utf8(s))}function hex_md5(s){return rstr2hex(raw_md5(s))}function raw_hmac_md5(k,d){return rstr_hmac_md5(str2rstr_utf8(k),str2rstr_utf8(d))}function hex_hmac_md5(k,d){return rstr2hex(raw_hmac_md5(k,d))}function md5(string,key,raw){if(!key){if(!raw){return hex_md5(string)}return raw_md5(string)}if(!raw){return hex_hmac_md5(key,string)}return raw_hmac_md5(key,string)}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=md5}exports.md5=md5}else{if(typeof define==="function"&&define.amd){define(function(){return md5})}else{$.md5=md5}}})(this)},{}],10:[function(require,module,exports){var has=Object.prototype.hasOwnProperty;exports.keys=Object.keys||function(obj){var keys=[];for(var key in obj){if(has.call(obj,key)){keys.push(key)}}return keys};exports.values=function(obj){var vals=[];for(var key in obj){if(has.call(obj,key)){vals.push(obj[key])}}return vals};exports.merge=function(a,b){for(var key in b){if(has.call(b,key)){a[key]=b[key]}}return a};exports.length=function(obj){return exports.keys(obj).length};exports.isEmpty=function(obj){return 0==exports.length(obj)}},{}],11:[function(require,module,exports){var querystring=require("querystring");var Request=function(url,data){this.url=url;this.data=data||{}};Request.prototype.send=function(callback){var isIE=window.XDomainRequest?true:false;if(isIE){var xdr=new window.XDomainRequest;xdr.open("POST",this.url,true);xdr.onload=function(){callback(200,xdr.responseText)};xdr.onerror=function(){if(xdr.responseText==="Request Entity Too Large"){callback(413,xdr.responseText)}else{callback(500,xdr.responseText)}};xdr.ontimeout=function(){};xdr.onprogress=function(){};xdr.send(querystring.stringify(this.data))}else{var xhr=new XMLHttpRequest;xhr.open("POST",this.url,true);xhr.onreadystatechange=function(){if(xhr.readyState===4){callback(xhr.status,xhr.responseText)}};xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");xhr.send(querystring.stringify(this.data))}};module.exports=Request},{querystring:24}],24:[function(require,module,exports){var encode=encodeURIComponent;var decode=decodeURIComponent;var trim=require("trim");var type=require("type");exports.parse=function(str){if("string"!=typeof str)return{};str=trim(str);if(""==str)return{};if("?"==str.charAt(0))str=str.slice(1);var obj={};var pairs=str.split("&");for(var i=0;i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)};module.exports=uuid},{}],16:[function(require,module,exports){module.exports="2.10.0"},{}],17:[function(require,module,exports){var language=require("./language");module.exports={apiEndpoint:"api.amplitude.com",cookieExpiration:365*10,cookieName:"amplitude_id",domain:"",includeReferrer:false,includeUtm:false,language:language.language,optOut:false,platform:"Web",savedMaxCount:1e3,saveEvents:true,sessionTimeout:30*60*1e3,unsentKey:"amplitude_unsent",unsentIdentifyKey:"amplitude_unsent_identify",uploadBatchSize:100,batchEvents:false,eventUploadThreshold:30,eventUploadPeriodMillis:30*1e3}},{"./language":27}],27:[function(require,module,exports){var getLanguage=function(){return navigator&&(navigator.languages&&navigator.languages[0]||navigator.language||navigator.userLanguage)||undefined};module.exports={language:getLanguage()}},{}]},{},{1:""})); \ No newline at end of file diff --git a/documentation/Amplitude.html b/documentation/Amplitude.html new file mode 100644 index 00000000..3e5463ec --- /dev/null +++ b/documentation/Amplitude.html @@ -0,0 +1,2352 @@ + + + + + JSDoc: Class: Amplitude + + + + + + + + + + +
+ +

Class: Amplitude

+ + + + + + +
+ +
+ +

Amplitude

+ + +
+ +
+
+ + + + + +

new Amplitude()

+ + + + + +
+ Amplitude SDK API - instance constructor. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
var amplitude = new Amplitude();
+ + + + +
+ + + + + + + + + + + + +

Members

+ + + +

__VERSION__

+ + + + +
+ Get the current version of Amplitude's Javascript SDK. +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + +
Example
+ +
var amplitudeVersion = amplitude.__VERSION__;
+ + + + + + + +

Methods

+ + + + + + +

clearUserProperties()

+ + + + + +
+ Clear all of the user properties for the current user. Note: clearing user properties is irreversible! +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.clearUserProperties();
+ + + + + + + + +

getSessionId() → {number}

+ + + + + +
+ Returns the id of the current session. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Id of the current session. +
+ + + +
+
+ Type +
+
+ +number + + +
+
+ + + + + + + + + + +

identify(identify_obj, opt_callback)

+ + + + + +
+ Send an identify call containing user property operations to Amplitude servers. +See Readme +for more information on the Identify API and user property operations. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
identify_obj + + +Identify + + + + the Identify object containing the user property operations to send.
opt_callback + + +Amplitude~eventCallback + + + + (optional) callback function to run when the identify event has been sent. +Note: the server response code and response body from the identify event upload are passed to the callback function.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31');
+amplitude.identify(identify);
+ + + + + + + + +

init(apiKey, opt_userId, opt_config, opt_callback)

+ + + + + +
+ Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. +This is required before any other methods can be called. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
apiKey + + +string + + + + The API key for your app.
opt_userId + + +string + + + + (optional) An identifier for this user.
opt_config + + +object + + + + (optional) Configuration options. +See Readme for list of options and default values.
opt_callback + + +function + + + + (optional) Provide a callback function to run after initialization is complete.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); });
+ + + + + + + + +

isNewSession() → {boolean}

+ + + + + +
+ Returns true if a new session was created during initialization, otherwise false. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Whether a new session was created during initialization. +
+ + + +
+
+ Type +
+
+ +boolean + + +
+
+ + + + + + + + + + +

logEvent(eventType, eventProperties, opt_callback)

+ + + + + +
+ Log an event with eventType and eventProperties +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
eventType + + +string + + + + name of event
eventProperties + + +object + + + + (optional) an object with string keys and values for the event properties.
opt_callback + + +Amplitude~eventCallback + + + + (optional) a callback function to run after the event is logged. +Note: the server response code and response body from the event upload are passed to the callback function.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
+ + + + + + + + +

logRevenue(price, quantity, product)

+ + + + + +
+ Log revenue event with a price, quantity, and product identifier. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
price + + +number + + + + price of revenue event
quantity + + +number + + + + (optional) quantity of products in revenue event. If no quantity specified default to 1.
product + + +string + + + + (optional) product identifier
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.logRevenue(3.99, 1, 'product_1234');
+ + + + + + + + +

setDeviceId(deviceId)

+ + + + + +
+ Sets a custom deviceId for current user. Note: this is not recommended unless you know what you are doing +(like if you have your own system for managing deviceIds). Make sure the deviceId you set is sufficiently unique +(we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
deviceId + + +string + + + + custom deviceId for current user.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0');
+ + + + + + + + +

setDomain(domain)

+ + + + + +
+ Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
domain + + +string + + + + to set.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.setDomain('.amplitude.com');
+ + + + + + + + +

setGlobalUserProperties()

+ + + + + +
+ Set global user properties. Note this is deprecated, and we recommend using setUserProperties +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
Deprecated:
  • Yes
+ + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

setOptOut(enable)

+ + + + + +
+ Sets whether to opt current user out of tracking. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
enable + + +boolean + + + + if true then no events will be logged or sent.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

setUserId(userId)

+ + + + + +
+ Sets an identifier for the current user. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userId + + +string + + + + identifier to set. Can be null.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.setUserId('joe@gmail.com');
+ + + + + + + + +

setUserProperties(userProperties)

+ + + + + +
+ Sets user properties for the current user. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
userProperties + + +object + + + + object with string keys and values for the user properties to set.
+ + +boolean + + + + DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in +memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true})
+ + + + + + + + +

setVersionName(versionName)

+ + + + + +
+ Set a versionName for your application. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
versionName + + +string + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitude.setVersionName('1.12.3');
+ + + + + + + +

Type Definitions

+ + + + + + +

eventCallback(responseCode, responseBody)

+ + + + + +
+ This is the callback for logEvent and identify calls. It gets called after the event/identify is uploaded, +and the server response code and response body from the upload request are passed to the callback function. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
responseCode + + +number + + + + Server response code for the event / identify upload request.
responseBody + + +string + + + + Server response body for the event / identify upload request.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Tue Apr 05 2016 14:28:54 GMT-0700 (PDT) +
+ + + + + \ No newline at end of file diff --git a/documentation/Identify.html b/documentation/Identify.html new file mode 100644 index 00000000..db52e025 --- /dev/null +++ b/documentation/Identify.html @@ -0,0 +1,1304 @@ + + + + + JSDoc: Class: Identify + + + + + + + + + + +
+ +

Class: Identify

+ + + + + + +
+ +
+ +

Identify

+ + +
+ +
+
+ + + + + +

new Identify()

+ + + + + +
+ Identify API - instance constructor. Identify objects are a wrapper for user property operations. +Each method adds a user property operation to the Identify object, and returns the same Identify object, +allowing you to chain multiple method calls together. +Note: if the same user property is used in multiple operations on a single Identify object, +only the first operation on that property will be saved, and the rest will be ignored. +See Readme +for more information on the Identify API and user property operations. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
var identify = new amplitude.Identify();
+ + + + +
+ + + + + + + + + + + + + + +

Methods

+ + + + + + +

add(property, value) → {Identify}

+ + + + + +
+ Increment a user property by a given value (can also be negative to decrement). +If the user property does not have a value set yet, it will be initialized to 0 before being incremented. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
property + + +string + + + + The user property key.
value + + +number +| + +string + + + + The amount by which to increment the user property. Allows numbers as strings (ex: '123').
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Returns the same Identify object, allowing you to chain multiple method calls together. +
+ + + +
+
+ Type +
+
+ +Identify + + +
+
+ + + + +
Example
+ +
var identify = new amplitude.Identify().add('karma', 1).add('friends', 1);
+amplitude.identify(identify); // send the Identify call
+ + + + + + + + +

append(property, value) → {Identify}

+ + + + + +
+ Append a value or values to a user property. +If the user property does not have a value set yet, +it will be initialized to an empty list before the new values are appended. +If the user property has an existing value and it is not a list, +the existing value will be converted into a list with the new values appended. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
property + + +string + + + + The user property key.
value + + +number +| + +string +| + +list +| + +object + + + + A value or values to append. +Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Returns the same Identify object, allowing you to chain multiple method calls together. +
+ + + +
+
+ Type +
+
+ +Identify + + +
+
+ + + + +
Example
+ +
var identify = new amplitude.Identify().append('ab-tests', 'new-user-tests');
+identify.append('some_list', [1, 2, 3, 4, 'values']);
+amplitude.identify(identify); // send the Identify call
+ + + + + + + + +

prepend(property, value) → {Identify}

+ + + + + +
+ Prepend a value or values to a user property. +Prepend means inserting the value or values at the front of a list. +If the user property does not have a value set yet, +it will be initialized to an empty list before the new values are prepended. +If the user property has an existing value and it is not a list, +the existing value will be converted into a list with the new values prepended. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
property + + +string + + + + The user property key.
value + + +number +| + +string +| + +list +| + +object + + + + A value or values to prepend. +Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Returns the same Identify object, allowing you to chain multiple method calls together. +
+ + + +
+
+ Type +
+
+ +Identify + + +
+
+ + + + +
Example
+ +
var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-tests');
+identify.prepend('some_list', [1, 2, 3, 4, 'values']);
+amplitude.identify(identify); // send the Identify call
+ + + + + + + + +

set(property, value) → {Identify}

+ + + + + +
+ Sets the value of a given user property. If a value already exists, it will be overwriten with the new value. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
property + + +string + + + + The user property key.
value + + +number +| + +string +| + +list +| + +object + + + + A value or values to set. +Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Returns the same Identify object, allowing you to chain multiple method calls together. +
+ + + +
+
+ Type +
+
+ +Identify + + +
+
+ + + + +
Example
+ +
var identify = new amplitude.Identify().set('user_type', 'beta');
+identify.set('name', {'first': 'John', 'last': 'Doe'}); // dict is flattened and becomes name.first: John, name.last: Doe
+amplitude.identify(identify); // send the Identify call
+ + + + + + + + +

setOnce(property, value) → {Identify}

+ + + + + +
+ Sets the value of a given user property only once. Subsequent setOnce operations on that user property will be ignored; +however, that user property can still be modified through any of the other operations. +Useful for capturing properties such as 'initial_signup_date', 'initial_referrer', etc. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
property + + +string + + + + The user property key.
value + + +number +| + +string +| + +list +| + +object + + + + A value or values to set once. +Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Returns the same Identify object, allowing you to chain multiple method calls together. +
+ + + +
+
+ Type +
+
+ +Identify + + +
+
+ + + + +
Example
+ +
var identify = new amplitude.Identify().setOnce('sign_up_date', '2016-04-01');
+amplitude.identify(identify); // send the Identify call
+ + + + + + + + +

unset(property) → {Identify}

+ + + + + +
+ Unset and remove a user property. This user property will no longer show up in a user's profile. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
property + + +string + + + + The user property key.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Returns:
+ + +
+ Returns the same Identify object, allowing you to chain multiple method calls together. +
+ + + +
+
+ Type +
+
+ +Identify + + +
+
+ + + + +
Example
+ +
var identify = new amplitude.Identify().unset('user_type').unset('age');
+amplitude.identify(identify); // send the Identify call
+ + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Tue Apr 05 2016 14:28:54 GMT-0700 (PDT) +
+ + + + + \ No newline at end of file diff --git a/documentation/amplitude.js.html b/documentation/amplitude.js.html new file mode 100644 index 00000000..50b0a0cb --- /dev/null +++ b/documentation/amplitude.js.html @@ -0,0 +1,1063 @@ + + + + + JSDoc: Source: amplitude.js + + + + + + + + + + +
+ +

Source: amplitude.js

+ + + + + + +
+
+
var Constants = require('./constants');
+var cookieStorage = require('./cookiestorage');
+var getUtmData = require('./utm');
+var Identify = require('./identify');
+var JSON = require('json'); // jshint ignore:line
+var localStorage = require('./localstorage');  // jshint ignore:line
+var md5 = require('JavaScript-MD5');
+var object = require('object');
+var Request = require('./xhr');
+var type = require('./type');
+var UAParser = require('ua-parser-js');
+var utils = require('./utils');
+var UUID = require('./uuid');
+var version = require('./version');
+var DEFAULT_OPTIONS = require('./options');
+
+/**
+ * Amplitude SDK API - instance constructor.
+ * @constructor Amplitude
+ * @public
+ * @example var amplitude = new Amplitude();
+ */
+var Amplitude = function Amplitude() {
+  this._unsentEvents = [];
+  this._unsentIdentifys = [];
+  this._ua = new UAParser(navigator.userAgent).getResult();
+  this.options = object.merge({}, DEFAULT_OPTIONS);
+  this.cookieStorage = new cookieStorage().getStorage();
+  this._q = []; // queue for proxied functions before script load
+  this._sending = false;
+  this._updateScheduled = false;
+
+  // event meta data
+  this._eventId = 0;
+  this._identifyId = 0;
+  this._lastEventTime = null;
+  this._newSession = false;
+  this._sequenceNumber = 0;
+  this._sessionId = null;
+};
+
+Amplitude.prototype.Identify = Identify;
+
+/**
+ * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations.
+ * This is required before any other methods can be called.
+ * @public
+ * @param {string} apiKey - The API key for your app.
+ * @param {string} opt_userId - (optional) An identifier for this user.
+ * @param {object} opt_config - (optional) Configuration options.
+ * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#configuration-options} for list of options and default values.
+ * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete.
+ * @example amplitude.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); });
+ */
+Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) {
+  if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) {
+    utils.log('Invalid apiKey. Please re-initialize with a valid apiKey');
+    return;
+  }
+
+  try {
+    this.options.apiKey = apiKey;
+    _parseConfig(this.options, opt_config);
+    this.cookieStorage.options({
+      expirationDays: this.options.cookieExpiration,
+      domain: this.options.domain
+    });
+    this.options.domain = this.cookieStorage.options().domain;
+
+    _upgradeCookeData(this);
+    _loadCookieData(this);
+
+    // load deviceId and userId from input, or try to fetch existing value from cookie
+    this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' &&
+        !utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R';
+    this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) ||
+        this.options.userId || null;
+
+    var now = new Date().getTime();
+    if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) {
+      this._newSession = true;
+      this._sessionId = now;
+    }
+    this._lastEventTime = now;
+    _saveCookieData(this);
+
+    if (this.options.saveEvents) {
+      this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey);
+      this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey);
+
+      // validate event properties for unsent events
+      for (var i = 0; i < this._unsentEvents.length; i++) {
+        var eventProperties = this._unsentEvents[i].event_properties;
+        this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties);
+      }
+
+      // validate user properties for unsent identifys
+      for (var j = 0; j < this._unsentIdentifys.length; j++) {
+        var userProperties = this._unsentIdentifys[j].user_properties;
+        this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties);
+      }
+
+      this._sendEventsIfReady(); // try sending unsent events
+    }
+
+    if (this.options.includeUtm) {
+      this._initUtmData();
+    }
+
+    if (this.options.includeReferrer) {
+      this._saveReferrer(this._getReferrer());
+    }
+  } catch (e) {
+    utils.log(e);
+  } finally {
+    if (type(opt_callback) === 'function') {
+      opt_callback();
+    }
+  }
+};
+
+/**
+ * Parse and validate user specified config values and overwrite existing option value
+ * DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values
+ * @private
+ */
+var _parseConfig = function _parseConfig(options, config) {
+  if (type(config) !== 'object') {
+    return;
+  }
+
+  // validates config value is defined, is the correct type, and some additional value sanity checks
+  var parseValidateAndLoad = function parseValidateAndLoad(key) {
+    if (!DEFAULT_OPTIONS.hasOwnProperty(key)) {
+      return;  // skip bogus config values
+    }
+
+    var inputValue = config[key];
+    var expectedType = type(DEFAULT_OPTIONS[key]);
+    if (!utils.validateInput(inputValue, key + ' option', expectedType)) {
+      return;
+    }
+    if (expectedType === 'boolean') {
+      options[key] = !!inputValue;
+    } else if ((expectedType === 'string' && !utils.isEmptyString(inputValue)) ||
+        (expectedType === 'number' && inputValue > 0)) {
+      options[key] = inputValue;
+    }
+   };
+
+   for (var key in config) {
+    if (config.hasOwnProperty(key)) {
+      parseValidateAndLoad(key);
+    }
+   }
+};
+
+/**
+ * Run functions queued up by proxy loading snippet
+ * @private
+ */
+Amplitude.prototype.runQueuedFunctions = function () {
+  for (var i = 0; i < this._q.length; i++) {
+    var fn = this[this._q[i][0]];
+    if (type(fn) === 'function') {
+      fn.apply(this, this._q[i].slice(1));
+    }
+  }
+  this._q = []; // clear function queue after running
+};
+
+/**
+ * Check that the apiKey is set before calling a function. Logs a warning message if not set.
+ * @private
+ */
+Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) {
+  if (utils.isEmptyString(this.options.apiKey)) {
+    utils.log('Invalid apiKey. Please set a valid apiKey with init() before calling ' + methodName);
+    return false;
+  }
+  return true;
+};
+
+/**
+ * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted.
+ * @private
+ */
+Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) {
+  var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey);
+  if (utils.isEmptyString(savedUnsentEventsString)) {
+    return []; // new app, does not have any saved events
+  }
+
+  if (type(savedUnsentEventsString) === 'string') {
+    try {
+      var events = JSON.parse(savedUnsentEventsString);
+      if (type(events) === 'array') { // handle case where JSON dumping of unsent events is corrupted
+        return events;
+      }
+    } catch (e) {}
+  }
+  utils.log('Unable to load ' + unsentKey + ' events. Restart with a new empty queue.');
+  return [];
+};
+
+/**
+ * Returns true if a new session was created during initialization, otherwise false.
+ * @public
+ * @return {boolean} Whether a new session was created during initialization.
+ */
+Amplitude.prototype.isNewSession = function isNewSession() {
+  return this._newSession;
+};
+
+/**
+ * Returns the id of the current session.
+ * @public
+ * @return {number} Id of the current session.
+ */
+Amplitude.prototype.getSessionId = function getSessionId() {
+  return this._sessionId;
+};
+
+/**
+ * Increments the eventId and returns it.
+ * @private
+ */
+Amplitude.prototype.nextEventId = function nextEventId() {
+  this._eventId++;
+  return this._eventId;
+};
+
+/**
+ * Increments the identifyId and returns it.
+ * @private
+ */
+Amplitude.prototype.nextIdentifyId = function nextIdentifyId() {
+  this._identifyId++;
+  return this._identifyId;
+};
+
+/**
+ * Increments the sequenceNumber and returns it.
+ * @private
+ */
+Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() {
+  this._sequenceNumber++;
+  return this._sequenceNumber;
+};
+
+/**
+ * Returns the total count of unsent events and identifys
+ * @private
+ */
+Amplitude.prototype._unsentCount = function _unsentCount() {
+  return this._unsentEvents.length + this._unsentIdentifys.length;
+};
+
+/**
+ * Send events if ready. Returns true if events are sent.
+ * @private
+ */
+Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) {
+  if (this._unsentCount() === 0) {
+    return false;
+  }
+
+  // if batching disabled, send any unsent events immediately
+  if (!this.options.batchEvents) {
+    this.sendEvents(callback);
+    return true;
+  }
+
+  // if batching enabled, check if min threshold met for batch size
+  if (this._unsentCount() >= this.options.eventUploadThreshold) {
+    this.sendEvents(callback);
+    return true;
+  }
+
+  // otherwise schedule an upload after 30s
+  if (!this._updateScheduled) {  // make sure we only schedule 1 upload
+    this._updateScheduled = true;
+    setTimeout(function() {
+        this._updateScheduled = false;
+        this.sendEvents();
+      }.bind(this), this.options.eventUploadPeriodMillis
+    );
+  }
+
+  return false; // an upload was scheduled, no events were uploaded
+};
+
+/**
+ * Helper function to fetch values from storage
+ * Storage argument allows for localStoraoge and sessionStoraoge
+ * @private
+ */
+Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) {
+  return storage.getItem(key);
+};
+
+/**
+ * Helper function to set values in storage
+ * Storage argument allows for localStoraoge and sessionStoraoge
+ * @private
+ */
+Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) {
+  storage.setItem(key, value);
+};
+
+/**
+ * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber)
+ * can be stored in many different places (localStorage, cookie, etc).
+ * Need to unify all sources into one place with a one-time upgrade/migration.
+ * @private
+ */
+var _upgradeCookeData = function _upgradeCookeData(scope) {
+  // skip if migration already happened
+  var cookieData = scope.cookieStorage.get(scope.options.cookieName);
+  if (type(cookieData) === 'object' && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) {
+    return;
+  }
+
+  var _getAndRemoveFromLocalStorage = function _getAndRemoveFromLocalStorage(key) {
+    var value = localStorage.getItem(key);
+    localStorage.removeItem(key);
+    return value;
+  };
+
+  // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey
+  var apiKeySuffix = (type(scope.options.apiKey) === 'string' && ('_' + scope.options.apiKey.slice(0, 6))) || '';
+  var localStorageDeviceId = _getAndRemoveFromLocalStorage(Constants.DEVICE_ID + apiKeySuffix);
+  var localStorageUserId = _getAndRemoveFromLocalStorage(Constants.USER_ID + apiKeySuffix);
+  var localStorageOptOut = _getAndRemoveFromLocalStorage(Constants.OPT_OUT + apiKeySuffix);
+  if (localStorageOptOut !== null && localStorageOptOut !== undefined) {
+    localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean
+  }
+
+  // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support
+  var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID));
+  var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME));
+  var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID));
+  var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID));
+  var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER));
+
+  var _getFromCookie = function _getFromCookie(key) {
+    return type(cookieData) === 'object' && cookieData[key];
+  };
+  scope.options.deviceId = _getFromCookie('deviceId') || localStorageDeviceId;
+  scope.options.userId = _getFromCookie('userId') || localStorageUserId;
+  scope._sessionId = _getFromCookie('sessionId') || localStorageSessionId || scope._sessionId;
+  scope._lastEventTime = _getFromCookie('lastEventTime') || localStorageLastEventTime || scope._lastEventTime;
+  scope._eventId = _getFromCookie('eventId') || localStorageEventId || scope._eventId;
+  scope._identifyId = _getFromCookie('identifyId') || localStorageIdentifyId || scope._identifyId;
+  scope._sequenceNumber = _getFromCookie('sequenceNumber') || localStorageSequenceNumber || scope._sequenceNumber;
+
+  // optOut is a little trickier since it is a boolean
+  scope.options.optOut = localStorageOptOut || false;
+  if (cookieData && cookieData.optOut !== undefined && cookieData.optOut !== null) {
+    scope.options.optOut = String(cookieData.optOut) === 'true';
+  }
+
+  _saveCookieData(scope);
+};
+
+/**
+ * Fetches deviceId, userId, event meta data from amplitude cookie
+ * @private
+ */
+var _loadCookieData = function _loadCookieData(scope) {
+  var cookieData = scope.cookieStorage.get(scope.options.cookieName);
+  if (type(cookieData) === 'object') {
+    if (cookieData.deviceId) {
+      scope.options.deviceId = cookieData.deviceId;
+    }
+    if (cookieData.userId) {
+      scope.options.userId = cookieData.userId;
+    }
+    if (cookieData.optOut !== null && cookieData.optOut !== undefined) {
+      scope.options.optOut = cookieData.optOut;
+    }
+    if (cookieData.sessionId) {
+      scope._sessionId = parseInt(cookieData.sessionId);
+    }
+    if (cookieData.lastEventTime) {
+      scope._lastEventTime = parseInt(cookieData.lastEventTime);
+    }
+    if (cookieData.eventId) {
+      scope._eventId = parseInt(cookieData.eventId);
+    }
+    if (cookieData.identifyId) {
+      scope._identifyId = parseInt(cookieData.identifyId);
+    }
+    if (cookieData.sequenceNumber) {
+      scope._sequenceNumber = parseInt(cookieData.sequenceNumber);
+    }
+  }
+};
+
+/**
+ * Saves deviceId, userId, event meta data to amplitude cookie
+ * @private
+ */
+var _saveCookieData = function _saveCookieData(scope) {
+  scope.cookieStorage.set(scope.options.cookieName, {
+    deviceId: scope.options.deviceId,
+    userId: scope.options.userId,
+    optOut: scope.options.optOut,
+    sessionId: scope._sessionId,
+    lastEventTime: scope._lastEventTime,
+    eventId: scope._eventId,
+    identifyId: scope._identifyId,
+    sequenceNumber: scope._sequenceNumber
+  });
+};
+
+/**
+ * Parse the utm properties out of cookies and query for adding to user properties.
+ * @private
+ */
+Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) {
+  queryParams = queryParams || location.search;
+  cookieParams = cookieParams || this.cookieStorage.get('__utmz');
+  var utmProperties = getUtmData(cookieParams, queryParams);
+  _sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties);
+};
+
+/**
+ * Since user properties are propagated on server, only send once per session, don't need to send with every event
+ * @private
+ */
+var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) {
+  if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) {
+    return;
+  }
+
+  // setOnce the initial user properties
+  var identify = new Identify();
+  for (var key in userProperties) {
+    if (userProperties.hasOwnProperty(key)) {
+      identify.setOnce('initial_' + key, userProperties[key]);
+    }
+  }
+
+  // only save userProperties if not already in sessionStorage under key or if storage disabled
+  var hasSessionStorage = utils.sessionStorageEnabled();
+  if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) {
+    for (var property in userProperties) {
+      if (userProperties.hasOwnProperty(property)) {
+        identify.set(property, userProperties[property]);
+      }
+    }
+
+    if (hasSessionStorage) {
+      scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties));
+    }
+  }
+
+  scope.identify(identify);
+};
+
+/**
+ * @private
+ */
+Amplitude.prototype._getReferrer = function _getReferrer() {
+  return document.referrer;
+};
+
+/**
+ * Parse the domain from referrer info
+ * @private
+ */
+Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) {
+  if (utils.isEmptyString(referrer)) {
+    return null;
+  }
+  var parts = referrer.split('/');
+  if (parts.length >= 3) {
+    return parts[2];
+  }
+  return null;
+};
+
+/**
+ * Fetch the referrer information, parse the domain and send.
+ * Since user properties are propagated on the server, only send once per session, don't need to send with every event
+ * @private
+ */
+Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) {
+  if (utils.isEmptyString(referrer)) {
+    return;
+  }
+  var referrerInfo = {
+    'referrer': referrer,
+    'referring_domain': this._getReferringDomain(referrer)
+  };
+  _sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo);
+};
+
+/**
+ * Saves unsent events and identifies to localStorage. JSON stringifies event queues before saving.
+ * Note: this is called automatically every time events are logged, unless you explicitly set option saveEvents to false.
+ * @private
+ */
+Amplitude.prototype.saveEvents = function saveEvents() {
+  try {
+    this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents));
+  } catch (e) {}
+
+  try {
+    this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys));
+  } catch (e) {}
+};
+
+/**
+ * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking.
+ * @public
+ * @param {string} domain to set.
+ * @example amplitude.setDomain('.amplitude.com');
+ */
+Amplitude.prototype.setDomain = function setDomain(domain) {
+  if (!utils.validateInput(domain, 'domain', 'string')) {
+    return;
+  }
+
+  try {
+    this.cookieStorage.options({
+      domain: domain
+    });
+    this.options.domain = this.cookieStorage.options().domain;
+    _loadCookieData(this);
+    _saveCookieData(this);
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+ * Sets an identifier for the current user.
+ * @public
+ * @param {string} userId - identifier to set. Can be null.
+ * @example amplitude.setUserId('joe@gmail.com');
+ */
+Amplitude.prototype.setUserId = function setUserId(userId) {
+  try {
+    this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null;
+    _saveCookieData(this);
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+ * Sets whether to opt current user out of tracking.
+ * @public
+ * @param {boolean} enable - if true then no events will be logged or sent.
+ * @example: amplitude.setOptOut(true);
+ */
+Amplitude.prototype.setOptOut = function setOptOut(enable) {
+  if (!utils.validateInput(enable, 'enable', 'boolean')) {
+    return;
+  }
+
+  try {
+    this.options.optOut = enable;
+    _saveCookieData(this);
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+  * Sets a custom deviceId for current user. Note: this is not recommended unless you know what you are doing
+  * (like if you have your own system for managing deviceIds). Make sure the deviceId you set is sufficiently unique
+  * (we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system.
+  * @public
+  * @param {string} deviceId - custom deviceId for current user.
+  * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0');
+  */
+Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) {
+  if (!utils.validateInput(deviceId, 'deviceId', 'string')) {
+    return;
+  }
+
+  try {
+    if (!utils.isEmptyString(deviceId)) {
+      this.options.deviceId = ('' + deviceId);
+      _saveCookieData(this);
+    }
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+ * Sets user properties for the current user.
+ * @public
+ * @param {object} - object with string keys and values for the user properties to set.
+ * @param {boolean} - DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in
+ * memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated.
+ * @example amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true})
+ */
+Amplitude.prototype.setUserProperties = function setUserProperties(userProperties) {
+  if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) {
+    return;
+  }
+  // convert userProperties into an identify call
+  var identify = new Identify();
+  for (var property in userProperties) {
+    if (userProperties.hasOwnProperty(property)) {
+      identify.set(property, userProperties[property]);
+    }
+  }
+  this.identify(identify);
+};
+
+/**
+ * Clear all of the user properties for the current user. Note: clearing user properties is irreversible!
+ * @public
+ * @example amplitude.clearUserProperties();
+ */
+Amplitude.prototype.clearUserProperties = function clearUserProperties(){
+  if (!this._apiKeySet('clearUserProperties()')) {
+    return;
+  }
+
+  var identify = new Identify();
+  identify.clearAll();
+  this.identify(identify);
+};
+
+/**
+ * Send an identify call containing user property operations to Amplitude servers.
+ * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations}
+ * for more information on the Identify API and user property operations.
+ * @param {Identify} identify_obj - the Identify object containing the user property operations to send.
+ * @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent.
+ * Note: the server response code and response body from the identify event upload are passed to the callback function.
+ * @example
+ * var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31');
+ * amplitude.identify(identify);
+ */
+Amplitude.prototype.identify = function(identify_obj, opt_callback) {
+  if (!this._apiKeySet('identify()')) {
+    if (type(opt_callback) === 'function') {
+      opt_callback(0, 'No request sent');
+    }
+    return;
+  }
+
+  // if identify input is a proxied object created by the async loading snippet, convert it into an identify object
+  if (type(identify_obj) === 'object' && identify_obj.hasOwnProperty('_q')) {
+    var instance = new Identify();
+    for (var i = 0; i < identify_obj._q.length; i++) {
+        var fn = instance[identify_obj._q[i][0]];
+        if (type(fn) === 'function') {
+          fn.apply(instance, identify_obj._q[i].slice(1));
+        }
+    }
+    identify_obj = instance;
+  }
+
+  if (identify_obj instanceof Identify) {
+    // only send if there are operations
+    if (Object.keys(identify_obj.userPropertiesOperations).length > 0) {
+      return this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, opt_callback);
+    }
+  } else {
+    utils.log('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj));
+  }
+
+  if (type(opt_callback) === 'function') {
+    opt_callback(0, 'No request sent');
+  }
+};
+
+/**
+ * Set a versionName for your application.
+ * @public
+ * @param {string} versionName
+ * @example amplitude.setVersionName('1.12.3');
+ */
+Amplitude.prototype.setVersionName = function setVersionName(versionName) {
+  if (!utils.validateInput(versionName, 'versionName', 'string')) {
+    return;
+  }
+  this.options.versionName = versionName;
+};
+
+/**
+ * Private logEvent method. Keeps apiProperties from being publicly exposed.
+ * @private
+ */
+Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, callback) {
+  _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs
+  if (!eventType || this.options.optOut) {
+    if (type(callback) === 'function') {
+      callback(0, 'No request sent');
+    }
+    return;
+  }
+
+  try {
+    var eventId;
+    if (eventType === Constants.IDENTIFY_EVENT) {
+      eventId = this.nextIdentifyId();
+    } else {
+      eventId = this.nextEventId();
+    }
+    var sequenceNumber = this.nextSequenceNumber();
+    var eventTime = new Date().getTime();
+    if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) {
+      this._sessionId = eventTime;
+    }
+    this._lastEventTime = eventTime;
+    _saveCookieData(this);
+
+    userProperties = userProperties || {};
+    apiProperties = apiProperties || {};
+    eventProperties = eventProperties || {};
+    var event = {
+      device_id: this.options.deviceId,
+      user_id: this.options.userId,
+      timestamp: eventTime,
+      event_id: eventId,
+      session_id: this._sessionId || -1,
+      event_type: eventType,
+      version_name: this.options.versionName || null,
+      platform: this.options.platform,
+      os_name: this._ua.browser.name || null,
+      os_version: this._ua.browser.major || null,
+      device_model: this._ua.os.name || null,
+      language: this.options.language,
+      api_properties: apiProperties,
+      event_properties: utils.truncate(utils.validateProperties(eventProperties)),
+      user_properties: utils.truncate(utils.validateProperties(userProperties)),
+      uuid: UUID(),
+      library: {
+        name: 'amplitude-js',
+        version: version
+      },
+      sequence_number: sequenceNumber // for ordering events and identifys
+      // country: null
+    };
+
+    if (eventType === Constants.IDENTIFY_EVENT) {
+      this._unsentIdentifys.push(event);
+      this._limitEventsQueued(this._unsentIdentifys);
+    } else {
+      this._unsentEvents.push(event);
+      this._limitEventsQueued(this._unsentEvents);
+    }
+
+    if (this.options.saveEvents) {
+      this.saveEvents();
+    }
+
+    if (!this._sendEventsIfReady(callback) && type(callback) === 'function') {
+      callback(0, 'No request sent');
+    }
+
+    return eventId;
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+ * Remove old events from the beginning of the array if too many have accumulated. Default limit is 1000 events.
+ * @private
+ */
+Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) {
+  if (queue.length > this.options.savedMaxCount) {
+    queue.splice(0, queue.length - this.options.savedMaxCount);
+  }
+};
+
+/**
+ * This is the callback for logEvent and identify calls. It gets called after the event/identify is uploaded,
+ * and the server response code and response body from the upload request are passed to the callback function.
+ * @callback Amplitude~eventCallback
+ * @param {number} responseCode - Server response code for the event / identify upload request.
+ * @param {string} responseBody - Server response body for the event / identify upload request.
+ */
+
+/**
+ * Log an event with eventType and eventProperties
+ * @public
+ * @param {string} eventType - name of event
+ * @param {object} eventProperties - (optional) an object with string keys and values for the event properties.
+ * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged.
+ * Note: the server response code and response body from the event upload are passed to the callback function.
+ * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
+ */
+Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) {
+  if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') ||
+        utils.isEmptyString(eventType)) {
+    if (type(opt_callback) === 'function') {
+      opt_callback(0, 'No request sent');
+    }
+    return -1;
+  }
+  return this._logEvent(eventType, eventProperties, null, null, opt_callback);
+};
+
+/**
+ * Test that n is a number or a numeric value.
+ * @private
+ */
+var _isNumber = function _isNumber(n) {
+  return !isNaN(parseFloat(n)) && isFinite(n);
+};
+
+/**
+ * Log revenue event with a price, quantity, and product identifier.
+ * @public
+ * @param {number} price - price of revenue event
+ * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1.
+ * @param {string} product - (optional) product identifier
+ * @example amplitude.logRevenue(3.99, 1, 'product_1234');
+ */
+Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) {
+  // Test that the parameters are of the right type.
+  if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) {
+    // utils.log('Price and quantity arguments to logRevenue must be numbers');
+    return -1;
+  }
+
+  return this._logEvent('revenue_amount', {}, {
+    productId: product,
+    special: 'revenue_amount',
+    quantity: quantity || 1,
+    price: price
+  });
+};
+
+/**
+ * Remove events in storage with event ids up to and including maxEventId.
+ * @private
+ */
+Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) {
+  _removeEvents(this, '_unsentEvents', maxEventId);
+  _removeEvents(this, '_unsentIdentifys', maxIdentifyId);
+};
+
+/**
+ * Helper function to remove events up to maxId from a single queue.
+ * Does a true filter in case events get out of order or old events are removed.
+ * @private
+ */
+var _removeEvents = function _removeEvents(scope, eventQueue, maxId) {
+  if (maxId < 0) {
+    return;
+  }
+
+  var filteredEvents = [];
+  for (var i = 0; i < scope[eventQueue].length || 0; i++) {
+    if (scope[eventQueue][i].event_id > maxId) {
+      filteredEvents.push(scope[eventQueue][i]);
+    }
+  }
+  scope[eventQueue] = filteredEvents;
+};
+
+/**
+ * Send unsent events. Note: this is called automatically after events are logged if option batchEvents is false.
+ * If batchEvents is true, then events are only sent when batch criterias are met.
+ * @private
+ * @param {Amplitude~eventCallback} callback - (optional) callback to run after events are sent.
+ * Note the server response code and response body are passed to the callback as input arguments.
+ */
+Amplitude.prototype.sendEvents = function sendEvents(callback) {
+  if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) {
+    if (type(callback) === 'function') {
+      callback(0, 'No request sent');
+    }
+    return;
+  }
+
+  this._sending = true;
+  var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + this.options.apiEndpoint + '/';
+
+  // fetch events to send
+  var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize);
+  var mergedEvents = this._mergeEventsAndIdentifys(numEvents);
+  var maxEventId = mergedEvents.maxEventId;
+  var maxIdentifyId = mergedEvents.maxIdentifyId;
+  var events = JSON.stringify(mergedEvents.eventsToSend);
+  var uploadTime = new Date().getTime();
+
+  var data = {
+    client: this.options.apiKey,
+    e: events,
+    v: Constants.API_VERSION,
+    upload_time: uploadTime,
+    checksum: md5(Constants.API_VERSION + this.options.apiKey + events + uploadTime)
+  };
+
+  var scope = this;
+  new Request(url, data).send(function(status, response) {
+    scope._sending = false;
+    try {
+      if (status === 200 && response === 'success') {
+        scope.removeEvents(maxEventId, maxIdentifyId);
+
+        // Update the event cache after the removal of sent events.
+        if (scope.options.saveEvents) {
+          scope.saveEvents();
+        }
+
+        // Send more events if any queued during previous send.
+        if (!scope._sendEventsIfReady(callback) && type(callback) === 'function') {
+          callback(status, response);
+        }
+
+      // handle payload too large
+      } else if (status === 413) {
+        // utils.log('request too large');
+        // Can't even get this one massive event through. Drop it, even if it is an identify.
+        if (scope.options.uploadBatchSize === 1) {
+          scope.removeEvents(maxEventId, maxIdentifyId);
+        }
+
+        // The server complained about the length of the request. Backoff and try again.
+        scope.options.uploadBatchSize = Math.ceil(numEvents / 2);
+        scope.sendEvents(callback);
+
+      } else if (type(callback) === 'function') { // If server turns something like a 400
+        callback(status, response);
+      }
+    } catch (e) {
+      // utils.log('failed upload');
+    }
+  });
+};
+
+/**
+ * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading.
+ * @private
+ */
+Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) {
+  // coalesce events from both queues
+  var eventsToSend = [];
+  var eventIndex = 0;
+  var maxEventId = -1;
+  var identifyIndex = 0;
+  var maxIdentifyId = -1;
+
+  while (eventsToSend.length < numEvents) {
+    var event;
+    var noIdentifys = identifyIndex >= this._unsentIdentifys.length;
+    var noEvents = eventIndex >= this._unsentEvents.length;
+
+    // case 0: no events or identifys left
+    // note this should not happen, this means we have less events and identifys than expected
+    if (noEvents && noIdentifys) {
+      utils.log('Merging Events and Identifys, less events and identifys than expected');
+      break;
+    }
+
+    // case 1: no identifys - grab from events
+    else if (noIdentifys) {
+      event = this._unsentEvents[eventIndex++];
+      maxEventId = event.event_id;
+
+    // case 2: no events - grab from identifys
+    } else if (noEvents) {
+      event = this._unsentIdentifys[identifyIndex++];
+      maxIdentifyId = event.event_id;
+
+    // case 3: need to compare sequence numbers
+    } else {
+      // events logged before v2.5.0 won't have a sequence number, put those first
+      if (!('sequence_number' in this._unsentEvents[eventIndex]) ||
+          this._unsentEvents[eventIndex].sequence_number <
+          this._unsentIdentifys[identifyIndex].sequence_number) {
+        event = this._unsentEvents[eventIndex++];
+        maxEventId = event.event_id;
+      } else {
+        event = this._unsentIdentifys[identifyIndex++];
+        maxIdentifyId = event.event_id;
+      }
+    }
+
+    eventsToSend.push(event);
+  }
+
+  return {
+    eventsToSend: eventsToSend,
+    maxEventId: maxEventId,
+    maxIdentifyId: maxIdentifyId
+  };
+};
+
+/**
+ * Set global user properties. Note this is deprecated, and we recommend using setUserProperties
+ * @public
+ * @deprecated
+ */
+Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) {
+  this.setUserProperties(userProperties);
+};
+
+/**
+ * Get the current version of Amplitude's Javascript SDK.
+ * @public
+ * @returns {number} version number
+ * @example var amplitudeVersion = amplitude.__VERSION__;
+ */
+Amplitude.prototype.__VERSION__ = version;
+
+module.exports = Amplitude;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Tue Apr 05 2016 14:28:54 GMT-0700 (PDT) +
+ + + + + diff --git a/documentation/fonts/OpenSans-Bold-webfont.eot b/documentation/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 00000000..5d20d916 Binary files /dev/null and b/documentation/fonts/OpenSans-Bold-webfont.eot differ diff --git a/documentation/fonts/OpenSans-Bold-webfont.svg b/documentation/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 00000000..3ed7be4b --- /dev/null +++ b/documentation/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/fonts/OpenSans-Bold-webfont.woff b/documentation/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 00000000..1205787b Binary files /dev/null and b/documentation/fonts/OpenSans-Bold-webfont.woff differ diff --git a/documentation/fonts/OpenSans-BoldItalic-webfont.eot b/documentation/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 00000000..1f639a15 Binary files /dev/null and b/documentation/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/documentation/fonts/OpenSans-BoldItalic-webfont.svg b/documentation/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 00000000..6a2607b9 --- /dev/null +++ b/documentation/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/fonts/OpenSans-BoldItalic-webfont.woff b/documentation/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 00000000..ed760c06 Binary files /dev/null and b/documentation/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/documentation/fonts/OpenSans-Italic-webfont.eot b/documentation/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 00000000..0c8a0ae0 Binary files /dev/null and b/documentation/fonts/OpenSans-Italic-webfont.eot differ diff --git a/documentation/fonts/OpenSans-Italic-webfont.svg b/documentation/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 00000000..e1075dcc --- /dev/null +++ b/documentation/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/fonts/OpenSans-Italic-webfont.woff b/documentation/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 00000000..ff652e64 Binary files /dev/null and b/documentation/fonts/OpenSans-Italic-webfont.woff differ diff --git a/documentation/fonts/OpenSans-Light-webfont.eot b/documentation/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 00000000..14868406 Binary files /dev/null and b/documentation/fonts/OpenSans-Light-webfont.eot differ diff --git a/documentation/fonts/OpenSans-Light-webfont.svg b/documentation/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 00000000..11a472ca --- /dev/null +++ b/documentation/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/fonts/OpenSans-Light-webfont.woff b/documentation/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 00000000..e7860748 Binary files /dev/null and b/documentation/fonts/OpenSans-Light-webfont.woff differ diff --git a/documentation/fonts/OpenSans-LightItalic-webfont.eot b/documentation/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 00000000..8f445929 Binary files /dev/null and b/documentation/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/documentation/fonts/OpenSans-LightItalic-webfont.svg b/documentation/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 00000000..431d7e35 --- /dev/null +++ b/documentation/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/fonts/OpenSans-LightItalic-webfont.woff b/documentation/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 00000000..43e8b9e6 Binary files /dev/null and b/documentation/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/documentation/fonts/OpenSans-Regular-webfont.eot b/documentation/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 00000000..6bbc3cf5 Binary files /dev/null and b/documentation/fonts/OpenSans-Regular-webfont.eot differ diff --git a/documentation/fonts/OpenSans-Regular-webfont.svg b/documentation/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 00000000..25a39523 --- /dev/null +++ b/documentation/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/fonts/OpenSans-Regular-webfont.woff b/documentation/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 00000000..e231183d Binary files /dev/null and b/documentation/fonts/OpenSans-Regular-webfont.woff differ diff --git a/documentation/identify.js.html b/documentation/identify.js.html new file mode 100644 index 00000000..a31ee4ea --- /dev/null +++ b/documentation/identify.js.html @@ -0,0 +1,235 @@ + + + + + JSDoc: Source: identify.js + + + + + + + + + + +
+ +

Source: identify.js

+ + + + + + +
+
+
var type = require('./type');
+var utils = require('./utils');
+
+/*
+ * Wrapper for a user properties JSON object that supports operations.
+ * Note: if a user property is used in multiple operations on the same Identify object,
+ * only the first operation will be saved, and the rest will be ignored.
+ */
+
+var AMP_OP_ADD = '$add';
+var AMP_OP_APPEND = '$append';
+var AMP_OP_CLEAR_ALL = '$clearAll';
+var AMP_OP_PREPEND = '$prepend';
+var AMP_OP_SET = '$set';
+var AMP_OP_SET_ONCE = '$setOnce';
+var AMP_OP_UNSET = '$unset';
+
+/**
+ * Identify API - instance constructor. Identify objects are a wrapper for user property operations.
+ * Each method adds a user property operation to the Identify object, and returns the same Identify object,
+ * allowing you to chain multiple method calls together.
+ * Note: if the same user property is used in multiple operations on a single Identify object,
+ * only the first operation on that property will be saved, and the rest will be ignored.
+ * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations}
+ * for more information on the Identify API and user property operations.
+ * @constructor Identify
+ * @public
+ * @example var identify = new amplitude.Identify();
+ */
+var Identify = function() {
+  this.userPropertiesOperations = {};
+  this.properties = []; // keep track of keys that have been added
+};
+
+/**
+ * Increment a user property by a given value (can also be negative to decrement).
+ * If the user property does not have a value set yet, it will be initialized to 0 before being incremented.
+ * @public
+ * @param {string} property - The user property key.
+ * @param {number|string} value - The amount by which to increment the user property. Allows numbers as strings (ex: '123').
+ * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together.
+ * @example var identify = new amplitude.Identify().add('karma', 1).add('friends', 1);
+ * amplitude.identify(identify); // send the Identify call
+ */
+Identify.prototype.add = function(property, value) {
+  if (type(value) === 'number' || type(value) === 'string') {
+    this._addOperation(AMP_OP_ADD, property, value);
+  } else {
+    utils.log('Unsupported type for value: ' + type(value) + ', expecting number or string');
+  }
+  return this;
+};
+
+/**
+ * Append a value or values to a user property.
+ * If the user property does not have a value set yet,
+ * it will be initialized to an empty list before the new values are appended.
+ * If the user property has an existing value and it is not a list,
+ * the existing value will be converted into a list with the new values appended.
+ * @public
+ * @param {string} property - The user property key.
+ * @param {number|string|list|object} value - A value or values to append.
+ * Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together.
+ * @example var identify = new amplitude.Identify().append('ab-tests', 'new-user-tests');
+ * identify.append('some_list', [1, 2, 3, 4, 'values']);
+ * amplitude.identify(identify); // send the Identify call
+ */
+Identify.prototype.append = function(property, value) {
+  this._addOperation(AMP_OP_APPEND, property, value);
+  return this;
+};
+
+/**
+ * Clear all user properties for the current user.
+ * SDK user should instead call amplitude.clearUserProperties() instead of using this.
+ * $clearAll needs to be sent on its own Identify object. If there are already other operations, then don't add $clearAll.
+ * If $clearAll already in an Identify object, don't allow other operations to be added.
+ * @private
+ */
+Identify.prototype.clearAll = function() {
+  if (Object.keys(this.userPropertiesOperations).length > 0) {
+    if (!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) {
+      utils.log('Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll');
+    }
+    return this;
+  }
+  this.userPropertiesOperations[AMP_OP_CLEAR_ALL] = '-';
+  return this;
+};
+
+/**
+ * Prepend a value or values to a user property.
+ * Prepend means inserting the value or values at the front of a list.
+ * If the user property does not have a value set yet,
+ * it will be initialized to an empty list before the new values are prepended.
+ * If the user property has an existing value and it is not a list,
+ * the existing value will be converted into a list with the new values prepended.
+ * @public
+ * @param {string} property - The user property key.
+ * @param {number|string|list|object} value - A value or values to prepend.
+ * Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together.
+ * @example var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-tests');
+ * identify.prepend('some_list', [1, 2, 3, 4, 'values']);
+ * amplitude.identify(identify); // send the Identify call
+ */
+Identify.prototype.prepend = function(property, value) {
+  this._addOperation(AMP_OP_PREPEND, property, value);
+  return this;
+};
+
+/**
+ * Sets the value of a given user property. If a value already exists, it will be overwriten with the new value.
+ * @public
+ * @param {string} property - The user property key.
+ * @param {number|string|list|object} value - A value or values to set.
+ * Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together.
+ * @example var identify = new amplitude.Identify().set('user_type', 'beta');
+ * identify.set('name', {'first': 'John', 'last': 'Doe'}); // dict is flattened and becomes name.first: John, name.last: Doe
+ * amplitude.identify(identify); // send the Identify call
+ */
+Identify.prototype.set = function(property, value) {
+  this._addOperation(AMP_OP_SET, property, value);
+  return this;
+};
+
+/**
+ * Sets the value of a given user property only once. Subsequent setOnce operations on that user property will be ignored;
+ * however, that user property can still be modified through any of the other operations.
+ * Useful for capturing properties such as 'initial_signup_date', 'initial_referrer', etc.
+ * @public
+ * @param {string} property - The user property key.
+ * @param {number|string|list|object} value - A value or values to set once.
+ * Values can be numbers, strings, lists, or object (key:value dict will be flattened).
+ * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together.
+ * @example var identify = new amplitude.Identify().setOnce('sign_up_date', '2016-04-01');
+ * amplitude.identify(identify); // send the Identify call
+ */
+Identify.prototype.setOnce = function(property, value) {
+  this._addOperation(AMP_OP_SET_ONCE, property, value);
+  return this;
+};
+
+/**
+ * Unset and remove a user property. This user property will no longer show up in a user's profile.
+ * @public
+ * @param {string} property - The user property key.
+ * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together.
+ * @example var identify = new amplitude.Identify().unset('user_type').unset('age');
+ * amplitude.identify(identify); // send the Identify call
+ */
+Identify.prototype.unset = function(property) {
+  this._addOperation(AMP_OP_UNSET, property, '-');
+  return this;
+};
+
+/**
+ * Helper function that adds operation to the Identify's object
+ * Handle's filtering of duplicate user property keys, and filtering for clearAll.
+ * @private
+ */
+Identify.prototype._addOperation = function(operation, property, value) {
+  // check that the identify doesn't already contain a clearAll
+  if (this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) {
+    utils.log('This identify already contains a $clearAll operation, skipping operation ' + operation);
+    return;
+  }
+
+  // check that property wasn't already used in this Identify
+  if (this.properties.indexOf(property) !== -1) {
+    utils.log('User property "' + property + '" already used in this identify, skipping operation ' + operation);
+    return;
+  }
+
+  if (!this.userPropertiesOperations.hasOwnProperty(operation)){
+    this.userPropertiesOperations[operation] = {};
+  }
+  this.userPropertiesOperations[operation][property] = value;
+  this.properties.push(property);
+};
+
+module.exports = Identify;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Tue Apr 05 2016 14:28:54 GMT-0700 (PDT) +
+ + + + + diff --git a/documentation/index.html b/documentation/index.html new file mode 100644 index 00000000..19deff0d --- /dev/null +++ b/documentation/index.html @@ -0,0 +1,65 @@ + + + + + JSDoc: Home + + + + + + + + + + +
+ +

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Tue Apr 05 2016 14:28:54 GMT-0700 (PDT) +
+ + + + + \ No newline at end of file diff --git a/documentation/scripts/linenumber.js b/documentation/scripts/linenumber.js new file mode 100644 index 00000000..8d52f7ea --- /dev/null +++ b/documentation/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(function() { + var source = document.getElementsByClassName('prettyprint source linenums'); + var i = 0; + var lineNumber = 0; + var lineId; + var lines; + var totalLines; + var anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = 'line' + lineNumber; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/documentation/scripts/prettify/Apache-License-2.0.txt b/documentation/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/documentation/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/documentation/scripts/prettify/lang-css.js b/documentation/scripts/prettify/lang-css.js new file mode 100644 index 00000000..041e1f59 --- /dev/null +++ b/documentation/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/documentation/scripts/prettify/prettify.js b/documentation/scripts/prettify/prettify.js new file mode 100644 index 00000000..eef5ad7e --- /dev/null +++ b/documentation/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/documentation/styles/prettify-jsdoc.css b/documentation/styles/prettify-jsdoc.css new file mode 100644 index 00000000..5a2526e3 --- /dev/null +++ b/documentation/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/documentation/styles/prettify-tomorrow.css b/documentation/styles/prettify-tomorrow.css new file mode 100644 index 00000000..b6f92a78 --- /dev/null +++ b/documentation/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/package.json b/package.json index 3083ef7d..aca8c3ab 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "duo": "0.8", "express": "4.x", "fs-extra": "^0.15.0", + "jsdoc": "3.4.0", "jshint": "^2.6.3", "mocha-phantomjs": "^3.6.0", "uglify-js": ">= 1.3.4" diff --git a/src/amplitude.js b/src/amplitude.js index 5315f8a5..22181466 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -1,3 +1,4 @@ +var Constants = require('./constants'); var cookieStorage = require('./cookiestorage'); var getUtmData = require('./utm'); var Identify = require('./identify'); @@ -13,83 +14,53 @@ var UUID = require('./uuid'); var version = require('./version'); var DEFAULT_OPTIONS = require('./options'); -var IDENTIFY_EVENT = '$identify'; -var API_VERSION = 2; -var MAX_STRING_LENGTH = 1024; -var LocalStorageKeys = { - LAST_EVENT_ID: 'amplitude_lastEventId', - LAST_EVENT_TIME: 'amplitude_lastEventTime', - LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', - LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', - REFERRER: 'amplitude_referrer', - SESSION_ID: 'amplitude_sessionId', - - // Used in cookie as well - DEVICE_ID: 'amplitude_deviceId', - OPT_OUT: 'amplitude_optOut', - USER_ID: 'amplitude_userId' -}; - -/* - * Amplitude API +/** + * Amplitude SDK API - instance constructor. + * @constructor Amplitude + * @public + * @example var amplitude = new Amplitude(); */ -var Amplitude = function() { +var Amplitude = function Amplitude() { this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); this.options = object.merge({}, DEFAULT_OPTIONS); this.cookieStorage = new cookieStorage().getStorage(); this._q = []; // queue for proxied functions before script load -}; + this._sending = false; + this._updateScheduled = false; -Amplitude.prototype._eventId = 0; -Amplitude.prototype._identifyId = 0; -Amplitude.prototype._sequenceNumber = 0; -Amplitude.prototype._sending = false; -Amplitude.prototype._lastEventTime = null; -Amplitude.prototype._sessionId = null; -Amplitude.prototype._newSession = false; -Amplitude.prototype._updateScheduled = false; + // event meta data + this._eventId = 0; + this._identifyId = 0; + this._lastEventTime = null; + this._newSession = false; + this._sequenceNumber = 0; + this._sessionId = null; +}; Amplitude.prototype.Identify = Identify; /** - * Initializes Amplitude. - * apiKey The API Key for your app - * opt_userId An identifier for this user - * opt_config Configuration options - * - saveEvents (boolean) Whether to save events to local storage. Defaults to true. - * - includeUtm (boolean) Whether to send utm parameters with events. Defaults to false. - * - includeReferrer (boolean) Whether to send referrer info with events. Defaults to false. + * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. + * This is required before any other methods can be called. + * @public + * @param {string} apiKey - The API key for your app. + * @param {string} opt_userId - (optional) An identifier for this user. + * @param {object} opt_config - (optional) Configuration options. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#configuration-options} for list of options and default values. + * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete. + * @example amplitude.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); */ -Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { +Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { + if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { + utils.log('Invalid apiKey. Please re-initialize with a valid apiKey'); + return; + } + try { this.options.apiKey = apiKey; - if (opt_config) { - if (opt_config.saveEvents !== undefined) { - this.options.saveEvents = !!opt_config.saveEvents; - } - if (opt_config.domain !== undefined) { - this.options.domain = opt_config.domain; - } - if (opt_config.includeUtm !== undefined) { - this.options.includeUtm = !!opt_config.includeUtm; - } - if (opt_config.includeReferrer !== undefined) { - this.options.includeReferrer = !!opt_config.includeReferrer; - } - if (opt_config.batchEvents !== undefined) { - this.options.batchEvents = !!opt_config.batchEvents; - } - this.options.platform = opt_config.platform || this.options.platform; - this.options.language = opt_config.language || this.options.language; - this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout; - this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize; - this.options.eventUploadThreshold = opt_config.eventUploadThreshold || this.options.eventUploadThreshold; - this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount; - this.options.eventUploadPeriodMillis = opt_config.eventUploadPeriodMillis || this.options.eventUploadPeriodMillis; - } - + _parseConfig(this.options, opt_config); this.cookieStorage.options({ expirationDays: this.options.cookieExpiration, domain: this.options.domain @@ -99,10 +70,11 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { _upgradeCookeData(this); _loadCookieData(this); - this.options.deviceId = (opt_config && opt_config.deviceId !== undefined && - opt_config.deviceId !== null && opt_config.deviceId) || - this.options.deviceId || UUID(); - this.options.userId = (opt_userId !== undefined && opt_userId !== null && opt_userId) || this.options.userId || null; + // load deviceId and userId from input, or try to fetch existing value from cookie + this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' && + !utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R'; + this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) || + this.options.userId || null; var now = new Date().getTime(); if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) { @@ -112,12 +84,9 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { this._lastEventTime = now; _saveCookieData(this); - //utils.log('initialized with apiKey=' + apiKey); - //opt_userId !== undefined && opt_userId !== null && utils.log('initialized with userId=' + opt_userId); - if (this.options.saveEvents) { - this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey) || this._unsentEvents; - this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey) || this._unsentIdentifys; + this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey); + this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey); // validate event properties for unsent events for (var i = 0; i < this._unsentEvents.length; i++) { @@ -125,7 +94,13 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); } - this._sendEventsIfReady(); + // validate user properties for unsent identifys + for (var j = 0; j < this._unsentIdentifys.length; j++) { + var userProperties = this._unsentIdentifys[j].user_properties; + this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); + } + + this._sendEventsIfReady(); // try sending unsent events } if (this.options.includeUtm) { @@ -137,146 +112,239 @@ Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { } } catch (e) { utils.log(e); + } finally { + if (type(opt_callback) === 'function') { + opt_callback(); + } } +}; - if (callback && type(callback) === 'function') { - callback(); +/** + * Parse and validate user specified config values and overwrite existing option value + * DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values + * @private + */ +var _parseConfig = function _parseConfig(options, config) { + if (type(config) !== 'object') { + return; } + + // validates config value is defined, is the correct type, and some additional value sanity checks + var parseValidateAndLoad = function parseValidateAndLoad(key) { + if (!DEFAULT_OPTIONS.hasOwnProperty(key)) { + return; // skip bogus config values + } + + var inputValue = config[key]; + var expectedType = type(DEFAULT_OPTIONS[key]); + if (!utils.validateInput(inputValue, key + ' option', expectedType)) { + return; + } + if (expectedType === 'boolean') { + options[key] = !!inputValue; + } else if ((expectedType === 'string' && !utils.isEmptyString(inputValue)) || + (expectedType === 'number' && inputValue > 0)) { + options[key] = inputValue; + } + }; + + for (var key in config) { + if (config.hasOwnProperty(key)) { + parseValidateAndLoad(key); + } + } }; +/** + * Run functions queued up by proxy loading snippet + * @private + */ Amplitude.prototype.runQueuedFunctions = function () { for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; - if (fn && type(fn) === 'function') { + if (type(fn) === 'function') { fn.apply(this, this._q[i].slice(1)); } } this._q = []; // clear function queue after running }; -Amplitude.prototype._apiKeySet = function(methodName) { - if (!this.options.apiKey) { - utils.log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName); +/** + * Check that the apiKey is set before calling a function. Logs a warning message if not set. + * @private + */ +Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { + if (utils.isEmptyString(this.options.apiKey)) { + utils.log('Invalid apiKey. Please set a valid apiKey with init() before calling ' + methodName); return false; } return true; }; -Amplitude.prototype._loadSavedUnsentEvents = function(unsentKey) { +/** + * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. + * @private + */ +Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); - if (savedUnsentEventsString) { + if (utils.isEmptyString(savedUnsentEventsString)) { + return []; // new app, does not have any saved events + } + + if (type(savedUnsentEventsString) === 'string') { try { - return JSON.parse(savedUnsentEventsString); - } catch (e) { - // utils.log(e); - } + var events = JSON.parse(savedUnsentEventsString); + if (type(events) === 'array') { // handle case where JSON dumping of unsent events is corrupted + return events; + } + } catch (e) {} } - return null; + utils.log('Unable to load ' + unsentKey + ' events. Restart with a new empty queue.'); + return []; }; -Amplitude.prototype.isNewSession = function() { +/** + * Returns true if a new session was created during initialization, otherwise false. + * @public + * @return {boolean} Whether a new session was created during initialization. + */ +Amplitude.prototype.isNewSession = function isNewSession() { return this._newSession; }; -Amplitude.prototype.getSessionId = function() { +/** + * Returns the id of the current session. + * @public + * @return {number} Id of the current session. + */ +Amplitude.prototype.getSessionId = function getSessionId() { return this._sessionId; }; -Amplitude.prototype.nextEventId = function() { +/** + * Increments the eventId and returns it. + * @private + */ +Amplitude.prototype.nextEventId = function nextEventId() { this._eventId++; return this._eventId; }; -Amplitude.prototype.nextIdentifyId = function() { +/** + * Increments the identifyId and returns it. + * @private + */ +Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { this._identifyId++; return this._identifyId; }; -Amplitude.prototype.nextSequenceNumber = function() { +/** + * Increments the sequenceNumber and returns it. + * @private + */ +Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { this._sequenceNumber++; return this._sequenceNumber; }; -// returns the number of unsent events and identifys -Amplitude.prototype._unsentCount = function() { +/** + * Returns the total count of unsent events and identifys + * @private + */ +Amplitude.prototype._unsentCount = function _unsentCount() { return this._unsentEvents.length + this._unsentIdentifys.length; }; -// returns true if sendEvents called immediately -Amplitude.prototype._sendEventsIfReady = function(callback) { +/** + * Send events if ready. Returns true if events are sent. + * @private + */ +Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { if (this._unsentCount() === 0) { return false; } + // if batching disabled, send any unsent events immediately if (!this.options.batchEvents) { this.sendEvents(callback); return true; } + // if batching enabled, check if min threshold met for batch size if (this._unsentCount() >= this.options.eventUploadThreshold) { this.sendEvents(callback); return true; } - if (!this._updateScheduled) { + // otherwise schedule an upload after 30s + if (!this._updateScheduled) { // make sure we only schedule 1 upload this._updateScheduled = true; - setTimeout( - function() { + setTimeout(function() { this._updateScheduled = false; this.sendEvents(); }.bind(this), this.options.eventUploadPeriodMillis ); } - return false; + return false; // an upload was scheduled, no events were uploaded }; -// storage argument allows for localStorage and sessionStorage -Amplitude.prototype._getFromStorage = function(storage, key) { +/** + * Helper function to fetch values from storage + * Storage argument allows for localStoraoge and sessionStoraoge + * @private + */ +Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { return storage.getItem(key); }; -// storage argument allows for localStorage and sessionStorage -Amplitude.prototype._setInStorage = function(storage, key, value) { +/** + * Helper function to set values in storage + * Storage argument allows for localStoraoge and sessionStoraoge + * @private + */ +Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) { storage.setItem(key, value); }; -/* +/** * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber) * can be stored in many different places (localStorage, cookie, etc). - * Need to unify all sources into one place with a one-time upgrade/migration for the defaultInstance. + * Need to unify all sources into one place with a one-time upgrade/migration. + * @private */ -var _upgradeCookeData = function(scope) { +var _upgradeCookeData = function _upgradeCookeData(scope) { // skip if migration already happened var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (cookieData && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { + if (type(cookieData) === 'object' && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { return; } - var _getAndRemoveFromLocalStorage = function(key) { + var _getAndRemoveFromLocalStorage = function _getAndRemoveFromLocalStorage(key) { var value = localStorage.getItem(key); localStorage.removeItem(key); return value; }; // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey - var apiKeySuffix = '_' + scope.options.apiKey.slice(0, 6); - var localStorageDeviceId = _getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID + apiKeySuffix); - var localStorageUserId = _getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID + apiKeySuffix); - var localStorageOptOut = _getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT + apiKeySuffix); + var apiKeySuffix = (type(scope.options.apiKey) === 'string' && ('_' + scope.options.apiKey.slice(0, 6))) || ''; + var localStorageDeviceId = _getAndRemoveFromLocalStorage(Constants.DEVICE_ID + apiKeySuffix); + var localStorageUserId = _getAndRemoveFromLocalStorage(Constants.USER_ID + apiKeySuffix); + var localStorageOptOut = _getAndRemoveFromLocalStorage(Constants.OPT_OUT + apiKeySuffix); if (localStorageOptOut !== null && localStorageOptOut !== undefined) { localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean } // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support - var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID)); - var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME)); - var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID)); - var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID)); - var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER)); - - var _getFromCookie = function(key) { - return cookieData && cookieData[key]; + var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID)); + var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME)); + var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID)); + var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID)); + var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER)); + + var _getFromCookie = function _getFromCookie(key) { + return type(cookieData) === 'object' && cookieData[key]; }; scope.options.deviceId = _getFromCookie('deviceId') || localStorageDeviceId; scope.options.userId = _getFromCookie('userId') || localStorageUserId; @@ -295,9 +363,13 @@ var _upgradeCookeData = function(scope) { _saveCookieData(scope); }; -var _loadCookieData = function(scope) { +/** + * Fetches deviceId, userId, event meta data from amplitude cookie + * @private + */ +var _loadCookieData = function _loadCookieData(scope) { var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (cookieData) { + if (type(cookieData) === 'object') { if (cookieData.deviceId) { scope.options.deviceId = cookieData.deviceId; } @@ -325,7 +397,11 @@ var _loadCookieData = function(scope) { } }; -var _saveCookieData = function(scope) { +/** + * Saves deviceId, userId, event meta data to amplitude cookie + * @private + */ +var _saveCookieData = function _saveCookieData(scope) { scope.cookieStorage.set(scope.options.cookieName, { deviceId: scope.options.deviceId, userId: scope.options.userId, @@ -340,75 +416,110 @@ var _saveCookieData = function(scope) { /** * Parse the utm properties out of cookies and query for adding to user properties. + * @private */ -Amplitude.prototype._initUtmData = function(queryParams, cookieParams) { +Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { queryParams = queryParams || location.search; cookieParams = cookieParams || this.cookieStorage.get('__utmz'); - this._utmProperties = getUtmData(cookieParams, queryParams); -}; - -Amplitude.prototype._getReferrer = function() { - return document.referrer; + var utmProperties = getUtmData(cookieParams, queryParams); + _sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties); }; -Amplitude.prototype._getReferringDomain = function(referrer) { - if (referrer === null || referrer === undefined || referrer === '') { - return null; - } - var parts = referrer.split('/'); - if (parts.length >= 3) { - return parts[2]; - } - return null; -}; - -// since user properties are propagated on the server, only send once per session, don't need to send with every event -Amplitude.prototype._saveReferrer = function(referrer) { - if (referrer === null || referrer === undefined || referrer === '') { +/** + * Since user properties are propagated on server, only send once per session, don't need to send with every event + * @private + */ +var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) { + if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) { return; } - // always setOnce initial referrer - var referring_domain = this._getReferringDomain(referrer); - var identify = new Identify().setOnce('initial_referrer', referrer); - identify.setOnce('initial_referring_domain', referring_domain); - - // only save referrer if not already in session storage or if storage disabled - var hasSessionStorage = false; - try { - if (window.sessionStorage) { - hasSessionStorage = true; + // setOnce the initial user properties + var identify = new Identify(); + for (var key in userProperties) { + if (userProperties.hasOwnProperty(key)) { + identify.setOnce('initial_' + key, userProperties[key]); } - } catch (e) { - // utils.log(e); // sessionStorage disabled } - if ((hasSessionStorage && !(this._getFromStorage(sessionStorage, LocalStorageKeys.REFERRER))) || !hasSessionStorage) { - identify.set('referrer', referrer).set('referring_domain', referring_domain); + // only save userProperties if not already in sessionStorage under key or if storage disabled + var hasSessionStorage = utils.sessionStorageEnabled(); + if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) { + for (var property in userProperties) { + if (userProperties.hasOwnProperty(property)) { + identify.set(property, userProperties[property]); + } + } if (hasSessionStorage) { - this._setInStorage(sessionStorage, LocalStorageKeys.REFERRER, referrer); + scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties)); } } - this.identify(identify); + scope.identify(identify); }; -Amplitude.prototype.saveEvents = function() { - if (!this._apiKeySet('saveEvents()')) { +/** + * @private + */ +Amplitude.prototype._getReferrer = function _getReferrer() { + return document.referrer; +}; + +/** + * Parse the domain from referrer info + * @private + */ +Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) { + if (utils.isEmptyString(referrer)) { + return null; + } + var parts = referrer.split('/'); + if (parts.length >= 3) { + return parts[2]; + } + return null; +}; + +/** + * Fetch the referrer information, parse the domain and send. + * Since user properties are propagated on the server, only send once per session, don't need to send with every event + * @private + */ +Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { + if (utils.isEmptyString(referrer)) { return; } + var referrerInfo = { + 'referrer': referrer, + 'referring_domain': this._getReferringDomain(referrer) + }; + _sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo); +}; +/** + * Saves unsent events and identifies to localStorage. JSON stringifies event queues before saving. + * Note: this is called automatically every time events are logged, unless you explicitly set option saveEvents to false. + * @private + */ +Amplitude.prototype.saveEvents = function saveEvents() { try { this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); + } catch (e) {} + + try { this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); - } catch (e) { - // utils.log(e); - } + } catch (e) {} }; -Amplitude.prototype.setDomain = function(domain) { - if (!this._apiKeySet('setDomain()')) { +/** + * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. + * @public + * @param {string} domain to set. + * @example amplitude.setDomain('.amplitude.com'); + */ +Amplitude.prototype.setDomain = function setDomain(domain) { + if (!utils.validateInput(domain, 'domain', 'string')) { return; } @@ -419,47 +530,60 @@ Amplitude.prototype.setDomain = function(domain) { this.options.domain = this.cookieStorage.options().domain; _loadCookieData(this); _saveCookieData(this); - // utils.log('set domain=' + domain); } catch (e) { utils.log(e); } }; -Amplitude.prototype.setUserId = function(userId) { - if (!this._apiKeySet('setUserId()')) { - return; - } - +/** + * Sets an identifier for the current user. + * @public + * @param {string} userId - identifier to set. Can be null. + * @example amplitude.setUserId('joe@gmail.com'); + */ +Amplitude.prototype.setUserId = function setUserId(userId) { try { this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; _saveCookieData(this); - // utils.log('set userId=' + userId); } catch (e) { utils.log(e); } }; -Amplitude.prototype.setOptOut = function(enable) { - if (!this._apiKeySet('setOptOut()')) { +/** + * Sets whether to opt current user out of tracking. + * @public + * @param {boolean} enable - if true then no events will be logged or sent. + * @example: amplitude.setOptOut(true); + */ +Amplitude.prototype.setOptOut = function setOptOut(enable) { + if (!utils.validateInput(enable, 'enable', 'boolean')) { return; } try { this.options.optOut = enable; _saveCookieData(this); - // utils.log('set optOut=' + enable); } catch (e) { utils.log(e); } }; -Amplitude.prototype.setDeviceId = function(deviceId) { - if (!this._apiKeySet('setDeviceId()')) { +/** + * Sets a custom deviceId for current user. Note: this is not recommended unless you know what you are doing + * (like if you have your own system for managing deviceIds). Make sure the deviceId you set is sufficiently unique + * (we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system. + * @public + * @param {string} deviceId - custom deviceId for current user. + * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); + */ +Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { + if (!utils.validateInput(deviceId, 'deviceId', 'string')) { return; } try { - if (deviceId) { + if (!utils.isEmptyString(deviceId)) { this.options.deviceId = ('' + deviceId); _saveCookieData(this); } @@ -468,8 +592,16 @@ Amplitude.prototype.setDeviceId = function(deviceId) { } }; -Amplitude.prototype.setUserProperties = function(userProperties) { - if (!this._apiKeySet('setUserProperties()')) { +/** + * Sets user properties for the current user. + * @public + * @param {object} - object with string keys and values for the user properties to set. + * @param {boolean} - DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in + * memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated. + * @example amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true}) + */ +Amplitude.prototype.setUserProperties = function setUserProperties(userProperties) { + if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { return; } // convert userProperties into an identify call @@ -482,8 +614,12 @@ Amplitude.prototype.setUserProperties = function(userProperties) { this.identify(identify); }; -// Clearing user properties is irreversible! -Amplitude.prototype.clearUserProperties = function(){ +/** + * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! + * @public + * @example amplitude.clearUserProperties(); + */ +Amplitude.prototype.clearUserProperties = function clearUserProperties(){ if (!this._apiKeySet('clearUserProperties()')) { return; } @@ -493,93 +629,86 @@ Amplitude.prototype.clearUserProperties = function(){ this.identify(identify); }; -Amplitude.prototype.identify = function(identify, callback) { +/** + * Send an identify call containing user property operations to Amplitude servers. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations} + * for more information on the Identify API and user property operations. + * @param {Identify} identify_obj - the Identify object containing the user property operations to send. + * @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent. + * Note: the server response code and response body from the identify event upload are passed to the callback function. + * @example + * var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31'); + * amplitude.identify(identify); + */ +Amplitude.prototype.identify = function(identify_obj, opt_callback) { if (!this._apiKeySet('identify()')) { - if (callback && type(callback) === 'function') { - callback(0, 'No request sent'); + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); } return; } - if (type(identify) === 'object' && '_q' in identify) { + // if identify input is a proxied object created by the async loading snippet, convert it into an identify object + if (type(identify_obj) === 'object' && identify_obj.hasOwnProperty('_q')) { var instance = new Identify(); - // Apply the queued commands - for (var i = 0; i < identify._q.length; i++) { - var fn = instance[identify._q[i][0]]; - if (fn && type(fn) === 'function') { - fn.apply(instance, identify._q[i].slice(1)); + for (var i = 0; i < identify_obj._q.length; i++) { + var fn = instance[identify_obj._q[i][0]]; + if (type(fn) === 'function') { + fn.apply(instance, identify_obj._q[i].slice(1)); } } - identify = instance; - } - - if (identify instanceof Identify && Object.keys(identify.userPropertiesOperations).length > 0) { - this._logEvent(IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, callback); - } else if (callback && type(callback) === 'function') { - callback(0, 'No request sent'); - } -}; - -Amplitude.prototype.setVersionName = function(versionName) { - try { - this.options.versionName = versionName; - // utils.log('set versionName=' + versionName); - } catch (e) { - utils.log(e); + identify_obj = instance; } -}; -// truncate string values in event and user properties so that request size does not get too large -Amplitude.prototype._truncate = function(value) { - if (type(value) === 'array') { - for (var i = 0; i < value.length; i++) { - value[i] = this._truncate(value[i]); - } - } else if (type(value) === 'object') { - for (var key in value) { - if (value.hasOwnProperty(key)) { - value[key] = this._truncate(value[key]); - } + if (identify_obj instanceof Identify) { + // only send if there are operations + if (Object.keys(identify_obj.userPropertiesOperations).length > 0) { + return this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, opt_callback); } } else { - value = _truncateValue(value); + utils.log('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj)); } - return value; + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } }; -var _truncateValue = function(value) { - if (type(value) === 'string') { - return value.length > MAX_STRING_LENGTH ? value.substring(0, MAX_STRING_LENGTH) : value; +/** + * Set a versionName for your application. + * @public + * @param {string} versionName + * @example amplitude.setVersionName('1.12.3'); + */ +Amplitude.prototype.setVersionName = function setVersionName(versionName) { + if (!utils.validateInput(versionName, 'versionName', 'string')) { + return; } - return value; + this.options.versionName = versionName; }; /** * Private logEvent method. Keeps apiProperties from being publicly exposed. + * @private */ -Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperties, userProperties, callback) { - if (type(callback) !== 'function') { - callback = null; - } - - _loadCookieData(this); +Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, callback) { + _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs if (!eventType || this.options.optOut) { - if (callback) { + if (type(callback) === 'function') { callback(0, 'No request sent'); } return; } + try { var eventId; - if (eventType === IDENTIFY_EVENT) { + if (eventType === Constants.IDENTIFY_EVENT) { eventId = this.nextIdentifyId(); } else { eventId = this.nextEventId(); } var sequenceNumber = this.nextSequenceNumber(); var eventTime = new Date().getTime(); - var ua = this._ua; if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { this._sessionId = eventTime; } @@ -587,29 +716,24 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti _saveCookieData(this); userProperties = userProperties || {}; - // Only add utm properties to user properties for events - if (eventType !== IDENTIFY_EVENT) { - object.merge(userProperties, this._utmProperties); - } - apiProperties = apiProperties || {}; eventProperties = eventProperties || {}; var event = { device_id: this.options.deviceId, - user_id: this.options.userId || this.options.deviceId, + user_id: this.options.userId, timestamp: eventTime, event_id: eventId, session_id: this._sessionId || -1, event_type: eventType, version_name: this.options.versionName || null, platform: this.options.platform, - os_name: ua.browser.name || null, - os_version: ua.browser.major || null, - device_model: ua.os.name || null, + os_name: this._ua.browser.name || null, + os_version: this._ua.browser.major || null, + device_model: this._ua.os.name || null, language: this.options.language, api_properties: apiProperties, - event_properties: this._truncate(utils.validateProperties(eventProperties)), - user_properties: this._truncate(userProperties), + event_properties: utils.truncate(utils.validateProperties(eventProperties)), + user_properties: utils.truncate(utils.validateProperties(userProperties)), uuid: UUID(), library: { name: 'amplitude-js', @@ -619,7 +743,7 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti // country: null }; - if (eventType === IDENTIFY_EVENT) { + if (eventType === Constants.IDENTIFY_EVENT) { this._unsentIdentifys.push(event); this._limitEventsQueued(this._unsentIdentifys); } else { @@ -631,7 +755,7 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti this.saveEvents(); } - if (!this._sendEventsIfReady(callback) && callback) { + if (!this._sendEventsIfReady(callback) && type(callback) === 'function') { callback(0, 'No request sent'); } @@ -641,32 +765,63 @@ Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperti } }; -// Remove old events from the beginning of the array if too many -// have accumulated. Don't want to kill memory. Default is 1000 events. -Amplitude.prototype._limitEventsQueued = function(queue) { +/** + * Remove old events from the beginning of the array if too many have accumulated. Default limit is 1000 events. + * @private + */ +Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { if (queue.length > this.options.savedMaxCount) { queue.splice(0, queue.length - this.options.savedMaxCount); } }; -Amplitude.prototype.logEvent = function(eventType, eventProperties, callback) { - if (!this._apiKeySet('logEvent()')) { - if (callback && type(callback) === 'function') { - callback(0, 'No request sent'); +/** + * This is the callback for logEvent and identify calls. It gets called after the event/identify is uploaded, + * and the server response code and response body from the upload request are passed to the callback function. + * @callback Amplitude~eventCallback + * @param {number} responseCode - Server response code for the event / identify upload request. + * @param {string} responseBody - Server response body for the event / identify upload request. + */ + +/** + * Log an event with eventType and eventProperties + * @public + * @param {string} eventType - name of event + * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. + * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. + * Note: the server response code and response body from the event upload are passed to the callback function. + * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); + */ +Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { + if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || + utils.isEmptyString(eventType)) { + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); } return -1; } - return this._logEvent(eventType, eventProperties, null, null, callback); + return this._logEvent(eventType, eventProperties, null, null, opt_callback); }; -// Test that n is a number or a numeric value. -var _isNumber = function(n) { +/** + * Test that n is a number or a numeric value. + * @private + */ +var _isNumber = function _isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; -Amplitude.prototype.logRevenue = function(price, quantity, product) { +/** + * Log revenue event with a price, quantity, and product identifier. + * @public + * @param {number} price - price of revenue event + * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1. + * @param {string} product - (optional) product identifier + * @example amplitude.logRevenue(3.99, 1, 'product_1234'); + */ +Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { // Test that the parameters are of the right type. - if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || quantity !== undefined && !_isNumber(quantity)) { + if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { // utils.log('Price and quantity arguments to logRevenue must be numbers'); return -1; } @@ -680,104 +835,110 @@ Amplitude.prototype.logRevenue = function(price, quantity, product) { }; /** - * Remove events in storage with event ids up to and including maxEventId. Does - * a true filter in case events get out of order or old events are removed. + * Remove events in storage with event ids up to and including maxEventId. + * @private */ -Amplitude.prototype.removeEvents = function (maxEventId, maxIdentifyId) { - if (maxEventId >= 0) { - var filteredEvents = []; - for (var i = 0; i < this._unsentEvents.length; i++) { - if (this._unsentEvents[i].event_id > maxEventId) { - filteredEvents.push(this._unsentEvents[i]); - } - } - this._unsentEvents = filteredEvents; +Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { + _removeEvents(this, '_unsentEvents', maxEventId); + _removeEvents(this, '_unsentIdentifys', maxIdentifyId); +}; + +/** + * Helper function to remove events up to maxId from a single queue. + * Does a true filter in case events get out of order or old events are removed. + * @private + */ +var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { + if (maxId < 0) { + return; } - if (maxIdentifyId >= 0) { - var filteredIdentifys = []; - for (var j = 0; j < this._unsentIdentifys.length; j++) { - if (this._unsentIdentifys[j].event_id > maxIdentifyId) { - filteredIdentifys.push(this._unsentIdentifys[j]); - } + var filteredEvents = []; + for (var i = 0; i < scope[eventQueue].length || 0; i++) { + if (scope[eventQueue][i].event_id > maxId) { + filteredEvents.push(scope[eventQueue][i]); } - this._unsentIdentifys = filteredIdentifys; } + scope[eventQueue] = filteredEvents; }; -Amplitude.prototype.sendEvents = function(callback) { - if (!this._apiKeySet('sendEvents()')) { - if (callback && type(callback) === 'function') { +/** + * Send unsent events. Note: this is called automatically after events are logged if option batchEvents is false. + * If batchEvents is true, then events are only sent when batch criterias are met. + * @private + * @param {Amplitude~eventCallback} callback - (optional) callback to run after events are sent. + * Note the server response code and response body are passed to the callback as input arguments. + */ +Amplitude.prototype.sendEvents = function sendEvents(callback) { + if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) { + if (type(callback) === 'function') { callback(0, 'No request sent'); } return; } - if (!this._sending && !this.options.optOut && this._unsentCount() > 0) { - this._sending = true; - var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + - this.options.apiEndpoint + '/'; - - // fetch events to send - var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); - var mergedEvents = this._mergeEventsAndIdentifys(numEvents); - var maxEventId = mergedEvents.maxEventId; - var maxIdentifyId = mergedEvents.maxIdentifyId; - var events = JSON.stringify(mergedEvents.eventsToSend); - - var uploadTime = new Date().getTime(); - var data = { - client: this.options.apiKey, - e: events, - v: API_VERSION, - upload_time: uploadTime, - checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime) - }; + this._sending = true; + var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + this.options.apiEndpoint + '/'; - var scope = this; - new Request(url, data).send(function(status, response) { - scope._sending = false; - try { - if (status === 200 && response === 'success') { - // utils.log('sucessful upload'); - scope.removeEvents(maxEventId, maxIdentifyId); + // fetch events to send + var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); + var mergedEvents = this._mergeEventsAndIdentifys(numEvents); + var maxEventId = mergedEvents.maxEventId; + var maxIdentifyId = mergedEvents.maxIdentifyId; + var events = JSON.stringify(mergedEvents.eventsToSend); + var uploadTime = new Date().getTime(); + + var data = { + client: this.options.apiKey, + e: events, + v: Constants.API_VERSION, + upload_time: uploadTime, + checksum: md5(Constants.API_VERSION + this.options.apiKey + events + uploadTime) + }; + + var scope = this; + new Request(url, data).send(function(status, response) { + scope._sending = false; + try { + if (status === 200 && response === 'success') { + scope.removeEvents(maxEventId, maxIdentifyId); + + // Update the event cache after the removal of sent events. + if (scope.options.saveEvents) { + scope.saveEvents(); + } - // Update the event cache after the removal of sent events. - if (scope.options.saveEvents) { - scope.saveEvents(); - } - - // Send more events if any queued during previous send. - if (!scope._sendEventsIfReady(callback) && callback) { - callback(status, response); - } - - } else if (status === 413) { - // utils.log('request too large'); - // Can't even get this one massive event through. Drop it. - if (scope.options.uploadBatchSize === 1) { - // if massive event is identify, still need to drop it - scope.removeEvents(maxEventId, maxIdentifyId); - } - - // The server complained about the length of the request. - // Backoff and try again. - scope.options.uploadBatchSize = Math.ceil(numEvents / 2); - scope.sendEvents(callback); - - } else if (callback) { // If server turns something like a 400 + // Send more events if any queued during previous send. + if (!scope._sendEventsIfReady(callback) && type(callback) === 'function') { callback(status, response); } - } catch (e) { - // utils.log('failed upload'); + + // handle payload too large + } else if (status === 413) { + // utils.log('request too large'); + // Can't even get this one massive event through. Drop it, even if it is an identify. + if (scope.options.uploadBatchSize === 1) { + scope.removeEvents(maxEventId, maxIdentifyId); + } + + // The server complained about the length of the request. Backoff and try again. + scope.options.uploadBatchSize = Math.ceil(numEvents / 2); + scope.sendEvents(callback); + + } else if (type(callback) === 'function') { // If server turns something like a 400 + callback(status, response); } - }); - } else if (callback) { - callback(0, 'No request sent'); - } + } catch (e) { + // utils.log('failed upload'); + } + }); }; -Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { +/** + * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. + * @private + */ +Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { // coalesce events from both queues var eventsToSend = []; var eventIndex = 0; @@ -787,14 +948,23 @@ Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { while (eventsToSend.length < numEvents) { var event; + var noIdentifys = identifyIndex >= this._unsentIdentifys.length; + var noEvents = eventIndex >= this._unsentEvents.length; + + // case 0: no events or identifys left + // note this should not happen, this means we have less events and identifys than expected + if (noEvents && noIdentifys) { + utils.log('Merging Events and Identifys, less events and identifys than expected'); + break; + } // case 1: no identifys - grab from events - if (identifyIndex >= this._unsentIdentifys.length) { + else if (noIdentifys) { event = this._unsentEvents[eventIndex++]; maxEventId = event.event_id; // case 2: no events - grab from identifys - } else if (eventIndex >= this._unsentEvents.length) { + } else if (noEvents) { event = this._unsentIdentifys[identifyIndex++]; maxIdentifyId = event.event_id; @@ -823,10 +993,20 @@ Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { }; /** - * @deprecated + * Set global user properties. Note this is deprecated, and we recommend using setUserProperties + * @public + * @deprecated */ -Amplitude.prototype.setGlobalUserProperties = Amplitude.prototype.setUserProperties; +Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { + this.setUserProperties(userProperties); +}; +/** + * Get the current version of Amplitude's Javascript SDK. + * @public + * @returns {number} version number + * @example var amplitudeVersion = amplitude.__VERSION__; + */ Amplitude.prototype.__VERSION__ = version; module.exports = Amplitude; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 00000000..9aebd39e --- /dev/null +++ b/src/constants.js @@ -0,0 +1,19 @@ +module.exports = { + API_VERSION: 2, + MAX_STRING_LENGTH: 4096, + IDENTIFY_EVENT: '$identify', + + // localStorageKeys + LAST_EVENT_ID: 'amplitude_lastEventId', + LAST_EVENT_TIME: 'amplitude_lastEventTime', + LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', + LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', + REFERRER: 'amplitude_referrer', + SESSION_ID: 'amplitude_sessionId', + UTM_PROPERTIES: 'amplitude_utm_properties', + + // Used in cookie as well + DEVICE_ID: 'amplitude_deviceId', + OPT_OUT: 'amplitude_optOut', + USER_ID: 'amplitude_userId' +}; diff --git a/src/cookie.js b/src/cookie.js index 70b51d8c..1ec5d3c8 100644 --- a/src/cookie.js +++ b/src/cookie.js @@ -5,6 +5,7 @@ var Base64 = require('./base64'); var JSON = require('json'); // jshint ignore:line var topDomain = require('top-domain'); +var utils = require('./utils'); var _options = { @@ -30,7 +31,7 @@ var options = function(opts) { _options.expirationDays = opts.expirationDays; - var domain = (opts.domain !== undefined) ? opts.domain : '.' + topDomain(window.location.href); + var domain = (!utils.isEmptyString(opts.domain)) ? opts.domain : '.' + topDomain(window.location.href); var token = Math.random(); _options.domain = domain; set('amplitude_test', token); diff --git a/src/detect.js b/src/detect.js index 2e07d316..708a3a43 100644 --- a/src/detect.js +++ b/src/detect.js @@ -5,7 +5,7 @@ * Detect.js: User-Agent Parser * https://github.com/darcyclarke/Detect.js * Dual licensed under the MIT and GPL licenses. - * + * @private * @version 2.2.1 * @author Darcy Clarke * @url http://darcyclarke.me diff --git a/src/identify.js b/src/identify.js index 83ba8285..7bcc5c1c 100644 --- a/src/identify.js +++ b/src/identify.js @@ -15,11 +15,33 @@ var AMP_OP_SET = '$set'; var AMP_OP_SET_ONCE = '$setOnce'; var AMP_OP_UNSET = '$unset'; +/** + * Identify API - instance constructor. Identify objects are a wrapper for user property operations. + * Each method adds a user property operation to the Identify object, and returns the same Identify object, + * allowing you to chain multiple method calls together. + * Note: if the same user property is used in multiple operations on a single Identify object, + * only the first operation on that property will be saved, and the rest will be ignored. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations} + * for more information on the Identify API and user property operations. + * @constructor Identify + * @public + * @example var identify = new amplitude.Identify(); + */ var Identify = function() { this.userPropertiesOperations = {}; this.properties = []; // keep track of keys that have been added }; +/** + * Increment a user property by a given value (can also be negative to decrement). + * If the user property does not have a value set yet, it will be initialized to 0 before being incremented. + * @public + * @param {string} property - The user property key. + * @param {number|string} value - The amount by which to increment the user property. Allows numbers as strings (ex: '123'). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().add('karma', 1).add('friends', 1); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.add = function(property, value) { if (type(value) === 'number' || type(value) === 'string') { this._addOperation(AMP_OP_ADD, property, value); @@ -29,14 +51,33 @@ Identify.prototype.add = function(property, value) { return this; }; +/** + * Append a value or values to a user property. + * If the user property does not have a value set yet, + * it will be initialized to an empty list before the new values are appended. + * If the user property has an existing value and it is not a list, + * the existing value will be converted into a list with the new values appended. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to append. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().append('ab-tests', 'new-user-tests'); + * identify.append('some_list', [1, 2, 3, 4, 'values']); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.append = function(property, value) { this._addOperation(AMP_OP_APPEND, property, value); return this; }; -// clearAll should be sent on its own Identify object -// If there are already other operations, then don't add clearAll -// If clearAll already in Identify, don't add other operations +/** + * Clear all user properties for the current user. + * SDK user should instead call amplitude.clearUserProperties() instead of using this. + * $clearAll needs to be sent on its own Identify object. If there are already other operations, then don't add $clearAll. + * If $clearAll already in an Identify object, don't allow other operations to be added. + * @private + */ Identify.prototype.clearAll = function() { if (Object.keys(this.userPropertiesOperations).length > 0) { if (!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) { @@ -48,26 +89,78 @@ Identify.prototype.clearAll = function() { return this; }; +/** + * Prepend a value or values to a user property. + * Prepend means inserting the value or values at the front of a list. + * If the user property does not have a value set yet, + * it will be initialized to an empty list before the new values are prepended. + * If the user property has an existing value and it is not a list, + * the existing value will be converted into a list with the new values prepended. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to prepend. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-tests'); + * identify.prepend('some_list', [1, 2, 3, 4, 'values']); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.prepend = function(property, value) { this._addOperation(AMP_OP_PREPEND, property, value); return this; }; +/** + * Sets the value of a given user property. If a value already exists, it will be overwriten with the new value. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to set. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().set('user_type', 'beta'); + * identify.set('name', {'first': 'John', 'last': 'Doe'}); // dict is flattened and becomes name.first: John, name.last: Doe + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.set = function(property, value) { this._addOperation(AMP_OP_SET, property, value); return this; }; +/** + * Sets the value of a given user property only once. Subsequent setOnce operations on that user property will be ignored; + * however, that user property can still be modified through any of the other operations. + * Useful for capturing properties such as 'initial_signup_date', 'initial_referrer', etc. + * @public + * @param {string} property - The user property key. + * @param {number|string|list|object} value - A value or values to set once. + * Values can be numbers, strings, lists, or object (key:value dict will be flattened). + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().setOnce('sign_up_date', '2016-04-01'); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.setOnce = function(property, value) { this._addOperation(AMP_OP_SET_ONCE, property, value); return this; }; +/** + * Unset and remove a user property. This user property will no longer show up in a user's profile. + * @public + * @param {string} property - The user property key. + * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. + * @example var identify = new amplitude.Identify().unset('user_type').unset('age'); + * amplitude.identify(identify); // send the Identify call + */ Identify.prototype.unset = function(property) { this._addOperation(AMP_OP_UNSET, property, '-'); return this; }; +/** + * Helper function that adds operation to the Identify's object + * Handle's filtering of duplicate user property keys, and filtering for clearAll. + * @private + */ Identify.prototype._addOperation = function(operation, property, value) { // check that the identify doesn't already contain a clearAll if (this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) { diff --git a/src/options.js b/src/options.js index 0c2b913d..64b9b619 100644 --- a/src/options.js +++ b/src/options.js @@ -5,7 +5,8 @@ module.exports = { apiEndpoint: 'api.amplitude.com', cookieExpiration: 365 * 10, cookieName: 'amplitude_id', - domain: undefined, + domain: '', + includeReferrer: false, includeUtm: false, language: language.language, optOut: false, diff --git a/src/type.js b/src/type.js index 0c64e12a..c0e41981 100644 --- a/src/type.js +++ b/src/type.js @@ -1,14 +1,13 @@ -/* Taken from: https://github.com/component/type */ - /** * toString ref. + * @private */ var toString = Object.prototype.toString; /** * Return the type of `val`. - * + * @private * @param {Mixed} val * @return {String} * @api public diff --git a/src/utils.js b/src/utils.js index a436e0eb..733c9121 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,7 @@ +var constants = require('./constants'); var type = require('./type'); -var log = function(s) { +var log = function log(s) { try { console.log('[Amplitude] ' + s); } catch (e) { @@ -8,11 +9,54 @@ var log = function(s) { } }; -var isEmptyString = function(str) { +var isEmptyString = function isEmptyString(str) { return (!str || str.length === 0); }; -var validateProperties = function(properties) { +var sessionStorageEnabled = function sessionStorageEnabled() { + try { + if (window.sessionStorage) { + return true; + } + } catch (e) {} // sessionStorage disabled + return false; +}; + +// truncate string values in event and user properties so that request size does not get too large +var truncate = function truncate(value) { + if (type(value) === 'array') { + for (var i = 0; i < value.length; i++) { + value[i] = truncate(value[i]); + } + } else if (type(value) === 'object') { + for (var key in value) { + if (value.hasOwnProperty(key)) { + value[key] = truncate(value[key]); + } + } + } else { + value = _truncateValue(value); + } + + return value; +}; + +var _truncateValue = function _truncateValue(value) { + if (type(value) === 'string') { + return value.length > constants.MAX_STRING_LENGTH ? value.substring(0, constants.MAX_STRING_LENGTH) : value; + } + return value; +}; + +var validateInput = function validateInput(input, name, expectedType) { + if (type(input) !== expectedType) { + log('Invalid ' + name + ' input type. Expected ' + expectedType + ' but received ' + type(input)); + return false; + } + return true; +}; + +var validateProperties = function validateProperties(properties) { var propsType = type(properties); if (propsType !== 'object') { log('Error: invalid event properties format. Expecting Javascript object, received ' + propsType + ', ignoring'); @@ -47,7 +91,7 @@ var invalidValueTypes = [ 'null', 'nan', 'undefined', 'function', 'arguments', 'regexp', 'element' ]; -var validatePropertyValue = function(key, value) { +var validatePropertyValue = function validatePropertyValue(key, value) { var valueType = type(value); if (invalidValueTypes.indexOf(valueType) !== -1) { log('WARNING: Property key "' + key + '" with invalid value type ' + valueType + ', ignoring'); @@ -77,5 +121,8 @@ var validatePropertyValue = function(key, value) { module.exports = { log: log, isEmptyString: isEmptyString, + sessionStorageEnabled: sessionStorageEnabled, + truncate: truncate, + validateInput: validateInput, validateProperties: validateProperties }; diff --git a/src/uuid.js b/src/uuid.js index 947f8ef6..5ad44d08 100644 --- a/src/uuid.js +++ b/src/uuid.js @@ -1,13 +1,13 @@ /* jshint bitwise: false, laxbreak: true */ /** - * Taken straight from jed's gist: https://gist.github.com/982883 - * + * Source: [jed's gist]{@link https://gist.github.com/982883}. * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, * where each x is replaced with a random hexadecimal digit from 0 to f, and * y is replaced with a random hexadecimal digit from 8 to b. + * Used to generate UUIDs for deviceIds. + * @private */ - var uuid = function(a) { return a // if the placeholder was passed, return ? ( // a random number from 0 to 15 diff --git a/test/amplitude.js b/test/amplitude.js index 347d8555..0d8664eb 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -45,17 +45,58 @@ describe('Amplitude', function() { reset(); }); + it('fails on invalid apiKeys', function() { + amplitude.init(null); + assert.equal(amplitude.options.apiKey, undefined); + assert.equal(amplitude.options.deviceId, undefined); + + amplitude.init(''); + assert.equal(amplitude.options.apiKey, undefined); + assert.equal(amplitude.options.deviceId, undefined); + + amplitude.init(apiKey); + assert.equal(amplitude.options.apiKey, apiKey); + assert.lengthOf(amplitude.options.deviceId, 37); + }); + it('should accept userId', function() { amplitude.init(apiKey, userId); assert.equal(amplitude.options.userId, userId); }); + it('should generate a random deviceId', function() { + amplitude.init(apiKey, userId); + assert.lengthOf(amplitude.options.deviceId, 37); // UUID is length 36, but we append 'R' at end + assert.equal(amplitude.options.deviceId[36], 'R'); + }); + + it('should validate config values', function() { + var config = { + apiEndpoint: 100, // invalid type + batchEvents: 'True', // invalid type + cookieExpiration: -1, // negative number + cookieName: '', // empty string + eventUploadPeriodMillis: '30', // 30s + eventUploadThreshold: 0, // zero value + bogusKey: false + }; + + amplitude.init(apiKey, userId, config); + assert.equal(amplitude.options.apiEndpoint, 'api.amplitude.com'); + assert.equal(amplitude.options.batchEvents, false); + assert.equal(amplitude.options.cookieExpiration, 3650); + assert.equal(amplitude.options.cookieName, 'amplitude_id'); + assert.equal(amplitude.options.eventUploadPeriodMillis, 30000); + assert.equal(amplitude.options.eventUploadThreshold, 30); + assert.equal(amplitude.options.bogusKey, undefined); + }); + it('should set cookie', function() { amplitude.init(apiKey, userId); var stored = cookie.get(amplitude.options.cookieName); assert.property(stored, 'deviceId'); assert.propertyVal(stored, 'userId', userId); - assert.lengthOf(stored.deviceId, 36); + assert.lengthOf(stored.deviceId, 37); // increase deviceId length by 1 for 'R' character }); it('should set language', function() { @@ -359,6 +400,34 @@ describe('Amplitude', function() { assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); }); + it('should validate user properties when loading saved identifys from localStorage', function() { + var existingEvents = '[{"device_id":"15a82a' + + 'aa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","timestamp":1455744746295,' + + '"event_id":3,"session_id":1455744733865,"event_type":"$identify","version_name":"Web","platform":"Web",' + + '"os_name":"Chrome","os_version":"48","device_model":"Mac","language":"en-US","api_properties":{},' + + '"user_properties":{"$set":{"10":"false","bool":true,"null":null,"string":"test","array":' + + '[0,1,2,"3"],"nested_array":["a",{"key":"value"},["b"]],"object":{"key":"value"},"nested_object":' + + '{"k":"v","l":[0,1],"o":{"k2":"v2","l2":["e2",{"k3":"v3"}]}}}},"event_properties":{},"uuid":"650407a1-d705-' + + '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]' + localStorage.setItem('amplitude_unsent_identify', existingEvents); + + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey, null, {batchEvents: true}); + + var expected = { + '10': 'false', + 'bool': true, + 'string': 'test', + 'array': [0, 1, 2, '3'], + 'nested_array': ['a'], + 'object': {'key':'value'}, + 'nested_object': {'k':'v', 'l':[0,1], 'o':{'k2':'v2', 'l2': ['e2']}} + } + + // check that event loaded into memory + assert.deepEqual(amplitude2._unsentIdentifys[0].user_properties, {'$set': expected}); + }); + it ('should load saved events from localStorage new keys and send events', function() { var existingEvent = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769146589,' + '"event_id":49,"session_id":1453763315544,"event_type":"clicked","version_name":"Web","platform":"Web"' + @@ -534,6 +603,28 @@ describe('Amplitude', function() { }); }); + describe('setVersionName', function() { + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should set version name', function() { + amplitude.init(apiKey, null, {batchEvents: true}); + amplitude.setVersionName('testVersionName1'); + amplitude.logEvent('testEvent1'); + assert.equal(amplitude._unsentEvents[0].version_name, 'testVersionName1'); + + // should ignore non-string values + amplitude.setVersionName(15000); + amplitude.logEvent('testEvent2'); + assert.equal(amplitude._unsentEvents[1].version_name, 'testVersionName1'); + }); + }); + describe('setDeviceId', function() { beforeEach(function() { @@ -1528,21 +1619,21 @@ describe('Amplitude', function() { }); it('should truncate long event property strings', function() { - var longString = new Array(2000).join('a'); + var longString = new Array(5000).join('a'); amplitude.logEvent('test', {'key': longString}); var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; assert.isTrue('key' in event.event_properties); - assert.lengthOf(event.event_properties['key'], 1024); + assert.lengthOf(event.event_properties['key'], 4096); }); it('should truncate long user property strings', function() { - var longString = new Array(2000).join('a'); + var longString = new Array(5000).join('a'); amplitude.identify(new Identify().set('key', longString)); var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; assert.isTrue('$set' in event.user_properties); - assert.lengthOf(event.user_properties['$set']['key'], 1024); + assert.lengthOf(event.user_properties['$set']['key'], 4096); }); it('should increment the counters in local storage if cookies disabled', function() { @@ -1622,6 +1713,14 @@ describe('Amplitude', function() { }); }); + it('should validate user propeorties', function() { + var identify = new Identify().set(10, 10); + amplitude.init(apiKey, null, {batchEvents: true}); + amplitude.identify(identify); + + assert.deepEqual(amplitude._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); + }); + it('should synchronize event data across multiple amplitude instances that share the same cookie', function() { // this test fails if logEvent does not reload cookie data every time var amplitude1 = new Amplitude(); @@ -1740,35 +1839,57 @@ describe('Amplitude', function() { assert.equal(events[0].user_properties.utm_term, undefined); }); - it('should send utm data when the includeUtm flag is true', function() { + it('should send utm data via identify when the includeUtm flag is true', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); reset(); - amplitude.init(apiKey, undefined, {includeUtm: true}); + amplitude.init(apiKey, undefined, {includeUtm: true, batchEvents: true, eventUploadThreshold: 2}); amplitude.logEvent('UTM Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { - utm_campaign: 'new', - utm_content: 'top' + '$setOnce': { + initial_utm_campaign: 'new', + initial_utm_content: 'top' + }, + '$set': { + utm_campaign: 'new', + utm_content: 'top' + } }); + + assert.equal(events[1].event_type, 'UTM Test Event'); + assert.deepEqual(events[1].user_properties, {}); }); - it('should add utm params to the user properties', function() { + it('should parse utm params', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; amplitude._initUtmData(utmParams); - amplitude.setUserProperties({user_prop: true}); + var expectedProperties = { + utm_campaign: 'new', + utm_content: 'top', + utm_medium: 'email', + utm_source: 'amplitude', + utm_term: 'terms' + } + assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); - // identify event should not have utm properties + assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { - '$set': { - 'user_prop': true - } + '$setOnce': { + initial_utm_campaign: 'new', + initial_utm_content: 'top', + initial_utm_medium: 'email', + initial_utm_source: 'amplitude', + initial_utm_term: 'terms' + }, + '$set': expectedProperties }); server.respondWith('success'); server.respond(); @@ -1776,13 +1897,45 @@ describe('Amplitude', function() { amplitude.logEvent('UTM Test Event', {}); assert.lengthOf(server.requests, 2); var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.deepEqual(events[0].user_properties, {}); + + // verify session storage set + assert.deepEqual(JSON.parse(sessionStorage.getItem('amplitude_utm_properties')), expectedProperties); + }); + + it('should not set utmProperties if utmProperties data already in session storage', function() { + reset(); + var existingProperties = { + utm_campaign: 'old', + utm_content: 'bottom', + utm_medium: 'texts', + utm_source: 'datamonster', + utm_term: 'conditions' + }; + sessionStorage.setItem('amplitude_utm_properties', JSON.stringify(existingProperties)); + + cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); + var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; + amplitude._initUtmData(utmParams); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + + // first event should be identify with initial_utm properties and NO existing utm properties + assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { - utm_campaign: 'new', - utm_content: 'top', - utm_medium: 'email', - utm_source: 'amplitude', - utm_term: 'terms' + '$setOnce': { + initial_utm_campaign: 'new', + initial_utm_content: 'top', + initial_utm_medium: 'email', + initial_utm_source: 'amplitude', + initial_utm_term: 'terms' + } }); + + // should not override any existing utm properties values in session storage + assert.equal(sessionStorage.getItem('amplitude_utm_properties'), JSON.stringify(existingProperties)); }); }); @@ -1815,13 +1968,15 @@ describe('Amplitude', function() { var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 2); + var expected = { + 'referrer': 'https://amplitude.com/contact', + 'referring_domain': 'amplitude.com' + }; + // first event should be identify with initial_referrer and referrer assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { - '$set': { - 'referrer': 'https://amplitude.com/contact', - 'referring_domain': 'amplitude.com' - }, + '$set': expected, '$setOnce': { 'initial_referrer': 'https://amplitude.com/contact', 'initial_referring_domain': 'amplitude.com' @@ -1833,7 +1988,7 @@ describe('Amplitude', function() { assert.deepEqual(events[1].user_properties, {}); // referrer should be propagated to session storage - assert.equal(sessionStorage.getItem('amplitude_referrer'), 'https://amplitude.com/contact'); + assert.equal(sessionStorage.getItem('amplitude_referrer'), JSON.stringify(expected)); }); it('should not set referrer if referrer data already in session storage', function() {