From 178a3daf218ff437569061f9d6188b444fb8219c Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Fri, 20 May 2016 18:39:31 -0700 Subject: [PATCH 01/13] update changelog and snippet --- CHANGELOG.md | 3 +++ README.md | 14 ++++++++------ amplitude-segment-snippet.min.js | 16 +++++++++------- amplitude-snippet.min.js | 14 ++++++++------ src/amplitude-snippet.js | 9 ++++++++- 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a801d57..b54f8eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +* Add support for logging events to multiple Amplitude apps. **Note this is a major update, and may break backwards compatability.** See [Readme](https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps) for details. +* Init callback now passes the Amplitude instance as an argument to the callback function. + ### 2.12.1 (April 21, 2016) * Silence console warnings for various UTM property keys with undefined values. diff --git a/README.md b/README.md index 191c8752..0db18e0b 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,18 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use ```html diff --git a/amplitude-segment-snippet.min.js b/amplitude-segment-snippet.min.js index 806fcd4b..4e63c623 100644 --- a/amplitude-segment-snippet.min.js +++ b/amplitude-segment-snippet.min.js @@ -1,8 +1,10 @@ -(function(e,t){var r=e.amplitude||{_q:[]};function n(e,t){e.prototype[t]=function(){ -this._q.push([t].concat(Array.prototype.slice.call(arguments,0)));return this}}var s=function(){ -this._q=[];return this};var i=["add","append","clearAll","prepend","set","setOnce","unset"]; -for(var o=0;o Date: Fri, 20 May 2016 18:50:50 -0700 Subject: [PATCH 02/13] updated index to copy all instances, and added snippet tests --- amplitude.js | 11 ++++++++--- amplitude.min.js | 4 ++-- src/index.js | 11 ++++++++--- test/snippet-tests.js | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/amplitude.js b/amplitude.js index 70f3d2ce..84e9e4ab 100644 --- a/amplitude.js +++ b/amplitude.js @@ -96,11 +96,16 @@ var Amplitude = require('./amplitude'); var old = window.amplitude || {}; -var instance = new Amplitude(); -instance._q = old._q || []; +var newInstance = new Amplitude(); +newInstance._q = old._q || []; +for (var instance in old._iq) { // migrate eat instance's queue + if (old._iq.hasOwnProperty(instance)) { + newInstance.getInstance(instance)._q = old._iq[instance]._q || []; + } +} // export the instance -module.exports = instance; +module.exports = newInstance; }, {"./amplitude":2}], 2: [function(require, module, exports) { diff --git a/amplitude.min.js b/amplitude.min.js index 74fc4341..e856e5e5 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(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 Revenue=require("./revenue");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.Revenue=Revenue;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.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};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.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};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)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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,null,opt_callback)};Amplitude.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};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":22}],22:[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":23}],23:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":13,"./utils":15}],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:25}],25:[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;jthis.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.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};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.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};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)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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,null,opt_callback)};Amplitude.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};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":22}],22:[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":23}],23:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":13,"./utils":15}],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:25}],25:[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},{}],17:[function(require,module,exports){module.exports="2.12.1"},{}],18:[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":28}],28:[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/src/index.js b/src/index.js index c5cab082..8f1d5388 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,13 @@ var Amplitude = require('./amplitude'); var old = window.amplitude || {}; -var instance = new Amplitude(); -instance._q = old._q || []; +var newInstance = new Amplitude(); +newInstance._q = old._q || []; +for (var instance in old._iq) { // migrate eat instance's queue + if (old._iq.hasOwnProperty(instance)) { + newInstance.getInstance(instance)._q = old._iq[instance]._q || []; + } +} // export the instance -module.exports = instance; +module.exports = newInstance; diff --git a/test/snippet-tests.js b/test/snippet-tests.js index 03674387..36e495e6 100644 --- a/test/snippet-tests.js +++ b/test/snippet-tests.js @@ -32,4 +32,24 @@ describe('Snippet', function() { assert.deepEqual(revenue._q[1], ['setQuantity', 5]); assert.deepEqual(revenue._q[2], ['setPrice', 10.99]); }); + + it('amplitude object should proxy instance functions', function() { + amplitude.getInstance(null).init('API_KEY'); + amplitude.getInstance('$DEFAULT_instance').logEvent('Click'); + amplitude.getInstance('').clearUserProperties(); + amplitude.getInstance('INSTANCE1').init('API_KEY1'); + amplitude.getInstance('instanCE2').init('API_KEY2'); + amplitude.getInstance('instaNce2').logEvent('Event'); + + assert.deepEqual(Object.keys(amplitude._iq), ['$default_instance', 'instance1', 'instance2']); + assert.lengthOf(amplitude._iq['$default_instance']._q, 3); + assert.deepEqual(amplitude._iq['$default_instance']._q[0], ['init', 'API_KEY']); + assert.deepEqual(amplitude._iq['$default_instance']._q[1], ['logEvent', 'Click']); + assert.deepEqual(amplitude._iq['$default_instance']._q[2], ['clearUserProperties']); + assert.lengthOf(amplitude._iq['instance1']._q, 1); + assert.deepEqual(amplitude._iq['instance1']._q[0], ['init', 'API_KEY1']); + assert.lengthOf(amplitude._iq['instance2']._q, 2); + assert.deepEqual(amplitude._iq['instance2']._q[0], ['init', 'API_KEY2']); + assert.deepEqual(amplitude._iq['instance2']._q[1], ['logEvent', 'Event']); + }); }); From f2038108025a1010d3d0a1a7aa8da473acb6d5b1 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Fri, 20 May 2016 19:17:19 -0700 Subject: [PATCH 03/13] migrate code out of amplitude into amplitude client --- amplitude.js | 446 ++++++++++++++-- amplitude.min.js | 6 +- src/amplitude-client.js | 1123 +++++++++++++++++++++++++++++++++++++++ src/amplitude.js | 889 +++---------------------------- src/constants.js | 1 + 5 files changed, 1599 insertions(+), 866 deletions(-) create mode 100644 src/amplitude-client.js diff --git a/amplitude.js b/amplitude.js index 84e9e4ab..01455225 100644 --- a/amplitude.js +++ b/amplitude.js @@ -109,6 +109,373 @@ module.exports = newInstance; }, {"./amplitude":2}], 2: [function(require, module, exports) { +var AmplitudeClient = require('./amplitude-client'); +var constants = require('./constants'); +var Identify = require('./identify'); +var object = require('object'); +var Revenue = require('./revenue'); +var type = require('./type'); +var utils = require('./utils'); +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.options = object.merge({}, DEFAULT_OPTIONS); + this._instances = {}; // mapping of instance names to instances +}; + +Amplitude.prototype.Identify = Identify; +Amplitude.prototype.Revenue = Revenue; + +Amplitude.prototype.getInstance = function getInstance(instance) { + instance = (utils.isEmptyString(instance) ? constants.DEFAULT_INSTANCE : instance).toLowerCase(); + var client = this._instances[instance]; + if (client === undefined) { + client = new AmplitudeClient(instance); + this._instances[instance] = client; + } + return client; +}; + +/** + * 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. + * @deprecated Please use amplitude.getInstance().init(apiKey, opt_userId, opt_config, opt_callback); + * @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) { + this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { + // make options such as deviceId available for callback functions + this.options = instance.options; + if (opt_callback && type(opt_callback) === 'function') { + opt_callback(instance); + } + }.bind(this)); +}; + +/** + * Run functions queued up by proxy loading snippet + * @private + */ +Amplitude.prototype.runQueuedFunctions = function () { + // run queued up old versions of functions + 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 + + // run queued up functions on instances + for (var instance in this._instances) { + if (this._instances.hasOwnProperty(instance)) { + this._instances[instance].runQueuedFunctions(); + } + } +}; + +/** + * Returns true if a new session was created during initialization, otherwise false. + * @public + * @return {boolean} Whether a new session was created during initialization. + * @deprecated Please use amplitude.getInstance().isNewSession(); + */ +Amplitude.prototype.isNewSession = function isNewSession() { + return this.getInstance().isNewSession(); +}; + +/** + * Returns the id of the current session. + * @public + * @return {number} Id of the current session. + * @deprecated Please use amplitude.getInstance().getSessionId(); + */ +Amplitude.prototype.getSessionId = function getSessionId() { + return this.getInstance().getSessionId(); +}; + +/** + * Increments the eventId and returns it. + * @private + */ +Amplitude.prototype.nextEventId = function nextEventId() { + return this.getInstance().nextEventId(); +}; + +/** + * Increments the identifyId and returns it. + * @private + */ +Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { + return this.getInstance().nextIdentifyId(); +}; + +/** + * Increments the sequenceNumber and returns it. + * @private + */ +Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { + return this.getInstance().nextSequenceNumber(); +}; + +/** + * 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() { + this.getInstance().saveEvents(); +}; + +/** + * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. + * @public + * @param {string} domain to set. + * @deprecated Please use amplitude.getInstance().setDomain(domain); + * @example amplitude.setDomain('.amplitude.com'); + */ +Amplitude.prototype.setDomain = function setDomain(domain) { + this.getInstance().setDomain(domain); +}; + +/** + * Sets an identifier for the current user. + * @public + * @param {string} userId - identifier to set. Can be null. + * @deprecatedPlease use amplitude.getInstance().setUserId(userId); + * @example amplitude.setUserId('joe@gmail.com'); + */ +Amplitude.prototype.setUserId = function setUserId(userId) { + this.getInstance().setUserId(userId); +}; + +/** + * Add user to a group or groups. You need to specify a groupType and groupName(s). + * For example you can group people by their organization. + * In that case groupType is "orgId" and groupName would be the actual ID(s). + * groupName can be a string or an array of strings to indicate a user in multiple gruups. + * You can also call setGroup multiple times with different groupTypes to track multiple types of groups (up to 5 per app). + * Note: this will also set groupType: groupName as a user property. + * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information. + * @public + * @param {string} groupType - the group type (ex: orgId) + * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @deprecated Please use amplitude.getInstance().setGroup(groupType, groupName); + * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. + */ +Amplitude.prototype.setGroup = function(groupType, groupName) { + this.getInstance().setGroup(groupType, groupName); +}; + +/** + * Sets whether to opt current user out of tracking. + * @public + * @param {boolean} enable - if true then no events will be logged or sent. + * @deprecated Please use amplitude.getInstance().setOptOut(enable); + * @example: amplitude.setOptOut(true); + */ +Amplitude.prototype.setOptOut = function setOptOut(enable) { + this.getInstance().setOptOut(enable); +}; + +/** + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. + * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. + * This uses src/uuid.js to regenerate the deviceId. + * @public + * deprecated Please use amplitude.getInstance().regenerateDeviceId(); + */ +Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { + this.getInstance().regenerateDeviceId(); +}; + +/** + * 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. + * @deprecated Please use amplitude.getInstance().setDeviceId(deviceId); + * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); + */ +Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { + this.getInstance().setDeviceId(deviceId); +}; + +/** + * 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. + * @deprecated Please use amplitude.getInstance.setUserProperties(userProperties); + * @example amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true}) + */ +Amplitude.prototype.setUserProperties = function setUserProperties(userProperties) { + this.getInstance().setUserProperties(userProperties); +}; + +/** + * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! + * @public + * @deprecated Please use amplitude.getInstance().clearUserProperties(); + * @example amplitude.clearUserProperties(); + */ +Amplitude.prototype.clearUserProperties = function clearUserProperties(){ + this.getInstance().clearUserProperties(); +}; + +/** + * 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. + * @deprecated Please use amplitude.getInstance().identify(identify); + * @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) { + this.getInstance().identify(identify_obj, opt_callback); +}; + +/** + * Set a versionName for your application. + * @public + * @param {string} versionName - The version to set for your application. + * @deprecated Please use amplitude.getInstance().setVersionName(versionName); + * @example amplitude.setVersionName('1.12.3'); + */ +Amplitude.prototype.setVersionName = function setVersionName(versionName) { + this.getInstance().setVersionName(versionName); +}; + +/** + * 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. + * @deprecated Please use amplitude.getInstance().logEvent(eventType, eventProperties, opt_callback); + * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); + */ +Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { + return this.getInstance().logEvent(eventType, eventProperties, opt_callback); +}; + +/** + * Log an event with eventType, eventProperties, and groups. Use this to set event-level groups. + * Note: the group(s) set only apply for the specific event type being logged and does not persist on the user + * (unless you explicitly set it with setGroup). + * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information + * about groups and Count by Distinct on the Amplitude platform. + * @public + * @param {string} eventType - name of event + * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. + * @param {object} groups - (optional) an object with string groupType: groupName values for the event being logged. + * groupName can be a string or an array of strings. + * @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. + * Deprecated Please use amplitude.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); + * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); + */ +Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { + return this.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); +}; + +/** + * Log revenue with Revenue interface. The new revenue interface allows for more revenue fields like + * revenueType and event properties. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#tracking-revenue} + * for more information on the Revenue interface and logging revenue. + * @public + * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); + * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); + * amplitude.logRevenueV2(revenue); + */ +Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { + return this.getInstance().logRevenueV2(revenue_obj); +}; + +/** + * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 + * @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 + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); + * @example amplitude.logRevenue(3.99, 1, 'product_1234'); + */ +Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { + return this.getInstance().logRevenue(price, quantity, product); +}; + +/** + * Remove events in storage with event ids up to and including maxEventId. + * @private + */ +Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { + this.getInstance().removeEvents(maxEventId, maxIdentifyId); +}; + +/** + * 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) { + this.getInstance().sendEvents(callback); +}; + +/** + * Set global user properties. Note this is deprecated, and we recommend using setUserProperties + * @public + * @deprecated + */ +Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { + this.getInstance().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; + +}, {"./amplitude-client":3,"./constants":4,"./identify":5,"object":6,"./revenue":7,"./type":8,"./utils":9,"./version":10,"./options":11}], +3: [function(require, module, exports) { var Constants = require('./constants'); var cookieStorage = require('./cookiestorage'); var getUtmData = require('./utm'); @@ -1233,9 +1600,10 @@ Amplitude.prototype.__VERSION__ = version; module.exports = Amplitude; -}, {"./constants":3,"./cookiestorage":4,"./utm":5,"./identify":6,"json":7,"./localstorage":8,"JavaScript-MD5":9,"object":10,"./xhr":11,"./revenue":12,"./type":13,"ua-parser-js":14,"./utils":15,"./uuid":16,"./version":17,"./options":18}], -3: [function(require, module, exports) { +}, {"./constants":4,"./cookiestorage":12,"./utm":13,"./identify":5,"json":14,"./localstorage":15,"JavaScript-MD5":16,"object":6,"./xhr":17,"./revenue":7,"./type":8,"ua-parser-js":18,"./utils":9,"./uuid":19,"./version":10,"./options":11}], +4: [function(require, module, exports) { module.exports = { + DEFAULT_INSTANCE: '$default_instance', API_VERSION: 2, MAX_STRING_LENGTH: 4096, IDENTIFY_EVENT: '$identify', @@ -1265,7 +1633,7 @@ module.exports = { }; }, {}], -4: [function(require, module, exports) { +12: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -1359,8 +1727,8 @@ cookieStorage.prototype.getStorage = function() { module.exports = cookieStorage; -}, {"./constants":3,"./cookie":19,"json":7,"./localstorage":8}], -19: [function(require, module, exports) { +}, {"./constants":4,"./cookie":20,"json":14,"./localstorage":15}], +20: [function(require, module, exports) { /* * Cookie data */ @@ -1490,8 +1858,8 @@ module.exports = { }; -}, {"./base64":20,"json":7,"top-domain":21,"./utils":15}], -20: [function(require, module, exports) { +}, {"./base64":21,"json":14,"top-domain":22,"./utils":9}], +21: [function(require, module, exports) { /* jshint bitwise: false */ /* global escape, unescape */ @@ -1590,8 +1958,8 @@ var Base64 = { module.exports = Base64; -}, {"./utf8":22}], -22: [function(require, module, exports) { +}, {"./utf8":23}], +23: [function(require, module, exports) { /* jshint bitwise: false */ /* @@ -1651,7 +2019,7 @@ var UTF8 = { module.exports = UTF8; }, {}], -7: [function(require, module, exports) { +14: [function(require, module, exports) { var json = window.JSON || {}; var stringify = json.stringify; @@ -1661,8 +2029,8 @@ module.exports = parse && stringify ? JSON : require('json-fallback'); -}, {"json-fallback":23}], -23: [function(require, module, exports) { +}, {"json-fallback":24}], +24: [function(require, module, exports) { /* json2.js 2014-02-04 @@ -2152,7 +2520,7 @@ module.exports = parse && stringify }()); }, {}], -21: [function(require, module, exports) { +22: [function(require, module, exports) { /** * Module dependencies. @@ -2200,8 +2568,8 @@ function domain(url){ return match ? match[0] : ''; }; -}, {"url":24}], -24: [function(require, module, exports) { +}, {"url":25}], +25: [function(require, module, exports) { /** * Parse the given `url`. @@ -2286,7 +2654,7 @@ function port (protocol){ } }, {}], -15: [function(require, module, exports) { +9: [function(require, module, exports) { var constants = require('./constants'); var type = require('./type'); @@ -2481,8 +2849,8 @@ module.exports = { validateProperties: validateProperties }; -}, {"./constants":3,"./type":13}], -13: [function(require, module, exports) { +}, {"./constants":4,"./type":8}], +8: [function(require, module, exports) { /** * toString ref. * @private @@ -2529,7 +2897,7 @@ module.exports = function(val){ }; }, {}], -8: [function(require, module, exports) { +15: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -2633,7 +3001,7 @@ if (!localStorage) { module.exports = localStorage; }, {}], -5: [function(require, module, exports) { +13: [function(require, module, exports) { var utils = require('./utils'); var getUtmParam = function getUtmParam(name, query) { @@ -2676,8 +3044,8 @@ var getUtmData = function getUtmData(rawCookie, query) { module.exports = getUtmData; -}, {"./utils":15}], -6: [function(require, module, exports) { +}, {"./utils":9}], +5: [function(require, module, exports) { var type = require('./type'); var utils = require('./utils'); @@ -2863,8 +3231,8 @@ Identify.prototype._addOperation = function(operation, property, value) { module.exports = Identify; -}, {"./type":13,"./utils":15}], -9: [function(require, module, exports) { +}, {"./type":8,"./utils":9}], +16: [function(require, module, exports) { /* * JavaScript MD5 1.0.1 * https://github.com/blueimp/JavaScript-MD5 @@ -3152,7 +3520,7 @@ module.exports = Identify; }(this)); }, {}], -10: [function(require, module, exports) { +6: [function(require, module, exports) { /** * HOP ref. @@ -3238,7 +3606,7 @@ exports.isEmpty = function(obj){ return 0 == exports.length(obj); }; }, {}], -11: [function(require, module, exports) { +17: [function(require, module, exports) { var querystring = require('querystring'); /* @@ -3284,8 +3652,8 @@ Request.prototype.send = function(callback) { module.exports = Request; -}, {"querystring":25}], -25: [function(require, module, exports) { +}, {"querystring":26}], +26: [function(require, module, exports) { /** * Module dependencies. @@ -3360,8 +3728,8 @@ exports.stringify = function(obj){ return pairs.join('&'); }; -}, {"trim":26,"type":27}], -26: [function(require, module, exports) { +}, {"trim":27,"type":28}], +27: [function(require, module, exports) { exports = module.exports = trim; @@ -3381,7 +3749,7 @@ exports.right = function(str){ }; }, {}], -27: [function(require, module, exports) { +28: [function(require, module, exports) { /** * toString ref. */ @@ -3430,7 +3798,7 @@ function isBuffer(obj) { } }, {}], -12: [function(require, module, exports) { +7: [function(require, module, exports) { var constants = require('./constants'); var type = require('./type'); var utils = require('./utils'); @@ -3590,8 +3958,8 @@ Revenue.prototype._toJSONObject = function _toJSONObject() { module.exports = Revenue; -}, {"./constants":3,"./type":13,"./utils":15}], -14: [function(require, module, exports) { +}, {"./constants":4,"./type":8,"./utils":9}], +18: [function(require, module, exports) { /* jshint eqeqeq: false, forin: false */ /* global define */ @@ -4474,7 +4842,7 @@ module.exports = Revenue; })(this); }, {}], -16: [function(require, module, exports) { +19: [function(require, module, exports) { /* jshint bitwise: false, laxbreak: true */ /** @@ -4508,11 +4876,11 @@ var uuid = function(a) { module.exports = uuid; }, {}], -17: [function(require, module, exports) { +10: [function(require, module, exports) { module.exports = '2.12.1'; }, {}], -18: [function(require, module, exports) { +11: [function(require, module, exports) { var language = require('./language'); // default options @@ -4537,8 +4905,8 @@ module.exports = { eventUploadPeriodMillis: 30 * 1000, // 30s }; -}, {"./language":28}], -28: [function(require, module, exports) { +}, {"./language":29}], +29: [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 e856e5e5..c7dda26c 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./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 Revenue=require("./revenue");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.Revenue=Revenue;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.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};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.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};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)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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,null,opt_callback)};Amplitude.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};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":22}],22:[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":23}],23:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":13,"./utils":15}],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:25}],25:[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},{}],17:[function(require,module,exports){module.exports="2.12.1"},{}],18:[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":28}],28:[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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};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.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};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)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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,null,opt_callback)};Amplitude.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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},{}],10:[function(require,module,exports){module.exports="2.12.1"},{}],11:[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":29}],29:[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/src/amplitude-client.js b/src/amplitude-client.js new file mode 100644 index 00000000..998ae69f --- /dev/null +++ b/src/amplitude-client.js @@ -0,0 +1,1123 @@ +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 Revenue = require('./revenue'); +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; +Amplitude.prototype.Revenue = Revenue; + +/** + * 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; + var groups = this._unsentEvents[i].groups; + this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); + this._unsentEvents[i].groups = utils.validateGroups(groups); + } + + // validate user properties for unsent identifys + for (var j = 0; j < this._unsentIdentifys.length; j++) { + var userProperties = this._unsentIdentifys[j].user_properties; + var identifyGroups = this._unsentIdentifys[j].groups; + this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); + this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups); + } + + 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); + } +}; + +/** + * Add user to a group or groups. You need to specify a groupType and groupName(s). + * For example you can group people by their organization. + * In that case groupType is "orgId" and groupName would be the actual ID(s). + * groupName can be a string or an array of strings to indicate a user in multiple gruups. + * You can also call setGroup multiple times with different groupTypes to track multiple types of groups (up to 5 per app). + * Note: this will also set groupType: groupName as a user property. + * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information. + * @public + * @param {string} groupType - the group type (ex: orgId) + * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. + */ +Amplitude.prototype.setGroup = function(groupType, groupName) { + if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || + utils.isEmptyString(groupType)) { + return; + } + + var groups = {}; + groups[groupType] = groupName; + var identify = new Identify().set(groupType, groupName); + this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null); +}; + +/** + * 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); + } +}; + +/** + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. + * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. + * This uses src/uuid.js to regenerate the deviceId. + * @public + */ +Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { + this.setDeviceId(UUID() + 'R'); +}; + +/** + * 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); +}; + +/** + * Applies the proxied functions on the proxied object to an instance of the real object. + * Used to convert proxied Identify and Revenue objects. + * @private + */ +var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) { + for (var i = 0; i < proxy._q.length; i++) { + var fn = instance[proxy._q[i][0]]; + if (type(fn) === 'function') { + fn.apply(instance, proxy._q[i].slice(1)); + } + } + return instance; +}; + +/** + * 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')) { + identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj); + } + + 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, null, 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 - The version to set for your application. + * @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, groups, 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 || {}; + groups = groups || {}; + 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 + groups: utils.truncate(utils.validateGroups(groups)) + // 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, null, opt_callback); +}; + +/** + * Log an event with eventType, eventProperties, and groups. Use this to set event-level groups. + * Note: the group(s) set only apply for the specific event type being logged and does not persist on the user + * (unless you explicitly set it with setGroup). + * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information + * about groups and Count by Distinct on the Amplitude platform. + * @public + * @param {string} eventType - name of event + * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. + * @param {object} groups - (optional) an object with string groupType: groupName values for the event being logged. + * groupName can be a string or an array of strings. + * @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.logEventWithGroups('Clicked Button', null, {'orgId': 24}); + */ +Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { + if (!this._apiKeySet('logEventWithGroup()') || + !utils.validateInput(eventType, 'eventType', 'string')) { + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } + return -1; + } + return this._logEvent(eventType, eventProperties, null, null, groups, 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 with Revenue interface. The new revenue interface allows for more revenue fields like + * revenueType and event properties. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#tracking-revenue} + * for more information on the Revenue interface and logging revenue. + * @public + * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); + * amplitude.logRevenueV2(revenue); + */ +Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { + if (!this._apiKeySet('logRevenueV2()')) { + return; + } + + // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object + if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) { + revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj); + } + + if (revenue_obj instanceof Revenue) { + // only send if revenue is valid + if (revenue_obj && revenue_obj._isValidRevenue()) { + return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject()); + } + } else { + utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj)); + } +}; + +/** + * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 + * @public + * @deprecated + * @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(Constants.REVENUE_EVENT, {}, { + productId: product, + special: 'revenue_amount', + quantity: quantity || 1, + price: price + }, null, null, null); +}; + +/** + * 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; diff --git a/src/amplitude.js b/src/amplitude.js index 998ae69f..9753cb6c 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -1,17 +1,10 @@ -var Constants = require('./constants'); -var cookieStorage = require('./cookiestorage'); -var getUtmData = require('./utm'); +var AmplitudeClient = require('./amplitude-client'); +var constants = require('./constants'); 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 Revenue = require('./revenue'); 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'); @@ -22,27 +15,23 @@ var DEFAULT_OPTIONS = require('./options'); * @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; + this._instances = {}; // mapping of instance names to instances }; Amplitude.prototype.Identify = Identify; Amplitude.prototype.Revenue = Revenue; +Amplitude.prototype.getInstance = function getInstance(instance) { + instance = (utils.isEmptyString(instance) ? constants.DEFAULT_INSTANCE : instance).toLowerCase(); + var client = this._instances[instance]; + if (client === undefined) { + client = new AmplitudeClient(instance); + this._instances[instance] = client; + } + return client; +}; + /** * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. * This is required before any other methods can be called. @@ -52,113 +41,17 @@ Amplitude.prototype.Revenue = Revenue; * @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. + * @deprecated Please use amplitude.getInstance().init(apiKey, opt_userId, opt_config, opt_callback); * @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; - var groups = this._unsentEvents[i].groups; - this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); - this._unsentEvents[i].groups = utils.validateGroups(groups); - } - - // validate user properties for unsent identifys - for (var j = 0; j < this._unsentIdentifys.length; j++) { - var userProperties = this._unsentIdentifys[j].user_properties; - var identifyGroups = this._unsentIdentifys[j].groups; - this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); - this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups); - } - - 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); + this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { + // make options such as deviceId available for callback functions + this.options = instance.options; + if (opt_callback && type(opt_callback) === 'function') { + opt_callback(instance); } - } + }.bind(this)); }; /** @@ -166,6 +59,7 @@ var _parseConfig = function _parseConfig(options, config) { * @private */ Amplitude.prototype.runQueuedFunctions = function () { + // run queued up old versions of functions for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; if (type(fn) === 'function') { @@ -173,58 +67,33 @@ Amplitude.prototype.runQueuedFunctions = function () { } } 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) {} + // run queued up functions on instances + for (var instance in this._instances) { + if (this._instances.hasOwnProperty(instance)) { + this._instances[instance].runQueuedFunctions(); + } } - 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. + * @deprecated Please use amplitude.getInstance().isNewSession(); */ Amplitude.prototype.isNewSession = function isNewSession() { - return this._newSession; + return this.getInstance().isNewSession(); }; /** * Returns the id of the current session. * @public * @return {number} Id of the current session. + * @deprecated Please use amplitude.getInstance().getSessionId(); */ Amplitude.prototype.getSessionId = function getSessionId() { - return this._sessionId; + return this.getInstance().getSessionId(); }; /** @@ -232,8 +101,7 @@ Amplitude.prototype.getSessionId = function getSessionId() { * @private */ Amplitude.prototype.nextEventId = function nextEventId() { - this._eventId++; - return this._eventId; + return this.getInstance().nextEventId(); }; /** @@ -241,8 +109,7 @@ Amplitude.prototype.nextEventId = function nextEventId() { * @private */ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { - this._identifyId++; - return this._identifyId; + return this.getInstance().nextIdentifyId(); }; /** @@ -250,257 +117,7 @@ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { * @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); + return this.getInstance().nextSequenceNumber(); }; /** @@ -509,51 +126,29 @@ Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { * @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) {} + this.getInstance().saveEvents(); }; /** * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. * @public * @param {string} domain to set. + * @deprecated Please use amplitude.getInstance().setDomain(domain); * @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); - } + this.getInstance().setDomain(domain); }; /** * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. + * @deprecatedPlease use amplitude.getInstance().setUserId(userId); * @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); - } + this.getInstance().setUserId(userId); }; /** @@ -567,37 +162,22 @@ Amplitude.prototype.setUserId = function setUserId(userId) { * @public * @param {string} groupType - the group type (ex: orgId) * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @deprecated Please use amplitude.getInstance().setGroup(groupType, groupName); * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. */ Amplitude.prototype.setGroup = function(groupType, groupName) { - if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || - utils.isEmptyString(groupType)) { - return; - } - - var groups = {}; - groups[groupType] = groupName; - var identify = new Identify().set(groupType, groupName); - this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null); + this.getInstance().setGroup(groupType, groupName); }; /** * Sets whether to opt current user out of tracking. * @public * @param {boolean} enable - if true then no events will be logged or sent. + * @deprecated Please use amplitude.getInstance().setOptOut(enable); * @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); - } + this.getInstance().setOptOut(enable); }; /** @@ -606,9 +186,10 @@ Amplitude.prototype.setOptOut = function setOptOut(enable) { * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. * @public + * deprecated Please use amplitude.getInstance().regenerateDeviceId(); */ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { - this.setDeviceId(UUID() + 'R'); + this.getInstance().regenerateDeviceId(); }; /** @@ -617,21 +198,11 @@ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { * (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. + * @deprecated Please use amplitude.getInstance().setDeviceId(deviceId); * @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); - } + this.getInstance().setDeviceId(deviceId); }; /** @@ -640,50 +211,21 @@ Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { * @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. + * @deprecated Please use amplitude.getInstance.setUserProperties(userProperties); * @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); + this.getInstance().setUserProperties(userProperties); }; /** * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! * @public + * @deprecated Please use amplitude.getInstance().clearUserProperties(); * @example amplitude.clearUserProperties(); */ Amplitude.prototype.clearUserProperties = function clearUserProperties(){ - if (!this._apiKeySet('clearUserProperties()')) { - return; - } - - var identify = new Identify(); - identify.clearAll(); - this.identify(identify); -}; - -/** - * Applies the proxied functions on the proxied object to an instance of the real object. - * Used to convert proxied Identify and Revenue objects. - * @private - */ -var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) { - for (var i = 0; i < proxy._q.length; i++) { - var fn = instance[proxy._q[i][0]]; - if (type(fn) === 'function') { - fn.apply(instance, proxy._q[i].slice(1)); - } - } - return instance; + this.getInstance().clearUserProperties(); }; /** @@ -693,140 +235,24 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i * @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. + * @deprecated Please use amplitude.getInstance().identify(identify); * @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')) { - identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj); - } - - 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, null, 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'); - } + this.getInstance().identify(identify_obj, opt_callback); }; /** * Set a versionName for your application. * @public * @param {string} versionName - The version to set for your application. + * @deprecated Please use amplitude.getInstance().setVersionName(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, groups, 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 || {}; - groups = groups || {}; - 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 - groups: utils.truncate(utils.validateGroups(groups)) - // 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.getInstance().setVersionName(versionName); }; /** @@ -844,17 +270,11 @@ Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { * @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. + * @deprecated Please use amplitude.getInstance().logEvent(eventType, eventProperties, opt_callback); * @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, null, opt_callback); + return this.getInstance().logEvent(eventType, eventProperties, opt_callback); }; /** @@ -870,25 +290,11 @@ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt * groupName can be a string or an array of strings. * @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. + * Deprecated Please use amplitude.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { - if (!this._apiKeySet('logEventWithGroup()') || - !utils.validateInput(eventType, 'eventType', 'string')) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback); -}; - -/** - * Test that n is a number or a numeric value. - * @private - */ -var _isNumber = function _isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); + return this.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); }; /** @@ -898,51 +304,25 @@ var _isNumber = function _isNumber(n) { * for more information on the Revenue interface and logging revenue. * @public * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { - if (!this._apiKeySet('logRevenueV2()')) { - return; - } - - // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object - if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) { - revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj); - } - - if (revenue_obj instanceof Revenue) { - // only send if revenue is valid - if (revenue_obj && revenue_obj._isValidRevenue()) { - return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject()); - } - } else { - utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj)); - } + return this.getInstance().logRevenueV2(revenue_obj); }; /** * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 * @public - * @deprecated * @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 + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @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(Constants.REVENUE_EVENT, {}, { - productId: product, - special: 'revenue_amount', - quantity: quantity || 1, - price: price - }, null, null, null); + return this.getInstance().logRevenue(price, quantity, product); }; /** @@ -950,27 +330,7 @@ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { * @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; + this.getInstance().removeEvents(maxEventId, maxIdentifyId); }; /** @@ -981,126 +341,7 @@ var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { * 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 - }; + this.getInstance().sendEvents(callback); }; /** @@ -1109,7 +350,7 @@ Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys * @deprecated */ Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { - this.setUserProperties(userProperties); + this.getInstance().setUserProperties(userProperties); }; /** diff --git a/src/constants.js b/src/constants.js index 7072e5a3..172dc061 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,5 @@ module.exports = { + DEFAULT_INSTANCE: '$default_instance', API_VERSION: 2, MAX_STRING_LENGTH: 4096, IDENTIFY_EVENT: '$identify', From 66f7c5d97634ac6da641c272e42e236d2f480ee8 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Fri, 20 May 2016 19:25:40 -0700 Subject: [PATCH 04/13] fixing backwards compatability in amplitude tests --- amplitude.js | 86 ++--- amplitude.min.js | 4 +- src/amplitude-client.js | 86 ++--- test/amplitude.js | 786 ++++++++++++++++++++-------------------- 4 files changed, 481 insertions(+), 481 deletions(-) diff --git a/amplitude.js b/amplitude.js index 01455225..ba499f6a 100644 --- a/amplitude.js +++ b/amplitude.js @@ -499,7 +499,7 @@ var DEFAULT_OPTIONS = require('./options'); * @public * @example var amplitude = new Amplitude(); */ -var Amplitude = function Amplitude() { +var AmplitudeClient = function Amplitude() { this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); @@ -518,8 +518,8 @@ var Amplitude = function Amplitude() { this._sessionId = null; }; -Amplitude.prototype.Identify = Identify; -Amplitude.prototype.Revenue = Revenue; +AmplitudeClient.prototype.Identify = Identify; +AmplitudeClient.prototype.Revenue = Revenue; /** * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. @@ -532,7 +532,7 @@ Amplitude.prototype.Revenue = Revenue; * @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) { +AmplitudeClient.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; @@ -643,7 +643,7 @@ var _parseConfig = function _parseConfig(options, config) { * Run functions queued up by proxy loading snippet * @private */ -Amplitude.prototype.runQueuedFunctions = function () { +AmplitudeClient.prototype.runQueuedFunctions = function () { for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; if (type(fn) === 'function') { @@ -657,7 +657,7 @@ Amplitude.prototype.runQueuedFunctions = function () { * Check that the apiKey is set before calling a function. Logs a warning message if not set. * @private */ -Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { +AmplitudeClient.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; @@ -669,7 +669,7 @@ Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. * @private */ -Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { +AmplitudeClient.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); if (utils.isEmptyString(savedUnsentEventsString)) { return []; // new app, does not have any saved events @@ -692,7 +692,7 @@ Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(uns * @public * @return {boolean} Whether a new session was created during initialization. */ -Amplitude.prototype.isNewSession = function isNewSession() { +AmplitudeClient.prototype.isNewSession = function isNewSession() { return this._newSession; }; @@ -701,7 +701,7 @@ Amplitude.prototype.isNewSession = function isNewSession() { * @public * @return {number} Id of the current session. */ -Amplitude.prototype.getSessionId = function getSessionId() { +AmplitudeClient.prototype.getSessionId = function getSessionId() { return this._sessionId; }; @@ -709,7 +709,7 @@ Amplitude.prototype.getSessionId = function getSessionId() { * Increments the eventId and returns it. * @private */ -Amplitude.prototype.nextEventId = function nextEventId() { +AmplitudeClient.prototype.nextEventId = function nextEventId() { this._eventId++; return this._eventId; }; @@ -718,7 +718,7 @@ Amplitude.prototype.nextEventId = function nextEventId() { * Increments the identifyId and returns it. * @private */ -Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { +AmplitudeClient.prototype.nextIdentifyId = function nextIdentifyId() { this._identifyId++; return this._identifyId; }; @@ -727,7 +727,7 @@ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { * Increments the sequenceNumber and returns it. * @private */ -Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { +AmplitudeClient.prototype.nextSequenceNumber = function nextSequenceNumber() { this._sequenceNumber++; return this._sequenceNumber; }; @@ -736,7 +736,7 @@ Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { * Returns the total count of unsent events and identifys * @private */ -Amplitude.prototype._unsentCount = function _unsentCount() { +AmplitudeClient.prototype._unsentCount = function _unsentCount() { return this._unsentEvents.length + this._unsentIdentifys.length; }; @@ -744,7 +744,7 @@ Amplitude.prototype._unsentCount = function _unsentCount() { * Send events if ready. Returns true if events are sent. * @private */ -Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { +AmplitudeClient.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { if (this._unsentCount() === 0) { return false; } @@ -779,7 +779,7 @@ Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { * Storage argument allows for localStoraoge and sessionStoraoge * @private */ -Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { +AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, key) { return storage.getItem(key); }; @@ -788,7 +788,7 @@ Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { * Storage argument allows for localStoraoge and sessionStoraoge * @private */ -Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) { +AmplitudeClient.prototype._setInStorage = function _setInStorage(storage, key, value) { storage.setItem(key, value); }; @@ -902,7 +902,7 @@ var _saveCookieData = function _saveCookieData(scope) { * Parse the utm properties out of cookies and query for adding to user properties. * @private */ -Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { +AmplitudeClient.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { queryParams = queryParams || location.search; cookieParams = cookieParams || this.cookieStorage.get('__utmz'); var utmProperties = getUtmData(cookieParams, queryParams); @@ -946,7 +946,7 @@ var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSessi /** * @private */ -Amplitude.prototype._getReferrer = function _getReferrer() { +AmplitudeClient.prototype._getReferrer = function _getReferrer() { return document.referrer; }; @@ -954,7 +954,7 @@ Amplitude.prototype._getReferrer = function _getReferrer() { * Parse the domain from referrer info * @private */ -Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) { +AmplitudeClient.prototype._getReferringDomain = function _getReferringDomain(referrer) { if (utils.isEmptyString(referrer)) { return null; } @@ -970,7 +970,7 @@ Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) * 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) { +AmplitudeClient.prototype._saveReferrer = function _saveReferrer(referrer) { if (utils.isEmptyString(referrer)) { return; } @@ -986,7 +986,7 @@ Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { * Note: this is called automatically every time events are logged, unless you explicitly set option saveEvents to false. * @private */ -Amplitude.prototype.saveEvents = function saveEvents() { +AmplitudeClient.prototype.saveEvents = function saveEvents() { try { this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); } catch (e) {} @@ -1002,7 +1002,7 @@ Amplitude.prototype.saveEvents = function saveEvents() { * @param {string} domain to set. * @example amplitude.setDomain('.amplitude.com'); */ -Amplitude.prototype.setDomain = function setDomain(domain) { +AmplitudeClient.prototype.setDomain = function setDomain(domain) { if (!utils.validateInput(domain, 'domain', 'string')) { return; } @@ -1025,7 +1025,7 @@ Amplitude.prototype.setDomain = function setDomain(domain) { * @param {string} userId - identifier to set. Can be null. * @example amplitude.setUserId('joe@gmail.com'); */ -Amplitude.prototype.setUserId = function setUserId(userId) { +AmplitudeClient.prototype.setUserId = function setUserId(userId) { try { this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; _saveCookieData(this); @@ -1047,7 +1047,7 @@ Amplitude.prototype.setUserId = function setUserId(userId) { * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. */ -Amplitude.prototype.setGroup = function(groupType, groupName) { +AmplitudeClient.prototype.setGroup = function(groupType, groupName) { if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || utils.isEmptyString(groupType)) { return; @@ -1065,7 +1065,7 @@ Amplitude.prototype.setGroup = function(groupType, groupName) { * @param {boolean} enable - if true then no events will be logged or sent. * @example: amplitude.setOptOut(true); */ -Amplitude.prototype.setOptOut = function setOptOut(enable) { +AmplitudeClient.prototype.setOptOut = function setOptOut(enable) { if (!utils.validateInput(enable, 'enable', 'boolean')) { return; } @@ -1085,7 +1085,7 @@ Amplitude.prototype.setOptOut = function setOptOut(enable) { * This uses src/uuid.js to regenerate the deviceId. * @public */ -Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { +AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() { this.setDeviceId(UUID() + 'R'); }; @@ -1097,7 +1097,7 @@ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { * @param {string} deviceId - custom deviceId for current user. * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); */ -Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { +AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { if (!utils.validateInput(deviceId, 'deviceId', 'string')) { return; } @@ -1120,7 +1120,7 @@ Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { * 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) { +AmplitudeClient.prototype.setUserProperties = function setUserProperties(userProperties) { if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { return; } @@ -1139,7 +1139,7 @@ Amplitude.prototype.setUserProperties = function setUserProperties(userPropertie * @public * @example amplitude.clearUserProperties(); */ -Amplitude.prototype.clearUserProperties = function clearUserProperties(){ +AmplitudeClient.prototype.clearUserProperties = function clearUserProperties(){ if (!this._apiKeySet('clearUserProperties()')) { return; } @@ -1175,7 +1175,7 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i * 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) { +AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) { if (!this._apiKeySet('identify()')) { if (type(opt_callback) === 'function') { opt_callback(0, 'No request sent'); @@ -1210,7 +1210,7 @@ Amplitude.prototype.identify = function(identify_obj, opt_callback) { * @param {string} versionName - The version to set for your application. * @example amplitude.setVersionName('1.12.3'); */ -Amplitude.prototype.setVersionName = function setVersionName(versionName) { +AmplitudeClient.prototype.setVersionName = function setVersionName(versionName) { if (!utils.validateInput(versionName, 'versionName', 'string')) { return; } @@ -1221,7 +1221,7 @@ Amplitude.prototype.setVersionName = function setVersionName(versionName) { * Private logEvent method. Keeps apiProperties from being publicly exposed. * @private */ -Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) { +AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, 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') { @@ -1301,7 +1301,7 @@ Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, a * 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) { +AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue) { if (queue.length > this.options.savedMaxCount) { queue.splice(0, queue.length - this.options.savedMaxCount); } @@ -1324,7 +1324,7 @@ Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { * 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) { +AmplitudeClient.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') { @@ -1350,7 +1350,7 @@ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt * Note: the server response code and response body from the event upload are passed to the callback function. * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ -Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { +AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { if (!this._apiKeySet('logEventWithGroup()') || !utils.validateInput(eventType, 'eventType', 'string')) { if (type(opt_callback) === 'function') { @@ -1379,7 +1379,7 @@ var _isNumber = function _isNumber(n) { * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ -Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { +AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { if (!this._apiKeySet('logRevenueV2()')) { return; } @@ -1408,7 +1408,7 @@ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { * @param {string} product - (optional) product identifier * @example amplitude.logRevenue(3.99, 1, 'product_1234'); */ -Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { +AmplitudeClient.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'); @@ -1427,7 +1427,7 @@ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { * Remove events in storage with event ids up to and including maxEventId. * @private */ -Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { +AmplitudeClient.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { _removeEvents(this, '_unsentEvents', maxEventId); _removeEvents(this, '_unsentIdentifys', maxIdentifyId); }; @@ -1458,7 +1458,7 @@ var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { * @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) { +AmplitudeClient.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'); @@ -1527,7 +1527,7 @@ Amplitude.prototype.sendEvents = function sendEvents(callback) { * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. * @private */ -Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { +AmplitudeClient.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { // coalesce events from both queues var eventsToSend = []; var eventIndex = 0; @@ -1586,7 +1586,7 @@ Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys * @public * @deprecated */ -Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { +AmplitudeClient.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { this.setUserProperties(userProperties); }; @@ -1596,9 +1596,9 @@ Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(u * @returns {number} version number * @example var amplitudeVersion = amplitude.__VERSION__; */ -Amplitude.prototype.__VERSION__ = version; +AmplitudeClient.prototype.__VERSION__ = version; -module.exports = Amplitude; +module.exports = AmplitudeClient; }, {"./constants":4,"./cookiestorage":12,"./utm":13,"./identify":5,"json":14,"./localstorage":15,"JavaScript-MD5":16,"object":6,"./xhr":17,"./revenue":7,"./type":8,"ua-parser-js":18,"./utils":9,"./uuid":19,"./version":10,"./options":11}], 4: [function(require, module, exports) { diff --git a/amplitude.min.js b/amplitude.min.js index c7dda26c..da53d7a4 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};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.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};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)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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,null,opt_callback)};Amplitude.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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;jthis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key)};AmplitudeClient.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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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},{}],10:[function(require,module,exports){module.exports="2.12.1"},{}],11:[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":29}],29:[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/src/amplitude-client.js b/src/amplitude-client.js index 998ae69f..a268877f 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -21,7 +21,7 @@ var DEFAULT_OPTIONS = require('./options'); * @public * @example var amplitude = new Amplitude(); */ -var Amplitude = function Amplitude() { +var AmplitudeClient = function Amplitude() { this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); @@ -40,8 +40,8 @@ var Amplitude = function Amplitude() { this._sessionId = null; }; -Amplitude.prototype.Identify = Identify; -Amplitude.prototype.Revenue = Revenue; +AmplitudeClient.prototype.Identify = Identify; +AmplitudeClient.prototype.Revenue = Revenue; /** * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. @@ -54,7 +54,7 @@ Amplitude.prototype.Revenue = Revenue; * @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) { +AmplitudeClient.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; @@ -165,7 +165,7 @@ var _parseConfig = function _parseConfig(options, config) { * Run functions queued up by proxy loading snippet * @private */ -Amplitude.prototype.runQueuedFunctions = function () { +AmplitudeClient.prototype.runQueuedFunctions = function () { for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; if (type(fn) === 'function') { @@ -179,7 +179,7 @@ Amplitude.prototype.runQueuedFunctions = function () { * Check that the apiKey is set before calling a function. Logs a warning message if not set. * @private */ -Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { +AmplitudeClient.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; @@ -191,7 +191,7 @@ Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. * @private */ -Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { +AmplitudeClient.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); if (utils.isEmptyString(savedUnsentEventsString)) { return []; // new app, does not have any saved events @@ -214,7 +214,7 @@ Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(uns * @public * @return {boolean} Whether a new session was created during initialization. */ -Amplitude.prototype.isNewSession = function isNewSession() { +AmplitudeClient.prototype.isNewSession = function isNewSession() { return this._newSession; }; @@ -223,7 +223,7 @@ Amplitude.prototype.isNewSession = function isNewSession() { * @public * @return {number} Id of the current session. */ -Amplitude.prototype.getSessionId = function getSessionId() { +AmplitudeClient.prototype.getSessionId = function getSessionId() { return this._sessionId; }; @@ -231,7 +231,7 @@ Amplitude.prototype.getSessionId = function getSessionId() { * Increments the eventId and returns it. * @private */ -Amplitude.prototype.nextEventId = function nextEventId() { +AmplitudeClient.prototype.nextEventId = function nextEventId() { this._eventId++; return this._eventId; }; @@ -240,7 +240,7 @@ Amplitude.prototype.nextEventId = function nextEventId() { * Increments the identifyId and returns it. * @private */ -Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { +AmplitudeClient.prototype.nextIdentifyId = function nextIdentifyId() { this._identifyId++; return this._identifyId; }; @@ -249,7 +249,7 @@ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { * Increments the sequenceNumber and returns it. * @private */ -Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { +AmplitudeClient.prototype.nextSequenceNumber = function nextSequenceNumber() { this._sequenceNumber++; return this._sequenceNumber; }; @@ -258,7 +258,7 @@ Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { * Returns the total count of unsent events and identifys * @private */ -Amplitude.prototype._unsentCount = function _unsentCount() { +AmplitudeClient.prototype._unsentCount = function _unsentCount() { return this._unsentEvents.length + this._unsentIdentifys.length; }; @@ -266,7 +266,7 @@ Amplitude.prototype._unsentCount = function _unsentCount() { * Send events if ready. Returns true if events are sent. * @private */ -Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { +AmplitudeClient.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { if (this._unsentCount() === 0) { return false; } @@ -301,7 +301,7 @@ Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { * Storage argument allows for localStoraoge and sessionStoraoge * @private */ -Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { +AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, key) { return storage.getItem(key); }; @@ -310,7 +310,7 @@ Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { * Storage argument allows for localStoraoge and sessionStoraoge * @private */ -Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) { +AmplitudeClient.prototype._setInStorage = function _setInStorage(storage, key, value) { storage.setItem(key, value); }; @@ -424,7 +424,7 @@ var _saveCookieData = function _saveCookieData(scope) { * Parse the utm properties out of cookies and query for adding to user properties. * @private */ -Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { +AmplitudeClient.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { queryParams = queryParams || location.search; cookieParams = cookieParams || this.cookieStorage.get('__utmz'); var utmProperties = getUtmData(cookieParams, queryParams); @@ -468,7 +468,7 @@ var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSessi /** * @private */ -Amplitude.prototype._getReferrer = function _getReferrer() { +AmplitudeClient.prototype._getReferrer = function _getReferrer() { return document.referrer; }; @@ -476,7 +476,7 @@ Amplitude.prototype._getReferrer = function _getReferrer() { * Parse the domain from referrer info * @private */ -Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) { +AmplitudeClient.prototype._getReferringDomain = function _getReferringDomain(referrer) { if (utils.isEmptyString(referrer)) { return null; } @@ -492,7 +492,7 @@ Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) * 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) { +AmplitudeClient.prototype._saveReferrer = function _saveReferrer(referrer) { if (utils.isEmptyString(referrer)) { return; } @@ -508,7 +508,7 @@ Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { * Note: this is called automatically every time events are logged, unless you explicitly set option saveEvents to false. * @private */ -Amplitude.prototype.saveEvents = function saveEvents() { +AmplitudeClient.prototype.saveEvents = function saveEvents() { try { this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); } catch (e) {} @@ -524,7 +524,7 @@ Amplitude.prototype.saveEvents = function saveEvents() { * @param {string} domain to set. * @example amplitude.setDomain('.amplitude.com'); */ -Amplitude.prototype.setDomain = function setDomain(domain) { +AmplitudeClient.prototype.setDomain = function setDomain(domain) { if (!utils.validateInput(domain, 'domain', 'string')) { return; } @@ -547,7 +547,7 @@ Amplitude.prototype.setDomain = function setDomain(domain) { * @param {string} userId - identifier to set. Can be null. * @example amplitude.setUserId('joe@gmail.com'); */ -Amplitude.prototype.setUserId = function setUserId(userId) { +AmplitudeClient.prototype.setUserId = function setUserId(userId) { try { this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; _saveCookieData(this); @@ -569,7 +569,7 @@ Amplitude.prototype.setUserId = function setUserId(userId) { * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. */ -Amplitude.prototype.setGroup = function(groupType, groupName) { +AmplitudeClient.prototype.setGroup = function(groupType, groupName) { if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || utils.isEmptyString(groupType)) { return; @@ -587,7 +587,7 @@ Amplitude.prototype.setGroup = function(groupType, groupName) { * @param {boolean} enable - if true then no events will be logged or sent. * @example: amplitude.setOptOut(true); */ -Amplitude.prototype.setOptOut = function setOptOut(enable) { +AmplitudeClient.prototype.setOptOut = function setOptOut(enable) { if (!utils.validateInput(enable, 'enable', 'boolean')) { return; } @@ -607,7 +607,7 @@ Amplitude.prototype.setOptOut = function setOptOut(enable) { * This uses src/uuid.js to regenerate the deviceId. * @public */ -Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { +AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() { this.setDeviceId(UUID() + 'R'); }; @@ -619,7 +619,7 @@ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { * @param {string} deviceId - custom deviceId for current user. * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); */ -Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { +AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { if (!utils.validateInput(deviceId, 'deviceId', 'string')) { return; } @@ -642,7 +642,7 @@ Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { * 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) { +AmplitudeClient.prototype.setUserProperties = function setUserProperties(userProperties) { if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { return; } @@ -661,7 +661,7 @@ Amplitude.prototype.setUserProperties = function setUserProperties(userPropertie * @public * @example amplitude.clearUserProperties(); */ -Amplitude.prototype.clearUserProperties = function clearUserProperties(){ +AmplitudeClient.prototype.clearUserProperties = function clearUserProperties(){ if (!this._apiKeySet('clearUserProperties()')) { return; } @@ -697,7 +697,7 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i * 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) { +AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) { if (!this._apiKeySet('identify()')) { if (type(opt_callback) === 'function') { opt_callback(0, 'No request sent'); @@ -732,7 +732,7 @@ Amplitude.prototype.identify = function(identify_obj, opt_callback) { * @param {string} versionName - The version to set for your application. * @example amplitude.setVersionName('1.12.3'); */ -Amplitude.prototype.setVersionName = function setVersionName(versionName) { +AmplitudeClient.prototype.setVersionName = function setVersionName(versionName) { if (!utils.validateInput(versionName, 'versionName', 'string')) { return; } @@ -743,7 +743,7 @@ Amplitude.prototype.setVersionName = function setVersionName(versionName) { * Private logEvent method. Keeps apiProperties from being publicly exposed. * @private */ -Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) { +AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, 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') { @@ -823,7 +823,7 @@ Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, a * 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) { +AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue) { if (queue.length > this.options.savedMaxCount) { queue.splice(0, queue.length - this.options.savedMaxCount); } @@ -846,7 +846,7 @@ Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { * 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) { +AmplitudeClient.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') { @@ -872,7 +872,7 @@ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt * Note: the server response code and response body from the event upload are passed to the callback function. * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ -Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { +AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { if (!this._apiKeySet('logEventWithGroup()') || !utils.validateInput(eventType, 'eventType', 'string')) { if (type(opt_callback) === 'function') { @@ -901,7 +901,7 @@ var _isNumber = function _isNumber(n) { * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ -Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { +AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { if (!this._apiKeySet('logRevenueV2()')) { return; } @@ -930,7 +930,7 @@ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { * @param {string} product - (optional) product identifier * @example amplitude.logRevenue(3.99, 1, 'product_1234'); */ -Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { +AmplitudeClient.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'); @@ -949,7 +949,7 @@ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { * Remove events in storage with event ids up to and including maxEventId. * @private */ -Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { +AmplitudeClient.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { _removeEvents(this, '_unsentEvents', maxEventId); _removeEvents(this, '_unsentIdentifys', maxIdentifyId); }; @@ -980,7 +980,7 @@ var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { * @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) { +AmplitudeClient.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'); @@ -1049,7 +1049,7 @@ Amplitude.prototype.sendEvents = function sendEvents(callback) { * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. * @private */ -Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { +AmplitudeClient.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { // coalesce events from both queues var eventsToSend = []; var eventIndex = 0; @@ -1108,7 +1108,7 @@ Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys * @public * @deprecated */ -Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { +AmplitudeClient.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { this.setUserProperties(userProperties); }; @@ -1118,6 +1118,6 @@ Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(u * @returns {number} version number * @example var amplitudeVersion = amplitude.__VERSION__; */ -Amplitude.prototype.__VERSION__ = version; +AmplitudeClient.prototype.__VERSION__ = version; -module.exports = Amplitude; +module.exports = AmplitudeClient; diff --git a/test/amplitude.js b/test/amplitude.js index 6cd941d0..a01c4fc9 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -33,7 +33,7 @@ describe('Amplitude', function() { function reset() { localStorage.clear(); sessionStorage.clear(); - cookie.remove(amplitude.options.cookieName); + cookie.remove(amplitude.getInstance().options.cookieName); cookie.reset(); } @@ -47,28 +47,28 @@ describe('Amplitude', function() { }); it('fails on invalid apiKeys', function() { - amplitude.init(null); - assert.equal(amplitude.options.apiKey, undefined); - assert.equal(amplitude.options.deviceId, undefined); + amplitude.getInstance().init(null); + assert.equal(amplitude.getInstance().options.apiKey, undefined); + assert.equal(amplitude.getInstance().options.deviceId, undefined); - amplitude.init(''); - assert.equal(amplitude.options.apiKey, undefined); - assert.equal(amplitude.options.deviceId, undefined); + amplitude.getInstance().init(''); + assert.equal(amplitude.getInstance().options.apiKey, undefined); + assert.equal(amplitude.getInstance().options.deviceId, undefined); - amplitude.init(apiKey); - assert.equal(amplitude.options.apiKey, apiKey); - assert.lengthOf(amplitude.options.deviceId, 37); + amplitude.getInstance().init(apiKey); + assert.equal(amplitude.getInstance().options.apiKey, apiKey); + assert.lengthOf(amplitude.getInstance().options.deviceId, 37); }); it('should accept userId', function() { - amplitude.init(apiKey, userId); - assert.equal(amplitude.options.userId, userId); + amplitude.getInstance().init(apiKey, userId); + assert.equal(amplitude.getInstance().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'); + amplitude.getInstance().init(apiKey, userId); + assert.lengthOf(amplitude.getInstance().options.deviceId, 37); // UUID is length 36, but we append 'R' at end + assert.equal(amplitude.getInstance().options.deviceId[36], 'R'); }); it('should validate config values', function() { @@ -82,37 +82,37 @@ describe('Amplitude', function() { 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); + amplitude.getInstance().init(apiKey, userId, config); + assert.equal(amplitude.getInstance().options.apiEndpoint, 'api.amplitude.getInstance().com'); + assert.equal(amplitude.getInstance().options.batchEvents, false); + assert.equal(amplitude.getInstance().options.cookieExpiration, 3650); + assert.equal(amplitude.getInstance().options.cookieName, 'amplitude_id'); + assert.equal(amplitude.getInstance().options.eventUploadPeriodMillis, 30000); + assert.equal(amplitude.getInstance().options.eventUploadThreshold, 30); + assert.equal(amplitude.getInstance().options.bogusKey, undefined); }); it('should set cookie', function() { - amplitude.init(apiKey, userId); - var stored = cookie.get(amplitude.options.cookieName); + amplitude.getInstance().init(apiKey, userId); + var stored = cookie.get(amplitude.getInstance().options.cookieName); assert.property(stored, 'deviceId'); assert.propertyVal(stored, 'userId', userId); assert.lengthOf(stored.deviceId, 37); // increase deviceId length by 1 for 'R' character }); it('should set language', function() { - amplitude.init(apiKey, userId); - assert.property(amplitude.options, 'language'); - assert.isNotNull(amplitude.options.language); + amplitude.getInstance().init(apiKey, userId); + assert.property(amplitude.getInstance().options, 'language'); + assert.isNotNull(amplitude.getInstance().options.language); }); it('should allow language override', function() { - amplitude.init(apiKey, userId, {language: 'en-GB'}); - assert.propertyVal(amplitude.options, 'language', 'en-GB'); + amplitude.getInstance().init(apiKey, userId, {language: 'en-GB'}); + assert.propertyVal(amplitude.getInstance().options, 'language', 'en-GB'); }); it ('should not run callback if invalid callback', function() { - amplitude.init(apiKey, userId, null, 'invalid callback'); + amplitude.getInstance().init(apiKey, userId, null, 'invalid callback'); }); it ('should run valid callbacks', function() { @@ -120,7 +120,7 @@ describe('Amplitude', function() { var callback = function() { counter++; }; - amplitude.init(apiKey, userId, null, callback); + amplitude.getInstance().init(apiKey, userId, null, callback); assert.equal(counter, 1); }); @@ -128,17 +128,17 @@ describe('Amplitude', function() { var deviceId = 'test_device_id'; var userId = 'test_user_id'; - assert.isNull(cookie.get(amplitude.options.cookieName)); + assert.isNull(cookie.get(amplitude.getInstance().options.cookieName)); localStorage.setItem('amplitude_deviceId' + keySuffix, deviceId); localStorage.setItem('amplitude_userId' + keySuffix, userId); localStorage.setItem('amplitude_optOut' + keySuffix, true); - amplitude.init(apiKey); - assert.equal(amplitude.options.deviceId, deviceId); - assert.equal(amplitude.options.userId, userId); - assert.isTrue(amplitude.options.optOut); + amplitude.getInstance().init(apiKey); + assert.equal(amplitude.getInstance().options.deviceId, deviceId); + assert.equal(amplitude.getInstance().options.userId, userId); + assert.isTrue(amplitude.getInstance().options.optOut); - var cookieData = cookie.get(amplitude.options.cookieName); + var cookieData = cookie.get(amplitude.getInstance().options.cookieName); assert.equal(cookieData.deviceId, deviceId); assert.equal(cookieData.userId, userId); assert.isTrue(cookieData.optOut); @@ -147,24 +147,24 @@ describe('Amplitude', function() { it('should migrate session and event info from localStorage to cookie', function() { var now = new Date().getTime(); - assert.isNull(cookie.get(amplitude.options.cookieName)); + assert.isNull(cookie.get(amplitude.getInstance().options.cookieName)); localStorage.setItem('amplitude_sessionId', now); localStorage.setItem('amplitude_lastEventTime', now); localStorage.setItem('amplitude_lastEventId', 3000); localStorage.setItem('amplitude_lastIdentifyId', 4000); localStorage.setItem('amplitude_lastSequenceNumber', 5000); - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); - assert.equal(amplitude._sessionId, now); - assert.isTrue(amplitude._lastEventTime >= now); - assert.equal(amplitude._eventId, 3000); - assert.equal(amplitude._identifyId, 4000); - assert.equal(amplitude._sequenceNumber, 5000); + assert.equal(amplitude.getInstance()._sessionId, now); + assert.isTrue(amplitude.getInstance()._lastEventTime >= now); + assert.equal(amplitude.getInstance()._eventId, 3000); + assert.equal(amplitude.getInstance()._identifyId, 4000); + assert.equal(amplitude.getInstance()._sequenceNumber, 5000); - var cookieData = cookie.get(amplitude.options.cookieName); + var cookieData = cookie.get(amplitude.getInstance().options.cookieName); assert.equal(cookieData.sessionId, now); - assert.equal(cookieData.lastEventTime, amplitude._lastEventTime); + assert.equal(cookieData.lastEventTime, amplitude.getInstance()._lastEventTime); assert.equal(cookieData.eventId, 3000); assert.equal(cookieData.identifyId, 4000); assert.equal(cookieData.sequenceNumber, 5000); @@ -183,7 +183,7 @@ describe('Amplitude', function() { identifyId: 60 } - cookie.set(amplitude.options.cookieName, cookieData); + cookie.set(amplitude.getInstance().options.cookieName, cookieData); localStorage.setItem('amplitude_deviceId' + keySuffix, 'old_device_id'); localStorage.setItem('amplitude_userId' + keySuffix, 'fake_user_id'); localStorage.setItem('amplitude_optOut' + keySuffix, true); @@ -193,21 +193,21 @@ describe('Amplitude', function() { localStorage.setItem('amplitude_lastIdentifyId', 30); localStorage.setItem('amplitude_lastSequenceNumber', 40); - amplitude.init(apiKey); - assert.equal(amplitude.options.deviceId, 'old_device_id'); - assert.equal(amplitude.options.userId, 'test_user_id'); - assert.isFalse(amplitude.options.optOut); - assert.equal(amplitude._sessionId, now); - assert.isTrue(amplitude._lastEventTime >= now); - assert.equal(amplitude._eventId, 50); - assert.equal(amplitude._identifyId, 60); - assert.equal(amplitude._sequenceNumber, 40); + amplitude.getInstance().init(apiKey); + assert.equal(amplitude.getInstance().options.deviceId, 'old_device_id'); + assert.equal(amplitude.getInstance().options.userId, 'test_user_id'); + assert.isFalse(amplitude.getInstance().options.optOut); + assert.equal(amplitude.getInstance()._sessionId, now); + assert.isTrue(amplitude.getInstance()._lastEventTime >= now); + assert.equal(amplitude.getInstance()._eventId, 50); + assert.equal(amplitude.getInstance()._identifyId, 60); + assert.equal(amplitude.getInstance()._sequenceNumber, 40); }); it('should skip the migration if the new cookie already has deviceId, sessionId, lastEventTime', function() { var now = new Date().getTime(); - cookie.set(amplitude.options.cookieName, { + cookie.set(amplitude.getInstance().options.cookieName, { deviceId: 'new_device_id', sessionId: now, lastEventTime: now @@ -222,15 +222,15 @@ describe('Amplitude', function() { localStorage.setItem('amplitude_lastIdentifyId', 30); localStorage.setItem('amplitude_lastSequenceNumber', 40); - amplitude.init(apiKey, 'new_user_id'); - assert.equal(amplitude.options.deviceId, 'new_device_id'); - assert.equal(amplitude.options.userId, 'new_user_id'); - assert.isFalse(amplitude.options.optOut); - assert.isTrue(amplitude._sessionId >= now); - assert.isTrue(amplitude._lastEventTime >= now); - assert.equal(amplitude._eventId, 0); - assert.equal(amplitude._identifyId, 0); - assert.equal(amplitude._sequenceNumber, 0); + amplitude.getInstance().init(apiKey, 'new_user_id'); + assert.equal(amplitude.getInstance().options.deviceId, 'new_device_id'); + assert.equal(amplitude.getInstance().options.userId, 'new_user_id'); + assert.isFalse(amplitude.getInstance().options.optOut); + assert.isTrue(amplitude.getInstance()._sessionId >= now); + assert.isTrue(amplitude.getInstance()._lastEventTime >= now); + assert.equal(amplitude.getInstance()._eventId, 0); + assert.equal(amplitude.getInstance()._identifyId, 0); + assert.equal(amplitude.getInstance()._sequenceNumber, 0); }); it('should save cookie data to localStorage if cookies are not enabled', function() { @@ -512,7 +512,7 @@ describe('Amplitude', function() { describe('runQueuedFunctions', function() { beforeEach(function() { - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -520,7 +520,7 @@ describe('Amplitude', function() { }); it('should run queued functions', function() { - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); assert.lengthOf(server.requests, 0); var userId = 'testUserId' var eventType = 'test_event' @@ -528,24 +528,24 @@ describe('Amplitude', function() { ['setUserId', userId], ['logEvent', eventType] ]; - amplitude._q = functions; - assert.lengthOf(amplitude._q, 2); - amplitude.runQueuedFunctions(); + amplitude.getInstance()._q = functions; + assert.lengthOf(amplitude.getInstance()._q, 2); + amplitude.getInstance().runQueuedFunctions(); - assert.equal(amplitude.options.userId, userId); - assert.equal(amplitude._unsentCount(), 1); + assert.equal(amplitude.getInstance().options.userId, userId); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); assert.equal(events[0].event_type, eventType); - assert.lengthOf(amplitude._q, 0); + assert.lengthOf(amplitude.getInstance()._q, 0); }); }); describe('setUserProperties', function() { beforeEach(function() { - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -553,12 +553,12 @@ describe('Amplitude', function() { }); it('should log identify call from set user properties', function() { - assert.equal(amplitude._unsentCount(), 0); - amplitude.setUserProperties({'prop': true, 'key': 'value'}); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + amplitude.getInstance().setUserProperties({'prop': true, 'key': 'value'}); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -577,7 +577,7 @@ describe('Amplitude', function() { describe('clearUserProperties', function() { beforeEach(function() { - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -585,12 +585,12 @@ describe('Amplitude', function() { }); it('should log identify call from clear user properties', function() { - assert.equal(amplitude._unsentCount(), 0); - amplitude.clearUserProperties(); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + amplitude.getInstance().clearUserProperties(); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -607,7 +607,7 @@ describe('Amplitude', function() { describe('setGroup', function() { beforeEach(function() { reset(); - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -615,7 +615,7 @@ describe('Amplitude', function() { }); it('should generate an identify event with groups set', function() { - amplitude.setGroup('orgId', 15); + amplitude.getInstance().setGroup('orgId', 15); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -633,15 +633,15 @@ describe('Amplitude', function() { }); it('should ignore empty string groupTypes', function() { - amplitude.setGroup('', 15); + amplitude.getInstance().setGroup('', 15); assert.lengthOf(server.requests, 0); }); it('should ignore non-string groupTypes', function() { - amplitude.setGroup(10, 10); - amplitude.setGroup([], 15); - amplitude.setGroup({}, 20); - amplitude.setGroup(true, false); + amplitude.getInstance().setGroup(10, 10); + amplitude.getInstance().setGroup([], 15); + amplitude.getInstance().setGroup({}, 20); + amplitude.getInstance().setGroup(true, false); assert.lengthOf(server.requests, 0); }); }); @@ -657,15 +657,15 @@ describe('setVersionName', function() { }); 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'); + amplitude.getInstance().init(apiKey, null, {batchEvents: true}); + amplitude.getInstance().setVersionName('testVersionName1'); + amplitude.getInstance().logEvent('testEvent1'); + assert.equal(amplitude.getInstance()._unsentEvents[0].version_name, 'testVersionName1'); // should ignore non-string values - amplitude.setVersionName(15000); - amplitude.logEvent('testEvent2'); - assert.equal(amplitude._unsentEvents[1].version_name, 'testVersionName1'); + amplitude.getInstance().setVersionName(15000); + amplitude.getInstance().logEvent('testEvent2'); + assert.equal(amplitude.getInstance()._unsentEvents[1].version_name, 'testVersionName1'); }); }); @@ -680,11 +680,11 @@ describe('setVersionName', function() { it('should regenerate the deviceId', function() { var deviceId = 'oldDeviceId'; - amplitude.init(apiKey, null, {'deviceId': deviceId}); - amplitude.regenerateDeviceId(); - assert.notEqual(amplitude.options.deviceId, deviceId); - assert.lengthOf(amplitude.options.deviceId, 37); - assert.equal(amplitude.options.deviceId[36], 'R'); + amplitude.getInstance().init(apiKey, null, {'deviceId': deviceId}); + amplitude.getInstance().regenerateDeviceId(); + assert.notEqual(amplitude.getInstance().options.deviceId, deviceId); + assert.lengthOf(amplitude.getInstance().options.deviceId, 37); + assert.equal(amplitude.getInstance().options.deviceId[36], 'R'); }); }); @@ -699,29 +699,29 @@ describe('setVersionName', function() { }); it('should change device id', function() { - amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); - amplitude.setDeviceId('deviceId'); - assert.equal(amplitude.options.deviceId, 'deviceId'); + amplitude.getInstance().init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.getInstance().setDeviceId('deviceId'); + assert.equal(amplitude.getInstance().options.deviceId, 'deviceId'); }); it('should not change device id if empty', function() { - amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); - amplitude.setDeviceId(''); - assert.notEqual(amplitude.options.deviceId, ''); - assert.equal(amplitude.options.deviceId, 'deviceId'); + amplitude.getInstance().init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.getInstance().setDeviceId(''); + assert.notEqual(amplitude.getInstance().options.deviceId, ''); + assert.equal(amplitude.getInstance().options.deviceId, 'deviceId'); }); it('should not change device id if null', function() { - amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); - amplitude.setDeviceId(null); - assert.notEqual(amplitude.options.deviceId, null); - assert.equal(amplitude.options.deviceId, 'deviceId'); + amplitude.getInstance().init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.getInstance().setDeviceId(null); + assert.notEqual(amplitude.getInstance().options.deviceId, null); + assert.equal(amplitude.getInstance().options.deviceId, 'deviceId'); }); it('should store device id in cookie', function() { - amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); - amplitude.setDeviceId('deviceId'); - var stored = cookie.get(amplitude.options.cookieName); + amplitude.getInstance().init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.getInstance().setDeviceId('deviceId'); + var stored = cookie.get(amplitude.getInstance().options.cookieName); assert.propertyVal(stored, 'deviceId', 'deviceId'); }); }); @@ -730,7 +730,7 @@ describe('setVersionName', function() { beforeEach(function() { clock = sinon.useFakeTimers(); - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -739,30 +739,30 @@ describe('setVersionName', function() { }); it('should ignore inputs that are not identify objects', function() { - amplitude.identify('This is a test'); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify('This is a test'); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.identify(150); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify(150); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.identify(['test']); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify(['test']); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.identify({'user_prop': true}); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify({'user_prop': true}); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); it('should generate an event from the identify object', function() { var identify = new Identify().set('prop1', 'value1').unset('prop2').add('prop3', 3).setOnce('prop4', true); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -785,18 +785,18 @@ describe('setVersionName', function() { }); it('should ignore empty identify objects', function() { - amplitude.identify(new Identify()); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify(new Identify()); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); it('should ignore empty proxy identify objects', function() { - amplitude.identify({'_q': {}}); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify({'_q': {}}); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.identify({}); - assert.lengthOf(amplitude._unsentIdentifys, 0); + amplitude.getInstance().identify({}); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); @@ -809,11 +809,11 @@ describe('setVersionName', function() { ['set', 'key4', 'value5'], ['prepend', 'key5', 'value6'] ]}; - amplitude.identify(proxyObject); + amplitude.getInstance().identify(proxyObject); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -836,8 +836,8 @@ describe('setVersionName', function() { value = status; message = response; } - var identify = new amplitude.Identify().set('key', 'value'); - amplitude.identify(identify, callback); + var identify = new amplitude.getInstance().Identify().set('key', 'value'); + amplitude.getInstance().identify(identify, callback); // before server responds, callback should not fire assert.lengthOf(server.requests, 1); @@ -862,7 +862,7 @@ describe('setVersionName', function() { value = status; message = response; } - var identify = new amplitude.Identify().set('key', 'value'); + var identify = new amplitude.getInstance().Identify().set('key', 'value'); new Amplitude().identify(identify, callback); // verify callback fired @@ -880,7 +880,7 @@ describe('setVersionName', function() { value = status; message = response; } - amplitude.identify(null, callback); + amplitude.getInstance().identify(null, callback); // verify callback fired assert.equal(counter, 1); @@ -895,7 +895,7 @@ describe('setVersionName', function() { beforeEach(function() { clock = sinon.useFakeTimers(); - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -904,32 +904,32 @@ describe('setVersionName', function() { }); it('should send request', function() { - amplitude.logEvent('Event Type 1'); + amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); - assert.equal(server.requests[0].url, 'http://api.amplitude.com/'); + assert.equal(server.requests[0].url, 'http://api.amplitude.getInstance().com/'); assert.equal(server.requests[0].method, 'POST'); assert.equal(server.requests[0].async, true); }); it('should reject empty event types', function() { - amplitude.logEvent(); + amplitude.getInstance().logEvent(); assert.lengthOf(server.requests, 0); }); it('should send api key', function() { - amplitude.logEvent('Event Type 2'); + amplitude.getInstance().logEvent('Event Type 2'); assert.lengthOf(server.requests, 1); assert.equal(querystring.parse(server.requests[0].requestBody).client, apiKey); }); it('should send api version', function() { - amplitude.logEvent('Event Type 3'); + amplitude.getInstance().logEvent('Event Type 3'); assert.lengthOf(server.requests, 1); assert.equal(querystring.parse(server.requests[0].requestBody).v, '2'); }); it('should send event JSON', function() { - amplitude.logEvent('Event Type 4'); + amplitude.getInstance().logEvent('Event Type 4'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); @@ -937,7 +937,7 @@ describe('setVersionName', function() { }); it('should send language', function() { - amplitude.logEvent('Event Should Send Language'); + amplitude.getInstance().logEvent('Event Should Send Language'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); @@ -945,20 +945,20 @@ describe('setVersionName', function() { }); it('should accept properties', function() { - amplitude.logEvent('Event Type 5', {prop: true}); + amplitude.getInstance().logEvent('Event Type 5', {prop: true}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.deepEqual(events[0].event_properties, {prop: true}); }); it('should queue events', function() { - amplitude._sending = true; - amplitude.logEvent('Event', {index: 1}); - amplitude.logEvent('Event', {index: 2}); - amplitude.logEvent('Event', {index: 3}); - amplitude._sending = false; + amplitude.getInstance()._sending = true; + amplitude.getInstance().logEvent('Event', {index: 1}); + amplitude.getInstance().logEvent('Event', {index: 2}); + amplitude.getInstance().logEvent('Event', {index: 3}); + amplitude.getInstance()._sending = false; - amplitude.logEvent('Event', {index: 100}); + amplitude.getInstance().logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -968,15 +968,15 @@ describe('setVersionName', function() { }); it('should limit events queued', function() { - amplitude.init(apiKey, null, {savedMaxCount: 10}); + amplitude.getInstance().init(apiKey, null, {savedMaxCount: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; - amplitude.logEvent('Event', {index: 100}); + amplitude.getInstance().logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -986,16 +986,16 @@ describe('setVersionName', function() { }); it('should remove only sent events', function() { - amplitude._sending = true; - amplitude.logEvent('Event', {index: 1}); - amplitude.logEvent('Event', {index: 2}); - amplitude._sending = false; - amplitude.logEvent('Event', {index: 3}); + amplitude.getInstance()._sending = true; + amplitude.getInstance().logEvent('Event', {index: 1}); + amplitude.getInstance().logEvent('Event', {index: 2}); + amplitude.getInstance()._sending = false; + amplitude.getInstance().logEvent('Event', {index: 3}); server.respondWith('success'); server.respond(); - amplitude.logEvent('Event', {index: 4}); + amplitude.getInstance().logEvent('Event', {index: 4}); assert.lengthOf(server.requests, 2); var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); @@ -1004,21 +1004,21 @@ describe('setVersionName', function() { }); it('should save events', function() { - amplitude.init(apiKey, null, {saveEvents: true}); - amplitude.logEvent('Event', {index: 1}); - amplitude.logEvent('Event', {index: 2}); - amplitude.logEvent('Event', {index: 3}); + amplitude.getInstance().init(apiKey, null, {saveEvents: true}); + amplitude.getInstance().logEvent('Event', {index: 1}); + amplitude.getInstance().logEvent('Event', {index: 2}); + amplitude.getInstance().logEvent('Event', {index: 3}); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.deepEqual(amplitude2._unsentEvents, amplitude._unsentEvents); + assert.deepEqual(amplitude2._unsentEvents, amplitude.getInstance()._unsentEvents); }); it('should not save events', function() { - amplitude.init(apiKey, null, {saveEvents: false}); - amplitude.logEvent('Event', {index: 1}); - amplitude.logEvent('Event', {index: 2}); - amplitude.logEvent('Event', {index: 3}); + amplitude.getInstance().init(apiKey, null, {saveEvents: false}); + amplitude.getInstance().logEvent('Event', {index: 1}); + amplitude.getInstance().logEvent('Event', {index: 2}); + amplitude.getInstance().logEvent('Event', {index: 3}); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); @@ -1026,15 +1026,15 @@ describe('setVersionName', function() { }); it('should limit events sent', function() { - amplitude.init(apiKey, null, {uploadBatchSize: 10}); + amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; - amplitude.logEvent('Event', {index: 100}); + amplitude.getInstance().logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1054,14 +1054,14 @@ describe('setVersionName', function() { it('should batch events sent', function() { var eventUploadPeriodMillis = 10*1000; - amplitude.init(apiKey, null, { + amplitude.getInstance().init(apiKey, null, { batchEvents: true, eventUploadThreshold: 10, eventUploadPeriodMillis: eventUploadPeriodMillis }); for (var i = 0; i < 15; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } assert.lengthOf(server.requests, 1); @@ -1074,7 +1074,7 @@ describe('setVersionName', function() { server.respond(); assert.lengthOf(server.requests, 1); - var unsentEvents = amplitude._unsentEvents; + var unsentEvents = amplitude.getInstance()._unsentEvents; assert.lengthOf(unsentEvents, 5); assert.deepEqual(unsentEvents[4].event_properties, {index: 14}); @@ -1083,7 +1083,7 @@ describe('setVersionName', function() { assert.lengthOf(server.requests, 2); server.respondWith('success'); server.respond(); - assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); assert.lengthOf(events, 5); assert.deepEqual(events[4].event_properties, {index: 14}); @@ -1091,15 +1091,15 @@ describe('setVersionName', function() { it('should send events after a delay', function() { var eventUploadPeriodMillis = 10*1000; - amplitude.init(apiKey, null, { + amplitude.getInstance().init(apiKey, null, { batchEvents: true, eventUploadThreshold: 2, eventUploadPeriodMillis: eventUploadPeriodMillis }); - amplitude.logEvent('Event'); + amplitude.getInstance().logEvent('Event'); // saveEvent should not have been called yet - assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); assert.lengthOf(server.requests, 0); // saveEvent should be called after delay @@ -1114,16 +1114,16 @@ describe('setVersionName', function() { it('should not send events after a delay if no events to send', function() { var eventUploadPeriodMillis = 10*1000; - amplitude.init(apiKey, null, { + amplitude.getInstance().init(apiKey, null, { batchEvents: true, eventUploadThreshold: 2, eventUploadPeriodMillis: eventUploadPeriodMillis }); - amplitude.logEvent('Event1'); - amplitude.logEvent('Event2'); + amplitude.getInstance().logEvent('Event1'); + amplitude.getInstance().logEvent('Event2'); // saveEvent triggered by 2 event batch threshold - assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); assert.lengthOf(server.requests, 1); server.respondWith('success'); server.respond(); @@ -1132,24 +1132,24 @@ describe('setVersionName', function() { assert.deepEqual(events[1].event_type, 'Event2'); // saveEvent should be called after delay, but no request made - assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); clock.tick(eventUploadPeriodMillis); assert.lengthOf(server.requests, 1); }); it('should not schedule more than one upload', function() { var eventUploadPeriodMillis = 5*1000; // 5s - amplitude.init(apiKey, null, { + amplitude.getInstance().init(apiKey, null, { batchEvents: true, eventUploadThreshold: 30, eventUploadPeriodMillis: eventUploadPeriodMillis }); // log 2 events, 1 millisecond apart, second event should not schedule upload - amplitude.logEvent('Event1'); + amplitude.getInstance().logEvent('Event1'); clock.tick(1); - amplitude.logEvent('Event2'); - assert.lengthOf(amplitude._unsentEvents, 2); + amplitude.getInstance().logEvent('Event2'); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); assert.lengthOf(server.requests, 0); // advance to upload period millis, and should have 1 server request @@ -1160,7 +1160,7 @@ describe('setVersionName', function() { server.respond(); // log 3rd event, advance 1 more millisecond, verify no 2nd server request - amplitude.logEvent('Event3'); + amplitude.getInstance().logEvent('Event3'); clock.tick(1); assert.lengthOf(server.requests, 1); @@ -1172,15 +1172,15 @@ describe('setVersionName', function() { }); it('should back off on 413 status', function() { - amplitude.init(apiKey, null, {uploadBatchSize: 10}); + amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; - amplitude.logEvent('Event', {index: 100}); + amplitude.getInstance().logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1199,14 +1199,14 @@ describe('setVersionName', function() { }); it('should back off on 413 status all the way to 1 event with drops', function() { - amplitude.init(apiKey, null, {uploadBatchSize: 9}); + amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 9}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 10; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } - amplitude._sending = false; - amplitude.logEvent('Event', {index: 100}); + amplitude.getInstance()._sending = false; + amplitude.getInstance().logEvent('Event', {index: 100}); for (var i = 0; i < 6; i++) { assert.lengthOf(server.requests, i+1); @@ -1228,14 +1228,14 @@ describe('setVersionName', function() { value = status; message = response; } - amplitude.logEvent(null, null, callback); + amplitude.getInstance().logEvent(null, null, callback); assert.equal(counter, 1); assert.equal(value, 0); assert.equal(message, 'No request sent'); }); it ('should run callback if optout', function () { - amplitude.setOptOut(true); + amplitude.getInstance().setOptOut(true); var counter = 0; var value = -1; var message = ''; @@ -1244,14 +1244,14 @@ describe('setVersionName', function() { value = status; message = response; }; - amplitude.logEvent('test', null, callback); + amplitude.getInstance().logEvent('test', null, callback); assert.equal(counter, 1); assert.equal(value, 0); assert.equal(message, 'No request sent'); }); it ('should not run callback if invalid callback and no eventType', function () { - amplitude.logEvent(null, null, 'invalid callback'); + amplitude.getInstance().logEvent(null, null, 'invalid callback'); }); it ('should run callback after logging event', function () { @@ -1263,7 +1263,7 @@ describe('setVersionName', function() { value = status; message = response; }; - amplitude.logEvent('test', null, callback); + amplitude.getInstance().logEvent('test', null, callback); // before server responds, callback should not fire assert.lengthOf(server.requests, 1); @@ -1281,7 +1281,7 @@ describe('setVersionName', function() { it ('should run callback if batchEvents but under threshold', function () { var eventUploadPeriodMillis = 5*1000; - amplitude.init(apiKey, null, { + amplitude.getInstance().init(apiKey, null, { batchEvents: true, eventUploadThreshold: 2, eventUploadPeriodMillis: eventUploadPeriodMillis @@ -1294,7 +1294,7 @@ describe('setVersionName', function() { value = status; message = response; }; - amplitude.logEvent('test', null, callback); + amplitude.getInstance().logEvent('test', null, callback); assert.lengthOf(server.requests, 0); assert.equal(counter, 1); assert.equal(value, 0); @@ -1309,7 +1309,7 @@ describe('setVersionName', function() { }); it ('should run callback once and only after all events are uploaded', function () { - amplitude.init(apiKey, null, {uploadBatchSize: 10}); + amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 10}); var counter = 0; var value = -1; var message = ''; @@ -1320,13 +1320,13 @@ describe('setVersionName', function() { }; // queue up 15 events, since batchsize 10, need to send in 2 batches - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; - amplitude.logEvent('Event', {index: 100}, callback); + amplitude.getInstance().logEvent('Event', {index: 100}, callback); assert.lengthOf(server.requests, 1); server.respondWith('success'); @@ -1358,14 +1358,14 @@ describe('setVersionName', function() { }; // queue up 15 events - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.logEvent('Event', {index: i}); + amplitude.getInstance().logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; // 16th event with 413 will backoff to batches of 8 - amplitude.logEvent('Event', {index: 100}, callback); + amplitude.getInstance().logEvent('Event', {index: 100}, callback); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1409,7 +1409,7 @@ describe('setVersionName', function() { message = response; }; - amplitude.logEvent('test', null, callback); + amplitude.getInstance().logEvent('test', null, callback); server.respondWith([404, {}, 'Not found']); server.respond(); assert.equal(counter, 1); @@ -1418,19 +1418,19 @@ describe('setVersionName', function() { }); it('should send 3 identify events', function() { - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude._unsentCount(), 0); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.identify(new Identify().add('photoCount', 1)); - amplitude.identify(new Identify().add('photoCount', 1).set('country', 'USA')); - amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().identify(new Identify().add('photoCount', 1).set('country', 'USA')); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); // verify some internal counters - assert.equal(amplitude._eventId, 0); - assert.equal(amplitude._identifyId, 3); - assert.equal(amplitude._unsentCount(), 3); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 3); + assert.equal(amplitude.getInstance()._eventId, 0); + assert.equal(amplitude.getInstance()._identifyId, 3); + assert.equal(amplitude.getInstance()._unsentCount(), 3); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 3); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1446,24 +1446,24 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should send 3 events', function() { - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude._unsentCount(), 0); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.logEvent('test'); - amplitude.logEvent('test'); - amplitude.logEvent('test'); + amplitude.getInstance().logEvent('test'); + amplitude.getInstance().logEvent('test'); + amplitude.getInstance().logEvent('test'); // verify some internal counters - assert.equal(amplitude._eventId, 3); - assert.equal(amplitude._identifyId, 0); - assert.equal(amplitude._unsentCount(), 3); - assert.lengthOf(amplitude._unsentEvents, 3); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._eventId, 3); + assert.equal(amplitude.getInstance()._identifyId, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 3); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 3); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1477,23 +1477,23 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); }); it('should send 1 event and 1 identify event', function() { - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); - assert.equal(amplitude._unsentCount(), 0); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.logEvent('test'); - amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().logEvent('test'); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); // verify some internal counters - assert.equal(amplitude._eventId, 1); - assert.equal(amplitude._identifyId, 1); - assert.equal(amplitude._unsentCount(), 2); - assert.lengthOf(amplitude._unsentEvents, 1); - assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._eventId, 1); + assert.equal(amplitude.getInstance()._identifyId, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 2); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1513,32 +1513,32 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should properly coalesce events and identify events into a request', function() { - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); - assert.equal(amplitude._unsentCount(), 0); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); + assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.logEvent('test1'); + amplitude.getInstance().logEvent('test1'); clock.tick(1); - amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); clock.tick(1); - amplitude.logEvent('test2'); + amplitude.getInstance().logEvent('test2'); clock.tick(1); - amplitude.logEvent('test3'); + amplitude.getInstance().logEvent('test3'); clock.tick(1); - amplitude.logEvent('test4'); - amplitude.identify(new Identify().add('photoCount', 2)); + amplitude.getInstance().logEvent('test4'); + amplitude.getInstance().identify(new Identify().add('photoCount', 2)); // verify some internal counters - assert.equal(amplitude._eventId, 4); - assert.equal(amplitude._identifyId, 2); - assert.equal(amplitude._unsentCount(), 6); - assert.lengthOf(amplitude._unsentEvents, 4); - assert.lengthOf(amplitude._unsentIdentifys, 2); + assert.equal(amplitude.getInstance()._eventId, 4); + assert.equal(amplitude.getInstance()._identifyId, 2); + assert.equal(amplitude.getInstance()._unsentCount(), 6); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 4); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 2); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1569,28 +1569,28 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should merged events supporting backwards compatability', function() { // events logged before v2.5.0 won't have sequence number, should get priority - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude._unsentCount(), 0); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.identify(new Identify().add('photoCount', 1)); - amplitude.logEvent('test'); - delete amplitude._unsentEvents[0].sequence_number; // delete sequence number to simulate old event - amplitude._sequenceNumber = 1; // reset sequence number - amplitude.identify(new Identify().add('photoCount', 2)); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().logEvent('test'); + delete amplitude.getInstance()._unsentEvents[0].sequence_number; // delete sequence number to simulate old event + amplitude.getInstance()._sequenceNumber = 1; // reset sequence number + amplitude.getInstance().identify(new Identify().add('photoCount', 2)); // verify some internal counters - assert.equal(amplitude._eventId, 1); - assert.equal(amplitude._identifyId, 2); - assert.equal(amplitude._unsentCount(), 3); - assert.lengthOf(amplitude._unsentEvents, 1); - assert.lengthOf(amplitude._unsentIdentifys, 2); + assert.equal(amplitude.getInstance()._eventId, 1); + assert.equal(amplitude.getInstance()._identifyId, 2); + assert.equal(amplitude.getInstance()._unsentCount(), 3); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 2); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1617,32 +1617,32 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should drop event and keep identify on 413 response', function() { - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); - amplitude.logEvent('test'); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.getInstance().logEvent('test'); clock.tick(1); - amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 1); server.respondWith([413, {}, '']); server.respond(); // backoff and retry - assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 2); server.respondWith([413, {}, '']); server.respond(); // after dropping massive event, only 1 event left - assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 3); var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); @@ -1653,26 +1653,26 @@ describe('setVersionName', function() { }); it('should drop identify if 413 and uploadBatchSize is 1', function() { - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); - amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.getInstance().identify(new Identify().add('photoCount', 1)); clock.tick(1); - amplitude.logEvent('test'); + amplitude.getInstance().logEvent('test'); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 1); server.respondWith([413, {}, '']); server.respond(); // backoff and retry - assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 2); server.respondWith([413, {}, '']); server.respond(); // after dropping massive event, only 1 event left - assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 3); var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); @@ -1683,7 +1683,7 @@ describe('setVersionName', function() { it('should truncate long event property strings', function() { var longString = new Array(5000).join('a'); - amplitude.logEvent('test', {'key': longString}); + amplitude.getInstance().logEvent('test', {'key': longString}); var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; assert.isTrue('key' in event.event_properties); @@ -1692,7 +1692,7 @@ describe('setVersionName', function() { it('should truncate long user property strings', function() { var longString = new Array(5000).join('a'); - amplitude.identify(new Identify().set('key', longString)); + amplitude.getInstance().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); @@ -1730,17 +1730,17 @@ describe('setVersionName', function() { it('should validate event properties', function() { var e = new Error('oops'); clock.tick(1); - amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); clock.tick(1); - amplitude.logEvent('String event properties', '{}'); + amplitude.getInstance().logEvent('String event properties', '{}'); clock.tick(1); - amplitude.logEvent('Bool event properties', true); + amplitude.getInstance().logEvent('Bool event properties', true); clock.tick(1); - amplitude.logEvent('Number event properties', 15); + amplitude.getInstance().logEvent('Number event properties', 15); clock.tick(1); - amplitude.logEvent('Array event properties', [1, 2, 3]); + amplitude.getInstance().logEvent('Array event properties', [1, 2, 3]); clock.tick(1); - amplitude.logEvent('Object event properties', { + amplitude.getInstance().logEvent('Object event properties', { 10: 'false', // coerce key 'bool': true, 'null': null, // should be ignored @@ -1755,7 +1755,7 @@ describe('setVersionName', function() { }); clock.tick(1); - assert.lengthOf(amplitude._unsentEvents, 5); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 5); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 5); @@ -1778,10 +1778,10 @@ describe('setVersionName', function() { it('should validate user propeorties', function() { var identify = new Identify().set(10, 10); - amplitude.init(apiKey, null, {batchEvents: true}); - amplitude.identify(identify); + amplitude.getInstance().init(apiKey, null, {batchEvents: true}); + amplitude.getInstance().identify(identify); - assert.deepEqual(amplitude._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); + assert.deepEqual(amplitude.getInstance()._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); }); it('should synchronize event data across multiple amplitude instances that share the same cookie', function() { @@ -1830,7 +1830,7 @@ describe('setVersionName', function() { 'null': null, // ignore null values } - amplitude.logEventWithGroups('Test', eventProperties, groups, callback); + amplitude.getInstance().logEventWithGroups('Test', eventProperties, groups, callback); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -1860,7 +1860,7 @@ describe('setVersionName', function() { describe('optOut', function() { beforeEach(function() { - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -1868,28 +1868,28 @@ describe('setVersionName', function() { }); it('should not send events while enabled', function() { - amplitude.setOptOut(true); - amplitude.logEvent('Event Type 1'); + amplitude.getInstance().setOptOut(true); + amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 0); }); it('should not send saved events while enabled', function() { - amplitude.logEvent('Event Type 1'); + amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); - amplitude._sending = false; - amplitude.setOptOut(true); - amplitude.init(apiKey); + amplitude.getInstance()._sending = false; + amplitude.getInstance().setOptOut(true); + amplitude.getInstance().init(apiKey); assert.lengthOf(server.requests, 1); }); it('should start sending events again when disabled', function() { - amplitude.setOptOut(true); - amplitude.logEvent('Event Type 1'); + amplitude.getInstance().setOptOut(true); + amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 0); - amplitude.setOptOut(false); - amplitude.logEvent('Event Type 1'); + amplitude.getInstance().setOptOut(false); + amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1898,10 +1898,10 @@ describe('setVersionName', function() { it('should have state be persisted in the cookie', function() { var amplitude = new Amplitude(); - amplitude.init(apiKey); - assert.strictEqual(amplitude.options.optOut, false); + amplitude.getInstance().init(apiKey); + assert.strictEqual(amplitude.getInstance().options.optOut, false); - amplitude.setOptOut(true); + amplitude.getInstance().setOptOut(true); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); @@ -1909,15 +1909,15 @@ describe('setVersionName', function() { }); it('should limit identify events queued', function() { - amplitude.init(apiKey, null, {savedMaxCount: 10}); + amplitude.getInstance().init(apiKey, null, {savedMaxCount: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.identify(new Identify().add('test', i)); + amplitude.getInstance().identify(new Identify().add('test', i)); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; - amplitude.identify(new Identify().add('test', 100)); + amplitude.getInstance().identify(new Identify().add('test', 100)); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 10); @@ -1928,7 +1928,7 @@ describe('setVersionName', function() { describe('gatherUtm', function() { beforeEach(function() { - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -1938,9 +1938,9 @@ describe('setVersionName', function() { it('should not send utm data when the includeUtm flag is false', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); reset(); - amplitude.init(apiKey, undefined, {}); + amplitude.getInstance().init(apiKey, undefined, {}); - amplitude.setUserProperties({user_prop: true}); + amplitude.getInstance().setUserProperties({user_prop: true}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events[0].user_properties.utm_campaign, undefined); @@ -1953,9 +1953,9 @@ describe('setVersionName', 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, batchEvents: true, eventUploadThreshold: 2}); + amplitude.getInstance().init(apiKey, undefined, {includeUtm: true, batchEvents: true, eventUploadThreshold: 2}); - amplitude.logEvent('UTM Test Event', {}); + amplitude.getInstance().logEvent('UTM Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1979,7 +1979,7 @@ describe('setVersionName', 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.getInstance()._initUtmData(utmParams); var expectedProperties = { utm_campaign: 'new', @@ -2005,7 +2005,7 @@ describe('setVersionName', function() { server.respondWith('success'); server.respond(); - amplitude.logEvent('UTM Test Event', {}); + amplitude.getInstance().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, {}); @@ -2027,7 +2027,7 @@ describe('setVersionName', 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.getInstance()._initUtmData(utmParams); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -2052,19 +2052,19 @@ describe('setVersionName', function() { describe('gatherReferrer', function() { beforeEach(function() { - amplitude.init(apiKey); - sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.com/contact'); + amplitude.getInstance().init(apiKey); + sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.getInstance().com/contact'); }); afterEach(function() { - amplitude._getReferrer.restore(); + amplitude.getInstance()._getReferrer.restore(); reset(); }); it('should not send referrer data when the includeReferrer flag is false', function() { - amplitude.init(apiKey, undefined, {}); + amplitude.getInstance().init(apiKey, undefined, {}); - amplitude.setUserProperties({user_prop: true}); + amplitude.getInstance().setUserProperties({user_prop: true}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events[0].user_properties.referrer, undefined); @@ -2073,15 +2073,15 @@ describe('setVersionName', function() { it('should only send referrer via identify call when the includeReferrer flag is true', function() { reset(); - amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); - amplitude.logEvent('Referrer Test Event', {}); + amplitude.getInstance().init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.getInstance().logEvent('Referrer Test Event', {}); assert.lengthOf(server.requests, 1); 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' + 'referrer': 'https://amplitude.getInstance().com/contact', + 'referring_domain': 'amplitude.getInstance().com' }; // first event should be identify with initial_referrer and referrer @@ -2089,8 +2089,8 @@ describe('setVersionName', function() { assert.deepEqual(events[0].user_properties, { '$set': expected, '$setOnce': { - 'initial_referrer': 'https://amplitude.com/contact', - 'initial_referring_domain': 'amplitude.com' + 'initial_referrer': 'https://amplitude.getInstance().com/contact', + 'initial_referring_domain': 'amplitude.getInstance().com' } }); @@ -2105,8 +2105,8 @@ describe('setVersionName', function() { it('should not set referrer if referrer data already in session storage', function() { reset(); sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); - amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); - amplitude.logEvent('Referrer Test Event', {}); + amplitude.getInstance().init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.getInstance().logEvent('Referrer Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 2); @@ -2115,8 +2115,8 @@ describe('setVersionName', function() { assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { '$setOnce': { - 'initial_referrer': 'https://amplitude.com/contact', - 'initial_referring_domain': 'amplitude.com' + 'initial_referrer': 'https://amplitude.getInstance().com/contact', + 'initial_referring_domain': 'amplitude.getInstance().com' } }); @@ -2128,9 +2128,9 @@ describe('setVersionName', function() { it('should not override any existing initial referrer values in session storage', function() { reset(); sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); - amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); - amplitude._saveReferrer('https://facebook.com/contact'); - amplitude.logEvent('Referrer Test Event', {}); + amplitude.getInstance().init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); + amplitude.getInstance()._saveReferrer('https://facebook.com/contact'); + amplitude.getInstance().logEvent('Referrer Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 3); @@ -2139,8 +2139,8 @@ describe('setVersionName', function() { assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { '$setOnce': { - 'initial_referrer': 'https://amplitude.com/contact', - 'initial_referring_domain': 'amplitude.com' + 'initial_referrer': 'https://amplitude.getInstance().com/contact', + 'initial_referring_domain': 'amplitude.getInstance().com' } }); @@ -2164,7 +2164,7 @@ describe('setVersionName', function() { describe('logRevenue', function() { beforeEach(function() { - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -2183,7 +2183,7 @@ describe('setVersionName', function() { } it('should log simple amount', function() { - amplitude.logRevenue(10.10); + amplitude.getInstance().logRevenue(10.10); revenueEqual({ special: 'revenue_amount', price: 10.10, @@ -2192,7 +2192,7 @@ describe('setVersionName', function() { }); it('should log complex amount', function() { - amplitude.logRevenue(10.10, 7); + amplitude.getInstance().logRevenue(10.10, 7); revenueEqual({ special: 'revenue_amount', price: 10.10, @@ -2201,17 +2201,17 @@ describe('setVersionName', function() { }); it('shouldn\'t log invalid price', function() { - amplitude.logRevenue('kitten', 7); + amplitude.getInstance().logRevenue('kitten', 7); assert.lengthOf(server.requests, 0); }); it('shouldn\'t log invalid quantity', function() { - amplitude.logRevenue(10.00, 'puppy'); + amplitude.getInstance().logRevenue(10.00, 'puppy'); assert.lengthOf(server.requests, 0); }); it('should log complex amount with product id', function() { - amplitude.logRevenue(10.10, 7, 'chicken.dinner'); + amplitude.getInstance().logRevenue(10.10, 7, 'chicken.dinner'); revenueEqual({ special: 'revenue_amount', price: 10.10, @@ -2224,7 +2224,7 @@ describe('setVersionName', function() { describe('logRevenueV2', function() { beforeEach(function() { reset(); - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -2233,11 +2233,11 @@ describe('setVersionName', function() { it('should log with the Revenue object', function () { // ignore invalid revenue objects - amplitude.logRevenueV2(null); + amplitude.getInstance().logRevenueV2(null); assert.lengthOf(server.requests, 0); - amplitude.logRevenueV2({}); + amplitude.getInstance().logRevenueV2({}); assert.lengthOf(server.requests, 0); - amplitude.logRevenueV2(new amplitude.Revenue()); + amplitude.getInstance().logRevenueV2(new amplitude.getInstance().Revenue()); // log valid revenue object var productId = 'testProductId'; @@ -2246,10 +2246,10 @@ describe('setVersionName', function() { var revenueType = 'testRevenueType' var properties = {'city': 'San Francisco'}; - var revenue = new amplitude.Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); + var revenue = new amplitude.getInstance().Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); revenue.setRevenueType(revenueType).setEventProperties(properties); - amplitude.logRevenueV2(revenue); + amplitude.getInstance().logRevenueV2(revenue); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); @@ -2277,7 +2277,7 @@ describe('setVersionName', function() { ['setQuantity', 10], ['setPrice', 'key1'] // invalid price type, this will fail to generate revenue event ]}; - amplitude.logRevenueV2(fakeRevenue); + amplitude.getInstance().logRevenueV2(fakeRevenue); assert.lengthOf(server.requests, 0); var proxyRevenue = {'_q':[ @@ -2286,7 +2286,7 @@ describe('setVersionName', function() { ['setPrice', 10.99], ['setRevenueType', 'purchase'] ]}; - amplitude.logRevenueV2(proxyRevenue); + amplitude.getInstance().logRevenueV2(proxyRevenue); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); var event = events[0]; @@ -2305,7 +2305,7 @@ describe('setVersionName', function() { var clock; beforeEach(function() { clock = sinon.useFakeTimers(); - amplitude.init(apiKey); + amplitude.getInstance().init(apiKey); }); afterEach(function() { @@ -2314,15 +2314,15 @@ describe('setVersionName', function() { }); it('should create new session IDs on timeout', function() { - var sessionId = amplitude._sessionId; + var sessionId = amplitude.getInstance()._sessionId; clock.tick(30 * 60 * 1000 + 1); - amplitude.logEvent('Event Type 1'); + amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); assert.notEqual(events[0].session_id, sessionId); - assert.notEqual(amplitude._sessionId, sessionId); - assert.equal(events[0].session_id, amplitude._sessionId); + assert.notEqual(amplitude.getInstance()._sessionId, sessionId); + assert.equal(events[0].session_id, amplitude.getInstance()._sessionId); }); it('should be fetched correctly by getSessionId', function() { From aa03c3525fcf776efd4837c418be1c175781c763 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Fri, 20 May 2016 23:35:10 -0700 Subject: [PATCH 05/13] fix amplitude tests, pass instance to init callback --- amplitude.js | 2 +- amplitude.min.js | 2 +- src/amplitude-client.js | 2 +- test/amplitude-client.js | 2338 ++++++++++++++++++++++++++++++++++++++ test/amplitude.js | 102 +- 5 files changed, 2378 insertions(+), 68 deletions(-) create mode 100644 test/amplitude-client.js diff --git a/amplitude.js b/amplitude.js index ba499f6a..dd71cf13 100644 --- a/amplitude.js +++ b/amplitude.js @@ -598,7 +598,7 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o utils.log(e); } finally { if (type(opt_callback) === 'function') { - opt_callback(); + opt_callback(this); } } }; diff --git a/amplitude.min.js b/amplitude.min.js index da53d7a4..af0f4572 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key)};AmplitudeClient.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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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){} +(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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key)};AmplitudeClient.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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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},{}],10:[function(require,module,exports){module.exports="2.12.1"},{}],11:[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":29}],29:[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/src/amplitude-client.js b/src/amplitude-client.js index a268877f..5486f382 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -120,7 +120,7 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o utils.log(e); } finally { if (type(opt_callback) === 'function') { - opt_callback(); + opt_callback(this); } } }; diff --git a/test/amplitude-client.js b/test/amplitude-client.js new file mode 100644 index 00000000..375d4749 --- /dev/null +++ b/test/amplitude-client.js @@ -0,0 +1,2338 @@ +// maintain for testing backwards compatability +describe('AmplitudeClient', function() { + var AmplitudeClient = require('../src/amplitude-client.js'); + var getUtmData = require('../src/utm.js'); + var localStorage = require('../src/localstorage.js'); + var CookieStorage = require('../src/cookiestorage.js'); + var Base64 = require('../src/base64.js'); + var cookie = require('../src/cookie.js'); + var utils = require('../src/utils.js'); + var querystring = require('querystring'); + var JSON = require('json'); + var Identify = require('../src/identify.js'); + var Revenue = require('../src/revenue.js'); + var apiKey = '000000'; + var keySuffix = '_' + apiKey.slice(0,6); + var userId = 'user'; + var amplitude; + var server; + + beforeEach(function() { + amplitude = new Amplitude(); + server = sinon.fakeServer.create(); + }); + + afterEach(function() { + server.restore(); + }); + + it('amplitude object should exist', function() { + assert.isObject(amplitude); + }); + + function reset() { + localStorage.clear(); + sessionStorage.clear(); + cookie.remove(amplitude.options.cookieName); + cookie.reset(); + } + + describe('init', function() { + beforeEach(function() { + reset(); + }); + + afterEach(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, 37); // increase deviceId length by 1 for 'R' character + }); + + it('should set language', function() { + amplitude.init(apiKey, userId); + assert.property(amplitude.options, 'language'); + assert.isNotNull(amplitude.options.language); + }); + + it('should allow language override', function() { + amplitude.init(apiKey, userId, {language: 'en-GB'}); + assert.propertyVal(amplitude.options, 'language', 'en-GB'); + }); + + it ('should not run callback if invalid callback', function() { + amplitude.init(apiKey, userId, null, 'invalid callback'); + }); + + it ('should run valid callbacks', function() { + var counter = 0; + var callback = function() { + counter++; + }; + amplitude.init(apiKey, userId, null, callback); + assert.equal(counter, 1); + }); + + it ('should migrate deviceId, userId, optOut from localStorage to cookie', function() { + var deviceId = 'test_device_id'; + var userId = 'test_user_id'; + + assert.isNull(cookie.get(amplitude.options.cookieName)); + localStorage.setItem('amplitude_deviceId' + keySuffix, deviceId); + localStorage.setItem('amplitude_userId' + keySuffix, userId); + localStorage.setItem('amplitude_optOut' + keySuffix, true); + + amplitude.init(apiKey); + assert.equal(amplitude.options.deviceId, deviceId); + assert.equal(amplitude.options.userId, userId); + assert.isTrue(amplitude.options.optOut); + + var cookieData = cookie.get(amplitude.options.cookieName); + assert.equal(cookieData.deviceId, deviceId); + assert.equal(cookieData.userId, userId); + assert.isTrue(cookieData.optOut); + }); + + it('should migrate session and event info from localStorage to cookie', function() { + var now = new Date().getTime(); + + assert.isNull(cookie.get(amplitude.options.cookieName)); + localStorage.setItem('amplitude_sessionId', now); + localStorage.setItem('amplitude_lastEventTime', now); + localStorage.setItem('amplitude_lastEventId', 3000); + localStorage.setItem('amplitude_lastIdentifyId', 4000); + localStorage.setItem('amplitude_lastSequenceNumber', 5000); + + amplitude.init(apiKey); + + assert.equal(amplitude._sessionId, now); + assert.isTrue(amplitude._lastEventTime >= now); + assert.equal(amplitude._eventId, 3000); + assert.equal(amplitude._identifyId, 4000); + assert.equal(amplitude._sequenceNumber, 5000); + + var cookieData = cookie.get(amplitude.options.cookieName); + assert.equal(cookieData.sessionId, now); + assert.equal(cookieData.lastEventTime, amplitude._lastEventTime); + assert.equal(cookieData.eventId, 3000); + assert.equal(cookieData.identifyId, 4000); + assert.equal(cookieData.sequenceNumber, 5000); + }); + + it('should migrate cookie data from old cookie name and ignore local storage values', function(){ + var now = new Date().getTime(); + + // deviceId and sequenceNumber not set, init should load value from localStorage + var cookieData = { + userId: 'test_user_id', + optOut: false, + sessionId: now, + lastEventTime: now, + eventId: 50, + identifyId: 60 + } + + cookie.set(amplitude.options.cookieName, cookieData); + localStorage.setItem('amplitude_deviceId' + keySuffix, 'old_device_id'); + localStorage.setItem('amplitude_userId' + keySuffix, 'fake_user_id'); + localStorage.setItem('amplitude_optOut' + keySuffix, true); + localStorage.setItem('amplitude_sessionId', now-1000); + localStorage.setItem('amplitude_lastEventTime', now-1000); + localStorage.setItem('amplitude_lastEventId', 20); + localStorage.setItem('amplitude_lastIdentifyId', 30); + localStorage.setItem('amplitude_lastSequenceNumber', 40); + + amplitude.init(apiKey); + assert.equal(amplitude.options.deviceId, 'old_device_id'); + assert.equal(amplitude.options.userId, 'test_user_id'); + assert.isFalse(amplitude.options.optOut); + assert.equal(amplitude._sessionId, now); + assert.isTrue(amplitude._lastEventTime >= now); + assert.equal(amplitude._eventId, 50); + assert.equal(amplitude._identifyId, 60); + assert.equal(amplitude._sequenceNumber, 40); + }); + + it('should skip the migration if the new cookie already has deviceId, sessionId, lastEventTime', function() { + var now = new Date().getTime(); + + cookie.set(amplitude.options.cookieName, { + deviceId: 'new_device_id', + sessionId: now, + lastEventTime: now + }); + + localStorage.setItem('amplitude_deviceId' + keySuffix, 'fake_device_id'); + localStorage.setItem('amplitude_userId' + keySuffix, 'fake_user_id'); + localStorage.setItem('amplitude_optOut' + keySuffix, true); + localStorage.setItem('amplitude_sessionId', now-1000); + localStorage.setItem('amplitude_lastEventTime', now-1000); + localStorage.setItem('amplitude_lastEventId', 20); + localStorage.setItem('amplitude_lastIdentifyId', 30); + localStorage.setItem('amplitude_lastSequenceNumber', 40); + + amplitude.init(apiKey, 'new_user_id'); + assert.equal(amplitude.options.deviceId, 'new_device_id'); + assert.equal(amplitude.options.userId, 'new_user_id'); + assert.isFalse(amplitude.options.optOut); + assert.isTrue(amplitude._sessionId >= now); + assert.isTrue(amplitude._lastEventTime >= now); + assert.equal(amplitude._eventId, 0); + assert.equal(amplitude._identifyId, 0); + assert.equal(amplitude._sequenceNumber, 0); + }); + + it('should save cookie data to localStorage if cookies are not enabled', function() { + var cookieStorageKey = 'amp_cookiestore_amplitude_id'; + var deviceId = 'test_device_id'; + var clock = sinon.useFakeTimers(); + clock.tick(1000); + + localStorage.clear(); + sinon.stub(CookieStorage.prototype, '_cookiesEnabled').returns(false); + var amplitude2 = new Amplitude(); + CookieStorage.prototype._cookiesEnabled.restore(); + amplitude2.init(apiKey, userId, {'deviceId': deviceId}); + clock.restore(); + + var cookieData = JSON.parse(localStorage.getItem(cookieStorageKey)); + assert.deepEqual(cookieData, { + 'deviceId': deviceId, + 'userId': userId, + 'optOut': false, + 'sessionId': 1000, + 'lastEventTime': 1000, + 'eventId': 0, + 'identifyId': 0, + 'sequenceNumber': 0 + }); + + assert.isNull(cookie.get(amplitude2.options.cookieName)); // assert did not write to cookies + }); + + it('should load sessionId, eventId from cookie and ignore the one in localStorage', function() { + var sessionIdKey = 'amplitude_sessionId'; + var lastEventTimeKey = 'amplitude_lastEventTime'; + var eventIdKey = 'amplitude_lastEventId'; + var identifyIdKey = 'amplitude_lastIdentifyId'; + var sequenceNumberKey = 'amplitude_lastSequenceNumber'; + var amplitude2 = new Amplitude(); + + var clock = sinon.useFakeTimers(); + clock.tick(1000); + var sessionId = new Date().getTime(); + + // the following values in localStorage will all be ignored + localStorage.clear(); + localStorage.setItem(sessionIdKey, 3); + localStorage.setItem(lastEventTimeKey, 4); + localStorage.setItem(eventIdKey, 5); + localStorage.setItem(identifyIdKey, 6); + localStorage.setItem(sequenceNumberKey, 7); + + var cookieData = { + deviceId: 'test_device_id', + userId: 'test_user_id', + optOut: true, + sessionId: sessionId, + lastEventTime: sessionId, + eventId: 50, + identifyId: 60, + sequenceNumber: 70 + } + cookie.set(amplitude2.options.cookieName, cookieData); + + clock.tick(10); + amplitude2.init(apiKey); + clock.restore(); + + assert.equal(amplitude2._sessionId, sessionId); + assert.equal(amplitude2._lastEventTime, sessionId + 10); + assert.equal(amplitude2._eventId, 50); + assert.equal(amplitude2._identifyId, 60); + assert.equal(amplitude2._sequenceNumber, 70); + }); + + it('should load sessionId from localStorage if not in cookie', function() { + var sessionIdKey = 'amplitude_sessionId'; + var lastEventTimeKey = 'amplitude_lastEventTime'; + var eventIdKey = 'amplitude_lastEventId'; + var identifyIdKey = 'amplitude_lastIdentifyId'; + var sequenceNumberKey = 'amplitude_lastSequenceNumber'; + var amplitude2 = new Amplitude(); + + var cookieData = { + deviceId: 'test_device_id', + userId: userId, + optOut: true + } + cookie.set(amplitude2.options.cookieName, cookieData); + + var clock = sinon.useFakeTimers(); + clock.tick(1000); + var sessionId = new Date().getTime(); + + localStorage.clear(); + localStorage.setItem(sessionIdKey, sessionId); + localStorage.setItem(lastEventTimeKey, sessionId); + localStorage.setItem(eventIdKey, 50); + localStorage.setItem(identifyIdKey, 60); + localStorage.setItem(sequenceNumberKey, 70); + + clock.tick(10); + amplitude2.init(apiKey, userId); + clock.restore(); + + assert.equal(amplitude2._sessionId, sessionId); + assert.equal(amplitude2._lastEventTime, sessionId + 10); + assert.equal(amplitude2._eventId, 50); + assert.equal(amplitude2._identifyId, 60); + assert.equal(amplitude2._sequenceNumber, 70); + }); + + it('should load saved events from localStorage', 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"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130, "groups":{}}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131, "groups":{}}]'; + localStorage.setItem('amplitude_unsent', existingEvent); + localStorage.setItem('amplitude_unsent_identify', existingIdentify); + + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey, null, {batchEvents: true}); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, JSON.parse(existingEvent)); + assert.deepEqual(amplitude2._unsentIdentifys, JSON.parse(existingIdentify)); + + // check local storage keys are still same for default instance + assert.equal(localStorage.getItem('amplitude_unsent'), existingEvent); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), existingIdentify); + }); + + it('should validate event properties when loading saved events from localStorage', function() { + var existingEvents = '[{"device_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d' + + '-2352191877e6","timestamp":1455744744413,"event_id":2,"session_id":1455744733865,"event_type":"clicked",' + + '"version_name":"Web","platform":"Web","os_name":"Chrome","os_version":"48","device_model":"Mac","language"' + + ':"en-US","api_properties":{},"event_properties":"{}","user_properties":{},"uuid":"1b8859d9-e91e-403e-92d4-' + + 'c600dfb83432","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":4},{"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":"clicked","version_name":"Web","platform":"Web",' + + '"os_name":"Chrome","os_version":"48","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{"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"}]}}},"user_properties":{},"uuid":"650407a1-d705-' + + '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]' + localStorage.setItem('amplitude_unsent', 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._unsentEvents[0].event_properties, {}); + 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"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131}]'; + localStorage.setItem('amplitude_unsent', existingEvent); + localStorage.setItem('amplitude_unsent_identify', existingIdentify); + + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + server.respondWith('success'); + server.respond(); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2._unsentIdentifys, []); + + // check local storage keys are still same + assert.equal(localStorage.getItem('amplitude_unsent'), JSON.stringify([])); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), JSON.stringify([])); + + // check request + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + assert.equal(events[0].event_id, 49); + assert.equal(events[1].event_type, '$identify'); + }); + + it('should validate event properties when loading saved events from localStorage', function() { + var existingEvents = '[{"device_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d' + + '-2352191877e6","timestamp":1455744744413,"event_id":2,"session_id":1455744733865,"event_type":"clicked",' + + '"version_name":"Web","platform":"Web","os_name":"Chrome","os_version":"48","device_model":"Mac","language"' + + ':"en-US","api_properties":{},"event_properties":"{}","user_properties":{},"uuid":"1b8859d9-e91e-403e-92d4-' + + 'c600dfb83432","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":4},{"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":"clicked","version_name":"Web","platform":"Web",' + + '"os_name":"Chrome","os_version":"48","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{"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"}]}}},"user_properties":{},"uuid":"650407a1-d705-' + + '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]'; + localStorage.setItem('amplitude_unsent', 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._unsentEvents[0].event_properties, {}); + assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); + }); + }); + + describe('runQueuedFunctions', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should run queued functions', function() { + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(server.requests, 0); + var userId = 'testUserId' + var eventType = 'test_event' + var functions = [ + ['setUserId', userId], + ['logEvent', eventType] + ]; + amplitude._q = functions; + assert.lengthOf(amplitude._q, 2); + amplitude.runQueuedFunctions(); + + assert.equal(amplitude.options.userId, userId); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, eventType); + + assert.lengthOf(amplitude._q, 0); + }); + }); + + describe('setUserProperties', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should log identify call from set user properties', function() { + assert.equal(amplitude._unsentCount(), 0); + amplitude.setUserProperties({'prop': true, 'key': 'value'}); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + + var expected = { + '$set': { + 'prop': true, + 'key': 'value' + } + }; + assert.deepEqual(events[0].user_properties, expected); + }); + }); + + describe('clearUserProperties', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should log identify call from clear user properties', function() { + assert.equal(amplitude._unsentCount(), 0); + amplitude.clearUserProperties(); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + + var expected = { + '$clearAll': '-' + }; + assert.deepEqual(events[0].user_properties, expected); + }); + }); + + describe('setGroup', function() { + beforeEach(function() { + reset(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should generate an identify event with groups set', function() { + amplitude.setGroup('orgId', 15); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + + // verify identify event + var identify = events[0]; + assert.equal(identify.event_type, '$identify'); + assert.deepEqual(identify.user_properties, { + '$set': {'orgId': 15}, + }); + assert.deepEqual(identify.event_properties, {}); + assert.deepEqual(identify.groups, { + 'orgId': '15', + }); + }); + + it('should ignore empty string groupTypes', function() { + amplitude.setGroup('', 15); + assert.lengthOf(server.requests, 0); + }); + + it('should ignore non-string groupTypes', function() { + amplitude.setGroup(10, 10); + amplitude.setGroup([], 15); + amplitude.setGroup({}, 20); + amplitude.setGroup(true, false); + assert.lengthOf(server.requests, 0); + }); + }); + + +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('regenerateDeviceId', function() { + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should regenerate the deviceId', function() { + var deviceId = 'oldDeviceId'; + amplitude.init(apiKey, null, {'deviceId': deviceId}); + amplitude.regenerateDeviceId(); + assert.notEqual(amplitude.options.deviceId, deviceId); + assert.lengthOf(amplitude.options.deviceId, 37); + assert.equal(amplitude.options.deviceId[36], 'R'); + }); + }); + + describe('setDeviceId', function() { + + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should change device id', function() { + amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.setDeviceId('deviceId'); + assert.equal(amplitude.options.deviceId, 'deviceId'); + }); + + it('should not change device id if empty', function() { + amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.setDeviceId(''); + assert.notEqual(amplitude.options.deviceId, ''); + assert.equal(amplitude.options.deviceId, 'deviceId'); + }); + + it('should not change device id if null', function() { + amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.setDeviceId(null); + assert.notEqual(amplitude.options.deviceId, null); + assert.equal(amplitude.options.deviceId, 'deviceId'); + }); + + it('should store device id in cookie', function() { + amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.setDeviceId('deviceId'); + var stored = cookie.get(amplitude.options.cookieName); + assert.propertyVal(stored, 'deviceId', 'deviceId'); + }); + }); + + describe('identify', function() { + + beforeEach(function() { + clock = sinon.useFakeTimers(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + clock.restore(); + }); + + it('should ignore inputs that are not identify objects', function() { + amplitude.identify('This is a test'); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify(150); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify(['test']); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify({'user_prop': true}); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + }); + + it('should generate an event from the identify object', function() { + var identify = new Identify().set('prop1', 'value1').unset('prop2').add('prop3', 3).setOnce('prop4', true); + amplitude.identify(identify); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + assert.deepEqual(events[0].user_properties, { + '$set': { + 'prop1': 'value1' + }, + '$unset': { + 'prop2': '-' + }, + '$add': { + 'prop3': 3 + }, + '$setOnce': { + 'prop4': true + } + }); + }); + + it('should ignore empty identify objects', function() { + amplitude.identify(new Identify()); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + }); + + it('should ignore empty proxy identify objects', function() { + amplitude.identify({'_q': {}}); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify({}); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + }); + + it('should generate an event from a proxy identify object', function() { + var proxyObject = {'_q':[ + ['setOnce', 'key2', 'value4'], + ['unset', 'key1'], + ['add', 'key1', 'value1'], + ['set', 'key2', 'value3'], + ['set', 'key4', 'value5'], + ['prepend', 'key5', 'value6'] + ]}; + amplitude.identify(proxyObject); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + assert.deepEqual(events[0].user_properties, { + '$setOnce': {'key2': 'value4'}, + '$unset': {'key1': '-'}, + '$set': {'key4': 'value5'}, + '$prepend': {'key5': 'value6'} + }); + }); + + it('should run the callback after making the identify call', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + var identify = new amplitude.Identify().set('key', 'value'); + amplitude.identify(identify, callback); + + // before server responds, callback should not fire + assert.lengthOf(server.requests, 1); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after server response, fire callback + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it('should run the callback even if client not initialized with apiKey', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + var identify = new amplitude.Identify().set('key', 'value'); + new Amplitude().identify(identify, callback); + + // verify callback fired + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + + it('should run the callback even with an invalid identify object', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + amplitude.identify(null, callback); + + // verify callback fired + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + }); + + describe('logEvent', function() { + + var clock; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + clock.restore(); + }); + + it('should send request', function() { + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + assert.equal(server.requests[0].url, 'http://api.amplitude.com/'); + assert.equal(server.requests[0].method, 'POST'); + assert.equal(server.requests[0].async, true); + }); + + it('should reject empty event types', function() { + amplitude.logEvent(); + assert.lengthOf(server.requests, 0); + }); + + it('should send api key', function() { + amplitude.logEvent('Event Type 2'); + assert.lengthOf(server.requests, 1); + assert.equal(querystring.parse(server.requests[0].requestBody).client, apiKey); + }); + + it('should send api version', function() { + amplitude.logEvent('Event Type 3'); + assert.lengthOf(server.requests, 1); + assert.equal(querystring.parse(server.requests[0].requestBody).v, '2'); + }); + + it('should send event JSON', function() { + amplitude.logEvent('Event Type 4'); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + assert.equal(events[0].event_type, 'Event Type 4'); + }); + + it('should send language', function() { + amplitude.logEvent('Event Should Send Language'); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + assert.isNotNull(events[0].language); + }); + + it('should accept properties', function() { + amplitude.logEvent('Event Type 5', {prop: true}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.deepEqual(events[0].event_properties, {prop: true}); + }); + + it('should queue events', function() { + amplitude._sending = true; + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 4); + assert.deepEqual(events[0].event_properties, {index: 1}); + assert.deepEqual(events[3].event_properties, {index: 100}); + }); + + it('should limit events queued', function() { + amplitude.init(apiKey, null, {savedMaxCount: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 6}); + assert.deepEqual(events[9].event_properties, {index: 100}); + }); + + it('should remove only sent events', function() { + amplitude._sending = true; + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude._sending = false; + amplitude.logEvent('Event', {index: 3}); + + server.respondWith('success'); + server.respond(); + + amplitude.logEvent('Event', {index: 4}); + + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 1); + assert.deepEqual(events[0].event_properties, {index: 4}); + }); + + it('should save events', function() { + amplitude.init(apiKey, null, {saveEvents: true}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); + + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey); + assert.deepEqual(amplitude2._unsentEvents, amplitude._unsentEvents); + }); + + it('should not save events', function() { + amplitude.init(apiKey, null, {saveEvents: false}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); + + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey); + assert.deepEqual(amplitude2._unsentEvents, []); + }); + + it('should limit events sent', function() { + amplitude.init(apiKey, null, {uploadBatchSize: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[9].event_properties, {index: 9}); + + server.respondWith('success'); + server.respond(); + + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 6); + assert.deepEqual(events[0].event_properties, {index: 10}); + assert.deepEqual(events[5].event_properties, {index: 100}); + }); + + it('should batch events sent', function() { + var eventUploadPeriodMillis = 10*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 10, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[9].event_properties, {index: 9}); + + server.respondWith('success'); + server.respond(); + + assert.lengthOf(server.requests, 1); + var unsentEvents = amplitude._unsentEvents; + assert.lengthOf(unsentEvents, 5); + assert.deepEqual(unsentEvents[4].event_properties, {index: 14}); + + // remaining 5 events should be sent by the delayed sendEvent call + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 2); + server.respondWith('success'); + server.respond(); + assert.lengthOf(amplitude._unsentEvents, 0); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 5); + assert.deepEqual(events[4].event_properties, {index: 14}); + }); + + it('should send events after a delay', function() { + var eventUploadPeriodMillis = 10*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 2, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + amplitude.logEvent('Event'); + + // saveEvent should not have been called yet + assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(server.requests, 0); + + // saveEvent should be called after delay + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.deepEqual(events[0].event_type, 'Event'); + }); + + it('should not send events after a delay if no events to send', function() { + var eventUploadPeriodMillis = 10*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 2, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + amplitude.logEvent('Event1'); + amplitude.logEvent('Event2'); + + // saveEvent triggered by 2 event batch threshold + assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + assert.deepEqual(events[1].event_type, 'Event2'); + + // saveEvent should be called after delay, but no request made + assert.lengthOf(amplitude._unsentEvents, 0); + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 1); + }); + + it('should not schedule more than one upload', function() { + var eventUploadPeriodMillis = 5*1000; // 5s + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 30, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + + // log 2 events, 1 millisecond apart, second event should not schedule upload + amplitude.logEvent('Event1'); + clock.tick(1); + amplitude.logEvent('Event2'); + assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(server.requests, 0); + + // advance to upload period millis, and should have 1 server request + // from the first scheduled upload + clock.tick(eventUploadPeriodMillis-1); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + + // log 3rd event, advance 1 more millisecond, verify no 2nd server request + amplitude.logEvent('Event3'); + clock.tick(1); + assert.lengthOf(server.requests, 1); + + // the 3rd event, however, should have scheduled another upload after 5s + clock.tick(eventUploadPeriodMillis-2); + assert.lengthOf(server.requests, 1); + clock.tick(1); + assert.lengthOf(server.requests, 2); + }); + + it('should back off on 413 status', function() { + amplitude.init(apiKey, null, {uploadBatchSize: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[9].event_properties, {index: 9}); + + server.respondWith([413, {}, '']); + server.respond(); + + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 5); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[4].event_properties, {index: 4}); + }); + + it('should back off on 413 status all the way to 1 event with drops', function() { + amplitude.init(apiKey, null, {uploadBatchSize: 9}); + + amplitude._sending = true; + for (var i = 0; i < 10; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + amplitude.logEvent('Event', {index: 100}); + + for (var i = 0; i < 6; i++) { + assert.lengthOf(server.requests, i+1); + server.respondWith([413, {}, '']); + server.respond(); + } + + var events = JSON.parse(querystring.parse(server.requests[6].requestBody).e); + assert.lengthOf(events, 1); + assert.deepEqual(events[0].event_properties, {index: 2}); + }); + + it ('should run callback if no eventType', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + amplitude.logEvent(null, null, callback); + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + + it ('should run callback if optout', function () { + amplitude.setOptOut(true); + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + amplitude.logEvent('test', null, callback); + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + + it ('should not run callback if invalid callback and no eventType', function () { + amplitude.logEvent(null, null, 'invalid callback'); + }); + + it ('should run callback after logging event', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + amplitude.logEvent('test', null, callback); + + // before server responds, callback should not fire + assert.lengthOf(server.requests, 1); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after server response, fire callback + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it ('should run callback if batchEvents but under threshold', function () { + var eventUploadPeriodMillis = 5*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 2, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + amplitude.logEvent('test', null, callback); + assert.lengthOf(server.requests, 0); + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + + // check that request is made after delay, but callback is not run a second time + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + }); + + it ('should run callback once and only after all events are uploaded', function () { + amplitude.init(apiKey, null, {uploadBatchSize: 10}); + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + // queue up 15 events, since batchsize 10, need to send in 2 batches + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}, callback); + + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + + // after first response received, callback should not have fired + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + assert.lengthOf(server.requests, 2); + server.respondWith('success'); + server.respond(); + + // after last response received, callback should fire + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it ('should run callback once and only after 413 resolved', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + // queue up 15 events + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + // 16th event with 413 will backoff to batches of 8 + amplitude.logEvent('Event', {index: 100}, callback); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 16); + + // after 413 response received, callback should not have fired + server.respondWith([413, {}, '']); + server.respond(); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after sending first backoff batch, callback still should not have fired + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 8); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after sending second backoff batch, callback should fire + assert.lengthOf(server.requests, 3); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 8); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it ('should run callback if server returns something other than 200 and 413', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + amplitude.logEvent('test', null, callback); + server.respondWith([404, {}, 'Not found']); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 404); + assert.equal(message, 'Not found'); + }); + + it('should send 3 identify events', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.identify(new Identify().add('photoCount', 1).set('country', 'USA')); + amplitude.identify(new Identify().add('photoCount', 1)); + + // verify some internal counters + assert.equal(amplitude._eventId, 0); + assert.equal(amplitude._identifyId, 3); + assert.equal(amplitude._unsentCount(), 3); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 3); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + for (var i = 0; i < 3; i++) { + assert.equal(events[i].event_type, '$identify'); + assert.isTrue('$add' in events[i].user_properties); + assert.deepEqual(events[i].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[i].event_id, i+1); + assert.equal(events[i].sequence_number, i+1); + } + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should send 3 events', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.logEvent('test'); + amplitude.logEvent('test'); + amplitude.logEvent('test'); + + // verify some internal counters + assert.equal(amplitude._eventId, 3); + assert.equal(amplitude._identifyId, 0); + assert.equal(amplitude._unsentCount(), 3); + assert.lengthOf(amplitude._unsentEvents, 3); + assert.lengthOf(amplitude._unsentIdentifys, 0); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + for (var i = 0; i < 3; i++) { + assert.equal(events[i].event_type, 'test'); + assert.equal(events[i].event_id, i+1); + assert.equal(events[i].sequence_number, i+1); + } + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + }); + + it('should send 1 event and 1 identify event', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.logEvent('test'); + amplitude.identify(new Identify().add('photoCount', 1)); + + // verify some internal counters + assert.equal(amplitude._eventId, 1); + assert.equal(amplitude._identifyId, 1); + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(amplitude._unsentIdentifys, 1); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + + // event should come before identify - maintain order using sequence number + assert.equal(events[0].event_type, 'test'); + assert.equal(events[0].event_id, 1); + assert.deepEqual(events[0].user_properties, {}); + assert.equal(events[0].sequence_number, 1); + assert.equal(events[1].event_type, '$identify'); + assert.equal(events[1].event_id, 1); + assert.isTrue('$add' in events[1].user_properties); + assert.deepEqual(events[1].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[1].sequence_number, 2); + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should properly coalesce events and identify events into a request', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.logEvent('test1'); + clock.tick(1); + amplitude.identify(new Identify().add('photoCount', 1)); + clock.tick(1); + amplitude.logEvent('test2'); + clock.tick(1); + amplitude.logEvent('test3'); + clock.tick(1); + amplitude.logEvent('test4'); + amplitude.identify(new Identify().add('photoCount', 2)); + + // verify some internal counters + assert.equal(amplitude._eventId, 4); + assert.equal(amplitude._identifyId, 2); + assert.equal(amplitude._unsentCount(), 6); + assert.lengthOf(amplitude._unsentEvents, 4); + assert.lengthOf(amplitude._unsentIdentifys, 2); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 6); + + // verify the correct coalescing + assert.equal(events[0].event_type, 'test1'); + assert.deepEqual(events[0].user_properties, {}); + assert.equal(events[0].sequence_number, 1); + assert.equal(events[1].event_type, '$identify'); + assert.isTrue('$add' in events[1].user_properties); + assert.deepEqual(events[1].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[1].sequence_number, 2); + assert.equal(events[2].event_type, 'test2'); + assert.deepEqual(events[2].user_properties, {}); + assert.equal(events[2].sequence_number, 3); + assert.equal(events[3].event_type, 'test3'); + assert.deepEqual(events[3].user_properties, {}); + assert.equal(events[3].sequence_number, 4); + assert.equal(events[4].event_type, 'test4'); + assert.deepEqual(events[4].user_properties, {}); + assert.equal(events[4].sequence_number, 5); + assert.equal(events[5].event_type, '$identify'); + assert.isTrue('$add' in events[5].user_properties); + assert.deepEqual(events[5].user_properties['$add'], {'photoCount': 2}); + assert.equal(events[5].sequence_number, 6); + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should merged events supporting backwards compatability', function() { + // events logged before v2.5.0 won't have sequence number, should get priority + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.logEvent('test'); + delete amplitude._unsentEvents[0].sequence_number; // delete sequence number to simulate old event + amplitude._sequenceNumber = 1; // reset sequence number + amplitude.identify(new Identify().add('photoCount', 2)); + + // verify some internal counters + assert.equal(amplitude._eventId, 1); + assert.equal(amplitude._identifyId, 2); + assert.equal(amplitude._unsentCount(), 3); + assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(amplitude._unsentIdentifys, 2); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + + // event should come before identify - prioritize events with no sequence number + assert.equal(events[0].event_type, 'test'); + assert.equal(events[0].event_id, 1); + assert.deepEqual(events[0].user_properties, {}); + assert.isFalse('sequence_number' in events[0]); + + assert.equal(events[1].event_type, '$identify'); + assert.equal(events[1].event_id, 1); + assert.isTrue('$add' in events[1].user_properties); + assert.deepEqual(events[1].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[1].sequence_number, 1); + + assert.equal(events[2].event_type, '$identify'); + assert.equal(events[2].event_id, 2); + assert.isTrue('$add' in events[2].user_properties); + assert.deepEqual(events[2].user_properties['$add'], {'photoCount': 2}); + assert.equal(events[2].sequence_number, 2); + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should drop event and keep identify on 413 response', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('test'); + clock.tick(1); + amplitude.identify(new Identify().add('photoCount', 1)); + + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 1); + server.respondWith([413, {}, '']); + server.respond(); + + // backoff and retry + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 2); + server.respondWith([413, {}, '']); + server.respond(); + + // after dropping massive event, only 1 event left + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 3); + + var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.isTrue('$add' in events[0].user_properties); + assert.deepEqual(events[0].user_properties['$add'], {'photoCount': 1}); + }); + + it('should drop identify if 413 and uploadBatchSize is 1', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.identify(new Identify().add('photoCount', 1)); + clock.tick(1); + amplitude.logEvent('test'); + + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 1); + server.respondWith([413, {}, '']); + server.respond(); + + // backoff and retry + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 2); + server.respondWith([413, {}, '']); + server.respond(); + + // after dropping massive event, only 1 event left + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 3); + + var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, 'test'); + assert.deepEqual(events[0].user_properties, {}); + }); + + it('should truncate long event property strings', function() { + 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'], 4096); + }); + + it('should truncate long user property strings', function() { + 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'], 4096); + }); + + it('should increment the counters in local storage if cookies disabled', function() { + localStorage.clear(); + var deviceId = 'test_device_id'; + var amplitude2 = new Amplitude(); + + sinon.stub(CookieStorage.prototype, '_cookiesEnabled').returns(false); + amplitude2.init(apiKey, null, {deviceId: deviceId, batchEvents: true, eventUploadThreshold: 5}); + CookieStorage.prototype._cookiesEnabled.restore(); + + amplitude2.logEvent('test'); + clock.tick(10); // starts the session + amplitude2.logEvent('test2'); + clock.tick(20); + amplitude2.setUserProperties({'key':'value'}); // identify event at time 30 + + var cookieData = JSON.parse(localStorage.getItem('amp_cookiestore_amplitude_id')); + assert.deepEqual(cookieData, { + 'deviceId': deviceId, + 'userId': null, + 'optOut': false, + 'sessionId': 10, + 'lastEventTime': 30, + 'eventId': 2, + 'identifyId': 1, + 'sequenceNumber': 3 + }); + }); + + it('should validate event properties', function() { + var e = new Error('oops'); + clock.tick(1); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + clock.tick(1); + amplitude.logEvent('String event properties', '{}'); + clock.tick(1); + amplitude.logEvent('Bool event properties', true); + clock.tick(1); + amplitude.logEvent('Number event properties', 15); + clock.tick(1); + amplitude.logEvent('Array event properties', [1, 2, 3]); + clock.tick(1); + amplitude.logEvent('Object event properties', { + 10: 'false', // coerce key + 'bool': true, + 'null': null, // should be ignored + 'function': console.log, // should be ignored + 'regex': /afdg/, // should be ignored + 'error': e, // coerce value + 'string': 'test', + 'array': [0, 1, 2, '3'], + 'nested_array': ['a', {'key': 'value'}, ['b']], + 'object': {'key':'value', 15: e}, + 'nested_object': {'k':'v', 'l':[0,1], 'o':{'k2':'v2', 'l2': ['e2', {'k3': 'v3'}]}} + }); + clock.tick(1); + + assert.lengthOf(amplitude._unsentEvents, 5); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 5); + + assert.deepEqual(events[0].event_properties, {}); + assert.deepEqual(events[1].event_properties, {}); + assert.deepEqual(events[2].event_properties, {}); + assert.deepEqual(events[3].event_properties, {}); + assert.deepEqual(events[4].event_properties, { + '10': 'false', + 'bool': true, + 'error': 'Error: oops', + 'string': 'test', + 'array': [0, 1, 2, '3'], + 'nested_array': ['a'], + 'object': {'key':'value', '15':'Error: oops'}, + 'nested_object': {'k':'v', 'l':[0,1], 'o':{'k2':'v2', 'l2': ['e2']}} + }); + }); + + 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(); + amplitude1.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + + amplitude1.logEvent('test1'); + amplitude2.logEvent('test2'); + amplitude1.logEvent('test3'); + amplitude2.logEvent('test4'); + amplitude2.identify(new amplitude2.Identify().set('key', 'value')); + amplitude1.logEvent('test5'); + + // the event ids should all be sequential since amplitude1 and amplitude2 have synchronized cookies + var eventId = amplitude1._unsentEvents[0]['event_id']; + assert.equal(amplitude2._unsentEvents[0]['event_id'], eventId + 1); + assert.equal(amplitude1._unsentEvents[1]['event_id'], eventId + 2); + assert.equal(amplitude2._unsentEvents[1]['event_id'], eventId + 3); + + var sequenceNumber = amplitude1._unsentEvents[0]['sequence_number']; + assert.equal(amplitude2._unsentIdentifys[0]['sequence_number'], sequenceNumber + 4); + assert.equal(amplitude1._unsentEvents[2]['sequence_number'], sequenceNumber + 5); + }); + + it('should handle groups input', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + var eventProperties = { + 'key': 'value' + }; + + var groups = { + 10: 1.23, // coerce numbers to strings + 'array': ['test2', false, ['test', 23, null], null], // should ignore nested array and nulls + 'dictionary': {160: 'test3'}, // should ignore dictionaries + 'null': null, // ignore null values + } + + amplitude.logEventWithGroups('Test', eventProperties, groups, callback); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + + // verify event is correctly formatted + var event = events[0]; + assert.equal(event.event_type, 'Test'); + assert.equal(event.event_id, 1); + assert.deepEqual(event.user_properties, {}); + assert.deepEqual(event.event_properties, eventProperties); + assert.deepEqual(event.groups, { + '10': '1.23', + 'array': ['test2', 'false'], + }); + + // verify callback behavior + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + }); + + describe('optOut', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should not send events while enabled', function() { + amplitude.setOptOut(true); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 0); + }); + + it('should not send saved events while enabled', function() { + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + + amplitude._sending = false; + amplitude.setOptOut(true); + amplitude.init(apiKey); + assert.lengthOf(server.requests, 1); + }); + + it('should start sending events again when disabled', function() { + amplitude.setOptOut(true); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 0); + + amplitude.setOptOut(false); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + }); + + it('should have state be persisted in the cookie', function() { + var amplitude = new Amplitude(); + amplitude.init(apiKey); + assert.strictEqual(amplitude.options.optOut, false); + + amplitude.setOptOut(true); + + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey); + assert.strictEqual(amplitude2.options.optOut, true); + }); + + it('should limit identify events queued', function() { + amplitude.init(apiKey, null, {savedMaxCount: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.identify(new Identify().add('test', i)); + } + amplitude._sending = false; + + amplitude.identify(new Identify().add('test', 100)); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].user_properties, {$add: {'test': 6}}); + assert.deepEqual(events[9].user_properties, {$add: {'test': 100}}); + }); + }); + + describe('gatherUtm', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should not send utm data when the includeUtm flag is false', function() { + cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); + reset(); + amplitude.init(apiKey, undefined, {}); + + amplitude.setUserProperties({user_prop: true}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events[0].user_properties.utm_campaign, undefined); + assert.equal(events[0].user_properties.utm_content, undefined); + assert.equal(events[0].user_properties.utm_medium, undefined); + assert.equal(events[0].user_properties.utm_source, undefined); + assert.equal(events[0].user_properties.utm_term, undefined); + }); + + 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, 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, { + '$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 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); + + 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); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$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(); + + 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, { + '$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)); + }); + }); + + describe('gatherReferrer', function() { + beforeEach(function() { + amplitude.init(apiKey); + sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.com/contact'); + }); + + afterEach(function() { + amplitude._getReferrer.restore(); + reset(); + }); + + it('should not send referrer data when the includeReferrer flag is false', function() { + amplitude.init(apiKey, undefined, {}); + + amplitude.setUserProperties({user_prop: true}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events[0].user_properties.referrer, undefined); + assert.equal(events[0].user_properties.referring_domain, undefined); + }); + + it('should only send referrer via identify call when the includeReferrer flag is true', function() { + reset(); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + 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': expected, + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be the test event with no referrer information + assert.equal(events[1].event_type, 'Referrer Test Event'); + assert.deepEqual(events[1].user_properties, {}); + + // referrer should be propagated to session storage + assert.equal(sessionStorage.getItem('amplitude_referrer'), JSON.stringify(expected)); + }); + + it('should not set referrer if referrer data already in session storage', function() { + reset(); + sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + + // first event should be identify with initial_referrer and NO referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be the test event with no referrer information + assert.equal(events[1].event_type, 'Referrer Test Event'); + assert.deepEqual(events[1].user_properties, {}); + }); + + it('should not override any existing initial referrer values in session storage', function() { + reset(); + sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); + amplitude._saveReferrer('https://facebook.com/contact'); + amplitude.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + + // first event should be identify with initial_referrer and NO referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be another identify but with the new referrer + assert.equal(events[1].event_type, '$identify'); + assert.deepEqual(events[1].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://facebook.com/contact', + 'initial_referring_domain': 'facebook.com' + } + }); + + // third event should be the test event with no referrer information + assert.equal(events[2].event_type, 'Referrer Test Event'); + assert.deepEqual(events[2].user_properties, {}); + + // existing value persists + assert.equal(sessionStorage.getItem('amplitude_referrer'), 'https://www.google.com/search?'); + }); + }); + + describe('logRevenue', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + /** + * Deep compare an object against the api_properties of the + * event queued for sending. + */ + function revenueEqual(api, event) { + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.deepEqual(events[0].api_properties, api || {}); + assert.deepEqual(events[0].event_properties, event || {}); + } + + it('should log simple amount', function() { + amplitude.logRevenue(10.10); + revenueEqual({ + special: 'revenue_amount', + price: 10.10, + quantity: 1 + }) + }); + + it('should log complex amount', function() { + amplitude.logRevenue(10.10, 7); + revenueEqual({ + special: 'revenue_amount', + price: 10.10, + quantity: 7 + }) + }); + + it('shouldn\'t log invalid price', function() { + amplitude.logRevenue('kitten', 7); + assert.lengthOf(server.requests, 0); + }); + + it('shouldn\'t log invalid quantity', function() { + amplitude.logRevenue(10.00, 'puppy'); + assert.lengthOf(server.requests, 0); + }); + + it('should log complex amount with product id', function() { + amplitude.logRevenue(10.10, 7, 'chicken.dinner'); + revenueEqual({ + special: 'revenue_amount', + price: 10.10, + quantity: 7, + productId: 'chicken.dinner' + }); + }); + }); + + describe('logRevenueV2', function() { + beforeEach(function() { + reset(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should log with the Revenue object', function () { + // ignore invalid revenue objects + amplitude.logRevenueV2(null); + assert.lengthOf(server.requests, 0); + amplitude.logRevenueV2({}); + assert.lengthOf(server.requests, 0); + amplitude.logRevenueV2(new amplitude.Revenue()); + + // log valid revenue object + var productId = 'testProductId'; + var quantity = 15; + var price = 10.99; + var revenueType = 'testRevenueType' + var properties = {'city': 'San Francisco'}; + + var revenue = new amplitude.Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); + revenue.setRevenueType(revenueType).setEventProperties(properties); + + amplitude.logRevenueV2(revenue); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + var event = events[0]; + assert.equal(event.event_type, 'revenue_amount'); + + assert.deepEqual(event.event_properties, { + '$productId': productId, + '$quantity': quantity, + '$price': price, + '$revenueType': revenueType, + 'city': 'San Francisco' + }); + + // verify user properties empty + assert.deepEqual(event.user_properties, {}); + + // verify no revenue data in api_properties + assert.deepEqual(event.api_properties, {}); + }); + + it('should convert proxied Revenue object into real revenue object', function() { + var fakeRevenue = {'_q':[ + ['setProductId', 'questionable'], + ['setQuantity', 10], + ['setPrice', 'key1'] // invalid price type, this will fail to generate revenue event + ]}; + amplitude.logRevenueV2(fakeRevenue); + assert.lengthOf(server.requests, 0); + + var proxyRevenue = {'_q':[ + ['setProductId', 'questionable'], + ['setQuantity', 15], + ['setPrice', 10.99], + ['setRevenueType', 'purchase'] + ]}; + amplitude.logRevenueV2(proxyRevenue); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + var event = events[0]; + assert.equal(event.event_type, 'revenue_amount'); + + assert.deepEqual(event.event_properties, { + '$productId': 'questionable', + '$quantity': 15, + '$price': 10.99, + '$revenueType': 'purchase' + }); + }); + }); + + describe('sessionId', function() { + var clock; + beforeEach(function() { + clock = sinon.useFakeTimers(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + clock.restore(); + }); + + it('should create new session IDs on timeout', function() { + var sessionId = amplitude._sessionId; + clock.tick(30 * 60 * 1000 + 1); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + assert.notEqual(events[0].session_id, sessionId); + assert.notEqual(amplitude._sessionId, sessionId); + assert.equal(events[0].session_id, amplitude._sessionId); + }); + + it('should be fetched correctly by getSessionId', function() { + var timestamp = 1000; + clock.tick(timestamp); + var amplitude2 = new Amplitude(); + amplitude2.init(apiKey); + assert.equal(amplitude2._sessionId, timestamp); + assert.equal(amplitude2.getSessionId(), timestamp); + assert.equal(amplitude2.getSessionId(), amplitude2._sessionId); + }); + }); +}); \ No newline at end of file diff --git a/test/amplitude.js b/test/amplitude.js index a01c4fc9..18ed46ee 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -83,7 +83,7 @@ describe('Amplitude', function() { }; amplitude.getInstance().init(apiKey, userId, config); - assert.equal(amplitude.getInstance().options.apiEndpoint, 'api.amplitude.getInstance().com'); + assert.equal(amplitude.getInstance().options.apiEndpoint, 'api.amplitude.com'); assert.equal(amplitude.getInstance().options.batchEvents, false); assert.equal(amplitude.getInstance().options.cookieExpiration, 3650); assert.equal(amplitude.getInstance().options.cookieName, 'amplitude_id'); @@ -233,34 +233,6 @@ describe('Amplitude', function() { assert.equal(amplitude.getInstance()._sequenceNumber, 0); }); - it('should save cookie data to localStorage if cookies are not enabled', function() { - var cookieStorageKey = 'amp_cookiestore_amplitude_id'; - var deviceId = 'test_device_id'; - var clock = sinon.useFakeTimers(); - clock.tick(1000); - - localStorage.clear(); - sinon.stub(CookieStorage.prototype, '_cookiesEnabled').returns(false); - var amplitude2 = new Amplitude(); - CookieStorage.prototype._cookiesEnabled.restore(); - amplitude2.init(apiKey, userId, {'deviceId': deviceId}); - clock.restore(); - - var cookieData = JSON.parse(localStorage.getItem(cookieStorageKey)); - assert.deepEqual(cookieData, { - 'deviceId': deviceId, - 'userId': userId, - 'optOut': false, - 'sessionId': 1000, - 'lastEventTime': 1000, - 'eventId': 0, - 'identifyId': 0, - 'sequenceNumber': 0 - }); - - assert.isNull(cookie.get(amplitude2.options.cookieName)); // assert did not write to cookies - }); - it('should load sessionId, eventId from cookie and ignore the one in localStorage', function() { var sessionIdKey = 'amplitude_sessionId'; var lastEventTimeKey = 'amplitude_lastEventTime'; @@ -297,11 +269,11 @@ describe('Amplitude', function() { amplitude2.init(apiKey); clock.restore(); - assert.equal(amplitude2._sessionId, sessionId); - assert.equal(amplitude2._lastEventTime, sessionId + 10); - assert.equal(amplitude2._eventId, 50); - assert.equal(amplitude2._identifyId, 60); - assert.equal(amplitude2._sequenceNumber, 70); + assert.equal(amplitude2.getInstance()._sessionId, sessionId); + assert.equal(amplitude2.getInstance()._lastEventTime, sessionId + 10); + assert.equal(amplitude2.getInstance()._eventId, 50); + assert.equal(amplitude2.getInstance()._identifyId, 60); + assert.equal(amplitude2.getInstance()._sequenceNumber, 70); }); it('should load sessionId from localStorage if not in cookie', function() { @@ -334,11 +306,11 @@ describe('Amplitude', function() { amplitude2.init(apiKey, userId); clock.restore(); - assert.equal(amplitude2._sessionId, sessionId); - assert.equal(amplitude2._lastEventTime, sessionId + 10); - assert.equal(amplitude2._eventId, 50); - assert.equal(amplitude2._identifyId, 60); - assert.equal(amplitude2._sequenceNumber, 70); + assert.equal(amplitude2.getInstance()._sessionId, sessionId); + assert.equal(amplitude2.getInstance()._lastEventTime, sessionId + 10); + assert.equal(amplitude2.getInstance()._eventId, 50); + assert.equal(amplitude2.getInstance()._identifyId, 60); + assert.equal(amplitude2.getInstance()._sequenceNumber, 70); }); it('should load saved events from localStorage', function() { @@ -360,8 +332,8 @@ describe('Amplitude', function() { amplitude2.init(apiKey, null, {batchEvents: true}); // check event loaded into memory - assert.deepEqual(amplitude2._unsentEvents, JSON.parse(existingEvent)); - assert.deepEqual(amplitude2._unsentIdentifys, JSON.parse(existingIdentify)); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, JSON.parse(existingEvent)); + assert.deepEqual(amplitude2.getInstance()._unsentIdentifys, JSON.parse(existingIdentify)); // check local storage keys are still same for default instance assert.equal(localStorage.getItem('amplitude_unsent'), existingEvent); @@ -397,8 +369,8 @@ describe('Amplitude', function() { } // check that event loaded into memory - assert.deepEqual(amplitude2._unsentEvents[0].event_properties, {}); - assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[0].event_properties, {}); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[1].event_properties, expected); }); it('should validate user properties when loading saved identifys from localStorage', function() { @@ -426,7 +398,7 @@ describe('Amplitude', function() { } // check that event loaded into memory - assert.deepEqual(amplitude2._unsentIdentifys[0].user_properties, {'$set': expected}); + assert.deepEqual(amplitude2.getInstance()._unsentIdentifys[0].user_properties, {'$set': expected}); }); it ('should load saved events from localStorage new keys and send events', function() { @@ -450,8 +422,8 @@ describe('Amplitude', function() { server.respond(); // check event loaded into memory - assert.deepEqual(amplitude2._unsentEvents, []); - assert.deepEqual(amplitude2._unsentIdentifys, []); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, []); + assert.deepEqual(amplitude2.getInstance()._unsentIdentifys, []); // check local storage keys are still same assert.equal(localStorage.getItem('amplitude_unsent'), JSON.stringify([])); @@ -505,8 +477,8 @@ describe('Amplitude', function() { } // check that event loaded into memory - assert.deepEqual(amplitude2._unsentEvents[0].event_properties, {}); - assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[0].event_properties, {}); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[1].event_properties, expected); }); }); @@ -836,7 +808,7 @@ describe('setVersionName', function() { value = status; message = response; } - var identify = new amplitude.getInstance().Identify().set('key', 'value'); + var identify = new amplitude.Identify().set('key', 'value'); amplitude.getInstance().identify(identify, callback); // before server responds, callback should not fire @@ -862,7 +834,7 @@ describe('setVersionName', function() { value = status; message = response; } - var identify = new amplitude.getInstance().Identify().set('key', 'value'); + var identify = new amplitude.Identify().set('key', 'value'); new Amplitude().identify(identify, callback); // verify callback fired @@ -906,7 +878,7 @@ describe('setVersionName', function() { it('should send request', function() { amplitude.getInstance().logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); - assert.equal(server.requests[0].url, 'http://api.amplitude.getInstance().com/'); + assert.equal(server.requests[0].url, 'http://api.amplitude.com/'); assert.equal(server.requests[0].method, 'POST'); assert.equal(server.requests[0].async, true); }); @@ -1011,7 +983,7 @@ describe('setVersionName', function() { var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.deepEqual(amplitude2._unsentEvents, amplitude.getInstance()._unsentEvents); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, amplitude.getInstance()._unsentEvents); }); it('should not save events', function() { @@ -1022,7 +994,7 @@ describe('setVersionName', function() { var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, []); }); it('should limit events sent', function() { @@ -1799,14 +1771,14 @@ describe('setVersionName', function() { amplitude1.logEvent('test5'); // the event ids should all be sequential since amplitude1 and amplitude2 have synchronized cookies - var eventId = amplitude1._unsentEvents[0]['event_id']; - assert.equal(amplitude2._unsentEvents[0]['event_id'], eventId + 1); - assert.equal(amplitude1._unsentEvents[1]['event_id'], eventId + 2); - assert.equal(amplitude2._unsentEvents[1]['event_id'], eventId + 3); + var eventId = amplitude1.getInstance()._unsentEvents[0]['event_id']; + assert.equal(amplitude2.getInstance()._unsentEvents[0]['event_id'], eventId + 1); + assert.equal(amplitude1.getInstance()._unsentEvents[1]['event_id'], eventId + 2); + assert.equal(amplitude2.getInstance()._unsentEvents[1]['event_id'], eventId + 3); - var sequenceNumber = amplitude1._unsentEvents[0]['sequence_number']; - assert.equal(amplitude2._unsentIdentifys[0]['sequence_number'], sequenceNumber + 4); - assert.equal(amplitude1._unsentEvents[2]['sequence_number'], sequenceNumber + 5); + var sequenceNumber = amplitude1.getInstance()._unsentEvents[0]['sequence_number']; + assert.equal(amplitude2.getInstance()._unsentIdentifys[0]['sequence_number'], sequenceNumber + 4); + assert.equal(amplitude1.getInstance()._unsentEvents[2]['sequence_number'], sequenceNumber + 5); }); it('should handle groups input', function() { @@ -2053,7 +2025,7 @@ describe('setVersionName', function() { describe('gatherReferrer', function() { beforeEach(function() { amplitude.getInstance().init(apiKey); - sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.getInstance().com/contact'); + sinon.stub(amplitude.getInstance(), '_getReferrer').returns('https://amplitude.getInstance().com/contact'); }); afterEach(function() { @@ -2237,7 +2209,7 @@ describe('setVersionName', function() { assert.lengthOf(server.requests, 0); amplitude.getInstance().logRevenueV2({}); assert.lengthOf(server.requests, 0); - amplitude.getInstance().logRevenueV2(new amplitude.getInstance().Revenue()); + amplitude.getInstance().logRevenueV2(new amplitude.Revenue()); // log valid revenue object var productId = 'testProductId'; @@ -2246,7 +2218,7 @@ describe('setVersionName', function() { var revenueType = 'testRevenueType' var properties = {'city': 'San Francisco'}; - var revenue = new amplitude.getInstance().Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); + var revenue = new amplitude.Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); revenue.setRevenueType(revenueType).setEventProperties(properties); amplitude.getInstance().logRevenueV2(revenue); @@ -2330,9 +2302,9 @@ describe('setVersionName', function() { clock.tick(timestamp); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.equal(amplitude2._sessionId, timestamp); + assert.equal(amplitude2.getInstance()._sessionId, timestamp); assert.equal(amplitude2.getSessionId(), timestamp); - assert.equal(amplitude2.getSessionId(), amplitude2._sessionId); + assert.equal(amplitude2.getSessionId(), amplitude2.getInstance()._sessionId); }); }); }); From a73cbf548496487b9c48301c7f7f5ec6ced7c97a Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Sat, 21 May 2016 00:25:14 -0700 Subject: [PATCH 06/13] add tests for amplitude instance handling --- amplitude.js | 16 ++-- amplitude.min.js | 6 +- src/amplitude-client.js | 16 ++-- test/amplitude.js | 180 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 15 deletions(-) diff --git a/amplitude.js b/amplitude.js index dd71cf13..21bfa9a8 100644 --- a/amplitude.js +++ b/amplitude.js @@ -499,7 +499,9 @@ var DEFAULT_OPTIONS = require('./options'); * @public * @example var amplitude = new Amplitude(); */ -var AmplitudeClient = function Amplitude() { +var AmplitudeClient = function Amplitude(instanceName) { + this._instanceName = (utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName).toLowerCase(); + this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName; this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); @@ -547,7 +549,9 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o }); this.options.domain = this.cookieStorage.options().domain; - _upgradeCookeData(this); + if (this._instanceName === Constants.DEFAULT_INSTANCE) { + _upgradeCookeData(this); + } _loadCookieData(this); // load deviceId and userId from input, or try to fetch existing value from cookie @@ -780,7 +784,7 @@ AmplitudeClient.prototype._sendEventsIfReady = function _sendEventsIfReady(callb * @private */ AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, key) { - return storage.getItem(key); + return storage.getItem(key + this._storageSuffix); }; /** @@ -789,7 +793,7 @@ AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, ke * @private */ AmplitudeClient.prototype._setInStorage = function _setInStorage(storage, key, value) { - storage.setItem(key, value); + storage.setItem(key + this._storageSuffix, value); }; /** @@ -852,7 +856,7 @@ var _upgradeCookeData = function _upgradeCookeData(scope) { * @private */ var _loadCookieData = function _loadCookieData(scope) { - var cookieData = scope.cookieStorage.get(scope.options.cookieName); + var cookieData = scope.cookieStorage.get(scope.options.cookieName + scope._storageSuffix); if (type(cookieData) === 'object') { if (cookieData.deviceId) { scope.options.deviceId = cookieData.deviceId; @@ -886,7 +890,7 @@ var _loadCookieData = function _loadCookieData(scope) { * @private */ var _saveCookieData = function _saveCookieData(scope) { - scope.cookieStorage.set(scope.options.cookieName, { + scope.cookieStorage.set(scope.options.cookieName + scope._storageSuffix, { deviceId: scope.options.deviceId, userId: scope.options.userId, optOut: scope.options.optOut, diff --git a/amplitude.min.js b/amplitude.min.js index af0f4572..29c1383b 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key)};AmplitudeClient.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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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},{}],10:[function(require,module,exports){module.exports="2.12.1"},{}],11:[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":29}],29:[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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key+this._storageSuffix)};AmplitudeClient.prototype._setInStorage=function _setInStorage(storage,key,value){storage.setItem(key+this._storageSuffix,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+scope._storageSuffix);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+scope._storageSuffix,{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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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},{}],10:[function(require,module,exports){module.exports="2.12.1"},{}],11:[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":29}],29:[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/src/amplitude-client.js b/src/amplitude-client.js index 5486f382..d11cfae1 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -21,7 +21,9 @@ var DEFAULT_OPTIONS = require('./options'); * @public * @example var amplitude = new Amplitude(); */ -var AmplitudeClient = function Amplitude() { +var AmplitudeClient = function Amplitude(instanceName) { + this._instanceName = (utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName).toLowerCase(); + this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName; this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); @@ -69,7 +71,9 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o }); this.options.domain = this.cookieStorage.options().domain; - _upgradeCookeData(this); + if (this._instanceName === Constants.DEFAULT_INSTANCE) { + _upgradeCookeData(this); + } _loadCookieData(this); // load deviceId and userId from input, or try to fetch existing value from cookie @@ -302,7 +306,7 @@ AmplitudeClient.prototype._sendEventsIfReady = function _sendEventsIfReady(callb * @private */ AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, key) { - return storage.getItem(key); + return storage.getItem(key + this._storageSuffix); }; /** @@ -311,7 +315,7 @@ AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, ke * @private */ AmplitudeClient.prototype._setInStorage = function _setInStorage(storage, key, value) { - storage.setItem(key, value); + storage.setItem(key + this._storageSuffix, value); }; /** @@ -374,7 +378,7 @@ var _upgradeCookeData = function _upgradeCookeData(scope) { * @private */ var _loadCookieData = function _loadCookieData(scope) { - var cookieData = scope.cookieStorage.get(scope.options.cookieName); + var cookieData = scope.cookieStorage.get(scope.options.cookieName + scope._storageSuffix); if (type(cookieData) === 'object') { if (cookieData.deviceId) { scope.options.deviceId = cookieData.deviceId; @@ -408,7 +412,7 @@ var _loadCookieData = function _loadCookieData(scope) { * @private */ var _saveCookieData = function _saveCookieData(scope) { - scope.cookieStorage.set(scope.options.cookieName, { + scope.cookieStorage.set(scope.options.cookieName + scope._storageSuffix, { deviceId: scope.options.deviceId, userId: scope.options.userId, optOut: scope.options.optOut, diff --git a/test/amplitude.js b/test/amplitude.js index 18ed46ee..75af51bf 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -34,9 +34,189 @@ describe('Amplitude', function() { localStorage.clear(); sessionStorage.clear(); cookie.remove(amplitude.getInstance().options.cookieName); + cookie.remove(amplitude.getInstance().options.cookieName + keySuffix); + cookie.remove(amplitude.getInstance().options.cookieName + '_app1'); + cookie.remove(amplitude.getInstance().options.cookieName + '_app2'); cookie.reset(); } + describe('getInstance', function() { + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should map no instance to default instance', function() { + amplitude.init(apiKey); + assert.equal(amplitude.getInstance().options.apiKey, apiKey); + assert.equal(amplitude.options, amplitude.getInstance().options); + assert.equal(amplitude.getInstance('$default_instance').options.apiKey, apiKey); + assert.equal(amplitude.getInstance(), amplitude.getInstance('$default_instance')); + assert.equal(amplitude.options.deviceId, amplitude.getInstance().options.deviceId); + + // test for case insensitivity + assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_INSTANCE')); + assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_instance')); + }); + + it('should create two separate instances', function() { + var app1 = amplitude.getInstance('app1'); + app1.init('1'); + var app2 = amplitude.getInstance('app2'); + app2.init('2'); + + assert.notEqual(app1, app2); + assert.equal(app1.options.apiKey, '1'); + assert.equal(app2.options.apiKey, '2'); + + assert.equal(app1, amplitude.getInstance('app1')); + assert.equal(app1, amplitude.getInstance('APP1')); + assert.equal(app1, amplitude.getInstance('aPp1')); + assert.equal(app2, amplitude.getInstance('app2')); + assert.equal(app2, amplitude.getInstance('APP2')); + assert.equal(app2, amplitude.getInstance('aPp2')); + + assert.equal(amplitude.getInstance('APP3')._instanceName, 'app3'); + }); + + it('should return the same instance for same key', function() { + var app = amplitude.getInstance('app'); + app.init('1'); + assert.equal(app, amplitude.getInstance('app')); + assert.equal(amplitude.getInstance('app').options.apiKey, '1'); + }); + + it('instances should have separate event queues and settings', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + var app1 = amplitude.getInstance('app1'); + app1.init('1'); + var app2 = amplitude.getInstance('app2'); + app2.init('2'); + + assert.notEqual(amplitude.options.deviceId, app1.options.deviceId); + assert.notEqual(amplitude.options.deviceId, app2.options.deviceId); + assert.notEqual(app1.options.deviceId, app2.options.deviceId); + + amplitude.logEvent('amplitude event'); + amplitude.logEvent('amplitude event2'); + var identify = new Identify().set('key', 'value'); + app1.identify(identify); + app2.logEvent('app2 event'); + + assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + + assert.lengthOf(app1._unsentEvents, 0); + assert.lengthOf(app1._unsentIdentifys, 1); + assert.lengthOf(app2._unsentEvents, 1); + assert.lengthOf(app2._unsentIdentifys, 0); + + assert.deepEqual(amplitude.getInstance()._unsentEvents[0].event_type, 'amplitude event'); + assert.deepEqual(amplitude.getInstance()._unsentEvents[1].event_type, 'amplitude event2'); + assert.deepEqual(amplitude.getInstance()._unsentIdentifys, []); + assert.deepEqual(app1._unsentEvents, []); + assert.deepEqual(app1._unsentIdentifys[0].user_properties, {'$set':{'key':'value'}}); + assert.deepEqual(app2._unsentEvents[0].event_type, 'app2 event'); + assert.deepEqual(app2._unsentIdentifys, []); + + assert.equal(amplitude.getInstance()._eventId, 2); + assert.equal(amplitude.getInstance()._identifyId, 0); + assert.equal(amplitude.getInstance()._sequenceNumber, 2); + assert.equal(app1._eventId, 0); + assert.equal(app1._identifyId, 1); + assert.equal(app1._sequenceNumber, 1); + assert.equal(app2._eventId, 1); + assert.equal(app2._identifyId, 0); + assert.equal(app2._sequenceNumber, 1); + + // verify separate localstorages + assert.deepEqual( + JSON.parse(localStorage.getItem('amplitude_unsent'))[0].event_type, 'amplitude event' + ); + assert.deepEqual( + JSON.parse(localStorage.getItem('amplitude_unsent'))[1].event_type, 'amplitude event2' + ); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), JSON.stringify([])); + assert.equal(localStorage.getItem('amplitude_unsent_app1'), JSON.stringify([])); + assert.deepEqual( + JSON.parse(localStorage.getItem('amplitude_unsent_identify_app1'))[0].user_properties, {'$set':{'key':'value'}} + ); + assert.equal( + JSON.parse(localStorage.getItem('amplitude_unsent_app2'))[0].event_type, 'app2 event' + ); + assert.equal(localStorage.getItem('amplitude_unsent_identify_app2'), JSON.stringify([])); + + // verify separate apiKeys in server requests + assert.lengthOf(server.requests, 3); + assert.equal(JSON.parse(querystring.parse(server.requests[1].requestBody).client), 1); + assert.equal(JSON.parse(querystring.parse(server.requests[2].requestBody).client), 2); + + // verify separate cookie data + var cookieData = cookie.get(amplitude.options.cookieName); + assert.equal(cookieData.deviceId, amplitude.options.deviceId); + + var cookieData1 = cookie.get(app1.options.cookieName + '_app1'); + assert.equal(cookieData1.deviceId, app1.options.deviceId); + + var cookieData2 = cookie.get(app2.options.cookieName + '_app2'); + assert.equal(cookieData2.deviceId, app2.options.deviceId); + }); + + it('new instances should not load historical cookie data', function() { + var now = new Date().getTime(); + + var cookieData = { + deviceId: 'test_device_id', + userId: 'test_user_id', + optOut: true, + sessionId: now-500, + lastEventTime: now-500, + eventId: 50, + identifyId: 60, + sequenceNumber: 70 + } + cookie.set(amplitude.options.cookieName, cookieData); + + // default instance loads from existing cookie + var app = amplitude.getInstance(); + app.init(apiKey); + assert.equal(app.options.deviceId, 'test_device_id'); + assert.equal(app.options.userId, 'test_user_id'); + assert.isTrue(app.options.optOut); + assert.equal(app._sessionId, now-500); + assert.isTrue(app._lastEventTime >= now); + assert.equal(app._eventId, 50); + assert.equal(app._identifyId, 60); + assert.equal(app._sequenceNumber, 70); + + var app1 = amplitude.getInstance('app1'); + app1.init('1'); + assert.notEqual(app1.options.deviceId, 'test_device_id'); + assert.isNull(app1.options.userId); + assert.isFalse(app1.options.optOut); + console.log(app1._sessionId); + assert.isTrue(app1._sessionId >= now); + assert.isTrue(app1._lastEventTime >= now); + assert.equal(app1._eventId, 0); + assert.equal(app1._identifyId, 0); + assert.equal(app1._sequenceNumber, 0); + + var app2 = amplitude.getInstance('app2'); + app2.init('2'); + assert.notEqual(app2.options.deviceId, 'test_device_id'); + assert.isNull(app2.options.userId); + assert.isFalse(app2.options.optOut); + assert.isTrue(app2._sessionId >= now); + assert.isTrue(app2._lastEventTime >= now); + assert.equal(app2._eventId, 0); + assert.equal(app2._identifyId, 0); + assert.equal(app2._sequenceNumber, 0); + }); + }); + describe('init', function() { beforeEach(function() { reset(); From e53703d5964c9df098ce4188845ce522e6940c91 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Sat, 21 May 2016 01:20:36 -0700 Subject: [PATCH 07/13] cleanup --- amplitude.js | 7 +- amplitude.min.js | 4 +- src/amplitude-client.js | 2 +- src/amplitude.js | 3 +- src/index.js | 2 +- test/amplitude-client.js | 48 +++- test/amplitude.js | 530 +++++++++++++++++++-------------------- 7 files changed, 313 insertions(+), 283 deletions(-) diff --git a/amplitude.js b/amplitude.js index 21bfa9a8..03241f4f 100644 --- a/amplitude.js +++ b/amplitude.js @@ -98,7 +98,7 @@ var Amplitude = require('./amplitude'); var old = window.amplitude || {}; var newInstance = new Amplitude(); newInstance._q = old._q || []; -for (var instance in old._iq) { // migrate eat instance's queue +for (var instance in old._iq) { // migrate each instance's queue if (old._iq.hasOwnProperty(instance)) { newInstance.getInstance(instance)._q = old._iq[instance]._q || []; } @@ -127,6 +127,7 @@ var DEFAULT_OPTIONS = require('./options'); */ var Amplitude = function Amplitude() { this.options = object.merge({}, DEFAULT_OPTIONS); + this._q = []; this._instances = {}; // mapping of instance names to instances }; @@ -159,7 +160,7 @@ Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_cal this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { // make options such as deviceId available for callback functions this.options = instance.options; - if (opt_callback && type(opt_callback) === 'function') { + if (type(opt_callback) === 'function') { opt_callback(instance); } }.bind(this)); @@ -499,7 +500,7 @@ var DEFAULT_OPTIONS = require('./options'); * @public * @example var amplitude = new Amplitude(); */ -var AmplitudeClient = function Amplitude(instanceName) { +var AmplitudeClient = function AmplitudeClient(instanceName) { this._instanceName = (utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName).toLowerCase(); this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName; this._unsentEvents = []; diff --git a/amplitude.min.js b/amplitude.min.js index 29c1383b..8437e1e5 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(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 newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=(utils.isEmptyString(instance)?constants.DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(opt_callback&&type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key+this._storageSuffix)};AmplitudeClient.prototype._setInStorage=function _setInStorage(storage,key,value){storage.setItem(key+this._storageSuffix,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+scope._storageSuffix);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+scope._storageSuffix,{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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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;jthis.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)}}};AmplitudeClient.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};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key+this._storageSuffix)};AmplitudeClient.prototype._setInStorage=function _setInStorage(storage,key,value){storage.setItem(key+this._storageSuffix,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+scope._storageSuffix);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+scope._storageSuffix,{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})};AmplitudeClient.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)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.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){}};AmplitudeClient.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)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.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)}};AmplitudeClient.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)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,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")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,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||{};groups=groups||{};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,groups:utils.truncate(utils.validateGroups(groups))};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)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.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,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.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};AmplitudeClient.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){}})};AmplitudeClient.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":23}],23:[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},{}],14:[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":24}],24:[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"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to 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":8,"./utils":9}],16:[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)},{}],6:[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)}},{}],17:[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:26}],26:[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},{}],10:[function(require,module,exports){module.exports="2.12.1"},{}],11:[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":29}],29:[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/src/amplitude-client.js b/src/amplitude-client.js index d11cfae1..03a67281 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -21,7 +21,7 @@ var DEFAULT_OPTIONS = require('./options'); * @public * @example var amplitude = new Amplitude(); */ -var AmplitudeClient = function Amplitude(instanceName) { +var AmplitudeClient = function AmplitudeClient(instanceName) { this._instanceName = (utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName).toLowerCase(); this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName; this._unsentEvents = []; diff --git a/src/amplitude.js b/src/amplitude.js index 9753cb6c..cefcef13 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -16,6 +16,7 @@ var DEFAULT_OPTIONS = require('./options'); */ var Amplitude = function Amplitude() { this.options = object.merge({}, DEFAULT_OPTIONS); + this._q = []; this._instances = {}; // mapping of instance names to instances }; @@ -48,7 +49,7 @@ Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_cal this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { // make options such as deviceId available for callback functions this.options = instance.options; - if (opt_callback && type(opt_callback) === 'function') { + if (type(opt_callback) === 'function') { opt_callback(instance); } }.bind(this)); diff --git a/src/index.js b/src/index.js index 8f1d5388..e0835fe1 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ var Amplitude = require('./amplitude'); var old = window.amplitude || {}; var newInstance = new Amplitude(); newInstance._q = old._q || []; -for (var instance in old._iq) { // migrate eat instance's queue +for (var instance in old._iq) { // migrate each instance's queue if (old._iq.hasOwnProperty(instance)) { newInstance.getInstance(instance)._q = old._iq[instance]._q || []; } diff --git a/test/amplitude-client.js b/test/amplitude-client.js index 375d4749..c035ea67 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -18,7 +18,7 @@ describe('AmplitudeClient', function() { var server; beforeEach(function() { - amplitude = new Amplitude(); + amplitude = new AmplitudeClient(); server = sinon.fakeServer.create(); }); @@ -34,6 +34,8 @@ describe('AmplitudeClient', function() { localStorage.clear(); sessionStorage.clear(); cookie.remove(amplitude.options.cookieName); + cookie.remove(amplitude.options.cookieName + keySuffix); + cookie.remove(amplitude.options.cookieName + '_new_app'); cookie.reset(); } @@ -46,6 +48,11 @@ describe('AmplitudeClient', function() { reset(); }); + it('should make instanceName case-insensitive', function() { + assert.equal(new AmplitudeClient('APP3')._instanceName, 'app3'); + assert.equal(new AmplitudeClient('$DEFAULT_INSTANCE')._instanceName, '$default_instance'); + }); + it('fails on invalid apiKeys', function() { amplitude.init(null); assert.equal(amplitude.options.apiKey, undefined); @@ -124,7 +131,7 @@ describe('AmplitudeClient', function() { assert.equal(counter, 1); }); - it ('should migrate deviceId, userId, optOut from localStorage to cookie', function() { + it ('should migrate deviceId, userId, optOut from localStorage to cookie on default instance', function() { var deviceId = 'test_device_id'; var userId = 'test_user_id'; @@ -144,6 +151,27 @@ describe('AmplitudeClient', function() { assert.isTrue(cookieData.optOut); }); + it('should not migrate any cookie or LS data for non-default instances', function() { + var deviceId = 'testDeviceId'; + var userId = 'test_user_id'; + + assert.isNull(cookie.get(amplitude.options.cookieName)); + localStorage.setItem('amplitude_deviceId' + keySuffix, deviceId); + localStorage.setItem('amplitude_userId' + keySuffix, userId); + localStorage.setItem('amplitude_optOut' + keySuffix, true); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey); + assert.notEqual(amplitude.options.deviceId, deviceId); + assert.isNull(amplitude2.options.userId); + assert.isFalse(amplitude2.options.optOut); + + var cookieData = cookie.get(amplitude.options.cookieName + '_new_app'); + assert.equal(cookieData.deviceId, amplitude2.options.deviceId); + assert.isNull(cookieData.userId); + assert.isFalse(cookieData.optOut); + }); + it('should migrate session and event info from localStorage to cookie', function() { var now = new Date().getTime(); @@ -241,7 +269,7 @@ describe('AmplitudeClient', function() { localStorage.clear(); sinon.stub(CookieStorage.prototype, '_cookiesEnabled').returns(false); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); CookieStorage.prototype._cookiesEnabled.restore(); amplitude2.init(apiKey, userId, {'deviceId': deviceId}); clock.restore(); @@ -267,7 +295,7 @@ describe('AmplitudeClient', function() { var eventIdKey = 'amplitude_lastEventId'; var identifyIdKey = 'amplitude_lastIdentifyId'; var sequenceNumberKey = 'amplitude_lastSequenceNumber'; - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); var clock = sinon.useFakeTimers(); clock.tick(1000); @@ -310,7 +338,7 @@ describe('AmplitudeClient', function() { var eventIdKey = 'amplitude_lastEventId'; var identifyIdKey = 'amplitude_lastIdentifyId'; var sequenceNumberKey = 'amplitude_lastSequenceNumber'; - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); var cookieData = { deviceId: 'test_device_id', @@ -341,7 +369,7 @@ describe('AmplitudeClient', function() { assert.equal(amplitude2._sequenceNumber, 70); }); - it('should load saved events from localStorage', function() { + it('should load saved events from localStorage for default instance', 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"' + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + @@ -383,7 +411,7 @@ describe('AmplitudeClient', function() { '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]' localStorage.setItem('amplitude_unsent', existingEvents); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient('$default_instance'); amplitude2.init(apiKey, null, {batchEvents: true}); var expected = { @@ -412,7 +440,7 @@ describe('AmplitudeClient', function() { '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]' localStorage.setItem('amplitude_unsent_identify', existingEvents); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey, null, {batchEvents: true}); var expected = { @@ -444,7 +472,7 @@ describe('AmplitudeClient', function() { localStorage.setItem('amplitude_unsent', existingEvent); localStorage.setItem('amplitude_unsent_identify', existingIdentify); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); server.respondWith('success'); server.respond(); @@ -480,7 +508,7 @@ describe('AmplitudeClient', function() { '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]'; localStorage.setItem('amplitude_unsent', existingEvents); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey, null, { batchEvents: true }); diff --git a/test/amplitude.js b/test/amplitude.js index 75af51bf..11ae32bd 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -33,10 +33,10 @@ describe('Amplitude', function() { function reset() { localStorage.clear(); sessionStorage.clear(); - cookie.remove(amplitude.getInstance().options.cookieName); - cookie.remove(amplitude.getInstance().options.cookieName + keySuffix); - cookie.remove(amplitude.getInstance().options.cookieName + '_app1'); - cookie.remove(amplitude.getInstance().options.cookieName + '_app2'); + cookie.remove(amplitude.options.cookieName); + cookie.remove(amplitude.options.cookieName + keySuffix); + cookie.remove(amplitude.options.cookieName + '_app1'); + cookie.remove(amplitude.options.cookieName + '_app2'); cookie.reset(); } @@ -51,11 +51,11 @@ describe('Amplitude', function() { it('should map no instance to default instance', function() { amplitude.init(apiKey); - assert.equal(amplitude.getInstance().options.apiKey, apiKey); - assert.equal(amplitude.options, amplitude.getInstance().options); + assert.equal(amplitude.options.apiKey, apiKey); + assert.equal(amplitude.options, amplitude.options); assert.equal(amplitude.getInstance('$default_instance').options.apiKey, apiKey); assert.equal(amplitude.getInstance(), amplitude.getInstance('$default_instance')); - assert.equal(amplitude.options.deviceId, amplitude.getInstance().options.deviceId); + assert.equal(amplitude.options.deviceId, amplitude.options.deviceId); // test for case insensitivity assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_INSTANCE')); @@ -227,28 +227,28 @@ describe('Amplitude', function() { }); it('fails on invalid apiKeys', function() { - amplitude.getInstance().init(null); - assert.equal(amplitude.getInstance().options.apiKey, undefined); - assert.equal(amplitude.getInstance().options.deviceId, undefined); + amplitude.init(null); + assert.equal(amplitude.options.apiKey, undefined); + assert.equal(amplitude.options.deviceId, undefined); - amplitude.getInstance().init(''); - assert.equal(amplitude.getInstance().options.apiKey, undefined); - assert.equal(amplitude.getInstance().options.deviceId, undefined); + amplitude.init(''); + assert.equal(amplitude.options.apiKey, undefined); + assert.equal(amplitude.options.deviceId, undefined); - amplitude.getInstance().init(apiKey); - assert.equal(amplitude.getInstance().options.apiKey, apiKey); - assert.lengthOf(amplitude.getInstance().options.deviceId, 37); + amplitude.init(apiKey); + assert.equal(amplitude.options.apiKey, apiKey); + assert.lengthOf(amplitude.options.deviceId, 37); }); it('should accept userId', function() { - amplitude.getInstance().init(apiKey, userId); - assert.equal(amplitude.getInstance().options.userId, userId); + amplitude.init(apiKey, userId); + assert.equal(amplitude.options.userId, userId); }); it('should generate a random deviceId', function() { - amplitude.getInstance().init(apiKey, userId); - assert.lengthOf(amplitude.getInstance().options.deviceId, 37); // UUID is length 36, but we append 'R' at end - assert.equal(amplitude.getInstance().options.deviceId[36], 'R'); + 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() { @@ -262,37 +262,37 @@ describe('Amplitude', function() { bogusKey: false }; - amplitude.getInstance().init(apiKey, userId, config); - assert.equal(amplitude.getInstance().options.apiEndpoint, 'api.amplitude.com'); - assert.equal(amplitude.getInstance().options.batchEvents, false); - assert.equal(amplitude.getInstance().options.cookieExpiration, 3650); - assert.equal(amplitude.getInstance().options.cookieName, 'amplitude_id'); - assert.equal(amplitude.getInstance().options.eventUploadPeriodMillis, 30000); - assert.equal(amplitude.getInstance().options.eventUploadThreshold, 30); - assert.equal(amplitude.getInstance().options.bogusKey, undefined); + 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.getInstance().init(apiKey, userId); - var stored = cookie.get(amplitude.getInstance().options.cookieName); + amplitude.init(apiKey, userId); + var stored = cookie.get(amplitude.options.cookieName); assert.property(stored, 'deviceId'); assert.propertyVal(stored, 'userId', userId); assert.lengthOf(stored.deviceId, 37); // increase deviceId length by 1 for 'R' character }); it('should set language', function() { - amplitude.getInstance().init(apiKey, userId); - assert.property(amplitude.getInstance().options, 'language'); - assert.isNotNull(amplitude.getInstance().options.language); + amplitude.init(apiKey, userId); + assert.property(amplitude.options, 'language'); + assert.isNotNull(amplitude.options.language); }); it('should allow language override', function() { - amplitude.getInstance().init(apiKey, userId, {language: 'en-GB'}); - assert.propertyVal(amplitude.getInstance().options, 'language', 'en-GB'); + amplitude.init(apiKey, userId, {language: 'en-GB'}); + assert.propertyVal(amplitude.options, 'language', 'en-GB'); }); it ('should not run callback if invalid callback', function() { - amplitude.getInstance().init(apiKey, userId, null, 'invalid callback'); + amplitude.init(apiKey, userId, null, 'invalid callback'); }); it ('should run valid callbacks', function() { @@ -300,7 +300,7 @@ describe('Amplitude', function() { var callback = function() { counter++; }; - amplitude.getInstance().init(apiKey, userId, null, callback); + amplitude.init(apiKey, userId, null, callback); assert.equal(counter, 1); }); @@ -308,17 +308,17 @@ describe('Amplitude', function() { var deviceId = 'test_device_id'; var userId = 'test_user_id'; - assert.isNull(cookie.get(amplitude.getInstance().options.cookieName)); + assert.isNull(cookie.get(amplitude.options.cookieName)); localStorage.setItem('amplitude_deviceId' + keySuffix, deviceId); localStorage.setItem('amplitude_userId' + keySuffix, userId); localStorage.setItem('amplitude_optOut' + keySuffix, true); - amplitude.getInstance().init(apiKey); - assert.equal(amplitude.getInstance().options.deviceId, deviceId); - assert.equal(amplitude.getInstance().options.userId, userId); - assert.isTrue(amplitude.getInstance().options.optOut); + amplitude.init(apiKey); + assert.equal(amplitude.options.deviceId, deviceId); + assert.equal(amplitude.options.userId, userId); + assert.isTrue(amplitude.options.optOut); - var cookieData = cookie.get(amplitude.getInstance().options.cookieName); + var cookieData = cookie.get(amplitude.options.cookieName); assert.equal(cookieData.deviceId, deviceId); assert.equal(cookieData.userId, userId); assert.isTrue(cookieData.optOut); @@ -327,14 +327,14 @@ describe('Amplitude', function() { it('should migrate session and event info from localStorage to cookie', function() { var now = new Date().getTime(); - assert.isNull(cookie.get(amplitude.getInstance().options.cookieName)); + assert.isNull(cookie.get(amplitude.options.cookieName)); localStorage.setItem('amplitude_sessionId', now); localStorage.setItem('amplitude_lastEventTime', now); localStorage.setItem('amplitude_lastEventId', 3000); localStorage.setItem('amplitude_lastIdentifyId', 4000); localStorage.setItem('amplitude_lastSequenceNumber', 5000); - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); assert.equal(amplitude.getInstance()._sessionId, now); assert.isTrue(amplitude.getInstance()._lastEventTime >= now); @@ -342,7 +342,7 @@ describe('Amplitude', function() { assert.equal(amplitude.getInstance()._identifyId, 4000); assert.equal(amplitude.getInstance()._sequenceNumber, 5000); - var cookieData = cookie.get(amplitude.getInstance().options.cookieName); + var cookieData = cookie.get(amplitude.options.cookieName); assert.equal(cookieData.sessionId, now); assert.equal(cookieData.lastEventTime, amplitude.getInstance()._lastEventTime); assert.equal(cookieData.eventId, 3000); @@ -363,7 +363,7 @@ describe('Amplitude', function() { identifyId: 60 } - cookie.set(amplitude.getInstance().options.cookieName, cookieData); + cookie.set(amplitude.options.cookieName, cookieData); localStorage.setItem('amplitude_deviceId' + keySuffix, 'old_device_id'); localStorage.setItem('amplitude_userId' + keySuffix, 'fake_user_id'); localStorage.setItem('amplitude_optOut' + keySuffix, true); @@ -373,10 +373,10 @@ describe('Amplitude', function() { localStorage.setItem('amplitude_lastIdentifyId', 30); localStorage.setItem('amplitude_lastSequenceNumber', 40); - amplitude.getInstance().init(apiKey); - assert.equal(amplitude.getInstance().options.deviceId, 'old_device_id'); - assert.equal(amplitude.getInstance().options.userId, 'test_user_id'); - assert.isFalse(amplitude.getInstance().options.optOut); + amplitude.init(apiKey); + assert.equal(amplitude.options.deviceId, 'old_device_id'); + assert.equal(amplitude.options.userId, 'test_user_id'); + assert.isFalse(amplitude.options.optOut); assert.equal(amplitude.getInstance()._sessionId, now); assert.isTrue(amplitude.getInstance()._lastEventTime >= now); assert.equal(amplitude.getInstance()._eventId, 50); @@ -387,7 +387,7 @@ describe('Amplitude', function() { it('should skip the migration if the new cookie already has deviceId, sessionId, lastEventTime', function() { var now = new Date().getTime(); - cookie.set(amplitude.getInstance().options.cookieName, { + cookie.set(amplitude.options.cookieName, { deviceId: 'new_device_id', sessionId: now, lastEventTime: now @@ -402,10 +402,10 @@ describe('Amplitude', function() { localStorage.setItem('amplitude_lastIdentifyId', 30); localStorage.setItem('amplitude_lastSequenceNumber', 40); - amplitude.getInstance().init(apiKey, 'new_user_id'); - assert.equal(amplitude.getInstance().options.deviceId, 'new_device_id'); - assert.equal(amplitude.getInstance().options.userId, 'new_user_id'); - assert.isFalse(amplitude.getInstance().options.optOut); + amplitude.init(apiKey, 'new_user_id'); + assert.equal(amplitude.options.deviceId, 'new_device_id'); + assert.equal(amplitude.options.userId, 'new_user_id'); + assert.isFalse(amplitude.options.optOut); assert.isTrue(amplitude.getInstance()._sessionId >= now); assert.isTrue(amplitude.getInstance()._lastEventTime >= now); assert.equal(amplitude.getInstance()._eventId, 0); @@ -664,7 +664,7 @@ describe('Amplitude', function() { describe('runQueuedFunctions', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -682,9 +682,9 @@ describe('Amplitude', function() { ]; amplitude.getInstance()._q = functions; assert.lengthOf(amplitude.getInstance()._q, 2); - amplitude.getInstance().runQueuedFunctions(); + amplitude.runQueuedFunctions(); - assert.equal(amplitude.getInstance().options.userId, userId); + assert.equal(amplitude.options.userId, userId); assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -697,7 +697,7 @@ describe('Amplitude', function() { describe('setUserProperties', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -706,7 +706,7 @@ describe('Amplitude', function() { it('should log identify call from set user properties', function() { assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().setUserProperties({'prop': true, 'key': 'value'}); + amplitude.setUserProperties({'prop': true, 'key': 'value'}); assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); @@ -729,7 +729,7 @@ describe('Amplitude', function() { describe('clearUserProperties', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -738,7 +738,7 @@ describe('Amplitude', function() { it('should log identify call from clear user properties', function() { assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().clearUserProperties(); + amplitude.clearUserProperties(); assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); @@ -759,7 +759,7 @@ describe('Amplitude', function() { describe('setGroup', function() { beforeEach(function() { reset(); - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -767,7 +767,7 @@ describe('Amplitude', function() { }); it('should generate an identify event with groups set', function() { - amplitude.getInstance().setGroup('orgId', 15); + amplitude.setGroup('orgId', 15); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -785,15 +785,15 @@ describe('Amplitude', function() { }); it('should ignore empty string groupTypes', function() { - amplitude.getInstance().setGroup('', 15); + amplitude.setGroup('', 15); assert.lengthOf(server.requests, 0); }); it('should ignore non-string groupTypes', function() { - amplitude.getInstance().setGroup(10, 10); - amplitude.getInstance().setGroup([], 15); - amplitude.getInstance().setGroup({}, 20); - amplitude.getInstance().setGroup(true, false); + amplitude.setGroup(10, 10); + amplitude.setGroup([], 15); + amplitude.setGroup({}, 20); + amplitude.setGroup(true, false); assert.lengthOf(server.requests, 0); }); }); @@ -809,14 +809,14 @@ describe('setVersionName', function() { }); it('should set version name', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true}); - amplitude.getInstance().setVersionName('testVersionName1'); - amplitude.getInstance().logEvent('testEvent1'); + amplitude.init(apiKey, null, {batchEvents: true}); + amplitude.setVersionName('testVersionName1'); + amplitude.logEvent('testEvent1'); assert.equal(amplitude.getInstance()._unsentEvents[0].version_name, 'testVersionName1'); // should ignore non-string values - amplitude.getInstance().setVersionName(15000); - amplitude.getInstance().logEvent('testEvent2'); + amplitude.setVersionName(15000); + amplitude.logEvent('testEvent2'); assert.equal(amplitude.getInstance()._unsentEvents[1].version_name, 'testVersionName1'); }); }); @@ -832,11 +832,11 @@ describe('setVersionName', function() { it('should regenerate the deviceId', function() { var deviceId = 'oldDeviceId'; - amplitude.getInstance().init(apiKey, null, {'deviceId': deviceId}); - amplitude.getInstance().regenerateDeviceId(); - assert.notEqual(amplitude.getInstance().options.deviceId, deviceId); - assert.lengthOf(amplitude.getInstance().options.deviceId, 37); - assert.equal(amplitude.getInstance().options.deviceId[36], 'R'); + amplitude.init(apiKey, null, {'deviceId': deviceId}); + amplitude.regenerateDeviceId(); + assert.notEqual(amplitude.options.deviceId, deviceId); + assert.lengthOf(amplitude.options.deviceId, 37); + assert.equal(amplitude.options.deviceId[36], 'R'); }); }); @@ -851,29 +851,29 @@ describe('setVersionName', function() { }); it('should change device id', function() { - amplitude.getInstance().init(apiKey, null, {'deviceId': 'fakeDeviceId'}); - amplitude.getInstance().setDeviceId('deviceId'); - assert.equal(amplitude.getInstance().options.deviceId, 'deviceId'); + amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.setDeviceId('deviceId'); + assert.equal(amplitude.options.deviceId, 'deviceId'); }); it('should not change device id if empty', function() { - amplitude.getInstance().init(apiKey, null, {'deviceId': 'deviceId'}); - amplitude.getInstance().setDeviceId(''); - assert.notEqual(amplitude.getInstance().options.deviceId, ''); - assert.equal(amplitude.getInstance().options.deviceId, 'deviceId'); + amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.setDeviceId(''); + assert.notEqual(amplitude.options.deviceId, ''); + assert.equal(amplitude.options.deviceId, 'deviceId'); }); it('should not change device id if null', function() { - amplitude.getInstance().init(apiKey, null, {'deviceId': 'deviceId'}); - amplitude.getInstance().setDeviceId(null); - assert.notEqual(amplitude.getInstance().options.deviceId, null); - assert.equal(amplitude.getInstance().options.deviceId, 'deviceId'); + amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.setDeviceId(null); + assert.notEqual(amplitude.options.deviceId, null); + assert.equal(amplitude.options.deviceId, 'deviceId'); }); it('should store device id in cookie', function() { - amplitude.getInstance().init(apiKey, null, {'deviceId': 'fakeDeviceId'}); - amplitude.getInstance().setDeviceId('deviceId'); - var stored = cookie.get(amplitude.getInstance().options.cookieName); + amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.setDeviceId('deviceId'); + var stored = cookie.get(amplitude.options.cookieName); assert.propertyVal(stored, 'deviceId', 'deviceId'); }); }); @@ -882,7 +882,7 @@ describe('setVersionName', function() { beforeEach(function() { clock = sinon.useFakeTimers(); - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -891,26 +891,26 @@ describe('setVersionName', function() { }); it('should ignore inputs that are not identify objects', function() { - amplitude.getInstance().identify('This is a test'); + amplitude.identify('This is a test'); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.getInstance().identify(150); + amplitude.identify(150); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.getInstance().identify(['test']); + amplitude.identify(['test']); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.getInstance().identify({'user_prop': true}); + amplitude.identify({'user_prop': true}); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); it('should generate an event from the identify object', function() { var identify = new Identify().set('prop1', 'value1').unset('prop2').add('prop3', 3).setOnce('prop4', true); - amplitude.getInstance().identify(identify); + amplitude.identify(identify); assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); @@ -937,17 +937,17 @@ describe('setVersionName', function() { }); it('should ignore empty identify objects', function() { - amplitude.getInstance().identify(new Identify()); + amplitude.identify(new Identify()); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); it('should ignore empty proxy identify objects', function() { - amplitude.getInstance().identify({'_q': {}}); + amplitude.identify({'_q': {}}); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); - amplitude.getInstance().identify({}); + amplitude.identify({}); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); @@ -961,7 +961,7 @@ describe('setVersionName', function() { ['set', 'key4', 'value5'], ['prepend', 'key5', 'value6'] ]}; - amplitude.getInstance().identify(proxyObject); + amplitude.identify(proxyObject); assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); @@ -989,7 +989,7 @@ describe('setVersionName', function() { message = response; } var identify = new amplitude.Identify().set('key', 'value'); - amplitude.getInstance().identify(identify, callback); + amplitude.identify(identify, callback); // before server responds, callback should not fire assert.lengthOf(server.requests, 1); @@ -1032,7 +1032,7 @@ describe('setVersionName', function() { value = status; message = response; } - amplitude.getInstance().identify(null, callback); + amplitude.identify(null, callback); // verify callback fired assert.equal(counter, 1); @@ -1047,7 +1047,7 @@ describe('setVersionName', function() { beforeEach(function() { clock = sinon.useFakeTimers(); - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -1056,7 +1056,7 @@ describe('setVersionName', function() { }); it('should send request', function() { - amplitude.getInstance().logEvent('Event Type 1'); + amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); assert.equal(server.requests[0].url, 'http://api.amplitude.com/'); assert.equal(server.requests[0].method, 'POST'); @@ -1064,24 +1064,24 @@ describe('setVersionName', function() { }); it('should reject empty event types', function() { - amplitude.getInstance().logEvent(); + amplitude.logEvent(); assert.lengthOf(server.requests, 0); }); it('should send api key', function() { - amplitude.getInstance().logEvent('Event Type 2'); + amplitude.logEvent('Event Type 2'); assert.lengthOf(server.requests, 1); assert.equal(querystring.parse(server.requests[0].requestBody).client, apiKey); }); it('should send api version', function() { - amplitude.getInstance().logEvent('Event Type 3'); + amplitude.logEvent('Event Type 3'); assert.lengthOf(server.requests, 1); assert.equal(querystring.parse(server.requests[0].requestBody).v, '2'); }); it('should send event JSON', function() { - amplitude.getInstance().logEvent('Event Type 4'); + amplitude.logEvent('Event Type 4'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); @@ -1089,7 +1089,7 @@ describe('setVersionName', function() { }); it('should send language', function() { - amplitude.getInstance().logEvent('Event Should Send Language'); + amplitude.logEvent('Event Should Send Language'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); @@ -1097,7 +1097,7 @@ describe('setVersionName', function() { }); it('should accept properties', function() { - amplitude.getInstance().logEvent('Event Type 5', {prop: true}); + amplitude.logEvent('Event Type 5', {prop: true}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.deepEqual(events[0].event_properties, {prop: true}); @@ -1105,12 +1105,12 @@ describe('setVersionName', function() { it('should queue events', function() { amplitude.getInstance()._sending = true; - amplitude.getInstance().logEvent('Event', {index: 1}); - amplitude.getInstance().logEvent('Event', {index: 2}); - amplitude.getInstance().logEvent('Event', {index: 3}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 100}); + amplitude.logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1120,15 +1120,15 @@ describe('setVersionName', function() { }); it('should limit events queued', function() { - amplitude.getInstance().init(apiKey, null, {savedMaxCount: 10}); + amplitude.init(apiKey, null, {savedMaxCount: 10}); amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 100}); + amplitude.logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1139,15 +1139,15 @@ describe('setVersionName', function() { it('should remove only sent events', function() { amplitude.getInstance()._sending = true; - amplitude.getInstance().logEvent('Event', {index: 1}); - amplitude.getInstance().logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 3}); + amplitude.logEvent('Event', {index: 3}); server.respondWith('success'); server.respond(); - amplitude.getInstance().logEvent('Event', {index: 4}); + amplitude.logEvent('Event', {index: 4}); assert.lengthOf(server.requests, 2); var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); @@ -1156,10 +1156,10 @@ describe('setVersionName', function() { }); it('should save events', function() { - amplitude.getInstance().init(apiKey, null, {saveEvents: true}); - amplitude.getInstance().logEvent('Event', {index: 1}); - amplitude.getInstance().logEvent('Event', {index: 2}); - amplitude.getInstance().logEvent('Event', {index: 3}); + amplitude.init(apiKey, null, {saveEvents: true}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); @@ -1167,10 +1167,10 @@ describe('setVersionName', function() { }); it('should not save events', function() { - amplitude.getInstance().init(apiKey, null, {saveEvents: false}); - amplitude.getInstance().logEvent('Event', {index: 1}); - amplitude.getInstance().logEvent('Event', {index: 2}); - amplitude.getInstance().logEvent('Event', {index: 3}); + amplitude.init(apiKey, null, {saveEvents: false}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); @@ -1178,15 +1178,15 @@ describe('setVersionName', function() { }); it('should limit events sent', function() { - amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 10}); + amplitude.init(apiKey, null, {uploadBatchSize: 10}); amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 100}); + amplitude.logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1206,14 +1206,14 @@ describe('setVersionName', function() { it('should batch events sent', function() { var eventUploadPeriodMillis = 10*1000; - amplitude.getInstance().init(apiKey, null, { + amplitude.init(apiKey, null, { batchEvents: true, eventUploadThreshold: 10, eventUploadPeriodMillis: eventUploadPeriodMillis }); for (var i = 0; i < 15; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } assert.lengthOf(server.requests, 1); @@ -1243,12 +1243,12 @@ describe('setVersionName', function() { it('should send events after a delay', function() { var eventUploadPeriodMillis = 10*1000; - amplitude.getInstance().init(apiKey, null, { + amplitude.init(apiKey, null, { batchEvents: true, eventUploadThreshold: 2, eventUploadPeriodMillis: eventUploadPeriodMillis }); - amplitude.getInstance().logEvent('Event'); + amplitude.logEvent('Event'); // saveEvent should not have been called yet assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); @@ -1266,13 +1266,13 @@ describe('setVersionName', function() { it('should not send events after a delay if no events to send', function() { var eventUploadPeriodMillis = 10*1000; - amplitude.getInstance().init(apiKey, null, { + amplitude.init(apiKey, null, { batchEvents: true, eventUploadThreshold: 2, eventUploadPeriodMillis: eventUploadPeriodMillis }); - amplitude.getInstance().logEvent('Event1'); - amplitude.getInstance().logEvent('Event2'); + amplitude.logEvent('Event1'); + amplitude.logEvent('Event2'); // saveEvent triggered by 2 event batch threshold assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); @@ -1291,16 +1291,16 @@ describe('setVersionName', function() { it('should not schedule more than one upload', function() { var eventUploadPeriodMillis = 5*1000; // 5s - amplitude.getInstance().init(apiKey, null, { + amplitude.init(apiKey, null, { batchEvents: true, eventUploadThreshold: 30, eventUploadPeriodMillis: eventUploadPeriodMillis }); // log 2 events, 1 millisecond apart, second event should not schedule upload - amplitude.getInstance().logEvent('Event1'); + amplitude.logEvent('Event1'); clock.tick(1); - amplitude.getInstance().logEvent('Event2'); + amplitude.logEvent('Event2'); assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); assert.lengthOf(server.requests, 0); @@ -1312,7 +1312,7 @@ describe('setVersionName', function() { server.respond(); // log 3rd event, advance 1 more millisecond, verify no 2nd server request - amplitude.getInstance().logEvent('Event3'); + amplitude.logEvent('Event3'); clock.tick(1); assert.lengthOf(server.requests, 1); @@ -1324,15 +1324,15 @@ describe('setVersionName', function() { }); it('should back off on 413 status', function() { - amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 10}); + amplitude.init(apiKey, null, {uploadBatchSize: 10}); amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 100}); + amplitude.logEvent('Event', {index: 100}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1351,14 +1351,14 @@ describe('setVersionName', function() { }); it('should back off on 413 status all the way to 1 event with drops', function() { - amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 9}); + amplitude.init(apiKey, null, {uploadBatchSize: 9}); amplitude.getInstance()._sending = true; for (var i = 0; i < 10; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 100}); + amplitude.logEvent('Event', {index: 100}); for (var i = 0; i < 6; i++) { assert.lengthOf(server.requests, i+1); @@ -1380,14 +1380,14 @@ describe('setVersionName', function() { value = status; message = response; } - amplitude.getInstance().logEvent(null, null, callback); + amplitude.logEvent(null, null, callback); assert.equal(counter, 1); assert.equal(value, 0); assert.equal(message, 'No request sent'); }); it ('should run callback if optout', function () { - amplitude.getInstance().setOptOut(true); + amplitude.setOptOut(true); var counter = 0; var value = -1; var message = ''; @@ -1396,14 +1396,14 @@ describe('setVersionName', function() { value = status; message = response; }; - amplitude.getInstance().logEvent('test', null, callback); + amplitude.logEvent('test', null, callback); assert.equal(counter, 1); assert.equal(value, 0); assert.equal(message, 'No request sent'); }); it ('should not run callback if invalid callback and no eventType', function () { - amplitude.getInstance().logEvent(null, null, 'invalid callback'); + amplitude.logEvent(null, null, 'invalid callback'); }); it ('should run callback after logging event', function () { @@ -1415,7 +1415,7 @@ describe('setVersionName', function() { value = status; message = response; }; - amplitude.getInstance().logEvent('test', null, callback); + amplitude.logEvent('test', null, callback); // before server responds, callback should not fire assert.lengthOf(server.requests, 1); @@ -1433,7 +1433,7 @@ describe('setVersionName', function() { it ('should run callback if batchEvents but under threshold', function () { var eventUploadPeriodMillis = 5*1000; - amplitude.getInstance().init(apiKey, null, { + amplitude.init(apiKey, null, { batchEvents: true, eventUploadThreshold: 2, eventUploadPeriodMillis: eventUploadPeriodMillis @@ -1446,7 +1446,7 @@ describe('setVersionName', function() { value = status; message = response; }; - amplitude.getInstance().logEvent('test', null, callback); + amplitude.logEvent('test', null, callback); assert.lengthOf(server.requests, 0); assert.equal(counter, 1); assert.equal(value, 0); @@ -1461,7 +1461,7 @@ describe('setVersionName', function() { }); it ('should run callback once and only after all events are uploaded', function () { - amplitude.getInstance().init(apiKey, null, {uploadBatchSize: 10}); + amplitude.init(apiKey, null, {uploadBatchSize: 10}); var counter = 0; var value = -1; var message = ''; @@ -1474,11 +1474,11 @@ describe('setVersionName', function() { // queue up 15 events, since batchsize 10, need to send in 2 batches amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } amplitude.getInstance()._sending = false; - amplitude.getInstance().logEvent('Event', {index: 100}, callback); + amplitude.logEvent('Event', {index: 100}, callback); assert.lengthOf(server.requests, 1); server.respondWith('success'); @@ -1512,12 +1512,12 @@ describe('setVersionName', function() { // queue up 15 events amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.getInstance().logEvent('Event', {index: i}); + amplitude.logEvent('Event', {index: i}); } amplitude.getInstance()._sending = false; // 16th event with 413 will backoff to batches of 8 - amplitude.getInstance().logEvent('Event', {index: 100}, callback); + amplitude.logEvent('Event', {index: 100}, callback); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1561,7 +1561,7 @@ describe('setVersionName', function() { message = response; }; - amplitude.getInstance().logEvent('test', null, callback); + amplitude.logEvent('test', null, callback); server.respondWith([404, {}, 'Not found']); server.respond(); assert.equal(counter, 1); @@ -1570,12 +1570,12 @@ describe('setVersionName', function() { }); it('should send 3 identify events', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); - amplitude.getInstance().identify(new Identify().add('photoCount', 1).set('country', 'USA')); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.identify(new Identify().add('photoCount', 1).set('country', 'USA')); + amplitude.identify(new Identify().add('photoCount', 1)); // verify some internal counters assert.equal(amplitude.getInstance()._eventId, 0); @@ -1603,12 +1603,12 @@ describe('setVersionName', function() { }); it('should send 3 events', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().logEvent('test'); - amplitude.getInstance().logEvent('test'); - amplitude.getInstance().logEvent('test'); + amplitude.logEvent('test'); + amplitude.logEvent('test'); + amplitude.logEvent('test'); // verify some internal counters assert.equal(amplitude.getInstance()._eventId, 3); @@ -1634,11 +1634,11 @@ describe('setVersionName', function() { }); it('should send 1 event and 1 identify event', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().logEvent('test'); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.logEvent('test'); + amplitude.identify(new Identify().add('photoCount', 1)); // verify some internal counters assert.equal(amplitude.getInstance()._eventId, 1); @@ -1671,19 +1671,19 @@ describe('setVersionName', function() { }); it('should properly coalesce events and identify events into a request', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().logEvent('test1'); + amplitude.logEvent('test1'); clock.tick(1); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.identify(new Identify().add('photoCount', 1)); clock.tick(1); - amplitude.getInstance().logEvent('test2'); + amplitude.logEvent('test2'); clock.tick(1); - amplitude.getInstance().logEvent('test3'); + amplitude.logEvent('test3'); clock.tick(1); - amplitude.getInstance().logEvent('test4'); - amplitude.getInstance().identify(new Identify().add('photoCount', 2)); + amplitude.logEvent('test4'); + amplitude.identify(new Identify().add('photoCount', 2)); // verify some internal counters assert.equal(amplitude.getInstance()._eventId, 4); @@ -1728,14 +1728,14 @@ describe('setVersionName', function() { it('should merged events supporting backwards compatability', function() { // events logged before v2.5.0 won't have sequence number, should get priority - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); assert.equal(amplitude.getInstance()._unsentCount(), 0); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); - amplitude.getInstance().logEvent('test'); + amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.logEvent('test'); delete amplitude.getInstance()._unsentEvents[0].sequence_number; // delete sequence number to simulate old event amplitude.getInstance()._sequenceNumber = 1; // reset sequence number - amplitude.getInstance().identify(new Identify().add('photoCount', 2)); + amplitude.identify(new Identify().add('photoCount', 2)); // verify some internal counters assert.equal(amplitude.getInstance()._eventId, 1); @@ -1775,10 +1775,10 @@ describe('setVersionName', function() { }); it('should drop event and keep identify on 413 response', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); - amplitude.getInstance().logEvent('test'); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('test'); clock.tick(1); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.identify(new Identify().add('photoCount', 1)); assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 1); @@ -1786,14 +1786,14 @@ describe('setVersionName', function() { server.respond(); // backoff and retry - assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.options.uploadBatchSize, 1); assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 2); server.respondWith([413, {}, '']); server.respond(); // after dropping massive event, only 1 event left - assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.options.uploadBatchSize, 1); assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 3); @@ -1805,10 +1805,10 @@ describe('setVersionName', function() { }); it('should drop identify if 413 and uploadBatchSize is 1', function() { - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); - amplitude.getInstance().identify(new Identify().add('photoCount', 1)); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.identify(new Identify().add('photoCount', 1)); clock.tick(1); - amplitude.getInstance().logEvent('test'); + amplitude.logEvent('test'); assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 1); @@ -1816,14 +1816,14 @@ describe('setVersionName', function() { server.respond(); // backoff and retry - assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.options.uploadBatchSize, 1); assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 2); server.respondWith([413, {}, '']); server.respond(); // after dropping massive event, only 1 event left - assert.equal(amplitude.getInstance().options.uploadBatchSize, 1); + assert.equal(amplitude.options.uploadBatchSize, 1); assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 3); @@ -1835,7 +1835,7 @@ describe('setVersionName', function() { it('should truncate long event property strings', function() { var longString = new Array(5000).join('a'); - amplitude.getInstance().logEvent('test', {'key': longString}); + amplitude.logEvent('test', {'key': longString}); var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; assert.isTrue('key' in event.event_properties); @@ -1844,7 +1844,7 @@ describe('setVersionName', function() { it('should truncate long user property strings', function() { var longString = new Array(5000).join('a'); - amplitude.getInstance().identify(new Identify().set('key', longString)); + 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); @@ -1882,17 +1882,17 @@ describe('setVersionName', function() { it('should validate event properties', function() { var e = new Error('oops'); clock.tick(1); - amplitude.getInstance().init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); clock.tick(1); - amplitude.getInstance().logEvent('String event properties', '{}'); + amplitude.logEvent('String event properties', '{}'); clock.tick(1); - amplitude.getInstance().logEvent('Bool event properties', true); + amplitude.logEvent('Bool event properties', true); clock.tick(1); - amplitude.getInstance().logEvent('Number event properties', 15); + amplitude.logEvent('Number event properties', 15); clock.tick(1); - amplitude.getInstance().logEvent('Array event properties', [1, 2, 3]); + amplitude.logEvent('Array event properties', [1, 2, 3]); clock.tick(1); - amplitude.getInstance().logEvent('Object event properties', { + amplitude.logEvent('Object event properties', { 10: 'false', // coerce key 'bool': true, 'null': null, // should be ignored @@ -1930,8 +1930,8 @@ describe('setVersionName', function() { it('should validate user propeorties', function() { var identify = new Identify().set(10, 10); - amplitude.getInstance().init(apiKey, null, {batchEvents: true}); - amplitude.getInstance().identify(identify); + amplitude.init(apiKey, null, {batchEvents: true}); + amplitude.identify(identify); assert.deepEqual(amplitude.getInstance()._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); }); @@ -1982,7 +1982,7 @@ describe('setVersionName', function() { 'null': null, // ignore null values } - amplitude.getInstance().logEventWithGroups('Test', eventProperties, groups, callback); + amplitude.logEventWithGroups('Test', eventProperties, groups, callback); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -2012,7 +2012,7 @@ describe('setVersionName', function() { describe('optOut', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -2020,28 +2020,28 @@ describe('setVersionName', function() { }); it('should not send events while enabled', function() { - amplitude.getInstance().setOptOut(true); - amplitude.getInstance().logEvent('Event Type 1'); + amplitude.setOptOut(true); + amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 0); }); it('should not send saved events while enabled', function() { - amplitude.getInstance().logEvent('Event Type 1'); + amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); amplitude.getInstance()._sending = false; - amplitude.getInstance().setOptOut(true); - amplitude.getInstance().init(apiKey); + amplitude.setOptOut(true); + amplitude.init(apiKey); assert.lengthOf(server.requests, 1); }); it('should start sending events again when disabled', function() { - amplitude.getInstance().setOptOut(true); - amplitude.getInstance().logEvent('Event Type 1'); + amplitude.setOptOut(true); + amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 0); - amplitude.getInstance().setOptOut(false); - amplitude.getInstance().logEvent('Event Type 1'); + amplitude.setOptOut(false); + amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -2050,10 +2050,10 @@ describe('setVersionName', function() { it('should have state be persisted in the cookie', function() { var amplitude = new Amplitude(); - amplitude.getInstance().init(apiKey); - assert.strictEqual(amplitude.getInstance().options.optOut, false); + amplitude.init(apiKey); + assert.strictEqual(amplitude.options.optOut, false); - amplitude.getInstance().setOptOut(true); + amplitude.setOptOut(true); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); @@ -2061,15 +2061,15 @@ describe('setVersionName', function() { }); it('should limit identify events queued', function() { - amplitude.getInstance().init(apiKey, null, {savedMaxCount: 10}); + amplitude.init(apiKey, null, {savedMaxCount: 10}); amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { - amplitude.getInstance().identify(new Identify().add('test', i)); + amplitude.identify(new Identify().add('test', i)); } amplitude.getInstance()._sending = false; - amplitude.getInstance().identify(new Identify().add('test', 100)); + amplitude.identify(new Identify().add('test', 100)); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 10); @@ -2080,7 +2080,7 @@ describe('setVersionName', function() { describe('gatherUtm', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -2090,9 +2090,9 @@ describe('setVersionName', function() { it('should not send utm data when the includeUtm flag is false', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); reset(); - amplitude.getInstance().init(apiKey, undefined, {}); + amplitude.init(apiKey, undefined, {}); - amplitude.getInstance().setUserProperties({user_prop: true}); + amplitude.setUserProperties({user_prop: true}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events[0].user_properties.utm_campaign, undefined); @@ -2105,9 +2105,9 @@ describe('setVersionName', 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.getInstance().init(apiKey, undefined, {includeUtm: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.init(apiKey, undefined, {includeUtm: true, batchEvents: true, eventUploadThreshold: 2}); - amplitude.getInstance().logEvent('UTM Test Event', {}); + amplitude.logEvent('UTM Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -2157,7 +2157,7 @@ describe('setVersionName', function() { server.respondWith('success'); server.respond(); - amplitude.getInstance().logEvent('UTM Test Event', {}); + 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, {}); @@ -2204,8 +2204,8 @@ describe('setVersionName', function() { describe('gatherReferrer', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); - sinon.stub(amplitude.getInstance(), '_getReferrer').returns('https://amplitude.getInstance().com/contact'); + amplitude.init(apiKey); + sinon.stub(amplitude.getInstance(), '_getReferrer').returns('https://amplitude.com/contact'); }); afterEach(function() { @@ -2214,9 +2214,9 @@ describe('setVersionName', function() { }); it('should not send referrer data when the includeReferrer flag is false', function() { - amplitude.getInstance().init(apiKey, undefined, {}); + amplitude.init(apiKey, undefined, {}); - amplitude.getInstance().setUserProperties({user_prop: true}); + amplitude.setUserProperties({user_prop: true}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events[0].user_properties.referrer, undefined); @@ -2225,15 +2225,15 @@ describe('setVersionName', function() { it('should only send referrer via identify call when the includeReferrer flag is true', function() { reset(); - amplitude.getInstance().init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); - amplitude.getInstance().logEvent('Referrer Test Event', {}); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('Referrer Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 2); var expected = { - 'referrer': 'https://amplitude.getInstance().com/contact', - 'referring_domain': 'amplitude.getInstance().com' + 'referrer': 'https://amplitude.com/contact', + 'referring_domain': 'amplitude.com' }; // first event should be identify with initial_referrer and referrer @@ -2241,8 +2241,8 @@ describe('setVersionName', function() { assert.deepEqual(events[0].user_properties, { '$set': expected, '$setOnce': { - 'initial_referrer': 'https://amplitude.getInstance().com/contact', - 'initial_referring_domain': 'amplitude.getInstance().com' + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' } }); @@ -2257,8 +2257,8 @@ describe('setVersionName', function() { it('should not set referrer if referrer data already in session storage', function() { reset(); sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); - amplitude.getInstance().init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); - amplitude.getInstance().logEvent('Referrer Test Event', {}); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('Referrer Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 2); @@ -2267,8 +2267,8 @@ describe('setVersionName', function() { assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { '$setOnce': { - 'initial_referrer': 'https://amplitude.getInstance().com/contact', - 'initial_referring_domain': 'amplitude.getInstance().com' + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' } }); @@ -2280,9 +2280,9 @@ describe('setVersionName', function() { it('should not override any existing initial referrer values in session storage', function() { reset(); sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); - amplitude.getInstance().init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); amplitude.getInstance()._saveReferrer('https://facebook.com/contact'); - amplitude.getInstance().logEvent('Referrer Test Event', {}); + amplitude.logEvent('Referrer Test Event', {}); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 3); @@ -2291,8 +2291,8 @@ describe('setVersionName', function() { assert.equal(events[0].event_type, '$identify'); assert.deepEqual(events[0].user_properties, { '$setOnce': { - 'initial_referrer': 'https://amplitude.getInstance().com/contact', - 'initial_referring_domain': 'amplitude.getInstance().com' + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' } }); @@ -2316,7 +2316,7 @@ describe('setVersionName', function() { describe('logRevenue', function() { beforeEach(function() { - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -2335,7 +2335,7 @@ describe('setVersionName', function() { } it('should log simple amount', function() { - amplitude.getInstance().logRevenue(10.10); + amplitude.logRevenue(10.10); revenueEqual({ special: 'revenue_amount', price: 10.10, @@ -2344,7 +2344,7 @@ describe('setVersionName', function() { }); it('should log complex amount', function() { - amplitude.getInstance().logRevenue(10.10, 7); + amplitude.logRevenue(10.10, 7); revenueEqual({ special: 'revenue_amount', price: 10.10, @@ -2353,17 +2353,17 @@ describe('setVersionName', function() { }); it('shouldn\'t log invalid price', function() { - amplitude.getInstance().logRevenue('kitten', 7); + amplitude.logRevenue('kitten', 7); assert.lengthOf(server.requests, 0); }); it('shouldn\'t log invalid quantity', function() { - amplitude.getInstance().logRevenue(10.00, 'puppy'); + amplitude.logRevenue(10.00, 'puppy'); assert.lengthOf(server.requests, 0); }); it('should log complex amount with product id', function() { - amplitude.getInstance().logRevenue(10.10, 7, 'chicken.dinner'); + amplitude.logRevenue(10.10, 7, 'chicken.dinner'); revenueEqual({ special: 'revenue_amount', price: 10.10, @@ -2376,7 +2376,7 @@ describe('setVersionName', function() { describe('logRevenueV2', function() { beforeEach(function() { reset(); - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -2385,11 +2385,11 @@ describe('setVersionName', function() { it('should log with the Revenue object', function () { // ignore invalid revenue objects - amplitude.getInstance().logRevenueV2(null); + amplitude.logRevenueV2(null); assert.lengthOf(server.requests, 0); - amplitude.getInstance().logRevenueV2({}); + amplitude.logRevenueV2({}); assert.lengthOf(server.requests, 0); - amplitude.getInstance().logRevenueV2(new amplitude.Revenue()); + amplitude.logRevenueV2(new amplitude.Revenue()); // log valid revenue object var productId = 'testProductId'; @@ -2401,7 +2401,7 @@ describe('setVersionName', function() { var revenue = new amplitude.Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); revenue.setRevenueType(revenueType).setEventProperties(properties); - amplitude.getInstance().logRevenueV2(revenue); + amplitude.logRevenueV2(revenue); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); @@ -2429,7 +2429,7 @@ describe('setVersionName', function() { ['setQuantity', 10], ['setPrice', 'key1'] // invalid price type, this will fail to generate revenue event ]}; - amplitude.getInstance().logRevenueV2(fakeRevenue); + amplitude.logRevenueV2(fakeRevenue); assert.lengthOf(server.requests, 0); var proxyRevenue = {'_q':[ @@ -2438,7 +2438,7 @@ describe('setVersionName', function() { ['setPrice', 10.99], ['setRevenueType', 'purchase'] ]}; - amplitude.getInstance().logRevenueV2(proxyRevenue); + amplitude.logRevenueV2(proxyRevenue); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); var event = events[0]; @@ -2457,7 +2457,7 @@ describe('setVersionName', function() { var clock; beforeEach(function() { clock = sinon.useFakeTimers(); - amplitude.getInstance().init(apiKey); + amplitude.init(apiKey); }); afterEach(function() { @@ -2468,7 +2468,7 @@ describe('setVersionName', function() { it('should create new session IDs on timeout', function() { var sessionId = amplitude.getInstance()._sessionId; clock.tick(30 * 60 * 1000 + 1); - amplitude.getInstance().logEvent('Event Type 1'); + amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); From ba195e68db6794397c36756f0ff8ff62f224cbd9 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Sat, 21 May 2016 01:29:10 -0700 Subject: [PATCH 08/13] add amplitude client tests --- test/amplitude-client.js | 20 ++++++++++---------- test/tests.js | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/amplitude-client.js b/test/amplitude-client.js index c035ea67..9c3a0402 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -384,7 +384,7 @@ describe('AmplitudeClient', function() { localStorage.setItem('amplitude_unsent', existingEvent); localStorage.setItem('amplitude_unsent_identify', existingIdentify); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient('$default_Instance'); amplitude2.init(apiKey, null, {batchEvents: true}); // check event loaded into memory @@ -891,7 +891,7 @@ describe('setVersionName', function() { message = response; } var identify = new amplitude.Identify().set('key', 'value'); - new Amplitude().identify(identify, callback); + new AmplitudeClient().identify(identify, callback); // verify callback fired assert.equal(counter, 1); @@ -1037,7 +1037,7 @@ describe('setVersionName', function() { amplitude.logEvent('Event', {index: 2}); amplitude.logEvent('Event', {index: 3}); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey); assert.deepEqual(amplitude2._unsentEvents, amplitude._unsentEvents); }); @@ -1048,7 +1048,7 @@ describe('setVersionName', function() { amplitude.logEvent('Event', {index: 2}); amplitude.logEvent('Event', {index: 3}); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey); assert.deepEqual(amplitude2._unsentEvents, []); }); @@ -1730,7 +1730,7 @@ describe('setVersionName', function() { it('should increment the counters in local storage if cookies disabled', function() { localStorage.clear(); var deviceId = 'test_device_id'; - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); sinon.stub(CookieStorage.prototype, '_cookiesEnabled').returns(false); amplitude2.init(apiKey, null, {deviceId: deviceId, batchEvents: true, eventUploadThreshold: 5}); @@ -1814,9 +1814,9 @@ describe('setVersionName', function() { 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(); + var amplitude1 = new AmplitudeClient(); amplitude1.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); amplitude1.logEvent('test1'); @@ -1925,13 +1925,13 @@ describe('setVersionName', function() { }); it('should have state be persisted in the cookie', function() { - var amplitude = new Amplitude(); + var amplitude = new AmplitudeClient(); amplitude.init(apiKey); assert.strictEqual(amplitude.options.optOut, false); amplitude.setOptOut(true); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey); assert.strictEqual(amplitude2.options.optOut, true); }); @@ -2356,7 +2356,7 @@ describe('setVersionName', function() { it('should be fetched correctly by getSessionId', function() { var timestamp = 1000; clock.tick(timestamp); - var amplitude2 = new Amplitude(); + var amplitude2 = new AmplitudeClient(); amplitude2.init(apiKey); assert.equal(amplitude2._sessionId, timestamp); assert.equal(amplitude2.getSessionId(), timestamp); diff --git a/test/tests.js b/test/tests.js index e6e2b753..6dbe2eb7 100644 --- a/test/tests.js +++ b/test/tests.js @@ -8,5 +8,6 @@ require('./cookiestorage.js'); require('./utm.js'); require('./amplitude.js'); + require('./amplitude-client.js'); require('./utils.js'); require('./revenue.js'); From 9f7ba43427c74b8a8b2e9bd39055576e5d86a193 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Sat, 21 May 2016 15:54:44 -0700 Subject: [PATCH 09/13] update readme --- README.md | 152 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 112 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 0db18e0b..c479a935 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use if(!n._iq.hasOwnProperty(e)){n._iq[e]={_q:[]};v(n._iq[e])}return n._iq[e]};e.amplitude=n; })(window,document); - amplitude.init("YOUR_API_KEY_HERE"); + amplitude.getInstance().init("YOUR_API_KEY_HERE"); ``` @@ -34,11 +34,83 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use 4. To track an event anywhere on the page, call: ```javascript - amplitude.logEvent('EVENT_IDENTIFIER_HERE'); + amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE'); ``` 5. Events are uploaded immediately and saved to the browser's local storage until the server confirms the upload. After calling logEvent in your app, you will immediately see data appear on Amplitude. +# 3.0.0 Update and Logging Events to Multiple Amplitude Apps # + +Version 3.0.0 is a major update that brings support for logging events to multiple Amplitude apps (multiple API keys). **Note: this change is not 100% backwards compatible and may break on your setup.** See the subsection below on backwards compatibility. + +### API Changes and Backwards Compatibility ### + +The `amplitude` object now maintains one or more instances, where each instance has separate apiKey, userId, deviceId, and settings. Having separate instances allows for the logging of events to separate Amplitude apps. + +The most important API change is how you interact with the `amplitude` object. Before v3.0.0, you would directly call `amplitude.logEvent('EVENT_NAME')`. Now the preferred way is to call functions on an instance as follows: `amplitude.getInstance('INSTANCE_NAME').logEvent('EVENT_NAME')` This notation will be familiar to people who have used our iOS and Android SDKs. + +Most people upgrading to v3.0.0 will continue logging events to a single Amplitude app. To make this transition as smooth as possible, we try to maintain backwards compatibility for most things by having a `default instance`, which you can fetch by calling `amplitude.getInstance()` with no instance name. The code examples in this README have been updated to follow this use case. All of the existing event data, existing settings, and returning users (users who already have a deviceId and/or userId) will stay with the `default instance`. You should initialize the default instance with your existing apiKey. + +All of the *public* methods of `amplitude` should still work as expected, as they have all been mapped to their equivalent on the default instance. + +For example `amplitude.init('API_KEY')` should still work as it has been mapped to `amplitude.getInstance().init('API_KEY')`. + +Likewise `amplitude.logEvent('EVENT_NAME')` should still work as it has been mapped to `amplitude.getInstance().logEvent('EVENT_NAME')`. + +`amplitude.options` will still work and will map to `amplitude.getInstance().options`, if for example you were using it to access the deviceId. + +**Things that will break:** if you were accessing private properties on the `amplitude` object, those will no longer work, e.g. `amplitude._sessionId`, `amplitude._eventId`, etc. You will need to update those references to fetch from the default instance like so: `amplitude.getInstance()._sessionId` and `amplitude.getInstance()._eventId`, etc. + +### Logging Events to a Single Amplitude App / API Key (Preferred Method) ### + +If you want to continue logging events to a single Amplitude App (and a single API key), then you should call functions on the `default instance`, which you can fetch by calling `amplitude.getInstance()` with no instance name. Here is an example: + +```javascript +amplitude.getInstance().init('API_KEY'); +amplitude.getInstance().logEvent('EVENT_NAME'); +``` + +You can also assign instances to a variable and call functions on that variable like so: + +```javascript +var app = amplitude.getInstance(); +app.init('API_KEY'); +app.logEvent('EVENT_NAME'); +``` + +### Logging Events to Multiple Amplitude Apps ### + +If you want to log events to multiple Amplitude apps, you will need to have separate instances for each Amplitude app. As mentioned earlier, each instance will allow for completely independent apiKeys, userIds, deviceIds, and settings. + +You need to assign a name to each Amplitude app / instance, and use that name consistently when fetching that instance to call functions. **IMPORTANT: Once you have chosen a name for that instance you cannot change it.** Every instance's data and settings are tied to its name, and you will need to continue using that instance name for all future versions of your app to maintain data continuity, so chose your instance names wisely. Note these names do not need to be the names of your apps in the Amplitude dashboards, but they need to remain consistent throughout your code. You also need to be sure that each instance is initialized with the correct apiKey. + +Instance names must be nonnull and nonempty strings. Names are case-insensitive. You can fetch each instance by name by calling `amplitude.getInstance('INSTANCE_NAME')`. + +As mentioned before, each new instance created will have its own apiKey, userId, deviceId, and settings. **You will have to reconfigure all the settings for each instance.** This gives you the freedom to have different settings for each instance. + +### Example of how to Set Up and Log Events to Two Separate Apps ### +```javascript +amplitude.getInstance().init('12345', null, {batchEvents: true}); // existing app, existing settings, and existing API key +amplitude.getInstance('new_app').init('67890', null, {includeReferrer: true}); // new app, new API key + +amplitude.getInstance('new_app').setUserId('joe@gmail.com'); // need to reconfigure new app +amplitude.getInstance('new_app').setUserProperties({'gender':'male'}); +amplitude.getInstance('new_app').logEvent('Clicked'); + +var identify = new amplitude.Identify().add('karma', 1); +amplitude.getInstance().identify(identify); +amplitude.getInstance().logEvent('Viewed Home Page'); +``` + +### Synchronizing Device Ids Between Apps ### + +As mentioned before, each instance will have its own deviceId. If you want your apps to share the same deviceId, you can do so *after init* via the `getDeviceId` and `setDeviceId` methods. Here's an example of how to copy the existing deviceId to the `new_app` instance: + +```javascript +var deviceId = amplitude.getInstance().getDeviceId(); // existing deviceId +amplitude.getInstance('new_app').setDeviceId(deviceId); // transferring existing deviceId to new_app +``` + # Tracking Events # It's important to think about what types of events you care about as a developer. You should aim to track between 20 and 200 types of events on your site. Common event types are actions the user initiates (such as pressing a button) and events you want the user to complete (such as filling out a form, completing a level, or making a payment). @@ -59,13 +131,13 @@ Anything past the above thresholds will not be visualized. **Note that the raw d If your app has its own login system that you want to track users with, you can call `setUserId` at any time: ```javascript -amplitude.setUserId('USER_ID_HERE'); +amplitude.getInstance().setUserId('USER_ID_HERE'); ``` You can also add the user ID as an argument to the `init` call: ```javascript -amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); +amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); ``` ### Logging Out and Anonymous Users ### @@ -73,8 +145,8 @@ amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); A user's data will be merged on the backend so that any events up to that point from the same browser will be tracked under the same user. Note: if a user logs out, or you want to log the events under an anonymous user, you need to do 2 things: 1) set the userId to `null` 2) regenerate a new deviceId. After doing that, events coming from the current user will appear as a brand new user in Amplitude dashboards. Note if you choose to do this, then you won't be able to see that the 2 users were using the same browser/device. ```javascript -amplitude.setUserId(null); // not string 'null' -amplitude.regenerateDeviceId(); +amplitude.getInstance().setUserId(null); // not string 'null' +amplitude.getInstance().regenerateDeviceId(); ``` # Setting Event Properties # @@ -84,7 +156,7 @@ You can attach additional data to any event by passing a Javascript object as th ```javascript var eventProperties = {}; eventProperties.key = 'value'; -amplitude.logEvent('EVENT_IDENTIFIER_HERE', eventProperties); +amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE', eventProperties); ``` Alternatively, you can set multiple event properties like this: @@ -94,7 +166,7 @@ var eventProperties = { 'age': 20, 'key': 'value' }; -amplitude.logEvent('EVENT_IDENTIFIER_HERE', eventProperties); +amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE', eventProperties); ``` # User Properties and User Property Operations # @@ -105,45 +177,45 @@ The SDK supports the operations `set`, `setOnce`, `unset`, and `add` on individu ```javascript var identify = new amplitude.Identify().set('gender', 'female').set('age', 20); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 2. `setOnce`: this sets the value of a user property only once. Subsequent `setOnce` operations on that user property will be ignored. In the following example, `sign_up_date` will be set once to `08/24/2015`, and the following setOnce to `09/14/2015` will be ignored: ```javascript var identify = new amplitude.Identify().setOnce('sign_up_date', '08/24/2015'); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); var identify = new amplitude.Identify().setOnce('sign_up_date', '09/14/2015'); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 3. `unset`: this will unset and remove a user property. ```javascript var identify = new amplitude.Identify().unset('gender').unset('age'); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 4. `add`: this will increment a user property by some numerical value. If the user property does not have a value set yet, it will be initialized to 0 before being incremented. ```javascript var identify = new amplitude.Identify().add('karma', 1).add('friends', 1); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 5. `append`: this will 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, it will be converted into a list with the new value appended. ```javascript var identify = new amplitude.Identify().append('ab-tests', 'new-user-test').append('some_list', [1, 2, 3, 4, 'values']); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 6. `prepend`: this will prepend a value or values to a user property. Prepend means inserting the value(s) at the front of a given 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, it will be converted into a list with the new value prepended. ```javascript var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-test').prepend('some_list', [1, 2, 3, 4, 'values']); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 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. In this example, only the set operation will be saved, and the add and unset will be ignored: @@ -153,7 +225,7 @@ var identify = new amplitude.Identify() .set('karma', 10) .add('karma', 1) .unset('karma'); -amplitude.identify(identify); +amplitude.getInstance().identify(identify); ``` ### Arrays in User Properties ### @@ -165,7 +237,7 @@ var identify = new amplitude.Identify() .set('colors', ['rose', 'gold']) .append('ab-tests', 'campaign_a') .append('existing_list', [4, 5]); -amplitude.identify(identify); +amplitude.getInstance().identify(identify); ``` ### Setting Multiple Properties with `setUserProperties` ### @@ -177,7 +249,7 @@ var userProperties = { gender: 'female', age: 20 }; -amplitude.setUserProperties(userProperties); +amplitude.getInstance().setUserProperties(userProperties); ``` ### Clearing User Properties ### @@ -185,7 +257,7 @@ amplitude.setUserProperties(userProperties); You may use `clearUserProperties` to clear all user properties at once. Note: the result is irreversible! ```javascript -amplitude.clearUserProperties(); +amplitude.getInstance().clearUserProperties(); ``` # Tracking Revenue # @@ -195,7 +267,7 @@ The preferred method of tracking revenue for a user now is to use `logRevenueV2( Each time a user generates revenue, you create a `Revenue` object and fill out the revenue properties: ```javascript var revenue = new amplitude.Revenue().setProductId('com.company.productId').setPrice(3.99).setQuantity(3); -amplitude.logRevenueV2(revenue); +amplitude.getInstance().logRevenueV2(revenue); ``` `productId` and `price` are required fields. `quantity` defaults to 1 if not specified. Each field has a corresponding `set` method (for example `setProductId`, `setQuantity`, etc). This table describes the different fields available: @@ -219,14 +291,14 @@ The existing `logRevenue` methods still work but are deprecated. Fields such as You can turn off logging for a given user: ```javascript -amplitude.setOptOut(true); +amplitude.getInstance().setOptOut(true); ``` No events will be saved or sent to the server while opt out is enabled. The opt out setting will persist across page loads. Calling ```javascript -amplitude.setOptOut(false); +amplitude.getInstance().setOptOut(false); ``` will reenable logging. @@ -236,7 +308,7 @@ will reenable logging. You can configure Amplitude by passing an object as the third argument to the `init`: ```javascript -amplitude.init('YOUR_API_KEY_HERE', null, { +amplitude.getInstance().init('YOUR_API_KEY_HERE', null, { // optional configuration options saveEvents: true, includeUtm: true, @@ -277,8 +349,8 @@ When setting groups you need to define a `groupType` and `groupName`(s). In the You can use `setGroup(groupType, groupName)` to designate which groups a user belongs to. Note: this will also set the `groupType`: `groupName` as a user property. **This will overwrite any existing groupName value set for that user's groupType, as well as the corresponding user property value.** `groupType` is a string, and `groupName` can be either a string or an array of strings to indicate a user being in multiple groups (for example Joe is in orgId 10 and 16, so the `groupName` would be [10, 16]). ```javascript -amplitude.setGroup('orgId', '15'); -amplitude.setGroup('sport', ['soccer', 'tennis']); +amplitude.getInstance().setGroup('orgId', '15'); +amplitude.getInstance().setGroup('sport', ['soccer', 'tennis']); ``` You can also use `logEventWithGroups` to set event-level groups, meaning the group designation only applies for the specific event being logged and does not persist on the user (unless you explicitly set it with `setGroup`). @@ -288,21 +360,21 @@ var eventProperties = { 'key': 'value' } -amplitude.logEventWithGroups('initialize_game', eventProperties, {'sport': 'soccer'}); +amplitude.getInstance().logEventWithGroups('initialize_game', eventProperties, {'sport': 'soccer'}); ``` ### Setting Version Name ### By default, no version name is set. You can specify a version name to distinguish between different versions of your site by calling `setVersionName`: ```javascript -amplitude.setVersionName('VERSION_NAME_HERE'); +amplitude.getInstance().setVersionName('VERSION_NAME_HERE'); ``` ### Custom Device Ids ### Device IDs are generated randomly, although you can define a custom device ID setting it as a configuration option or by calling: ```javascript -amplitude.setDeviceId('CUSTOM_DEVICE_ID'); +amplitude.getInstance().setDeviceId('CUSTOM_DEVICE_ID'); ``` **Note: this is not recommended unless you really know what you are doing** (like if you have your own system for tracking user devices). 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. @@ -311,12 +383,12 @@ amplitude.setDeviceId('CUSTOM_DEVICE_ID'); You can pass a callback function to logEvent and identify, which will get called after receiving a response from the server: ```javascript -amplitude.logEvent("EVENT_IDENTIFIER_HERE", null, callback_function); +amplitude.getInstance().logEvent("EVENT_IDENTIFIER_HERE", null, callback_function); ``` ```javascript var identify = new amplitude.Identify().set('key', 'value'); -amplitude.identify(identify, callback_function); +amplitude.getInstance().identify(identify, callback_function); ``` The status and response body from the server are passed to the callback function, which you might find useful. An example of a callback function which redirects the browser to another site after a response: @@ -340,7 +412,7 @@ And then you would define a function that is called when the link is clicked lik ```javascript var trackClickLinkA = function() { - amplitude.logEvent('Clicked Link A', null, function() { + amplitude.getInstance().logEvent('Clicked Link A', null, function() { window.location='LINK_A_URL'; }); }; @@ -349,11 +421,11 @@ var trackClickLinkA = function() { In the case that `optOut` is true, then no event will be logged, but the callback will be called. In the case that `batchEvents` is true, if the batch requirements `eventUploadThreshold` and `eventUploadPeriodMillis` are not met when `logEvent` is called, then no request is sent, but the callback is still called. In these cases, the callback will be called with an input status of 0 and response 'No request sent'. ### Init Callbacks ### -You can also pass a callback function to init, which will get called after the SDK finishes its asynchronous loading. *Note: no values are passed to the init callback function*: +You can also pass a callback function to init, which will get called after the SDK finishes its asynchronous loading. *Note: the instance is passed as an argument to the callback*: ```javascript -amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE', null, function() { - console.log(amplitude.options.deviceId); // access Amplitude's deviceId after initialization +amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE', null, function(instance) { + console.log(instance.options.deviceId); // access Amplitude's deviceId after initialization }); ``` @@ -364,9 +436,9 @@ If you are using [RequireJS](http://requirejs.org/) to load your Javascript file ``` @@ -382,14 +454,14 @@ You can also define the path in your RequireJS configuration like so: }); require(['amplitude'], function(amplitude) { - amplitude.init('YOUR_API_KEY_HERE'); // replace YOUR_API_KEY_HERE with your Amplitude api key. + amplitude.getInstance().init('YOUR_API_KEY_HERE'); // replace YOUR_API_KEY_HERE with your Amplitude api key. window.amplitude = amplitude; // You can bind the amplitude object to window if you want to use it directly. - amplitude.logEvent('Clicked Link A'); + amplitude.getInstance().logEvent('Clicked Link A'); }); ``` @@ -401,5 +473,5 @@ You can also define the path in your RequireJS configuration like so: 2. Call SDK functions in Google Tag Manager using [Custom HTML tags](https://support.google.com/tagmanager/answer/6107167?hl=en) and adding Javascript in this form as the custom tag: ```html - + ``` From a6c78a710b5ac464faeb67b4456016e777ab6cab Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Sat, 21 May 2016 20:46:33 -0700 Subject: [PATCH 10/13] add more test cases, update readme script --- scripts/readme.js | 2 +- test/amplitude-client.js | 143 ++++++++++++++++++++++++++++++++- test/browser/amplitudejs.html | 22 ++++- test/browser/amplitudejs2.html | 10 ++- 4 files changed, 171 insertions(+), 6 deletions(-) diff --git a/scripts/readme.js b/scripts/readme.js index 81bab201..2c865878 100644 --- a/scripts/readme.js +++ b/scripts/readme.js @@ -11,7 +11,7 @@ var snippet = fs.readFileSync(snippetFilename, 'utf-8'); var script = ' '; var updated = readme.replace(/ +/, script); diff --git a/test/amplitude-client.js b/test/amplitude-client.js index 9c3a0402..b6cebb68 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -162,7 +162,7 @@ describe('AmplitudeClient', function() { var amplitude2 = new AmplitudeClient('new_app'); amplitude2.init(apiKey); - assert.notEqual(amplitude.options.deviceId, deviceId); + assert.notEqual(amplitude2.options.deviceId, deviceId); assert.isNull(amplitude2.options.userId); assert.isFalse(amplitude2.options.optOut); @@ -396,6 +396,35 @@ describe('AmplitudeClient', function() { assert.equal(localStorage.getItem('amplitude_unsent_identify'), existingIdentify); }); + it('should load saved events for non-default instances', 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"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130, "groups":{}}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131, "groups":{}}]'; + localStorage.setItem('amplitude_unsent_new_app', existingEvent); + localStorage.setItem('amplitude_unsent_identify_new_app', existingIdentify); + assert.isNull(localStorage.getItem('amplitude_unsent')); + assert.isNull(localStorage.getItem('amplitude_unsent_identify')); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey, null, {batchEvents: true}); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, JSON.parse(existingEvent)); + assert.deepEqual(amplitude2._unsentIdentifys, JSON.parse(existingIdentify)); + + // check local storage keys are still same + assert.equal(localStorage.getItem('amplitude_unsent_new_app'), existingEvent); + assert.equal(localStorage.getItem('amplitude_unsent_identify_new_app'), existingIdentify); + }); + it('should validate event properties when loading saved events from localStorage', function() { var existingEvents = '[{"device_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d' + '-2352191877e6","timestamp":1455744744413,"event_id":2,"session_id":1455744733865,"event_type":"clicked",' + @@ -457,7 +486,7 @@ describe('AmplitudeClient', function() { assert.deepEqual(amplitude2._unsentIdentifys[0].user_properties, {'$set': expected}); }); - it ('should load saved events from localStorage new keys and send events', function() { + it ('should load saved events from localStorage and send events for default instance', 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"' + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + @@ -493,6 +522,42 @@ describe('AmplitudeClient', function() { assert.equal(events[1].event_type, '$identify'); }); +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"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131}]'; + localStorage.setItem('amplitude_unsent_new_app', existingEvent); + localStorage.setItem('amplitude_unsent_identify_new_app', existingIdentify); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + server.respondWith('success'); + server.respond(); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2._unsentIdentifys, []); + + // check local storage keys are still same + assert.equal(localStorage.getItem('amplitude_unsent_new_app'), JSON.stringify([])); + assert.equal(localStorage.getItem('amplitude_unsent_identify_new_app'), JSON.stringify([])); + + // check request + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + assert.equal(events[0].event_id, 49); + assert.equal(events[1].event_type, '$identify'); + }); + it('should validate event properties when loading saved events from localStorage', function() { var existingEvents = '[{"device_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d' + '-2352191877e6","timestamp":1455744744413,"event_id":2,"session_id":1455744733865,"event_type":"clicked",' + @@ -536,6 +601,40 @@ describe('AmplitudeClient', function() { assert.deepEqual(amplitude2._unsentEvents[0].event_properties, {}); assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); }); + + it('should not load saved events from another instances\'s localStorage', 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"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131}]'; + localStorage.setItem('amplitude_unsent', existingEvent); + localStorage.setItem('amplitude_unsent_identify', existingIdentify); + assert.isNull(localStorage.getItem('amplitude_unsent_new_app')); + assert.isNull(localStorage.getItem('amplitude_unsent_identify_new_app')); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + + // check events not loaded into memory + assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2._unsentIdentifys, []); + + // check local storage + assert.equal(localStorage.getItem('amplitude_unsent'), existingEvent); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), existingIdentify); + assert.isNull(localStorage.getItem('amplitude_unsent_new_app')); + assert.isNull(localStorage.getItem('amplitude_unsent_identify_new_app')); + + // check request + assert.lengthOf(server.requests, 0); + }); }); describe('runQueuedFunctions', function() { @@ -2188,6 +2287,46 @@ describe('setVersionName', function() { // existing value persists assert.equal(sessionStorage.getItem('amplitude_referrer'), 'https://www.google.com/search?'); }); + + it('should not override any existing referrer values in session storage for non-default instances', function() { + reset(); + sessionStorage.setItem('amplitude_referrer_new_app', 'https://www.google.com/search?'); + var amplitude2 = new AmplitudeClient('new_app'); + sinon.stub(amplitude2, '_getReferrer').returns('https://amplitude.com/contact'); + amplitude2.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); + amplitude2._getReferrer.restore(); + + amplitude2._saveReferrer('https://facebook.com/contact'); + amplitude2.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + + // first event should be identify with initial_referrer and NO referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be another identify but with the new referrer + assert.equal(events[1].event_type, '$identify'); + assert.deepEqual(events[1].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://facebook.com/contact', + 'initial_referring_domain': 'facebook.com' + } + }); + + // third event should be the test event with no referrer information + assert.equal(events[2].event_type, 'Referrer Test Event'); + assert.deepEqual(events[2].user_properties, {}); + + // existing value persists + assert.equal(sessionStorage.getItem('amplitude_referrer_new_app'), 'https://www.google.com/search?'); + }); }); describe('logRevenue', function() { diff --git a/test/browser/amplitudejs.html b/test/browser/amplitudejs.html index bba74070..e2871305 100644 --- a/test/browser/amplitudejs.html +++ b/test/browser/amplitudejs.html @@ -2,7 +2,7 @@

Amplitude JS Test

diff --git a/test/browser/amplitudejs2.html b/test/browser/amplitudejs2.html index 8cb502c6..f2c1fcfe 100644 --- a/test/browser/amplitudejs2.html +++ b/test/browser/amplitudejs2.html @@ -2,7 +2,7 @@ diff --git a/documentation/AmplitudeClient.html b/documentation/AmplitudeClient.html new file mode 100644 index 00000000..31868681 --- /dev/null +++ b/documentation/AmplitudeClient.html @@ -0,0 +1,2800 @@ + + + + + JSDoc: Class: AmplitudeClient + + + + + + + + + + +
+ +

Class: AmplitudeClient

+ + + + + + +
+ +
+ +

AmplitudeClient

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

new AmplitudeClient()

+ + + + + +
+ AmplitudeClient SDK API - instance constructor. +The Amplitude class handles creation of client instances, all you need to do is call amplitude.getInstance() +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
var amplitudeClient = new AmplitudeClient();
+ + + + +
+ + + + + + + + + + + + +

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
+ +
amplitudeClient.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
+ +
amplitudeClient.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
+ +
amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
+ + + + + + + + +

logEventWithGroups(eventType, eventProperties, groups, opt_callback)

+ + + + + +
+ Log an event with eventType, eventProperties, and groups. Use this to set event-level groups. +Note: the group(s) set only apply for the specific event type being logged and does not persist on the user +(unless you explicitly set it with setGroup). +See the SDK Readme for more information +about groups and Count by Distinct on the Amplitude platform. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
eventType + + +string + + + + name of event
eventProperties + + +object + + + + (optional) an object with string keys and values for the event properties.
groups + + +object + + + + (optional) an object with string groupType: groupName values for the event being logged. +groupName can be a string or an array of strings.
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
+ +
amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24});
+ + + + + + + + +

logRevenue(price, quantity, product)

+ + + + + +
+ Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 +
+ + + + + + + + + +
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
+ + + + + + +
+ + + + + + + + + + + + + + + + +
Deprecated:
  • Yes
+ + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitudeClient.logRevenue(3.99, 1, 'product_1234');
+ + + + + + + + +

logRevenueV2(revenue_obj)

+ + + + + +
+ Log revenue with Revenue interface. The new revenue interface allows for more revenue fields like +revenueType and event properties. +See Readme +for more information on the Revenue interface and logging revenue. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
revenue_obj + + +Revenue + + + + the revenue object containing the revenue data being logged.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99);
+amplitude.logRevenueV2(revenue);
+ + + + + + + + +

regenerateDeviceId()

+ + + + + +
+ Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you +are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. +With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. +This uses src/uuid.js to regenerate the deviceId. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

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
+ +
amplitudeClient.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
+ +
amplitudeClient.setDomain('.amplitude.com');
+ + + + + + + + +

setGlobalUserProperties()

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

setGroup(groupType, groupName)

+ + + + + +
+ Add user to a group or groups. You need to specify a groupType and groupName(s). +For example you can group people by their organization. +In that case groupType is "orgId" and groupName would be the actual ID(s). +groupName can be a string or an array of strings to indicate a user in multiple gruups. +You can also call setGroup multiple times with different groupTypes to track multiple types of groups (up to 5 per app). +Note: this will also set groupType: groupName as a user property. +See the SDK Readme for more information. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
groupType + + +string + + + + the group type (ex: orgId)
groupName + + +string +| + +list + + + + the name of the group (ex: 15), or a list of names of the groups
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitudeClient.setGroup('orgId', 15); // this adds the current user to orgId 15.
+ + + + + + + + +

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
+ +
amplitudeClient.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
+ +
amplitudeClient.setUserProperties({'gender': 'female', 'sign_up_complete': true})
+ + + + + + + + +

setVersionName(versionName)

+ + + + + +
+ Set a versionName for your application. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
versionName + + +string + + + + The version to set for your application.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitudeClient.setVersionName('1.12.3');
+ + + + + + + + + +
+ +
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT) +
+ + + + + \ No newline at end of file diff --git a/documentation/Identify.html b/documentation/Identify.html index f902fb52..948cd089 100644 --- a/documentation/Identify.html +++ b/documentation/Identify.html @@ -1289,13 +1289,13 @@
Example

- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT)
diff --git a/documentation/Revenue.html b/documentation/Revenue.html index 681cbac9..a747bab5 100644 --- a/documentation/Revenue.html +++ b/documentation/Revenue.html @@ -959,13 +959,13 @@
Example

- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT)
diff --git a/documentation/amplitude-client.js.html b/documentation/amplitude-client.js.html new file mode 100644 index 00000000..f1fcce55 --- /dev/null +++ b/documentation/amplitude-client.js.html @@ -0,0 +1,1179 @@ + + + + + JSDoc: Source: amplitude-client.js + + + + + + + + + + +
+ +

Source: amplitude-client.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 Revenue = require('./revenue');
+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');
+
+/**
+ * AmplitudeClient SDK API - instance constructor.
+ * The Amplitude class handles creation of client instances, all you need to do is call amplitude.getInstance()
+ * @constructor AmplitudeClient
+ * @public
+ * @example var amplitudeClient = new AmplitudeClient();
+ */
+var AmplitudeClient = function AmplitudeClient(instanceName) {
+  this._instanceName = utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName.toLowerCase();
+  this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName;
+  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;
+};
+
+AmplitudeClient.prototype.Identify = Identify;
+AmplitudeClient.prototype.Revenue = Revenue;
+
+/**
+ * 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 amplitudeClient.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); });
+ */
+AmplitudeClient.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;
+
+    if (this._instanceName === Constants.DEFAULT_INSTANCE) {
+      _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;
+        var groups = this._unsentEvents[i].groups;
+        this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties);
+        this._unsentEvents[i].groups = utils.validateGroups(groups);
+      }
+
+      // validate user properties for unsent identifys
+      for (var j = 0; j < this._unsentIdentifys.length; j++) {
+        var userProperties = this._unsentIdentifys[j].user_properties;
+        var identifyGroups = this._unsentIdentifys[j].groups;
+        this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties);
+        this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups);
+      }
+
+      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(this);
+    }
+  }
+};
+
+/**
+ * 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
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.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.
+ */
+AmplitudeClient.prototype.isNewSession = function isNewSession() {
+  return this._newSession;
+};
+
+/**
+ * Returns the id of the current session.
+ * @public
+ * @return {number} Id of the current session.
+ */
+AmplitudeClient.prototype.getSessionId = function getSessionId() {
+  return this._sessionId;
+};
+
+/**
+ * Increments the eventId and returns it.
+ * @private
+ */
+AmplitudeClient.prototype.nextEventId = function nextEventId() {
+  this._eventId++;
+  return this._eventId;
+};
+
+/**
+ * Increments the identifyId and returns it.
+ * @private
+ */
+AmplitudeClient.prototype.nextIdentifyId = function nextIdentifyId() {
+  this._identifyId++;
+  return this._identifyId;
+};
+
+/**
+ * Increments the sequenceNumber and returns it.
+ * @private
+ */
+AmplitudeClient.prototype.nextSequenceNumber = function nextSequenceNumber() {
+  this._sequenceNumber++;
+  return this._sequenceNumber;
+};
+
+/**
+ * Returns the total count of unsent events and identifys
+ * @private
+ */
+AmplitudeClient.prototype._unsentCount = function _unsentCount() {
+  return this._unsentEvents.length + this._unsentIdentifys.length;
+};
+
+/**
+ * Send events if ready. Returns true if events are sent.
+ * @private
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, key) {
+  return storage.getItem(key + this._storageSuffix);
+};
+
+/**
+ * Helper function to set values in storage
+ * Storage argument allows for localStoraoge and sessionStoraoge
+ * @private
+ */
+AmplitudeClient.prototype._setInStorage = function _setInStorage(storage, key, value) {
+  storage.setItem(key + this._storageSuffix, 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 + scope._storageSuffix);
+  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 + scope._storageSuffix, {
+    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
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.prototype._getReferrer = function _getReferrer() {
+  return document.referrer;
+};
+
+/**
+ * Parse the domain from referrer info
+ * @private
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.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 amplitudeClient.setDomain('.amplitude.com');
+ */
+AmplitudeClient.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 amplitudeClient.setUserId('joe@gmail.com');
+ */
+AmplitudeClient.prototype.setUserId = function setUserId(userId) {
+  try {
+    this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null;
+    _saveCookieData(this);
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+ * Add user to a group or groups. You need to specify a groupType and groupName(s).
+ * For example you can group people by their organization.
+ * In that case groupType is "orgId" and groupName would be the actual ID(s).
+ * groupName can be a string or an array of strings to indicate a user in multiple gruups.
+ * You can also call setGroup multiple times with different groupTypes to track multiple types of groups (up to 5 per app).
+ * Note: this will also set groupType: groupName as a user property.
+ * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information.
+ * @public
+ * @param {string} groupType - the group type (ex: orgId)
+ * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups
+ * @example amplitudeClient.setGroup('orgId', 15); // this adds the current user to orgId 15.
+ */
+AmplitudeClient.prototype.setGroup = function(groupType, groupName) {
+  if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') ||
+        utils.isEmptyString(groupType)) {
+    return;
+  }
+
+  var groups = {};
+  groups[groupType] = groupName;
+  var identify = new Identify().set(groupType, groupName);
+  this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null);
+};
+
+/**
+ * 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);
+ */
+AmplitudeClient.prototype.setOptOut = function setOptOut(enable) {
+  if (!utils.validateInput(enable, 'enable', 'boolean')) {
+    return;
+  }
+
+  try {
+    this.options.optOut = enable;
+    _saveCookieData(this);
+  } catch (e) {
+    utils.log(e);
+  }
+};
+
+/**
+  * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you
+  * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out.
+  * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard.
+  * This uses src/uuid.js to regenerate the deviceId.
+  * @public
+  */
+AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() {
+  this.setDeviceId(UUID() + 'R');
+};
+
+/**
+  * 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 amplitudeClient.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0');
+  */
+AmplitudeClient.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 amplitudeClient.setUserProperties({'gender': 'female', 'sign_up_complete': true})
+ */
+AmplitudeClient.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 amplitudeClient.clearUserProperties();
+ */
+AmplitudeClient.prototype.clearUserProperties = function clearUserProperties(){
+  if (!this._apiKeySet('clearUserProperties()')) {
+    return;
+  }
+
+  var identify = new Identify();
+  identify.clearAll();
+  this.identify(identify);
+};
+
+/**
+ * Applies the proxied functions on the proxied object to an instance of the real object.
+ * Used to convert proxied Identify and Revenue objects.
+ * @private
+ */
+var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) {
+  for (var i = 0; i < proxy._q.length; i++) {
+    var fn = instance[proxy._q[i][0]];
+    if (type(fn) === 'function') {
+      fn.apply(instance, proxy._q[i].slice(1));
+    }
+  }
+  return instance;
+};
+
+/**
+ * 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);
+ */
+AmplitudeClient.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')) {
+    identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj);
+  }
+
+  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, null, 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 - The version to set for your application.
+ * @example amplitudeClient.setVersionName('1.12.3');
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, 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 || {};
+    groups = groups || {};
+    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
+      groups: utils.truncate(utils.validateGroups(groups))
+      // 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
+ */
+AmplitudeClient.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 amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15});
+ */
+AmplitudeClient.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, null, opt_callback);
+};
+
+/**
+ * Log an event with eventType, eventProperties, and groups. Use this to set event-level groups.
+ * Note: the group(s) set only apply for the specific event type being logged and does not persist on the user
+ * (unless you explicitly set it with setGroup).
+ * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information
+ * about groups and Count by Distinct on the Amplitude platform.
+ * @public
+ * @param {string} eventType - name of event
+ * @param {object} eventProperties - (optional) an object with string keys and values for the event properties.
+ * @param {object} groups - (optional) an object with string groupType: groupName values for the event being logged.
+ * groupName can be a string or an array of strings.
+ * @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 amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24});
+ */
+AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) {
+  if (!this._apiKeySet('logEventWithGroup()') ||
+        !utils.validateInput(eventType, 'eventType', 'string')) {
+    if (type(opt_callback) === 'function') {
+      opt_callback(0, 'No request sent');
+    }
+    return -1;
+  }
+  return this._logEvent(eventType, eventProperties, null, null, groups, 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 with Revenue interface. The new revenue interface allows for more revenue fields like
+ * revenueType and event properties.
+ * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#tracking-revenue}
+ * for more information on the Revenue interface and logging revenue.
+ * @public
+ * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged.
+ * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99);
+ * amplitude.logRevenueV2(revenue);
+ */
+AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) {
+  if (!this._apiKeySet('logRevenueV2()')) {
+    return;
+  }
+
+  // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object
+  if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) {
+    revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj);
+  }
+
+  if (revenue_obj instanceof Revenue) {
+    // only send if revenue is valid
+    if (revenue_obj && revenue_obj._isValidRevenue()) {
+      return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject());
+    }
+  } else {
+    utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj));
+  }
+};
+
+/**
+ * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2
+ * @public
+ * @deprecated
+ * @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 amplitudeClient.logRevenue(3.99, 1, 'product_1234');
+ */
+AmplitudeClient.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(Constants.REVENUE_EVENT, {}, {
+    productId: product,
+    special: 'revenue_amount',
+    quantity: quantity || 1,
+    price: price
+  }, null, null, null);
+};
+
+/**
+ * Remove events in storage with event ids up to and including maxEventId.
+ * @private
+ */
+AmplitudeClient.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.
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.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
+ */
+AmplitudeClient.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__;
+ */
+AmplitudeClient.prototype.__VERSION__ = version;
+
+module.exports = AmplitudeClient;
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT) +
+ + + + + diff --git a/documentation/amplitude.js.html b/documentation/amplitude.js.html index d0917b80..0b30025f 100644 --- a/documentation/amplitude.js.html +++ b/documentation/amplitude.js.html @@ -26,51 +26,43 @@

Source: amplitude.js

-
var Constants = require('./constants');
-var cookieStorage = require('./cookiestorage');
-var getUtmData = require('./utm');
+            
var AmplitudeClient = require('./amplitude-client');
+var Constants = require('./constants');
 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 Revenue = require('./revenue');
 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.
+ * Amplitude SDK API - instance manager.
+ * Function calls directly on amplitude have been deprecated. Please call methods on the default shared instance: amplitude.getInstance() instead.
+ * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps} for more information about this change.
  * @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;
+  this._q = [];
+  this._instances = {}; // mapping of instance names to instances
 };
 
 Amplitude.prototype.Identify = Identify;
 Amplitude.prototype.Revenue = Revenue;
 
+Amplitude.prototype.getInstance = function getInstance(instance) {
+  instance = utils.isEmptyString(instance) ? Constants.DEFAULT_INSTANCE : instance.toLowerCase();
+  var client = this._instances[instance];
+  if (client === undefined) {
+    client = new AmplitudeClient(instance);
+    this._instances[instance] = client;
+  }
+  return client;
+};
+
 /**
  * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations.
  * This is required before any other methods can be called.
@@ -80,113 +72,17 @@ 

Source: amplitude.js

* @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. + * @deprecated Please use amplitude.getInstance().init(apiKey, opt_userId, opt_config, opt_callback); * @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; - var groups = this._unsentEvents[i].groups; - this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); - this._unsentEvents[i].groups = utils.validateGroups(groups); - } - - // validate user properties for unsent identifys - for (var j = 0; j < this._unsentIdentifys.length; j++) { - var userProperties = this._unsentIdentifys[j].user_properties; - var identifyGroups = this._unsentIdentifys[j].groups; - this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); - this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups); - } - - 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 { + this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { + // make options such as deviceId available for callback functions + this.options = instance.options; 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); + opt_callback(instance); } - } + }.bind(this)); }; /** @@ -194,6 +90,7 @@

Source: amplitude.js

* @private */ Amplitude.prototype.runQueuedFunctions = function () { + // run queued up old versions of functions for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; if (type(fn) === 'function') { @@ -201,58 +98,33 @@

Source: amplitude.js

} } 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) {} + // run queued up functions on instances + for (var instance in this._instances) { + if (this._instances.hasOwnProperty(instance)) { + this._instances[instance].runQueuedFunctions(); + } } - 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. + * @deprecated Please use amplitude.getInstance().isNewSession(); */ Amplitude.prototype.isNewSession = function isNewSession() { - return this._newSession; + return this.getInstance().isNewSession(); }; /** * Returns the id of the current session. * @public * @return {number} Id of the current session. + * @deprecated Please use amplitude.getInstance().getSessionId(); */ Amplitude.prototype.getSessionId = function getSessionId() { - return this._sessionId; + return this.getInstance().getSessionId(); }; /** @@ -260,8 +132,7 @@

Source: amplitude.js

* @private */ Amplitude.prototype.nextEventId = function nextEventId() { - this._eventId++; - return this._eventId; + return this.getInstance().nextEventId(); }; /** @@ -269,8 +140,7 @@

Source: amplitude.js

* @private */ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { - this._identifyId++; - return this._identifyId; + return this.getInstance().nextIdentifyId(); }; /** @@ -278,257 +148,7 @@

Source: amplitude.js

* @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); + return this.getInstance().nextSequenceNumber(); }; /** @@ -537,51 +157,29 @@

Source: amplitude.js

* @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) {} + this.getInstance().saveEvents(); }; /** * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. * @public * @param {string} domain to set. + * @deprecated Please use amplitude.getInstance().setDomain(domain); * @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); - } + this.getInstance().setDomain(domain); }; /** * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. + * @deprecatedPlease use amplitude.getInstance().setUserId(userId); * @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); - } + this.getInstance().setUserId(userId); }; /** @@ -595,37 +193,22 @@

Source: amplitude.js

* @public * @param {string} groupType - the group type (ex: orgId) * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @deprecated Please use amplitude.getInstance().setGroup(groupType, groupName); * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. */ Amplitude.prototype.setGroup = function(groupType, groupName) { - if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || - utils.isEmptyString(groupType)) { - return; - } - - var groups = {}; - groups[groupType] = groupName; - var identify = new Identify().set(groupType, groupName); - this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null); + this.getInstance().setGroup(groupType, groupName); }; /** * Sets whether to opt current user out of tracking. * @public * @param {boolean} enable - if true then no events will be logged or sent. + * @deprecated Please use amplitude.getInstance().setOptOut(enable); * @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); - } + this.getInstance().setOptOut(enable); }; /** @@ -634,9 +217,10 @@

Source: amplitude.js

* With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. * @public + * @deprecated Please use amplitude.getInstance().regenerateDeviceId(); */ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { - this.setDeviceId(UUID() + 'R'); + this.getInstance().regenerateDeviceId(); }; /** @@ -645,21 +229,11 @@

Source: amplitude.js

* (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. + * @deprecated Please use amplitude.getInstance().setDeviceId(deviceId); * @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); - } + this.getInstance().setDeviceId(deviceId); }; /** @@ -668,50 +242,21 @@

Source: amplitude.js

* @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. + * @deprecated Please use amplitude.getInstance.setUserProperties(userProperties); * @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); + this.getInstance().setUserProperties(userProperties); }; /** * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! * @public + * @deprecated Please use amplitude.getInstance().clearUserProperties(); * @example amplitude.clearUserProperties(); */ Amplitude.prototype.clearUserProperties = function clearUserProperties(){ - if (!this._apiKeySet('clearUserProperties()')) { - return; - } - - var identify = new Identify(); - identify.clearAll(); - this.identify(identify); -}; - -/** - * Applies the proxied functions on the proxied object to an instance of the real object. - * Used to convert proxied Identify and Revenue objects. - * @private - */ -var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) { - for (var i = 0; i < proxy._q.length; i++) { - var fn = instance[proxy._q[i][0]]; - if (type(fn) === 'function') { - fn.apply(instance, proxy._q[i].slice(1)); - } - } - return instance; + this.getInstance().clearUserProperties(); }; /** @@ -721,140 +266,24 @@

Source: amplitude.js

* @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. + * @deprecated Please use amplitude.getInstance().identify(identify); * @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')) { - identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj); - } - - 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, null, 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'); - } + this.getInstance().identify(identify_obj, opt_callback); }; /** * Set a versionName for your application. * @public * @param {string} versionName - The version to set for your application. + * @deprecated Please use amplitude.getInstance().setVersionName(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, groups, 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 || {}; - groups = groups || {}; - 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 - groups: utils.truncate(utils.validateGroups(groups)) - // 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.getInstance().setVersionName(versionName); }; /** @@ -872,17 +301,11 @@

Source: amplitude.js

* @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. + * @deprecated Please use amplitude.getInstance().logEvent(eventType, eventProperties, opt_callback); * @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, null, opt_callback); + return this.getInstance().logEvent(eventType, eventProperties, opt_callback); }; /** @@ -898,25 +321,11 @@

Source: amplitude.js

* groupName can be a string or an array of strings. * @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. + * Deprecated Please use amplitude.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { - if (!this._apiKeySet('logEventWithGroup()') || - !utils.validateInput(eventType, 'eventType', 'string')) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback); -}; - -/** - * Test that n is a number or a numeric value. - * @private - */ -var _isNumber = function _isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); + return this.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); }; /** @@ -926,51 +335,25 @@

Source: amplitude.js

* for more information on the Revenue interface and logging revenue. * @public * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { - if (!this._apiKeySet('logRevenueV2()')) { - return; - } - - // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object - if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) { - revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj); - } - - if (revenue_obj instanceof Revenue) { - // only send if revenue is valid - if (revenue_obj && revenue_obj._isValidRevenue()) { - return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject()); - } - } else { - utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj)); - } + return this.getInstance().logRevenueV2(revenue_obj); }; /** * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 * @public - * @deprecated * @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 + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @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(Constants.REVENUE_EVENT, {}, { - productId: product, - special: 'revenue_amount', - quantity: quantity || 1, - price: price - }, null, null, null); + return this.getInstance().logRevenue(price, quantity, product); }; /** @@ -978,27 +361,7 @@

Source: amplitude.js

* @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; + this.getInstance().removeEvents(maxEventId, maxIdentifyId); }; /** @@ -1009,126 +372,7 @@

Source: amplitude.js

* 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 - }; + this.getInstance().sendEvents(callback); }; /** @@ -1137,7 +381,7 @@

Source: amplitude.js

* @deprecated */ Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { - this.setUserProperties(userProperties); + this.getInstance().setUserProperties(userProperties); }; /** @@ -1159,13 +403,13 @@

Source: amplitude.js


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT)
diff --git a/documentation/identify.js.html b/documentation/identify.js.html index 956a279e..476aa835 100644 --- a/documentation/identify.js.html +++ b/documentation/identify.js.html @@ -220,13 +220,13 @@

Source: identify.js


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT)
diff --git a/documentation/index.html b/documentation/index.html index 801ba1ae..900bd3f5 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -50,13 +50,13 @@


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT)
diff --git a/documentation/revenue.js.html b/documentation/revenue.js.html index 39786bfd..f401b350 100644 --- a/documentation/revenue.js.html +++ b/documentation/revenue.js.html @@ -194,13 +194,13 @@

Source: revenue.js


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Sat May 21 2016 21:10:14 GMT-0700 (PDT)
diff --git a/src/amplitude-client.js b/src/amplitude-client.js index 03a67281..0d143e27 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -16,13 +16,14 @@ var version = require('./version'); var DEFAULT_OPTIONS = require('./options'); /** - * Amplitude SDK API - instance constructor. - * @constructor Amplitude + * AmplitudeClient SDK API - instance constructor. + * The Amplitude class handles creation of client instances, all you need to do is call amplitude.getInstance() + * @constructor AmplitudeClient * @public - * @example var amplitude = new Amplitude(); + * @example var amplitudeClient = new AmplitudeClient(); */ var AmplitudeClient = function AmplitudeClient(instanceName) { - this._instanceName = (utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName).toLowerCase(); + this._instanceName = utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName.toLowerCase(); this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName; this._unsentEvents = []; this._unsentIdentifys = []; @@ -54,7 +55,7 @@ AmplitudeClient.prototype.Revenue = Revenue; * @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'); }); + * @example amplitudeClient.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); */ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { @@ -526,7 +527,7 @@ AmplitudeClient.prototype.saveEvents = function saveEvents() { * 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'); + * @example amplitudeClient.setDomain('.amplitude.com'); */ AmplitudeClient.prototype.setDomain = function setDomain(domain) { if (!utils.validateInput(domain, 'domain', 'string')) { @@ -549,7 +550,7 @@ AmplitudeClient.prototype.setDomain = function setDomain(domain) { * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. - * @example amplitude.setUserId('joe@gmail.com'); + * @example amplitudeClient.setUserId('joe@gmail.com'); */ AmplitudeClient.prototype.setUserId = function setUserId(userId) { try { @@ -571,7 +572,7 @@ AmplitudeClient.prototype.setUserId = function setUserId(userId) { * @public * @param {string} groupType - the group type (ex: orgId) * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups - * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. + * @example amplitudeClient.setGroup('orgId', 15); // this adds the current user to orgId 15. */ AmplitudeClient.prototype.setGroup = function(groupType, groupName) { if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || @@ -621,7 +622,7 @@ AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() { * (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'); + * @example amplitudeClient.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); */ AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { if (!utils.validateInput(deviceId, 'deviceId', 'string')) { @@ -644,7 +645,7 @@ AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { * @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}) + * @example amplitudeClient.setUserProperties({'gender': 'female', 'sign_up_complete': true}) */ AmplitudeClient.prototype.setUserProperties = function setUserProperties(userProperties) { if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { @@ -663,7 +664,7 @@ AmplitudeClient.prototype.setUserProperties = function setUserProperties(userPro /** * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! * @public - * @example amplitude.clearUserProperties(); + * @example amplitudeClient.clearUserProperties(); */ AmplitudeClient.prototype.clearUserProperties = function clearUserProperties(){ if (!this._apiKeySet('clearUserProperties()')) { @@ -734,7 +735,7 @@ AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) { * Set a versionName for your application. * @public * @param {string} versionName - The version to set for your application. - * @example amplitude.setVersionName('1.12.3'); + * @example amplitudeClient.setVersionName('1.12.3'); */ AmplitudeClient.prototype.setVersionName = function setVersionName(versionName) { if (!utils.validateInput(versionName, 'versionName', 'string')) { @@ -848,7 +849,7 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue * @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}); + * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || @@ -874,7 +875,7 @@ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventPropertie * groupName can be a string or an array of strings. * @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.logEventWithGroups('Clicked Button', null, {'orgId': 24}); + * @example amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { if (!this._apiKeySet('logEventWithGroup()') || @@ -932,7 +933,7 @@ AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { * @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'); + * @example amplitudeClient.logRevenue(3.99, 1, 'product_1234'); */ AmplitudeClient.prototype.logRevenue = function logRevenue(price, quantity, product) { // Test that the parameters are of the right type. diff --git a/src/amplitude.js b/src/amplitude.js index cefcef13..603f0b0e 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -1,5 +1,5 @@ var AmplitudeClient = require('./amplitude-client'); -var constants = require('./constants'); +var Constants = require('./constants'); var Identify = require('./identify'); var object = require('object'); var Revenue = require('./revenue'); @@ -9,7 +9,9 @@ var version = require('./version'); var DEFAULT_OPTIONS = require('./options'); /** - * Amplitude SDK API - instance constructor. + * Amplitude SDK API - instance manager. + * Function calls directly on amplitude have been deprecated. Please call methods on the default shared instance: amplitude.getInstance() instead. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps} for more information about this change. * @constructor Amplitude * @public * @example var amplitude = new Amplitude(); @@ -24,7 +26,7 @@ Amplitude.prototype.Identify = Identify; Amplitude.prototype.Revenue = Revenue; Amplitude.prototype.getInstance = function getInstance(instance) { - instance = (utils.isEmptyString(instance) ? constants.DEFAULT_INSTANCE : instance).toLowerCase(); + instance = utils.isEmptyString(instance) ? Constants.DEFAULT_INSTANCE : instance.toLowerCase(); var client = this._instances[instance]; if (client === undefined) { client = new AmplitudeClient(instance); @@ -187,7 +189,7 @@ Amplitude.prototype.setOptOut = function setOptOut(enable) { * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. * @public - * deprecated Please use amplitude.getInstance().regenerateDeviceId(); + * @deprecated Please use amplitude.getInstance().regenerateDeviceId(); */ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { this.getInstance().regenerateDeviceId(); diff --git a/test/browser/amplitudejs.html b/test/browser/amplitudejs.html index e2871305..43cce0a3 100644 --- a/test/browser/amplitudejs.html +++ b/test/browser/amplitudejs.html @@ -103,7 +103,7 @@ amplitude.getInstance('app2').logEvent('app 2 page load'); amplitude.getInstance('app3').init('a2dbce0e18dfe5f8e74493843ff5c053', null, {batchEvents: true, eventUploadThreshold: 2}); - amplitude.getInstance('app3').logEvent('app3 pageLoad'); + amplitude.getInstance('app3').logEvent('app3 page Load'); amplitude.getInstance('app4').init('1d2fe1e104eb3f07a24e94d359f70fd5', 'joe@gmail.com'); amplitude.getInstance('app4').logEvent('app 4 page load'); From 6bc8f697fb636fcee5659a75bb02b8859d2ae9c2 Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Fri, 27 May 2016 15:00:34 -0700 Subject: [PATCH 12/13] update from master --- CHANGELOG.md | 4 + README.md | 8 +- amplitude-snippet.min.js | 2 +- amplitude.js | 103 ++++--- amplitude.min.js | 4 +- component.json | 4 +- documentation/Amplitude.html | 6 +- documentation/AmplitudeClient.html | 4 +- documentation/Identify.html | 2 +- documentation/Revenue.html | 2 +- documentation/amplitude-client.js.html | 4 +- documentation/amplitude.js.html | 6 +- documentation/identify.js.html | 2 +- documentation/index.html | 2 +- documentation/revenue.js.html | 2 +- package.json | 2 +- src/amplitude-client.js | 2 +- src/amplitude-snippet.js | 2 +- src/amplitude.js | 4 +- src/version.js | 2 +- test/ua-parser.js | 357 ------------------------- 21 files changed, 99 insertions(+), 425 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b54f8eb9..002560b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ * Add support for logging events to multiple Amplitude apps. **Note this is a major update, and may break backwards compatability.** See [Readme](https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps) for details. * Init callback now passes the Amplitude instance as an argument to the callback function. +### 2.13.0 (May 26, 2016) + +* Update our fork of [UAParser.js](https://github.com/faisalman/ua-parser-js) from v0.7.7 to v0.7.10. This will improve the resolution of user agent strings to device and OS information. + ### 2.12.1 (April 21, 2016) * Silence console warnings for various UTM property keys with undefined values. diff --git a/README.md b/README.md index c479a935..45aed694 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use ```html diff --git a/documentation/AmplitudeClient.html b/documentation/AmplitudeClient.html index 31868681..0556af0a 100644 --- a/documentation/AmplitudeClient.html +++ b/documentation/AmplitudeClient.html @@ -1607,7 +1607,7 @@

reg
- Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. This uses src/uuid.js to regenerate the deviceId. @@ -2791,7 +2791,7 @@

Home

Classes

  • diff --git a/documentation/Identify.html b/documentation/Identify.html index 948cd089..40bc972c 100644 --- a/documentation/Identify.html +++ b/documentation/Identify.html @@ -1295,7 +1295,7 @@

    Home

    Classes

    • diff --git a/documentation/Revenue.html b/documentation/Revenue.html index a747bab5..9e1c409b 100644 --- a/documentation/Revenue.html +++ b/documentation/Revenue.html @@ -965,7 +965,7 @@

      Home

      Classes

      • diff --git a/documentation/amplitude-client.js.html b/documentation/amplitude-client.js.html index f1fcce55..a1519819 100644 --- a/documentation/amplitude-client.js.html +++ b/documentation/amplitude-client.js.html @@ -634,7 +634,7 @@

        Source: amplitude-client.js

        }; /** - * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. @@ -1170,7 +1170,7 @@

        Home

        Classes

        • diff --git a/documentation/amplitude.js.html b/documentation/amplitude.js.html index 0b30025f..c58c18b3 100644 --- a/documentation/amplitude.js.html +++ b/documentation/amplitude.js.html @@ -175,7 +175,7 @@

          Source: amplitude.js

          * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. - * @deprecatedPlease use amplitude.getInstance().setUserId(userId); + * @deprecated Please use amplitude.getInstance().setUserId(userId); * @example amplitude.setUserId('joe@gmail.com'); */ Amplitude.prototype.setUserId = function setUserId(userId) { @@ -212,7 +212,7 @@

          Source: amplitude.js

          }; /** - * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. @@ -409,7 +409,7 @@

          Home

          Classes

          • diff --git a/documentation/identify.js.html b/documentation/identify.js.html index 476aa835..4b905b49 100644 --- a/documentation/identify.js.html +++ b/documentation/identify.js.html @@ -226,7 +226,7 @@

            Home

            Classes

            • diff --git a/documentation/index.html b/documentation/index.html index 900bd3f5..eacf2338 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -56,7 +56,7 @@

              Home

              Classes

              • diff --git a/documentation/revenue.js.html b/documentation/revenue.js.html index f401b350..16e29b87 100644 --- a/documentation/revenue.js.html +++ b/documentation/revenue.js.html @@ -200,7 +200,7 @@

                Home

                Classes

                • diff --git a/package.json b/package.json index f1541ed5..8c89b01a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "amplitude-js", "author": "Amplitude ", - "version": "2.12.1", + "version": "2.13.0", "license": "MIT", "description": "Javascript library for Amplitude Analytics", "keywords": [ diff --git a/src/amplitude-client.js b/src/amplitude-client.js index 0d143e27..300c161d 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -606,7 +606,7 @@ AmplitudeClient.prototype.setOptOut = function setOptOut(enable) { }; /** - * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. diff --git a/src/amplitude-snippet.js b/src/amplitude-snippet.js index 865b3e0c..5251dd8d 100644 --- a/src/amplitude-snippet.js +++ b/src/amplitude-snippet.js @@ -3,7 +3,7 @@ var as = document.createElement('script'); as.type = 'text/javascript'; as.async = true; - as.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.12.1-min.gz.js'; + as.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.13.0-min.gz.js'; as.onload = function() {window.amplitude.runQueuedFunctions();}; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(as, s); diff --git a/src/amplitude.js b/src/amplitude.js index 603f0b0e..839cd2eb 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -147,7 +147,7 @@ Amplitude.prototype.setDomain = function setDomain(domain) { * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. - * @deprecatedPlease use amplitude.getInstance().setUserId(userId); + * @deprecated Please use amplitude.getInstance().setUserId(userId); * @example amplitude.setUserId('joe@gmail.com'); */ Amplitude.prototype.setUserId = function setUserId(userId) { @@ -184,7 +184,7 @@ Amplitude.prototype.setOptOut = function setOptOut(enable) { }; /** - * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. diff --git a/src/version.js b/src/version.js index a3eb76dc..ec89c0bd 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -module.exports = '2.12.1'; +module.exports = '2.13.0'; diff --git a/test/ua-parser.js b/test/ua-parser.js index 78bfb4d6..221795e7 100644 --- a/test/ua-parser.js +++ b/test/ua-parser.js @@ -22,25 +22,6 @@ var browsers = [ ["(device unknown) - Android - Puffin 2.9174AT (AT=android tablet)","Mozilla/5.0 (X11; U; Linux x86_64; en-us) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.35 Puffin/2.9174AT","Chrome","11","Linux","x86_64"], ["(device unknown) - Android 4.1 - AppleWebKit 534.30","Mozilla/5.0 (Linux; U; Android 4.1; en-us; sdk Build/MR1) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.1 Safari/534.30","Android Browser","4","Android","4.1"], ["(device unknown) - Android 4.2 - Safari 535.19","Mozilla/5.0 (Linux; U; Android 4.2; en-us; sdk Build/MR1) AppleWebKit/535.19 (KHTML, like Gecko) Version/4.2 Safari/535.19","Android Browser","4","Android","4.2"], -["3230 - SymbianOS 7.0s","Nokia3230/2.0 (5.0614.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0",undefined,undefined,"Symbian","7.0s"], -["5700 - SymbianOS 9.2 - Safari 413","Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 Nokia5700/3.27; Profile/MIDP-2.0 Configuration/CLDC-1.1) AppleWebKit/413 (KHTML, like Gecko) Safari/413","Safari",undefined,"Symbian","9.2"], -["6120 Classic - SymbianOS 9.2 - Safari 413","Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 Nokia6120c/3.70; Profile/MIDP-2.0 Configuration/CLDC-1.1) AppleWebKit/413 (KHTML, like Gecko) Safari/413","Safari",undefined,"Symbian","9.2"], -["6230","Nokia6230/2.0 (04.44) Profile/MIDP-2.0 Configuration/CLDC-1.1",undefined,undefined,undefined,undefined], -["6230i","Nokia6230i/2.0 (03.80) Profile/MIDP-2.0 Configuration/CLDC-1.1",undefined,undefined,undefined,undefined], -["6600 Smartphone - Symbian OS - Opera 6.20","Mozilla/4.1 (compatible; MSIE 5.0; Symbian OS; Nokia 6600;452) Opera 6.20 [en-US]","Opera","6","Symbian",undefined], -["6630 - SymbianOS 8.0","Nokia6630/1.0 (2.39.15) SymbianOS/8.0 Series60/2.6 Profile/MIDP-2.0 Configuration/CLDC-1.1",undefined,undefined,"Symbian","8.0"], -["6800 - WinCE - IEMobile 7.11 (MSIE 6.0) - Sprint","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11) Sprint:PPC6800 ","IE Mobile","7","Windows","CE"], -["6800 - WinCE - IEMobile 7.11 (MSIE 6.0) - Verizon","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11) XV6800 ","IE Mobile","7","Windows","CE"], -["7100","BlackBerry7100i/4.1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/103",undefined,undefined,"BlackBerry","4.1.0"], -["7250","Nokia7250/1.0 (3.14) Profile/MIDP-1.0 Configuration/CLDC-1.0",undefined,undefined,undefined,undefined], -["8300 Bold","BlackBerry8300/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/107 UP.Link/6.2.3.15.0",undefined,undefined,"BlackBerry","4.2.2"], -["8320 Curve","BlackBerry8320/4.2.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/100",undefined,undefined,"BlackBerry","4.2.2"], -["8330","BlackBerry8330/4.3.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/105",undefined,undefined,"BlackBerry","4.3.0"], -["9000","BlackBerry9000/4.6.0.167 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102",undefined,undefined,"BlackBerry","4.6.0.167"], -["9500","Mozilla/4.0 (compatible; MSIE 5.0; Series80/2.0 Nokia9500/4.51 Profile/MIDP-2.0 Configuration/CLDC-1.1)","IE","5",undefined,undefined], -["9530 Storm","BlackBerry9530/4.7.0.167 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102 UP.Link/6.3.1.20.0",undefined,undefined,"BlackBerry","4.7.0.167"], -["9700","BlackBerry9700/5.0.0.351 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/123",undefined,undefined,"BlackBerry","5.0.0.351"], -["9800 Torch - Safari 534.1","Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1 (KHTML, Like Gecko) Version/6.0.0.141 Mobile Safari/534.1","Mobile Safari","6","BlackBerry",undefined], ["Acer Iconia - Android - 3.0.1 - AppleWebKit 534.13","Mozilla/5.0 (Linux; U; Android 3.0.1; fr-fr; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13","Android Browser","4","Android","3.0.1"], ["Adobe Application Manager 2.0","Adobe Application Manager 2.0",undefined,undefined,undefined,undefined], ["Android - 3.0.1 - Mobile Safari 523.12 - Motorola Xoom","Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2","Android Browser","3","Android","3.0"], @@ -59,7 +40,6 @@ var browsers = [ ["Android 2.2 - Samsung Galaxy - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-ca; GT-P1000M Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], ["Android 4.0.3 - Mobile Safari 534.30 - HTC Sensation","Mozilla/5.0 (Linux; U; Android 4.0.3; de-ch; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30","Android Browser","4","Android","4.0.3"], ["Android 4.0.3 - Mobile Safari 534.30 - Samsung Galaxy S II","Mozilla/5.0 (Linux; U; Android 4.0.3; de-de; Galaxy S II Build/GRJ22) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30","Android Browser","4","Android","4.0.3"], -["Android 4.0.4 - Opera 12.00","Opera/9.80 (Android 4.0.4; Linux; Opera Mobi/ADR-1205181138; U; pl) Presto/2.10.254 Version/12.00","Opera Mobile","12","Android","4.0.4"], ["Android 4.1.2 - Chrome 30.0","Mozilla/5.0 (Linux; Android 4.1.2; SHV-E250S Build/JZO54K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.82 Mobile Safari/537.36","Chrome Mobile",undefined,"Android","4.1.2"], ["Android 4.2 - Firefox 19.0","Mozilla/5.0 (Android 4.2; rv:19.0) Gecko/20121129 Firefox/19.0","Firefox","19","Android","4.2"], ["Android 4.3 - AppleWebKit/536.23","Mozilla/5.0 (Linux; U; Android 4.3; en-us; sdk Build/MR1) AppleWebKit/536.23 (KHTML, like Gecko) Version/4.3 Mobile Safari/536.23","Android Browser","4","Android","4.3"], @@ -73,26 +53,6 @@ var browsers = [ ["Apple iPad 1 - iOS 4_2_1 - Safari 533","Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; ja-jp) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5","Mobile Safari","5","iPad","4.2.1"], ["Apple iPad 2 - iOS 4_3 - Safari 533","Mozilla/5.0 (iPad; U; CPU OS 4_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8F190 Safari/6533.18.5","Mobile Safari","5","iPad","4.3"], ["Apple iPad 2 - iOS 6_0 - Safari 6 (8536.25)","Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25","Mobile Safari","6","iPad","6.0"], -["Arora 0.11.0","Mozilla/5.0 (OS/2; U; OS/2; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Arora/0.11.0 Safari/533.3 ","Arora","0","OS/2",undefined], -["Arora 0.11.1 - WebKit","Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527 (KHTML, like Gecko, Safari/419.3) Arora/0.10.1","Arora","0","Linux",undefined], -["Arora 0.6.0 - (Vista)","Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/527 (KHTML, like Gecko, Safari/419.3) Arora/0.6 (Change: )","Arora","0","Windows","Vista"], -["Arora 0.8.0 - (Windows)","Mozilla/5.0 (Windows; U; ; en-NZ) AppleWebKit/527 (KHTML, like Gecko, Safari/419.3) Arora/0.8.0","Arora","0",undefined,undefined], -["Arora/0.10.2 (BSD/Haiku)","Mozilla/5.0 (Unknown; U; UNIX BSD/SYSV system; C -) AppleWebKit/527 (KHTML, like Gecko, Safari/419.3) Arora/0.10.2","Arora","0","Linux",undefined], -["Ask Jeeves/Teoma","Mozilla/2.0 (compatible; Ask Jeeves/Teoma)",undefined,undefined,undefined,undefined], -["Avant Browser - MSIE 7 (Win XP)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser; Avant Browser; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)","Avant ",undefined,"Windows","XP"], -["Avant Browser 1.2","Avant Browser/1.2.789rel1 (http://www.avantbrowser.com)","Avant ","1",undefined,undefined], -["Bahamas - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; en-us; htc_bahamas Build/CRB17) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], -["Baiduspider ","Baiduspider ( http://www.baidu.com/search/spider.htm)","Baidu","spider","Linux","spider.htm"], -["Barnes & Noble Nook Color - (Masked: IDs as: OS_X 10_5_7) - Safari 530.17","Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7;en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17","Safari","4","Mac","10.5.7"], -["Beamrise - (Win 7) - Webkit 535.8","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.8 (KHTML, like Gecko) Beamrise/17.2.0.9 Chrome/17.0.939.0 Safari/535.8","Chrome","17","Windows","7"], -["Bing Bot 2.0 (renamed Msnbot)","Mozilla/5.0 (compatible; bingbot/2.0 http://www.bing.com/bingbot.htm)",undefined,undefined,undefined,undefined], -["BlackBerry (Google WAP)","BlackBerry7520/4.0.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Browser/5.0.3.3 UP.Link/5.1.2.12 (Google WAP Proxy/1.0)",undefined,undefined,"BlackBerry","4.0.0"], -["Bloglines 3.1","Bloglines/3.1 (http://www.bloglines.com)",undefined,undefined,undefined,undefined], -["Bolt 2.8 (webkit 534.6) (Sony Ericsson K800i)","Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; BOLT/2.800) AppleWebKit/534.6 (KHTML, like Gecko) Version/5.0 Safari/534.6.3","BOLT","2","Windows","XP"], -["C6-01 - Symbian 3 - Safari 525","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaC6-01/011.010; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 BrowserNG/7.2.7.2 3gpp-gba","WebKit","525","Symbian","3"], -["C7 - Symbian 3 - Safari 525","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaC7-00/012.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 BrowserNG/7.2.7.3 3gpp-gba","WebKit","525","Symbian","3"], -["Camino 2.2.1 (Firefox 4.0.1) (OS X 10.6 Intel)","Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 Camino/2.2.1","Camino","2","Mac","10.6"], -["Camino 2.2a1pre (Firefox 4.0.1) (OS X 10.6 Intel)","Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b6pre) Gecko/20100907 Firefox/4.0b6pre Camino/2.2a1pre","Camino","2","Mac","10.6"], ["Chrome 10.0.601.0 (Win 7)","Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14","Chrome","10","Windows","7"], ["Chrome 10.0.613.0 (64 bit)","Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.613.0 Safari/534.15","Chrome","10","Linux","x86_64"], ["Chrome 10.0.613.0 (Ubuntu 32 bit)","Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.613.0 Chrome/10.0.613.0 Safari/534.15","Chromium","10","Linux","10.10"], @@ -151,42 +111,10 @@ var browsers = [ ["Chrome 9.0.597.15 (OS X 10_6_5 Intel)","Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13","Chrome","9","Mac","10.6.5"], ["Chrome 9.0.601.0 (Vista)","Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14","Chrome","9","Windows","Vista"], ["Chrome 9.1.0.0 (Ubuntu 64 bit)","Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/9.1.0.0 Safari/540.0","Chrome","9","Linux","10.10"], -["Chromium 25.0.1364 (Ubuntu 32 bit)","Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML like Gecko) Ubuntu Chromium/25.0.1364.160 Chrome/25.0.1364.160 Safari/537.22","Chromium","25","Linux","Chromium"], -["Chromium 36.0.1985.125 (64 bit)","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/36.0.1985.125 Safari/537.36","Chrome","36","Linux","x86_64"], -["Chromium 39.0.2166.2 (32 bit)","Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2166.2 Safari/537.36","Chrome","39","Linux","i686"], -["Chromium 41.0.2227.0 (64 bit)","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36","Chrome","41","Linux","x86_64"], -["Desire - Android 2.1 - Mobile Safari 530.17","Mozilla/5.0 (Linux; U; Android 2.1-update1; de-de; HTC Desire 1.19.161.5 Build/ERE27) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.1"], -["Dillo 3.0","Mozilla/4.0 (compatible; Dillo 3.0)","Dillo","3",undefined,undefined], -["DoCoMo 2.0","DoCoMo/2.0 SH901iC(c100;TB;W24H12)",undefined,undefined,undefined,undefined], -["Download Demon","Download Demon/3.5.0.11",undefined,undefined,undefined,undefined], ["Dream - Android 1.5 - Mobile Safari 525","HTC_Dream Mozilla/5.0 (Linux; U; Android 1.5; en-ca; Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], ["Droid - Android 2.0 - Mobile Safari 530.17","Mozilla/5.0 (Linux; U; Android 2.0; en-us; Droid Build/ESD20) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.0"], ["Droid V2.2 - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], -["E50 - SymbianOS 9.1 - Safari 413 es50","Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 (KHTML, like Gecko) Safari/413 es50","Safari",undefined,"Symbian","9.1"], -["E6-00 - SymbianOS 3 - Safari 533.4","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaE6-00/021.002; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.16 Mobile Safari/533.4 3gpp-gba","NokiaBrowser","7","Symbian","3"], -["E63 - SymbianOS 9.2 - UCWEB 8.8 (webkit)","UCWEB/8.8 (SymbianOS/9.2; U; en-US; NokiaE63) AppleWebKit/534.1 UCBrowser/8.8.0.245 Mobile","UCBrowser","8","Symbian","9.2"], -["E65 - SymbianOS 9.1 - Safari 413 es65","Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 (KHTML, like Gecko) Safari/413 es65","Safari",undefined,"Symbian","9.1"], -["E7 - Symbian 3 - Safari 525","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaE7-00/010.016; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 BrowserNG/7.2.7.3 3gpp-gba","WebKit","525","Symbian","3"], -["E70 - SymbianOS 9.1 - Safari 413 es70","Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 (KHTML, like Gecko) Safari/413 es70","Safari",undefined,"Symbian","9.1"], -["E90 - SymbianOS 9.2 - Safari 413","Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaE90-1/07.24.0.3; Profile/MIDP-2.0 Configuration/CLDC-1.1 ) AppleWebKit/413 (KHTML, like Gecko) Safari/413 UP.Link/6.2.3.18.0","Safari",undefined,"Symbian","9.2"], -["e900 - Opera/Netfront","SEC-SGHE900/1.0 NetFront/3.2 Profile/MIDP-2.0 Configuration/CLDC-1.1 Opera/8.01 (J2ME/MIDP; Opera Mini/2.0.4509/1378; nl; U; ssr)","Opera Mini","2",undefined,undefined], -["ELinks 0.12~pre5-4","ELinks/0.12~pre5-4",undefined,undefined,undefined,undefined], -["ELinks 0.4.3 (NetBSD)","ELinks (0.4.3; NetBSD 3.0.2PATCH sparc64; 141x19)","Links","0","Linux","3.0.2PATCH"], -["Elinks 0.4pre5","ELinks (0.4pre5; Linux 2.6.10-ac7 i686; 80x33)","Links","0","Linux","2.6.10"], -["ELinks 0.9.3 (Kanotix)","ELinks/0.9.3 (textmode; Linux 2.6.9-kanotix-8 i686; 127x41)",undefined,undefined,"Linux","2.6.9"], -["Email Wolf","EmailWolf 1.00",undefined,undefined,undefined,undefined], -["EMobile 7.11 (MSIE 6 - Win CE)","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11) ","IE Mobile","7","Windows","CE"], -["Epiphany - WebKit (528.5)","Mozilla/5.0 (X11; U; Linux i686; en-us) AppleWebKit/528.5 (KHTML, like Gecko, Safari/528.5 ) lt-GtkLauncher","Safari",undefined,"Linux","i686"], -["Epiphany 1.2 - Gecko","Mozilla/5.0 (X11; U; Linux; i686; en-US; rv:1.6) Gecko Epiphany/1.2.5","Epiphany","1","Linux",undefined], -["Epiphany 1.4 - Gecko (Ubuntu)","Mozilla/5.0 (X11; U; Linux i586; en-US; rv:1.7.3) Gecko/20040924 Epiphany/1.4.4 (Ubuntu)","Epiphany","1","Linux",undefined], -["Epiphany 2.30.0 (OpenBSD)","Mozilla/5.0 (X11; U; OpenBSD arm; en-us) AppleWebKit/531.2 (KHTML, like Gecko) Safari/531.2 Epiphany/2.30.0","Epiphany","2","Linux","arm"], -["EudoraWeb 2.1 (PalmOS)","Mozilla/1.22 (compatible; MSIE 5.01; PalmOS 3.0) EudoraWeb 2.1","IE","5","PalmOS","3.0"], -["everyfeed spider 2.0","everyfeed-spider/2.0 (http://www.everyfeed.com)",undefined,undefined,undefined,undefined], -["Evo - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], -["Exabot 3.0","Mozilla/5.0 (compatible; Exabot/3.0; http://www.exabot.com/go/robot) ",undefined,undefined,undefined,undefined], ["Facebook Scraper 1.0","facebookscraper/1.0( http://www.facebook.com/sharescraper_help.php)",undefined,undefined,undefined,undefined], -["FAST-WebCrawler 3.8","FAST-WebCrawler/3.8 (crawler at trd dot overture dot com; http://www.alltheweb.com/help/webmaster/crawler)",undefined,undefined,undefined,undefined], -["Firebird 0.6 (SunOs)","Mozilla/5.0 (X11; U; SunOS sun4m; en-US; rv:1.4b) Gecko/20030517 Mozilla Firebird/0.6","Firebird","0","Solaris","sun4"], ["Firefox 0.8","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6) Gecko/20040614 Firefox/0.8","Firefox","0","Linux","i686"], ["Firefox 0.9 (OS X Mach)","Mozilla/5.0 (Macintosh; U; Mac OS X Mach-O; en-US; rv:2.0a) Gecko/20040614 Firefox/3.0.0 ","Firefox","3","Mac","Mach"], ["Firefox 10.0.1 (64 bit)","Mozilla/5.0 (X11; Linux x86_64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1","Firefox","10","Linux","x86_64"], @@ -263,20 +191,11 @@ var browsers = [ ["Firefox 7.0a1 (64 bit)","Mozilla/5.0 (X11; Linux x86_64; rv:7.0a1) Gecko/20110623 Firefox/7.0a1","Firefox","7","Linux","x86_64"], ["Firefox 8.0 (32 bit)","Mozilla/5.0 (X11; Linux i686; rv:8.0) Gecko/20100101 Firefox/8.0","Firefox","8","Linux","i686"], ["Firefox 9.0 (OS X 10.6 Intel)","Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0","Firefox","9","Mac","10.6"], -["Firefox Fennec 1.0.a1 (Linux arm)","Mozilla/5.0 (X11; U; Linux armv61; en-US; rv:1.9.1b2pre) Gecko/20081015 Fennec/1.0a1","Fennec","1","Linux","armv61"], -["Firefox Fennec 10.0.1 (Maemo arm)","Mozilla/5.0 (Maemo; Linux armv7l; rv:10.0.1) Gecko/20100101 Firefox/10.0.1 Fennec/10.0.1","Fennec","10","Linux","armv7l"], -["Firefox Fennec 2.0.1 (Maemo arm)","Mozilla/5.0 (Maemo; Linux armv7l; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 Fennec/2.0.1","Fennec","2","Linux","armv7l"], -["Fusic LX550","LG-LX550 AU-MIC-LX550/2.0 MMP/2.0 Profile/MIDP-2.0 Configuration/CLDC-1.1",undefined,undefined,undefined,undefined], -["Gaisbot 3.0","Gaisbot/3.0 (robot@gais.cs.ccu.edu.tw; http://gais.cs.ccu.edu.tw/robot.php)",undefined,undefined,undefined,undefined], ["Galaxy (Verizon) - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], ["Galaxy - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; de-de; Galaxy Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], ["Galaxy - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-ca; GT-P1000M Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], ["Galaxy S 3 (SPH-L710) - Android 4.3 - Chrome 32.0.1700.99","Mozilla/5.0 (Linux; Android 4.3; SPH-L710 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.99 Mobile Safari/537.36","Chrome Mobile",undefined,"Android","4.3"], ["Galaxy S II - Android 4.0.3 - Mobile Safari 534.30","Mozilla/5.0 (Linux; U; Android 4.0.3; de-de; Galaxy S II Build/GRJ22) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30","Android Browser","4","Android","4.0.3"], -["Galeon 1.3","Mozilla/5.0 (X11; U; Linux; i686; en-US; rv:1.6) Gecko Galeon/1.3.14",undefined,undefined,"Linux",undefined], -["Galeon 1.3.15 (FreeBSD)","Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.6) Gecko/20040406 Galeon/1.3.15","Mozilla","5","Linux","i386"], -["Galeon 2.0.6 (Gentoo)","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080716 (Gentoo) Galeon/2.0.6","Mozilla","5","Linux",undefined], -["Galeon 2.0.6 (Ubuntu)","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Galeon/2.0.6 (Ubuntu 2.0.6-2)",undefined,undefined,"Linux","2.0.6-2"], ["Google AdsBot 1.0","AdsBot-Google ( http://www.google.com/adsbot.html)",undefined,undefined,undefined,undefined], ["Google Feed Fetcher","FeedFetcher-Google; ( http://www.google.com/feedfetcher.html)",undefined,undefined,undefined,undefined], ["Google Toolbar 4.0 (XP - MSIE 6)","Mozilla/4.0 (compatible; GoogleToolbar 4.0.1019.5266-big; Windows XP 5.1; MSIE 6.0.2900.2180)","IE","6","Windows"," X"], @@ -290,29 +209,8 @@ var browsers = [ ["Googlebot-Mobile 2.1 (ID: SAMSUNG-SGH-E250)","SAMSUNG-SGH-E250/1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Browser/6.2.3.3.c.1.101 (GUI) MMP/2.0 (compatible; Googlebot-Mobile/2.1; http://www.google.com/bot.html)",undefined,undefined,undefined,undefined], ["Googlebot-News","Googlebot-News",undefined,undefined,undefined,undefined], ["Googlebot-Video","Googlebot-Video/1.0",undefined,undefined,undefined,undefined], -["Gregarius 0.5.2","Gregarius/0.5.2 ( http://devlog.gregarius.net/docs/ua)",undefined,undefined,undefined,undefined], -["Grub-Client","grub-client-1.5.3; (grub-client-1.5.3; Crawl your own stuff with http://grub.org)",undefined,undefined,undefined,undefined], -["Grub-Client","grub-client-1.5.3; (grub-client-1.5.3; Crawl your own stuff with http://grub.org)",undefined,undefined,undefined,undefined], -["GT-P7100 tablet - Android 3.0.1 - AppleWebKit 534.13","Mozilla/5.0 (Linux; U; Android 3.0.1; en-us; GT-P7100 Build/HRI83) AppleWebkit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13","Android Browser","4","Android","3.0.1"], -["gulperbot","Gulper Web Bot 0.2.4 (www.ecsl.cs.sunysb.edu/~maxim/cgi-bin/Link/GulperBot)",undefined,undefined,undefined,undefined], ["Hero - Android 1.5 - Mobile Safari 525.20","Mozilla/5.0 (Linux; U; Android 1.5; de-ch; HTC Hero Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], -["HP Touchpad 1.0 - WebOS 3.0.2 - wOSBrowser 234.40.1","Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0","Safari",undefined,"Linux",undefined], ["HTMLParser (1.60)","HTMLParser/1.6",undefined,undefined,undefined,undefined], -["Iceape (SeaMonkey) 1.1.9 (Debian)","Mozilla/5.0 (X11; U; Linux ppc; en-US; rv:1.8.1.13) Gecko/20080313 Iceape/1.1.9 (Debian-1.1.9-5)","Iceape","1","Linux","1.1.9-5"], -["Iceape (SeaMonkey) 2.0.8 (Debian)","Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.13) Gecko/20100916 Iceape/2.0.8","Iceape","2","Linux","x86_64"], -["Iceweasel (Firefox) 14.0.1","Mozilla/5.0 (X11; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1 Iceweasel/14.0.1","Iceweasel","14","Linux","i686"], -["Iceweasel (Firefox) 15.02 (Debian)","Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20120724 Debian Iceweasel/15.02","Iceweasel","15","Linux","Iceweasel"], -["Iceweasel (Firefox) 19.0.2 (Debian 64)","Mozilla/5.0 (X11; Linux x86_64; rv:19.0) Gecko/20100101 Firefox/19.0 Iceweasel/19.0.2","Iceweasel","19","Linux","x86_64"], -["Iceweasel (Firefox) 3.6.3 (Debian)","Mozilla/5.0 (X11; U; Linux i686; pt-PT; rv:1.9.2.3) Gecko/20100402 Iceweasel/3.6.3 (like Firefox/3.6.3) GTB7.0","Iceweasel","3","Linux","i686"], -["Iceweasel (Firefox) 5.0 (Debian 64)","Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 Iceweasel/5.0","Iceweasel","5","Linux","x86_64"], -["Iceweasel (Firefox) 6.0a2 (Debian 32)","Mozilla/5.0 (X11; Linux i686; rv:6.0a2) Gecko/20110615 Firefox/6.0a2 Iceweasel/6.0a2","Iceweasel","6","Linux","i686"], -["Iconia Tablet - Android - 3.0.1 - AppleWebKit 534.13","Mozilla/5.0 (Linux; U; Android 3.0.1; fr-fr; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13","Android Browser","4","Android","3.0.1"], -["IEMobile 10.0 - WinPhone OS 8.0 - ARM","Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch) ","IE Mobile","10","Windows Phone","8.0"], -["IEMobile 6.12 (Win CE) (with zune id)","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 6.12; Microsoft ZuneHD 4.3)","IE Mobile","6","Windows","CE"], -["IEMobile 7.0 (MSIE 7.0) - WinPhone OS 7.0 - Asus Galaxy","Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0) Asus;Galaxy6","IE Mobile","7","Windows Phone","7.0"], -["IEMobile 7.5 (MSIE 9 - WP7.5)","Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)","IE Mobile","9","Windows Phone","7.5"], -["IEMobile 9.0 - WinPhone OS 7.5","Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)","IE Mobile","9","Windows Phone","7.5"], -["Incredible - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-us; ADR6300 Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], ["iOS 1.0 - iPhone - Safari 419.3","Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420 (KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3","Mobile Safari","3","Mac",undefined], ["iOS 2.0 - iPhone - Safari 525.200","Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_0 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5A347 Safari/525.200","Mobile Safari","3","iPhone","2.0"], ["iOS 2.2.1 - iPod - Safari 525.20","Mozilla/5.0 (iPod; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11a Safari/525.20","Mobile Safari","3","iPhone","2.2.1"], @@ -340,195 +238,12 @@ var browsers = [ ["iPod Touch - iOS 2.2.1 - Safari 525.20","Mozilla/5.0 (iPod; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11a Safari/525.20","Mobile Safari","3","iPhone","2.2.1"], ["iPod Touch - iOS 3_1_1 - Safari 528.16","Mozilla/5.0 (iPod; U; CPU iPhone OS 3_1_1 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7C145","WebKit","528","iPhone","3.1.1"], ["iPod Touch - iOS 7_1 - Safari 7.0/537.51.2","Mozilla/5.0 (iPod touch; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML like Gecko) Version/7.0 Mobile/11D167 Safari/123E71C","Mobile Safari","7","iPhone","7.1"], -["iTunes 4.2 (OS X 10.2 PPC)","iTunes/4.2 (Macintosh; U; PPC Mac OS X 10.2)",undefined,undefined,"Mac","10.2"], -["iTunes 4.2 (OS X 10.2 PPC)","iTunes/4.2 (Macintosh; U; PPC Mac OS X 10.2)",undefined,undefined,"Mac","10.2"], -["iTunes 9.0.2 (Windows)","iTunes/9.0.2 (Windows; N)",undefined,undefined,undefined,undefined], -["iTunes 9.0.3 (OS X 10_6_2)","iTunes/9.0.3 (Macintosh; U; Intel Mac OS X 10_6_2; en-ca)",undefined,undefined,"Mac","10.6.2"], -["Java 1.6.0_13","Java/1.6.0_13",undefined,undefined,undefined,undefined], -["Jet","SAMSUNG-S8000/S8000XXIF3 SHP/VPP/R5 Jasmine/1.0 Nextreaming SMM-MMS/1.2.0 profile/MIDP-2.1 configuration/CLDC-1.1 FirePHP/0.3","Jasmine","1",undefined,undefined], -["K310iv","SonyEricssonK310iv/R4DA Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.1.13.0","NetFront","3",undefined,undefined], -["K550i","SonyEricssonK550i/R1JD Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["K610i","SonyEricssonK610i/R1CB Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["K750i","SonyEricssonK750i/R1CA Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration/CLDC-1.1",undefined,undefined,undefined,undefined], -["K800 - Opera 9.8","Opera/9.80 (J2ME/MIDP; Opera Mini/5.0.16823/1428; U; en) Presto/2.2.0","Opera Mini","5",undefined,undefined], -["K800i","SonyEricssonK800i/R1CB Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.0.0.0","NetFront","3",undefined,undefined], -["K810i","SonyEricssonK810i/R1KG Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["Kindle 2.0 - Linux","Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 600x800)","Kindle","2","Linux","2.6.22"], -["Kindle 3.0 - AppleWebKit 528.5 - Linux","Mozilla/5.0 (Linux U; en-US) AppleWebKit/528.5 (KHTML, like Gecko, Safari/528.5 ) Version/4.0 Kindle/3.0 (screen 600x800; rotate)","Kindle","3","Linux","U"], -["Kindle Fire - Android 4.0.3 - Silk 2.1 (AppleWebKit 535.19)","Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; KFTT Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Silk/2.1 Mobile Safari/535.19 Silk-Accelerated=true","Silk","2","Android","4.0.3"], -["Kindle Fire - Silk/2.1 (AppleWebKit 535.19) - Android","Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; KFTT Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Silk/2.1 Mobile Safari/535.19 Silk-Accelerated=true","Silk","2","Android","4.0.3"], -["Konqueror 3 rc4 - khtml","Konqueror/3.0-rc4; (Konqueror/3.0-rc4; i686 Linux;;datecode)","Konqueror","3","Linux",undefined], -["Konqueror 3.3 - khtml (Gentoo)","Mozilla/5.0 (compatible; Konqueror/3.3; Linux 2.6.8-gentoo-r3; X11;","Konqueror","3","Linux","r3"], -["Konqueror 3.5 - khtml (Debian)","Mozilla/5.0 (compatible; Konqueror/3.5; Linux 2.6.30-7.dmz.1-liquorix-686; X11) KHTML/3.5.10 (like Gecko) (Debian package 4:3.5.10.dfsg.1-1 b1)","Konqueror","3","Linux","package"], -["Konqueror 3.5 - khtml (NetBSD 4.0)","Mozilla/5.0 (compatible; Konqueror/3.5; NetBSD 4.0_RC3; X11) KHTML/3.5.7 (like Gecko)","Konqueror","3","Linux","4.0_RC3"], -["Konqueror 3.5.1 - khtml (SunOS)","Mozilla/5.0 (compatible; Konqueror/3.5; SunOS) KHTML/3.5.1 (like Gecko)","Konqueror","3","Solaris",undefined], -["Konqueror 3.5.6 - khtml (Kubuntu)","Mozilla/5.0 (compatible; Konqueror/3.5; Linux; en_US) KHTML/3.5.6 (like Gecko) (Kubuntu)","Konqueror","3","Linux",undefined], -["Konqueror 4.1 - khtml (DragonFly)","Mozilla/5.0 (compatible; Konqueror/4.1; DragonFly) KHTML/4.1.4 (like Gecko)","Konqueror","4","Linux",undefined], -["Konqueror 4.1 - khtml (OpenBSD)","Mozilla/5.0 (compatible; Konqueror/4.1; OpenBSD) KHTML/4.1.4 (like Gecko)","Konqueror","4","Linux",undefined], -["Konqueror 4.3 - khtml (Slackware 13)","Mozilla/5.0 (compatible; Konqueror/4.2; Linux) KHTML/4.2.4 (like Gecko) Slackware/13.0","Konqueror","4","Linux","13.0"], -["Konqueror 4.3.1 - khtml (Fedora 11)","Mozilla/5.0 (compatible; Konqueror/4.3; Linux) KHTML/4.3.1 (like Gecko) Fedora/4.3.1-3.fc11","Konqueror","4","Linux","4.3.1-3.fc11"], -["Konqueror 4.4.3 - khtml (Fedora 12)","Mozilla/5.0 (compatible; Konqueror/4.4; Linux) KHTML/4.4.1 (like Gecko) Fedora/4.4.1-1.fc12","Konqueror","4","Linux","4.4.1-1.fc12"], -["Konqueror 4.4.3 - khtml (Kubuntu)","Mozilla/5.0 (compatible; Konqueror/4.4; Linux 2.6.32-22-generic; X11; en_US) KHTML/4.4.3 (like Gecko) Kubuntu","Konqueror","4","Linux",undefined], -["Konqueror 4.4.3 - khtml (Kubuntu)","Mozilla/5.0 (compatible; Konqueror/4.4; Linux 2.6.32-22-generic; X11; en_US) KHTML/4.4.3 (like Gecko) Kubuntu","Konqueror","4","Linux",undefined], -["Konqueror 4.5 (Win XP - KDE native)","Mozilla/5.0 (compatible; Konqueror/4.5; Windows) KHTML/4.5.4 (like Gecko)","Konqueror","4",undefined,undefined], -["Konqueror 4.5.4 - khtml (FreeBSD)","Mozilla/5.0 (compatible; Konqueror/4.5; FreeBSD) KHTML/4.5.4 (like Gecko)","Konqueror","4","Linux",undefined], -["Konqueror 4.5.4 - khtml (NetBSD 5.0.2)","Mozilla/5.0 (compatible; Konqueror/4.5; NetBSD 5.0.2; X11; amd64; en_US) KHTML/4.5.4 (like Gecko)","Konqueror","4","Linux","5.0.2"], -["Konqueror 4.8.4 - khtml (Debian)","Mozilla/5.0 (X11; Linux 3.8-6.dmz.1-liquorix-686) KHTML/4.8.4 (like Gecko) Konqueror/4.8","Konqueror","4","Linux","3.8"], -["Konqueror 4.9 - khtml","Mozilla/5.0 (X11; Linux) KHTML/4.9.1 (like Gecko) Konqueror/4.9","Konqueror","4","Linux",undefined], -["L7","MOT-L7v/08.B7.5DR MIB/2.2.1 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.0.0.0",undefined,undefined,undefined,undefined], -["Legend - Android 2.1 - Mobile Safari 530.17","Mozilla/5.0 (Linux; U; Android 2.1; en-us; HTC Legend Build/cupcake) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.1"], -["libwww-perl 5.820","libwww-perl/5.820",undefined,undefined,undefined,undefined], -["Links 0.9.1","Links/0.9.1 (Linux 2.4.24; i386;)",undefined,undefined,"Linux","2.4.24"], -["Links 2.1 (FreeBSD)","Links (2.1pre15; FreeBSD 5.3-RELEASE i386; 196x84)","Links","2","Linux","5.3"], -["Links 2.1","Links (2.1pre15; Linux 2.4.26 i686; 158x61)","Links","2","Linux","2.4.26"], -["Links 2.3pre1","Links (2.3pre1; Linux 2.6.38-8-generic x86_64; 170x48)","Links","2","Linux","2.6.38"], -["Links 2.8.7","Lynx/2.8.7dev.4 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.8d","Lynx","2",undefined,undefined], -["Linux - Fennec 2.0.1 (686 on x86_64)","Mozilla/5.0 (X11; Linux i686 on x86_64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 Fennec/2.0.1","Fennec","2","Linux","i686"], -["Lumia 620 ARM - Windows Phone OS 8.0 - IEMobile 10.0","Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)","IE Mobile","10","Windows Phone","8.0"], -["Lynx 2.8.5rel.1","Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/0.8.12","Lynx","2","Linux","TLS"], -["Maemo - Fennec 2.0.1 (arm)","Mozilla/5.0 (Maemo; Linux armv7l; rv:2.0.1) Gecko/20100101 Firefox/4.0.1 Fennec/2.0.1","Fennec","2","Linux","armv7l"], -["Magic - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; de-de; HTC Magic Build/PLAT-RC33) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1 FirePHP/0.3","Android Browser","3","Android","1.5"], -["Maxthon 2.0 (Trident/MSIE) (Win 7)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Maxthon 2.0)","Maxthon","2","Windows","7"], -["Maxthon 3.0.8.2 (Webkit) (Vista)","Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.1 (KHTML, like Gecko) Maxthon/3.0.8.2 Safari/533.1","Maxthon","3","Windows","Vista"], -["Maxthon 4.0.0.2000 (Webkit) (Win7 64 bit)","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML like Gecko) Maxthon/4.0.0.2000 Chrome/22.0.1229.79 Safari/537.1","Maxthon","4","Windows","7"], -["MDA Pro - Win CE","Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; MDA Pro/1.0 Profile/MIDP-2.0 Configuration/CLDC-1.1)","IE","4","Windows","CE"], -["Midori 0.1.10 (Webkit 531)","Midori/0.1.10 (X11; Linux i686; U; en-us) WebKit/(531).(2) ","Midori","0","Linux","i686"], -["Milestone - Android 2.0 - Mobile Safari 530.17","Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.0"], -["Milestone Android 2.0.1 - Mobile Safari 530.17","Mozilla/5.0 (Linux; U; Android 2.0.1; de-de; Milestone Build/SHOLS_U2_01.14.0) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.0.1"], -["Minefield (Firefox Nightly) 4.0b2pre","Mozilla/5.0 (X11; Linux x86_64; en-US; rv:2.0b2pre) Gecko/20100712 Minefield/4.0b2pre","Mozilla","5","Linux","x86_64"], -["Minefield (Firefox nightly) 4.0b4pre (Win 7)","Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b4pre) Gecko/20100815 Minefield/4.0b4pre","Mozilla","5","Windows","7"], -["Minimo 0.016 (Win CE)","Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016","Minimo","0","Windows","CE 5.1"], -["Minimo 0.020 (Linux)","Mozilla/5.0 (X11; U; Linux armv6l; rv 1.8.1.5pre) Gecko/20070619 Minimo/0.020","Minimo","0","Linux","armv6l"], -["Minimo 0.025 (Linux arm)","Mozilla/5.0 (X11; U; Linux arm7tdmi; rv:1.8.1.11) Gecko/20071130 Minimo/0.025","Minimo","0","Linux","arm7tdmi"], -["Mobile Safari 530.17 (Android)","Mozilla/5.0 (Linux; U; Android 2.0; en-us; Droid Build/ESD20) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.0"], -["Moment - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; en-us; SPH-M900 Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], ["Motorola Xoom - Android 3.0.1 - Mobile Safari 523.12","Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2","Android Browser","3","Android","3.0"], -["Mozilla 1.6 (Debian)","Mozilla/5.0 (X11; U; Linux; i686; en-US; rv:1.6) Gecko Debian/1.6-7",undefined,undefined,"Linux","1.6-7"], -["Mozilla 1.7 (FreeBSD)","Mozilla/5.0 (X11; U; FreeBSD; i386; en-US; rv:1.7) Gecko",undefined,undefined,"Linux",undefined], -["Mozilla 1.9.0 (Debian)","Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.3) Gecko/2008092814 (Debian-3.0.1-1)","Mozilla","5","Linux","3.0.1-1"], -["Mozilla 1.9a3pre","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a3pre) Gecko/20070330","Mozilla","5","Linux","i686"], -["MS URL Control","Microsoft URL Control - 6.00.8862",undefined,undefined,undefined,undefined], -["MSIE 10 - compat mode (Win 7 64)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0)","IE","7","Windows","7"], -["MSIE 10 - standard mode (Win 7 64)","Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)","IE","10","Windows","7"], -["MSIE 10.6 - (Win 7 32)","Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0","IE","10","Windows","7"], -["MSIE 11.0 (compatibility mode IE 7)- (Win 8.1 32)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)","IE","7","Windows","8.1"], -["MSIE 11.0 - (Win 7 64)","Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko","IE","11","Windows","7"], -["MSIE 11.0 - (Win 8.1 32)","Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko","IE","11","Windows","8.1"], -["MSIE 5.15 (OS 9)","Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC)","IE","5",undefined,undefined], -["MSIE 5.5 (Win 2000)","Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0 )","IE","5","Windows","2000"], -["MSIE 5.5 (Win ME)","Mozilla/4.0 (compatible; MSIE 5.5; Windows 98; Win 9x 4.90)","IE","5","Windows","98"], -["MSIE 6 (Win XP)","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)","IE","6","Windows","XP"], -["MSIE 7 (Vista)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)","IE","7","Windows","Vista"], -["MSIE 8 - compat mode (Vista)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0)","IE","7","Windows","Vista"], -["MSIE 8 - standard mode (Vista)","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)","IE","8","Windows","Vista"], -["MSIE 8 - standard mode (Win 7)","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)","IE","8","Windows","7"], -["MSIE 8 - standard mode (Win XP)","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)","IE","8","Windows","XP"], -["MSIE 9 - compat mode (Vista)","Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/5.0)","IE","7","Windows","Vista"], -["MSIE 9 - standard mode (NT 6.2 32 Win 8)","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.2; Trident/5.0)","IE","9","Windows","8"], -["MSIE 9 - standard mode (NT 6.2 64 Win 8)","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.2; WOW64; Trident/5.0)","IE","9","Windows","8"], -["MSIE 9 - standard mode (Win 7)","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)","IE","9","Windows","7"], -["MSIE 9 - standard mode (with Zune plugin) (NT 6.1 Win 7)","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7)","IE","9","Windows","7"], -["Msnbot 0.11 (beta version)","msnbot/0.11 ( http://search.msn.com/msnbot.htm)",undefined,undefined,"Linux",".msn.com"], -["Msnbot 1.0 (current version)","msnbot/1.0 ( http://search.msn.com/msnbot.htm)",undefined,undefined,"Linux",".msn.com"], -["Msnbot 1.1","msnbot/1.1 ( http://search.msn.com/msnbot.htm)",undefined,undefined,"Linux",".msn.com"], -["Msnbot-Media 1.1","msnbot-media/1.1 ( http://search.msn.com/msnbot.htm)",undefined,undefined,"Linux",".msn.com"], -["Multizilla 1.6 (Win XP)","Mozilla/5.0 (Windows; U; Windows XP) Gecko MultiZilla/1.6.1.0a",undefined,undefined,"Windows"," X"], -["N70","NokiaN70-1/5.0609.2.0.1 Series60/2.8 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.1.13.0",undefined,undefined,undefined,undefined], -["N73 (Service)","NokiaN73-1/3.0649.0.0.1 Series60/3.0 Profile/MIDP2.0 Configuration/CLDC-1.1",undefined,undefined,undefined,undefined], -["N73 - SymbianOS 9.1 - Safari 413","Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 (KHTML, like Gecko) Safari/413","Safari",undefined,"Symbian","9.1"], -["N8 - Symbian 3 - Safari 525","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaN8-00/014.002; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 BrowserNG/7.2.6.4 3gpp-gba","WebKit","525","Symbian","3"], -["N80 - SymbianOS 9.1 - Safari 413","Mozilla/5.0 (SymbianOS/9.1; U; en-us) AppleWebKit/413 (KHTML, like Gecko) Safari/413","Safari",undefined,"Symbian","9.1"], -["N9 - MeeGo - Safari 534.13","Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13","NokiaBrowser","8","MeeGo",undefined], -["N93 - SymbianOS 9.1 - Safari 413","Mozilla/5.0 (SymbianOS/9.1; U; de) AppleWebKit/413 (KHTML, like Gecko) Safari/413","Safari",undefined,"Symbian","9.1"], -["N95 - SymbianOS 9.2 - Safari 413","Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaN95/10.0.018; Profile/MIDP-2.0 Configuration/CLDC-1.1) AppleWebKit/413 (KHTML, like Gecko) Safari/413 UP.Link/6.3.0.0.0","Safari",undefined,"Symbian","9.2"], -["N950 - MeeGo - Safari 534.13","Mozilla/5.0 (MeeGo; NokiaN950-00/00) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13","NokiaBrowser","8","MeeGo",undefined], -["N97 - SymbianOS 9.4 - WicKed 7.1.12344","Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/10.0.012; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) WicKed/7.1.12344","WebKit","525","Symbian","9.4"], -["Namoroka 3.6.15 (Firefox) (NetBSD)","Mozilla/5.0 (X11; U; NetBSD amd64; en-US; rv:1.9.2.15) Gecko/20110308 Namoroka/3.6.15","Mozilla","5","Linux","amd64"], -["NEC n410i i-Mode","portalmmm/2.0 N410i(c20;TB) ",undefined,undefined,undefined,undefined], -["Net Positive 2.1","Mozilla/3.0 (compatible; NetPositive/2.1.1; BeOS)",undefined,undefined,"BeOS",undefined], -["NetFront 3.0 (PalmOS)","Mozilla/4.0 (PDA; PalmOS/sony/model prmr/Revision:1.1.54 (en)) NetFront/3.0","NetFront","3","PalmOS","sony"], -["Netscape 2.02 (Win 95)","Mozilla/2.02E (Win95; U)",undefined,undefined,"Windows","95"], -["Netscape 3.01 gold (Win 95)","Mozilla/3.01Gold (Win95; I)",undefined,undefined,"Windows","95"], -["Netscape 4.77 (Irix)","Mozilla/4.77 [en] (X11; I; IRIX;64 6.5 IP30)",undefined,undefined,undefined,undefined], -["Netscape 4.8 (SunOS)","Mozilla/4.8 [en] (X11; U; SunOS; 5.7 sun4u)",undefined,undefined,"Solaris",undefined], -["Netscape 4.8 (Win XP)","Mozilla/4.8 [en] (Windows NT 5.1; U)",undefined,undefined,"Windows","XP"], -["Netscape 7.1 (Win 98)","Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.4) Gecko Netscape/7.1 (ax)","Netscape","7","Windows","98"], -["NetSurf 1.2 (NetBSD)","NetSurf/1.2 (NetBSD; amd64)","NetSurf","1",undefined,undefined], ["Nexus 5 - Android 4.4 - AppleWebKit/536.23","Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36","Chrome Mobile",undefined,"Android","4.4"], ["Nexus 7 - Android 4.4.4 - AppleWebKit/537.36","Mozilla/5.0 (Linux; Android 4.4.4; Nexus 7 Build/KTU84P) AppleWebKit/537.36 (KHTML like Gecko) Chrome/36.0.1985.135 Safari/537.36","Chrome","36","Android","4.4.4"], ["Nexus One - Android 2.1 - Mobile Safari 530.17","Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17","Android Browser","4","Android","2.1"], ["Nexus One - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], -["Nokia (6100) WAP","Nokia6100/1.0 (04.01) Profile/MIDP-1.0 Configuration/CLDC-1.0",undefined,undefined,undefined,undefined], -["Nokia 6630","Nokia6630/1.0 (2.3.129) SymbianOS/8.0 Series60/2.6 Profile/MIDP-2.0 Configuration/CLDC-1.1",undefined,undefined,"Symbian","8.0"], -["Nook 2 (limited data)","nook browser/1.0",undefined,undefined,undefined,undefined], -["Nook Color - Android - IDs as: OS_X 10_5_7 - Safari 530.17","Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7;en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17","Safari","4","Mac","10.5.7"], -["Nook Tablet - Android 2.3.4 - Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; BNTV250 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Safari/533.1","Android Browser","4","Android","2.3.4"], -["Novarra-Vision 6.9","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 MG(Novarra-Vision/6.9)","Firefox","1","Linux","i686"], -["Offline Explorer 2.5","Offline Explorer/2.5",undefined,undefined,undefined,undefined], -["Omniweb 563.15 (OS X PPC)","Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US) AppleWebKit/125.4 (KHTML, like Gecko, Safari) OmniWeb/v563.15","OmniWeb","563","Mac",undefined], -["Omniweb 622.8.0 (OS X 10_5_6 Intel)","Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/528.16 (KHTML, like Gecko, Safari/528.16) OmniWeb/v622.8.0","OmniWeb","622","Mac","10.5.6"], -["Omniweb 622.8.0 (OS X Intel)","Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US) AppleWebKit/528.16 (KHTML, like Gecko, Safari/528.16) OmniWeb/v622.8.0.112941","OmniWeb","622","Mac",undefined], -["Opera 10.00 Mobi - SymbOS","Opera/9.80 (S60; SymbOS; Opera Mobi/499; U; ru) Presto/2.4.18 Version/10.00","Opera Mobile","10","Symbian",undefined], -["Opera 10.10 (id as 9.8) (Server 2003)","Opera/9.80 (Windows NT 5.2; U; en) Presto/2.2.15 Version/10.10","Opera","10","Windows","XP"], -["Opera 10.10 (id as 9.8)","Opera/9.80 (X11; Linux i686; U; en) Presto/2.2.15 Version/10.10","Opera","10","Linux","i686"], -["Opera 10.61 (id as 9.8) (OS X Intel)","Opera/9.80 (Macintosh; Intel Mac OS X; U; en) Presto/2.6.30 Version/10.61","Opera","10","Mac",undefined], -["Opera 10.61 Mini 5.1 (J2ME/MIDP)","Opera/10.61 (J2ME/MIDP; Opera Mini/5.1.21219/19.999; en-US; rv:1.9.3a5) WebKit/534.5 Presto/2.6.30","Opera Mini","5",undefined,undefined], -["Opera 11.00 (id as 9.8) (OS X Intel)","Opera/9.80 (Macintosh; Intel Mac OS X 10.4.11; U; en) Presto/2.7.62 Version/11.00","Opera","11","Mac","10.4.11"], -["Opera 11.00 (id as 9.8)","Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00","Opera","11","Linux","x86_64"], -["Opera 11.01 (id as 9.8) (Win 7)","Opera/9.80 (Windows NT 6.1; U; en) Presto/2.7.62 Version/11.01","Opera","11","Windows","7"], -["Opera 11.10 (id as 9.8) (Win XP)","Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10","Opera","11","Windows","XP"], -["Opera 11.1010 Mini 7.5.33361 (Android)","Opera/9.80 (Android; Opera Mini/7.5.33361/31.1543; U; en) Presto/2.8.119 Version/11.1010","Opera Mini","7","Android",undefined], -["Opera 11.52 (id as 9.8) (OS X Intel)","Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52","Opera","11","Mac","10.6.8"], -["Opera 12.00 (id as 9.8) (Win 7)","Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00","Opera","12","Windows","7"], -["Opera 12.10 (FreeBSD)","Opera/9.80 (X11; FreeBSD 8.1-RELEASE i386; Edition Next) Presto/2.12.388 Version/12.10","Opera","12","Linux","8.1"], -["Opera 12.14 (id as 9.8) (Win Vista)","Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14","Opera","12","Windows","Vista"], -["Opera 12.16 (id as 9.8) (Win 7)","Opera/9.80 (Windows NT 6.1; WOW64) Presto/2.12.388 Version/12.16","Opera","12","Windows","7"], -["Opera 12.16 (id as 9.8, last presto)","Opera/9.80 (X11; Linux i686) Presto/2.12.388 Version/12.16","Opera","12","Linux","i686"], -["Opera 14.0.1116.4 (Webkit 537.36) (Win 7)","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.12 Safari/537.36 OPR/14.0.1116.4","Opera","14","Windows","7"], -["Opera 15.0.1147.24 (Webkit 537.36) (Win 7)","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.29 Safari/537.36 OPR/15.0.1147.24 (Edition Next)","Opera","15","Windows","7"], -["Opera 18.0.1284.49 (Webkit 537.36) (Win 8)","Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36 OPR/18.0.1284.49","Opera","18","Windows","8.1"], -["Opera 19.0.1326.56 (Webkit 537.36) (Win 7)","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.76 Safari/537.36 OPR/19.0.1326.56","Opera","19","Windows","7"], -["Opera 20.0.1387.91 (Webkit 537.36) (Win 7 64 bit)","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36 OPR/20.0.1387.91","Opera","20","Windows","7"], -["Opera 20.0.1396 (Webkit 537.36)","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.166 Safari/537.36 OPR/20.0.1396.73172","Opera","20","Linux","x86_64"], -["Opera 7.23","MSIE (MSIE 6.0; X11; Linux; i686) Opera 7.23","Opera","7","Linux",undefined], -["Opera 7.5 (Win ME)","Opera/7.50 (Windows ME; U) [en]","Opera","7","Windows"," M"], -["Opera 7.5 (Win XP)","Opera/7.50 (Windows XP; U)","Opera","7","Windows"," X"], -["Opera 7.51 (Win XP)","Opera/7.51 (Windows NT 5.1; U) [en]","Opera","7","Windows","XP"], -["Opera 8.0 (Win 2000)","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.0","Opera","8","Windows","2000"], -["Opera 9.0 (OS X PPC)","Opera/9.0 (Macintosh; PPC Mac OS X; U; en)","Opera","9","Mac",undefined], -["Opera 9.20 (OS X Intel)","Opera/9.20 (Macintosh; Intel Mac OS X; U; en)","Opera","9","Mac",undefined], -["Opera 9.25 - (Vista)","Opera/9.25 (Windows NT 6.0; U; en)","Opera","9","Windows","Vista"], -["Opera 9.51 beta (Windows)","Opera/9.51 Beta (Microsoft Windows; PPC; Opera Mobi/1718; U; en)","Opera","9",undefined,undefined], -["Opera 9.60 Mini 4.1 beta (Windows)","Opera/9.60 (J2ME/MIDP; Opera Mini/4.1.11320/608; U; en) Presto/2.2.0","Opera Mini","4",undefined,undefined], -["Opera 9.60 Mini 4.2 J2ME/MIDP","Opera/9.60 (J2ME/MIDP; Opera Mini/4.2.14320/554; U; cs) Presto/2.2.0","Opera Mini","4",undefined,undefined], -["Opera 9.64 (Linux Mint)","Opera/9.64 (X11; Linux i686; U; Linux Mint; nb) Presto/2.1.1","Opera","9","Linux",undefined], -["Opera 9.64 (OS X PPC)","Opera/9.64 (Macintosh; PPC Mac OS X; U; en) Presto/2.1.1","Opera","9","Mac",undefined], -["P900 - Opera 8.0 Mini","Opera/8.01 (J2ME/MIDP; Opera Mini/1.0.1479/HiFi; SonyEricsson P900; no; U; ssr)","Opera Mini","1",undefined,undefined], -["Palm WebOS 1.3 - Safari 525","Mozilla/5.0 (webOS/1.3; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Desktop/1.0","Safari","1","webOS","1.3"], -["PalmSource hspr-H102 - Palm Treo 650","Mozilla/4.0 (compatible; MSIE 6.0; Windows 98; PalmSource/hspr-H102; Blazer/4.0) 16;320x320","Blazer","4","Windows","98"], -["Peach 1.01","Peach/1.01 (Ubuntu 8.04 LTS; U; en)",undefined,undefined,"Linux","8.04"], -["Phoenix 0.2 (NT 4.0)","Mozilla/5.0 (Windows; U; WinNT4.0; en-US; rv:1.2b) Gecko/20021001 Phoenix/0.2","Phoenix","0","Windows","NT 4.0"], -["Playbook (tablet) - OS 2.1.0 - Safari 536.2+","Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+","Safari","7","RIM Tablet OS","2.1.0"], -["Playstation 3 (1.10)","Mozilla/5.0 (PLAYSTATION 3; 1.10)",undefined,undefined,"Linux","3"], -["Playstation 3 (2.00)","Mozilla/5.0 (PLAYSTATION 3; 2.00)",undefined,undefined,"Linux","3"], -["Polaris 6.01","POLARIS/6.01 (BREW 3.1.5; U; en-us; LG; LX265; POLARIS/6.01/WAP) MMP/2.0 profile/MIDP-2.1 Configuration/CLDC-1.1","POLARIS","6",undefined,undefined], -["Pre3 - webOS 2.2.4 - wOSBrowser 221.56","Mozilla/5.0 (Linux; webOS/2.2.4; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) webOSBrowser/221.56 Safari/534.6 Pre/3.0 ","Safari",undefined,"webOS","2.2.4"], -["PSP (2.00)","Mozilla/4.0 (PSP (PlayStation Portable); 2.00)",undefined,undefined,"Linux","Portable"], -["Puffin 2.9174AP - Android - (AP=Android Phone)","Mozilla/5.0 (X11; U; Linux x86_64; en-gb) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.35 Puffin/2.9174AP","Chrome","11","Linux","x86_64"], -["Puffin 2.9174AT - Android - (AT=Android Tablet)","Mozilla/5.0 (X11; U; Linux x86_64; en-us) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.35 Puffin/2.9174AT","Chrome","11","Linux","x86_64"], -["Puffin 3.9174IP - iOS 6_1 - (IP=iphone)","Mozilla/5.0 (iPod; U; CPU iPhone OS 6_1 like Mac OS X; en-HK) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.35 Puffin/3.9174IP Mobile ","Chrome","11","iPhone","6.1"], -["Puffin 3.9174IT -(says Linux) - (IT=iOS tablet)","Mozilla/5.0 (X11; U; Linux x86_64; en-AU) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.35 Puffin/3.9174IT ","Chrome","11","Linux","x86_64"], -["Puffin2.0.5603M - Linux - (M=mobile)","Mozilla/5.0 (X11; U; Linux i686; en-gb) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/11.0.696.65 Safari/534.35 Puffin/2.0.5603M","Chrome","11","Linux","i686"], ["Python-urllib 2.5","Python-urllib/2.5",undefined,undefined,undefined,undefined], -["QupZilla 1.2.0 (Webkit 534.34)","Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.34 (KHTML, like Gecko) QupZilla/1.2.0 Safari/534.34","Safari",undefined,"Linux","i686"], -["QupZilla 1.3.1","Mozilla/5.0 (OS/2; U; OS/2; en-US) AppleWebKit/533.3 (KHTML, like Gecko) QupZilla/1.3.1 Safari/533.3 ","Safari",undefined,"OS/2",undefined], -["Razr V9","MOT-V9mm/00.62 UP.Browser/6.2.3.4.c.1.123 (GUI) MMP/2.0",undefined,undefined,undefined,undefined], -["ReqwirelessWeb 3.5","Mozilla/4.0 (compatible; MSIE 6.0; j2me) ReqwirelessWeb/3.5","IE","6",undefined,undefined], -["RIM (Blackberry) Playbook - OS 2.1.0 - Safari 536.2+","Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+","Safari","7","RIM Tablet OS","2.1.0"], -["RIZR - Symbian OS - Opera 8.65","MOTORIZR-Z8/46.00.00 Mozilla/4.0 (compatible; MSIE 6.0; Symbian OS; 356) Opera 8.65 [it] UP.Link/6.3.0.0.0","Opera","8","Symbian",undefined], -["Roku DVP-4.1","Roku/DVP-4.1 (024.01E01250A)",undefined,undefined,undefined,undefined], -["Rumor2 LX265 - Polaris","POLARIS/6.01(BREW 3.1.5;U;en-us;LG;LX265;POLARIS/6.01/WAP;)MMP/2.0 profile/MIDP-201 Configuration /CLDC-1.1","POLARIS","6",undefined,undefined], -["S500i","SonyEricssonS500i/R6BC Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], ["Safari 125.8 (OS X PPC)","Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8","Safari","1","Mac",undefined], ["Safari 312.3 (OS X PPC)","Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3","Safari","1","Mac",undefined], ["Safari 419.3 (OS X PPC)","Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3","Safari","2","Mac",undefined], @@ -555,92 +270,20 @@ var browsers = [ ["Samsung Galaxy - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; de-de; Galaxy Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], ["Samsung Galaxy - Android 2.2 - Mobile Safari 533.1","Mozilla/5.0 (Linux; U; Android 2.2; en-ca; GT-P1000M Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","Android Browser","4","Android","2.2"], ["Samsung GT-P7100 - Android 3.0.1 - AppleWebKit 534.13","Mozilla/5.0 (Linux; U; Android 3.0.1; en-us; GT-P7100 Build/HRI83) AppleWebkit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13","Android Browser","4","Android","3.0.1"], -["Satio - Safari 525","Mozilla/5.0 (SymbianOS/9.4; U; Series60/5.0 SonyEricssonP100/01; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 Safari/525","Safari","3","Symbian","9.4"], -["SeaMonkey (Mozilla) 2.0.12 (Win 7)","Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.1.17) Gecko/20110123 (like Firefox/3.x) SeaMonkey/2.0.12","Firefox","3","Windows","7"], -["SeaMonkey (Mozilla) 2.7.1 (NT 5.2)","Mozilla/5.0 (Windows NT 5.2; rv:10.0.1) Gecko/20100101 Firefox/10.0.1 SeaMonkey/2.7.1","Firefox","10","Windows","XP"], -["SeaMonkey (Mozilla) 2.9 (Win7 64 bit)","Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20120422 Firefox/12.0 SeaMonkey/2.9","Firefox","12","Windows","7"], -["SeaMonkey 1.1.18 (Win XP)","Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.23) Gecko/20090825 SeaMonkey/1.1.18","SeaMonkey","1","Windows","XP"], -["Seamonkey 1.1.8 (Mozilla) (SunOS 32bit)","Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.8.1.12) Gecko/20080303 SeaMonkey/1.1.8","SeaMonkey","1","Solaris","i86"], -["SeaMonkey 1.5a","Mozilla/5.0 (BeOS; U; BeOS BePC; en-US; rv:1.9a1) Gecko/20060702 SeaMonkey/1.5a","SeaMonkey","1","BeOS",undefined], -["SeaMonkey 2.0.12 (Mozilla)","Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.17) Gecko/20110123 SeaMonkey/2.0.12","SeaMonkey","2","Linux","x86_64"], -["SeaMonkey 2.21 - OS/2 Warp 4.5","Mozilla/5.0 (OS/2; Warp 4.5; rv:24.0) Gecko/20100101 Firefox/24.0 SeaMonkey/2.21 ","Firefox","24","OS/2",undefined], -["Seamonkey 2.25 (Firefox/28.0) (FreeBSD)","Mozilla/5.0 (X11; FreeBSD i386; rv:28.0) Gecko/20100101 Firefox/28.0 SeaMonkey/2.25","Firefox","28","Linux","i386"], -["SeaMonkey 2.7.1 (Mozilla)","Mozilla/5.0 (X11; Linux i686; rv:10.0.1) Gecko/20100101 Firefox/10.0.1 SeaMonkey/2.7.1","Firefox","10","Linux","i686"], -["SeaMonkey 2.7.1 (OS X 10.5 - Mozilla)","Mozilla/5.0 (Macintosh; Intel Mac OS X 10.5; rv:10.0.1) Gecko/20100101 Firefox/10.0.1 SeaMonkey/2.7.1","Firefox","10","Mac","10.5"], -["SeaMonkey 2.7.2 - OS/2 Warp 4.5","Mozilla/5.0 (OS/2; Warp 4.5; rv:10.0.12) Gecko/20130108 Firefox/10.0.12 SeaMonkey/2.7.2","Firefox","10","OS/2",undefined], -["SeaMonkey 2.9.1 (Mozilla)","Mozilla/5.0 (X11; Linux i686; rv:12.0) Gecko/20120502 Firefox/12.0 SeaMonkey/2.9.1","Firefox","12","Linux","i686"], -["Sensation - Android 4.0.3 - Mobile Safari 534.30","Mozilla/5.0 (Linux; U; Android 4.0.3; de-ch; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30","Android Browser","4","Android","4.0.3"], -["SGH X210 (WML)","SEC-SGHX210/1.0 UP.Link/6.3.1.13.0",undefined,undefined,undefined,undefined], -["SGH-A867 - Netfront","SAMSUNG-SGH-A867/A867UCHJ3 SHP/VPP/R5 NetFront/35 SMM-MMS/1.2.0 profile/MIDP-2.0 configuration/CLDC-1.1 UP.Link/6.3.0.0.0","NetFront","35",undefined,undefined], -["Shadowfox 7.0 (Mozilla)","Mozilla/5.0 (X11; U; Linux x86_64; us; rv:1.9.1.19) Gecko/20110430 shadowfox/7.0 (like Firefox/7.0","Firefox","7","Linux","x86_64"], -["Silk 1.0.13 (AppleWebKit533.16) 2.9 (Mac OS X 10_6_3)","Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.13.81_10003810) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true","Silk","1","Mac","10.6.3"], -["SM-T537A - Android 4.4.2 - Chrome 35.0.1916.141","Mozilla/5.0 (Linux; Android 4.4.2; SAMSUNG-SM-T537A Build/KOT49H) AppleWebKit/537.36 (KHTML like Gecko) Chrome/35.0.1916.141 Safari/537.36","Chrome","35","Android","4.4.2"], -["Spica - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; fr-fr; GT-I5700 Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], -["ST7377 - Win XP - Opera 9.5","HTC-ST7377/1.59.502.3 (67150) Opera/9.50 (Windows NT 5.1; U; en) UP.Link/6.3.1.17.0","Opera","9","Windows","XP"], -["SuperBot 4.4.0 (Win XP)","SuperBot/4.4.0.60 (Windows XP)",undefined,undefined,"Windows"," X"], -["Swiftfox 2.0","Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Firefox/2.0 (Swiftfox)","Swiftfox",undefined,"Linux","i686"], -["Swiftfox 3.6.3","Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9.2.3) Gecko/20100406 Firefox/3.6.3 (Swiftfox)","Swiftfox",undefined,"Linux","i686"], -["Symbian 3 - N8 - Safari 525","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaN8-00/014.002; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 BrowserNG/7.2.6.4 3gpp-gba","WebKit","525","Symbian","3"], -["Symbian 3 - Nokia X7 - Safari 533.4","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaX7-00/021.004; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.21 Mobile Safari/533.4 3gpp-gba","NokiaBrowser","7","Symbian","3"], -["SymbianOS 9.2 - Nokia E90 - Safari","Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaE90-1/07.24.0.3; Profile/MIDP-2.0 Configuration/CLDC-1.1 ) AppleWebKit/413 (KHTML, like Gecko) Safari/413 UP.Link/6.2.3.18.0","Safari",undefined,"Symbian","9.2"], -["SymbianOS 9.4 - Nokia N97 - WicKed 7.1.12344","Mozilla/5.0 (SymbianOS 9.4; Series60/5.0 NokiaN97-1/10.0.012; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) WicKed/7.1.12344","WebKit","525","Symbian","9.4"], -["SymbOS - Opera 10.00 Mobi","Opera/9.80 (S60; SymbOS; Opera Mobi/499; U; ru) Presto/2.4.18 Version/10.00","Opera Mobile","10","Symbian",undefined], ["T-Mobile G1 - Android 1.0 - Mobile Safari 523.12.2","Mozilla/5.0 (Linux; U; Android 1.0; en-us; dream) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2","Android Browser","3","Android","1.0"], ["T-Mobile G1 - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; en-us; T-Mobile G1 Build/CRB43) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari 525.20.1","Android Browser","3","Android","1.5"], ["T-Mobile G2 - Android 1.5 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.5; en-gb; T-Mobile_G2_Touch Build/CUPCAKE) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.5"], -["T100 (WML)","SonyEricssonT100/R101",undefined,undefined,undefined,undefined], -["T610","SonyEricssonT610/R201 Profile/MIDP-1.0 Configuration/CLDC-1.0",undefined,undefined,undefined,undefined], -["T650i","SonyEricssonT650i/R7AA Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["T68 (WML)","SonyEricssonT68/R201A",undefined,undefined,undefined,undefined], -["Tattoo - Android 1.6 - Mobile Safari/525.20.1","Mozilla/5.0 (Linux; U; Android 1.6; en-us; HTC_TATTOO_A3288 Build/DRC79) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.6"], -["Touchpad 1.0 - WebOS 3.0.2 - wOSBrowser 234.40.1","Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.2; U; de-DE) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.40.1 Safari/534.6 TouchPad/1.0","Safari",undefined,"Linux",undefined], -["Treo 650 - PalmSource","Mozilla/4.0 (compatible; MSIE 6.0; Windows 98; PalmSource/hspr-H102; Blazer/4.0) 16;320x320","Blazer","4","Windows","98"], -["UCBrowser 2.9.0 - Trident/MSIE 9.0 - WindowsPhone 7","Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; XBLWP7; ZuneWP7) UCBrowser/2.9.0.263","IE","9","Windows","7"], -["UCBrowser 8.6.1 - Webkit 533 - Android 2.3.3","Mozilla/5.0 (Linux; U; Android 2.3.3; en-us ; LS670 Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1/UCBrowser/8.6.1.262/145/355","UCBrowser","8","Android","2.3.3"], -["Uzbl (Webkit 1.3)","Uzbl (Webkit 1.3) (Linux i686 [i686])",undefined,undefined,"Linux","i686"], -["V177","MOT-V177/0.1.75 UP.Browser/6.2.3.9.c.12 (GUI) MMP/2.0 UP.Link/6.3.1.13.0",undefined,undefined,undefined,undefined], -["Viewty","LG-GC900/V10a Obigo/WAP2.0 Profile/MIDP-2.1 Configuration/CLDC-1.1",undefined,undefined,undefined,undefined], -["Vodafone 1.0","Vodafone/1.0/V802SE/SEJ001 Browser/SEMC-Browser/4.1",undefined,undefined,undefined,undefined], ["W3C (X)HTML Validator (1.305.2.12)","W3C_Validator/1.305.2.12 libwww-perl/5.64",undefined,undefined,undefined,undefined], ["W3C (X)HTML Validator (1.654)","W3C_Validator/1.654",undefined,undefined,undefined,undefined], ["W3C CSS Validator","Jigsaw/2.2.5 W3C_CSS_Validator_JFouffa/2.0",undefined,undefined,undefined,undefined], ["W3C P3P Validator","P3P Validator",undefined,undefined,undefined,undefined], ["w3m 0.5.1","w3m/0.5.1","w3m","0",undefined,undefined], ["w3m 0.5.1","w3m/0.5.1","w3m","0",undefined,undefined], -["W580i","SonyEricssonW580i/R6BC Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["W660i","SonyEricssonW660i/R6AD Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["W810i","SonyEricssonW810i/R4EA Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.0.0.0","NetFront","3",undefined,undefined], -["W850i","SonyEricssonW850i/R1ED Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["W950i - Opera 8.60 - Symbian OS","SonyEricssonW950i/R100 Mozilla/4.0 (compatible; MSIE 6.0; Symbian OS; 323) Opera 8.60 [en-US]","Opera","8","Symbian",undefined], -["W995","SonyEricssonW995/R1EA Profile/MIDP-2.1 Configuration/CLDC-1.1 UNTRUSTED/1.0",undefined,undefined,undefined,undefined], ["WDG (X)HTML Validator (1.6.2)","WDG_Validator/1.6.2",undefined,undefined,undefined,undefined], ["WDG CSS Validator (1.2.2)","CSSCheck/1.2.2",undefined,undefined,undefined,undefined], ["Web Downloader 6.9","Web Downloader/6.9",undefined,undefined,undefined,undefined], -["WebCopier v4.6","WebCopier v4.6",undefined,undefined,undefined,undefined], -["WebZIP 3.5","WebZIP/3.5 (http://www.spidersoft.com)",undefined,undefined,undefined,undefined], ["Wget 1.9 (Redhat)","Wget/1.9 cvs-stable (Red Hat modified)",undefined,undefined,undefined,undefined], ["Wget 1.9.1","Wget/1.9.1",undefined,undefined,undefined,undefined], -["Wii 2.0.4.7-7","Opera/9.30 (Nintendo Wii; U; ; 2047-7; en)","Opera","9","Linux","Wii"], -["Wii libnup (1.00)","wii libnup/1.0",undefined,undefined,undefined,undefined], -["Windows CE - MSIE 6 - IEMobile 7.11","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11)","IE Mobile","7","Windows","CE"], -["Windows CE - MSIE 6 - IEMobile 8.12","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 8.12; MSIEMobile6.0)","IE Mobile","8","Windows","CE"], -["Windows CE - ZuneHD 4.3 - IEMobile 6.12","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 6.12; Microsoft ZuneHD 4.3)","IE Mobile","6","Windows","CE"], -["Windows CE 5.2 - Sprint (HTC Titan) - IEMobile 7.11 (MSIE 6.0)","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 7.11) Sprint:PPC6800","IE Mobile","7","Windows","CE"], -["Windows Phone 7 - MSIE 7 - IEMobile 7.0","Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0)","IE Mobile","7","Windows Phone","7.0"], -["Windows Phone OS 7.0 - Asus Galaxy - IEMobile 7.0 (MSIE 7.0)","Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0) Asus;Galaxy6","IE Mobile","7","Windows Phone","7.0"], -["Windows Phone OS 7.5 - IEMobile 9.0","Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)","IE Mobile","9","Windows Phone","7.5"], -["Windows Phone OS 8.0 - ARM - IEMobile 10.0","Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch) ","IE Mobile","10","Windows Phone","8.0"], -["Windows Phone OS 8.0 - Nokia Lumia 620 ARM - IEMobile 10.0","Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)","IE Mobile","10","Windows Phone","8.0"], -["winHTTP","SearchExpress",undefined,undefined,"Linux","Express"], ["X10 - Android 1.6 - Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.6; es-es; SonyEricssonX10i Build/R1FA016) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.6"], ["X10i - Android 1.6 - Mobile Safari 525.20.1","Mozilla/5.0 (Linux; U; Android 1.6; en-us; SonyEricssonX10i Build/R1AA056) AppleWebKit/528.5 (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1","Android Browser","3","Android","1.6"], -["X7 - Symbian 3 - Safari 533.4","Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaX7-00/021.004; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.21 Mobile Safari/533.4 3gpp-gba","NokiaBrowser","7","Symbian","3"], -["X820","SEC-SGHX820/1.0 NetFront/3.2 Profile/MIDP-2.0 Configuration/CLDC-1.1","NetFront","3",undefined,undefined], -["Xoom - Android 3.0.1 - Mobile Safari 523.12","Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2","Android Browser","3","Android","3.0"], -["Xperia X1 - Opera Mobi 9.5","Opera/9.5 (Microsoft Windows; PPC; Opera Mobi; U) SonyEricssonX1i/R2AA Profile/MIDP-2.0 Configuration/CLDC-1.1","Opera","9",undefined,undefined], -["Yahoo Slurp China","Mozilla/5.0 (compatible; Yahoo! Slurp China; http://misc.yahoo.com.cn/help.html)",undefined,undefined,undefined,undefined], -["Yahoo Slurp","Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)",undefined,undefined,"Linux","slurp"], -["Z10 - BB10 OS - Mobile Safari 537.10+","Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.1.0.2342 Mobile Safari/537.10+","Mobile Safari","10","BlackBerry","10"], -["Z800i","SonyEricssonZ800/R1Y Browser/SEMC-Browser/4.1 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.0.0.0",undefined,undefined,undefined,undefined], -["ZuneHD 4.3 - IEMobile 6.12 - CE","Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile 6.12; Microsoft ZuneHD 4.3)","IE Mobile","6","Windows","CE"], ]; \ No newline at end of file From b95fd4b6ba386370d1372adcb11843f3087944ae Mon Sep 17 00:00:00 2001 From: Daniel Jih Date: Fri, 27 May 2016 15:34:20 -0700 Subject: [PATCH 13/13] v3.0.0 --- CHANGELOG.md | 2 ++ README.md | 6 +++--- amplitude-snippet.min.js | 2 +- amplitude.js | 2 +- amplitude.min.js | 2 +- component.json | 2 +- package.json | 2 +- src/amplitude-snippet.js | 2 +- src/version.js | 2 +- 9 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002560b8..38b11e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased +### 3.0.0 (May 27, 2016) + * Add support for logging events to multiple Amplitude apps. **Note this is a major update, and may break backwards compatability.** See [Readme](https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps) for details. * Init callback now passes the Amplitude instance as an argument to the callback function. diff --git a/README.md b/README.md index 45aed694..975bb360 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use ```html