diff --git a/CHANGELOG.md b/CHANGELOG.md index 9845fd22..de2f98d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,9 @@ ## 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. * Fix bug where saveReferrer throws exception if sessionStorage is disabled. * Log messages with a try/catch to support IE 8. * Validate event properties during logEvent and initialization before sending request. * Add instructions for proper integration with RequireJS. -* Init callback now passes the Amplitude instance as an argument to the callback function. ## 2.9.0 (January 15, 2016) diff --git a/README.md b/README.md index a848d855..cb4454e2 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ Amplitude-Javascript ```html @@ -29,83 +29,11 @@ Amplitude-Javascript 4. To track an event anywhere on the page, call: ```javascript - amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE'); + amplitude.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). Here is our help article on [Event Taxonomy and Best Practices](https://amplitude.zendesk.com/hc/en-us/articles/211988918-How-to-Approach-Your-Event-Taxonomy). @@ -115,19 +43,19 @@ It's important to think about what types of events you care about as a developer If your app has its own login system that you want to track users with, you can call `setUserId` at any time: ```javascript -amplitude.getInstance().setUserId('USER_ID_HERE'); +amplitude.setUserId('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 may set the userId to `null` like so: ```javascript -amplitude.getInstance().setUserId(null); // not string 'null' +amplitude.setUserId(null); // not string 'null' ``` You can also add the user ID as an argument to the `init` call: ```javascript -amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); +amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); ``` # Setting Event Properties # @@ -137,7 +65,7 @@ You can attach additional data to any event by passing a Javascript object as th ```javascript var eventProperties = {}; eventProperties.key = 'value'; -amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE', eventProperties); +amplitude.logEvent('EVENT_IDENTIFIER_HERE', eventProperties); ``` Alternatively, you can set multiple event properties like this: @@ -147,7 +75,7 @@ var eventProperties = { 'age': 20, 'key': 'value' }; -amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE', eventProperties); +amplitude.logEvent('EVENT_IDENTIFIER_HERE', eventProperties); ``` # User Properties and User Property Operations # @@ -158,38 +86,38 @@ 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.getInstance().identify(identify); + amplitude.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.getInstance().identify(identify); + amplitude.identify(identify); var identify = new amplitude.Identify().setOnce('sign_up_date', '09/14/2015'); - amplitude.getInstance().identify(identify); + amplitude.identify(identify); ``` 3. `unset`: this will unset and remove a user property. ```javascript var identify = new amplitude.Identify().unset('gender').unset('age'); - amplitude.getInstance().identify(identify); + amplitude.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.getInstance().identify(identify); + amplitude.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.getInstance().identify(identify); + amplitude.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: @@ -199,7 +127,7 @@ var identify = new amplitude.Identify() .set('karma', 10) .add('karma', 1) .unset('karma'); -amplitude.getInstance().identify(identify); +amplitude.identify(identify); ``` ### Arrays in User Properties ### @@ -211,7 +139,7 @@ var identify = new amplitude.Identify() .set('colors', ['rose', 'gold']) .append('ab-tests', 'campaign_a') .append('existing_list', [4, 5]); -amplitude.getInstance().identify(identify); +amplitude.identify(identify); ``` ### Setting Multiple Properties with `setUserProperties` ### @@ -223,7 +151,7 @@ var userProperties = { gender: 'female', age: 20 }; -amplitude.getInstance().setUserProperties(userProperties); +amplitude.setUserProperties(userProperties); ``` ### Clearing User Properties ### @@ -231,7 +159,7 @@ amplitude.getInstance().setUserProperties(userProperties); You may use `clearUserProperties` to clear all user properties at once. Note: the result is irreversible! ```javascript -amplitude.getInstance().clearUserProperties(); +amplitude.clearUserProperties(); ``` # Tracking Revenue # @@ -239,7 +167,7 @@ amplitude.getInstance().clearUserProperties(); To track revenue from a user, call ```javascript -amplitude.getInstance().logRevenue(9.99, 1, 'product'); +amplitude.logRevenue(9.99, 1, 'product'); ``` The function takes a unit price, a quantity, and a product identifier. Quantity and product identifier are optional parameters. @@ -251,14 +179,14 @@ This allows us to automatically display data relevant to revenue on the Amplitud You can turn off logging for a given user: ```javascript -amplitude.getInstance().setOptOut(true); +amplitude.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.getInstance().setOptOut(false); +amplitude.setOptOut(false); ``` will reenable logging. @@ -268,7 +196,7 @@ will reenable logging. You can configure Amplitude by passing an object as the third argument to the `init`: ```javascript -amplitude.getInstance().init('YOUR_API_KEY_HERE', null, { +amplitude.init('YOUR_API_KEY_HERE', null, { // optional configuration options saveEvents: true, includeUtm: true, @@ -298,14 +226,14 @@ This SDK automatically grabs useful data about the browser, including browser ty 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.getInstance().setVersionName('VERSION_NAME_HERE'); +amplitude.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.getInstance().setDeviceId('CUSTOM_DEVICE_ID'); +amplitude.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. @@ -314,7 +242,7 @@ amplitude.getInstance().setDeviceId('CUSTOM_DEVICE_ID'); You can pass a callback function to logEvent, which will get called after receiving a response from the server: ```javascript -amplitude.getInstance().logEvent("EVENT_IDENTIFIER_HERE", null, callback_function); +amplitude.logEvent("EVENT_IDENTIFIER_HERE", null, callback_function); ``` The status and response 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: @@ -338,7 +266,7 @@ And then you would define a function that is called when the link is clicked lik ```javascript var trackClickLinkA = function() { - amplitude.getInstance().logEvent('Clicked Link A', null, function() { + amplitude.logEvent('Clicked Link A', null, function() { window.location='LINK_A_URL'; }); }; @@ -347,11 +275,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: the Amplitude instance is passed to the callback function as an argument*: +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*: ```javascript -amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE', null, function(instance) { - console.log(instance.options.deviceId); // access the instance's deviceId after initialization +amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE', null, function() { + console.log(amplitude.options.deviceId); // access Amplitude's deviceId after initialization }); ``` diff --git a/amplitude-segment-snippet.min.js b/amplitude-segment-snippet.min.js index 6b95ad5d..bc41cf19 100644 --- a/amplitude-segment-snippet.min.js +++ b/amplitude-segment-snippet.min.js @@ -1,8 +1,6 @@ -(function(e,t){var n=e.amplitude||{_q:[],_iq:{}};var r=function(){this._q=[];return this; +(function(e,t){var n=e.amplitude||{_q:[]};var r=function(){this._q=[];return this; };function i(e){r.prototype[e]=function(){this._q.push([e].concat(Array.prototype.slice.call(arguments,0))); -return this}}var s=["add","append","clearAll","set","setOnce","unset"];for(var o=0;o this.options.savedMaxCount) { queue.splice(0, queue.length - this.options.savedMaxCount); } }; -AmplitudeClient.prototype.logEvent = function(eventType, eventProperties, callback) { +Amplitude.prototype.logEvent = function(eventType, eventProperties, callback) { if (!this._apiKeySet('logEvent()')) { return -1; } @@ -914,7 +761,7 @@ var _isNumber = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; -AmplitudeClient.prototype.logRevenue = function(price, quantity, product) { +Amplitude.prototype.logRevenue = function(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'); @@ -933,7 +780,7 @@ AmplitudeClient.prototype.logRevenue = function(price, quantity, product) { * Remove events in storage with event ids up to and including maxEventId. Does * a true filter in case events get out of order or old events are removed. */ -AmplitudeClient.prototype.removeEvents = function (maxEventId, maxIdentifyId) { +Amplitude.prototype.removeEvents = function (maxEventId, maxIdentifyId) { if (maxEventId >= 0) { var filteredEvents = []; for (var i = 0; i < this._unsentEvents.length; i++) { @@ -955,7 +802,7 @@ AmplitudeClient.prototype.removeEvents = function (maxEventId, maxIdentifyId) { } }; -AmplitudeClient.prototype.sendEvents = function(callback) { +Amplitude.prototype.sendEvents = function(callback) { if (!this._apiKeySet('sendEvents()')) { return; } @@ -1024,7 +871,7 @@ AmplitudeClient.prototype.sendEvents = function(callback) { } }; -AmplitudeClient.prototype._mergeEventsAndIdentifys = function(numEvents) { +Amplitude.prototype._mergeEventsAndIdentifys = function(numEvents) { // coalesce events from both queues var eventsToSend = []; var eventIndex = 0; @@ -1069,10 +916,17 @@ AmplitudeClient.prototype._mergeEventsAndIdentifys = function(numEvents) { }; }; -module.exports = AmplitudeClient; +/** + * @deprecated + */ +Amplitude.prototype.setGlobalUserProperties = Amplitude.prototype.setUserProperties; + +Amplitude.prototype.__VERSION__ = version; -}, {"./cookiestorage":10,"./utm":11,"./identify":4,"json":12,"./localstorage":13,"JavaScript-MD5":14,"object":5,"./xhr":15,"ua-parser-js":16,"./utils":7,"./uuid":17,"./version":8,"./type":6,"./options":9}], -10: [function(require, module, exports) { +module.exports = Amplitude; + +}, {"./cookiestorage":3,"./utm":4,"./identify":5,"json":6,"./localstorage":7,"JavaScript-MD5":8,"object":9,"./xhr":10,"./type":11,"ua-parser-js":12,"./utils":13,"./uuid":14,"./version":15,"./options":16}], +3: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -1165,8 +1019,8 @@ cookieStorage.prototype.getStorage = function() { module.exports = cookieStorage; -}, {"./cookie":18,"json":12,"./localstorage":13}], -18: [function(require, module, exports) { +}, {"./cookie":17,"json":6,"./localstorage":7}], +17: [function(require, module, exports) { /* * Cookie data */ @@ -1295,8 +1149,8 @@ module.exports = { }; -}, {"./base64":19,"json":12,"top-domain":20}], -19: [function(require, module, exports) { +}, {"./base64":18,"json":6,"top-domain":19}], +18: [function(require, module, exports) { /* jshint bitwise: false */ /* global escape, unescape */ @@ -1395,8 +1249,8 @@ var Base64 = { module.exports = Base64; -}, {"./utf8":21}], -21: [function(require, module, exports) { +}, {"./utf8":20}], +20: [function(require, module, exports) { /* jshint bitwise: false */ /* @@ -1456,7 +1310,7 @@ var UTF8 = { module.exports = UTF8; }, {}], -12: [function(require, module, exports) { +6: [function(require, module, exports) { var json = window.JSON || {}; var stringify = json.stringify; @@ -1466,8 +1320,8 @@ module.exports = parse && stringify ? JSON : require('json-fallback'); -}, {"json-fallback":22}], -22: [function(require, module, exports) { +}, {"json-fallback":21}], +21: [function(require, module, exports) { /* json2.js 2014-02-04 @@ -1957,7 +1811,7 @@ module.exports = parse && stringify }()); }, {}], -20: [function(require, module, exports) { +19: [function(require, module, exports) { /** * Module dependencies. @@ -2005,8 +1859,8 @@ function domain(url){ return match ? match[0] : ''; }; -}, {"url":23}], -23: [function(require, module, exports) { +}, {"url":22}], +22: [function(require, module, exports) { /** * Parse the given `url`. @@ -2091,7 +1945,7 @@ function port (protocol){ } }, {}], -13: [function(require, module, exports) { +7: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -2195,7 +2049,7 @@ if (!localStorage) { module.exports = localStorage; }, {}], -11: [function(require, module, exports) { +4: [function(require, module, exports) { var getUtmParam = function(name, query) { name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); @@ -2224,7 +2078,7 @@ var getUtmData = function(rawCookie, query) { module.exports = getUtmData; }, {}], -4: [function(require, module, exports) { +5: [function(require, module, exports) { var type = require('./type'); var utils = require('./utils'); @@ -2311,8 +2165,8 @@ Identify.prototype._addOperation = function(operation, property, value) { module.exports = Identify; -}, {"./type":6,"./utils":7}], -6: [function(require, module, exports) { +}, {"./type":11,"./utils":13}], +11: [function(require, module, exports) { /* Taken from: https://github.com/component/type */ /** @@ -2360,7 +2214,7 @@ module.exports = function(val){ }; }, {}], -7: [function(require, module, exports) { +13: [function(require, module, exports) { var type = require('./type'); var log = function(s) { @@ -2446,8 +2300,8 @@ module.exports = { validateProperties: validateProperties }; -}, {"./type":6}], -14: [function(require, module, exports) { +}, {"./type":11}], +8: [function(require, module, exports) { /* * JavaScript MD5 1.0.1 * https://github.com/blueimp/JavaScript-MD5 @@ -2735,7 +2589,7 @@ module.exports = { }(this)); }, {}], -5: [function(require, module, exports) { +9: [function(require, module, exports) { /** * HOP ref. @@ -2821,7 +2675,7 @@ exports.isEmpty = function(obj){ return 0 == exports.length(obj); }; }, {}], -15: [function(require, module, exports) { +10: [function(require, module, exports) { var querystring = require('querystring'); /* @@ -2867,8 +2721,8 @@ Request.prototype.send = function(callback) { module.exports = Request; -}, {"querystring":24}], -24: [function(require, module, exports) { +}, {"querystring":23}], +23: [function(require, module, exports) { /** * Module dependencies. @@ -2943,8 +2797,8 @@ exports.stringify = function(obj){ return pairs.join('&'); }; -}, {"trim":25,"type":26}], -25: [function(require, module, exports) { +}, {"trim":24,"type":25}], +24: [function(require, module, exports) { exports = module.exports = trim; @@ -2964,7 +2818,7 @@ exports.right = function(str){ }; }, {}], -26: [function(require, module, exports) { +25: [function(require, module, exports) { /** * toString ref. */ @@ -3013,7 +2867,7 @@ function isBuffer(obj) { } }, {}], -16: [function(require, module, exports) { +12: [function(require, module, exports) { /* jshint eqeqeq: false, forin: false */ /* global define */ @@ -3896,7 +3750,7 @@ function isBuffer(obj) { })(this); }, {}], -17: [function(require, module, exports) { +14: [function(require, module, exports) { /* jshint bitwise: false, laxbreak: true */ /** @@ -3930,11 +3784,11 @@ var uuid = function(a) { module.exports = uuid; }, {}], -8: [function(require, module, exports) { +15: [function(require, module, exports) { module.exports = '2.9.0'; }, {}], -9: [function(require, module, exports) { +16: [function(require, module, exports) { var language = require('./language'); // default options @@ -3956,11 +3810,10 @@ module.exports = { batchEvents: false, eventUploadThreshold: 30, eventUploadPeriodMillis: 30 * 1000, // 30s - newBlankInstance: false }; -}, {"./language":27}], -27: [function(require, module, exports) { +}, {"./language":26}], +26: [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 24b5d6d4..e755e0eb 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,2 +1,2 @@ -(function umd(require){if("object"==typeof exports){module.exports=require("1")}else if("function"==typeof define&&define.amd){define(function(){return require("1")})}else{this["amplitude"]=require("1")}})(function outer(modules,cache,entries){var global=function(){return this}();function require(name,jumped){if(cache[name])return cache[name].exports;if(modules[name])return call(name,require);throw new Error('cannot find module "'+name+'"')}function call(id,require){var m=cache[id]={exports:{}};var mod=modules[id];var name=mod[2];var fn=mod[0];fn.call(m.exports,function(req){var dep=modules[id][1][req];return require(dep?dep:req)},m,m.exports,outer,modules,cache,entries);if(name)cache[name]=cache[id];return cache[id].exports}for(var id in entries){if(entries[id]){global[entries[id]]=require(id)}else{require(id)}}require.duo=true;require.cache=cache;require.modules=modules;return require}({1:[function(require,module,exports){var Amplitude=require("./amplitude");var old=window.amplitude||{};var 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 Identify=require("./identify");var object=require("object");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var DEFAULT_INSTANCE="$default_instance";var Amplitude=function(){this.options=object.merge({},DEFAULT_OPTIONS);this._instances={}};Amplitude.prototype.getInstance=function(instance){instance=(utils.isEmptyString(instance)?DEFAULT_INSTANCE:instance).toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.Identify=Identify;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._unsentEvents;this._unsentIdentifys=this._loadSavedUnsentEvents(this.options.unsentIdentifyKey)||this._unsentIdentifys;for(var i=0;i=this.options.eventUploadThreshold){this.sendEvents(callback);return true}if(!this._updateScheduled){this._updateScheduled=true;setTimeout(function(){this._updateScheduled=false;this.sendEvents()}.bind(this),this.options.eventUploadPeriodMillis)}return false};AmplitudeClient.prototype._getFromStorage=function(storage,key){return storage.getItem(key+this._storageSuffix)};AmplitudeClient.prototype._setInStorage=function(storage,key,value){storage.setItem(key+this._storageSuffix,value)};var _upgradeCookeData=function(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(cookieData&&cookieData.deviceId&&cookieData.sessionId&&cookieData.lastEventTime){return}var _getAndRemoveFromLocalStorage=function(key){var value=localStorage.getItem(key);localStorage.removeItem(key);return value};var apiKeySuffix="_"+scope.options.apiKey.slice(0,6);var localStorageDeviceId=_getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID+apiKeySuffix);var localStorageUserId=_getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID+apiKeySuffix);var localStorageOptOut=_getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT+apiKeySuffix);if(localStorageOptOut!==null&&localStorageOptOut!==undefined){localStorageOptOut=String(localStorageOptOut)==="true"}var localStorageSessionId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID));var localStorageLastEventTime=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME));var localStorageEventId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID));var localStorageIdentifyId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID));var localStorageSequenceNumber=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER));var _getFromCookie=function(key){return cookieData&&cookieData[key]};scope.options.deviceId=_getFromCookie("deviceId")||localStorageDeviceId;scope.options.userId=_getFromCookie("userId")||localStorageUserId;scope._sessionId=_getFromCookie("sessionId")||localStorageSessionId||scope._sessionId;scope._lastEventTime=_getFromCookie("lastEventTime")||localStorageLastEventTime||scope._lastEventTime;scope._eventId=_getFromCookie("eventId")||localStorageEventId||scope._eventId;scope._identifyId=_getFromCookie("identifyId")||localStorageIdentifyId||scope._identifyId;scope._sequenceNumber=_getFromCookie("sequenceNumber")||localStorageSequenceNumber||scope._sequenceNumber;scope.options.optOut=localStorageOptOut||false;if(cookieData&&cookieData.optOut!==undefined&&cookieData.optOut!==null){scope.options.optOut=String(cookieData.optOut)==="true"}_saveCookieData(scope)};var _loadCookieData=function(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName+scope._storageSuffix);if(cookieData){if(cookieData.deviceId){scope.options.deviceId=cookieData.deviceId}if(cookieData.userId){scope.options.userId=cookieData.userId}if(cookieData.optOut!==null&&cookieData.optOut!==undefined){scope.options.optOut=cookieData.optOut}if(cookieData.sessionId){scope._sessionId=parseInt(cookieData.sessionId)}if(cookieData.lastEventTime){scope._lastEventTime=parseInt(cookieData.lastEventTime)}if(cookieData.eventId){scope._eventId=parseInt(cookieData.eventId)}if(cookieData.identifyId){scope._identifyId=parseInt(cookieData.identifyId)}if(cookieData.sequenceNumber){scope._sequenceNumber=parseInt(cookieData.sequenceNumber)}}};var _saveCookieData=function(scope){scope.cookieStorage.set(scope.options.cookieName+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(queryParams,cookieParams){queryParams=queryParams||location.search;cookieParams=cookieParams||this.cookieStorage.get("__utmz");this._utmProperties=getUtmData(cookieParams,queryParams)};AmplitudeClient.prototype._getReferrer=function(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function(referrer){if(referrer===null||referrer===undefined||referrer===""){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function(referrer){if(referrer===null||referrer===undefined||referrer===""){return}var referring_domain=this._getReferringDomain(referrer);var identify=(new Identify).setOnce("initial_referrer",referrer);identify.setOnce("initial_referring_domain",referring_domain);var hasSessionStorage=false;try{if(window.sessionStorage){hasSessionStorage=true}}catch(e){}if(hasSessionStorage&&!this._getFromStorage(sessionStorage,LocalStorageKeys.REFERRER)||!hasSessionStorage){identify.set("referrer",referrer).set("referring_domain",referring_domain);if(hasSessionStorage){this._setInStorage(sessionStorage,LocalStorageKeys.REFERRER,referrer)}}this.identify(identify)};AmplitudeClient.prototype.saveEvents=function(){if(!this._apiKeySet("saveEvents()")){return}try{this._setInStorage(localStorage,this.options.unsentKey,JSON.stringify(this._unsentEvents));this._setInStorage(localStorage,this.options.unsentIdentifyKey,JSON.stringify(this._unsentIdentifys))}catch(e){}};AmplitudeClient.prototype.setDomain=function(domain){if(!this._apiKeySet("setDomain()")){return}try{this.cookieStorage.options({domain:domain});this.options.domain=this.cookieStorage.options().domain;_loadCookieData(this);_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setUserId=function(userId){if(!this._apiKeySet("setUserId()")){return}try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setOptOut=function(enable){if(!this._apiKeySet("setOptOut()")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setDeviceId=function(deviceId){if(!this._apiKeySet("setDeviceId()")){return}try{if(deviceId){this.options.deviceId=""+deviceId;_saveCookieData(this)}}catch(e){utils.log(e)}};AmplitudeClient.prototype.setUserProperties=function(userProperties){if(!this._apiKeySet("setUserProperties()")){return}var identify=new Identify;for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}this.identify(identify)};AmplitudeClient.prototype.clearUserProperties=function(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};AmplitudeClient.prototype.identify=function(identify){if(!this._apiKeySet("identify()")){return}if(type(identify)==="object"&&"_q"in identify){var instance=new Identify;for(var i=0;i0){this._logEvent(IDENTIFY_EVENT,null,null,identify.userPropertiesOperations)}};AmplitudeClient.prototype.setVersionName=function(versionName){try{this.options.versionName=versionName}catch(e){utils.log(e)}};AmplitudeClient.prototype._truncate=function(value){if(type(value)==="array"){for(var i=0;iMAX_STRING_LENGTH?value.substring(0,MAX_STRING_LENGTH):value}return value};AmplitudeClient.prototype._logEvent=function(eventType,eventProperties,apiProperties,userProperties,callback){if(type(callback)!=="function"){callback=null}if(!eventType||this.options.optOut){if(callback){callback(0,"No request sent")}return}try{var eventId;if(eventType===IDENTIFY_EVENT){eventId=this.nextIdentifyId()}else{eventId=this.nextEventId()}var sequenceNumber=this.nextSequenceNumber();var eventTime=(new Date).getTime();var ua=this._ua;if(!this._sessionId||!this._lastEventTime||eventTime-this._lastEventTime>this.options.sessionTimeout){this._sessionId=eventTime}this._lastEventTime=eventTime;_saveCookieData(this);userProperties=userProperties||{};if(eventType!==IDENTIFY_EVENT){object.merge(userProperties,this._utmProperties)}apiProperties=apiProperties||{};eventProperties=eventProperties||{};var event={device_id:this.options.deviceId,user_id:this.options.userId||this.options.deviceId,timestamp:eventTime,event_id:eventId,session_id:this._sessionId||-1,event_type:eventType,version_name:this.options.versionName||null,platform:this.options.platform,os_name:ua.browser.name||null,os_version:ua.browser.major||null,device_model:ua.os.name||null,language:this.options.language,api_properties:apiProperties,event_properties:this._truncate(utils.validateProperties(eventProperties)),user_properties:this._truncate(userProperties),uuid:UUID(),library:{name:"amplitude-js",version:version},sequence_number:sequenceNumber};if(eventType===IDENTIFY_EVENT){this._unsentIdentifys.push(event);this._limitEventsQueued(this._unsentIdentifys)}else{this._unsentEvents.push(event);this._limitEventsQueued(this._unsentEvents)}if(this.options.saveEvents){this.saveEvents()}if(!this._sendEventsIfReady(callback)&&callback){callback(0,"No request sent")}return eventId}catch(e){utils.log(e)}};AmplitudeClient.prototype._limitEventsQueued=function(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.prototype.logEvent=function(eventType,eventProperties,callback){if(!this._apiKeySet("logEvent()")){return-1}return this._logEvent(eventType,eventProperties,null,null,callback)};var _isNumber=function(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenue=function(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent("revenue_amount",{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price})};AmplitudeClient.prototype.removeEvents=function(maxEventId,maxIdentifyId){if(maxEventId>=0){var filteredEvents=[];for(var i=0;imaxEventId){filteredEvents.push(this._unsentEvents[i])}}this._unsentEvents=filteredEvents}if(maxIdentifyId>=0){var filteredIdentifys=[];for(var j=0;jmaxIdentifyId){filteredIdentifys.push(this._unsentIdentifys[j])}}this._unsentIdentifys=filteredIdentifys}};AmplitudeClient.prototype.sendEvents=function(callback){if(!this._apiKeySet("sendEvents()")){return}if(!this._sending&&!this.options.optOut&&this._unsentCount()>0){this._sending=true;var url=("https:"===window.location.protocol?"https":"http")+"://"+this.options.apiEndpoint+"/";var numEvents=Math.min(this._unsentCount(),this.options.uploadBatchSize);var mergedEvents=this._mergeEventsAndIdentifys(numEvents);var maxEventId=mergedEvents.maxEventId;var maxIdentifyId=mergedEvents.maxIdentifyId;var events=JSON.stringify(mergedEvents.eventsToSend);var uploadTime=(new Date).getTime();var data={client:this.options.apiKey,e:events,v:API_VERSION,upload_time:uploadTime,checksum:md5(API_VERSION+this.options.apiKey+events+uploadTime)};var scope=this;new Request(url,data).send(function(status,response){scope._sending=false;try{if(status===200&&response==="success"){scope.removeEvents(maxEventId,maxIdentifyId);if(scope.options.saveEvents){scope.saveEvents()}if(!scope._sendEventsIfReady(callback)&&callback){callback(status,response)}}else if(status===413){if(scope.options.uploadBatchSize===1){scope.removeEvents(maxEventId,maxIdentifyId)}scope.options.uploadBatchSize=Math.ceil(numEvents/2);scope.sendEvents(callback)}else if(callback){callback(status,response)}}catch(e){}})}else if(callback){callback(0,"No request sent")}};AmplitudeClient.prototype._mergeEventsAndIdentifys=function(numEvents){var eventsToSend=[];var eventIndex=0;var maxEventId=-1;var identifyIndex=0;var maxIdentifyId=-1;while(eventsToSend.length=this._unsentIdentifys.length){event=this._unsentEvents[eventIndex++];maxEventId=event.event_id}else if(eventIndex>=this._unsentEvents.length){event=this._unsentIdentifys[identifyIndex++];maxIdentifyId=event.event_id}else{if(!("sequence_number"in this._unsentEvents[eventIndex])||this._unsentEvents[eventIndex].sequence_number>2;enc2=(chr1&3)<<4|chr2>>4;enc3=(chr2&15)<<2|chr3>>6;enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else if(isNaN(chr3)){enc4=64}output=output+Base64._keyStr.charAt(enc1)+Base64._keyStr.charAt(enc2)+Base64._keyStr.charAt(enc3)+Base64._keyStr.charAt(enc4)}return output},decode:function(input){try{if(window.btoa&&window.atob){return decodeURIComponent(escape(window.atob(input)))}}catch(e){}return Base64._decode(input)},_decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(i>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}output=UTF8.decode(output);return output}};module.exports=Base64},{"./utf8":21}],21:[function(require,module,exports){var UTF8={encode:function(s){var utftext="";for(var n=0;n127&&c<2048){utftext+=String.fromCharCode(c>>6|192);utftext+=String.fromCharCode(c&63|128)}else{utftext+=String.fromCharCode(c>>12|224);utftext+=String.fromCharCode(c>>6&63|128);utftext+=String.fromCharCode(c&63|128)}}return utftext},decode:function(utftext){var s="";var i=0;var c=0,c1=0,c2=0;while(i191&&c<224){c1=utftext.charCodeAt(i+1);s+=String.fromCharCode((c&31)<<6|c1&63);i+=2}else{c1=utftext.charCodeAt(i+1);c2=utftext.charCodeAt(i+2);s+=String.fromCharCode((c&15)<<12|(c1&63)<<6|c2&63);i+=3}}return s}};module.exports=UTF8},{}],12:[function(require,module,exports){var json=window.JSON||{};var stringify=json.stringify;var parse=json.parse;module.exports=parse&&stringify?JSON:require("json-fallback")},{"json-fallback":22}],22:[function(require,module,exports){(function(){"use strict";var JSON=module.exports={};function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx,escapable,gap,indent,meta,rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;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.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":6,"./utils":7}],6:[function(require,module,exports){var toString=Object.prototype.toString;module.exports=function(val){switch(toString.call(val)){case"[object Date]":return"date";case"[object RegExp]":return"regexp";case"[object Arguments]":return"arguments";case"[object Array]":return"array";case"[object Error]":return"error"}if(val===null){return"null"}if(val===undefined){return"undefined"}if(val!==val){return"nan"}if(val&&val.nodeType===1){return"element"}if(typeof Buffer!=="undefined"&&Buffer.isBuffer(val)){return"buffer"}val=val.valueOf?val.valueOf():Object.prototype.valueOf.apply(val);return typeof val}},{}],7:[function(require,module,exports){var type=require("./type");var log=function(s){try{console.log("[Amplitude] "+s)}catch(e){}};var isEmptyString=function(str){return!str||str.length===0};var validateProperties=function(properties){var propsType=type(properties);if(propsType!=="object"){log("Error: invalid event properties format. Expecting Javascript object, received "+propsType+", ignoring");return{}}var copy={};for(var property in properties){if(!properties.hasOwnProperty(property)){continue}var key=property;var keyType=type(key);if(keyType!=="string"){log("WARNING: Non-string property key, received type "+keyType+', coercing to string "'+key+'"');key=String(key)}var value=validatePropertyValue(key,properties[property]);if(value===null){continue}copy[key]=value}return copy};var invalidValueTypes=["null","nan","undefined","function","arguments","regexp","element"];var validatePropertyValue=function(key,value){var valueType=type(value);if(invalidValueTypes.indexOf(valueType)!==-1){log('WARNING: Property key "'+key+'" with invalid value type '+valueType+", ignoring");value=null}else if(valueType==="error"){value=String(value);log('WARNING: Property key "'+key+'" with value type error, coercing to '+value)}else if(valueType==="array"){var arrayCopy=[];for(var i=0;i>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}function bit_rol(num,cnt){return num<>>32-cnt}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn(b&c|~b&d,a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn(b&d|c&~d,a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|~d),a,b,x,s,t)}function binl_md5(x,len){x[len>>5]|=128<>>9<<4)+14]=len;var i,olda,oldb,oldc,oldd,a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(i=0;i>5]>>>i%32&255)}return output}function rstr2binl(input){var i,output=[];output[(input.length>>2)-1]=undefined;for(i=0;i>5]|=(input.charCodeAt(i/8)&255)<16){bkey=binl_md5(bkey,key.length*8)}for(i=0;i<16;i+=1){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}hash=binl_md5(ipad.concat(rstr2binl(data)),512+data.length*8);return binl2rstr(binl_md5(opad.concat(hash),512+128))}function rstr2hex(input){var hex_tab="0123456789abcdef",output="",x,i;for(i=0;i>>4&15)+hex_tab.charAt(x&15)}return output}function str2rstr_utf8(input){return unescape(encodeURIComponent(input))}function raw_md5(s){return rstr_md5(str2rstr_utf8(s))}function hex_md5(s){return rstr2hex(raw_md5(s))}function raw_hmac_md5(k,d){return rstr_hmac_md5(str2rstr_utf8(k),str2rstr_utf8(d))}function hex_hmac_md5(k,d){return rstr2hex(raw_hmac_md5(k,d))}function md5(string,key,raw){if(!key){if(!raw){return hex_md5(string)}return raw_md5(string)}if(!raw){return hex_hmac_md5(key,string)}return raw_hmac_md5(key,string)}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=md5}exports.md5=md5}else{if(typeof define==="function"&&define.amd){define(function(){return md5})}else{$.md5=md5}}})(this)},{}],5:[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)}},{}],15:[function(require,module,exports){var querystring=require("querystring");var Request=function(url,data){this.url=url;this.data=data||{}};Request.prototype.send=function(callback){var isIE=window.XDomainRequest?true:false;if(isIE){var xdr=new window.XDomainRequest;xdr.open("POST",this.url,true);xdr.onload=function(){callback(200,xdr.responseText)};xdr.onerror=function(){if(xdr.responseText==="Request Entity Too Large"){callback(413,xdr.responseText)}else{callback(500,xdr.responseText)}};xdr.ontimeout=function(){};xdr.onprogress=function(){};xdr.send(querystring.stringify(this.data))}else{var xhr=new XMLHttpRequest;xhr.open("POST",this.url,true);xhr.onreadystatechange=function(){if(xhr.readyState===4){callback(xhr.status,xhr.responseText)}};xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");xhr.send(querystring.stringify(this.data))}};module.exports=Request},{querystring:24}],24:[function(require,module,exports){var encode=encodeURIComponent;var decode=decodeURIComponent;var trim=require("trim");var type=require("type");exports.parse=function(str){if("string"!=typeof str)return{};str=trim(str);if(""==str)return{};if("?"==str.charAt(0))str=str.slice(1);var obj={};var pairs=str.split("&");for(var i=0;i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)};module.exports=uuid},{}],8:[function(require,module,exports){module.exports="2.9.0"},{}],9:[function(require,module,exports){var language=require("./language");module.exports={apiEndpoint:"api.amplitude.com",cookieExpiration:365*10,cookieName:"amplitude_id",domain:undefined,includeUtm:false,language:language.language,optOut:false,platform:"Web",savedMaxCount:1e3,saveEvents:true,sessionTimeout:30*60*1e3,unsentKey:"amplitude_unsent",unsentIdentifyKey:"amplitude_unsent_identify",uploadBatchSize:100,batchEvents:false,eventUploadThreshold:30,eventUploadPeriodMillis:30*1e3,newBlankInstance:false}},{"./language":27}],27:[function(require,module,exports){var getLanguage=function(){return navigator&&(navigator.languages&&navigator.languages[0]||navigator.language||navigator.userLanguage)||undefined};module.exports={language:getLanguage()}},{}]},{},{1:""})); \ No newline at end of file +(function umd(require){if("object"==typeof exports){module.exports=require("1")}else if("function"==typeof define&&define.amd){define(function(){return require("1")})}else{this["amplitude"]=require("1")}})(function outer(modules,cache,entries){var global=function(){return this}();function require(name,jumped){if(cache[name])return cache[name].exports;if(modules[name])return call(name,require);throw new Error('cannot find module "'+name+'"')}function call(id,require){var m=cache[id]={exports:{}};var mod=modules[id];var name=mod[2];var fn=mod[0];fn.call(m.exports,function(req){var dep=modules[id][1][req];return require(dep?dep:req)},m,m.exports,outer,modules,cache,entries);if(name)cache[name]=cache[id];return cache[id].exports}for(var id in entries){if(entries[id]){global[entries[id]]=require(id)}else{require(id)}}require.duo=true;require.cache=cache;require.modules=modules;return require}({1:[function(require,module,exports){var Amplitude=require("./amplitude");var old=window.amplitude||{};var instance=new Amplitude;instance._q=old._q||[];module.exports=instance},{"./amplitude":2}],2:[function(require,module,exports){var cookieStorage=require("./cookiestorage");var getUtmData=require("./utm");var Identify=require("./identify");var JSON=require("json");var localStorage=require("./localstorage");var md5=require("JavaScript-MD5");var object=require("object");var Request=require("./xhr");var type=require("./type");var UAParser=require("ua-parser-js");var utils=require("./utils");var UUID=require("./uuid");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var IDENTIFY_EVENT="$identify";var API_VERSION=2;var MAX_STRING_LENGTH=1024;var LocalStorageKeys={LAST_EVENT_ID:"amplitude_lastEventId",LAST_EVENT_TIME:"amplitude_lastEventTime",LAST_IDENTIFY_ID:"amplitude_lastIdentifyId",LAST_SEQUENCE_NUMBER:"amplitude_lastSequenceNumber",REFERRER:"amplitude_referrer",SESSION_ID:"amplitude_sessionId",DEVICE_ID:"amplitude_deviceId",OPT_OUT:"amplitude_optOut",USER_ID:"amplitude_userId"};var Amplitude=function(){this._unsentEvents=[];this._unsentIdentifys=[];this._ua=new UAParser(navigator.userAgent).getResult();this.options=object.merge({},DEFAULT_OPTIONS);this.cookieStorage=(new cookieStorage).getStorage();this._q=[]};Amplitude.prototype._eventId=0;Amplitude.prototype._identifyId=0;Amplitude.prototype._sequenceNumber=0;Amplitude.prototype._sending=false;Amplitude.prototype._lastEventTime=null;Amplitude.prototype._sessionId=null;Amplitude.prototype._newSession=false;Amplitude.prototype._updateScheduled=false;Amplitude.prototype.Identify=Identify;Amplitude.prototype.init=function(apiKey,opt_userId,opt_config,callback){try{this.options.apiKey=apiKey;if(opt_config){if(opt_config.saveEvents!==undefined){this.options.saveEvents=!!opt_config.saveEvents}if(opt_config.domain!==undefined){this.options.domain=opt_config.domain}if(opt_config.includeUtm!==undefined){this.options.includeUtm=!!opt_config.includeUtm}if(opt_config.includeReferrer!==undefined){this.options.includeReferrer=!!opt_config.includeReferrer}if(opt_config.batchEvents!==undefined){this.options.batchEvents=!!opt_config.batchEvents}this.options.platform=opt_config.platform||this.options.platform;this.options.language=opt_config.language||this.options.language;this.options.sessionTimeout=opt_config.sessionTimeout||this.options.sessionTimeout;this.options.uploadBatchSize=opt_config.uploadBatchSize||this.options.uploadBatchSize;this.options.eventUploadThreshold=opt_config.eventUploadThreshold||this.options.eventUploadThreshold;this.options.savedMaxCount=opt_config.savedMaxCount||this.options.savedMaxCount;this.options.eventUploadPeriodMillis=opt_config.eventUploadPeriodMillis||this.options.eventUploadPeriodMillis}this.cookieStorage.options({expirationDays:this.options.cookieExpiration,domain:this.options.domain});this.options.domain=this.cookieStorage.options().domain;_upgradeCookeData(this);_loadCookieData(this);this.options.deviceId=opt_config&&opt_config.deviceId!==undefined&&opt_config.deviceId!==null&&opt_config.deviceId||this.options.deviceId||UUID();this.options.userId=opt_userId!==undefined&&opt_userId!==null&&opt_userId||this.options.userId||null;var now=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||now-this._lastEventTime>this.options.sessionTimeout){this._newSession=true;this._sessionId=now}this._lastEventTime=now;_saveCookieData(this);if(this.options.saveEvents){this._unsentEvents=this._loadSavedUnsentEvents(this.options.unsentKey)||this._unsentEvents;this._unsentIdentifys=this._loadSavedUnsentEvents(this.options.unsentIdentifyKey)||this._unsentIdentifys;for(var i=0;i=this.options.eventUploadThreshold){this.sendEvents(callback);return true}if(!this._updateScheduled){this._updateScheduled=true;setTimeout(function(){this._updateScheduled=false;this.sendEvents()}.bind(this),this.options.eventUploadPeriodMillis)}return false};Amplitude.prototype._getFromStorage=function(storage,key){return storage.getItem(key)};Amplitude.prototype._setInStorage=function(storage,key,value){storage.setItem(key,value)};var _upgradeCookeData=function(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(cookieData&&cookieData.deviceId&&cookieData.sessionId&&cookieData.lastEventTime){return}var _getAndRemoveFromLocalStorage=function(key){var value=localStorage.getItem(key);localStorage.removeItem(key);return value};var apiKeySuffix="_"+scope.options.apiKey.slice(0,6);var localStorageDeviceId=_getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID+apiKeySuffix);var localStorageUserId=_getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID+apiKeySuffix);var localStorageOptOut=_getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT+apiKeySuffix);if(localStorageOptOut!==null&&localStorageOptOut!==undefined){localStorageOptOut=String(localStorageOptOut)==="true"}var localStorageSessionId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID));var localStorageLastEventTime=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME));var localStorageEventId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID));var localStorageIdentifyId=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID));var localStorageSequenceNumber=parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER));var _getFromCookie=function(key){return cookieData&&cookieData[key]};scope.options.deviceId=_getFromCookie("deviceId")||localStorageDeviceId;scope.options.userId=_getFromCookie("userId")||localStorageUserId;scope._sessionId=_getFromCookie("sessionId")||localStorageSessionId||scope._sessionId;scope._lastEventTime=_getFromCookie("lastEventTime")||localStorageLastEventTime||scope._lastEventTime;scope._eventId=_getFromCookie("eventId")||localStorageEventId||scope._eventId;scope._identifyId=_getFromCookie("identifyId")||localStorageIdentifyId||scope._identifyId;scope._sequenceNumber=_getFromCookie("sequenceNumber")||localStorageSequenceNumber||scope._sequenceNumber;scope.options.optOut=localStorageOptOut||false;if(cookieData&&cookieData.optOut!==undefined&&cookieData.optOut!==null){scope.options.optOut=String(cookieData.optOut)==="true"}_saveCookieData(scope)};var _loadCookieData=function(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(cookieData){if(cookieData.deviceId){scope.options.deviceId=cookieData.deviceId}if(cookieData.userId){scope.options.userId=cookieData.userId}if(cookieData.optOut!==null&&cookieData.optOut!==undefined){scope.options.optOut=cookieData.optOut}if(cookieData.sessionId){scope._sessionId=parseInt(cookieData.sessionId)}if(cookieData.lastEventTime){scope._lastEventTime=parseInt(cookieData.lastEventTime)}if(cookieData.eventId){scope._eventId=parseInt(cookieData.eventId)}if(cookieData.identifyId){scope._identifyId=parseInt(cookieData.identifyId)}if(cookieData.sequenceNumber){scope._sequenceNumber=parseInt(cookieData.sequenceNumber)}}};var _saveCookieData=function(scope){scope.cookieStorage.set(scope.options.cookieName,{deviceId:scope.options.deviceId,userId:scope.options.userId,optOut:scope.options.optOut,sessionId:scope._sessionId,lastEventTime:scope._lastEventTime,eventId:scope._eventId,identifyId:scope._identifyId,sequenceNumber:scope._sequenceNumber})};Amplitude.prototype._initUtmData=function(queryParams,cookieParams){queryParams=queryParams||location.search;cookieParams=cookieParams||this.cookieStorage.get("__utmz");this._utmProperties=getUtmData(cookieParams,queryParams)};Amplitude.prototype._getReferrer=function(){return document.referrer};Amplitude.prototype._getReferringDomain=function(referrer){if(referrer===null||referrer===undefined||referrer===""){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};Amplitude.prototype._saveReferrer=function(referrer){if(referrer===null||referrer===undefined||referrer===""){return}var referring_domain=this._getReferringDomain(referrer);var identify=(new Identify).setOnce("initial_referrer",referrer);identify.setOnce("initial_referring_domain",referring_domain);var hasSessionStorage=false;try{if(window.sessionStorage){hasSessionStorage=true}}catch(e){}if(hasSessionStorage&&!this._getFromStorage(sessionStorage,LocalStorageKeys.REFERRER)||!hasSessionStorage){identify.set("referrer",referrer).set("referring_domain",referring_domain);if(hasSessionStorage){this._setInStorage(sessionStorage,LocalStorageKeys.REFERRER,referrer)}}this.identify(identify)};Amplitude.prototype.saveEvents=function(){if(!this._apiKeySet("saveEvents()")){return}try{this._setInStorage(localStorage,this.options.unsentKey,JSON.stringify(this._unsentEvents));this._setInStorage(localStorage,this.options.unsentIdentifyKey,JSON.stringify(this._unsentIdentifys))}catch(e){}};Amplitude.prototype.setDomain=function(domain){if(!this._apiKeySet("setDomain()")){return}try{this.cookieStorage.options({domain:domain});this.options.domain=this.cookieStorage.options().domain;_loadCookieData(this);_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setUserId=function(userId){if(!this._apiKeySet("setUserId()")){return}try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setOptOut=function(enable){if(!this._apiKeySet("setOptOut()")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setDeviceId=function(deviceId){if(!this._apiKeySet("setDeviceId()")){return}try{if(deviceId){this.options.deviceId=""+deviceId;_saveCookieData(this)}}catch(e){utils.log(e)}};Amplitude.prototype.setUserProperties=function(userProperties){if(!this._apiKeySet("setUserProperties()")){return}var identify=new Identify;for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}this.identify(identify)};Amplitude.prototype.clearUserProperties=function(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};Amplitude.prototype.identify=function(identify){if(!this._apiKeySet("identify()")){return}if(type(identify)==="object"&&"_q"in identify){var instance=new Identify;for(var i=0;i0){this._logEvent(IDENTIFY_EVENT,null,null,identify.userPropertiesOperations)}};Amplitude.prototype.setVersionName=function(versionName){try{this.options.versionName=versionName}catch(e){utils.log(e)}};Amplitude.prototype._truncate=function(value){if(type(value)==="array"){for(var i=0;iMAX_STRING_LENGTH?value.substring(0,MAX_STRING_LENGTH):value}return value};Amplitude.prototype._logEvent=function(eventType,eventProperties,apiProperties,userProperties,callback){if(type(callback)!=="function"){callback=null}if(!eventType||this.options.optOut){if(callback){callback(0,"No request sent")}return}try{var eventId;if(eventType===IDENTIFY_EVENT){eventId=this.nextIdentifyId()}else{eventId=this.nextEventId()}var sequenceNumber=this.nextSequenceNumber();var eventTime=(new Date).getTime();var ua=this._ua;if(!this._sessionId||!this._lastEventTime||eventTime-this._lastEventTime>this.options.sessionTimeout){this._sessionId=eventTime}this._lastEventTime=eventTime;_saveCookieData(this);userProperties=userProperties||{};if(eventType!==IDENTIFY_EVENT){object.merge(userProperties,this._utmProperties)}apiProperties=apiProperties||{};eventProperties=eventProperties||{};var event={device_id:this.options.deviceId,user_id:this.options.userId||this.options.deviceId,timestamp:eventTime,event_id:eventId,session_id:this._sessionId||-1,event_type:eventType,version_name:this.options.versionName||null,platform:this.options.platform,os_name:ua.browser.name||null,os_version:ua.browser.major||null,device_model:ua.os.name||null,language:this.options.language,api_properties:apiProperties,event_properties:this._truncate(utils.validateProperties(eventProperties)),user_properties:this._truncate(userProperties),uuid:UUID(),library:{name:"amplitude-js",version:version},sequence_number:sequenceNumber};if(eventType===IDENTIFY_EVENT){this._unsentIdentifys.push(event);this._limitEventsQueued(this._unsentIdentifys)}else{this._unsentEvents.push(event);this._limitEventsQueued(this._unsentEvents)}if(this.options.saveEvents){this.saveEvents()}if(!this._sendEventsIfReady(callback)&&callback){callback(0,"No request sent")}return eventId}catch(e){utils.log(e)}};Amplitude.prototype._limitEventsQueued=function(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};Amplitude.prototype.logEvent=function(eventType,eventProperties,callback){if(!this._apiKeySet("logEvent()")){return-1}return this._logEvent(eventType,eventProperties,null,null,callback)};var _isNumber=function(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenue=function(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent("revenue_amount",{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price})};Amplitude.prototype.removeEvents=function(maxEventId,maxIdentifyId){if(maxEventId>=0){var filteredEvents=[];for(var i=0;imaxEventId){filteredEvents.push(this._unsentEvents[i])}}this._unsentEvents=filteredEvents}if(maxIdentifyId>=0){var filteredIdentifys=[];for(var j=0;jmaxIdentifyId){filteredIdentifys.push(this._unsentIdentifys[j])}}this._unsentIdentifys=filteredIdentifys}};Amplitude.prototype.sendEvents=function(callback){if(!this._apiKeySet("sendEvents()")){return}if(!this._sending&&!this.options.optOut&&this._unsentCount()>0){this._sending=true;var url=("https:"===window.location.protocol?"https":"http")+"://"+this.options.apiEndpoint+"/";var numEvents=Math.min(this._unsentCount(),this.options.uploadBatchSize);var mergedEvents=this._mergeEventsAndIdentifys(numEvents);var maxEventId=mergedEvents.maxEventId;var maxIdentifyId=mergedEvents.maxIdentifyId;var events=JSON.stringify(mergedEvents.eventsToSend);var uploadTime=(new Date).getTime();var data={client:this.options.apiKey,e:events,v:API_VERSION,upload_time:uploadTime,checksum:md5(API_VERSION+this.options.apiKey+events+uploadTime)};var scope=this;new Request(url,data).send(function(status,response){scope._sending=false;try{if(status===200&&response==="success"){scope.removeEvents(maxEventId,maxIdentifyId);if(scope.options.saveEvents){scope.saveEvents()}if(!scope._sendEventsIfReady(callback)&&callback){callback(status,response)}}else if(status===413){if(scope.options.uploadBatchSize===1){scope.removeEvents(maxEventId,maxIdentifyId)}scope.options.uploadBatchSize=Math.ceil(numEvents/2);scope.sendEvents(callback)}else if(callback){callback(status,response)}}catch(e){}})}else if(callback){callback(0,"No request sent")}};Amplitude.prototype._mergeEventsAndIdentifys=function(numEvents){var eventsToSend=[];var eventIndex=0;var maxEventId=-1;var identifyIndex=0;var maxIdentifyId=-1;while(eventsToSend.length=this._unsentIdentifys.length){event=this._unsentEvents[eventIndex++];maxEventId=event.event_id}else if(eventIndex>=this._unsentEvents.length){event=this._unsentIdentifys[identifyIndex++];maxIdentifyId=event.event_id}else{if(!("sequence_number"in this._unsentEvents[eventIndex])||this._unsentEvents[eventIndex].sequence_number>2;enc2=(chr1&3)<<4|chr2>>4;enc3=(chr2&15)<<2|chr3>>6;enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else if(isNaN(chr3)){enc4=64}output=output+Base64._keyStr.charAt(enc1)+Base64._keyStr.charAt(enc2)+Base64._keyStr.charAt(enc3)+Base64._keyStr.charAt(enc4)}return output},decode:function(input){try{if(window.btoa&&window.atob){return decodeURIComponent(escape(window.atob(input)))}}catch(e){}return Base64._decode(input)},_decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(i>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}output=UTF8.decode(output);return output}};module.exports=Base64},{"./utf8":20}],20:[function(require,module,exports){var UTF8={encode:function(s){var utftext="";for(var n=0;n127&&c<2048){utftext+=String.fromCharCode(c>>6|192);utftext+=String.fromCharCode(c&63|128)}else{utftext+=String.fromCharCode(c>>12|224);utftext+=String.fromCharCode(c>>6&63|128);utftext+=String.fromCharCode(c&63|128)}}return utftext},decode:function(utftext){var s="";var i=0;var c=0,c1=0,c2=0;while(i191&&c<224){c1=utftext.charCodeAt(i+1);s+=String.fromCharCode((c&31)<<6|c1&63);i+=2}else{c1=utftext.charCodeAt(i+1);c2=utftext.charCodeAt(i+2);s+=String.fromCharCode((c&15)<<12|(c1&63)<<6|c2&63);i+=3}}return s}};module.exports=UTF8},{}],6:[function(require,module,exports){var json=window.JSON||{};var stringify=json.stringify;var parse=json.parse;module.exports=parse&&stringify?JSON:require("json-fallback")},{"json-fallback":21}],21:[function(require,module,exports){(function(){"use strict";var JSON=module.exports={};function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx,escapable,gap,indent,meta,rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i0){if(!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll")}return this}this.userPropertiesOperations[AMP_OP_CLEAR_ALL]="-";return this};Identify.prototype.set=function(property,value){this._addOperation(AMP_OP_SET,property,value);return this};Identify.prototype.setOnce=function(property,value){this._addOperation(AMP_OP_SET_ONCE,property,value);return this};Identify.prototype.unset=function(property){this._addOperation(AMP_OP_UNSET,property,"-");return this};Identify.prototype._addOperation=function(operation,property,value){if(this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("This identify already contains a $clearAll operation, skipping operation "+operation);return}if(this.properties.indexOf(property)!==-1){utils.log('User property "'+property+'" already used in this identify, skipping operation '+operation);return}if(!this.userPropertiesOperations.hasOwnProperty(operation)){this.userPropertiesOperations[operation]={}}this.userPropertiesOperations[operation][property]=value;this.properties.push(property)};module.exports=Identify},{"./type":11,"./utils":13}],11:[function(require,module,exports){var toString=Object.prototype.toString;module.exports=function(val){switch(toString.call(val)){case"[object Date]":return"date";case"[object RegExp]":return"regexp";case"[object Arguments]":return"arguments";case"[object Array]":return"array";case"[object Error]":return"error"}if(val===null){return"null"}if(val===undefined){return"undefined"}if(val!==val){return"nan"}if(val&&val.nodeType===1){return"element"}if(typeof Buffer!=="undefined"&&Buffer.isBuffer(val)){return"buffer"}val=val.valueOf?val.valueOf():Object.prototype.valueOf.apply(val);return typeof val}},{}],13:[function(require,module,exports){var type=require("./type");var log=function(s){try{console.log("[Amplitude] "+s)}catch(e){}};var isEmptyString=function(str){return!str||str.length===0};var validateProperties=function(properties){var propsType=type(properties);if(propsType!=="object"){log("Error: invalid event properties format. Expecting Javascript object, received "+propsType+", ignoring");return{}}var copy={};for(var property in properties){if(!properties.hasOwnProperty(property)){continue}var key=property;var keyType=type(key);if(keyType!=="string"){log("WARNING: Non-string property key, received type "+keyType+', coercing to string "'+key+'"');key=String(key)}var value=validatePropertyValue(key,properties[property]);if(value===null){continue}copy[key]=value}return copy};var invalidValueTypes=["null","nan","undefined","function","arguments","regexp","element"];var validatePropertyValue=function(key,value){var valueType=type(value);if(invalidValueTypes.indexOf(valueType)!==-1){log('WARNING: Property key "'+key+'" with invalid value type '+valueType+", ignoring");value=null}else if(valueType==="error"){value=String(value);log('WARNING: Property key "'+key+'" with value type error, coercing to '+value)}else if(valueType==="array"){var arrayCopy=[];for(var i=0;i>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}function bit_rol(num,cnt){return num<>>32-cnt}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn(b&c|~b&d,a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn(b&d|c&~d,a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|~d),a,b,x,s,t)}function binl_md5(x,len){x[len>>5]|=128<>>9<<4)+14]=len;var i,olda,oldb,oldc,oldd,a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(i=0;i>5]>>>i%32&255)}return output}function rstr2binl(input){var i,output=[];output[(input.length>>2)-1]=undefined;for(i=0;i>5]|=(input.charCodeAt(i/8)&255)<16){bkey=binl_md5(bkey,key.length*8)}for(i=0;i<16;i+=1){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}hash=binl_md5(ipad.concat(rstr2binl(data)),512+data.length*8);return binl2rstr(binl_md5(opad.concat(hash),512+128))}function rstr2hex(input){var hex_tab="0123456789abcdef",output="",x,i;for(i=0;i>>4&15)+hex_tab.charAt(x&15)}return output}function str2rstr_utf8(input){return unescape(encodeURIComponent(input))}function raw_md5(s){return rstr_md5(str2rstr_utf8(s))}function hex_md5(s){return rstr2hex(raw_md5(s))}function raw_hmac_md5(k,d){return rstr_hmac_md5(str2rstr_utf8(k),str2rstr_utf8(d))}function hex_hmac_md5(k,d){return rstr2hex(raw_hmac_md5(k,d))}function md5(string,key,raw){if(!key){if(!raw){return hex_md5(string)}return raw_md5(string)}if(!raw){return hex_hmac_md5(key,string)}return raw_hmac_md5(key,string)}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=md5}exports.md5=md5}else{if(typeof define==="function"&&define.amd){define(function(){return md5})}else{$.md5=md5}}})(this)},{}],9:[function(require,module,exports){var has=Object.prototype.hasOwnProperty;exports.keys=Object.keys||function(obj){var keys=[];for(var key in obj){if(has.call(obj,key)){keys.push(key)}}return keys};exports.values=function(obj){var vals=[];for(var key in obj){if(has.call(obj,key)){vals.push(obj[key])}}return vals};exports.merge=function(a,b){for(var key in b){if(has.call(b,key)){a[key]=b[key]}}return a};exports.length=function(obj){return exports.keys(obj).length};exports.isEmpty=function(obj){return 0==exports.length(obj)}},{}],10:[function(require,module,exports){var querystring=require("querystring");var Request=function(url,data){this.url=url;this.data=data||{}};Request.prototype.send=function(callback){var isIE=window.XDomainRequest?true:false;if(isIE){var xdr=new window.XDomainRequest;xdr.open("POST",this.url,true);xdr.onload=function(){callback(200,xdr.responseText)};xdr.onerror=function(){if(xdr.responseText==="Request Entity Too Large"){callback(413,xdr.responseText)}else{callback(500,xdr.responseText)}};xdr.ontimeout=function(){};xdr.onprogress=function(){};xdr.send(querystring.stringify(this.data))}else{var xhr=new XMLHttpRequest;xhr.open("POST",this.url,true);xhr.onreadystatechange=function(){if(xhr.readyState===4){callback(xhr.status,xhr.responseText)}};xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");xhr.send(querystring.stringify(this.data))}};module.exports=Request},{querystring:23}],23:[function(require,module,exports){var encode=encodeURIComponent;var decode=decodeURIComponent;var trim=require("trim");var type=require("type");exports.parse=function(str){if("string"!=typeof str)return{};str=trim(str);if(""==str)return{};if("?"==str.charAt(0))str=str.slice(1);var obj={};var pairs=str.split("&");for(var i=0;i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)};module.exports=uuid},{}],15:[function(require,module,exports){module.exports="2.9.0"},{}],16:[function(require,module,exports){var language=require("./language");module.exports={apiEndpoint:"api.amplitude.com",cookieExpiration:365*10,cookieName:"amplitude_id",domain:undefined,includeUtm:false,language:language.language,optOut:false,platform:"Web",savedMaxCount:1e3,saveEvents:true,sessionTimeout:30*60*1e3,unsentKey:"amplitude_unsent",unsentIdentifyKey:"amplitude_unsent_identify",uploadBatchSize:100,batchEvents:false,eventUploadThreshold:30,eventUploadPeriodMillis:30*1e3}},{"./language":26}],26:[function(require,module,exports){var getLanguage=function(){return navigator&&(navigator.languages&&navigator.languages[0]||navigator.language||navigator.userLanguage)||undefined};module.exports={language:getLanguage()}},{}]},{},{1:""})); \ No newline at end of file diff --git a/src/amplitude-client.js b/src/amplitude-client.js deleted file mode 100644 index 55b053ce..00000000 --- a/src/amplitude-client.js +++ /dev/null @@ -1,823 +0,0 @@ -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 UAParser = require('ua-parser-js'); -var utils = require('./utils'); -var UUID = require('./uuid'); -var version = require('./version'); -var type = require('./type'); -var DEFAULT_OPTIONS = require('./options'); - -var DEFAULT_INSTANCE = '$default_instance'; -var IDENTIFY_EVENT = '$identify'; -var API_VERSION = 2; -var MAX_STRING_LENGTH = 1024; -var LocalStorageKeys = { - SESSION_ID: 'amplitude_sessionId', - LAST_EVENT_TIME: 'amplitude_lastEventTime', - LAST_EVENT_ID: 'amplitude_lastEventId', - LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', - LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', - REFERRER: 'amplitude_referrer', - - // Used in cookie as well - DEVICE_ID: 'amplitude_deviceId', - USER_ID: 'amplitude_userId', - OPT_OUT: 'amplitude_optOut' -}; - -/* - * AmplitudeClient API - */ -var AmplitudeClient = function(instanceName) { - this._instanceName = (utils.isEmptyString(instanceName) ? DEFAULT_INSTANCE : instanceName).toLowerCase(); - this._storageSuffix = this._instanceName === 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 -}; - -AmplitudeClient.prototype._eventId = 0; -AmplitudeClient.prototype._identifyId = 0; -AmplitudeClient.prototype._sequenceNumber = 0; -AmplitudeClient.prototype._sending = false; -AmplitudeClient.prototype._lastEventTime = null; -AmplitudeClient.prototype._sessionId = null; -AmplitudeClient.prototype._newSession = false; -AmplitudeClient.prototype._updateScheduled = false; - -/** - * Initializes AmplitudeClient. - * apiKey The API Key for your app - * opt_userId An identifier for this user - * opt_config Configuration options - * - saveEvents (boolean) Whether to save events to local storage. Defaults to true. - * - includeUtm (boolean) Whether to send utm parameters with events. Defaults to false. - * - includeReferrer (boolean) Whether to send referrer info with events. Defaults to false. - */ -AmplitudeClient.prototype.init = function(apiKey, opt_userId, opt_config, callback) { - try { - this.options.apiKey = apiKey; - - if (opt_config) { - if (opt_config.saveEvents !== undefined) { - this.options.saveEvents = !!opt_config.saveEvents; - } - if (opt_config.domain !== undefined) { - this.options.domain = opt_config.domain; - } - if (opt_config.includeUtm !== undefined) { - this.options.includeUtm = !!opt_config.includeUtm; - } - if (opt_config.includeReferrer !== undefined) { - this.options.includeReferrer = !!opt_config.includeReferrer; - } - if (opt_config.batchEvents !== undefined) { - this.options.batchEvents = !!opt_config.batchEvents; - } - if (opt_config.newBlankInstance !== undefined) { - this.options.newBlankInstance = !!opt_config.newBlankInstance; - } - this.options.platform = opt_config.platform || this.options.platform; - this.options.language = opt_config.language || this.options.language; - this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout; - this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize; - this.options.eventUploadThreshold = opt_config.eventUploadThreshold || this.options.eventUploadThreshold; - this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount; - this.options.eventUploadPeriodMillis = opt_config.eventUploadPeriodMillis || this.options.eventUploadPeriodMillis; - } - - this.cookieStorage.options({ - expirationDays: this.options.cookieExpiration, - domain: this.options.domain - }); - this.options.domain = this.cookieStorage.options().domain; - - if (this._instanceName === DEFAULT_INSTANCE) { - _upgradeCookeData(this); - } - _loadCookieData(this); - this.options.deviceId = (opt_config && opt_config.deviceId !== undefined && - opt_config.deviceId !== null && opt_config.deviceId) || - this.options.deviceId || UUID(); - this.options.userId = (opt_userId !== undefined && opt_userId !== null && opt_userId) || this.options.userId || null; - - var now = new Date().getTime(); - if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) { - this._newSession = true; - this._sessionId = now; - } - this._lastEventTime = now; - _saveCookieData(this); - - //utils.log('initialized with apiKey=' + apiKey); - //opt_userId !== undefined && opt_userId !== null && utils.log('initialized with userId=' + opt_userId); - - if (this.options.saveEvents) { - this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey) || this._unsentEvents; - this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey) || this._unsentIdentifys; - - // validate event properties for unsent events - for (var i = 0; i < this._unsentEvents.length; i++) { - var eventProperties = this._unsentEvents[i].event_properties; - this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); - } - - this._sendEventsIfReady(); - } - - if (this.options.includeUtm) { - this._initUtmData(); - } - - if (this.options.includeReferrer) { - this._saveReferrer(this._getReferrer()); - } - } catch (e) { - utils.log(e); - } - - if (callback && type(callback) === 'function') { - callback(this); - } -}; - -AmplitudeClient.prototype.Identify = Identify; - -AmplitudeClient.prototype._apiKeySet = function(methodName) { - if (!this.options.apiKey) { - utils.log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName); - return false; - } - return true; -}; - -AmplitudeClient.prototype.runQueuedFunctions = function () { - for (var i = 0; i < this._q.length; i++) { - var fn = this[this._q[i][0]]; - if (fn && type(fn) === 'function') { - fn.apply(this, this._q[i].slice(1)); - } - } - this._q = []; // clear function queue after running -}; - -AmplitudeClient.prototype._loadSavedUnsentEvents = function(unsentKey) { - var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); - if (savedUnsentEventsString) { - try { - return JSON.parse(savedUnsentEventsString); - } catch (e) { - // utils.log(e); - } - } - return null; -}; - -AmplitudeClient.prototype.isNewSession = function() { - return this._newSession; -}; - -AmplitudeClient.prototype.getSessionId = function() { - return this._sessionId; -}; - -AmplitudeClient.prototype.nextEventId = function() { - this._eventId++; - return this._eventId; -}; - -AmplitudeClient.prototype.nextIdentifyId = function() { - this._identifyId++; - return this._identifyId; -}; - -AmplitudeClient.prototype.nextSequenceNumber = function() { - this._sequenceNumber++; - return this._sequenceNumber; -}; - -// returns the number of unsent events and identifys -AmplitudeClient.prototype._unsentCount = function() { - return this._unsentEvents.length + this._unsentIdentifys.length; -}; - -// returns true if sendEvents called immediately -AmplitudeClient.prototype._sendEventsIfReady = function(callback) { - if (this._unsentCount() === 0) { - return false; - } - - if (!this.options.batchEvents) { - this.sendEvents(callback); - return true; - } - - if (this._unsentCount() >= 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; -}; - -// appends apiKey to storage key to support multiple apps -// storage argument allows for localStorage and sessionStorage -AmplitudeClient.prototype._getFromStorage = function(storage, key) { - return storage.getItem(key + this._storageSuffix); -}; - -// appends apiKey to storage key to support multiple apps -// storage argument allows for localStorage and sessionStorage -AmplitudeClient.prototype._setInStorage = function(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 for the defaultInstance. - */ -var _upgradeCookeData = function(scope) { - // skip if migration already happened - var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (cookieData && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { - return; - } - - var _getAndRemoveFromLocalStorage = function(key) { - var value = localStorage.getItem(key); - localStorage.removeItem(key); - return value; - }; - - // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey - var apiKeySuffix = '_' + scope.options.apiKey.slice(0, 6); - var localStorageDeviceId = _getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID + apiKeySuffix); - var localStorageUserId = _getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID + apiKeySuffix); - var localStorageOptOut = _getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT + apiKeySuffix); - if (localStorageOptOut !== null && localStorageOptOut !== undefined) { - localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean - } - - // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support - var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID)); - var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME)); - var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID)); - var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID)); - var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER)); - - var _getFromCookie = function(key) { - return cookieData && cookieData[key]; - }; - 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); -}; - -var _loadCookieData = function(scope) { - var cookieData = scope.cookieStorage.get(scope.options.cookieName + scope._storageSuffix); - if (cookieData) { - if (cookieData.deviceId) { - scope.options.deviceId = cookieData.deviceId; - } - if (cookieData.userId) { - scope.options.userId = cookieData.userId; - } - if (cookieData.optOut !== null && cookieData.optOut !== undefined) { - scope.options.optOut = cookieData.optOut; - } - if (cookieData.sessionId) { - scope._sessionId = parseInt(cookieData.sessionId); - } - if (cookieData.lastEventTime) { - scope._lastEventTime = parseInt(cookieData.lastEventTime); - } - if (cookieData.eventId) { - scope._eventId = parseInt(cookieData.eventId); - } - if (cookieData.identifyId) { - scope._identifyId = parseInt(cookieData.identifyId); - } - if (cookieData.sequenceNumber) { - scope._sequenceNumber = parseInt(cookieData.sequenceNumber); - } - } -}; - -var _saveCookieData = function(scope) { - scope.cookieStorage.set(scope.options.cookieName + 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. - */ -AmplitudeClient.prototype._initUtmData = function(queryParams, cookieParams) { - queryParams = queryParams || location.search; - cookieParams = cookieParams || this.cookieStorage.get('__utmz'); - this._utmProperties = getUtmData(cookieParams, queryParams); -}; - -AmplitudeClient.prototype._getReferrer = function() { - return document.referrer; -}; - -AmplitudeClient.prototype._getReferringDomain = function(referrer) { - if (referrer === null || referrer === undefined || referrer === '') { - return null; - } - var parts = referrer.split('/'); - if (parts.length >= 3) { - return parts[2]; - } - return null; -}; - -// since user properties are propagated on the server, only send once per session, don't need to send with every event -AmplitudeClient.prototype._saveReferrer = function(referrer) { - if (referrer === null || referrer === undefined || referrer === '') { - return; - } - - // always setOnce initial referrer - var referring_domain = this._getReferringDomain(referrer); - var identify = new Identify().setOnce('initial_referrer', referrer); - identify.setOnce('initial_referring_domain', referring_domain); - - // only save referrer if not already in session storage or if storage disabled - var hasSessionStorage = false; - try { - if (window.sessionStorage) { - hasSessionStorage = true; - } - } catch (e) { - // utils.log(e); // sessionStorage disabled - } - - if ((hasSessionStorage && !(this._getFromStorage(sessionStorage, LocalStorageKeys.REFERRER))) || !hasSessionStorage) { - identify.set('referrer', referrer).set('referring_domain', referring_domain); - - if (hasSessionStorage) { - this._setInStorage(sessionStorage, LocalStorageKeys.REFERRER, referrer); - } - } - - this.identify(identify); -}; - -AmplitudeClient.prototype.saveEvents = function() { - if (!this._apiKeySet('saveEvents()')) { - return; - } - - try { - this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); - this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); - } catch (e) { - // utils.log(e); - } -}; - -AmplitudeClient.prototype.setDomain = function(domain) { - if (!this._apiKeySet('setDomain()')) { - return; - } - - try { - this.cookieStorage.options({ - domain: domain - }); - this.options.domain = this.cookieStorage.options().domain; - _loadCookieData(this); - _saveCookieData(this); - // utils.log('set domain=' + domain); - } catch (e) { - utils.log(e); - } -}; - -AmplitudeClient.prototype.setUserId = function(userId) { - if (!this._apiKeySet('setUserId()')) { - return; - } - - try { - this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; - _saveCookieData(this); - // utils.log('set userId=' + userId); - } catch (e) { - utils.log(e); - } -}; - -AmplitudeClient.prototype.setOptOut = function(enable) { - if (!this._apiKeySet('setOptOut()')) { - return; - } - - try { - this.options.optOut = enable; - _saveCookieData(this); - // utils.log('set optOut=' + enable); - } catch (e) { - utils.log(e); - } -}; - -AmplitudeClient.prototype.setDeviceId = function(deviceId) { - if (!this._apiKeySet('setDeviceId()')) { - return; - } - - try { - if (deviceId) { - this.options.deviceId = ('' + deviceId); - _saveCookieData(this); - } - } catch (e) { - utils.log(e); - } -}; - -AmplitudeClient.prototype.setUserProperties = function(userProperties) { - if (!this._apiKeySet('setUserProperties()')) { - 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); -}; - -// Clearing user properties is irreversible! -AmplitudeClient.prototype.clearUserProperties = function(){ - if (!this._apiKeySet('clearUserProperties()')) { - return; - } - - var identify = new Identify(); - identify.clearAll(); - this.identify(identify); -}; - -AmplitudeClient.prototype.identify = function(identify) { - if (!this._apiKeySet('identify()')) { - return; - } - - if (type(identify) === 'object' && '_q' in identify) { - var instance = new Identify(); - // Apply the queued commands - for (var i = 0; i < identify._q.length; i++) { - var fn = instance[identify._q[i][0]]; - if (fn && type(fn) === 'function') { - fn.apply(instance, identify._q[i].slice(1)); - } - } - identify = instance; - } - - if (identify instanceof Identify && Object.keys(identify.userPropertiesOperations).length > 0) { - this._logEvent(IDENTIFY_EVENT, null, null, identify.userPropertiesOperations); - } -}; - -AmplitudeClient.prototype.setVersionName = function(versionName) { - try { - this.options.versionName = versionName; - // utils.log('set versionName=' + versionName); - } catch (e) { - utils.log(e); - } -}; - -// truncate string values in event and user properties so that request size does not get too large -AmplitudeClient.prototype._truncate = function(value) { - if (type(value) === 'array') { - for (var i = 0; i < value.length; i++) { - value[i] = this._truncate(value[i]); - } - } else if (type(value) === 'object') { - for (var key in value) { - if (value.hasOwnProperty(key)) { - value[key] = this._truncate(value[key]); - } - } - } else { - value = _truncateValue(value); - } - - return value; -}; - -var _truncateValue = function(value) { - if (type(value) === 'string') { - return value.length > MAX_STRING_LENGTH ? value.substring(0, MAX_STRING_LENGTH) : value; - } - return value; -}; - -/** - * Private logEvent method. Keeps apiProperties from being publicly exposed. - */ -AmplitudeClient.prototype._logEvent = function(eventType, eventProperties, apiProperties, userProperties, callback) { - if (type(callback) !== 'function') { - callback = null; - } - - if (!eventType || this.options.optOut) { - if (callback) { - callback(0, 'No request sent'); - } - return; - } - try { - var eventId; - if (eventType === IDENTIFY_EVENT) { - eventId = this.nextIdentifyId(); - } else { - eventId = this.nextEventId(); - } - var sequenceNumber = this.nextSequenceNumber(); - var eventTime = new Date().getTime(); - var ua = this._ua; - if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { - this._sessionId = eventTime; - } - this._lastEventTime = eventTime; - _saveCookieData(this); - - userProperties = userProperties || {}; - // Only add utm properties to user properties for events - if (eventType !== IDENTIFY_EVENT) { - object.merge(userProperties, this._utmProperties); - } - - apiProperties = apiProperties || {}; - eventProperties = eventProperties || {}; - var event = { - device_id: this.options.deviceId, - user_id: this.options.userId || this.options.deviceId, - timestamp: eventTime, - event_id: eventId, - session_id: this._sessionId || -1, - event_type: eventType, - version_name: this.options.versionName || null, - platform: this.options.platform, - os_name: ua.browser.name || null, - os_version: ua.browser.major || null, - device_model: ua.os.name || null, - language: this.options.language, - api_properties: apiProperties, - event_properties: this._truncate(utils.validateProperties(eventProperties)), - user_properties: this._truncate(userProperties), - uuid: UUID(), - library: { - name: 'amplitude-js', - version: version - }, - sequence_number: sequenceNumber // for ordering events and identifys - // country: null - }; - - if (eventType === IDENTIFY_EVENT) { - this._unsentIdentifys.push(event); - this._limitEventsQueued(this._unsentIdentifys); - } else { - this._unsentEvents.push(event); - this._limitEventsQueued(this._unsentEvents); - } - - if (this.options.saveEvents) { - this.saveEvents(); - } - - if (!this._sendEventsIfReady(callback) && callback) { - callback(0, 'No request sent'); - } - - return eventId; - } catch (e) { - utils.log(e); - } -}; - -// Remove old events from the beginning of the array if too many -// have accumulated. Don't want to kill memory. Default is 1000 events. -AmplitudeClient.prototype._limitEventsQueued = function(queue) { - if (queue.length > this.options.savedMaxCount) { - queue.splice(0, queue.length - this.options.savedMaxCount); - } -}; - -AmplitudeClient.prototype.logEvent = function(eventType, eventProperties, callback) { - if (!this._apiKeySet('logEvent()')) { - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, callback); -}; - -// Test that n is a number or a numeric value. -var _isNumber = function(n) { - return !isNaN(parseFloat(n)) && isFinite(n); -}; - -AmplitudeClient.prototype.logRevenue = function(price, quantity, product) { - // Test that the parameters are of the right type. - if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || quantity !== undefined && !_isNumber(quantity)) { - // utils.log('Price and quantity arguments to logRevenue must be numbers'); - return -1; - } - - return this._logEvent('revenue_amount', {}, { - productId: product, - special: 'revenue_amount', - quantity: quantity || 1, - price: price - }); -}; - -/** - * Remove events in storage with event ids up to and including maxEventId. Does - * a true filter in case events get out of order or old events are removed. - */ -AmplitudeClient.prototype.removeEvents = function (maxEventId, maxIdentifyId) { - if (maxEventId >= 0) { - var filteredEvents = []; - for (var i = 0; i < this._unsentEvents.length; i++) { - if (this._unsentEvents[i].event_id > maxEventId) { - filteredEvents.push(this._unsentEvents[i]); - } - } - this._unsentEvents = filteredEvents; - } - - if (maxIdentifyId >= 0) { - var filteredIdentifys = []; - for (var j = 0; j < this._unsentIdentifys.length; j++) { - if (this._unsentIdentifys[j].event_id > maxIdentifyId) { - filteredIdentifys.push(this._unsentIdentifys[j]); - } - } - this._unsentIdentifys = filteredIdentifys; - } -}; - -AmplitudeClient.prototype.sendEvents = function(callback) { - if (!this._apiKeySet('sendEvents()')) { - return; - } - - if (!this._sending && !this.options.optOut && this._unsentCount() > 0) { - this._sending = true; - var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + - this.options.apiEndpoint + '/'; - - // fetch events to send - var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); - var mergedEvents = this._mergeEventsAndIdentifys(numEvents); - var maxEventId = mergedEvents.maxEventId; - var maxIdentifyId = mergedEvents.maxIdentifyId; - var events = JSON.stringify(mergedEvents.eventsToSend); - - var uploadTime = new Date().getTime(); - var data = { - client: this.options.apiKey, - e: events, - v: API_VERSION, - upload_time: uploadTime, - checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime) - }; - - var scope = this; - new Request(url, data).send(function(status, response) { - scope._sending = false; - try { - if (status === 200 && response === 'success') { - // utils.log('sucessful upload'); - scope.removeEvents(maxEventId, maxIdentifyId); - - // Update the event cache after the removal of sent events. - if (scope.options.saveEvents) { - scope.saveEvents(); - } - - // Send more events if any queued during previous send. - if (!scope._sendEventsIfReady(callback) && callback) { - callback(status, response); - } - - } else if (status === 413) { - // utils.log('request too large'); - // Can't even get this one massive event through. Drop it. - if (scope.options.uploadBatchSize === 1) { - // if massive event is identify, still need to drop it - scope.removeEvents(maxEventId, maxIdentifyId); - } - - // The server complained about the length of the request. - // Backoff and try again. - scope.options.uploadBatchSize = Math.ceil(numEvents / 2); - scope.sendEvents(callback); - - } else if (callback) { // If server turns something like a 400 - callback(status, response); - } - } catch (e) { - // utils.log('failed upload'); - } - }); - } else if (callback) { - callback(0, 'No request sent'); - } -}; - -AmplitudeClient.prototype._mergeEventsAndIdentifys = function(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; - - // case 1: no identifys - grab from events - if (identifyIndex >= this._unsentIdentifys.length) { - event = this._unsentEvents[eventIndex++]; - maxEventId = event.event_id; - - // case 2: no events - grab from identifys - } else if (eventIndex >= this._unsentEvents.length) { - 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 - }; -}; - -module.exports = AmplitudeClient; diff --git a/src/amplitude-snippet.js b/src/amplitude-snippet.js index fc81e5d0..52e34fd8 100644 --- a/src/amplitude-snippet.js +++ b/src/amplitude-snippet.js @@ -1,9 +1,9 @@ (function(window, document) { - var amplitude = window.amplitude || {'_q':[], '_iq':{}}; + var amplitude = window.amplitude || {'_q':[]}; var as = document.createElement('script'); as.type = 'text/javascript'; as.async = true; - as.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.9.0-min.gz.js'; + as.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.9.1-min.gz.js'; as.onload = function() {window.amplitude.runQueuedFunctions();}; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(as, s); @@ -28,12 +28,5 @@ for (var j = 0; j < funcs.length; j++) {proxy(funcs[j]);} } setUpProxy(amplitude); - amplitude.getInstance = function(instance) { - instance = ((!instance || instance.length === 0) ? '$default_instance' : instance).toLowerCase(); - if (!amplitude._iq.hasOwnProperty(instance)) { - amplitude._iq[instance] = {'_q':[]}; setUpProxy(amplitude._iq[instance]); - } - return amplitude._iq[instance]; - }; window.amplitude = amplitude; })(window, document); diff --git a/src/amplitude.js b/src/amplitude.js index 2853d721..89d682b1 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -1,33 +1,150 @@ -var AmplitudeClient = require('./amplitude-client'); +var cookieStorage = require('./cookiestorage'); +var getUtmData = require('./utm'); var Identify = require('./identify'); +var JSON = require('json'); // jshint ignore:line +var localStorage = require('./localstorage'); // jshint ignore:line +var md5 = require('JavaScript-MD5'); var object = require('object'); +var Request = require('./xhr'); var type = require('./type'); +var UAParser = require('ua-parser-js'); var utils = require('./utils'); +var UUID = require('./uuid'); var version = require('./version'); var DEFAULT_OPTIONS = require('./options'); -var DEFAULT_INSTANCE = '$default_instance'; +var IDENTIFY_EVENT = '$identify'; +var API_VERSION = 2; +var MAX_STRING_LENGTH = 1024; +var LocalStorageKeys = { + LAST_EVENT_ID: 'amplitude_lastEventId', + LAST_EVENT_TIME: 'amplitude_lastEventTime', + LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', + LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', + REFERRER: 'amplitude_referrer', + SESSION_ID: 'amplitude_sessionId', + // Used in cookie as well + DEVICE_ID: 'amplitude_deviceId', + OPT_OUT: 'amplitude_optOut', + USER_ID: 'amplitude_userId' +}; + +/* + * Amplitude API + */ var Amplitude = function() { - this.options = object.merge({}, DEFAULT_OPTIONS); // maintain a copy for backwards compatibilty - this._instances = {}; // mapping of instance names to instances + 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 }; -Amplitude.prototype.getInstance = function(instance) { - instance = (utils.isEmptyString(instance) ? DEFAULT_INSTANCE : instance).toLowerCase(); +Amplitude.prototype._eventId = 0; +Amplitude.prototype._identifyId = 0; +Amplitude.prototype._sequenceNumber = 0; +Amplitude.prototype._sending = false; +Amplitude.prototype._lastEventTime = null; +Amplitude.prototype._sessionId = null; +Amplitude.prototype._newSession = false; +Amplitude.prototype._updateScheduled = false; + +Amplitude.prototype.Identify = Identify; + +/** + * Initializes Amplitude. + * apiKey The API Key for your app + * opt_userId An identifier for this user + * opt_config Configuration options + * - saveEvents (boolean) Whether to save events to local storage. Defaults to true. + * - includeUtm (boolean) Whether to send utm parameters with events. Defaults to false. + * - includeReferrer (boolean) Whether to send referrer info with events. Defaults to false. + */ +Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, callback) { + try { + this.options.apiKey = apiKey; + if (opt_config) { + if (opt_config.saveEvents !== undefined) { + this.options.saveEvents = !!opt_config.saveEvents; + } + if (opt_config.domain !== undefined) { + this.options.domain = opt_config.domain; + } + if (opt_config.includeUtm !== undefined) { + this.options.includeUtm = !!opt_config.includeUtm; + } + if (opt_config.includeReferrer !== undefined) { + this.options.includeReferrer = !!opt_config.includeReferrer; + } + if (opt_config.batchEvents !== undefined) { + this.options.batchEvents = !!opt_config.batchEvents; + } + this.options.platform = opt_config.platform || this.options.platform; + this.options.language = opt_config.language || this.options.language; + this.options.sessionTimeout = opt_config.sessionTimeout || this.options.sessionTimeout; + this.options.uploadBatchSize = opt_config.uploadBatchSize || this.options.uploadBatchSize; + this.options.eventUploadThreshold = opt_config.eventUploadThreshold || this.options.eventUploadThreshold; + this.options.savedMaxCount = opt_config.savedMaxCount || this.options.savedMaxCount; + this.options.eventUploadPeriodMillis = opt_config.eventUploadPeriodMillis || this.options.eventUploadPeriodMillis; + } + + this.cookieStorage.options({ + expirationDays: this.options.cookieExpiration, + domain: this.options.domain + }); + this.options.domain = this.cookieStorage.options().domain; + + _upgradeCookeData(this); + _loadCookieData(this); + + this.options.deviceId = (opt_config && opt_config.deviceId !== undefined && + opt_config.deviceId !== null && opt_config.deviceId) || + this.options.deviceId || UUID(); + this.options.userId = (opt_userId !== undefined && opt_userId !== null && opt_userId) || this.options.userId || null; - var client = this._instances[instance]; - if (client === undefined) { - client = new AmplitudeClient(instance); - this._instances[instance] = client; + 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); + + //utils.log('initialized with apiKey=' + apiKey); + //opt_userId !== undefined && opt_userId !== null && utils.log('initialized with userId=' + opt_userId); + + if (this.options.saveEvents) { + this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey) || this._unsentEvents; + this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey) || this._unsentIdentifys; + + // validate event properties for unsent events + for (var i = 0; i < this._unsentEvents.length; i++) { + var eventProperties = this._unsentEvents[i].event_properties; + this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); + } + + this._sendEventsIfReady(); + } + + if (this.options.includeUtm) { + this._initUtmData(); + } + + if (this.options.includeReferrer) { + this._saveReferrer(this._getReferrer()); + } + } catch (e) { + utils.log(e); } - return client; -}; -Amplitude.prototype.Identify = Identify; + if (callback && type(callback) === 'function') { + callback(); + } +}; Amplitude.prototype.runQueuedFunctions = function () { - // run queued up old version of functions for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; if (fn && type(fn) === 'function') { @@ -35,101 +152,667 @@ Amplitude.prototype.runQueuedFunctions = function () { } } 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(); - } +Amplitude.prototype._apiKeySet = function(methodName) { + if (!this.options.apiKey) { + utils.log('apiKey cannot be undefined or null, set apiKey with init() before calling ' + methodName); + return false; } + return true; }; -/** - * @deprecated - * Maintain mapping of old functions to new instance methods - */ -Amplitude.prototype.init = function(apiKey, opt_userId, opt_config, 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 (callback && type(callback) === 'function') { - callback(instance); +Amplitude.prototype._loadSavedUnsentEvents = function(unsentKey) { + var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); + if (savedUnsentEventsString) { + try { + return JSON.parse(savedUnsentEventsString); + } catch (e) { + // utils.log(e); } - }.bind(this)); + } + return null; }; Amplitude.prototype.isNewSession = function() { - return this.getInstance().isNewSession(); + return this._newSession; }; Amplitude.prototype.getSessionId = function() { - return this.getInstance().getSessionId(); + return this._sessionId; }; Amplitude.prototype.nextEventId = function() { - return this.getInstance().nextEventId(); + this._eventId++; + return this._eventId; }; Amplitude.prototype.nextIdentifyId = function() { - return this.getInstance().nextIdentifyId(); + this._identifyId++; + return this._identifyId; }; Amplitude.prototype.nextSequenceNumber = function() { - return this.getInstance().nextSequenceNumber(); + this._sequenceNumber++; + return this._sequenceNumber; +}; + +// returns the number of unsent events and identifys +Amplitude.prototype._unsentCount = function() { + return this._unsentEvents.length + this._unsentIdentifys.length; +}; + +// returns true if sendEvents called immediately +Amplitude.prototype._sendEventsIfReady = function(callback) { + if (this._unsentCount() === 0) { + return false; + } + + if (!this.options.batchEvents) { + this.sendEvents(callback); + return true; + } + + if (this._unsentCount() >= 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; +}; + +// storage argument allows for localStorage and sessionStorage +Amplitude.prototype._getFromStorage = function(storage, key) { + return storage.getItem(key); +}; + +// storage argument allows for localStorage and sessionStorage +Amplitude.prototype._setInStorage = function(storage, key, value) { + storage.setItem(key, value); +}; + +/* + * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber) + * can be stored in many different places (localStorage, cookie, etc). + * Need to unify all sources into one place with a one-time upgrade/migration for the defaultInstance. + */ +var _upgradeCookeData = function(scope) { + // skip if migration already happened + var cookieData = scope.cookieStorage.get(scope.options.cookieName); + if (cookieData && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { + return; + } + + var _getAndRemoveFromLocalStorage = function(key) { + var value = localStorage.getItem(key); + localStorage.removeItem(key); + return value; + }; + + // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey + var apiKeySuffix = '_' + scope.options.apiKey.slice(0, 6); + var localStorageDeviceId = _getAndRemoveFromLocalStorage(LocalStorageKeys.DEVICE_ID + apiKeySuffix); + var localStorageUserId = _getAndRemoveFromLocalStorage(LocalStorageKeys.USER_ID + apiKeySuffix); + var localStorageOptOut = _getAndRemoveFromLocalStorage(LocalStorageKeys.OPT_OUT + apiKeySuffix); + if (localStorageOptOut !== null && localStorageOptOut !== undefined) { + localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean + } + + // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support + var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.SESSION_ID)); + var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_TIME)); + var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_EVENT_ID)); + var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_IDENTIFY_ID)); + var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(LocalStorageKeys.LAST_SEQUENCE_NUMBER)); + + var _getFromCookie = function(key) { + return cookieData && cookieData[key]; + }; + 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); +}; + +var _loadCookieData = function(scope) { + var cookieData = scope.cookieStorage.get(scope.options.cookieName); + if (cookieData) { + if (cookieData.deviceId) { + scope.options.deviceId = cookieData.deviceId; + } + if (cookieData.userId) { + scope.options.userId = cookieData.userId; + } + if (cookieData.optOut !== null && cookieData.optOut !== undefined) { + scope.options.optOut = cookieData.optOut; + } + if (cookieData.sessionId) { + scope._sessionId = parseInt(cookieData.sessionId); + } + if (cookieData.lastEventTime) { + scope._lastEventTime = parseInt(cookieData.lastEventTime); + } + if (cookieData.eventId) { + scope._eventId = parseInt(cookieData.eventId); + } + if (cookieData.identifyId) { + scope._identifyId = parseInt(cookieData.identifyId); + } + if (cookieData.sequenceNumber) { + scope._sequenceNumber = parseInt(cookieData.sequenceNumber); + } + } +}; + +var _saveCookieData = function(scope) { + scope.cookieStorage.set(scope.options.cookieName, { + deviceId: scope.options.deviceId, + userId: scope.options.userId, + optOut: scope.options.optOut, + sessionId: scope._sessionId, + lastEventTime: scope._lastEventTime, + eventId: scope._eventId, + identifyId: scope._identifyId, + sequenceNumber: scope._sequenceNumber + }); +}; + +/** + * Parse the utm properties out of cookies and query for adding to user properties. + */ +Amplitude.prototype._initUtmData = function(queryParams, cookieParams) { + queryParams = queryParams || location.search; + cookieParams = cookieParams || this.cookieStorage.get('__utmz'); + this._utmProperties = getUtmData(cookieParams, queryParams); +}; + +Amplitude.prototype._getReferrer = function() { + return document.referrer; +}; + +Amplitude.prototype._getReferringDomain = function(referrer) { + if (referrer === null || referrer === undefined || referrer === '') { + return null; + } + var parts = referrer.split('/'); + if (parts.length >= 3) { + return parts[2]; + } + return null; +}; + +// since user properties are propagated on the server, only send once per session, don't need to send with every event +Amplitude.prototype._saveReferrer = function(referrer) { + if (referrer === null || referrer === undefined || referrer === '') { + return; + } + + // always setOnce initial referrer + var referring_domain = this._getReferringDomain(referrer); + var identify = new Identify().setOnce('initial_referrer', referrer); + identify.setOnce('initial_referring_domain', referring_domain); + + // only save referrer if not already in session storage or if storage disabled + var hasSessionStorage = false; + try { + if (window.sessionStorage) { + hasSessionStorage = true; + } + } catch (e) { + // utils.log(e); // sessionStorage disabled + } + + if ((hasSessionStorage && !(this._getFromStorage(sessionStorage, LocalStorageKeys.REFERRER))) || !hasSessionStorage) { + identify.set('referrer', referrer).set('referring_domain', referring_domain); + + if (hasSessionStorage) { + this._setInStorage(sessionStorage, LocalStorageKeys.REFERRER, referrer); + } + } + + this.identify(identify); }; Amplitude.prototype.saveEvents = function() { - this.getInstance().saveEvents(); + if (!this._apiKeySet('saveEvents()')) { + return; + } + + try { + this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); + this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); + } catch (e) { + // utils.log(e); + } }; Amplitude.prototype.setDomain = function(domain) { - this.getInstance().setDomain(domain); + if (!this._apiKeySet('setDomain()')) { + return; + } + + try { + this.cookieStorage.options({ + domain: domain + }); + this.options.domain = this.cookieStorage.options().domain; + _loadCookieData(this); + _saveCookieData(this); + // utils.log('set domain=' + domain); + } catch (e) { + utils.log(e); + } }; Amplitude.prototype.setUserId = function(userId) { - this.getInstance().setUserId(userId); + if (!this._apiKeySet('setUserId()')) { + return; + } + + try { + this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; + _saveCookieData(this); + // utils.log('set userId=' + userId); + } catch (e) { + utils.log(e); + } }; Amplitude.prototype.setOptOut = function(enable) { - this.getInstance().setOptOut(enable); + if (!this._apiKeySet('setOptOut()')) { + return; + } + + try { + this.options.optOut = enable; + _saveCookieData(this); + // utils.log('set optOut=' + enable); + } catch (e) { + utils.log(e); + } }; Amplitude.prototype.setDeviceId = function(deviceId) { - this.getInstance().setDeviceId(deviceId); + if (!this._apiKeySet('setDeviceId()')) { + return; + } + + try { + if (deviceId) { + this.options.deviceId = ('' + deviceId); + _saveCookieData(this); + } + } catch (e) { + utils.log(e); + } }; Amplitude.prototype.setUserProperties = function(userProperties) { - this.getInstance().setUserProperties(userProperties); + if (!this._apiKeySet('setUserProperties()')) { + 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); }; +// Clearing user properties is irreversible! Amplitude.prototype.clearUserProperties = function(){ - this.getInstance().clearUserProperties(); + if (!this._apiKeySet('clearUserProperties()')) { + return; + } + + var identify = new Identify(); + identify.clearAll(); + this.identify(identify); }; Amplitude.prototype.identify = function(identify) { - this.getInstance().identify(identify); + if (!this._apiKeySet('identify()')) { + return; + } + + if (type(identify) === 'object' && '_q' in identify) { + var instance = new Identify(); + // Apply the queued commands + for (var i = 0; i < identify._q.length; i++) { + var fn = instance[identify._q[i][0]]; + if (fn && type(fn) === 'function') { + fn.apply(instance, identify._q[i].slice(1)); + } + } + identify = instance; + } + + if (identify instanceof Identify && Object.keys(identify.userPropertiesOperations).length > 0) { + this._logEvent(IDENTIFY_EVENT, null, null, identify.userPropertiesOperations); + } }; Amplitude.prototype.setVersionName = function(versionName) { - this.getInstance().setVersionName(versionName); + try { + this.options.versionName = versionName; + // utils.log('set versionName=' + versionName); + } catch (e) { + utils.log(e); + } +}; + +// truncate string values in event and user properties so that request size does not get too large +Amplitude.prototype._truncate = function(value) { + if (type(value) === 'array') { + for (var i = 0; i < value.length; i++) { + value[i] = this._truncate(value[i]); + } + } else if (type(value) === 'object') { + for (var key in value) { + if (value.hasOwnProperty(key)) { + value[key] = this._truncate(value[key]); + } + } + } else { + value = _truncateValue(value); + } + + return value; +}; + +var _truncateValue = function(value) { + if (type(value) === 'string') { + return value.length > MAX_STRING_LENGTH ? value.substring(0, MAX_STRING_LENGTH) : value; + } + return value; +}; + +/** + * Private logEvent method. Keeps apiProperties from being publicly exposed. + */ +Amplitude.prototype._logEvent = function(eventType, eventProperties, apiProperties, userProperties, callback) { + if (type(callback) !== 'function') { + callback = null; + } + + if (!eventType || this.options.optOut) { + if (callback) { + callback(0, 'No request sent'); + } + return; + } + try { + var eventId; + if (eventType === IDENTIFY_EVENT) { + eventId = this.nextIdentifyId(); + } else { + eventId = this.nextEventId(); + } + var sequenceNumber = this.nextSequenceNumber(); + var eventTime = new Date().getTime(); + var ua = this._ua; + if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { + this._sessionId = eventTime; + } + this._lastEventTime = eventTime; + _saveCookieData(this); + + userProperties = userProperties || {}; + // Only add utm properties to user properties for events + if (eventType !== IDENTIFY_EVENT) { + object.merge(userProperties, this._utmProperties); + } + + apiProperties = apiProperties || {}; + eventProperties = eventProperties || {}; + var event = { + device_id: this.options.deviceId, + user_id: this.options.userId || this.options.deviceId, + timestamp: eventTime, + event_id: eventId, + session_id: this._sessionId || -1, + event_type: eventType, + version_name: this.options.versionName || null, + platform: this.options.platform, + os_name: ua.browser.name || null, + os_version: ua.browser.major || null, + device_model: ua.os.name || null, + language: this.options.language, + api_properties: apiProperties, + event_properties: this._truncate(utils.validateProperties(eventProperties)), + user_properties: this._truncate(userProperties), + uuid: UUID(), + library: { + name: 'amplitude-js', + version: version + }, + sequence_number: sequenceNumber // for ordering events and identifys + // country: null + }; + + if (eventType === IDENTIFY_EVENT) { + this._unsentIdentifys.push(event); + this._limitEventsQueued(this._unsentIdentifys); + } else { + this._unsentEvents.push(event); + this._limitEventsQueued(this._unsentEvents); + } + + if (this.options.saveEvents) { + this.saveEvents(); + } + + if (!this._sendEventsIfReady(callback) && callback) { + callback(0, 'No request sent'); + } + + return eventId; + } catch (e) { + utils.log(e); + } +}; + +// Remove old events from the beginning of the array if too many +// have accumulated. Don't want to kill memory. Default is 1000 events. +Amplitude.prototype._limitEventsQueued = function(queue) { + if (queue.length > this.options.savedMaxCount) { + queue.splice(0, queue.length - this.options.savedMaxCount); + } }; Amplitude.prototype.logEvent = function(eventType, eventProperties, callback) { - return this.getInstance().logEvent(eventType, eventProperties, callback); + if (!this._apiKeySet('logEvent()')) { + return -1; + } + return this._logEvent(eventType, eventProperties, null, null, callback); +}; + +// Test that n is a number or a numeric value. +var _isNumber = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); }; Amplitude.prototype.logRevenue = function(price, quantity, product) { - return this.getInstance().logRevenue(price, quantity, product); + // Test that the parameters are of the right type. + if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || quantity !== undefined && !_isNumber(quantity)) { + // utils.log('Price and quantity arguments to logRevenue must be numbers'); + return -1; + } + + return this._logEvent('revenue_amount', {}, { + productId: product, + special: 'revenue_amount', + quantity: quantity || 1, + price: price + }); }; +/** + * Remove events in storage with event ids up to and including maxEventId. Does + * a true filter in case events get out of order or old events are removed. + */ Amplitude.prototype.removeEvents = function (maxEventId, maxIdentifyId) { - this.getInstance().removeEvents(maxEventId, maxIdentifyId); + if (maxEventId >= 0) { + var filteredEvents = []; + for (var i = 0; i < this._unsentEvents.length; i++) { + if (this._unsentEvents[i].event_id > maxEventId) { + filteredEvents.push(this._unsentEvents[i]); + } + } + this._unsentEvents = filteredEvents; + } + + if (maxIdentifyId >= 0) { + var filteredIdentifys = []; + for (var j = 0; j < this._unsentIdentifys.length; j++) { + if (this._unsentIdentifys[j].event_id > maxIdentifyId) { + filteredIdentifys.push(this._unsentIdentifys[j]); + } + } + this._unsentIdentifys = filteredIdentifys; + } }; Amplitude.prototype.sendEvents = function(callback) { - this.getInstance().sendEvents(callback); + if (!this._apiKeySet('sendEvents()')) { + return; + } + + if (!this._sending && !this.options.optOut && this._unsentCount() > 0) { + this._sending = true; + var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + + this.options.apiEndpoint + '/'; + + // fetch events to send + var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); + var mergedEvents = this._mergeEventsAndIdentifys(numEvents); + var maxEventId = mergedEvents.maxEventId; + var maxIdentifyId = mergedEvents.maxIdentifyId; + var events = JSON.stringify(mergedEvents.eventsToSend); + + var uploadTime = new Date().getTime(); + var data = { + client: this.options.apiKey, + e: events, + v: API_VERSION, + upload_time: uploadTime, + checksum: md5(API_VERSION + this.options.apiKey + events + uploadTime) + }; + + var scope = this; + new Request(url, data).send(function(status, response) { + scope._sending = false; + try { + if (status === 200 && response === 'success') { + // utils.log('sucessful upload'); + scope.removeEvents(maxEventId, maxIdentifyId); + + // Update the event cache after the removal of sent events. + if (scope.options.saveEvents) { + scope.saveEvents(); + } + + // Send more events if any queued during previous send. + if (!scope._sendEventsIfReady(callback) && callback) { + callback(status, response); + } + + } else if (status === 413) { + // utils.log('request too large'); + // Can't even get this one massive event through. Drop it. + if (scope.options.uploadBatchSize === 1) { + // if massive event is identify, still need to drop it + scope.removeEvents(maxEventId, maxIdentifyId); + } + + // The server complained about the length of the request. + // Backoff and try again. + scope.options.uploadBatchSize = Math.ceil(numEvents / 2); + scope.sendEvents(callback); + + } else if (callback) { // If server turns something like a 400 + callback(status, response); + } + } catch (e) { + // utils.log('failed upload'); + } + }); + } else if (callback) { + callback(0, 'No request sent'); + } +}; + +Amplitude.prototype._mergeEventsAndIdentifys = function(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; + + // case 1: no identifys - grab from events + if (identifyIndex >= this._unsentIdentifys.length) { + event = this._unsentEvents[eventIndex++]; + maxEventId = event.event_id; + + // case 2: no events - grab from identifys + } else if (eventIndex >= this._unsentEvents.length) { + 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 + }; }; +/** + * @deprecated + */ Amplitude.prototype.setGlobalUserProperties = Amplitude.prototype.setUserProperties; Amplitude.prototype.__VERSION__ = version; diff --git a/src/index.js b/src/index.js index e735b6cc..c5cab082 100644 --- a/src/index.js +++ b/src/index.js @@ -1,13 +1,10 @@ /* jshint expr:true */ var Amplitude = require('./amplitude'); + var old = window.amplitude || {}; -var newInstance = new Amplitude(); -newInstance._q = old._q || []; -for (var instance in old._iq) { // migrate each instance's queue - if (old._iq.hasOwnProperty(instance)) { - newInstance.getInstance(instance)._q = old._iq[instance]._q || []; - } -} +var instance = new Amplitude(); +instance._q = old._q || []; -module.exports = newInstance; +// export the instance +module.exports = instance; diff --git a/src/options.js b/src/options.js index 778b3d34..0c2b913d 100644 --- a/src/options.js +++ b/src/options.js @@ -19,5 +19,4 @@ module.exports = { batchEvents: false, eventUploadThreshold: 30, eventUploadPeriodMillis: 30 * 1000, // 30s - newBlankInstance: false }; diff --git a/test/amplitude-client.js b/test/amplitude-client.js deleted file mode 100644 index 5f548679..00000000 --- a/test/amplitude-client.js +++ /dev/null @@ -1,2053 +0,0 @@ -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 apiKey = '000000'; - var keySuffix = '_' + apiKey.slice(0, 6); - var userId = 'user'; - var amplitude; - var server; - - beforeEach(function() { - amplitude = new AmplitudeClient(); - 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.remove(amplitude.options.cookieName + keySuffix); - cookie.remove(amplitude.options.cookieName + '_new_app'); - cookie.reset(); - } - - describe('init', function() { - beforeEach(function() { - reset(); - }); - - afterEach(function() { - reset(); - }); - - it('should make instanceName case-sensitive', function() { - assert.equal(new AmplitudeClient('APP3')._instanceName, 'app3'); - assert.equal(new AmplitudeClient('$DEFAULT_INSTANCE')._instanceName, '$default_instance'); - }); - - it('should accept userId', function() { - amplitude.init(apiKey, userId); - assert.equal(amplitude.options.userId, userId); - }); - - it('should set cookie', function() { - amplitude.init(apiKey, userId); - var stored = cookie.get(amplitude.options.cookieName); - assert.property(stored, 'deviceId'); - assert.propertyVal(stored, 'userId', userId); - assert.lengthOf(stored.deviceId, 36); - }); - - 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 on default instance', 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 not migrate any cookie or LS data for non-default instances', 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); - - var amplitude2 = new AmplitudeClient('new_app'); - amplitude2.init(apiKey); - assert.notEqual(amplitude2.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(); - - 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 AmplitudeClient(); - 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 AmplitudeClient(); - - 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 AmplitudeClient(); - - 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 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":{},' + - '"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 AmplitudeClient('$default_Instance'); - 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 load saved events from localStorage 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}]'; - 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); - 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",' + - '"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 AmplitudeClient('$default_Instance'); - 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 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 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: 5}); - - // check events not loaded into memory - assert.deepEqual(amplitude2._unsentEvents, []); - assert.deepEqual(amplitude2._unsentIdentifys, []); - - // check local storage - assert.isNull(localStorage.getItem('amplitude_unsent_new_app')); - assert.isNull(localStorage.getItem('amplitude_unsent_identify_new_app')); - }); - }); - - 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('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'], - ]}; - 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'} - }); - }); - }); - - 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 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(2000).join('a'); - amplitude.logEvent('test', {'key': longString}); - var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; - - assert.isTrue('key' in event.event_properties); - assert.lengthOf(event.event_properties['key'], 1024); - }); - - it('should truncate long user property strings', function() { - var longString = new Array(2000).join('a'); - amplitude.identify(new Identify().set('key', longString)); - var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; - - assert.isTrue('$set' in event.user_properties); - assert.lengthOf(event.user_properties['$set']['key'], 1024); - }); - - it('should increment the counters in local storage if cookies disabled', function() { - localStorage.clear(); - var deviceId = 'test_device_id'; - var amplitude2 = new AmplitudeClient(); - - 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': utils.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']}} - }); - }); - }); - - 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 AmplitudeClient(); - amplitude.init(apiKey); - assert.strictEqual(amplitude.options.optOut, false); - - amplitude.setOptOut(true); - - var amplitude2 = new AmplitudeClient(); - 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 when the includeUtm flag is true', function() { - cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); - reset(); - amplitude.init(apiKey, undefined, {includeUtm: true}); - - amplitude.logEvent('UTM Test Event', {}); - - assert.lengthOf(server.requests, 1); - var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); - assert.deepEqual(events[0].user_properties, { - utm_campaign: 'new', - utm_content: 'top' - }); - }); - - it('should add utm params to the user properties', function() { - cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); - - var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; - amplitude._initUtmData(utmParams); - - amplitude.setUserProperties({user_prop: true}); - assert.lengthOf(server.requests, 1); - var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); - // identify event should not have utm properties - assert.deepEqual(events[0].user_properties, { - '$set': { - 'user_prop': true - } - }); - 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, { - utm_campaign: 'new', - utm_content: 'top', - utm_medium: 'email', - utm_source: 'amplitude', - utm_term: 'terms' - }); - }); - }); - - 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); - - // first event should be identify with initial_referrer and referrer - assert.equal(events[0].event_type, '$identify'); - assert.deepEqual(events[0].user_properties, { - '$set': { - 'referrer': 'https://amplitude.com/contact', - 'referring_domain': 'amplitude.com' - }, - '$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'), 'https://amplitude.com/contact'); - }); - - it('should not set referrer if referrer data old key 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 set referrer if referrer data new key already in session storage', 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: 2}); - amplitude2._getReferrer.restore(); - - amplitude2.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 for default instance', 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?'); - }); - - it('should not override any existing initial 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() { - 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('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 AmplitudeClient(); - amplitude2.init(apiKey); - assert.equal(amplitude2._sessionId, timestamp); - assert.equal(amplitude2.getSessionId(), timestamp); - assert.equal(amplitude2.getSessionId(), amplitude2._sessionId); - }); - }); - - describe('truncate', function() { - var longString = new Array(2000).join('a'); - - it('should truncate long strings', function() { - eventProperties = amplitude._truncate({'test': longString}); - assert.lengthOf(eventProperties['test'], 1024); - }); - - it('should ignore keys', function() { - var eventProperties = {}; - eventProperties[longString] = 'test'; - eventProperties = amplitude._truncate(eventProperties); - assert.isTrue(longString in eventProperties); - }); - - it('should handle arrays', function() { - var eventProperties = [longString, longString]; - eventProperties = amplitude._truncate(eventProperties); - assert.lengthOf(eventProperties, 2); - assert.lengthOf(eventProperties[0], 1024); - assert.lengthOf(eventProperties[1], 1024); - }); - - it('should handle nested dictionaries', function() { - var name = {'first': 'John', 'last': longString}; - var eventProperties = amplitude._truncate({'name': name}); - assert.lengthOf(Object.keys(eventProperties), 1); - assert.lengthOf(Object.keys(eventProperties['name']), 2); - assert.lengthOf(eventProperties['name']['first'], 4); - assert.lengthOf(eventProperties['name']['last'], 1024); - }); - - it('should ignore boolean and number values', function() { - var test = {'key1': 24, 'key2': false}; - assert.deepEqual(test, amplitude._truncate(test)); - }); - - it('should handle nested arrays', function() { - var test = [longString, 'test']; - var eventProperties = amplitude._truncate([test, test]); - assert.lengthOf(eventProperties, 2); - assert.lengthOf(eventProperties[0], 2); - assert.lengthOf(eventProperties[1], 2); - assert.lengthOf(eventProperties[0][0], 1024); - assert.lengthOf(eventProperties[0][1], 4); - assert.lengthOf(eventProperties[1][0], 1024); - assert.lengthOf(eventProperties[1][1], 4); - }); - - it('should handle arrays nested inside dictionaries', function() { - var test = [longString, 'test']; - var eventProperties = amplitude._truncate({'name': test}); - assert.lengthOf(Object.keys(eventProperties), 1); - assert.lengthOf(eventProperties['name'], 2); - assert.lengthOf(eventProperties['name'][0], 1024); - assert.lengthOf(eventProperties['name'][1], 4); - }); - }); -}); diff --git a/test/amplitude.js b/test/amplitude.js index 86bed112..89646aee 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -6,6 +6,7 @@ describe('Amplitude', function() { 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'); @@ -32,189 +33,9 @@ describe('Amplitude', function() { localStorage.clear(); sessionStorage.clear(); 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(); } - 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')); - - // test for case insensitivity - assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_INSTANCE')); - assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_instance')); - assert.equal(amplitude.options.deviceId, amplitude.getInstance().options.deviceId); - }); - - 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 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(); @@ -280,6 +101,299 @@ describe('Amplitude', function() { 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}]'; + 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}); + + // 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 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'); + }); }); describe('runQueuedFunctions', function() { @@ -292,7 +406,7 @@ describe('Amplitude', function() { }); it('should run queued functions', function() { - assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); assert.lengthOf(server.requests, 0); var userId = 'testUserId' var eventType = 'test_event' @@ -305,7 +419,7 @@ describe('Amplitude', function() { amplitude.runQueuedFunctions(); assert.equal(amplitude.options.userId, userId); - assert.equal(amplitude.getInstance()._unsentCount(), 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); @@ -325,12 +439,12 @@ describe('Amplitude', function() { }); it('should log identify call from set user properties', function() { - assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); amplitude.setUserProperties({'prop': true, 'key': 'value'}); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); - assert.equal(amplitude.getInstance()._unsentCount(), 1); + 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); @@ -357,12 +471,12 @@ describe('Amplitude', function() { }); it('should log identify call from clear user properties', function() { - assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); amplitude.clearUserProperties(); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); - assert.equal(amplitude.getInstance()._unsentCount(), 1); + 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); @@ -428,19 +542,19 @@ describe('Amplitude', function() { it('should ignore inputs that are not identify objects', function() { amplitude.identify('This is a test'); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify(150); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify(['test']); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify({'user_prop': true}); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); @@ -448,9 +562,9 @@ describe('Amplitude', function() { var identify = new Identify().set('prop1', 'value1').unset('prop2').add('prop3', 3).setOnce('prop4', true); amplitude.identify(identify); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); - assert.equal(amplitude.getInstance()._unsentCount(), 1); + 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); @@ -474,17 +588,17 @@ describe('Amplitude', function() { it('should ignore empty identify objects', function() { amplitude.identify(new Identify()); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); it('should ignore empty proxy identify objects', function() { amplitude.identify({'_q': {}}); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify({}); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); @@ -498,9 +612,9 @@ describe('Amplitude', function() { ]}; amplitude.identify(proxyObject); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); - assert.equal(amplitude.getInstance()._unsentCount(), 1); + 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); @@ -577,11 +691,11 @@ describe('Amplitude', function() { }); it('should queue events', function() { - amplitude.getInstance()._sending = true; + amplitude._sending = true; amplitude.logEvent('Event', {index: 1}); amplitude.logEvent('Event', {index: 2}); amplitude.logEvent('Event', {index: 3}); - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -595,11 +709,11 @@ describe('Amplitude', function() { it('should limit events queued', function() { amplitude.init(apiKey, null, {savedMaxCount: 10}); - amplitude.getInstance()._sending = true; + amplitude._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -611,10 +725,10 @@ describe('Amplitude', function() { }); it('should remove only sent events', function() { - amplitude.getInstance()._sending = true; + amplitude._sending = true; amplitude.logEvent('Event', {index: 1}); amplitude.logEvent('Event', {index: 2}); - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 3}); server.respondWith('success'); @@ -628,14 +742,36 @@ describe('Amplitude', function() { 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.getInstance()._sending = true; + amplitude._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -677,7 +813,7 @@ describe('Amplitude', function() { server.respond(); assert.lengthOf(server.requests, 1); - var unsentEvents = amplitude.getInstance()._unsentEvents; + var unsentEvents = amplitude._unsentEvents; assert.lengthOf(unsentEvents, 5); assert.deepEqual(unsentEvents[4].event_properties, {index: 14}); @@ -686,7 +822,7 @@ describe('Amplitude', function() { assert.lengthOf(server.requests, 2); server.respondWith('success'); server.respond(); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + 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}); @@ -702,7 +838,7 @@ describe('Amplitude', function() { amplitude.logEvent('Event'); // saveEvent should not have been called yet - assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); + assert.lengthOf(amplitude._unsentEvents, 1); assert.lengthOf(server.requests, 0); // saveEvent should be called after delay @@ -726,7 +862,7 @@ describe('Amplitude', function() { amplitude.logEvent('Event2'); // saveEvent triggered by 2 event batch threshold - assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); + assert.lengthOf(amplitude._unsentEvents, 2); assert.lengthOf(server.requests, 1); server.respondWith('success'); server.respond(); @@ -735,7 +871,7 @@ describe('Amplitude', function() { assert.deepEqual(events[1].event_type, 'Event2'); // saveEvent should be called after delay, but no request made - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude._unsentEvents, 0); clock.tick(eventUploadPeriodMillis); assert.lengthOf(server.requests, 1); }); @@ -752,7 +888,7 @@ describe('Amplitude', function() { amplitude.logEvent('Event1'); clock.tick(1); amplitude.logEvent('Event2'); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); + assert.lengthOf(amplitude._unsentEvents, 2); assert.lengthOf(server.requests, 0); // advance to upload period millis, and should have 1 server request @@ -777,11 +913,11 @@ describe('Amplitude', function() { it('should back off on 413 status', function() { amplitude.init(apiKey, null, {uploadBatchSize: 10}); - amplitude.getInstance()._sending = true; + amplitude._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -804,11 +940,11 @@ describe('Amplitude', 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()._sending = true; + amplitude._sending = true; for (var i = 0; i < 10; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 100}); for (var i = 0; i < 6; i++) { @@ -923,11 +1059,11 @@ describe('Amplitude', function() { }; // queue up 15 events, since batchsize 10, need to send in 2 batches - amplitude.getInstance()._sending = true; + amplitude._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.logEvent('Event', {index: 100}, callback); @@ -961,11 +1097,11 @@ describe('Amplitude', function() { }; // queue up 15 events - amplitude.getInstance()._sending = true; + amplitude._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; // 16th event with 413 will backoff to batches of 8 amplitude.logEvent('Event', {index: 100}, callback); @@ -1022,18 +1158,18 @@ describe('Amplitude', function() { it('should send 3 identify events', function() { amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude.getInstance()._unsentCount(), 0); + 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.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.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); @@ -1049,24 +1185,24 @@ describe('Amplitude', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude.getInstance()._unsentCount(), 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + 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.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); amplitude.logEvent('test'); amplitude.logEvent('test'); amplitude.logEvent('test'); // verify some internal counters - 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.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); @@ -1080,23 +1216,23 @@ describe('Amplitude', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude.getInstance()._unsentCount(), 0); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + 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.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); amplitude.logEvent('test'); amplitude.identify(new Identify().add('photoCount', 1)); // verify some internal counters - 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.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); @@ -1116,14 +1252,14 @@ describe('Amplitude', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude.getInstance()._unsentCount(), 0); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + 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.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); amplitude.logEvent('test1'); clock.tick(1); @@ -1137,11 +1273,11 @@ describe('Amplitude', function() { amplitude.identify(new Identify().add('photoCount', 2)); // verify some internal counters - 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.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); @@ -1172,28 +1308,28 @@ describe('Amplitude', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude.getInstance()._unsentCount(), 0); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + 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.getInstance()._unsentCount(), 0); + assert.equal(amplitude._unsentCount(), 0); 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 + 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.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.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); @@ -1220,9 +1356,9 @@ describe('Amplitude', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude.getInstance()._unsentCount(), 0); - assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); - assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + 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() { @@ -1231,21 +1367,21 @@ describe('Amplitude', function() { clock.tick(1); amplitude.identify(new Identify().add('photoCount', 1)); - assert.equal(amplitude.getInstance()._unsentCount(), 2); + 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.getInstance()._unsentCount(), 2); + 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.getInstance()._unsentCount(), 1); + assert.equal(amplitude._unsentCount(), 1); assert.lengthOf(server.requests, 3); var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); @@ -1261,21 +1397,21 @@ describe('Amplitude', function() { clock.tick(1); amplitude.logEvent('test'); - assert.equal(amplitude.getInstance()._unsentCount(), 2); + 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.getInstance()._unsentCount(), 2); + 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.getInstance()._unsentCount(), 1); + assert.equal(amplitude._unsentCount(), 1); assert.lengthOf(server.requests, 3); var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); @@ -1329,6 +1465,55 @@ describe('Amplitude', function() { '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': utils.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']}} + }); + }); }); describe('optOut', function() { @@ -1350,7 +1535,7 @@ describe('Amplitude', function() { amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.setOptOut(true); amplitude.init(apiKey); assert.lengthOf(server.requests, 1); @@ -1384,11 +1569,11 @@ describe('Amplitude', function() { it('should limit identify events queued', function() { amplitude.init(apiKey, null, {savedMaxCount: 10}); - amplitude.getInstance()._sending = true; + amplitude._sending = true; for (var i = 0; i < 15; i++) { amplitude.identify(new Identify().add('test', i)); } - amplitude.getInstance()._sending = false; + amplitude._sending = false; amplitude.identify(new Identify().add('test', 100)); assert.lengthOf(server.requests, 1); @@ -1442,7 +1627,7 @@ describe('Amplitude', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; - amplitude.getInstance()._initUtmData(utmParams); + amplitude._initUtmData(utmParams); amplitude.setUserProperties({user_prop: true}); assert.lengthOf(server.requests, 1); @@ -1472,11 +1657,11 @@ describe('Amplitude', function() { describe('gatherReferrer', function() { beforeEach(function() { amplitude.init(apiKey); - sinon.stub(amplitude.getInstance(), '_getReferrer').returns('https://amplitude.com/contact'); + sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.com/contact'); }); afterEach(function() { - amplitude.getInstance()._getReferrer.restore(); + amplitude._getReferrer.restore(); reset(); }); @@ -1546,7 +1731,7 @@ describe('Amplitude', function() { reset(); sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); - amplitude.getInstance()._saveReferrer('https://facebook.com/contact'); + 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); @@ -1658,8 +1843,8 @@ describe('Amplitude', function() { 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.getInstance()._sessionId, sessionId); - assert.equal(events[0].session_id, amplitude.getInstance()._sessionId); + assert.notEqual(amplitude._sessionId, sessionId); + assert.equal(events[0].session_id, amplitude._sessionId); }); it('should be fetched correctly by getSessionId', function() { @@ -1667,9 +1852,9 @@ describe('Amplitude', function() { clock.tick(timestamp); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.equal(amplitude2.getInstance()._sessionId, timestamp); + assert.equal(amplitude2._sessionId, timestamp); assert.equal(amplitude2.getSessionId(), timestamp); - assert.equal(amplitude2.getSessionId(), amplitude2.getInstance()._sessionId); + assert.equal(amplitude2.getSessionId(), amplitude2._sessionId); }); }); }); diff --git a/test/browser/amplitudejs-requirejs.html b/test/browser/amplitudejs-requirejs.html index 7be05701..839331ac 100644 --- a/test/browser/amplitudejs-requirejs.html +++ b/test/browser/amplitudejs-requirejs.html @@ -4,7 +4,7 @@

Amplitude JS Test

diff --git a/test/browser/amplitudejs2.html b/test/browser/amplitudejs2.html index 51e6860a..a67b09c7 100644 --- a/test/browser/amplitudejs2.html +++ b/test/browser/amplitudejs2.html @@ -2,7 +2,7 @@