diff --git a/CHANGELOG.md b/CHANGELOG.md index 50077f24..38b11e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +### 3.0.0 (May 27, 2016) + +* Add support for logging events to multiple Amplitude apps. **Note this is a major update, and may break backwards compatability.** See [Readme](https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps) for details. +* Init callback now passes the Amplitude instance as an argument to the callback function. + ### 2.13.0 (May 26, 2016) * Update our fork of [UAParser.js](https://github.com/faisalman/ua-parser-js) from v0.7.7 to v0.7.10. This will improve the resolution of user agent strings to device and OS information. diff --git a/README.md b/README.md index 8995c093..975bb360 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,20 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use ```html ``` @@ -32,11 +34,83 @@ This Readme will guide you through using Amplitude's Javascript SDK to track use 4. To track an event anywhere on the page, call: ```javascript - amplitude.logEvent('EVENT_IDENTIFIER_HERE'); + amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE'); ``` 5. Events are uploaded immediately and saved to the browser's local storage until the server confirms the upload. After calling logEvent in your app, you will immediately see data appear on Amplitude. +# 3.0.0 Update and Logging Events to Multiple Amplitude Apps # + +Version 3.0.0 is a major update that brings support for logging events to multiple Amplitude apps (multiple API keys). **Note: this change is not 100% backwards compatible and may break on your setup.** See the subsection below on backwards compatibility. + +### API Changes and Backwards Compatibility ### + +The `amplitude` object now maintains one or more instances, where each instance has separate apiKey, userId, deviceId, and settings. Having separate instances allows for the logging of events to separate Amplitude apps. + +The most important API change is how you interact with the `amplitude` object. Before v3.0.0, you would directly call `amplitude.logEvent('EVENT_NAME')`. Now the preferred way is to call functions on an instance as follows: `amplitude.getInstance('INSTANCE_NAME').logEvent('EVENT_NAME')` This notation will be familiar to people who have used our iOS and Android SDKs. + +Most people upgrading to v3.0.0 will continue logging events to a single Amplitude app. To make this transition as smooth as possible, we try to maintain backwards compatibility for most things by having a `default instance`, which you can fetch by calling `amplitude.getInstance()` with no instance name. The code examples in this README have been updated to follow this use case. All of the existing event data, existing settings, and returning users (users who already have a deviceId and/or userId) will stay with the `default instance`. You should initialize the default instance with your existing apiKey. + +All of the *public* methods of `amplitude` should still work as expected, as they have all been mapped to their equivalent on the default instance. + +For example `amplitude.init('API_KEY')` should still work as it has been mapped to `amplitude.getInstance().init('API_KEY')`. + +Likewise `amplitude.logEvent('EVENT_NAME')` should still work as it has been mapped to `amplitude.getInstance().logEvent('EVENT_NAME')`. + +`amplitude.options` will still work and will map to `amplitude.getInstance().options`, if for example you were using it to access the deviceId. + +**Things that will break:** if you were accessing private properties on the `amplitude` object, those will no longer work, e.g. `amplitude._sessionId`, `amplitude._eventId`, etc. You will need to update those references to fetch from the default instance like so: `amplitude.getInstance()._sessionId` and `amplitude.getInstance()._eventId`, etc. + +### Logging Events to a Single Amplitude App / API Key (Preferred Method) ### + +If you want to continue logging events to a single Amplitude App (and a single API key), then you should call functions on the `default instance`, which you can fetch by calling `amplitude.getInstance()` with no instance name. Here is an example: + +```javascript +amplitude.getInstance().init('API_KEY'); +amplitude.getInstance().logEvent('EVENT_NAME'); +``` + +You can also assign instances to a variable and call functions on that variable like so: + +```javascript +var app = amplitude.getInstance(); +app.init('API_KEY'); +app.logEvent('EVENT_NAME'); +``` + +### Logging Events to Multiple Amplitude Apps ### + +If you want to log events to multiple Amplitude apps, you will need to have separate instances for each Amplitude app. As mentioned earlier, each instance will allow for completely independent apiKeys, userIds, deviceIds, and settings. + +You need to assign a name to each Amplitude app / instance, and use that name consistently when fetching that instance to call functions. **IMPORTANT: Once you have chosen a name for that instance you cannot change it.** Every instance's data and settings are tied to its name, and you will need to continue using that instance name for all future versions of your app to maintain data continuity, so chose your instance names wisely. Note these names do not need to be the names of your apps in the Amplitude dashboards, but they need to remain consistent throughout your code. You also need to be sure that each instance is initialized with the correct apiKey. + +Instance names must be nonnull and nonempty strings. Names are case-insensitive. You can fetch each instance by name by calling `amplitude.getInstance('INSTANCE_NAME')`. + +As mentioned before, each new instance created will have its own apiKey, userId, deviceId, and settings. **You will have to reconfigure all the settings for each instance.** This gives you the freedom to have different settings for each instance. + +### Example of how to Set Up and Log Events to Two Separate Apps ### +```javascript +amplitude.getInstance().init('12345', null, {batchEvents: true}); // existing app, existing settings, and existing API key +amplitude.getInstance('new_app').init('67890', null, {includeReferrer: true}); // new app, new API key + +amplitude.getInstance('new_app').setUserId('joe@gmail.com'); // need to reconfigure new app +amplitude.getInstance('new_app').setUserProperties({'gender':'male'}); +amplitude.getInstance('new_app').logEvent('Clicked'); + +var identify = new amplitude.Identify().add('karma', 1); +amplitude.getInstance().identify(identify); +amplitude.getInstance().logEvent('Viewed Home Page'); +``` + +### Synchronizing Device Ids Between Apps ### + +As mentioned before, each instance will have its own deviceId. If you want your apps to share the same deviceId, you can do so *after init* via the `getDeviceId` and `setDeviceId` methods. Here's an example of how to copy the existing deviceId to the `new_app` instance: + +```javascript +var deviceId = amplitude.getInstance().getDeviceId(); // existing deviceId +amplitude.getInstance('new_app').setDeviceId(deviceId); // transferring existing deviceId to new_app +``` + # Tracking Events # It's important to think about what types of events you care about as a developer. You should aim to track between 20 and 200 types of events on your site. Common event types are actions the user initiates (such as pressing a button) and events you want the user to complete (such as filling out a form, completing a level, or making a payment). @@ -57,13 +131,13 @@ Anything past the above thresholds will not be visualized. **Note that the raw d If your app has its own login system that you want to track users with, you can call `setUserId` at any time: ```javascript -amplitude.setUserId('USER_ID_HERE'); +amplitude.getInstance().setUserId('USER_ID_HERE'); ``` You can also add the user ID as an argument to the `init` call: ```javascript -amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); +amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); ``` ### Logging Out and Anonymous Users ### @@ -71,8 +145,8 @@ amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE'); A user's data will be merged on the backend so that any events up to that point from the same browser will be tracked under the same user. Note: if a user logs out, or you want to log the events under an anonymous user, you need to do 2 things: 1) set the userId to `null` 2) regenerate a new deviceId. After doing that, events coming from the current user will appear as a brand new user in Amplitude dashboards. Note if you choose to do this, then you won't be able to see that the 2 users were using the same browser/device. ```javascript -amplitude.setUserId(null); // not string 'null' -amplitude.regenerateDeviceId(); +amplitude.getInstance().setUserId(null); // not string 'null' +amplitude.getInstance().regenerateDeviceId(); ``` # Setting Event Properties # @@ -82,7 +156,7 @@ You can attach additional data to any event by passing a Javascript object as th ```javascript var eventProperties = {}; eventProperties.key = 'value'; -amplitude.logEvent('EVENT_IDENTIFIER_HERE', eventProperties); +amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE', eventProperties); ``` Alternatively, you can set multiple event properties like this: @@ -92,7 +166,7 @@ var eventProperties = { 'age': 20, 'key': 'value' }; -amplitude.logEvent('EVENT_IDENTIFIER_HERE', eventProperties); +amplitude.getInstance().logEvent('EVENT_IDENTIFIER_HERE', eventProperties); ``` # User Properties and User Property Operations # @@ -103,45 +177,45 @@ The SDK supports the operations `set`, `setOnce`, `unset`, and `add` on individu ```javascript var identify = new amplitude.Identify().set('gender', 'female').set('age', 20); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 2. `setOnce`: this sets the value of a user property only once. Subsequent `setOnce` operations on that user property will be ignored. In the following example, `sign_up_date` will be set once to `08/24/2015`, and the following setOnce to `09/14/2015` will be ignored: ```javascript var identify = new amplitude.Identify().setOnce('sign_up_date', '08/24/2015'); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); var identify = new amplitude.Identify().setOnce('sign_up_date', '09/14/2015'); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 3. `unset`: this will unset and remove a user property. ```javascript var identify = new amplitude.Identify().unset('gender').unset('age'); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 4. `add`: this will increment a user property by some numerical value. If the user property does not have a value set yet, it will be initialized to 0 before being incremented. ```javascript var identify = new amplitude.Identify().add('karma', 1).add('friends', 1); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 5. `append`: this will append a value or values to a user property. If the user property does not have a value set yet, it will be initialized to an empty list before the new values are appended. If the user property has an existing value and it is not a list, it will be converted into a list with the new value appended. ```javascript var identify = new amplitude.Identify().append('ab-tests', 'new-user-test').append('some_list', [1, 2, 3, 4, 'values']); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` 6. `prepend`: this will prepend a value or values to a user property. Prepend means inserting the value(s) at the front of a given list. If the user property does not have a value set yet, it will be initialized to an empty list before the new values are prepended. If the user property has an existing value and it is not a list, it will be converted into a list with the new value prepended. ```javascript var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-test').prepend('some_list', [1, 2, 3, 4, 'values']); - amplitude.identify(identify); + amplitude.getInstance().identify(identify); ``` Note: if a user property is used in multiple operations on the same `Identify` object, only the first operation will be saved, and the rest will be ignored. In this example, only the set operation will be saved, and the add and unset will be ignored: @@ -151,7 +225,7 @@ var identify = new amplitude.Identify() .set('karma', 10) .add('karma', 1) .unset('karma'); -amplitude.identify(identify); +amplitude.getInstance().identify(identify); ``` ### Arrays in User Properties ### @@ -163,7 +237,7 @@ var identify = new amplitude.Identify() .set('colors', ['rose', 'gold']) .append('ab-tests', 'campaign_a') .append('existing_list', [4, 5]); -amplitude.identify(identify); +amplitude.getInstance().identify(identify); ``` ### Setting Multiple Properties with `setUserProperties` ### @@ -175,7 +249,7 @@ var userProperties = { gender: 'female', age: 20 }; -amplitude.setUserProperties(userProperties); +amplitude.getInstance().setUserProperties(userProperties); ``` ### Clearing User Properties ### @@ -183,7 +257,7 @@ amplitude.setUserProperties(userProperties); You may use `clearUserProperties` to clear all user properties at once. Note: the result is irreversible! ```javascript -amplitude.clearUserProperties(); +amplitude.getInstance().clearUserProperties(); ``` # Tracking Revenue # @@ -193,7 +267,7 @@ The preferred method of tracking revenue for a user now is to use `logRevenueV2( Each time a user generates revenue, you create a `Revenue` object and fill out the revenue properties: ```javascript var revenue = new amplitude.Revenue().setProductId('com.company.productId').setPrice(3.99).setQuantity(3); -amplitude.logRevenueV2(revenue); +amplitude.getInstance().logRevenueV2(revenue); ``` `productId` and `price` are required fields. `quantity` defaults to 1 if not specified. Each field has a corresponding `set` method (for example `setProductId`, `setQuantity`, etc). This table describes the different fields available: @@ -217,14 +291,14 @@ The existing `logRevenue` methods still work but are deprecated. Fields such as You can turn off logging for a given user: ```javascript -amplitude.setOptOut(true); +amplitude.getInstance().setOptOut(true); ``` No events will be saved or sent to the server while opt out is enabled. The opt out setting will persist across page loads. Calling ```javascript -amplitude.setOptOut(false); +amplitude.getInstance().setOptOut(false); ``` will reenable logging. @@ -234,7 +308,7 @@ will reenable logging. You can configure Amplitude by passing an object as the third argument to the `init`: ```javascript -amplitude.init('YOUR_API_KEY_HERE', null, { +amplitude.getInstance().init('YOUR_API_KEY_HERE', null, { // optional configuration options saveEvents: true, includeUtm: true, @@ -275,8 +349,8 @@ When setting groups you need to define a `groupType` and `groupName`(s). In the You can use `setGroup(groupType, groupName)` to designate which groups a user belongs to. Note: this will also set the `groupType`: `groupName` as a user property. **This will overwrite any existing groupName value set for that user's groupType, as well as the corresponding user property value.** `groupType` is a string, and `groupName` can be either a string or an array of strings to indicate a user being in multiple groups (for example Joe is in orgId 10 and 16, so the `groupName` would be [10, 16]). ```javascript -amplitude.setGroup('orgId', '15'); -amplitude.setGroup('sport', ['soccer', 'tennis']); +amplitude.getInstance().setGroup('orgId', '15'); +amplitude.getInstance().setGroup('sport', ['soccer', 'tennis']); ``` You can also use `logEventWithGroups` to set event-level groups, meaning the group designation only applies for the specific event being logged and does not persist on the user (unless you explicitly set it with `setGroup`). @@ -286,21 +360,21 @@ var eventProperties = { 'key': 'value' } -amplitude.logEventWithGroups('initialize_game', eventProperties, {'sport': 'soccer'}); +amplitude.getInstance().logEventWithGroups('initialize_game', eventProperties, {'sport': 'soccer'}); ``` ### Setting Version Name ### By default, no version name is set. You can specify a version name to distinguish between different versions of your site by calling `setVersionName`: ```javascript -amplitude.setVersionName('VERSION_NAME_HERE'); +amplitude.getInstance().setVersionName('VERSION_NAME_HERE'); ``` ### Custom Device Ids ### Device IDs are generated randomly, although you can define a custom device ID setting it as a configuration option or by calling: ```javascript -amplitude.setDeviceId('CUSTOM_DEVICE_ID'); +amplitude.getInstance().setDeviceId('CUSTOM_DEVICE_ID'); ``` **Note: this is not recommended unless you really know what you are doing** (like if you have your own system for tracking user devices). Make sure the deviceId you set is sufficiently unique (we recommend something like a UUID - see `src/uuid.js` for an example of how to generate) to prevent conflicts with other devices in our system. @@ -309,12 +383,12 @@ amplitude.setDeviceId('CUSTOM_DEVICE_ID'); You can pass a callback function to logEvent and identify, which will get called after receiving a response from the server: ```javascript -amplitude.logEvent("EVENT_IDENTIFIER_HERE", null, callback_function); +amplitude.getInstance().logEvent("EVENT_IDENTIFIER_HERE", null, callback_function); ``` ```javascript var identify = new amplitude.Identify().set('key', 'value'); -amplitude.identify(identify, callback_function); +amplitude.getInstance().identify(identify, callback_function); ``` The status and response body from the server are passed to the callback function, which you might find useful. An example of a callback function which redirects the browser to another site after a response: @@ -338,7 +412,7 @@ And then you would define a function that is called when the link is clicked lik ```javascript var trackClickLinkA = function() { - amplitude.logEvent('Clicked Link A', null, function() { + amplitude.getInstance().logEvent('Clicked Link A', null, function() { window.location='LINK_A_URL'; }); }; @@ -347,11 +421,11 @@ var trackClickLinkA = function() { In the case that `optOut` is true, then no event will be logged, but the callback will be called. In the case that `batchEvents` is true, if the batch requirements `eventUploadThreshold` and `eventUploadPeriodMillis` are not met when `logEvent` is called, then no request is sent, but the callback is still called. In these cases, the callback will be called with an input status of 0 and response 'No request sent'. ### Init Callbacks ### -You can also pass a callback function to init, which will get called after the SDK finishes its asynchronous loading. *Note: no values are passed to the init callback function*: +You can also pass a callback function to init, which will get called after the SDK finishes its asynchronous loading. *Note: the instance is passed as an argument to the callback*: ```javascript -amplitude.init('YOUR_API_KEY_HERE', 'USER_ID_HERE', null, function() { - console.log(amplitude.options.deviceId); // access Amplitude's deviceId after initialization +amplitude.getInstance().init('YOUR_API_KEY_HERE', 'USER_ID_HERE', null, function(instance) { + console.log(instance.options.deviceId); // access Amplitude's deviceId after initialization }); ``` @@ -361,10 +435,10 @@ If you are using [RequireJS](http://requirejs.org/) to load your Javascript file ```html ``` @@ -375,19 +449,19 @@ You can also define the path in your RequireJS configuration like so: ``` @@ -399,5 +473,5 @@ You can also define the path in your RequireJS configuration like so: 2. Call SDK functions in Google Tag Manager using [Custom HTML tags](https://support.google.com/tagmanager/answer/6107167?hl=en) and adding Javascript in this form as the custom tag: ```html - + ``` diff --git a/amplitude-segment-snippet.min.js b/amplitude-segment-snippet.min.js index 806fcd4b..4e63c623 100644 --- a/amplitude-segment-snippet.min.js +++ b/amplitude-segment-snippet.min.js @@ -1,8 +1,10 @@ -(function(e,t){var r=e.amplitude||{_q:[]};function n(e,t){e.prototype[t]=function(){ -this._q.push([t].concat(Array.prototype.slice.call(arguments,0)));return this}}var s=function(){ -this._q=[];return this};var i=["add","append","clearAll","prepend","set","setOnce","unset"]; -for(var o=0;o this.options.savedMaxCount) { queue.splice(0, queue.length - this.options.savedMaxCount); } @@ -950,9 +1330,9 @@ Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. * Note: the server response code and response body from the event upload are passed to the callback function. - * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); + * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ -Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { +AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || utils.isEmptyString(eventType)) { if (type(opt_callback) === 'function') { @@ -976,9 +1356,9 @@ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt * groupName can be a string or an array of strings. * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. * Note: the server response code and response body from the event upload are passed to the callback function. - * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); + * @example amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ -Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { +AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { if (!this._apiKeySet('logEventWithGroup()') || !utils.validateInput(eventType, 'eventType', 'string')) { if (type(opt_callback) === 'function') { @@ -1007,7 +1387,7 @@ var _isNumber = function _isNumber(n) { * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ -Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { +AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { if (!this._apiKeySet('logRevenueV2()')) { return; } @@ -1034,9 +1414,9 @@ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { * @param {number} price - price of revenue event * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1. * @param {string} product - (optional) product identifier - * @example amplitude.logRevenue(3.99, 1, 'product_1234'); + * @example amplitudeClient.logRevenue(3.99, 1, 'product_1234'); */ -Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { +AmplitudeClient.prototype.logRevenue = function logRevenue(price, quantity, product) { // Test that the parameters are of the right type. if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { // utils.log('Price and quantity arguments to logRevenue must be numbers'); @@ -1055,7 +1435,7 @@ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { * Remove events in storage with event ids up to and including maxEventId. * @private */ -Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { +AmplitudeClient.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { _removeEvents(this, '_unsentEvents', maxEventId); _removeEvents(this, '_unsentIdentifys', maxIdentifyId); }; @@ -1086,7 +1466,7 @@ var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { * @param {Amplitude~eventCallback} callback - (optional) callback to run after events are sent. * Note the server response code and response body are passed to the callback as input arguments. */ -Amplitude.prototype.sendEvents = function sendEvents(callback) { +AmplitudeClient.prototype.sendEvents = function sendEvents(callback) { if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) { if (type(callback) === 'function') { callback(0, 'No request sent'); @@ -1155,7 +1535,7 @@ Amplitude.prototype.sendEvents = function sendEvents(callback) { * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. * @private */ -Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { +AmplitudeClient.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { // coalesce events from both queues var eventsToSend = []; var eventIndex = 0; @@ -1214,7 +1594,7 @@ Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys * @public * @deprecated */ -Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { +AmplitudeClient.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { this.setUserProperties(userProperties); }; @@ -1224,13 +1604,14 @@ Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(u * @returns {number} version number * @example var amplitudeVersion = amplitude.__VERSION__; */ -Amplitude.prototype.__VERSION__ = version; +AmplitudeClient.prototype.__VERSION__ = version; -module.exports = Amplitude; +module.exports = AmplitudeClient; -}, {"./constants":3,"./cookiestorage":4,"./utm":5,"./identify":6,"json":7,"./localstorage":8,"JavaScript-MD5":9,"object":10,"./xhr":11,"./revenue":12,"./type":13,"ua-parser-js":14,"./utils":15,"./uuid":16,"./version":17,"./options":18}], -3: [function(require, module, exports) { +}, {"./constants":4,"./cookiestorage":12,"./utm":13,"./identify":5,"json":14,"./localstorage":15,"JavaScript-MD5":16,"object":6,"./xhr":17,"./revenue":7,"./type":8,"ua-parser-js":18,"./utils":9,"./uuid":19,"./version":10,"./options":11}], +4: [function(require, module, exports) { module.exports = { + DEFAULT_INSTANCE: '$default_instance', API_VERSION: 2, MAX_STRING_LENGTH: 4096, IDENTIFY_EVENT: '$identify', @@ -1260,7 +1641,7 @@ module.exports = { }; }, {}], -4: [function(require, module, exports) { +12: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -1354,8 +1735,8 @@ cookieStorage.prototype.getStorage = function() { module.exports = cookieStorage; -}, {"./constants":3,"./cookie":19,"json":7,"./localstorage":8}], -19: [function(require, module, exports) { +}, {"./constants":4,"./cookie":20,"json":14,"./localstorage":15}], +20: [function(require, module, exports) { /* * Cookie data */ @@ -1485,8 +1866,8 @@ module.exports = { }; -}, {"./base64":20,"json":7,"top-domain":21,"./utils":15}], -20: [function(require, module, exports) { +}, {"./base64":21,"json":14,"top-domain":22,"./utils":9}], +21: [function(require, module, exports) { /* jshint bitwise: false */ /* global escape, unescape */ @@ -1585,8 +1966,8 @@ var Base64 = { module.exports = Base64; -}, {"./utf8":22}], -22: [function(require, module, exports) { +}, {"./utf8":23}], +23: [function(require, module, exports) { /* jshint bitwise: false */ /* @@ -1646,7 +2027,7 @@ var UTF8 = { module.exports = UTF8; }, {}], -7: [function(require, module, exports) { +14: [function(require, module, exports) { var json = window.JSON || {}; var stringify = json.stringify; @@ -1656,8 +2037,8 @@ module.exports = parse && stringify ? JSON : require('json-fallback'); -}, {"json-fallback":23}], -23: [function(require, module, exports) { +}, {"json-fallback":24}], +24: [function(require, module, exports) { /* json2.js 2014-02-04 @@ -2147,7 +2528,7 @@ module.exports = parse && stringify }()); }, {}], -21: [function(require, module, exports) { +22: [function(require, module, exports) { /** * Module dependencies. @@ -2195,8 +2576,8 @@ function domain(url){ return match ? match[0] : ''; }; -}, {"url":24}], -24: [function(require, module, exports) { +}, {"url":25}], +25: [function(require, module, exports) { /** * Parse the given `url`. @@ -2281,7 +2662,7 @@ function port (protocol){ } }, {}], -15: [function(require, module, exports) { +9: [function(require, module, exports) { var constants = require('./constants'); var type = require('./type'); @@ -2476,8 +2857,8 @@ module.exports = { validateProperties: validateProperties }; -}, {"./constants":3,"./type":13}], -13: [function(require, module, exports) { +}, {"./constants":4,"./type":8}], +8: [function(require, module, exports) { /** * toString ref. * @private @@ -2524,7 +2905,7 @@ module.exports = function(val){ }; }, {}], -8: [function(require, module, exports) { +15: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* @@ -2628,7 +3009,7 @@ if (!localStorage) { module.exports = localStorage; }, {}], -5: [function(require, module, exports) { +13: [function(require, module, exports) { var utils = require('./utils'); var getUtmParam = function getUtmParam(name, query) { @@ -2671,8 +3052,8 @@ var getUtmData = function getUtmData(rawCookie, query) { module.exports = getUtmData; -}, {"./utils":15}], -6: [function(require, module, exports) { +}, {"./utils":9}], +5: [function(require, module, exports) { var type = require('./type'); var utils = require('./utils'); @@ -2858,8 +3239,8 @@ Identify.prototype._addOperation = function(operation, property, value) { module.exports = Identify; -}, {"./type":13,"./utils":15}], -9: [function(require, module, exports) { +}, {"./type":8,"./utils":9}], +16: [function(require, module, exports) { /* * JavaScript MD5 1.0.1 * https://github.com/blueimp/JavaScript-MD5 @@ -3147,7 +3528,7 @@ module.exports = Identify; }(this)); }, {}], -10: [function(require, module, exports) { +6: [function(require, module, exports) { /** * HOP ref. @@ -3233,7 +3614,7 @@ exports.isEmpty = function(obj){ return 0 == exports.length(obj); }; }, {}], -11: [function(require, module, exports) { +17: [function(require, module, exports) { var querystring = require('querystring'); /* @@ -3279,8 +3660,8 @@ Request.prototype.send = function(callback) { module.exports = Request; -}, {"querystring":25}], -25: [function(require, module, exports) { +}, {"querystring":26}], +26: [function(require, module, exports) { /** * Module dependencies. @@ -3355,8 +3736,8 @@ exports.stringify = function(obj){ return pairs.join('&'); }; -}, {"trim":26,"type":27}], -26: [function(require, module, exports) { +}, {"trim":27,"type":28}], +27: [function(require, module, exports) { exports = module.exports = trim; @@ -3376,7 +3757,7 @@ exports.right = function(str){ }; }, {}], -27: [function(require, module, exports) { +28: [function(require, module, exports) { /** * toString ref. */ @@ -3425,7 +3806,7 @@ function isBuffer(obj) { } }, {}], -12: [function(require, module, exports) { +7: [function(require, module, exports) { var constants = require('./constants'); var type = require('./type'); var utils = require('./utils'); @@ -3585,8 +3966,8 @@ Revenue.prototype._toJSONObject = function _toJSONObject() { module.exports = Revenue; -}, {"./constants":3,"./type":13,"./utils":15}], -14: [function(require, module, exports) { +}, {"./constants":4,"./type":8,"./utils":9}], +18: [function(require, module, exports) { /* jshint eqeqeq: false, forin: false */ /* global define */ @@ -4494,7 +4875,7 @@ module.exports = Revenue; })(typeof window === 'object' ? window : this); }, {}], -16: [function(require, module, exports) { +19: [function(require, module, exports) { /* jshint bitwise: false, laxbreak: true */ /** @@ -4528,11 +4909,11 @@ var uuid = function(a) { module.exports = uuid; }, {}], -17: [function(require, module, exports) { -module.exports = '2.13.0'; +10: [function(require, module, exports) { +module.exports = '3.0.0'; }, {}], -18: [function(require, module, exports) { +11: [function(require, module, exports) { var language = require('./language'); // default options @@ -4557,8 +4938,8 @@ module.exports = { eventUploadPeriodMillis: 30 * 1000, // 30s }; -}, {"./language":28}], -28: [function(require, module, exports) { +}, {"./language":29}], +29: [function(require, module, exports) { var getLanguage = function() { return (navigator && ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage)) || undefined; diff --git a/amplitude.min.js b/amplitude.min.js index d6bc9020..102804a8 100644 --- a/amplitude.min.js +++ b/amplitude.min.js @@ -1,3 +1,3 @@ -(function umd(require){if("object"==typeof exports){module.exports=require("1")}else if("function"==typeof define&&define.amd){define(function(){return require("1")})}else{this["amplitude"]=require("1")}})(function outer(modules,cache,entries){var global=function(){return this}();function require(name,jumped){if(cache[name])return cache[name].exports;if(modules[name])return call(name,require);throw new Error('cannot find module "'+name+'"')}function call(id,require){var m=cache[id]={exports:{}};var mod=modules[id];var name=mod[2];var fn=mod[0];fn.call(m.exports,function(req){var dep=modules[id][1][req];return require(dep?dep:req)},m,m.exports,outer,modules,cache,entries);if(name)cache[name]=cache[id];return cache[id].exports}for(var id in entries){if(entries[id]){global[entries[id]]=require(id)}else{require(id)}}require.duo=true;require.cache=cache;require.modules=modules;return require}({1:[function(require,module,exports){var Amplitude=require("./amplitude");var old=window.amplitude||{};var instance=new Amplitude;instance._q=old._q||[];module.exports=instance},{"./amplitude":2}],2:[function(require,module,exports){var Constants=require("./constants");var cookieStorage=require("./cookiestorage");var getUtmData=require("./utm");var Identify=require("./identify");var JSON=require("json");var localStorage=require("./localstorage");var md5=require("JavaScript-MD5");var object=require("object");var Request=require("./xhr");var Revenue=require("./revenue");var type=require("./type");var UAParser=require("ua-parser-js");var utils=require("./utils");var UUID=require("./uuid");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this._unsentEvents=[];this._unsentIdentifys=[];this._ua=new UAParser(navigator.userAgent).getResult();this.options=object.merge({},DEFAULT_OPTIONS);this.cookieStorage=(new cookieStorage).getStorage();this._q=[];this._sending=false;this._updateScheduled=false;this._eventId=0;this._identifyId=0;this._lastEventTime=null;this._newSession=false;this._sequenceNumber=0;this._sessionId=null};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){if(type(apiKey)!=="string"||utils.isEmptyString(apiKey)){utils.log("Invalid apiKey. Please re-initialize with a valid apiKey");return}try{this.options.apiKey=apiKey;_parseConfig(this.options,opt_config);this.cookieStorage.options({expirationDays:this.options.cookieExpiration,domain:this.options.domain});this.options.domain=this.cookieStorage.options().domain;_upgradeCookeData(this);_loadCookieData(this);this.options.deviceId=type(opt_config)==="object"&&type(opt_config.deviceId)==="string"&&!utils.isEmptyString(opt_config.deviceId)&&opt_config.deviceId||this.options.deviceId||UUID()+"R";this.options.userId=type(opt_userId)==="string"&&!utils.isEmptyString(opt_userId)&&opt_userId||this.options.userId||null;var now=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||now-this._lastEventTime>this.options.sessionTimeout){this._newSession=true;this._sessionId=now}this._lastEventTime=now;_saveCookieData(this);if(this.options.saveEvents){this._unsentEvents=this._loadSavedUnsentEvents(this.options.unsentKey);this._unsentIdentifys=this._loadSavedUnsentEvents(this.options.unsentIdentifyKey);for(var i=0;i0){options[key]=inputValue}};for(var key in config){if(config.hasOwnProperty(key)){parseValidateAndLoad(key)}}};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;i=this.options.eventUploadThreshold){this.sendEvents(callback);return true}if(!this._updateScheduled){this._updateScheduled=true;setTimeout(function(){this._updateScheduled=false;this.sendEvents()}.bind(this),this.options.eventUploadPeriodMillis)}return false};Amplitude.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key)};Amplitude.prototype._setInStorage=function _setInStorage(storage,key,value){storage.setItem(key,value)};var _upgradeCookeData=function _upgradeCookeData(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(type(cookieData)==="object"&&cookieData.deviceId&&cookieData.sessionId&&cookieData.lastEventTime){return}var _getAndRemoveFromLocalStorage=function _getAndRemoveFromLocalStorage(key){var value=localStorage.getItem(key);localStorage.removeItem(key);return value};var apiKeySuffix=type(scope.options.apiKey)==="string"&&"_"+scope.options.apiKey.slice(0,6)||"";var localStorageDeviceId=_getAndRemoveFromLocalStorage(Constants.DEVICE_ID+apiKeySuffix);var localStorageUserId=_getAndRemoveFromLocalStorage(Constants.USER_ID+apiKeySuffix);var localStorageOptOut=_getAndRemoveFromLocalStorage(Constants.OPT_OUT+apiKeySuffix);if(localStorageOptOut!==null&&localStorageOptOut!==undefined){localStorageOptOut=String(localStorageOptOut)==="true"}var localStorageSessionId=parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID));var localStorageLastEventTime=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME));var localStorageEventId=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID));var localStorageIdentifyId=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID));var localStorageSequenceNumber=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER));var _getFromCookie=function _getFromCookie(key){return type(cookieData)==="object"&&cookieData[key]};scope.options.deviceId=_getFromCookie("deviceId")||localStorageDeviceId;scope.options.userId=_getFromCookie("userId")||localStorageUserId;scope._sessionId=_getFromCookie("sessionId")||localStorageSessionId||scope._sessionId;scope._lastEventTime=_getFromCookie("lastEventTime")||localStorageLastEventTime||scope._lastEventTime;scope._eventId=_getFromCookie("eventId")||localStorageEventId||scope._eventId;scope._identifyId=_getFromCookie("identifyId")||localStorageIdentifyId||scope._identifyId;scope._sequenceNumber=_getFromCookie("sequenceNumber")||localStorageSequenceNumber||scope._sequenceNumber;scope.options.optOut=localStorageOptOut||false;if(cookieData&&cookieData.optOut!==undefined&&cookieData.optOut!==null){scope.options.optOut=String(cookieData.optOut)==="true"}_saveCookieData(scope)};var _loadCookieData=function _loadCookieData(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(type(cookieData)==="object"){if(cookieData.deviceId){scope.options.deviceId=cookieData.deviceId}if(cookieData.userId){scope.options.userId=cookieData.userId}if(cookieData.optOut!==null&&cookieData.optOut!==undefined){scope.options.optOut=cookieData.optOut}if(cookieData.sessionId){scope._sessionId=parseInt(cookieData.sessionId)}if(cookieData.lastEventTime){scope._lastEventTime=parseInt(cookieData.lastEventTime)}if(cookieData.eventId){scope._eventId=parseInt(cookieData.eventId)}if(cookieData.identifyId){scope._identifyId=parseInt(cookieData.identifyId)}if(cookieData.sequenceNumber){scope._sequenceNumber=parseInt(cookieData.sequenceNumber)}}};var _saveCookieData=function _saveCookieData(scope){scope.cookieStorage.set(scope.options.cookieName,{deviceId:scope.options.deviceId,userId:scope.options.userId,optOut:scope.options.optOut,sessionId:scope._sessionId,lastEventTime:scope._lastEventTime,eventId:scope._eventId,identifyId:scope._identifyId,sequenceNumber:scope._sequenceNumber})};Amplitude.prototype._initUtmData=function _initUtmData(queryParams,cookieParams){queryParams=queryParams||location.search;cookieParams=cookieParams||this.cookieStorage.get("__utmz");var utmProperties=getUtmData(cookieParams,queryParams);_sendUserPropertiesOncePerSession(this,Constants.UTM_PROPERTIES,utmProperties)};var _sendUserPropertiesOncePerSession=function _sendUserPropertiesOncePerSession(scope,storageKey,userProperties){if(type(userProperties)!=="object"||Object.keys(userProperties).length===0){return}var identify=new Identify;for(var key in userProperties){if(userProperties.hasOwnProperty(key)){identify.setOnce("initial_"+key,userProperties[key])}}var hasSessionStorage=utils.sessionStorageEnabled();if(hasSessionStorage&&!scope._getFromStorage(sessionStorage,storageKey)||!hasSessionStorage){for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}if(hasSessionStorage){scope._setInStorage(sessionStorage,storageKey,JSON.stringify(userProperties))}}scope.identify(identify)};Amplitude.prototype._getReferrer=function _getReferrer(){return document.referrer};Amplitude.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};Amplitude.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};Amplitude.prototype.saveEvents=function saveEvents(){try{this._setInStorage(localStorage,this.options.unsentKey,JSON.stringify(this._unsentEvents))}catch(e){}try{this._setInStorage(localStorage,this.options.unsentIdentifyKey,JSON.stringify(this._unsentIdentifys))}catch(e){}};Amplitude.prototype.setDomain=function setDomain(domain){if(!utils.validateInput(domain,"domain","string")){return}try{this.cookieStorage.options({domain:domain});this.options.domain=this.cookieStorage.options().domain;_loadCookieData(this);_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};Amplitude.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};Amplitude.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};Amplitude.prototype.setDeviceId=function setDeviceId(deviceId){if(!utils.validateInput(deviceId,"deviceId","string")){return}try{if(!utils.isEmptyString(deviceId)){this.options.deviceId=""+deviceId;_saveCookieData(this)}}catch(e){utils.log(e)}};Amplitude.prototype.setUserProperties=function setUserProperties(userProperties){if(!this._apiKeySet("setUserProperties()")||!utils.validateInput(userProperties,"userProperties","object")){return}var identify=new Identify;for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}this.identify(identify)};Amplitude.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,opt_callback)}}else{utils.log("Invalid identify input type. Expected Identify object but saw "+type(identify_obj))}if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}};Amplitude.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};Amplitude.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,callback){_loadCookieData(this);if(!eventType||this.options.optOut){if(type(callback)==="function"){callback(0,"No request sent")}return}try{var eventId;if(eventType===Constants.IDENTIFY_EVENT){eventId=this.nextIdentifyId()}else{eventId=this.nextEventId()}var sequenceNumber=this.nextSequenceNumber();var eventTime=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||eventTime-this._lastEventTime>this.options.sessionTimeout){this._sessionId=eventTime}this._lastEventTime=eventTime;_saveCookieData(this);userProperties=userProperties||{};apiProperties=apiProperties||{};eventProperties=eventProperties||{};groups=groups||{};var event={device_id:this.options.deviceId,user_id:this.options.userId,timestamp:eventTime,event_id:eventId,session_id:this._sessionId||-1,event_type:eventType,version_name:this.options.versionName||null,platform:this.options.platform,os_name:this._ua.browser.name||null,os_version:this._ua.browser.major||null,device_model:this._ua.os.name||null,language:this.options.language,api_properties:apiProperties,event_properties:utils.truncate(utils.validateProperties(eventProperties)),user_properties:utils.truncate(utils.validateProperties(userProperties)),uuid:UUID(),library:{name:"amplitude-js",version:version},sequence_number:sequenceNumber,groups:utils.truncate(utils.validateGroups(groups))};if(eventType===Constants.IDENTIFY_EVENT){this._unsentIdentifys.push(event);this._limitEventsQueued(this._unsentIdentifys)}else{this._unsentEvents.push(event);this._limitEventsQueued(this._unsentEvents)}if(this.options.saveEvents){this.saveEvents()}if(!this._sendEventsIfReady(callback)&&type(callback)==="function"){callback(0,"No request sent")}return eventId}catch(e){utils.log(e)}};Amplitude.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};Amplitude.prototype.logEvent=function logEvent(eventType,eventProperties,opt_callback){if(!this._apiKeySet("logEvent()")||!utils.validateInput(eventType,"eventType","string")||utils.isEmptyString(eventType)){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,null,opt_callback)};Amplitude.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};Amplitude.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};Amplitude.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};Amplitude.prototype.removeEvents=function removeEvents(maxEventId,maxIdentifyId){_removeEvents(this,"_unsentEvents",maxEventId);_removeEvents(this,"_unsentIdentifys",maxIdentifyId)};var _removeEvents=function _removeEvents(scope,eventQueue,maxId){if(maxId<0){return}var filteredEvents=[];for(var i=0;imaxId){filteredEvents.push(scope[eventQueue][i])}}scope[eventQueue]=filteredEvents};Amplitude.prototype.sendEvents=function sendEvents(callback){if(!this._apiKeySet("sendEvents()")||this._sending||this.options.optOut||this._unsentCount()===0){if(type(callback)==="function"){callback(0,"No request sent")}return}this._sending=true;var url=("https:"===window.location.protocol?"https":"http")+"://"+this.options.apiEndpoint+"/";var numEvents=Math.min(this._unsentCount(),this.options.uploadBatchSize);var mergedEvents=this._mergeEventsAndIdentifys(numEvents);var maxEventId=mergedEvents.maxEventId;var maxIdentifyId=mergedEvents.maxIdentifyId;var events=JSON.stringify(mergedEvents.eventsToSend);var uploadTime=(new Date).getTime();var data={client:this.options.apiKey,e:events,v:Constants.API_VERSION,upload_time:uploadTime,checksum:md5(Constants.API_VERSION+this.options.apiKey+events+uploadTime)};var scope=this;new Request(url,data).send(function(status,response){scope._sending=false;try{if(status===200&&response==="success"){scope.removeEvents(maxEventId,maxIdentifyId);if(scope.options.saveEvents){scope.saveEvents()}if(!scope._sendEventsIfReady(callback)&&type(callback)==="function"){callback(status,response)}}else if(status===413){if(scope.options.uploadBatchSize===1){scope.removeEvents(maxEventId,maxIdentifyId)}scope.options.uploadBatchSize=Math.ceil(numEvents/2);scope.sendEvents(callback)}else if(type(callback)==="function"){callback(status,response)}}catch(e){}})};Amplitude.prototype._mergeEventsAndIdentifys=function _mergeEventsAndIdentifys(numEvents){var eventsToSend=[];var eventIndex=0;var maxEventId=-1;var identifyIndex=0;var maxIdentifyId=-1;while(eventsToSend.length=this._unsentIdentifys.length;var noEvents=eventIndex>=this._unsentEvents.length;if(noEvents&&noIdentifys){utils.log("Merging Events and Identifys, less events and identifys than expected");break}else if(noIdentifys){event=this._unsentEvents[eventIndex++];maxEventId=event.event_id}else if(noEvents){event=this._unsentIdentifys[identifyIndex++];maxIdentifyId=event.event_id}else{if(!("sequence_number"in this._unsentEvents[eventIndex])||this._unsentEvents[eventIndex].sequence_number>2;enc2=(chr1&3)<<4|chr2>>4;enc3=(chr2&15)<<2|chr3>>6;enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else if(isNaN(chr3)){enc4=64}output=output+Base64._keyStr.charAt(enc1)+Base64._keyStr.charAt(enc2)+Base64._keyStr.charAt(enc3)+Base64._keyStr.charAt(enc4)}return output},decode:function(input){try{if(window.btoa&&window.atob){return decodeURIComponent(escape(window.atob(input)))}}catch(e){}return Base64._decode(input)},_decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(i>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}output=UTF8.decode(output);return output}};module.exports=Base64},{"./utf8":22}],22:[function(require,module,exports){var UTF8={encode:function(s){var utftext="";for(var n=0;n127&&c<2048){utftext+=String.fromCharCode(c>>6|192);utftext+=String.fromCharCode(c&63|128)}else{utftext+=String.fromCharCode(c>>12|224);utftext+=String.fromCharCode(c>>6&63|128);utftext+=String.fromCharCode(c&63|128)}}return utftext},decode:function(utftext){var s="";var i=0;var c=0,c1=0,c2=0;while(i191&&c<224){c1=utftext.charCodeAt(i+1);s+=String.fromCharCode((c&31)<<6|c1&63);i+=2}else{c1=utftext.charCodeAt(i+1);c2=utftext.charCodeAt(i+2);s+=String.fromCharCode((c&15)<<12|(c1&63)<<6|c2&63);i+=3}}return s}};module.exports=UTF8},{}],7:[function(require,module,exports){var json=window.JSON||{};var stringify=json.stringify;var parse=json.parse;module.exports=parse&&stringify?JSON:require("json-fallback")},{"json-fallback":23}],23:[function(require,module,exports){(function(){"use strict";var JSON=module.exports={};function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx,escapable,gap,indent,meta,rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;iconstants.MAX_STRING_LENGTH?value.substring(0,constants.MAX_STRING_LENGTH):value}return value};var validateInput=function validateInput(input,name,expectedType){if(type(input)!==expectedType){log("Invalid "+name+" input type. Expected "+expectedType+" but received "+type(input));return false}return true};var validateProperties=function validateProperties(properties){var propsType=type(properties);if(propsType!=="object"){log("Error: invalid event properties format. Expecting Javascript object, received "+propsType+", ignoring");return{}}var copy={};for(var property in properties){if(!properties.hasOwnProperty(property)){continue}var key=property;var keyType=type(key);if(keyType!=="string"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to string "'+key+'"')}var value=validatePropertyValue(key,properties[property]);if(value===null){continue}copy[key]=value}return copy};var invalidValueTypes=["null","nan","undefined","function","arguments","regexp","element"];var validatePropertyValue=function validatePropertyValue(key,value){var valueType=type(value);if(invalidValueTypes.indexOf(valueType)!==-1){log('WARNING: Property key "'+key+'" with invalid value type '+valueType+", ignoring");value=null}else if(valueType==="error"){value=String(value);log('WARNING: Property key "'+key+'" with value type error, coercing to '+value)}else if(valueType==="array"){var arrayCopy=[];for(var i=0;i0){if(!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll")}return this}this.userPropertiesOperations[AMP_OP_CLEAR_ALL]="-";return this};Identify.prototype.prepend=function(property,value){this._addOperation(AMP_OP_PREPEND,property,value);return this};Identify.prototype.set=function(property,value){this._addOperation(AMP_OP_SET,property,value);return this};Identify.prototype.setOnce=function(property,value){this._addOperation(AMP_OP_SET_ONCE,property,value);return this};Identify.prototype.unset=function(property){this._addOperation(AMP_OP_UNSET,property,"-");return this};Identify.prototype._addOperation=function(operation,property,value){if(this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("This identify already contains a $clearAll operation, skipping operation "+operation);return}if(this.properties.indexOf(property)!==-1){utils.log('User property "'+property+'" already used in this identify, skipping operation '+operation);return}if(!this.userPropertiesOperations.hasOwnProperty(operation)){this.userPropertiesOperations[operation]={}}this.userPropertiesOperations[operation][property]=value;this.properties.push(property)};module.exports=Identify},{"./type":13,"./utils":15}],9:[function(require,module,exports){(function($){"use strict";function safe_add(x,y){var lsw=(x&65535)+(y&65535),msw=(x>>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}function bit_rol(num,cnt){return num<>>32-cnt}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn(b&c|~b&d,a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn(b&d|c&~d,a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|~d),a,b,x,s,t)}function binl_md5(x,len){x[len>>5]|=128<>>9<<4)+14]=len;var i,olda,oldb,oldc,oldd,a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(i=0;i>5]>>>i%32&255)}return output}function rstr2binl(input){var i,output=[];output[(input.length>>2)-1]=undefined;for(i=0;i>5]|=(input.charCodeAt(i/8)&255)<16){bkey=binl_md5(bkey,key.length*8)}for(i=0;i<16;i+=1){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}hash=binl_md5(ipad.concat(rstr2binl(data)),512+data.length*8);return binl2rstr(binl_md5(opad.concat(hash),512+128))}function rstr2hex(input){var hex_tab="0123456789abcdef",output="",x,i;for(i=0;i>>4&15)+hex_tab.charAt(x&15)}return output}function str2rstr_utf8(input){return unescape(encodeURIComponent(input))}function raw_md5(s){return rstr_md5(str2rstr_utf8(s))}function hex_md5(s){return rstr2hex(raw_md5(s))}function raw_hmac_md5(k,d){return rstr_hmac_md5(str2rstr_utf8(k),str2rstr_utf8(d))}function hex_hmac_md5(k,d){return rstr2hex(raw_hmac_md5(k,d))}function md5(string,key,raw){if(!key){if(!raw){return hex_md5(string)}return raw_md5(string)}if(!raw){return hex_hmac_md5(key,string)}return raw_hmac_md5(key,string)}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=md5}exports.md5=md5}else{if(typeof define==="function"&&define.amd){define(function(){return md5})}else{$.md5=md5}}})(this)},{}],10:[function(require,module,exports){var has=Object.prototype.hasOwnProperty;exports.keys=Object.keys||function(obj){var keys=[];for(var key in obj){if(has.call(obj,key)){keys.push(key)}}return keys};exports.values=function(obj){var vals=[];for(var key in obj){if(has.call(obj,key)){vals.push(obj[key])}}return vals};exports.merge=function(a,b){for(var key in b){if(has.call(b,key)){a[key]=b[key]}}return a};exports.length=function(obj){return exports.keys(obj).length};exports.isEmpty=function(obj){return 0==exports.length(obj)}},{}],11:[function(require,module,exports){var querystring=require("querystring");var Request=function(url,data){this.url=url;this.data=data||{}};Request.prototype.send=function(callback){var isIE=window.XDomainRequest?true:false;if(isIE){var xdr=new window.XDomainRequest;xdr.open("POST",this.url,true);xdr.onload=function(){callback(200,xdr.responseText)};xdr.onerror=function(){if(xdr.responseText==="Request Entity Too Large"){callback(413,xdr.responseText)}else{callback(500,xdr.responseText)}};xdr.ontimeout=function(){};xdr.onprogress=function(){};xdr.send(querystring.stringify(this.data))}else{var xhr=new XMLHttpRequest;xhr.open("POST",this.url,true);xhr.onreadystatechange=function(){if(xhr.readyState===4){callback(xhr.status,xhr.responseText)}};xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");xhr.send(querystring.stringify(this.data))}};module.exports=Request},{querystring:25}],25:[function(require,module,exports){var encode=encodeURIComponent;var decode=decodeURIComponent;var trim=require("trim");var type=require("type");exports.parse=function(str){if("string"!=typeof str)return{};str=trim(str);if(""==str)return{};if("?"==str.charAt(0))str=str.slice(1);var obj={};var pairs=str.split("&");for(var i=0;i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)};module.exports=uuid},{}],17:[function(require,module,exports){module.exports="2.13.0"},{}],18:[function(require,module,exports){var language=require("./language");module.exports={apiEndpoint:"api.amplitude.com",cookieExpiration:365*10,cookieName:"amplitude_id",domain:"",includeReferrer:false,includeUtm:false,language:language.language,optOut:false,platform:"Web",savedMaxCount:1e3,saveEvents:true,sessionTimeout:30*60*1e3,unsentKey:"amplitude_unsent",unsentIdentifyKey:"amplitude_unsent_identify",uploadBatchSize:100,batchEvents:false,eventUploadThreshold:30,eventUploadPeriodMillis:30*1e3}},{"./language":28}],28:[function(require,module,exports){var getLanguage=function(){return navigator&&(navigator.languages&&navigator.languages[0]||navigator.language||navigator.userLanguage)||undefined};module.exports={language:getLanguage()}},{}]},{},{1:""})); \ No newline at end of file +(function umd(require){if("object"==typeof exports){module.exports=require("1")}else if("function"==typeof define&&define.amd){define(function(){return require("1")})}else{this["amplitude"]=require("1")}})(function outer(modules,cache,entries){var global=function(){return this}();function require(name,jumped){if(cache[name])return cache[name].exports;if(modules[name])return call(name,require);throw new Error('cannot find module "'+name+'"')}function call(id,require){var m=cache[id]={exports:{}};var mod=modules[id];var name=mod[2];var fn=mod[0];fn.call(m.exports,function(req){var dep=modules[id][1][req];return require(dep?dep:req)},m,m.exports,outer,modules,cache,entries);if(name)cache[name]=cache[id];return cache[id].exports}for(var id in entries){if(entries[id]){global[entries[id]]=require(id)}else{require(id)}}require.duo=true;require.cache=cache;require.modules=modules;return require}({1:[function(require,module,exports){var Amplitude=require("./amplitude");var old=window.amplitude||{};var newInstance=new Amplitude;newInstance._q=old._q||[];for(var instance in old._iq){if(old._iq.hasOwnProperty(instance)){newInstance.getInstance(instance)._q=old._iq[instance]._q||[]}}module.exports=newInstance},{"./amplitude":2}],2:[function(require,module,exports){var AmplitudeClient=require("./amplitude-client");var Constants=require("./constants");var Identify=require("./identify");var object=require("object");var Revenue=require("./revenue");var type=require("./type");var utils=require("./utils");var version=require("./version");var DEFAULT_OPTIONS=require("./options");var Amplitude=function Amplitude(){this.options=object.merge({},DEFAULT_OPTIONS);this._q=[];this._instances={}};Amplitude.prototype.Identify=Identify;Amplitude.prototype.Revenue=Revenue;Amplitude.prototype.getInstance=function getInstance(instance){instance=utils.isEmptyString(instance)?Constants.DEFAULT_INSTANCE:instance.toLowerCase();var client=this._instances[instance];if(client===undefined){client=new AmplitudeClient(instance);this._instances[instance]=client}return client};Amplitude.prototype.init=function init(apiKey,opt_userId,opt_config,opt_callback){this.getInstance().init(apiKey,opt_userId,opt_config,function(instance){this.options=instance.options;if(type(opt_callback)==="function"){opt_callback(instance)}}.bind(this))};Amplitude.prototype.runQueuedFunctions=function(){for(var i=0;ithis.options.sessionTimeout){this._newSession=true;this._sessionId=now}this._lastEventTime=now;_saveCookieData(this);if(this.options.saveEvents){this._unsentEvents=this._loadSavedUnsentEvents(this.options.unsentKey);this._unsentIdentifys=this._loadSavedUnsentEvents(this.options.unsentIdentifyKey);for(var i=0;i0){options[key]=inputValue}};for(var key in config){if(config.hasOwnProperty(key)){parseValidateAndLoad(key)}}};AmplitudeClient.prototype.runQueuedFunctions=function(){for(var i=0;i=this.options.eventUploadThreshold){this.sendEvents(callback);return true}if(!this._updateScheduled){this._updateScheduled=true;setTimeout(function(){this._updateScheduled=false;this.sendEvents()}.bind(this),this.options.eventUploadPeriodMillis)}return false};AmplitudeClient.prototype._getFromStorage=function _getFromStorage(storage,key){return storage.getItem(key+this._storageSuffix)};AmplitudeClient.prototype._setInStorage=function _setInStorage(storage,key,value){storage.setItem(key+this._storageSuffix,value)};var _upgradeCookeData=function _upgradeCookeData(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName);if(type(cookieData)==="object"&&cookieData.deviceId&&cookieData.sessionId&&cookieData.lastEventTime){return}var _getAndRemoveFromLocalStorage=function _getAndRemoveFromLocalStorage(key){var value=localStorage.getItem(key);localStorage.removeItem(key);return value};var apiKeySuffix=type(scope.options.apiKey)==="string"&&"_"+scope.options.apiKey.slice(0,6)||"";var localStorageDeviceId=_getAndRemoveFromLocalStorage(Constants.DEVICE_ID+apiKeySuffix);var localStorageUserId=_getAndRemoveFromLocalStorage(Constants.USER_ID+apiKeySuffix);var localStorageOptOut=_getAndRemoveFromLocalStorage(Constants.OPT_OUT+apiKeySuffix);if(localStorageOptOut!==null&&localStorageOptOut!==undefined){localStorageOptOut=String(localStorageOptOut)==="true"}var localStorageSessionId=parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID));var localStorageLastEventTime=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME));var localStorageEventId=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID));var localStorageIdentifyId=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID));var localStorageSequenceNumber=parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER));var _getFromCookie=function _getFromCookie(key){return type(cookieData)==="object"&&cookieData[key]};scope.options.deviceId=_getFromCookie("deviceId")||localStorageDeviceId;scope.options.userId=_getFromCookie("userId")||localStorageUserId;scope._sessionId=_getFromCookie("sessionId")||localStorageSessionId||scope._sessionId;scope._lastEventTime=_getFromCookie("lastEventTime")||localStorageLastEventTime||scope._lastEventTime;scope._eventId=_getFromCookie("eventId")||localStorageEventId||scope._eventId;scope._identifyId=_getFromCookie("identifyId")||localStorageIdentifyId||scope._identifyId;scope._sequenceNumber=_getFromCookie("sequenceNumber")||localStorageSequenceNumber||scope._sequenceNumber;scope.options.optOut=localStorageOptOut||false;if(cookieData&&cookieData.optOut!==undefined&&cookieData.optOut!==null){scope.options.optOut=String(cookieData.optOut)==="true"}_saveCookieData(scope)};var _loadCookieData=function _loadCookieData(scope){var cookieData=scope.cookieStorage.get(scope.options.cookieName+scope._storageSuffix);if(type(cookieData)==="object"){if(cookieData.deviceId){scope.options.deviceId=cookieData.deviceId}if(cookieData.userId){scope.options.userId=cookieData.userId}if(cookieData.optOut!==null&&cookieData.optOut!==undefined){scope.options.optOut=cookieData.optOut}if(cookieData.sessionId){scope._sessionId=parseInt(cookieData.sessionId)}if(cookieData.lastEventTime){scope._lastEventTime=parseInt(cookieData.lastEventTime)}if(cookieData.eventId){scope._eventId=parseInt(cookieData.eventId)}if(cookieData.identifyId){scope._identifyId=parseInt(cookieData.identifyId)}if(cookieData.sequenceNumber){scope._sequenceNumber=parseInt(cookieData.sequenceNumber)}}};var _saveCookieData=function _saveCookieData(scope){scope.cookieStorage.set(scope.options.cookieName+scope._storageSuffix,{deviceId:scope.options.deviceId,userId:scope.options.userId,optOut:scope.options.optOut,sessionId:scope._sessionId,lastEventTime:scope._lastEventTime,eventId:scope._eventId,identifyId:scope._identifyId,sequenceNumber:scope._sequenceNumber})};AmplitudeClient.prototype._initUtmData=function _initUtmData(queryParams,cookieParams){queryParams=queryParams||location.search;cookieParams=cookieParams||this.cookieStorage.get("__utmz");var utmProperties=getUtmData(cookieParams,queryParams);_sendUserPropertiesOncePerSession(this,Constants.UTM_PROPERTIES,utmProperties)};var _sendUserPropertiesOncePerSession=function _sendUserPropertiesOncePerSession(scope,storageKey,userProperties){if(type(userProperties)!=="object"||Object.keys(userProperties).length===0){return}var identify=new Identify;for(var key in userProperties){if(userProperties.hasOwnProperty(key)){identify.setOnce("initial_"+key,userProperties[key])}}var hasSessionStorage=utils.sessionStorageEnabled();if(hasSessionStorage&&!scope._getFromStorage(sessionStorage,storageKey)||!hasSessionStorage){for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}if(hasSessionStorage){scope._setInStorage(sessionStorage,storageKey,JSON.stringify(userProperties))}}scope.identify(identify)};AmplitudeClient.prototype._getReferrer=function _getReferrer(){return document.referrer};AmplitudeClient.prototype._getReferringDomain=function _getReferringDomain(referrer){if(utils.isEmptyString(referrer)){return null}var parts=referrer.split("/");if(parts.length>=3){return parts[2]}return null};AmplitudeClient.prototype._saveReferrer=function _saveReferrer(referrer){if(utils.isEmptyString(referrer)){return}var referrerInfo={referrer:referrer,referring_domain:this._getReferringDomain(referrer)};_sendUserPropertiesOncePerSession(this,Constants.REFERRER,referrerInfo)};AmplitudeClient.prototype.saveEvents=function saveEvents(){try{this._setInStorage(localStorage,this.options.unsentKey,JSON.stringify(this._unsentEvents))}catch(e){}try{this._setInStorage(localStorage,this.options.unsentIdentifyKey,JSON.stringify(this._unsentIdentifys))}catch(e){}};AmplitudeClient.prototype.setDomain=function setDomain(domain){if(!utils.validateInput(domain,"domain","string")){return}try{this.cookieStorage.options({domain:domain});this.options.domain=this.cookieStorage.options().domain;_loadCookieData(this);_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setUserId=function setUserId(userId){try{this.options.userId=userId!==undefined&&userId!==null&&""+userId||null;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.setGroup=function(groupType,groupName){if(!this._apiKeySet("setGroup()")||!utils.validateInput(groupType,"groupType","string")||utils.isEmptyString(groupType)){return}var groups={};groups[groupType]=groupName;var identify=(new Identify).set(groupType,groupName);this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify.userPropertiesOperations,groups,null)};AmplitudeClient.prototype.setOptOut=function setOptOut(enable){if(!utils.validateInput(enable,"enable","boolean")){return}try{this.options.optOut=enable;_saveCookieData(this)}catch(e){utils.log(e)}};AmplitudeClient.prototype.regenerateDeviceId=function regenerateDeviceId(){this.setDeviceId(UUID()+"R")};AmplitudeClient.prototype.setDeviceId=function setDeviceId(deviceId){if(!utils.validateInput(deviceId,"deviceId","string")){return}try{if(!utils.isEmptyString(deviceId)){this.options.deviceId=""+deviceId;_saveCookieData(this)}}catch(e){utils.log(e)}};AmplitudeClient.prototype.setUserProperties=function setUserProperties(userProperties){if(!this._apiKeySet("setUserProperties()")||!utils.validateInput(userProperties,"userProperties","object")){return}var identify=new Identify;for(var property in userProperties){if(userProperties.hasOwnProperty(property)){identify.set(property,userProperties[property])}}this.identify(identify)};AmplitudeClient.prototype.clearUserProperties=function clearUserProperties(){if(!this._apiKeySet("clearUserProperties()")){return}var identify=new Identify;identify.clearAll();this.identify(identify)};var _convertProxyObjectToRealObject=function _convertProxyObjectToRealObject(instance,proxy){for(var i=0;i0){return this._logEvent(Constants.IDENTIFY_EVENT,null,null,identify_obj.userPropertiesOperations,null,opt_callback)}}else{utils.log("Invalid identify input type. Expected Identify object but saw "+type(identify_obj))}if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}};AmplitudeClient.prototype.setVersionName=function setVersionName(versionName){if(!utils.validateInput(versionName,"versionName","string")){return}this.options.versionName=versionName};AmplitudeClient.prototype._logEvent=function _logEvent(eventType,eventProperties,apiProperties,userProperties,groups,callback){_loadCookieData(this);if(!eventType||this.options.optOut){if(type(callback)==="function"){callback(0,"No request sent")}return}try{var eventId;if(eventType===Constants.IDENTIFY_EVENT){eventId=this.nextIdentifyId()}else{eventId=this.nextEventId()}var sequenceNumber=this.nextSequenceNumber();var eventTime=(new Date).getTime();if(!this._sessionId||!this._lastEventTime||eventTime-this._lastEventTime>this.options.sessionTimeout){this._sessionId=eventTime}this._lastEventTime=eventTime;_saveCookieData(this);userProperties=userProperties||{};apiProperties=apiProperties||{};eventProperties=eventProperties||{};groups=groups||{};var event={device_id:this.options.deviceId,user_id:this.options.userId,timestamp:eventTime,event_id:eventId,session_id:this._sessionId||-1,event_type:eventType,version_name:this.options.versionName||null,platform:this.options.platform,os_name:this._ua.browser.name||null,os_version:this._ua.browser.major||null,device_model:this._ua.os.name||null,language:this.options.language,api_properties:apiProperties,event_properties:utils.truncate(utils.validateProperties(eventProperties)),user_properties:utils.truncate(utils.validateProperties(userProperties)),uuid:UUID(),library:{name:"amplitude-js",version:version},sequence_number:sequenceNumber,groups:utils.truncate(utils.validateGroups(groups))};if(eventType===Constants.IDENTIFY_EVENT){this._unsentIdentifys.push(event);this._limitEventsQueued(this._unsentIdentifys)}else{this._unsentEvents.push(event);this._limitEventsQueued(this._unsentEvents)}if(this.options.saveEvents){this.saveEvents()}if(!this._sendEventsIfReady(callback)&&type(callback)==="function"){callback(0,"No request sent")}return eventId}catch(e){utils.log(e)}};AmplitudeClient.prototype._limitEventsQueued=function _limitEventsQueued(queue){if(queue.length>this.options.savedMaxCount){queue.splice(0,queue.length-this.options.savedMaxCount)}};AmplitudeClient.prototype.logEvent=function logEvent(eventType,eventProperties,opt_callback){if(!this._apiKeySet("logEvent()")||!utils.validateInput(eventType,"eventType","string")||utils.isEmptyString(eventType)){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,null,opt_callback)};AmplitudeClient.prototype.logEventWithGroups=function(eventType,eventProperties,groups,opt_callback){if(!this._apiKeySet("logEventWithGroup()")||!utils.validateInput(eventType,"eventType","string")){if(type(opt_callback)==="function"){opt_callback(0,"No request sent")}return-1}return this._logEvent(eventType,eventProperties,null,null,groups,opt_callback)};var _isNumber=function _isNumber(n){return!isNaN(parseFloat(n))&&isFinite(n)};AmplitudeClient.prototype.logRevenueV2=function logRevenueV2(revenue_obj){if(!this._apiKeySet("logRevenueV2()")){return}if(type(revenue_obj)==="object"&&revenue_obj.hasOwnProperty("_q")){revenue_obj=_convertProxyObjectToRealObject(new Revenue,revenue_obj)}if(revenue_obj instanceof Revenue){if(revenue_obj&&revenue_obj._isValidRevenue()){return this.logEvent(Constants.REVENUE_EVENT,revenue_obj._toJSONObject())}}else{utils.log("Invalid revenue input type. Expected Revenue object but saw "+type(revenue_obj))}};AmplitudeClient.prototype.logRevenue=function logRevenue(price,quantity,product){if(!this._apiKeySet("logRevenue()")||!_isNumber(price)||quantity!==undefined&&!_isNumber(quantity)){return-1}return this._logEvent(Constants.REVENUE_EVENT,{},{productId:product,special:"revenue_amount",quantity:quantity||1,price:price},null,null,null)};AmplitudeClient.prototype.removeEvents=function removeEvents(maxEventId,maxIdentifyId){_removeEvents(this,"_unsentEvents",maxEventId);_removeEvents(this,"_unsentIdentifys",maxIdentifyId)};var _removeEvents=function _removeEvents(scope,eventQueue,maxId){if(maxId<0){return}var filteredEvents=[];for(var i=0;imaxId){filteredEvents.push(scope[eventQueue][i])}}scope[eventQueue]=filteredEvents};AmplitudeClient.prototype.sendEvents=function sendEvents(callback){if(!this._apiKeySet("sendEvents()")||this._sending||this.options.optOut||this._unsentCount()===0){if(type(callback)==="function"){callback(0,"No request sent")}return}this._sending=true;var url=("https:"===window.location.protocol?"https":"http")+"://"+this.options.apiEndpoint+"/";var numEvents=Math.min(this._unsentCount(),this.options.uploadBatchSize);var mergedEvents=this._mergeEventsAndIdentifys(numEvents);var maxEventId=mergedEvents.maxEventId;var maxIdentifyId=mergedEvents.maxIdentifyId;var events=JSON.stringify(mergedEvents.eventsToSend);var uploadTime=(new Date).getTime();var data={client:this.options.apiKey,e:events,v:Constants.API_VERSION,upload_time:uploadTime,checksum:md5(Constants.API_VERSION+this.options.apiKey+events+uploadTime)};var scope=this;new Request(url,data).send(function(status,response){scope._sending=false;try{if(status===200&&response==="success"){scope.removeEvents(maxEventId,maxIdentifyId);if(scope.options.saveEvents){scope.saveEvents()}if(!scope._sendEventsIfReady(callback)&&type(callback)==="function"){callback(status,response)}}else if(status===413){if(scope.options.uploadBatchSize===1){scope.removeEvents(maxEventId,maxIdentifyId)}scope.options.uploadBatchSize=Math.ceil(numEvents/2);scope.sendEvents(callback)}else if(type(callback)==="function"){callback(status,response)}}catch(e){}})};AmplitudeClient.prototype._mergeEventsAndIdentifys=function _mergeEventsAndIdentifys(numEvents){var eventsToSend=[];var eventIndex=0;var maxEventId=-1;var identifyIndex=0;var maxIdentifyId=-1;while(eventsToSend.length=this._unsentIdentifys.length;var noEvents=eventIndex>=this._unsentEvents.length;if(noEvents&&noIdentifys){utils.log("Merging Events and Identifys, less events and identifys than expected");break}else if(noIdentifys){event=this._unsentEvents[eventIndex++];maxEventId=event.event_id}else if(noEvents){event=this._unsentIdentifys[identifyIndex++];maxIdentifyId=event.event_id}else{if(!("sequence_number"in this._unsentEvents[eventIndex])||this._unsentEvents[eventIndex].sequence_number>2;enc2=(chr1&3)<<4|chr2>>4;enc3=(chr2&15)<<2|chr3>>6; +enc4=chr3&63;if(isNaN(chr2)){enc3=enc4=64}else if(isNaN(chr3)){enc4=64}output=output+Base64._keyStr.charAt(enc1)+Base64._keyStr.charAt(enc2)+Base64._keyStr.charAt(enc3)+Base64._keyStr.charAt(enc4)}return output},decode:function(input){try{if(window.btoa&&window.atob){return decodeURIComponent(escape(window.atob(input)))}}catch(e){}return Base64._decode(input)},_decode:function(input){var output="";var chr1,chr2,chr3;var enc1,enc2,enc3,enc4;var i=0;input=input.replace(/[^A-Za-z0-9\+\/\=]/g,"");while(i>4;chr2=(enc2&15)<<4|enc3>>2;chr3=(enc3&3)<<6|enc4;output=output+String.fromCharCode(chr1);if(enc3!==64){output=output+String.fromCharCode(chr2)}if(enc4!==64){output=output+String.fromCharCode(chr3)}}output=UTF8.decode(output);return output}};module.exports=Base64},{"./utf8":23}],23:[function(require,module,exports){var UTF8={encode:function(s){var utftext="";for(var n=0;n127&&c<2048){utftext+=String.fromCharCode(c>>6|192);utftext+=String.fromCharCode(c&63|128)}else{utftext+=String.fromCharCode(c>>12|224);utftext+=String.fromCharCode(c>>6&63|128);utftext+=String.fromCharCode(c&63|128)}}return utftext},decode:function(utftext){var s="";var i=0;var c=0,c1=0,c2=0;while(i191&&c<224){c1=utftext.charCodeAt(i+1);s+=String.fromCharCode((c&31)<<6|c1&63);i+=2}else{c1=utftext.charCodeAt(i+1);c2=utftext.charCodeAt(i+2);s+=String.fromCharCode((c&15)<<12|(c1&63)<<6|c2&63);i+=3}}return s}};module.exports=UTF8},{}],14:[function(require,module,exports){var json=window.JSON||{};var stringify=json.stringify;var parse=json.parse;module.exports=parse&&stringify?JSON:require("json-fallback")},{"json-fallback":24}],24:[function(require,module,exports){(function(){"use strict";var JSON=module.exports={};function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx,escapable,gap,indent,meta,rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;iconstants.MAX_STRING_LENGTH?value.substring(0,constants.MAX_STRING_LENGTH):value}return value};var validateInput=function validateInput(input,name,expectedType){if(type(input)!==expectedType){log("Invalid "+name+" input type. Expected "+expectedType+" but received "+type(input));return false}return true};var validateProperties=function validateProperties(properties){var propsType=type(properties);if(propsType!=="object"){log("Error: invalid event properties format. Expecting Javascript object, received "+propsType+", ignoring");return{}}var copy={};for(var property in properties){if(!properties.hasOwnProperty(property)){continue}var key=property;var keyType=type(key);if(keyType!=="string"){key=String(key);log("WARNING: Non-string property key, received type "+keyType+', coercing to string "'+key+'"')}var value=validatePropertyValue(key,properties[property]);if(value===null){continue}copy[key]=value}return copy};var invalidValueTypes=["null","nan","undefined","function","arguments","regexp","element"];var validatePropertyValue=function validatePropertyValue(key,value){var valueType=type(value);if(invalidValueTypes.indexOf(valueType)!==-1){log('WARNING: Property key "'+key+'" with invalid value type '+valueType+", ignoring");value=null}else if(valueType==="error"){value=String(value);log('WARNING: Property key "'+key+'" with value type error, coercing to '+value)}else if(valueType==="array"){var arrayCopy=[];for(var i=0;i0){if(!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll")}return this}this.userPropertiesOperations[AMP_OP_CLEAR_ALL]="-";return this};Identify.prototype.prepend=function(property,value){this._addOperation(AMP_OP_PREPEND,property,value);return this};Identify.prototype.set=function(property,value){this._addOperation(AMP_OP_SET,property,value);return this};Identify.prototype.setOnce=function(property,value){this._addOperation(AMP_OP_SET_ONCE,property,value);return this};Identify.prototype.unset=function(property){this._addOperation(AMP_OP_UNSET,property,"-");return this};Identify.prototype._addOperation=function(operation,property,value){if(this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)){utils.log("This identify already contains a $clearAll operation, skipping operation "+operation);return}if(this.properties.indexOf(property)!==-1){utils.log('User property "'+property+'" already used in this identify, skipping operation '+operation);return}if(!this.userPropertiesOperations.hasOwnProperty(operation)){this.userPropertiesOperations[operation]={}}this.userPropertiesOperations[operation][property]=value;this.properties.push(property)};module.exports=Identify},{"./type":8,"./utils":9}],16:[function(require,module,exports){(function($){"use strict";function safe_add(x,y){var lsw=(x&65535)+(y&65535),msw=(x>>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}function bit_rol(num,cnt){return num<>>32-cnt}function md5_cmn(q,a,b,x,s,t){return safe_add(bit_rol(safe_add(safe_add(a,q),safe_add(x,t)),s),b)}function md5_ff(a,b,c,d,x,s,t){return md5_cmn(b&c|~b&d,a,b,x,s,t)}function md5_gg(a,b,c,d,x,s,t){return md5_cmn(b&d|c&~d,a,b,x,s,t)}function md5_hh(a,b,c,d,x,s,t){return md5_cmn(b^c^d,a,b,x,s,t)}function md5_ii(a,b,c,d,x,s,t){return md5_cmn(c^(b|~d),a,b,x,s,t)}function binl_md5(x,len){x[len>>5]|=128<>>9<<4)+14]=len;var i,olda,oldb,oldc,oldd,a=1732584193,b=-271733879,c=-1732584194,d=271733878;for(i=0;i>5]>>>i%32&255)}return output}function rstr2binl(input){var i,output=[];output[(input.length>>2)-1]=undefined;for(i=0;i>5]|=(input.charCodeAt(i/8)&255)<16){bkey=binl_md5(bkey,key.length*8)}for(i=0;i<16;i+=1){ipad[i]=bkey[i]^909522486;opad[i]=bkey[i]^1549556828}hash=binl_md5(ipad.concat(rstr2binl(data)),512+data.length*8);return binl2rstr(binl_md5(opad.concat(hash),512+128))}function rstr2hex(input){var hex_tab="0123456789abcdef",output="",x,i;for(i=0;i>>4&15)+hex_tab.charAt(x&15)}return output}function str2rstr_utf8(input){return unescape(encodeURIComponent(input))}function raw_md5(s){return rstr_md5(str2rstr_utf8(s))}function hex_md5(s){return rstr2hex(raw_md5(s))}function raw_hmac_md5(k,d){return rstr_hmac_md5(str2rstr_utf8(k),str2rstr_utf8(d))}function hex_hmac_md5(k,d){return rstr2hex(raw_hmac_md5(k,d))}function md5(string,key,raw){if(!key){if(!raw){return hex_md5(string)}return raw_md5(string)}if(!raw){return hex_hmac_md5(key,string)}return raw_hmac_md5(key,string)}if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports){exports=module.exports=md5}exports.md5=md5}else{if(typeof define==="function"&&define.amd){define(function(){return md5})}else{$.md5=md5}}})(this)},{}],6:[function(require,module,exports){var has=Object.prototype.hasOwnProperty;exports.keys=Object.keys||function(obj){var keys=[];for(var key in obj){if(has.call(obj,key)){keys.push(key)}}return keys};exports.values=function(obj){var vals=[];for(var key in obj){if(has.call(obj,key)){vals.push(obj[key])}}return vals};exports.merge=function(a,b){for(var key in b){if(has.call(b,key)){a[key]=b[key]}}return a};exports.length=function(obj){return exports.keys(obj).length};exports.isEmpty=function(obj){return 0==exports.length(obj)}},{}],17:[function(require,module,exports){var querystring=require("querystring");var Request=function(url,data){this.url=url;this.data=data||{}};Request.prototype.send=function(callback){var isIE=window.XDomainRequest?true:false;if(isIE){var xdr=new window.XDomainRequest;xdr.open("POST",this.url,true);xdr.onload=function(){callback(200,xdr.responseText)};xdr.onerror=function(){if(xdr.responseText==="Request Entity Too Large"){callback(413,xdr.responseText)}else{callback(500,xdr.responseText)}};xdr.ontimeout=function(){};xdr.onprogress=function(){};xdr.send(querystring.stringify(this.data))}else{var xhr=new XMLHttpRequest;xhr.open("POST",this.url,true);xhr.onreadystatechange=function(){if(xhr.readyState===4){callback(xhr.status,xhr.responseText)}};xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");xhr.send(querystring.stringify(this.data))}};module.exports=Request},{querystring:26}],26:[function(require,module,exports){var encode=encodeURIComponent;var decode=decodeURIComponent;var trim=require("trim");var type=require("type");exports.parse=function(str){if("string"!=typeof str)return{};str=trim(str);if(""==str)return{};if("?"==str.charAt(0))str=str.slice(1);var obj={};var pairs=str.split("&");for(var i=0;i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid)};module.exports=uuid},{}],10:[function(require,module,exports){module.exports="3.0.0"},{}],11:[function(require,module,exports){var language=require("./language");module.exports={apiEndpoint:"api.amplitude.com",cookieExpiration:365*10,cookieName:"amplitude_id",domain:"",includeReferrer:false,includeUtm:false,language:language.language,optOut:false,platform:"Web",savedMaxCount:1e3,saveEvents:true,sessionTimeout:30*60*1e3,unsentKey:"amplitude_unsent",unsentIdentifyKey:"amplitude_unsent_identify",uploadBatchSize:100,batchEvents:false,eventUploadThreshold:30,eventUploadPeriodMillis:30*1e3}},{"./language":29}],29:[function(require,module,exports){var getLanguage=function(){return navigator&&(navigator.languages&&navigator.languages[0]||navigator.language||navigator.userLanguage)||undefined};module.exports={language:getLanguage()}},{}]},{},{1:""})); \ No newline at end of file diff --git a/component.json b/component.json index da0ddd3a..49700e5b 100644 --- a/component.json +++ b/component.json @@ -3,7 +3,7 @@ "main": "src/index.js", "repo": "amplitude/amplitude-javascript", "description": "Javascript library for Amplitude Analytics", - "version": "2.13.0", + "version": "3.0.0", "keywords": [ "analytics", "amplitude" diff --git a/documentation/Amplitude.html b/documentation/Amplitude.html index eed65d1b..8f87c9aa 100644 --- a/documentation/Amplitude.html +++ b/documentation/Amplitude.html @@ -47,7 +47,9 @@

new Amplitud
- Amplitude SDK API - instance constructor. + Amplitude SDK API - instance manager. +Function calls directly on amplitude have been deprecated. Please call methods on the default shared instance: amplitude.getInstance() instead. +See Readme for more information about this change.
@@ -91,7 +93,7 @@

new Amplitud
Source:
@@ -184,7 +186,7 @@

__VERSION_
Source:
@@ -255,6 +257,8 @@

cl +
Deprecated:
  • Please use amplitude.getInstance().clearUserProperties();
+ @@ -267,7 +271,7 @@

cl
Source:
@@ -342,6 +346,8 @@

getSessio +
Deprecated:
  • Please use amplitude.getInstance().getSessionId();
+ @@ -354,7 +360,7 @@

getSessio
Source:
@@ -521,6 +527,8 @@

Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().identify(identify);
+ @@ -533,7 +541,7 @@
Parameters:
Source:
@@ -729,6 +737,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().init(apiKey, opt_userId, opt_config, opt_callback);
+ @@ -741,7 +751,7 @@
Parameters:
Source:
@@ -816,6 +826,8 @@

isNewSess +
Deprecated:
  • Please use amplitude.getInstance().isNewSession();
+ @@ -828,7 +840,7 @@

isNewSess
Source:
@@ -1016,6 +1028,8 @@

Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().logEvent(eventType, eventProperties, opt_callback);
+ @@ -1028,7 +1042,7 @@
Parameters:
Source:
@@ -1198,7 +1212,8 @@
Parameters:
(optional) a callback function to run after the event is logged. -Note: the server response code and response body from the event upload are passed to the callback function. +Note: the server response code and response body from the event upload are passed to the callback function. +Deprecated Please use amplitude.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); @@ -1239,7 +1254,7 @@
Parameters:
Source:
@@ -1409,7 +1424,7 @@
Parameters:
-
Deprecated:
  • Yes
+
Deprecated:
  • Please use amplitude.getInstance().logRevenueV2(revenue_obj);
@@ -1423,7 +1438,7 @@
Parameters:
Source:
@@ -1550,6 +1565,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().logRevenueV2(revenue_obj);
+ @@ -1562,7 +1579,7 @@
Parameters:
Source:
@@ -1606,7 +1623,7 @@

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

reg +
Deprecated:
  • Please use amplitude.getInstance().regenerateDeviceId();
+ @@ -1653,7 +1672,7 @@

reg
Source:
@@ -1774,6 +1793,8 @@

Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().setDeviceId(deviceId);
+ @@ -1786,7 +1807,7 @@
Parameters:
Source:
@@ -1910,6 +1931,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().setDomain(domain);
+ @@ -1922,7 +1945,7 @@
Parameters:
Source:
@@ -2011,7 +2034,7 @@

Source:
@@ -2162,6 +2185,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().setGroup(groupType, groupName);
+ @@ -2174,7 +2199,7 @@
Parameters:
Source:
@@ -2298,6 +2323,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().setOptOut(enable);
+ @@ -2310,7 +2337,7 @@
Parameters:
Source:
@@ -2429,6 +2456,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().setUserId(userId);
+ @@ -2441,7 +2470,7 @@
Parameters:
Source:
@@ -2589,6 +2618,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance.setUserProperties(userProperties);
+ @@ -2601,7 +2632,7 @@
Parameters:
Source:
@@ -2725,6 +2756,8 @@
Parameters:
+
Deprecated:
  • Please use amplitude.getInstance().setVersionName(versionName);
+ @@ -2737,7 +2770,7 @@
Parameters:
Source:
@@ -2903,7 +2936,162 @@
Parameters:
Source:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

eventCallback(responseCode, responseBody)

+ + + + + +
+ This is the callback for logEvent and identify calls. It gets called after the event/identify is uploaded, +and the server response code and response body from the upload request are passed to the callback function. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
responseCode + + +number + + + + Server response code for the event / identify upload request.
responseBody + + +string + + + + Server response body for the event / identify upload request.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -2943,13 +3131,13 @@
Parameters:


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Fri May 27 2016 15:00:10 GMT-0700 (PDT)
diff --git a/documentation/AmplitudeClient.html b/documentation/AmplitudeClient.html new file mode 100644 index 00000000..0556af0a --- /dev/null +++ b/documentation/AmplitudeClient.html @@ -0,0 +1,2800 @@ + + + + + JSDoc: Class: AmplitudeClient + + + + + + + + + + +
+ +

Class: AmplitudeClient

+ + + + + + +
+ +
+ +

AmplitudeClient

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

new AmplitudeClient()

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

Members

+ + + +

__VERSION__

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

Methods

+ + + + + + +

clearUserProperties()

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

getSessionId() → {number}

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

identify(identify_obj, opt_callback)

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

init(apiKey, opt_userId, opt_config, opt_callback)

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

isNewSession() → {boolean}

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

logEvent(eventType, eventProperties, opt_callback)

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

logEventWithGroups(eventType, eventProperties, groups, opt_callback)

+ + + + + +
+ Log an event with eventType, eventProperties, and groups. Use this to set event-level groups. +Note: the group(s) set only apply for the specific event type being logged and does not persist on the user +(unless you explicitly set it with setGroup). +See the SDK Readme for more information +about groups and Count by Distinct on the Amplitude platform. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
eventType + + +string + + + + name of event
eventProperties + + +object + + + + (optional) an object with string keys and values for the event properties.
groups + + +object + + + + (optional) an object with string groupType: groupName values for the event being logged. +groupName can be a string or an array of strings.
opt_callback + + +Amplitude~eventCallback + + + + (optional) a callback function to run after the event is logged. +Note: the server response code and response body from the event upload are passed to the callback function.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Example
+ +
amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24});
+ + + + + + + + +

logRevenue(price, quantity, product)

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

logRevenueV2(revenue_obj)

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

regenerateDeviceId()

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

setDeviceId(deviceId)

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

setDomain(domain)

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

setGlobalUserProperties()

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

setGroup(groupType, groupName)

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

setOptOut(enable)

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

setUserId(userId)

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

setUserProperties(userProperties)

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

setVersionName(versionName)

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

Example

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

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

Source: amplitude-client.js

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

Source: amplitude.js

-
var Constants = require('./constants');
-var cookieStorage = require('./cookiestorage');
-var getUtmData = require('./utm');
+            
var AmplitudeClient = require('./amplitude-client');
+var Constants = require('./constants');
 var Identify = require('./identify');
-var JSON = require('json'); // jshint ignore:line
-var localStorage = require('./localstorage');  // jshint ignore:line
-var md5 = require('JavaScript-MD5');
 var object = require('object');
-var Request = require('./xhr');
 var Revenue = require('./revenue');
 var type = require('./type');
-var UAParser = require('ua-parser-js');
 var utils = require('./utils');
-var UUID = require('./uuid');
 var version = require('./version');
 var DEFAULT_OPTIONS = require('./options');
 
 /**
- * Amplitude SDK API - instance constructor.
+ * Amplitude SDK API - instance manager.
+ * Function calls directly on amplitude have been deprecated. Please call methods on the default shared instance: amplitude.getInstance() instead.
+ * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps} for more information about this change.
  * @constructor Amplitude
  * @public
  * @example var amplitude = new Amplitude();
  */
 var Amplitude = function Amplitude() {
-  this._unsentEvents = [];
-  this._unsentIdentifys = [];
-  this._ua = new UAParser(navigator.userAgent).getResult();
   this.options = object.merge({}, DEFAULT_OPTIONS);
-  this.cookieStorage = new cookieStorage().getStorage();
-  this._q = []; // queue for proxied functions before script load
-  this._sending = false;
-  this._updateScheduled = false;
-
-  // event meta data
-  this._eventId = 0;
-  this._identifyId = 0;
-  this._lastEventTime = null;
-  this._newSession = false;
-  this._sequenceNumber = 0;
-  this._sessionId = null;
+  this._q = [];
+  this._instances = {}; // mapping of instance names to instances
 };
 
 Amplitude.prototype.Identify = Identify;
 Amplitude.prototype.Revenue = Revenue;
 
+Amplitude.prototype.getInstance = function getInstance(instance) {
+  instance = utils.isEmptyString(instance) ? Constants.DEFAULT_INSTANCE : instance.toLowerCase();
+  var client = this._instances[instance];
+  if (client === undefined) {
+    client = new AmplitudeClient(instance);
+    this._instances[instance] = client;
+  }
+  return client;
+};
+
 /**
  * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations.
  * This is required before any other methods can be called.
@@ -80,113 +72,17 @@ 

Source: amplitude.js

* @param {object} opt_config - (optional) Configuration options. * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#configuration-options} for list of options and default values. * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete. + * @deprecated Please use amplitude.getInstance().init(apiKey, opt_userId, opt_config, opt_callback); * @example amplitude.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); */ Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { - if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { - utils.log('Invalid apiKey. Please re-initialize with a valid apiKey'); - return; - } - - try { - this.options.apiKey = apiKey; - _parseConfig(this.options, opt_config); - this.cookieStorage.options({ - expirationDays: this.options.cookieExpiration, - domain: this.options.domain - }); - this.options.domain = this.cookieStorage.options().domain; - - _upgradeCookeData(this); - _loadCookieData(this); - - // load deviceId and userId from input, or try to fetch existing value from cookie - this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' && - !utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R'; - this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) || - this.options.userId || null; - - var now = new Date().getTime(); - if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) { - this._newSession = true; - this._sessionId = now; - } - this._lastEventTime = now; - _saveCookieData(this); - - if (this.options.saveEvents) { - this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey); - this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey); - - // validate event properties for unsent events - for (var i = 0; i < this._unsentEvents.length; i++) { - var eventProperties = this._unsentEvents[i].event_properties; - var groups = this._unsentEvents[i].groups; - this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); - this._unsentEvents[i].groups = utils.validateGroups(groups); - } - - // validate user properties for unsent identifys - for (var j = 0; j < this._unsentIdentifys.length; j++) { - var userProperties = this._unsentIdentifys[j].user_properties; - var identifyGroups = this._unsentIdentifys[j].groups; - this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); - this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups); - } - - this._sendEventsIfReady(); // try sending unsent events - } - - if (this.options.includeUtm) { - this._initUtmData(); - } - - if (this.options.includeReferrer) { - this._saveReferrer(this._getReferrer()); - } - } catch (e) { - utils.log(e); - } finally { + this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { + // make options such as deviceId available for callback functions + this.options = instance.options; if (type(opt_callback) === 'function') { - opt_callback(); - } - } -}; - -/** - * Parse and validate user specified config values and overwrite existing option value - * DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values - * @private - */ -var _parseConfig = function _parseConfig(options, config) { - if (type(config) !== 'object') { - return; - } - - // validates config value is defined, is the correct type, and some additional value sanity checks - var parseValidateAndLoad = function parseValidateAndLoad(key) { - if (!DEFAULT_OPTIONS.hasOwnProperty(key)) { - return; // skip bogus config values - } - - var inputValue = config[key]; - var expectedType = type(DEFAULT_OPTIONS[key]); - if (!utils.validateInput(inputValue, key + ' option', expectedType)) { - return; - } - if (expectedType === 'boolean') { - options[key] = !!inputValue; - } else if ((expectedType === 'string' && !utils.isEmptyString(inputValue)) || - (expectedType === 'number' && inputValue > 0)) { - options[key] = inputValue; - } - }; - - for (var key in config) { - if (config.hasOwnProperty(key)) { - parseValidateAndLoad(key); + opt_callback(instance); } - } + }.bind(this)); }; /** @@ -194,6 +90,7 @@

Source: amplitude.js

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

Source: amplitude.js

} } this._q = []; // clear function queue after running -}; -/** - * Check that the apiKey is set before calling a function. Logs a warning message if not set. - * @private - */ -Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { - if (utils.isEmptyString(this.options.apiKey)) { - utils.log('Invalid apiKey. Please set a valid apiKey with init() before calling ' + methodName); - return false; - } - return true; -}; - -/** - * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. - * @private - */ -Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { - var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); - if (utils.isEmptyString(savedUnsentEventsString)) { - return []; // new app, does not have any saved events - } - - if (type(savedUnsentEventsString) === 'string') { - try { - var events = JSON.parse(savedUnsentEventsString); - if (type(events) === 'array') { // handle case where JSON dumping of unsent events is corrupted - return events; - } - } catch (e) {} + // run queued up functions on instances + for (var instance in this._instances) { + if (this._instances.hasOwnProperty(instance)) { + this._instances[instance].runQueuedFunctions(); + } } - utils.log('Unable to load ' + unsentKey + ' events. Restart with a new empty queue.'); - return []; }; /** * Returns true if a new session was created during initialization, otherwise false. * @public * @return {boolean} Whether a new session was created during initialization. + * @deprecated Please use amplitude.getInstance().isNewSession(); */ Amplitude.prototype.isNewSession = function isNewSession() { - return this._newSession; + return this.getInstance().isNewSession(); }; /** * Returns the id of the current session. * @public * @return {number} Id of the current session. + * @deprecated Please use amplitude.getInstance().getSessionId(); */ Amplitude.prototype.getSessionId = function getSessionId() { - return this._sessionId; + return this.getInstance().getSessionId(); }; /** @@ -260,8 +132,7 @@

Source: amplitude.js

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

Source: amplitude.js

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

Source: amplitude.js

* @private */ Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { - this._sequenceNumber++; - return this._sequenceNumber; -}; - -/** - * Returns the total count of unsent events and identifys - * @private - */ -Amplitude.prototype._unsentCount = function _unsentCount() { - return this._unsentEvents.length + this._unsentIdentifys.length; -}; - -/** - * Send events if ready. Returns true if events are sent. - * @private - */ -Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { - if (this._unsentCount() === 0) { - return false; - } - - // if batching disabled, send any unsent events immediately - if (!this.options.batchEvents) { - this.sendEvents(callback); - return true; - } - - // if batching enabled, check if min threshold met for batch size - if (this._unsentCount() >= this.options.eventUploadThreshold) { - this.sendEvents(callback); - return true; - } - - // otherwise schedule an upload after 30s - if (!this._updateScheduled) { // make sure we only schedule 1 upload - this._updateScheduled = true; - setTimeout(function() { - this._updateScheduled = false; - this.sendEvents(); - }.bind(this), this.options.eventUploadPeriodMillis - ); - } - - return false; // an upload was scheduled, no events were uploaded -}; - -/** - * Helper function to fetch values from storage - * Storage argument allows for localStoraoge and sessionStoraoge - * @private - */ -Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { - return storage.getItem(key); -}; - -/** - * Helper function to set values in storage - * Storage argument allows for localStoraoge and sessionStoraoge - * @private - */ -Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) { - storage.setItem(key, value); -}; - -/** - * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber) - * can be stored in many different places (localStorage, cookie, etc). - * Need to unify all sources into one place with a one-time upgrade/migration. - * @private - */ -var _upgradeCookeData = function _upgradeCookeData(scope) { - // skip if migration already happened - var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (type(cookieData) === 'object' && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { - return; - } - - var _getAndRemoveFromLocalStorage = function _getAndRemoveFromLocalStorage(key) { - var value = localStorage.getItem(key); - localStorage.removeItem(key); - return value; - }; - - // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey - var apiKeySuffix = (type(scope.options.apiKey) === 'string' && ('_' + scope.options.apiKey.slice(0, 6))) || ''; - var localStorageDeviceId = _getAndRemoveFromLocalStorage(Constants.DEVICE_ID + apiKeySuffix); - var localStorageUserId = _getAndRemoveFromLocalStorage(Constants.USER_ID + apiKeySuffix); - var localStorageOptOut = _getAndRemoveFromLocalStorage(Constants.OPT_OUT + apiKeySuffix); - if (localStorageOptOut !== null && localStorageOptOut !== undefined) { - localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean - } - - // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support - var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID)); - var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME)); - var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID)); - var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID)); - var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER)); - - var _getFromCookie = function _getFromCookie(key) { - return type(cookieData) === 'object' && cookieData[key]; - }; - scope.options.deviceId = _getFromCookie('deviceId') || localStorageDeviceId; - scope.options.userId = _getFromCookie('userId') || localStorageUserId; - scope._sessionId = _getFromCookie('sessionId') || localStorageSessionId || scope._sessionId; - scope._lastEventTime = _getFromCookie('lastEventTime') || localStorageLastEventTime || scope._lastEventTime; - scope._eventId = _getFromCookie('eventId') || localStorageEventId || scope._eventId; - scope._identifyId = _getFromCookie('identifyId') || localStorageIdentifyId || scope._identifyId; - scope._sequenceNumber = _getFromCookie('sequenceNumber') || localStorageSequenceNumber || scope._sequenceNumber; - - // optOut is a little trickier since it is a boolean - scope.options.optOut = localStorageOptOut || false; - if (cookieData && cookieData.optOut !== undefined && cookieData.optOut !== null) { - scope.options.optOut = String(cookieData.optOut) === 'true'; - } - - _saveCookieData(scope); -}; - -/** - * Fetches deviceId, userId, event meta data from amplitude cookie - * @private - */ -var _loadCookieData = function _loadCookieData(scope) { - var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (type(cookieData) === 'object') { - if (cookieData.deviceId) { - scope.options.deviceId = cookieData.deviceId; - } - if (cookieData.userId) { - scope.options.userId = cookieData.userId; - } - if (cookieData.optOut !== null && cookieData.optOut !== undefined) { - scope.options.optOut = cookieData.optOut; - } - if (cookieData.sessionId) { - scope._sessionId = parseInt(cookieData.sessionId); - } - if (cookieData.lastEventTime) { - scope._lastEventTime = parseInt(cookieData.lastEventTime); - } - if (cookieData.eventId) { - scope._eventId = parseInt(cookieData.eventId); - } - if (cookieData.identifyId) { - scope._identifyId = parseInt(cookieData.identifyId); - } - if (cookieData.sequenceNumber) { - scope._sequenceNumber = parseInt(cookieData.sequenceNumber); - } - } -}; - -/** - * Saves deviceId, userId, event meta data to amplitude cookie - * @private - */ -var _saveCookieData = function _saveCookieData(scope) { - scope.cookieStorage.set(scope.options.cookieName, { - deviceId: scope.options.deviceId, - userId: scope.options.userId, - optOut: scope.options.optOut, - sessionId: scope._sessionId, - lastEventTime: scope._lastEventTime, - eventId: scope._eventId, - identifyId: scope._identifyId, - sequenceNumber: scope._sequenceNumber - }); -}; - -/** - * Parse the utm properties out of cookies and query for adding to user properties. - * @private - */ -Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { - queryParams = queryParams || location.search; - cookieParams = cookieParams || this.cookieStorage.get('__utmz'); - var utmProperties = getUtmData(cookieParams, queryParams); - _sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties); -}; - -/** - * Since user properties are propagated on server, only send once per session, don't need to send with every event - * @private - */ -var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) { - if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) { - return; - } - - // setOnce the initial user properties - var identify = new Identify(); - for (var key in userProperties) { - if (userProperties.hasOwnProperty(key)) { - identify.setOnce('initial_' + key, userProperties[key]); - } - } - - // only save userProperties if not already in sessionStorage under key or if storage disabled - var hasSessionStorage = utils.sessionStorageEnabled(); - if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) { - for (var property in userProperties) { - if (userProperties.hasOwnProperty(property)) { - identify.set(property, userProperties[property]); - } - } - - if (hasSessionStorage) { - scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties)); - } - } - - scope.identify(identify); -}; - -/** - * @private - */ -Amplitude.prototype._getReferrer = function _getReferrer() { - return document.referrer; -}; - -/** - * Parse the domain from referrer info - * @private - */ -Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) { - if (utils.isEmptyString(referrer)) { - return null; - } - var parts = referrer.split('/'); - if (parts.length >= 3) { - return parts[2]; - } - return null; -}; - -/** - * Fetch the referrer information, parse the domain and send. - * Since user properties are propagated on the server, only send once per session, don't need to send with every event - * @private - */ -Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { - if (utils.isEmptyString(referrer)) { - return; - } - var referrerInfo = { - 'referrer': referrer, - 'referring_domain': this._getReferringDomain(referrer) - }; - _sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo); + return this.getInstance().nextSequenceNumber(); }; /** @@ -537,51 +157,29 @@

Source: amplitude.js

* @private */ Amplitude.prototype.saveEvents = function saveEvents() { - try { - this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); - } catch (e) {} - - try { - this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); - } catch (e) {} + this.getInstance().saveEvents(); }; /** * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. * @public * @param {string} domain to set. + * @deprecated Please use amplitude.getInstance().setDomain(domain); * @example amplitude.setDomain('.amplitude.com'); */ Amplitude.prototype.setDomain = function setDomain(domain) { - if (!utils.validateInput(domain, 'domain', 'string')) { - return; - } - - try { - this.cookieStorage.options({ - domain: domain - }); - this.options.domain = this.cookieStorage.options().domain; - _loadCookieData(this); - _saveCookieData(this); - } catch (e) { - utils.log(e); - } + this.getInstance().setDomain(domain); }; /** * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. + * @deprecated Please use amplitude.getInstance().setUserId(userId); * @example amplitude.setUserId('joe@gmail.com'); */ Amplitude.prototype.setUserId = function setUserId(userId) { - try { - this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; - _saveCookieData(this); - } catch (e) { - utils.log(e); - } + this.getInstance().setUserId(userId); }; /** @@ -595,48 +193,34 @@

Source: amplitude.js

* @public * @param {string} groupType - the group type (ex: orgId) * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @deprecated Please use amplitude.getInstance().setGroup(groupType, groupName); * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. */ Amplitude.prototype.setGroup = function(groupType, groupName) { - if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || - utils.isEmptyString(groupType)) { - return; - } - - var groups = {}; - groups[groupType] = groupName; - var identify = new Identify().set(groupType, groupName); - this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null); + this.getInstance().setGroup(groupType, groupName); }; /** * Sets whether to opt current user out of tracking. * @public * @param {boolean} enable - if true then no events will be logged or sent. + * @deprecated Please use amplitude.getInstance().setOptOut(enable); * @example: amplitude.setOptOut(true); */ Amplitude.prototype.setOptOut = function setOptOut(enable) { - if (!utils.validateInput(enable, 'enable', 'boolean')) { - return; - } - - try { - this.options.optOut = enable; - _saveCookieData(this); - } catch (e) { - utils.log(e); - } + this.getInstance().setOptOut(enable); }; /** - * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. * @public + * @deprecated Please use amplitude.getInstance().regenerateDeviceId(); */ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { - this.setDeviceId(UUID() + 'R'); + this.getInstance().regenerateDeviceId(); }; /** @@ -645,21 +229,11 @@

Source: amplitude.js

* (we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system. * @public * @param {string} deviceId - custom deviceId for current user. + * @deprecated Please use amplitude.getInstance().setDeviceId(deviceId); * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); */ Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { - if (!utils.validateInput(deviceId, 'deviceId', 'string')) { - return; - } - - try { - if (!utils.isEmptyString(deviceId)) { - this.options.deviceId = ('' + deviceId); - _saveCookieData(this); - } - } catch (e) { - utils.log(e); - } + this.getInstance().setDeviceId(deviceId); }; /** @@ -668,50 +242,21 @@

Source: amplitude.js

* @param {object} - object with string keys and values for the user properties to set. * @param {boolean} - DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in * memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated. + * @deprecated Please use amplitude.getInstance.setUserProperties(userProperties); * @example amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true}) */ Amplitude.prototype.setUserProperties = function setUserProperties(userProperties) { - if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { - return; - } - // convert userProperties into an identify call - var identify = new Identify(); - for (var property in userProperties) { - if (userProperties.hasOwnProperty(property)) { - identify.set(property, userProperties[property]); - } - } - this.identify(identify); + this.getInstance().setUserProperties(userProperties); }; /** * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! * @public + * @deprecated Please use amplitude.getInstance().clearUserProperties(); * @example amplitude.clearUserProperties(); */ Amplitude.prototype.clearUserProperties = function clearUserProperties(){ - if (!this._apiKeySet('clearUserProperties()')) { - return; - } - - var identify = new Identify(); - identify.clearAll(); - this.identify(identify); -}; - -/** - * Applies the proxied functions on the proxied object to an instance of the real object. - * Used to convert proxied Identify and Revenue objects. - * @private - */ -var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) { - for (var i = 0; i < proxy._q.length; i++) { - var fn = instance[proxy._q[i][0]]; - if (type(fn) === 'function') { - fn.apply(instance, proxy._q[i].slice(1)); - } - } - return instance; + this.getInstance().clearUserProperties(); }; /** @@ -721,140 +266,24 @@

Source: amplitude.js

* @param {Identify} identify_obj - the Identify object containing the user property operations to send. * @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent. * Note: the server response code and response body from the identify event upload are passed to the callback function. + * @deprecated Please use amplitude.getInstance().identify(identify); * @example * var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31'); * amplitude.identify(identify); */ Amplitude.prototype.identify = function(identify_obj, opt_callback) { - if (!this._apiKeySet('identify()')) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return; - } - - // if identify input is a proxied object created by the async loading snippet, convert it into an identify object - if (type(identify_obj) === 'object' && identify_obj.hasOwnProperty('_q')) { - identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj); - } - - if (identify_obj instanceof Identify) { - // only send if there are operations - if (Object.keys(identify_obj.userPropertiesOperations).length > 0) { - return this._logEvent( - Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, opt_callback - ); - } - } else { - utils.log('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj)); - } - - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } + this.getInstance().identify(identify_obj, opt_callback); }; /** * Set a versionName for your application. * @public * @param {string} versionName - The version to set for your application. + * @deprecated Please use amplitude.getInstance().setVersionName(versionName); * @example amplitude.setVersionName('1.12.3'); */ Amplitude.prototype.setVersionName = function setVersionName(versionName) { - if (!utils.validateInput(versionName, 'versionName', 'string')) { - return; - } - this.options.versionName = versionName; -}; - -/** - * Private logEvent method. Keeps apiProperties from being publicly exposed. - * @private - */ -Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) { - _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs - if (!eventType || this.options.optOut) { - if (type(callback) === 'function') { - callback(0, 'No request sent'); - } - return; - } - - try { - var eventId; - if (eventType === Constants.IDENTIFY_EVENT) { - eventId = this.nextIdentifyId(); - } else { - eventId = this.nextEventId(); - } - var sequenceNumber = this.nextSequenceNumber(); - var eventTime = new Date().getTime(); - if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { - this._sessionId = eventTime; - } - this._lastEventTime = eventTime; - _saveCookieData(this); - - userProperties = userProperties || {}; - apiProperties = apiProperties || {}; - eventProperties = eventProperties || {}; - groups = groups || {}; - var event = { - device_id: this.options.deviceId, - user_id: this.options.userId, - timestamp: eventTime, - event_id: eventId, - session_id: this._sessionId || -1, - event_type: eventType, - version_name: this.options.versionName || null, - platform: this.options.platform, - os_name: this._ua.browser.name || null, - os_version: this._ua.browser.major || null, - device_model: this._ua.os.name || null, - language: this.options.language, - api_properties: apiProperties, - event_properties: utils.truncate(utils.validateProperties(eventProperties)), - user_properties: utils.truncate(utils.validateProperties(userProperties)), - uuid: UUID(), - library: { - name: 'amplitude-js', - version: version - }, - sequence_number: sequenceNumber, // for ordering events and identifys - groups: utils.truncate(utils.validateGroups(groups)) - // country: null - }; - - if (eventType === Constants.IDENTIFY_EVENT) { - this._unsentIdentifys.push(event); - this._limitEventsQueued(this._unsentIdentifys); - } else { - this._unsentEvents.push(event); - this._limitEventsQueued(this._unsentEvents); - } - - if (this.options.saveEvents) { - this.saveEvents(); - } - - if (!this._sendEventsIfReady(callback) && type(callback) === 'function') { - callback(0, 'No request sent'); - } - - return eventId; - } catch (e) { - utils.log(e); - } -}; - -/** - * Remove old events from the beginning of the array if too many have accumulated. Default limit is 1000 events. - * @private - */ -Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { - if (queue.length > this.options.savedMaxCount) { - queue.splice(0, queue.length - this.options.savedMaxCount); - } + this.getInstance().setVersionName(versionName); }; /** @@ -872,17 +301,11 @@

Source: amplitude.js

* @param {object} eventProperties - (optional) an object with string keys and values for the event properties. * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. * Note: the server response code and response body from the event upload are passed to the callback function. + * @deprecated Please use amplitude.getInstance().logEvent(eventType, eventProperties, opt_callback); * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { - if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || - utils.isEmptyString(eventType)) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, null, opt_callback); + return this.getInstance().logEvent(eventType, eventProperties, opt_callback); }; /** @@ -898,25 +321,11 @@

Source: amplitude.js

* groupName can be a string or an array of strings. * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. * Note: the server response code and response body from the event upload are passed to the callback function. + * Deprecated Please use amplitude.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { - if (!this._apiKeySet('logEventWithGroup()') || - !utils.validateInput(eventType, 'eventType', 'string')) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback); -}; - -/** - * Test that n is a number or a numeric value. - * @private - */ -var _isNumber = function _isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); + return this.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); }; /** @@ -926,51 +335,25 @@

Source: amplitude.js

* for more information on the Revenue interface and logging revenue. * @public * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { - if (!this._apiKeySet('logRevenueV2()')) { - return; - } - - // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object - if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) { - revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj); - } - - if (revenue_obj instanceof Revenue) { - // only send if revenue is valid - if (revenue_obj && revenue_obj._isValidRevenue()) { - return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject()); - } - } else { - utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj)); - } + return this.getInstance().logRevenueV2(revenue_obj); }; /** * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 * @public - * @deprecated * @param {number} price - price of revenue event * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1. * @param {string} product - (optional) product identifier + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @example amplitude.logRevenue(3.99, 1, 'product_1234'); */ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { - // Test that the parameters are of the right type. - if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { - // utils.log('Price and quantity arguments to logRevenue must be numbers'); - return -1; - } - - return this._logEvent(Constants.REVENUE_EVENT, {}, { - productId: product, - special: 'revenue_amount', - quantity: quantity || 1, - price: price - }, null, null, null); + return this.getInstance().logRevenue(price, quantity, product); }; /** @@ -978,27 +361,7 @@

Source: amplitude.js

* @private */ Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { - _removeEvents(this, '_unsentEvents', maxEventId); - _removeEvents(this, '_unsentIdentifys', maxIdentifyId); -}; - -/** - * Helper function to remove events up to maxId from a single queue. - * Does a true filter in case events get out of order or old events are removed. - * @private - */ -var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { - if (maxId < 0) { - return; - } - - var filteredEvents = []; - for (var i = 0; i < scope[eventQueue].length || 0; i++) { - if (scope[eventQueue][i].event_id > maxId) { - filteredEvents.push(scope[eventQueue][i]); - } - } - scope[eventQueue] = filteredEvents; + this.getInstance().removeEvents(maxEventId, maxIdentifyId); }; /** @@ -1009,126 +372,7 @@

Source: amplitude.js

* Note the server response code and response body are passed to the callback as input arguments. */ Amplitude.prototype.sendEvents = function sendEvents(callback) { - if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) { - if (type(callback) === 'function') { - callback(0, 'No request sent'); - } - return; - } - - this._sending = true; - var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + this.options.apiEndpoint + '/'; - - // fetch events to send - var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); - var mergedEvents = this._mergeEventsAndIdentifys(numEvents); - var maxEventId = mergedEvents.maxEventId; - var maxIdentifyId = mergedEvents.maxIdentifyId; - var events = JSON.stringify(mergedEvents.eventsToSend); - var uploadTime = new Date().getTime(); - - var data = { - client: this.options.apiKey, - e: events, - v: Constants.API_VERSION, - upload_time: uploadTime, - checksum: md5(Constants.API_VERSION + this.options.apiKey + events + uploadTime) - }; - - var scope = this; - new Request(url, data).send(function(status, response) { - scope._sending = false; - try { - if (status === 200 && response === 'success') { - scope.removeEvents(maxEventId, maxIdentifyId); - - // Update the event cache after the removal of sent events. - if (scope.options.saveEvents) { - scope.saveEvents(); - } - - // Send more events if any queued during previous send. - if (!scope._sendEventsIfReady(callback) && type(callback) === 'function') { - callback(status, response); - } - - // handle payload too large - } else if (status === 413) { - // utils.log('request too large'); - // Can't even get this one massive event through. Drop it, even if it is an identify. - if (scope.options.uploadBatchSize === 1) { - scope.removeEvents(maxEventId, maxIdentifyId); - } - - // The server complained about the length of the request. Backoff and try again. - scope.options.uploadBatchSize = Math.ceil(numEvents / 2); - scope.sendEvents(callback); - - } else if (type(callback) === 'function') { // If server turns something like a 400 - callback(status, response); - } - } catch (e) { - // utils.log('failed upload'); - } - }); -}; - -/** - * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. - * @private - */ -Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { - // coalesce events from both queues - var eventsToSend = []; - var eventIndex = 0; - var maxEventId = -1; - var identifyIndex = 0; - var maxIdentifyId = -1; - - while (eventsToSend.length < numEvents) { - var event; - var noIdentifys = identifyIndex >= this._unsentIdentifys.length; - var noEvents = eventIndex >= this._unsentEvents.length; - - // case 0: no events or identifys left - // note this should not happen, this means we have less events and identifys than expected - if (noEvents && noIdentifys) { - utils.log('Merging Events and Identifys, less events and identifys than expected'); - break; - } - - // case 1: no identifys - grab from events - else if (noIdentifys) { - event = this._unsentEvents[eventIndex++]; - maxEventId = event.event_id; - - // case 2: no events - grab from identifys - } else if (noEvents) { - event = this._unsentIdentifys[identifyIndex++]; - maxIdentifyId = event.event_id; - - // case 3: need to compare sequence numbers - } else { - // events logged before v2.5.0 won't have a sequence number, put those first - if (!('sequence_number' in this._unsentEvents[eventIndex]) || - this._unsentEvents[eventIndex].sequence_number < - this._unsentIdentifys[identifyIndex].sequence_number) { - event = this._unsentEvents[eventIndex++]; - maxEventId = event.event_id; - } else { - event = this._unsentIdentifys[identifyIndex++]; - maxIdentifyId = event.event_id; - } - } - - eventsToSend.push(event); - } - - return { - eventsToSend: eventsToSend, - maxEventId: maxEventId, - maxIdentifyId: maxIdentifyId - }; + this.getInstance().sendEvents(callback); }; /** @@ -1137,7 +381,7 @@

Source: amplitude.js

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

Source: amplitude.js


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Fri May 27 2016 15:00:10 GMT-0700 (PDT)
diff --git a/documentation/identify.js.html b/documentation/identify.js.html index 956a279e..4b905b49 100644 --- a/documentation/identify.js.html +++ b/documentation/identify.js.html @@ -220,13 +220,13 @@

Source: identify.js


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Fri May 27 2016 15:00:10 GMT-0700 (PDT)
diff --git a/documentation/index.html b/documentation/index.html index 801ba1ae..eacf2338 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -50,13 +50,13 @@


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

Source: revenue.js


- Documentation generated by JSDoc 3.4.0 on Wed Apr 20 2016 01:13:36 GMT-0700 (PDT) + Documentation generated by JSDoc 3.4.0 on Fri May 27 2016 15:00:10 GMT-0700 (PDT)
diff --git a/package.json b/package.json index 8c89b01a..63534d3e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "amplitude-js", "author": "Amplitude ", - "version": "2.13.0", + "version": "3.0.0", "license": "MIT", "description": "Javascript library for Amplitude Analytics", "keywords": [ diff --git a/scripts/readme.js b/scripts/readme.js index 81bab201..2c865878 100644 --- a/scripts/readme.js +++ b/scripts/readme.js @@ -11,7 +11,7 @@ var snippet = fs.readFileSync(snippetFilename, 'utf-8'); var script = ' '; var updated = readme.replace(/ +/, script); diff --git a/src/amplitude-client.js b/src/amplitude-client.js new file mode 100644 index 00000000..300c161d --- /dev/null +++ b/src/amplitude-client.js @@ -0,0 +1,1128 @@ +var Constants = require('./constants'); +var cookieStorage = require('./cookiestorage'); +var getUtmData = require('./utm'); +var Identify = require('./identify'); +var JSON = require('json'); // jshint ignore:line +var localStorage = require('./localstorage'); // jshint ignore:line +var md5 = require('JavaScript-MD5'); +var object = require('object'); +var Request = require('./xhr'); +var Revenue = require('./revenue'); +var type = require('./type'); +var UAParser = require('ua-parser-js'); +var utils = require('./utils'); +var UUID = require('./uuid'); +var version = require('./version'); +var DEFAULT_OPTIONS = require('./options'); + +/** + * AmplitudeClient SDK API - instance constructor. + * The Amplitude class handles creation of client instances, all you need to do is call amplitude.getInstance() + * @constructor AmplitudeClient + * @public + * @example var amplitudeClient = new AmplitudeClient(); + */ +var AmplitudeClient = function AmplitudeClient(instanceName) { + this._instanceName = utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName.toLowerCase(); + this._storageSuffix = this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName; + this._unsentEvents = []; + this._unsentIdentifys = []; + this._ua = new UAParser(navigator.userAgent).getResult(); + this.options = object.merge({}, DEFAULT_OPTIONS); + this.cookieStorage = new cookieStorage().getStorage(); + this._q = []; // queue for proxied functions before script load + this._sending = false; + this._updateScheduled = false; + + // event meta data + this._eventId = 0; + this._identifyId = 0; + this._lastEventTime = null; + this._newSession = false; + this._sequenceNumber = 0; + this._sessionId = null; +}; + +AmplitudeClient.prototype.Identify = Identify; +AmplitudeClient.prototype.Revenue = Revenue; + +/** + * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. + * This is required before any other methods can be called. + * @public + * @param {string} apiKey - The API key for your app. + * @param {string} opt_userId - (optional) An identifier for this user. + * @param {object} opt_config - (optional) Configuration options. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#configuration-options} for list of options and default values. + * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete. + * @example amplitudeClient.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); + */ +AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { + if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { + utils.log('Invalid apiKey. Please re-initialize with a valid apiKey'); + return; + } + + try { + this.options.apiKey = apiKey; + _parseConfig(this.options, opt_config); + this.cookieStorage.options({ + expirationDays: this.options.cookieExpiration, + domain: this.options.domain + }); + this.options.domain = this.cookieStorage.options().domain; + + if (this._instanceName === Constants.DEFAULT_INSTANCE) { + _upgradeCookeData(this); + } + _loadCookieData(this); + + // load deviceId and userId from input, or try to fetch existing value from cookie + this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' && + !utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R'; + this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) || + this.options.userId || null; + + var now = new Date().getTime(); + if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) { + this._newSession = true; + this._sessionId = now; + } + this._lastEventTime = now; + _saveCookieData(this); + + if (this.options.saveEvents) { + this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey); + this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey); + + // validate event properties for unsent events + for (var i = 0; i < this._unsentEvents.length; i++) { + var eventProperties = this._unsentEvents[i].event_properties; + var groups = this._unsentEvents[i].groups; + this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); + this._unsentEvents[i].groups = utils.validateGroups(groups); + } + + // validate user properties for unsent identifys + for (var j = 0; j < this._unsentIdentifys.length; j++) { + var userProperties = this._unsentIdentifys[j].user_properties; + var identifyGroups = this._unsentIdentifys[j].groups; + this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); + this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups); + } + + this._sendEventsIfReady(); // try sending unsent events + } + + if (this.options.includeUtm) { + this._initUtmData(); + } + + if (this.options.includeReferrer) { + this._saveReferrer(this._getReferrer()); + } + } catch (e) { + utils.log(e); + } finally { + if (type(opt_callback) === 'function') { + opt_callback(this); + } + } +}; + +/** + * Parse and validate user specified config values and overwrite existing option value + * DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values + * @private + */ +var _parseConfig = function _parseConfig(options, config) { + if (type(config) !== 'object') { + return; + } + + // validates config value is defined, is the correct type, and some additional value sanity checks + var parseValidateAndLoad = function parseValidateAndLoad(key) { + if (!DEFAULT_OPTIONS.hasOwnProperty(key)) { + return; // skip bogus config values + } + + var inputValue = config[key]; + var expectedType = type(DEFAULT_OPTIONS[key]); + if (!utils.validateInput(inputValue, key + ' option', expectedType)) { + return; + } + if (expectedType === 'boolean') { + options[key] = !!inputValue; + } else if ((expectedType === 'string' && !utils.isEmptyString(inputValue)) || + (expectedType === 'number' && inputValue > 0)) { + options[key] = inputValue; + } + }; + + for (var key in config) { + if (config.hasOwnProperty(key)) { + parseValidateAndLoad(key); + } + } +}; + +/** + * Run functions queued up by proxy loading snippet + * @private + */ +AmplitudeClient.prototype.runQueuedFunctions = function () { + for (var i = 0; i < this._q.length; i++) { + var fn = this[this._q[i][0]]; + if (type(fn) === 'function') { + fn.apply(this, this._q[i].slice(1)); + } + } + this._q = []; // clear function queue after running +}; + +/** + * Check that the apiKey is set before calling a function. Logs a warning message if not set. + * @private + */ +AmplitudeClient.prototype._apiKeySet = function _apiKeySet(methodName) { + if (utils.isEmptyString(this.options.apiKey)) { + utils.log('Invalid apiKey. Please set a valid apiKey with init() before calling ' + methodName); + return false; + } + return true; +}; + +/** + * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. + * @private + */ +AmplitudeClient.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { + var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); + if (utils.isEmptyString(savedUnsentEventsString)) { + return []; // new app, does not have any saved events + } + + if (type(savedUnsentEventsString) === 'string') { + try { + var events = JSON.parse(savedUnsentEventsString); + if (type(events) === 'array') { // handle case where JSON dumping of unsent events is corrupted + return events; + } + } catch (e) {} + } + utils.log('Unable to load ' + unsentKey + ' events. Restart with a new empty queue.'); + return []; +}; + +/** + * Returns true if a new session was created during initialization, otherwise false. + * @public + * @return {boolean} Whether a new session was created during initialization. + */ +AmplitudeClient.prototype.isNewSession = function isNewSession() { + return this._newSession; +}; + +/** + * Returns the id of the current session. + * @public + * @return {number} Id of the current session. + */ +AmplitudeClient.prototype.getSessionId = function getSessionId() { + return this._sessionId; +}; + +/** + * Increments the eventId and returns it. + * @private + */ +AmplitudeClient.prototype.nextEventId = function nextEventId() { + this._eventId++; + return this._eventId; +}; + +/** + * Increments the identifyId and returns it. + * @private + */ +AmplitudeClient.prototype.nextIdentifyId = function nextIdentifyId() { + this._identifyId++; + return this._identifyId; +}; + +/** + * Increments the sequenceNumber and returns it. + * @private + */ +AmplitudeClient.prototype.nextSequenceNumber = function nextSequenceNumber() { + this._sequenceNumber++; + return this._sequenceNumber; +}; + +/** + * Returns the total count of unsent events and identifys + * @private + */ +AmplitudeClient.prototype._unsentCount = function _unsentCount() { + return this._unsentEvents.length + this._unsentIdentifys.length; +}; + +/** + * Send events if ready. Returns true if events are sent. + * @private + */ +AmplitudeClient.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { + if (this._unsentCount() === 0) { + return false; + } + + // if batching disabled, send any unsent events immediately + if (!this.options.batchEvents) { + this.sendEvents(callback); + return true; + } + + // if batching enabled, check if min threshold met for batch size + if (this._unsentCount() >= this.options.eventUploadThreshold) { + this.sendEvents(callback); + return true; + } + + // otherwise schedule an upload after 30s + if (!this._updateScheduled) { // make sure we only schedule 1 upload + this._updateScheduled = true; + setTimeout(function() { + this._updateScheduled = false; + this.sendEvents(); + }.bind(this), this.options.eventUploadPeriodMillis + ); + } + + return false; // an upload was scheduled, no events were uploaded +}; + +/** + * Helper function to fetch values from storage + * Storage argument allows for localStoraoge and sessionStoraoge + * @private + */ +AmplitudeClient.prototype._getFromStorage = function _getFromStorage(storage, key) { + return storage.getItem(key + this._storageSuffix); +}; + +/** + * Helper function to set values in storage + * Storage argument allows for localStoraoge and sessionStoraoge + * @private + */ +AmplitudeClient.prototype._setInStorage = function _setInStorage(storage, key, value) { + storage.setItem(key + this._storageSuffix, value); +}; + +/** + * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber) + * can be stored in many different places (localStorage, cookie, etc). + * Need to unify all sources into one place with a one-time upgrade/migration. + * @private + */ +var _upgradeCookeData = function _upgradeCookeData(scope) { + // skip if migration already happened + var cookieData = scope.cookieStorage.get(scope.options.cookieName); + if (type(cookieData) === 'object' && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { + return; + } + + var _getAndRemoveFromLocalStorage = function _getAndRemoveFromLocalStorage(key) { + var value = localStorage.getItem(key); + localStorage.removeItem(key); + return value; + }; + + // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey + var apiKeySuffix = (type(scope.options.apiKey) === 'string' && ('_' + scope.options.apiKey.slice(0, 6))) || ''; + var localStorageDeviceId = _getAndRemoveFromLocalStorage(Constants.DEVICE_ID + apiKeySuffix); + var localStorageUserId = _getAndRemoveFromLocalStorage(Constants.USER_ID + apiKeySuffix); + var localStorageOptOut = _getAndRemoveFromLocalStorage(Constants.OPT_OUT + apiKeySuffix); + if (localStorageOptOut !== null && localStorageOptOut !== undefined) { + localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean + } + + // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support + var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID)); + var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME)); + var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID)); + var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID)); + var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER)); + + var _getFromCookie = function _getFromCookie(key) { + return type(cookieData) === 'object' && cookieData[key]; + }; + scope.options.deviceId = _getFromCookie('deviceId') || localStorageDeviceId; + scope.options.userId = _getFromCookie('userId') || localStorageUserId; + scope._sessionId = _getFromCookie('sessionId') || localStorageSessionId || scope._sessionId; + scope._lastEventTime = _getFromCookie('lastEventTime') || localStorageLastEventTime || scope._lastEventTime; + scope._eventId = _getFromCookie('eventId') || localStorageEventId || scope._eventId; + scope._identifyId = _getFromCookie('identifyId') || localStorageIdentifyId || scope._identifyId; + scope._sequenceNumber = _getFromCookie('sequenceNumber') || localStorageSequenceNumber || scope._sequenceNumber; + + // optOut is a little trickier since it is a boolean + scope.options.optOut = localStorageOptOut || false; + if (cookieData && cookieData.optOut !== undefined && cookieData.optOut !== null) { + scope.options.optOut = String(cookieData.optOut) === 'true'; + } + + _saveCookieData(scope); +}; + +/** + * Fetches deviceId, userId, event meta data from amplitude cookie + * @private + */ +var _loadCookieData = function _loadCookieData(scope) { + var cookieData = scope.cookieStorage.get(scope.options.cookieName + scope._storageSuffix); + if (type(cookieData) === 'object') { + if (cookieData.deviceId) { + scope.options.deviceId = cookieData.deviceId; + } + if (cookieData.userId) { + scope.options.userId = cookieData.userId; + } + if (cookieData.optOut !== null && cookieData.optOut !== undefined) { + scope.options.optOut = cookieData.optOut; + } + if (cookieData.sessionId) { + scope._sessionId = parseInt(cookieData.sessionId); + } + if (cookieData.lastEventTime) { + scope._lastEventTime = parseInt(cookieData.lastEventTime); + } + if (cookieData.eventId) { + scope._eventId = parseInt(cookieData.eventId); + } + if (cookieData.identifyId) { + scope._identifyId = parseInt(cookieData.identifyId); + } + if (cookieData.sequenceNumber) { + scope._sequenceNumber = parseInt(cookieData.sequenceNumber); + } + } +}; + +/** + * Saves deviceId, userId, event meta data to amplitude cookie + * @private + */ +var _saveCookieData = function _saveCookieData(scope) { + scope.cookieStorage.set(scope.options.cookieName + scope._storageSuffix, { + deviceId: scope.options.deviceId, + userId: scope.options.userId, + optOut: scope.options.optOut, + sessionId: scope._sessionId, + lastEventTime: scope._lastEventTime, + eventId: scope._eventId, + identifyId: scope._identifyId, + sequenceNumber: scope._sequenceNumber + }); +}; + +/** + * Parse the utm properties out of cookies and query for adding to user properties. + * @private + */ +AmplitudeClient.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { + queryParams = queryParams || location.search; + cookieParams = cookieParams || this.cookieStorage.get('__utmz'); + var utmProperties = getUtmData(cookieParams, queryParams); + _sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties); +}; + +/** + * Since user properties are propagated on server, only send once per session, don't need to send with every event + * @private + */ +var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) { + if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) { + return; + } + + // setOnce the initial user properties + var identify = new Identify(); + for (var key in userProperties) { + if (userProperties.hasOwnProperty(key)) { + identify.setOnce('initial_' + key, userProperties[key]); + } + } + + // only save userProperties if not already in sessionStorage under key or if storage disabled + var hasSessionStorage = utils.sessionStorageEnabled(); + if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) { + for (var property in userProperties) { + if (userProperties.hasOwnProperty(property)) { + identify.set(property, userProperties[property]); + } + } + + if (hasSessionStorage) { + scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties)); + } + } + + scope.identify(identify); +}; + +/** + * @private + */ +AmplitudeClient.prototype._getReferrer = function _getReferrer() { + return document.referrer; +}; + +/** + * Parse the domain from referrer info + * @private + */ +AmplitudeClient.prototype._getReferringDomain = function _getReferringDomain(referrer) { + if (utils.isEmptyString(referrer)) { + return null; + } + var parts = referrer.split('/'); + if (parts.length >= 3) { + return parts[2]; + } + return null; +}; + +/** + * Fetch the referrer information, parse the domain and send. + * Since user properties are propagated on the server, only send once per session, don't need to send with every event + * @private + */ +AmplitudeClient.prototype._saveReferrer = function _saveReferrer(referrer) { + if (utils.isEmptyString(referrer)) { + return; + } + var referrerInfo = { + 'referrer': referrer, + 'referring_domain': this._getReferringDomain(referrer) + }; + _sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo); +}; + +/** + * Saves unsent events and identifies to localStorage. JSON stringifies event queues before saving. + * Note: this is called automatically every time events are logged, unless you explicitly set option saveEvents to false. + * @private + */ +AmplitudeClient.prototype.saveEvents = function saveEvents() { + try { + this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); + } catch (e) {} + + try { + this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); + } catch (e) {} +}; + +/** + * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. + * @public + * @param {string} domain to set. + * @example amplitudeClient.setDomain('.amplitude.com'); + */ +AmplitudeClient.prototype.setDomain = function setDomain(domain) { + if (!utils.validateInput(domain, 'domain', 'string')) { + return; + } + + try { + this.cookieStorage.options({ + domain: domain + }); + this.options.domain = this.cookieStorage.options().domain; + _loadCookieData(this); + _saveCookieData(this); + } catch (e) { + utils.log(e); + } +}; + +/** + * Sets an identifier for the current user. + * @public + * @param {string} userId - identifier to set. Can be null. + * @example amplitudeClient.setUserId('joe@gmail.com'); + */ +AmplitudeClient.prototype.setUserId = function setUserId(userId) { + try { + this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; + _saveCookieData(this); + } catch (e) { + utils.log(e); + } +}; + +/** + * Add user to a group or groups. You need to specify a groupType and groupName(s). + * For example you can group people by their organization. + * In that case groupType is "orgId" and groupName would be the actual ID(s). + * groupName can be a string or an array of strings to indicate a user in multiple gruups. + * You can also call setGroup multiple times with different groupTypes to track multiple types of groups (up to 5 per app). + * Note: this will also set groupType: groupName as a user property. + * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information. + * @public + * @param {string} groupType - the group type (ex: orgId) + * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @example amplitudeClient.setGroup('orgId', 15); // this adds the current user to orgId 15. + */ +AmplitudeClient.prototype.setGroup = function(groupType, groupName) { + if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || + utils.isEmptyString(groupType)) { + return; + } + + var groups = {}; + groups[groupType] = groupName; + var identify = new Identify().set(groupType, groupName); + this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null); +}; + +/** + * Sets whether to opt current user out of tracking. + * @public + * @param {boolean} enable - if true then no events will be logged or sent. + * @example: amplitude.setOptOut(true); + */ +AmplitudeClient.prototype.setOptOut = function setOptOut(enable) { + if (!utils.validateInput(enable, 'enable', 'boolean')) { + return; + } + + try { + this.options.optOut = enable; + _saveCookieData(this); + } catch (e) { + utils.log(e); + } +}; + +/** + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you + * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. + * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. + * This uses src/uuid.js to regenerate the deviceId. + * @public + */ +AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() { + this.setDeviceId(UUID() + 'R'); +}; + +/** + * Sets a custom deviceId for current user. Note: this is not recommended unless you know what you are doing + * (like if you have your own system for managing deviceIds). Make sure the deviceId you set is sufficiently unique + * (we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system. + * @public + * @param {string} deviceId - custom deviceId for current user. + * @example amplitudeClient.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); + */ +AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { + if (!utils.validateInput(deviceId, 'deviceId', 'string')) { + return; + } + + try { + if (!utils.isEmptyString(deviceId)) { + this.options.deviceId = ('' + deviceId); + _saveCookieData(this); + } + } catch (e) { + utils.log(e); + } +}; + +/** + * Sets user properties for the current user. + * @public + * @param {object} - object with string keys and values for the user properties to set. + * @param {boolean} - DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in + * memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated. + * @example amplitudeClient.setUserProperties({'gender': 'female', 'sign_up_complete': true}) + */ +AmplitudeClient.prototype.setUserProperties = function setUserProperties(userProperties) { + if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { + return; + } + // convert userProperties into an identify call + var identify = new Identify(); + for (var property in userProperties) { + if (userProperties.hasOwnProperty(property)) { + identify.set(property, userProperties[property]); + } + } + this.identify(identify); +}; + +/** + * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! + * @public + * @example amplitudeClient.clearUserProperties(); + */ +AmplitudeClient.prototype.clearUserProperties = function clearUserProperties(){ + if (!this._apiKeySet('clearUserProperties()')) { + return; + } + + var identify = new Identify(); + identify.clearAll(); + this.identify(identify); +}; + +/** + * Applies the proxied functions on the proxied object to an instance of the real object. + * Used to convert proxied Identify and Revenue objects. + * @private + */ +var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) { + for (var i = 0; i < proxy._q.length; i++) { + var fn = instance[proxy._q[i][0]]; + if (type(fn) === 'function') { + fn.apply(instance, proxy._q[i].slice(1)); + } + } + return instance; +}; + +/** + * Send an identify call containing user property operations to Amplitude servers. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations} + * for more information on the Identify API and user property operations. + * @param {Identify} identify_obj - the Identify object containing the user property operations to send. + * @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent. + * Note: the server response code and response body from the identify event upload are passed to the callback function. + * @example + * var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31'); + * amplitude.identify(identify); + */ +AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) { + if (!this._apiKeySet('identify()')) { + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } + return; + } + + // if identify input is a proxied object created by the async loading snippet, convert it into an identify object + if (type(identify_obj) === 'object' && identify_obj.hasOwnProperty('_q')) { + identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj); + } + + if (identify_obj instanceof Identify) { + // only send if there are operations + if (Object.keys(identify_obj.userPropertiesOperations).length > 0) { + return this._logEvent( + Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, opt_callback + ); + } + } else { + utils.log('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj)); + } + + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } +}; + +/** + * Set a versionName for your application. + * @public + * @param {string} versionName - The version to set for your application. + * @example amplitudeClient.setVersionName('1.12.3'); + */ +AmplitudeClient.prototype.setVersionName = function setVersionName(versionName) { + if (!utils.validateInput(versionName, 'versionName', 'string')) { + return; + } + this.options.versionName = versionName; +}; + +/** + * Private logEvent method. Keeps apiProperties from being publicly exposed. + * @private + */ +AmplitudeClient.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) { + _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs + if (!eventType || this.options.optOut) { + if (type(callback) === 'function') { + callback(0, 'No request sent'); + } + return; + } + + try { + var eventId; + if (eventType === Constants.IDENTIFY_EVENT) { + eventId = this.nextIdentifyId(); + } else { + eventId = this.nextEventId(); + } + var sequenceNumber = this.nextSequenceNumber(); + var eventTime = new Date().getTime(); + if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { + this._sessionId = eventTime; + } + this._lastEventTime = eventTime; + _saveCookieData(this); + + userProperties = userProperties || {}; + apiProperties = apiProperties || {}; + eventProperties = eventProperties || {}; + groups = groups || {}; + var event = { + device_id: this.options.deviceId, + user_id: this.options.userId, + timestamp: eventTime, + event_id: eventId, + session_id: this._sessionId || -1, + event_type: eventType, + version_name: this.options.versionName || null, + platform: this.options.platform, + os_name: this._ua.browser.name || null, + os_version: this._ua.browser.major || null, + device_model: this._ua.os.name || null, + language: this.options.language, + api_properties: apiProperties, + event_properties: utils.truncate(utils.validateProperties(eventProperties)), + user_properties: utils.truncate(utils.validateProperties(userProperties)), + uuid: UUID(), + library: { + name: 'amplitude-js', + version: version + }, + sequence_number: sequenceNumber, // for ordering events and identifys + groups: utils.truncate(utils.validateGroups(groups)) + // country: null + }; + + if (eventType === Constants.IDENTIFY_EVENT) { + this._unsentIdentifys.push(event); + this._limitEventsQueued(this._unsentIdentifys); + } else { + this._unsentEvents.push(event); + this._limitEventsQueued(this._unsentEvents); + } + + if (this.options.saveEvents) { + this.saveEvents(); + } + + if (!this._sendEventsIfReady(callback) && type(callback) === 'function') { + callback(0, 'No request sent'); + } + + return eventId; + } catch (e) { + utils.log(e); + } +}; + +/** + * Remove old events from the beginning of the array if too many have accumulated. Default limit is 1000 events. + * @private + */ +AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue) { + if (queue.length > this.options.savedMaxCount) { + queue.splice(0, queue.length - this.options.savedMaxCount); + } +}; + +/** + * This is the callback for logEvent and identify calls. It gets called after the event/identify is uploaded, + * and the server response code and response body from the upload request are passed to the callback function. + * @callback Amplitude~eventCallback + * @param {number} responseCode - Server response code for the event / identify upload request. + * @param {string} responseBody - Server response body for the event / identify upload request. + */ + +/** + * Log an event with eventType and eventProperties + * @public + * @param {string} eventType - name of event + * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. + * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. + * Note: the server response code and response body from the event upload are passed to the callback function. + * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); + */ +AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { + if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || + utils.isEmptyString(eventType)) { + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } + return -1; + } + return this._logEvent(eventType, eventProperties, null, null, null, opt_callback); +}; + +/** + * Log an event with eventType, eventProperties, and groups. Use this to set event-level groups. + * Note: the group(s) set only apply for the specific event type being logged and does not persist on the user + * (unless you explicitly set it with setGroup). + * See the [SDK Readme]{@link https://github.com/amplitude/Amplitude-Javascript#setting-groups} for more information + * about groups and Count by Distinct on the Amplitude platform. + * @public + * @param {string} eventType - name of event + * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. + * @param {object} groups - (optional) an object with string groupType: groupName values for the event being logged. + * groupName can be a string or an array of strings. + * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. + * Note: the server response code and response body from the event upload are passed to the callback function. + * @example amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24}); + */ +AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { + if (!this._apiKeySet('logEventWithGroup()') || + !utils.validateInput(eventType, 'eventType', 'string')) { + if (type(opt_callback) === 'function') { + opt_callback(0, 'No request sent'); + } + return -1; + } + return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback); +}; + +/** + * Test that n is a number or a numeric value. + * @private + */ +var _isNumber = function _isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +}; + +/** + * Log revenue with Revenue interface. The new revenue interface allows for more revenue fields like + * revenueType and event properties. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#tracking-revenue} + * for more information on the Revenue interface and logging revenue. + * @public + * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); + * amplitude.logRevenueV2(revenue); + */ +AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { + if (!this._apiKeySet('logRevenueV2()')) { + return; + } + + // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object + if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) { + revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj); + } + + if (revenue_obj instanceof Revenue) { + // only send if revenue is valid + if (revenue_obj && revenue_obj._isValidRevenue()) { + return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject()); + } + } else { + utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj)); + } +}; + +/** + * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 + * @public + * @deprecated + * @param {number} price - price of revenue event + * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1. + * @param {string} product - (optional) product identifier + * @example amplitudeClient.logRevenue(3.99, 1, 'product_1234'); + */ +AmplitudeClient.prototype.logRevenue = function logRevenue(price, quantity, product) { + // Test that the parameters are of the right type. + if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { + // utils.log('Price and quantity arguments to logRevenue must be numbers'); + return -1; + } + + return this._logEvent(Constants.REVENUE_EVENT, {}, { + productId: product, + special: 'revenue_amount', + quantity: quantity || 1, + price: price + }, null, null, null); +}; + +/** + * Remove events in storage with event ids up to and including maxEventId. + * @private + */ +AmplitudeClient.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { + _removeEvents(this, '_unsentEvents', maxEventId); + _removeEvents(this, '_unsentIdentifys', maxIdentifyId); +}; + +/** + * Helper function to remove events up to maxId from a single queue. + * Does a true filter in case events get out of order or old events are removed. + * @private + */ +var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { + if (maxId < 0) { + return; + } + + var filteredEvents = []; + for (var i = 0; i < scope[eventQueue].length || 0; i++) { + if (scope[eventQueue][i].event_id > maxId) { + filteredEvents.push(scope[eventQueue][i]); + } + } + scope[eventQueue] = filteredEvents; +}; + +/** + * Send unsent events. Note: this is called automatically after events are logged if option batchEvents is false. + * If batchEvents is true, then events are only sent when batch criterias are met. + * @private + * @param {Amplitude~eventCallback} callback - (optional) callback to run after events are sent. + * Note the server response code and response body are passed to the callback as input arguments. + */ +AmplitudeClient.prototype.sendEvents = function sendEvents(callback) { + if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) { + if (type(callback) === 'function') { + callback(0, 'No request sent'); + } + return; + } + + this._sending = true; + var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + this.options.apiEndpoint + '/'; + + // fetch events to send + var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); + var mergedEvents = this._mergeEventsAndIdentifys(numEvents); + var maxEventId = mergedEvents.maxEventId; + var maxIdentifyId = mergedEvents.maxIdentifyId; + var events = JSON.stringify(mergedEvents.eventsToSend); + var uploadTime = new Date().getTime(); + + var data = { + client: this.options.apiKey, + e: events, + v: Constants.API_VERSION, + upload_time: uploadTime, + checksum: md5(Constants.API_VERSION + this.options.apiKey + events + uploadTime) + }; + + var scope = this; + new Request(url, data).send(function(status, response) { + scope._sending = false; + try { + if (status === 200 && response === 'success') { + scope.removeEvents(maxEventId, maxIdentifyId); + + // Update the event cache after the removal of sent events. + if (scope.options.saveEvents) { + scope.saveEvents(); + } + + // Send more events if any queued during previous send. + if (!scope._sendEventsIfReady(callback) && type(callback) === 'function') { + callback(status, response); + } + + // handle payload too large + } else if (status === 413) { + // utils.log('request too large'); + // Can't even get this one massive event through. Drop it, even if it is an identify. + if (scope.options.uploadBatchSize === 1) { + scope.removeEvents(maxEventId, maxIdentifyId); + } + + // The server complained about the length of the request. Backoff and try again. + scope.options.uploadBatchSize = Math.ceil(numEvents / 2); + scope.sendEvents(callback); + + } else if (type(callback) === 'function') { // If server turns something like a 400 + callback(status, response); + } + } catch (e) { + // utils.log('failed upload'); + } + }); +}; + +/** + * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. + * @private + */ +AmplitudeClient.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { + // coalesce events from both queues + var eventsToSend = []; + var eventIndex = 0; + var maxEventId = -1; + var identifyIndex = 0; + var maxIdentifyId = -1; + + while (eventsToSend.length < numEvents) { + var event; + var noIdentifys = identifyIndex >= this._unsentIdentifys.length; + var noEvents = eventIndex >= this._unsentEvents.length; + + // case 0: no events or identifys left + // note this should not happen, this means we have less events and identifys than expected + if (noEvents && noIdentifys) { + utils.log('Merging Events and Identifys, less events and identifys than expected'); + break; + } + + // case 1: no identifys - grab from events + else if (noIdentifys) { + event = this._unsentEvents[eventIndex++]; + maxEventId = event.event_id; + + // case 2: no events - grab from identifys + } else if (noEvents) { + event = this._unsentIdentifys[identifyIndex++]; + maxIdentifyId = event.event_id; + + // case 3: need to compare sequence numbers + } else { + // events logged before v2.5.0 won't have a sequence number, put those first + if (!('sequence_number' in this._unsentEvents[eventIndex]) || + this._unsentEvents[eventIndex].sequence_number < + this._unsentIdentifys[identifyIndex].sequence_number) { + event = this._unsentEvents[eventIndex++]; + maxEventId = event.event_id; + } else { + event = this._unsentIdentifys[identifyIndex++]; + maxIdentifyId = event.event_id; + } + } + + eventsToSend.push(event); + } + + return { + eventsToSend: eventsToSend, + maxEventId: maxEventId, + maxIdentifyId: maxIdentifyId + }; +}; + +/** + * Set global user properties. Note this is deprecated, and we recommend using setUserProperties + * @public + * @deprecated + */ +AmplitudeClient.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { + this.setUserProperties(userProperties); +}; + +/** + * Get the current version of Amplitude's Javascript SDK. + * @public + * @returns {number} version number + * @example var amplitudeVersion = amplitude.__VERSION__; + */ +AmplitudeClient.prototype.__VERSION__ = version; + +module.exports = AmplitudeClient; diff --git a/src/amplitude-snippet.js b/src/amplitude-snippet.js index 12c991fb..e23cab7a 100644 --- a/src/amplitude-snippet.js +++ b/src/amplitude-snippet.js @@ -1,9 +1,9 @@ (function(window, document) { - var amplitude = window.amplitude || {'_q':[]}; + var amplitude = window.amplitude || {'_q':[],'_iq':{}}; var as = document.createElement('script'); as.type = 'text/javascript'; as.async = true; - as.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.13.0-min.gz.js'; + as.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-3.0.0-min.gz.js'; as.onload = function() {window.amplitude.runQueuedFunctions();}; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(as, s); @@ -33,5 +33,12 @@ for (var k = 0; k < funcs.length; k++) {proxyMain(funcs[k]);} } 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 998ae69f..839cd2eb 100644 --- a/src/amplitude.js +++ b/src/amplitude.js @@ -1,48 +1,40 @@ +var AmplitudeClient = require('./amplitude-client'); var Constants = require('./constants'); -var cookieStorage = require('./cookiestorage'); -var getUtmData = require('./utm'); var Identify = require('./identify'); -var JSON = require('json'); // jshint ignore:line -var localStorage = require('./localstorage'); // jshint ignore:line -var md5 = require('JavaScript-MD5'); var object = require('object'); -var Request = require('./xhr'); var Revenue = require('./revenue'); var type = require('./type'); -var UAParser = require('ua-parser-js'); var utils = require('./utils'); -var UUID = require('./uuid'); var version = require('./version'); var DEFAULT_OPTIONS = require('./options'); /** - * Amplitude SDK API - instance constructor. + * Amplitude SDK API - instance manager. + * Function calls directly on amplitude have been deprecated. Please call methods on the default shared instance: amplitude.getInstance() instead. + * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#300-update-and-logging-events-to-multiple-amplitude-apps} for more information about this change. * @constructor Amplitude * @public * @example var amplitude = new Amplitude(); */ var Amplitude = function Amplitude() { - this._unsentEvents = []; - this._unsentIdentifys = []; - this._ua = new UAParser(navigator.userAgent).getResult(); this.options = object.merge({}, DEFAULT_OPTIONS); - this.cookieStorage = new cookieStorage().getStorage(); - this._q = []; // queue for proxied functions before script load - this._sending = false; - this._updateScheduled = false; - - // event meta data - this._eventId = 0; - this._identifyId = 0; - this._lastEventTime = null; - this._newSession = false; - this._sequenceNumber = 0; - this._sessionId = null; + this._q = []; + this._instances = {}; // mapping of instance names to instances }; Amplitude.prototype.Identify = Identify; Amplitude.prototype.Revenue = Revenue; +Amplitude.prototype.getInstance = function getInstance(instance) { + instance = utils.isEmptyString(instance) ? Constants.DEFAULT_INSTANCE : instance.toLowerCase(); + var client = this._instances[instance]; + if (client === undefined) { + client = new AmplitudeClient(instance); + this._instances[instance] = client; + } + return client; +}; + /** * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. * This is required before any other methods can be called. @@ -52,113 +44,17 @@ Amplitude.prototype.Revenue = Revenue; * @param {object} opt_config - (optional) Configuration options. * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#configuration-options} for list of options and default values. * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete. + * @deprecated Please use amplitude.getInstance().init(apiKey, opt_userId, opt_config, opt_callback); * @example amplitude.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); */ Amplitude.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { - if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { - utils.log('Invalid apiKey. Please re-initialize with a valid apiKey'); - return; - } - - try { - this.options.apiKey = apiKey; - _parseConfig(this.options, opt_config); - this.cookieStorage.options({ - expirationDays: this.options.cookieExpiration, - domain: this.options.domain - }); - this.options.domain = this.cookieStorage.options().domain; - - _upgradeCookeData(this); - _loadCookieData(this); - - // load deviceId and userId from input, or try to fetch existing value from cookie - this.options.deviceId = (type(opt_config) === 'object' && type(opt_config.deviceId) === 'string' && - !utils.isEmptyString(opt_config.deviceId) && opt_config.deviceId) || this.options.deviceId || UUID() + 'R'; - this.options.userId = (type(opt_userId) === 'string' && !utils.isEmptyString(opt_userId) && opt_userId) || - this.options.userId || null; - - var now = new Date().getTime(); - if (!this._sessionId || !this._lastEventTime || now - this._lastEventTime > this.options.sessionTimeout) { - this._newSession = true; - this._sessionId = now; - } - this._lastEventTime = now; - _saveCookieData(this); - - if (this.options.saveEvents) { - this._unsentEvents = this._loadSavedUnsentEvents(this.options.unsentKey); - this._unsentIdentifys = this._loadSavedUnsentEvents(this.options.unsentIdentifyKey); - - // validate event properties for unsent events - for (var i = 0; i < this._unsentEvents.length; i++) { - var eventProperties = this._unsentEvents[i].event_properties; - var groups = this._unsentEvents[i].groups; - this._unsentEvents[i].event_properties = utils.validateProperties(eventProperties); - this._unsentEvents[i].groups = utils.validateGroups(groups); - } - - // validate user properties for unsent identifys - for (var j = 0; j < this._unsentIdentifys.length; j++) { - var userProperties = this._unsentIdentifys[j].user_properties; - var identifyGroups = this._unsentIdentifys[j].groups; - this._unsentIdentifys[j].user_properties = utils.validateProperties(userProperties); - this._unsentIdentifys[j].groups = utils.validateGroups(identifyGroups); - } - - this._sendEventsIfReady(); // try sending unsent events - } - - if (this.options.includeUtm) { - this._initUtmData(); - } - - if (this.options.includeReferrer) { - this._saveReferrer(this._getReferrer()); - } - } catch (e) { - utils.log(e); - } finally { + this.getInstance().init(apiKey, opt_userId, opt_config, function(instance) { + // make options such as deviceId available for callback functions + this.options = instance.options; if (type(opt_callback) === 'function') { - opt_callback(); - } - } -}; - -/** - * Parse and validate user specified config values and overwrite existing option value - * DEFAULT_OPTIONS provides list of all config keys that are modifiable, as well as expected types for values - * @private - */ -var _parseConfig = function _parseConfig(options, config) { - if (type(config) !== 'object') { - return; - } - - // validates config value is defined, is the correct type, and some additional value sanity checks - var parseValidateAndLoad = function parseValidateAndLoad(key) { - if (!DEFAULT_OPTIONS.hasOwnProperty(key)) { - return; // skip bogus config values - } - - var inputValue = config[key]; - var expectedType = type(DEFAULT_OPTIONS[key]); - if (!utils.validateInput(inputValue, key + ' option', expectedType)) { - return; - } - if (expectedType === 'boolean') { - options[key] = !!inputValue; - } else if ((expectedType === 'string' && !utils.isEmptyString(inputValue)) || - (expectedType === 'number' && inputValue > 0)) { - options[key] = inputValue; - } - }; - - for (var key in config) { - if (config.hasOwnProperty(key)) { - parseValidateAndLoad(key); + opt_callback(instance); } - } + }.bind(this)); }; /** @@ -166,6 +62,7 @@ var _parseConfig = function _parseConfig(options, config) { * @private */ Amplitude.prototype.runQueuedFunctions = function () { + // run queued up old versions of functions for (var i = 0; i < this._q.length; i++) { var fn = this[this._q[i][0]]; if (type(fn) === 'function') { @@ -173,58 +70,33 @@ Amplitude.prototype.runQueuedFunctions = function () { } } this._q = []; // clear function queue after running -}; -/** - * Check that the apiKey is set before calling a function. Logs a warning message if not set. - * @private - */ -Amplitude.prototype._apiKeySet = function _apiKeySet(methodName) { - if (utils.isEmptyString(this.options.apiKey)) { - utils.log('Invalid apiKey. Please set a valid apiKey with init() before calling ' + methodName); - return false; - } - return true; -}; - -/** - * Load saved events from localStorage. JSON deserializes event array. Handles case where string is corrupted. - * @private - */ -Amplitude.prototype._loadSavedUnsentEvents = function _loadSavedUnsentEvents(unsentKey) { - var savedUnsentEventsString = this._getFromStorage(localStorage, unsentKey); - if (utils.isEmptyString(savedUnsentEventsString)) { - return []; // new app, does not have any saved events - } - - if (type(savedUnsentEventsString) === 'string') { - try { - var events = JSON.parse(savedUnsentEventsString); - if (type(events) === 'array') { // handle case where JSON dumping of unsent events is corrupted - return events; - } - } catch (e) {} + // run queued up functions on instances + for (var instance in this._instances) { + if (this._instances.hasOwnProperty(instance)) { + this._instances[instance].runQueuedFunctions(); + } } - utils.log('Unable to load ' + unsentKey + ' events. Restart with a new empty queue.'); - return []; }; /** * Returns true if a new session was created during initialization, otherwise false. * @public * @return {boolean} Whether a new session was created during initialization. + * @deprecated Please use amplitude.getInstance().isNewSession(); */ Amplitude.prototype.isNewSession = function isNewSession() { - return this._newSession; + return this.getInstance().isNewSession(); }; /** * Returns the id of the current session. * @public * @return {number} Id of the current session. + * @deprecated Please use amplitude.getInstance().getSessionId(); */ Amplitude.prototype.getSessionId = function getSessionId() { - return this._sessionId; + return this.getInstance().getSessionId(); }; /** @@ -232,8 +104,7 @@ Amplitude.prototype.getSessionId = function getSessionId() { * @private */ Amplitude.prototype.nextEventId = function nextEventId() { - this._eventId++; - return this._eventId; + return this.getInstance().nextEventId(); }; /** @@ -241,8 +112,7 @@ Amplitude.prototype.nextEventId = function nextEventId() { * @private */ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { - this._identifyId++; - return this._identifyId; + return this.getInstance().nextIdentifyId(); }; /** @@ -250,257 +120,7 @@ Amplitude.prototype.nextIdentifyId = function nextIdentifyId() { * @private */ Amplitude.prototype.nextSequenceNumber = function nextSequenceNumber() { - this._sequenceNumber++; - return this._sequenceNumber; -}; - -/** - * Returns the total count of unsent events and identifys - * @private - */ -Amplitude.prototype._unsentCount = function _unsentCount() { - return this._unsentEvents.length + this._unsentIdentifys.length; -}; - -/** - * Send events if ready. Returns true if events are sent. - * @private - */ -Amplitude.prototype._sendEventsIfReady = function _sendEventsIfReady(callback) { - if (this._unsentCount() === 0) { - return false; - } - - // if batching disabled, send any unsent events immediately - if (!this.options.batchEvents) { - this.sendEvents(callback); - return true; - } - - // if batching enabled, check if min threshold met for batch size - if (this._unsentCount() >= this.options.eventUploadThreshold) { - this.sendEvents(callback); - return true; - } - - // otherwise schedule an upload after 30s - if (!this._updateScheduled) { // make sure we only schedule 1 upload - this._updateScheduled = true; - setTimeout(function() { - this._updateScheduled = false; - this.sendEvents(); - }.bind(this), this.options.eventUploadPeriodMillis - ); - } - - return false; // an upload was scheduled, no events were uploaded -}; - -/** - * Helper function to fetch values from storage - * Storage argument allows for localStoraoge and sessionStoraoge - * @private - */ -Amplitude.prototype._getFromStorage = function _getFromStorage(storage, key) { - return storage.getItem(key); -}; - -/** - * Helper function to set values in storage - * Storage argument allows for localStoraoge and sessionStoraoge - * @private - */ -Amplitude.prototype._setInStorage = function _setInStorage(storage, key, value) { - storage.setItem(key, value); -}; - -/** - * cookieData (deviceId, userId, optOut, sessionId, lastEventTime, eventId, identifyId, sequenceNumber) - * can be stored in many different places (localStorage, cookie, etc). - * Need to unify all sources into one place with a one-time upgrade/migration. - * @private - */ -var _upgradeCookeData = function _upgradeCookeData(scope) { - // skip if migration already happened - var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (type(cookieData) === 'object' && cookieData.deviceId && cookieData.sessionId && cookieData.lastEventTime) { - return; - } - - var _getAndRemoveFromLocalStorage = function _getAndRemoveFromLocalStorage(key) { - var value = localStorage.getItem(key); - localStorage.removeItem(key); - return value; - }; - - // in v2.6.0, deviceId, userId, optOut was migrated to localStorage with keys + first 6 char of apiKey - var apiKeySuffix = (type(scope.options.apiKey) === 'string' && ('_' + scope.options.apiKey.slice(0, 6))) || ''; - var localStorageDeviceId = _getAndRemoveFromLocalStorage(Constants.DEVICE_ID + apiKeySuffix); - var localStorageUserId = _getAndRemoveFromLocalStorage(Constants.USER_ID + apiKeySuffix); - var localStorageOptOut = _getAndRemoveFromLocalStorage(Constants.OPT_OUT + apiKeySuffix); - if (localStorageOptOut !== null && localStorageOptOut !== undefined) { - localStorageOptOut = String(localStorageOptOut) === 'true'; // convert to boolean - } - - // pre-v2.7.0 event and session meta-data was stored in localStorage. move to cookie for sub-domain support - var localStorageSessionId = parseInt(_getAndRemoveFromLocalStorage(Constants.SESSION_ID)); - var localStorageLastEventTime = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_TIME)); - var localStorageEventId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_EVENT_ID)); - var localStorageIdentifyId = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_IDENTIFY_ID)); - var localStorageSequenceNumber = parseInt(_getAndRemoveFromLocalStorage(Constants.LAST_SEQUENCE_NUMBER)); - - var _getFromCookie = function _getFromCookie(key) { - return type(cookieData) === 'object' && cookieData[key]; - }; - scope.options.deviceId = _getFromCookie('deviceId') || localStorageDeviceId; - scope.options.userId = _getFromCookie('userId') || localStorageUserId; - scope._sessionId = _getFromCookie('sessionId') || localStorageSessionId || scope._sessionId; - scope._lastEventTime = _getFromCookie('lastEventTime') || localStorageLastEventTime || scope._lastEventTime; - scope._eventId = _getFromCookie('eventId') || localStorageEventId || scope._eventId; - scope._identifyId = _getFromCookie('identifyId') || localStorageIdentifyId || scope._identifyId; - scope._sequenceNumber = _getFromCookie('sequenceNumber') || localStorageSequenceNumber || scope._sequenceNumber; - - // optOut is a little trickier since it is a boolean - scope.options.optOut = localStorageOptOut || false; - if (cookieData && cookieData.optOut !== undefined && cookieData.optOut !== null) { - scope.options.optOut = String(cookieData.optOut) === 'true'; - } - - _saveCookieData(scope); -}; - -/** - * Fetches deviceId, userId, event meta data from amplitude cookie - * @private - */ -var _loadCookieData = function _loadCookieData(scope) { - var cookieData = scope.cookieStorage.get(scope.options.cookieName); - if (type(cookieData) === 'object') { - if (cookieData.deviceId) { - scope.options.deviceId = cookieData.deviceId; - } - if (cookieData.userId) { - scope.options.userId = cookieData.userId; - } - if (cookieData.optOut !== null && cookieData.optOut !== undefined) { - scope.options.optOut = cookieData.optOut; - } - if (cookieData.sessionId) { - scope._sessionId = parseInt(cookieData.sessionId); - } - if (cookieData.lastEventTime) { - scope._lastEventTime = parseInt(cookieData.lastEventTime); - } - if (cookieData.eventId) { - scope._eventId = parseInt(cookieData.eventId); - } - if (cookieData.identifyId) { - scope._identifyId = parseInt(cookieData.identifyId); - } - if (cookieData.sequenceNumber) { - scope._sequenceNumber = parseInt(cookieData.sequenceNumber); - } - } -}; - -/** - * Saves deviceId, userId, event meta data to amplitude cookie - * @private - */ -var _saveCookieData = function _saveCookieData(scope) { - scope.cookieStorage.set(scope.options.cookieName, { - deviceId: scope.options.deviceId, - userId: scope.options.userId, - optOut: scope.options.optOut, - sessionId: scope._sessionId, - lastEventTime: scope._lastEventTime, - eventId: scope._eventId, - identifyId: scope._identifyId, - sequenceNumber: scope._sequenceNumber - }); -}; - -/** - * Parse the utm properties out of cookies and query for adding to user properties. - * @private - */ -Amplitude.prototype._initUtmData = function _initUtmData(queryParams, cookieParams) { - queryParams = queryParams || location.search; - cookieParams = cookieParams || this.cookieStorage.get('__utmz'); - var utmProperties = getUtmData(cookieParams, queryParams); - _sendUserPropertiesOncePerSession(this, Constants.UTM_PROPERTIES, utmProperties); -}; - -/** - * Since user properties are propagated on server, only send once per session, don't need to send with every event - * @private - */ -var _sendUserPropertiesOncePerSession = function _sendUserPropertiesOncePerSession(scope, storageKey, userProperties) { - if (type(userProperties) !== 'object' || Object.keys(userProperties).length === 0) { - return; - } - - // setOnce the initial user properties - var identify = new Identify(); - for (var key in userProperties) { - if (userProperties.hasOwnProperty(key)) { - identify.setOnce('initial_' + key, userProperties[key]); - } - } - - // only save userProperties if not already in sessionStorage under key or if storage disabled - var hasSessionStorage = utils.sessionStorageEnabled(); - if ((hasSessionStorage && !(scope._getFromStorage(sessionStorage, storageKey))) || !hasSessionStorage) { - for (var property in userProperties) { - if (userProperties.hasOwnProperty(property)) { - identify.set(property, userProperties[property]); - } - } - - if (hasSessionStorage) { - scope._setInStorage(sessionStorage, storageKey, JSON.stringify(userProperties)); - } - } - - scope.identify(identify); -}; - -/** - * @private - */ -Amplitude.prototype._getReferrer = function _getReferrer() { - return document.referrer; -}; - -/** - * Parse the domain from referrer info - * @private - */ -Amplitude.prototype._getReferringDomain = function _getReferringDomain(referrer) { - if (utils.isEmptyString(referrer)) { - return null; - } - var parts = referrer.split('/'); - if (parts.length >= 3) { - return parts[2]; - } - return null; -}; - -/** - * Fetch the referrer information, parse the domain and send. - * Since user properties are propagated on the server, only send once per session, don't need to send with every event - * @private - */ -Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { - if (utils.isEmptyString(referrer)) { - return; - } - var referrerInfo = { - 'referrer': referrer, - 'referring_domain': this._getReferringDomain(referrer) - }; - _sendUserPropertiesOncePerSession(this, Constants.REFERRER, referrerInfo); + return this.getInstance().nextSequenceNumber(); }; /** @@ -509,51 +129,29 @@ Amplitude.prototype._saveReferrer = function _saveReferrer(referrer) { * @private */ Amplitude.prototype.saveEvents = function saveEvents() { - try { - this._setInStorage(localStorage, this.options.unsentKey, JSON.stringify(this._unsentEvents)); - } catch (e) {} - - try { - this._setInStorage(localStorage, this.options.unsentIdentifyKey, JSON.stringify(this._unsentIdentifys)); - } catch (e) {} + this.getInstance().saveEvents(); }; /** * Sets a customer domain for the amplitude cookie. Useful if you want to support cross-subdomain tracking. * @public * @param {string} domain to set. + * @deprecated Please use amplitude.getInstance().setDomain(domain); * @example amplitude.setDomain('.amplitude.com'); */ Amplitude.prototype.setDomain = function setDomain(domain) { - if (!utils.validateInput(domain, 'domain', 'string')) { - return; - } - - try { - this.cookieStorage.options({ - domain: domain - }); - this.options.domain = this.cookieStorage.options().domain; - _loadCookieData(this); - _saveCookieData(this); - } catch (e) { - utils.log(e); - } + this.getInstance().setDomain(domain); }; /** * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. + * @deprecated Please use amplitude.getInstance().setUserId(userId); * @example amplitude.setUserId('joe@gmail.com'); */ Amplitude.prototype.setUserId = function setUserId(userId) { - try { - this.options.userId = (userId !== undefined && userId !== null && ('' + userId)) || null; - _saveCookieData(this); - } catch (e) { - utils.log(e); - } + this.getInstance().setUserId(userId); }; /** @@ -567,48 +165,34 @@ Amplitude.prototype.setUserId = function setUserId(userId) { * @public * @param {string} groupType - the group type (ex: orgId) * @param {string|list} groupName - the name of the group (ex: 15), or a list of names of the groups + * @deprecated Please use amplitude.getInstance().setGroup(groupType, groupName); * @example amplitude.setGroup('orgId', 15); // this adds the current user to orgId 15. */ Amplitude.prototype.setGroup = function(groupType, groupName) { - if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || - utils.isEmptyString(groupType)) { - return; - } - - var groups = {}; - groups[groupType] = groupName; - var identify = new Identify().set(groupType, groupName); - this._logEvent(Constants.IDENTIFY_EVENT, null, null, identify.userPropertiesOperations, groups, null); + this.getInstance().setGroup(groupType, groupName); }; /** * Sets whether to opt current user out of tracking. * @public * @param {boolean} enable - if true then no events will be logged or sent. + * @deprecated Please use amplitude.getInstance().setOptOut(enable); * @example: amplitude.setOptOut(true); */ Amplitude.prototype.setOptOut = function setOptOut(enable) { - if (!utils.validateInput(enable, 'enable', 'boolean')) { - return; - } - - try { - this.options.optOut = enable; - _saveCookieData(this); - } catch (e) { - utils.log(e); - } + this.getInstance().setOptOut(enable); }; /** - * Regenerates a new random deviceId for current user. Note: this is not recommended unless you konw what you + * Regenerates a new random deviceId for current user. Note: this is not recommended unless you know what you * are doing. This can be used in conjunction with `setUserId(null)` to anonymize users after they log out. * With a null userId and a completely new deviceId, the current user would appear as a brand new user in dashboard. * This uses src/uuid.js to regenerate the deviceId. * @public + * @deprecated Please use amplitude.getInstance().regenerateDeviceId(); */ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { - this.setDeviceId(UUID() + 'R'); + this.getInstance().regenerateDeviceId(); }; /** @@ -617,21 +201,11 @@ Amplitude.prototype.regenerateDeviceId = function regenerateDeviceId() { * (we recommend something like a UUID - see src/uuid.js for an example of how to generate) to prevent conflicts with other devices in our system. * @public * @param {string} deviceId - custom deviceId for current user. + * @deprecated Please use amplitude.getInstance().setDeviceId(deviceId); * @example amplitude.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); */ Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { - if (!utils.validateInput(deviceId, 'deviceId', 'string')) { - return; - } - - try { - if (!utils.isEmptyString(deviceId)) { - this.options.deviceId = ('' + deviceId); - _saveCookieData(this); - } - } catch (e) { - utils.log(e); - } + this.getInstance().setDeviceId(deviceId); }; /** @@ -640,50 +214,21 @@ Amplitude.prototype.setDeviceId = function setDeviceId(deviceId) { * @param {object} - object with string keys and values for the user properties to set. * @param {boolean} - DEPRECATED opt_replace: in earlier versions of the JS SDK the user properties object was kept in * memory and replace = true would replace the object in memory. Now the properties are no longer stored in memory, so replace is deprecated. + * @deprecated Please use amplitude.getInstance.setUserProperties(userProperties); * @example amplitude.setUserProperties({'gender': 'female', 'sign_up_complete': true}) */ Amplitude.prototype.setUserProperties = function setUserProperties(userProperties) { - if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { - return; - } - // convert userProperties into an identify call - var identify = new Identify(); - for (var property in userProperties) { - if (userProperties.hasOwnProperty(property)) { - identify.set(property, userProperties[property]); - } - } - this.identify(identify); + this.getInstance().setUserProperties(userProperties); }; /** * Clear all of the user properties for the current user. Note: clearing user properties is irreversible! * @public + * @deprecated Please use amplitude.getInstance().clearUserProperties(); * @example amplitude.clearUserProperties(); */ Amplitude.prototype.clearUserProperties = function clearUserProperties(){ - if (!this._apiKeySet('clearUserProperties()')) { - return; - } - - var identify = new Identify(); - identify.clearAll(); - this.identify(identify); -}; - -/** - * Applies the proxied functions on the proxied object to an instance of the real object. - * Used to convert proxied Identify and Revenue objects. - * @private - */ -var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(instance, proxy) { - for (var i = 0; i < proxy._q.length; i++) { - var fn = instance[proxy._q[i][0]]; - if (type(fn) === 'function') { - fn.apply(instance, proxy._q[i].slice(1)); - } - } - return instance; + this.getInstance().clearUserProperties(); }; /** @@ -693,140 +238,24 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i * @param {Identify} identify_obj - the Identify object containing the user property operations to send. * @param {Amplitude~eventCallback} opt_callback - (optional) callback function to run when the identify event has been sent. * Note: the server response code and response body from the identify event upload are passed to the callback function. + * @deprecated Please use amplitude.getInstance().identify(identify); * @example * var identify = new amplitude.Identify().set('colors', ['rose', 'gold']).add('karma', 1).setOnce('sign_up_date', '2016-03-31'); * amplitude.identify(identify); */ Amplitude.prototype.identify = function(identify_obj, opt_callback) { - if (!this._apiKeySet('identify()')) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return; - } - - // if identify input is a proxied object created by the async loading snippet, convert it into an identify object - if (type(identify_obj) === 'object' && identify_obj.hasOwnProperty('_q')) { - identify_obj = _convertProxyObjectToRealObject(new Identify(), identify_obj); - } - - if (identify_obj instanceof Identify) { - // only send if there are operations - if (Object.keys(identify_obj.userPropertiesOperations).length > 0) { - return this._logEvent( - Constants.IDENTIFY_EVENT, null, null, identify_obj.userPropertiesOperations, null, opt_callback - ); - } - } else { - utils.log('Invalid identify input type. Expected Identify object but saw ' + type(identify_obj)); - } - - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } + this.getInstance().identify(identify_obj, opt_callback); }; /** * Set a versionName for your application. * @public * @param {string} versionName - The version to set for your application. + * @deprecated Please use amplitude.getInstance().setVersionName(versionName); * @example amplitude.setVersionName('1.12.3'); */ Amplitude.prototype.setVersionName = function setVersionName(versionName) { - if (!utils.validateInput(versionName, 'versionName', 'string')) { - return; - } - this.options.versionName = versionName; -}; - -/** - * Private logEvent method. Keeps apiProperties from being publicly exposed. - * @private - */ -Amplitude.prototype._logEvent = function _logEvent(eventType, eventProperties, apiProperties, userProperties, groups, callback) { - _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs - if (!eventType || this.options.optOut) { - if (type(callback) === 'function') { - callback(0, 'No request sent'); - } - return; - } - - try { - var eventId; - if (eventType === Constants.IDENTIFY_EVENT) { - eventId = this.nextIdentifyId(); - } else { - eventId = this.nextEventId(); - } - var sequenceNumber = this.nextSequenceNumber(); - var eventTime = new Date().getTime(); - if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { - this._sessionId = eventTime; - } - this._lastEventTime = eventTime; - _saveCookieData(this); - - userProperties = userProperties || {}; - apiProperties = apiProperties || {}; - eventProperties = eventProperties || {}; - groups = groups || {}; - var event = { - device_id: this.options.deviceId, - user_id: this.options.userId, - timestamp: eventTime, - event_id: eventId, - session_id: this._sessionId || -1, - event_type: eventType, - version_name: this.options.versionName || null, - platform: this.options.platform, - os_name: this._ua.browser.name || null, - os_version: this._ua.browser.major || null, - device_model: this._ua.os.name || null, - language: this.options.language, - api_properties: apiProperties, - event_properties: utils.truncate(utils.validateProperties(eventProperties)), - user_properties: utils.truncate(utils.validateProperties(userProperties)), - uuid: UUID(), - library: { - name: 'amplitude-js', - version: version - }, - sequence_number: sequenceNumber, // for ordering events and identifys - groups: utils.truncate(utils.validateGroups(groups)) - // country: null - }; - - if (eventType === Constants.IDENTIFY_EVENT) { - this._unsentIdentifys.push(event); - this._limitEventsQueued(this._unsentIdentifys); - } else { - this._unsentEvents.push(event); - this._limitEventsQueued(this._unsentEvents); - } - - if (this.options.saveEvents) { - this.saveEvents(); - } - - if (!this._sendEventsIfReady(callback) && type(callback) === 'function') { - callback(0, 'No request sent'); - } - - return eventId; - } catch (e) { - utils.log(e); - } -}; - -/** - * Remove old events from the beginning of the array if too many have accumulated. Default limit is 1000 events. - * @private - */ -Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { - if (queue.length > this.options.savedMaxCount) { - queue.splice(0, queue.length - this.options.savedMaxCount); - } + this.getInstance().setVersionName(versionName); }; /** @@ -844,17 +273,11 @@ Amplitude.prototype._limitEventsQueued = function _limitEventsQueued(queue) { * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. * Note: the server response code and response body from the event upload are passed to the callback function. + * @deprecated Please use amplitude.getInstance().logEvent(eventType, eventProperties, opt_callback); * @example amplitude.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { - if (!this._apiKeySet('logEvent()') || !utils.validateInput(eventType, 'eventType', 'string') || - utils.isEmptyString(eventType)) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, null, opt_callback); + return this.getInstance().logEvent(eventType, eventProperties, opt_callback); }; /** @@ -870,25 +293,11 @@ Amplitude.prototype.logEvent = function logEvent(eventType, eventProperties, opt * groupName can be a string or an array of strings. * @param {Amplitude~eventCallback} opt_callback - (optional) a callback function to run after the event is logged. * Note: the server response code and response body from the event upload are passed to the callback function. + * Deprecated Please use amplitude.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); * @example amplitude.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ Amplitude.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { - if (!this._apiKeySet('logEventWithGroup()') || - !utils.validateInput(eventType, 'eventType', 'string')) { - if (type(opt_callback) === 'function') { - opt_callback(0, 'No request sent'); - } - return -1; - } - return this._logEvent(eventType, eventProperties, null, null, groups, opt_callback); -}; - -/** - * Test that n is a number or a numeric value. - * @private - */ -var _isNumber = function _isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); + return this.getInstance().logEventWithGroups(eventType, eventProperties, groups, opt_callback); }; /** @@ -898,51 +307,25 @@ var _isNumber = function _isNumber(n) { * for more information on the Revenue interface and logging revenue. * @public * @param {Revenue} revenue_obj - the revenue object containing the revenue data being logged. + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ Amplitude.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { - if (!this._apiKeySet('logRevenueV2()')) { - return; - } - - // if revenue input is a proxied object created by the async loading snippet, convert it into an revenue object - if (type(revenue_obj) === 'object' && revenue_obj.hasOwnProperty('_q')) { - revenue_obj = _convertProxyObjectToRealObject(new Revenue(), revenue_obj); - } - - if (revenue_obj instanceof Revenue) { - // only send if revenue is valid - if (revenue_obj && revenue_obj._isValidRevenue()) { - return this.logEvent(Constants.REVENUE_EVENT, revenue_obj._toJSONObject()); - } - } else { - utils.log('Invalid revenue input type. Expected Revenue object but saw ' + type(revenue_obj)); - } + return this.getInstance().logRevenueV2(revenue_obj); }; /** * Log revenue event with a price, quantity, and product identifier. DEPRECATED - use logRevenueV2 * @public - * @deprecated * @param {number} price - price of revenue event * @param {number} quantity - (optional) quantity of products in revenue event. If no quantity specified default to 1. * @param {string} product - (optional) product identifier + * @deprecated Please use amplitude.getInstance().logRevenueV2(revenue_obj); * @example amplitude.logRevenue(3.99, 1, 'product_1234'); */ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { - // Test that the parameters are of the right type. - if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { - // utils.log('Price and quantity arguments to logRevenue must be numbers'); - return -1; - } - - return this._logEvent(Constants.REVENUE_EVENT, {}, { - productId: product, - special: 'revenue_amount', - quantity: quantity || 1, - price: price - }, null, null, null); + return this.getInstance().logRevenue(price, quantity, product); }; /** @@ -950,27 +333,7 @@ Amplitude.prototype.logRevenue = function logRevenue(price, quantity, product) { * @private */ Amplitude.prototype.removeEvents = function removeEvents(maxEventId, maxIdentifyId) { - _removeEvents(this, '_unsentEvents', maxEventId); - _removeEvents(this, '_unsentIdentifys', maxIdentifyId); -}; - -/** - * Helper function to remove events up to maxId from a single queue. - * Does a true filter in case events get out of order or old events are removed. - * @private - */ -var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { - if (maxId < 0) { - return; - } - - var filteredEvents = []; - for (var i = 0; i < scope[eventQueue].length || 0; i++) { - if (scope[eventQueue][i].event_id > maxId) { - filteredEvents.push(scope[eventQueue][i]); - } - } - scope[eventQueue] = filteredEvents; + this.getInstance().removeEvents(maxEventId, maxIdentifyId); }; /** @@ -981,126 +344,7 @@ var _removeEvents = function _removeEvents(scope, eventQueue, maxId) { * Note the server response code and response body are passed to the callback as input arguments. */ Amplitude.prototype.sendEvents = function sendEvents(callback) { - if (!this._apiKeySet('sendEvents()') || this._sending || this.options.optOut || this._unsentCount() === 0) { - if (type(callback) === 'function') { - callback(0, 'No request sent'); - } - return; - } - - this._sending = true; - var url = ('https:' === window.location.protocol ? 'https' : 'http') + '://' + this.options.apiEndpoint + '/'; - - // fetch events to send - var numEvents = Math.min(this._unsentCount(), this.options.uploadBatchSize); - var mergedEvents = this._mergeEventsAndIdentifys(numEvents); - var maxEventId = mergedEvents.maxEventId; - var maxIdentifyId = mergedEvents.maxIdentifyId; - var events = JSON.stringify(mergedEvents.eventsToSend); - var uploadTime = new Date().getTime(); - - var data = { - client: this.options.apiKey, - e: events, - v: Constants.API_VERSION, - upload_time: uploadTime, - checksum: md5(Constants.API_VERSION + this.options.apiKey + events + uploadTime) - }; - - var scope = this; - new Request(url, data).send(function(status, response) { - scope._sending = false; - try { - if (status === 200 && response === 'success') { - scope.removeEvents(maxEventId, maxIdentifyId); - - // Update the event cache after the removal of sent events. - if (scope.options.saveEvents) { - scope.saveEvents(); - } - - // Send more events if any queued during previous send. - if (!scope._sendEventsIfReady(callback) && type(callback) === 'function') { - callback(status, response); - } - - // handle payload too large - } else if (status === 413) { - // utils.log('request too large'); - // Can't even get this one massive event through. Drop it, even if it is an identify. - if (scope.options.uploadBatchSize === 1) { - scope.removeEvents(maxEventId, maxIdentifyId); - } - - // The server complained about the length of the request. Backoff and try again. - scope.options.uploadBatchSize = Math.ceil(numEvents / 2); - scope.sendEvents(callback); - - } else if (type(callback) === 'function') { // If server turns something like a 400 - callback(status, response); - } - } catch (e) { - // utils.log('failed upload'); - } - }); -}; - -/** - * Merge unsent events and identifys together in sequential order based on their sequence number, for uploading. - * @private - */ -Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys(numEvents) { - // coalesce events from both queues - var eventsToSend = []; - var eventIndex = 0; - var maxEventId = -1; - var identifyIndex = 0; - var maxIdentifyId = -1; - - while (eventsToSend.length < numEvents) { - var event; - var noIdentifys = identifyIndex >= this._unsentIdentifys.length; - var noEvents = eventIndex >= this._unsentEvents.length; - - // case 0: no events or identifys left - // note this should not happen, this means we have less events and identifys than expected - if (noEvents && noIdentifys) { - utils.log('Merging Events and Identifys, less events and identifys than expected'); - break; - } - - // case 1: no identifys - grab from events - else if (noIdentifys) { - event = this._unsentEvents[eventIndex++]; - maxEventId = event.event_id; - - // case 2: no events - grab from identifys - } else if (noEvents) { - event = this._unsentIdentifys[identifyIndex++]; - maxIdentifyId = event.event_id; - - // case 3: need to compare sequence numbers - } else { - // events logged before v2.5.0 won't have a sequence number, put those first - if (!('sequence_number' in this._unsentEvents[eventIndex]) || - this._unsentEvents[eventIndex].sequence_number < - this._unsentIdentifys[identifyIndex].sequence_number) { - event = this._unsentEvents[eventIndex++]; - maxEventId = event.event_id; - } else { - event = this._unsentIdentifys[identifyIndex++]; - maxIdentifyId = event.event_id; - } - } - - eventsToSend.push(event); - } - - return { - eventsToSend: eventsToSend, - maxEventId: maxEventId, - maxIdentifyId: maxIdentifyId - }; + this.getInstance().sendEvents(callback); }; /** @@ -1109,7 +353,7 @@ Amplitude.prototype._mergeEventsAndIdentifys = function _mergeEventsAndIdentifys * @deprecated */ Amplitude.prototype.setGlobalUserProperties = function setGlobalUserProperties(userProperties) { - this.setUserProperties(userProperties); + this.getInstance().setUserProperties(userProperties); }; /** diff --git a/src/constants.js b/src/constants.js index 7072e5a3..172dc061 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,4 +1,5 @@ module.exports = { + DEFAULT_INSTANCE: '$default_instance', API_VERSION: 2, MAX_STRING_LENGTH: 4096, IDENTIFY_EVENT: '$identify', diff --git a/src/index.js b/src/index.js index c5cab082..e0835fe1 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,13 @@ var Amplitude = require('./amplitude'); var old = window.amplitude || {}; -var instance = new Amplitude(); -instance._q = old._q || []; +var newInstance = new Amplitude(); +newInstance._q = old._q || []; +for (var instance in old._iq) { // migrate each instance's queue + if (old._iq.hasOwnProperty(instance)) { + newInstance.getInstance(instance)._q = old._iq[instance]._q || []; + } +} // export the instance -module.exports = instance; +module.exports = newInstance; diff --git a/src/version.js b/src/version.js index ec89c0bd..07b03858 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -module.exports = '2.13.0'; +module.exports = '3.0.0'; diff --git a/test/amplitude-client.js b/test/amplitude-client.js new file mode 100644 index 00000000..b6cebb68 --- /dev/null +++ b/test/amplitude-client.js @@ -0,0 +1,2505 @@ +// maintain for testing backwards compatability +describe('AmplitudeClient', function() { + var AmplitudeClient = require('../src/amplitude-client.js'); + var getUtmData = require('../src/utm.js'); + var localStorage = require('../src/localstorage.js'); + var CookieStorage = require('../src/cookiestorage.js'); + var Base64 = require('../src/base64.js'); + var cookie = require('../src/cookie.js'); + var utils = require('../src/utils.js'); + var querystring = require('querystring'); + var JSON = require('json'); + var Identify = require('../src/identify.js'); + var Revenue = require('../src/revenue.js'); + var apiKey = '000000'; + var keySuffix = '_' + apiKey.slice(0,6); + var userId = 'user'; + var amplitude; + var server; + + beforeEach(function() { + amplitude = new 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-insensitive', function() { + assert.equal(new AmplitudeClient('APP3')._instanceName, 'app3'); + assert.equal(new AmplitudeClient('$DEFAULT_INSTANCE')._instanceName, '$default_instance'); + }); + + it('fails on invalid apiKeys', function() { + amplitude.init(null); + assert.equal(amplitude.options.apiKey, undefined); + assert.equal(amplitude.options.deviceId, undefined); + + amplitude.init(''); + assert.equal(amplitude.options.apiKey, undefined); + assert.equal(amplitude.options.deviceId, undefined); + + amplitude.init(apiKey); + assert.equal(amplitude.options.apiKey, apiKey); + assert.lengthOf(amplitude.options.deviceId, 37); + }); + + it('should accept userId', function() { + amplitude.init(apiKey, userId); + assert.equal(amplitude.options.userId, userId); + }); + + it('should generate a random deviceId', function() { + amplitude.init(apiKey, userId); + assert.lengthOf(amplitude.options.deviceId, 37); // UUID is length 36, but we append 'R' at end + assert.equal(amplitude.options.deviceId[36], 'R'); + }); + + it('should validate config values', function() { + var config = { + apiEndpoint: 100, // invalid type + batchEvents: 'True', // invalid type + cookieExpiration: -1, // negative number + cookieName: '', // empty string + eventUploadPeriodMillis: '30', // 30s + eventUploadThreshold: 0, // zero value + bogusKey: false + }; + + amplitude.init(apiKey, userId, config); + assert.equal(amplitude.options.apiEndpoint, 'api.amplitude.com'); + assert.equal(amplitude.options.batchEvents, false); + assert.equal(amplitude.options.cookieExpiration, 3650); + assert.equal(amplitude.options.cookieName, 'amplitude_id'); + assert.equal(amplitude.options.eventUploadPeriodMillis, 30000); + assert.equal(amplitude.options.eventUploadThreshold, 30); + assert.equal(amplitude.options.bogusKey, undefined); + }); + + it('should set cookie', function() { + amplitude.init(apiKey, userId); + var stored = cookie.get(amplitude.options.cookieName); + assert.property(stored, 'deviceId'); + assert.propertyVal(stored, 'userId', userId); + assert.lengthOf(stored.deviceId, 37); // increase deviceId length by 1 for 'R' character + }); + + it('should set language', function() { + amplitude.init(apiKey, userId); + assert.property(amplitude.options, 'language'); + assert.isNotNull(amplitude.options.language); + }); + + it('should allow language override', function() { + amplitude.init(apiKey, userId, {language: 'en-GB'}); + assert.propertyVal(amplitude.options, 'language', 'en-GB'); + }); + + it ('should not run callback if invalid callback', function() { + amplitude.init(apiKey, userId, null, 'invalid callback'); + }); + + it ('should run valid callbacks', function() { + var counter = 0; + var callback = function() { + counter++; + }; + amplitude.init(apiKey, userId, null, callback); + assert.equal(counter, 1); + }); + + it ('should migrate deviceId, userId, optOut from localStorage to cookie 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 = 'testDeviceId'; + var userId = 'test_user_id'; + + assert.isNull(cookie.get(amplitude.options.cookieName)); + localStorage.setItem('amplitude_deviceId' + keySuffix, deviceId); + localStorage.setItem('amplitude_userId' + keySuffix, userId); + localStorage.setItem('amplitude_optOut' + keySuffix, true); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey); + assert.notEqual(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, "groups":{}}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131, "groups":{}}]'; + localStorage.setItem('amplitude_unsent', existingEvent); + localStorage.setItem('amplitude_unsent_identify', existingIdentify); + + var amplitude2 = new 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 for non-default instances', function() { + var existingEvent = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769146589,' + + '"event_id":49,"session_id":1453763315544,"event_type":"clicked","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130, "groups":{}}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131, "groups":{}}]'; + localStorage.setItem('amplitude_unsent_new_app', existingEvent); + localStorage.setItem('amplitude_unsent_identify_new_app', existingIdentify); + assert.isNull(localStorage.getItem('amplitude_unsent')); + assert.isNull(localStorage.getItem('amplitude_unsent_identify')); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey, null, {batchEvents: true}); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, JSON.parse(existingEvent)); + assert.deepEqual(amplitude2._unsentIdentifys, JSON.parse(existingIdentify)); + + // check local storage keys are still same + assert.equal(localStorage.getItem('amplitude_unsent_new_app'), existingEvent); + assert.equal(localStorage.getItem('amplitude_unsent_identify_new_app'), existingIdentify); + }); + + it('should validate event properties when loading saved events from localStorage', function() { + var existingEvents = '[{"device_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d' + + '-2352191877e6","timestamp":1455744744413,"event_id":2,"session_id":1455744733865,"event_type":"clicked",' + + '"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 validate user properties when loading saved identifys from localStorage', function() { + var existingEvents = '[{"device_id":"15a82a' + + 'aa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","timestamp":1455744746295,' + + '"event_id":3,"session_id":1455744733865,"event_type":"$identify","version_name":"Web","platform":"Web",' + + '"os_name":"Chrome","os_version":"48","device_model":"Mac","language":"en-US","api_properties":{},' + + '"user_properties":{"$set":{"10":"false","bool":true,"null":null,"string":"test","array":' + + '[0,1,2,"3"],"nested_array":["a",{"key":"value"},["b"]],"object":{"key":"value"},"nested_object":' + + '{"k":"v","l":[0,1],"o":{"k2":"v2","l2":["e2",{"k3":"v3"}]}}}},"event_properties":{},"uuid":"650407a1-d705-' + + '47a0-8918-b4530ce51f89","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number":5}]' + localStorage.setItem('amplitude_unsent_identify', existingEvents); + + var amplitude2 = new AmplitudeClient(); + amplitude2.init(apiKey, null, {batchEvents: true}); + + var expected = { + '10': 'false', + 'bool': true, + 'string': 'test', + 'array': [0, 1, 2, '3'], + 'nested_array': ['a'], + 'object': {'key':'value'}, + 'nested_object': {'k':'v', 'l':[0,1], 'o':{'k2':'v2', 'l2': ['e2']}} + } + + // check that event loaded into memory + assert.deepEqual(amplitude2._unsentIdentifys[0].user_properties, {'$set': expected}); + }); + + it ('should load saved events from localStorage and send events for default instance', function() { + var existingEvent = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769146589,' + + '"event_id":49,"session_id":1453763315544,"event_type":"clicked","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"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(); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + server.respondWith('success'); + server.respond(); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2._unsentIdentifys, []); + + // check local storage keys are still same + assert.equal(localStorage.getItem('amplitude_unsent'), JSON.stringify([])); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), JSON.stringify([])); + + // check request + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + assert.equal(events[0].event_id, 49); + assert.equal(events[1].event_type, '$identify'); + }); + +it ('should load saved events from localStorage new keys and send events', function() { + var existingEvent = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769146589,' + + '"event_id":49,"session_id":1453763315544,"event_type":"clicked","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131}]'; + localStorage.setItem('amplitude_unsent_new_app', existingEvent); + localStorage.setItem('amplitude_unsent_identify_new_app', existingIdentify); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + server.respondWith('success'); + server.respond(); + + // check event loaded into memory + assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2._unsentIdentifys, []); + + // check local storage keys are still same + assert.equal(localStorage.getItem('amplitude_unsent_new_app'), JSON.stringify([])); + assert.equal(localStorage.getItem('amplitude_unsent_identify_new_app'), JSON.stringify([])); + + // check request + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + assert.equal(events[0].event_id, 49); + assert.equal(events[1].event_type, '$identify'); + }); + + it('should validate event properties when loading saved events from localStorage', function() { + var existingEvents = '[{"device_id":"15a82aaa-0d9e-4083-a32d-2352191877e6","user_id":"15a82aaa-0d9e-4083-a32d' + + '-2352191877e6","timestamp":1455744744413,"event_id":2,"session_id":1455744733865,"event_type":"clicked",' + + '"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(); + 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 not load saved events from another instances\'s localStorage', function() { + var existingEvent = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769146589,' + + '"event_id":49,"session_id":1453763315544,"event_type":"clicked","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{},"uuid":"3c508faa-a5c9-45fa-9da7-9f4f3b992fb0","library"' + + ':{"name":"amplitude-js","version":"2.9.0"},"sequence_number":130}]'; + var existingIdentify = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769338995,' + + '"event_id":82,"session_id":1453763315544,"event_type":"$identify","version_name":"Web","platform":"Web"' + + ',"os_name":"Chrome","os_version":"47","device_model":"Mac","language":"en-US","api_properties":{},' + + '"event_properties":{},"user_properties":{"$set":{"age":30,"city":"San Francisco, CA"}},"uuid":"' + + 'c50e1be4-7976-436a-aa25-d9ee38951082","library":{"name":"amplitude-js","version":"2.9.0"},"sequence_number"' + + ':131}]'; + localStorage.setItem('amplitude_unsent', existingEvent); + localStorage.setItem('amplitude_unsent_identify', existingIdentify); + assert.isNull(localStorage.getItem('amplitude_unsent_new_app')); + assert.isNull(localStorage.getItem('amplitude_unsent_identify_new_app')); + + var amplitude2 = new AmplitudeClient('new_app'); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + + // check events not loaded into memory + assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2._unsentIdentifys, []); + + // check local storage + assert.equal(localStorage.getItem('amplitude_unsent'), existingEvent); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), existingIdentify); + assert.isNull(localStorage.getItem('amplitude_unsent_new_app')); + assert.isNull(localStorage.getItem('amplitude_unsent_identify_new_app')); + + // check request + assert.lengthOf(server.requests, 0); + }); + }); + + describe('runQueuedFunctions', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should run queued functions', function() { + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(server.requests, 0); + var userId = 'testUserId' + var eventType = 'test_event' + var functions = [ + ['setUserId', userId], + ['logEvent', eventType] + ]; + amplitude._q = functions; + assert.lengthOf(amplitude._q, 2); + amplitude.runQueuedFunctions(); + + assert.equal(amplitude.options.userId, userId); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, eventType); + + assert.lengthOf(amplitude._q, 0); + }); + }); + + describe('setUserProperties', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should log identify call from set user properties', function() { + assert.equal(amplitude._unsentCount(), 0); + amplitude.setUserProperties({'prop': true, 'key': 'value'}); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + + var expected = { + '$set': { + 'prop': true, + 'key': 'value' + } + }; + assert.deepEqual(events[0].user_properties, expected); + }); + }); + + describe('clearUserProperties', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should log identify call from clear user properties', function() { + assert.equal(amplitude._unsentCount(), 0); + amplitude.clearUserProperties(); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + + var expected = { + '$clearAll': '-' + }; + assert.deepEqual(events[0].user_properties, expected); + }); + }); + + describe('setGroup', function() { + beforeEach(function() { + reset(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should generate an identify event with groups set', function() { + amplitude.setGroup('orgId', 15); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + + // verify identify event + var identify = events[0]; + assert.equal(identify.event_type, '$identify'); + assert.deepEqual(identify.user_properties, { + '$set': {'orgId': 15}, + }); + assert.deepEqual(identify.event_properties, {}); + assert.deepEqual(identify.groups, { + 'orgId': '15', + }); + }); + + it('should ignore empty string groupTypes', function() { + amplitude.setGroup('', 15); + assert.lengthOf(server.requests, 0); + }); + + it('should ignore non-string groupTypes', function() { + amplitude.setGroup(10, 10); + amplitude.setGroup([], 15); + amplitude.setGroup({}, 20); + amplitude.setGroup(true, false); + assert.lengthOf(server.requests, 0); + }); + }); + + +describe('setVersionName', function() { + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should set version name', function() { + amplitude.init(apiKey, null, {batchEvents: true}); + amplitude.setVersionName('testVersionName1'); + amplitude.logEvent('testEvent1'); + assert.equal(amplitude._unsentEvents[0].version_name, 'testVersionName1'); + + // should ignore non-string values + amplitude.setVersionName(15000); + amplitude.logEvent('testEvent2'); + assert.equal(amplitude._unsentEvents[1].version_name, 'testVersionName1'); + }); + }); + + describe('regenerateDeviceId', function() { + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should regenerate the deviceId', function() { + var deviceId = 'oldDeviceId'; + amplitude.init(apiKey, null, {'deviceId': deviceId}); + amplitude.regenerateDeviceId(); + assert.notEqual(amplitude.options.deviceId, deviceId); + assert.lengthOf(amplitude.options.deviceId, 37); + assert.equal(amplitude.options.deviceId[36], 'R'); + }); + }); + + describe('setDeviceId', function() { + + beforeEach(function() { + reset(); + }); + + afterEach(function() { + reset(); + }); + + it('should change device id', function() { + amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.setDeviceId('deviceId'); + assert.equal(amplitude.options.deviceId, 'deviceId'); + }); + + it('should not change device id if empty', function() { + amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.setDeviceId(''); + assert.notEqual(amplitude.options.deviceId, ''); + assert.equal(amplitude.options.deviceId, 'deviceId'); + }); + + it('should not change device id if null', function() { + amplitude.init(apiKey, null, {'deviceId': 'deviceId'}); + amplitude.setDeviceId(null); + assert.notEqual(amplitude.options.deviceId, null); + assert.equal(amplitude.options.deviceId, 'deviceId'); + }); + + it('should store device id in cookie', function() { + amplitude.init(apiKey, null, {'deviceId': 'fakeDeviceId'}); + amplitude.setDeviceId('deviceId'); + var stored = cookie.get(amplitude.options.cookieName); + assert.propertyVal(stored, 'deviceId', 'deviceId'); + }); + }); + + describe('identify', function() { + + beforeEach(function() { + clock = sinon.useFakeTimers(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + clock.restore(); + }); + + it('should ignore inputs that are not identify objects', function() { + amplitude.identify('This is a test'); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify(150); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify(['test']); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify({'user_prop': true}); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + }); + + it('should generate an event from the identify object', function() { + var identify = new Identify().set('prop1', 'value1').unset('prop2').add('prop3', 3).setOnce('prop4', true); + amplitude.identify(identify); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + assert.deepEqual(events[0].user_properties, { + '$set': { + 'prop1': 'value1' + }, + '$unset': { + 'prop2': '-' + }, + '$add': { + 'prop3': 3 + }, + '$setOnce': { + 'prop4': true + } + }); + }); + + it('should ignore empty identify objects', function() { + amplitude.identify(new Identify()); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + }); + + it('should ignore empty proxy identify objects', function() { + amplitude.identify({'_q': {}}); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + + amplitude.identify({}); + assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(server.requests, 0); + }); + + it('should generate an event from a proxy identify object', function() { + var proxyObject = {'_q':[ + ['setOnce', 'key2', 'value4'], + ['unset', 'key1'], + ['add', 'key1', 'value1'], + ['set', 'key2', 'value3'], + ['set', 'key4', 'value5'], + ['prepend', 'key5', 'value6'] + ]}; + amplitude.identify(proxyObject); + + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].event_properties, {}); + assert.deepEqual(events[0].user_properties, { + '$setOnce': {'key2': 'value4'}, + '$unset': {'key1': '-'}, + '$set': {'key4': 'value5'}, + '$prepend': {'key5': 'value6'} + }); + }); + + it('should run the callback after making the identify call', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + var identify = new amplitude.Identify().set('key', 'value'); + amplitude.identify(identify, callback); + + // before server responds, callback should not fire + assert.lengthOf(server.requests, 1); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after server response, fire callback + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it('should run the callback even if client not initialized with apiKey', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + var identify = new amplitude.Identify().set('key', 'value'); + new AmplitudeClient().identify(identify, callback); + + // verify callback fired + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + + it('should run the callback even with an invalid identify object', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + amplitude.identify(null, callback); + + // verify callback fired + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + }); + + describe('logEvent', function() { + + var clock; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + clock.restore(); + }); + + it('should send request', function() { + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + assert.equal(server.requests[0].url, 'http://api.amplitude.com/'); + assert.equal(server.requests[0].method, 'POST'); + assert.equal(server.requests[0].async, true); + }); + + it('should reject empty event types', function() { + amplitude.logEvent(); + assert.lengthOf(server.requests, 0); + }); + + it('should send api key', function() { + amplitude.logEvent('Event Type 2'); + assert.lengthOf(server.requests, 1); + assert.equal(querystring.parse(server.requests[0].requestBody).client, apiKey); + }); + + it('should send api version', function() { + amplitude.logEvent('Event Type 3'); + assert.lengthOf(server.requests, 1); + assert.equal(querystring.parse(server.requests[0].requestBody).v, '2'); + }); + + it('should send event JSON', function() { + amplitude.logEvent('Event Type 4'); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + assert.equal(events[0].event_type, 'Event Type 4'); + }); + + it('should send language', function() { + amplitude.logEvent('Event Should Send Language'); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + assert.isNotNull(events[0].language); + }); + + it('should accept properties', function() { + amplitude.logEvent('Event Type 5', {prop: true}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.deepEqual(events[0].event_properties, {prop: true}); + }); + + it('should queue events', function() { + amplitude._sending = true; + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 4); + assert.deepEqual(events[0].event_properties, {index: 1}); + assert.deepEqual(events[3].event_properties, {index: 100}); + }); + + it('should limit events queued', function() { + amplitude.init(apiKey, null, {savedMaxCount: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 6}); + assert.deepEqual(events[9].event_properties, {index: 100}); + }); + + it('should remove only sent events', function() { + amplitude._sending = true; + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude._sending = false; + amplitude.logEvent('Event', {index: 3}); + + server.respondWith('success'); + server.respond(); + + amplitude.logEvent('Event', {index: 4}); + + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 1); + assert.deepEqual(events[0].event_properties, {index: 4}); + }); + + it('should save events', function() { + amplitude.init(apiKey, null, {saveEvents: true}); + amplitude.logEvent('Event', {index: 1}); + amplitude.logEvent('Event', {index: 2}); + amplitude.logEvent('Event', {index: 3}); + + var amplitude2 = new AmplitudeClient(); + 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 AmplitudeClient(); + amplitude2.init(apiKey); + assert.deepEqual(amplitude2._unsentEvents, []); + }); + + it('should limit events sent', function() { + amplitude.init(apiKey, null, {uploadBatchSize: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[9].event_properties, {index: 9}); + + server.respondWith('success'); + server.respond(); + + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 6); + assert.deepEqual(events[0].event_properties, {index: 10}); + assert.deepEqual(events[5].event_properties, {index: 100}); + }); + + it('should batch events sent', function() { + var eventUploadPeriodMillis = 10*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 10, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[9].event_properties, {index: 9}); + + server.respondWith('success'); + server.respond(); + + assert.lengthOf(server.requests, 1); + var unsentEvents = amplitude._unsentEvents; + assert.lengthOf(unsentEvents, 5); + assert.deepEqual(unsentEvents[4].event_properties, {index: 14}); + + // remaining 5 events should be sent by the delayed sendEvent call + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 2); + server.respondWith('success'); + server.respond(); + assert.lengthOf(amplitude._unsentEvents, 0); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 5); + assert.deepEqual(events[4].event_properties, {index: 14}); + }); + + it('should send events after a delay', function() { + var eventUploadPeriodMillis = 10*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 2, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + amplitude.logEvent('Event'); + + // saveEvent should not have been called yet + assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(server.requests, 0); + + // saveEvent should be called after delay + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + assert.deepEqual(events[0].event_type, 'Event'); + }); + + it('should not send events after a delay if no events to send', function() { + var eventUploadPeriodMillis = 10*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 2, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + amplitude.logEvent('Event1'); + amplitude.logEvent('Event2'); + + // saveEvent triggered by 2 event batch threshold + assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + assert.deepEqual(events[1].event_type, 'Event2'); + + // saveEvent should be called after delay, but no request made + assert.lengthOf(amplitude._unsentEvents, 0); + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 1); + }); + + it('should not schedule more than one upload', function() { + var eventUploadPeriodMillis = 5*1000; // 5s + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 30, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + + // log 2 events, 1 millisecond apart, second event should not schedule upload + amplitude.logEvent('Event1'); + clock.tick(1); + amplitude.logEvent('Event2'); + assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(server.requests, 0); + + // advance to upload period millis, and should have 1 server request + // from the first scheduled upload + clock.tick(eventUploadPeriodMillis-1); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + + // log 3rd event, advance 1 more millisecond, verify no 2nd server request + amplitude.logEvent('Event3'); + clock.tick(1); + assert.lengthOf(server.requests, 1); + + // the 3rd event, however, should have scheduled another upload after 5s + clock.tick(eventUploadPeriodMillis-2); + assert.lengthOf(server.requests, 1); + clock.tick(1); + assert.lengthOf(server.requests, 2); + }); + + it('should back off on 413 status', function() { + amplitude.init(apiKey, null, {uploadBatchSize: 10}); + + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 10); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[9].event_properties, {index: 9}); + + server.respondWith([413, {}, '']); + server.respond(); + + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 5); + assert.deepEqual(events[0].event_properties, {index: 0}); + assert.deepEqual(events[4].event_properties, {index: 4}); + }); + + it('should back off on 413 status all the way to 1 event with drops', function() { + amplitude.init(apiKey, null, {uploadBatchSize: 9}); + + amplitude._sending = true; + for (var i = 0; i < 10; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + amplitude.logEvent('Event', {index: 100}); + + for (var i = 0; i < 6; i++) { + assert.lengthOf(server.requests, i+1); + server.respondWith([413, {}, '']); + server.respond(); + } + + var events = JSON.parse(querystring.parse(server.requests[6].requestBody).e); + assert.lengthOf(events, 1); + assert.deepEqual(events[0].event_properties, {index: 2}); + }); + + it ('should run callback if no eventType', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + } + amplitude.logEvent(null, null, callback); + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + + it ('should run callback if optout', function () { + amplitude.setOptOut(true); + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + amplitude.logEvent('test', null, callback); + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + }); + + it ('should not run callback if invalid callback and no eventType', function () { + amplitude.logEvent(null, null, 'invalid callback'); + }); + + it ('should run callback after logging event', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + amplitude.logEvent('test', null, callback); + + // before server responds, callback should not fire + assert.lengthOf(server.requests, 1); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after server response, fire callback + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it ('should run callback if batchEvents but under threshold', function () { + var eventUploadPeriodMillis = 5*1000; + amplitude.init(apiKey, null, { + batchEvents: true, + eventUploadThreshold: 2, + eventUploadPeriodMillis: eventUploadPeriodMillis + }); + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + amplitude.logEvent('test', null, callback); + assert.lengthOf(server.requests, 0); + assert.equal(counter, 1); + assert.equal(value, 0); + assert.equal(message, 'No request sent'); + + // check that request is made after delay, but callback is not run a second time + clock.tick(eventUploadPeriodMillis); + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + }); + + it ('should run callback once and only after all events are uploaded', function () { + amplitude.init(apiKey, null, {uploadBatchSize: 10}); + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + // queue up 15 events, since batchsize 10, need to send in 2 batches + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + amplitude.logEvent('Event', {index: 100}, callback); + + assert.lengthOf(server.requests, 1); + server.respondWith('success'); + server.respond(); + + // after first response received, callback should not have fired + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + assert.lengthOf(server.requests, 2); + server.respondWith('success'); + server.respond(); + + // after last response received, callback should fire + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it ('should run callback once and only after 413 resolved', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + // queue up 15 events + amplitude._sending = true; + for (var i = 0; i < 15; i++) { + amplitude.logEvent('Event', {index: i}); + } + amplitude._sending = false; + + // 16th event with 413 will backoff to batches of 8 + amplitude.logEvent('Event', {index: 100}, callback); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 16); + + // after 413 response received, callback should not have fired + server.respondWith([413, {}, '']); + server.respond(); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after sending first backoff batch, callback still should not have fired + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 8); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + + // after sending second backoff batch, callback should fire + assert.lengthOf(server.requests, 3); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.lengthOf(events, 8); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + + it ('should run callback if server returns something other than 200 and 413', function () { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + amplitude.logEvent('test', null, callback); + server.respondWith([404, {}, 'Not found']); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 404); + assert.equal(message, 'Not found'); + }); + + it('should send 3 identify events', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.identify(new Identify().add('photoCount', 1).set('country', 'USA')); + amplitude.identify(new Identify().add('photoCount', 1)); + + // verify some internal counters + assert.equal(amplitude._eventId, 0); + assert.equal(amplitude._identifyId, 3); + assert.equal(amplitude._unsentCount(), 3); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 3); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + for (var i = 0; i < 3; i++) { + assert.equal(events[i].event_type, '$identify'); + assert.isTrue('$add' in events[i].user_properties); + assert.deepEqual(events[i].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[i].event_id, i+1); + assert.equal(events[i].sequence_number, i+1); + } + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should send 3 events', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.logEvent('test'); + amplitude.logEvent('test'); + amplitude.logEvent('test'); + + // verify some internal counters + assert.equal(amplitude._eventId, 3); + assert.equal(amplitude._identifyId, 0); + assert.equal(amplitude._unsentCount(), 3); + assert.lengthOf(amplitude._unsentEvents, 3); + assert.lengthOf(amplitude._unsentIdentifys, 0); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + for (var i = 0; i < 3; i++) { + assert.equal(events[i].event_type, 'test'); + assert.equal(events[i].event_id, i+1); + assert.equal(events[i].sequence_number, i+1); + } + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + }); + + it('should send 1 event and 1 identify event', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.logEvent('test'); + amplitude.identify(new Identify().add('photoCount', 1)); + + // verify some internal counters + assert.equal(amplitude._eventId, 1); + assert.equal(amplitude._identifyId, 1); + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(amplitude._unsentIdentifys, 1); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + + // event should come before identify - maintain order using sequence number + assert.equal(events[0].event_type, 'test'); + assert.equal(events[0].event_id, 1); + assert.deepEqual(events[0].user_properties, {}); + assert.equal(events[0].sequence_number, 1); + assert.equal(events[1].event_type, '$identify'); + assert.equal(events[1].event_id, 1); + assert.isTrue('$add' in events[1].user_properties); + assert.deepEqual(events[1].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[1].sequence_number, 2); + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should properly coalesce events and identify events into a request', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.logEvent('test1'); + clock.tick(1); + amplitude.identify(new Identify().add('photoCount', 1)); + clock.tick(1); + amplitude.logEvent('test2'); + clock.tick(1); + amplitude.logEvent('test3'); + clock.tick(1); + amplitude.logEvent('test4'); + amplitude.identify(new Identify().add('photoCount', 2)); + + // verify some internal counters + assert.equal(amplitude._eventId, 4); + assert.equal(amplitude._identifyId, 2); + assert.equal(amplitude._unsentCount(), 6); + assert.lengthOf(amplitude._unsentEvents, 4); + assert.lengthOf(amplitude._unsentIdentifys, 2); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 6); + + // verify the correct coalescing + assert.equal(events[0].event_type, 'test1'); + assert.deepEqual(events[0].user_properties, {}); + assert.equal(events[0].sequence_number, 1); + assert.equal(events[1].event_type, '$identify'); + assert.isTrue('$add' in events[1].user_properties); + assert.deepEqual(events[1].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[1].sequence_number, 2); + assert.equal(events[2].event_type, 'test2'); + assert.deepEqual(events[2].user_properties, {}); + assert.equal(events[2].sequence_number, 3); + assert.equal(events[3].event_type, 'test3'); + assert.deepEqual(events[3].user_properties, {}); + assert.equal(events[3].sequence_number, 4); + assert.equal(events[4].event_type, 'test4'); + assert.deepEqual(events[4].user_properties, {}); + assert.equal(events[4].sequence_number, 5); + assert.equal(events[5].event_type, '$identify'); + assert.isTrue('$add' in events[5].user_properties); + assert.deepEqual(events[5].user_properties['$add'], {'photoCount': 2}); + assert.equal(events[5].sequence_number, 6); + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should merged events supporting backwards compatability', function() { + // events logged before v2.5.0 won't have sequence number, should get priority + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); + assert.equal(amplitude._unsentCount(), 0); + + amplitude.identify(new Identify().add('photoCount', 1)); + amplitude.logEvent('test'); + delete amplitude._unsentEvents[0].sequence_number; // delete sequence number to simulate old event + amplitude._sequenceNumber = 1; // reset sequence number + amplitude.identify(new Identify().add('photoCount', 2)); + + // verify some internal counters + assert.equal(amplitude._eventId, 1); + assert.equal(amplitude._identifyId, 2); + assert.equal(amplitude._unsentCount(), 3); + assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(amplitude._unsentIdentifys, 2); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + + // event should come before identify - prioritize events with no sequence number + assert.equal(events[0].event_type, 'test'); + assert.equal(events[0].event_id, 1); + assert.deepEqual(events[0].user_properties, {}); + assert.isFalse('sequence_number' in events[0]); + + assert.equal(events[1].event_type, '$identify'); + assert.equal(events[1].event_id, 1); + assert.isTrue('$add' in events[1].user_properties); + assert.deepEqual(events[1].user_properties['$add'], {'photoCount': 1}); + assert.equal(events[1].sequence_number, 1); + + assert.equal(events[2].event_type, '$identify'); + assert.equal(events[2].event_id, 2); + assert.isTrue('$add' in events[2].user_properties); + assert.deepEqual(events[2].user_properties['$add'], {'photoCount': 2}); + assert.equal(events[2].sequence_number, 2); + + // send response and check that remove events works properly + server.respondWith('success'); + server.respond(); + assert.equal(amplitude._unsentCount(), 0); + assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude._unsentIdentifys, 0); + }); + + it('should drop event and keep identify on 413 response', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('test'); + clock.tick(1); + amplitude.identify(new Identify().add('photoCount', 1)); + + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 1); + server.respondWith([413, {}, '']); + server.respond(); + + // backoff and retry + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 2); + server.respondWith([413, {}, '']); + server.respond(); + + // after dropping massive event, only 1 event left + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 3); + + var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, '$identify'); + assert.isTrue('$add' in events[0].user_properties); + assert.deepEqual(events[0].user_properties['$add'], {'photoCount': 1}); + }); + + it('should drop identify if 413 and uploadBatchSize is 1', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + amplitude.identify(new Identify().add('photoCount', 1)); + clock.tick(1); + amplitude.logEvent('test'); + + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 1); + server.respondWith([413, {}, '']); + server.respond(); + + // backoff and retry + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 2); + assert.lengthOf(server.requests, 2); + server.respondWith([413, {}, '']); + server.respond(); + + // after dropping massive event, only 1 event left + assert.equal(amplitude.options.uploadBatchSize, 1); + assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(server.requests, 3); + + var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); + assert.lengthOf(events, 1); + assert.equal(events[0].event_type, 'test'); + assert.deepEqual(events[0].user_properties, {}); + }); + + it('should truncate long event property strings', function() { + var longString = new Array(5000).join('a'); + amplitude.logEvent('test', {'key': longString}); + var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; + + assert.isTrue('key' in event.event_properties); + assert.lengthOf(event.event_properties['key'], 4096); + }); + + it('should truncate long user property strings', function() { + var longString = new Array(5000).join('a'); + amplitude.identify(new Identify().set('key', longString)); + var event = JSON.parse(querystring.parse(server.requests[0].requestBody).e)[0]; + + assert.isTrue('$set' in event.user_properties); + assert.lengthOf(event.user_properties['$set']['key'], 4096); + }); + + it('should increment the counters in local storage if cookies disabled', function() { + localStorage.clear(); + var deviceId = 'test_device_id'; + var amplitude2 = new 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': console.log, // should be ignored + 'regex': /afdg/, // should be ignored + 'error': e, // coerce value + 'string': 'test', + 'array': [0, 1, 2, '3'], + 'nested_array': ['a', {'key': 'value'}, ['b']], + 'object': {'key':'value', 15: e}, + 'nested_object': {'k':'v', 'l':[0,1], 'o':{'k2':'v2', 'l2': ['e2', {'k3': 'v3'}]}} + }); + clock.tick(1); + + assert.lengthOf(amplitude._unsentEvents, 5); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 5); + + assert.deepEqual(events[0].event_properties, {}); + assert.deepEqual(events[1].event_properties, {}); + assert.deepEqual(events[2].event_properties, {}); + assert.deepEqual(events[3].event_properties, {}); + assert.deepEqual(events[4].event_properties, { + '10': 'false', + 'bool': true, + 'error': 'Error: oops', + 'string': 'test', + 'array': [0, 1, 2, '3'], + 'nested_array': ['a'], + 'object': {'key':'value', '15':'Error: oops'}, + 'nested_object': {'k':'v', 'l':[0,1], 'o':{'k2':'v2', 'l2': ['e2']}} + }); + }); + + it('should validate user propeorties', function() { + var identify = new Identify().set(10, 10); + amplitude.init(apiKey, null, {batchEvents: true}); + amplitude.identify(identify); + + assert.deepEqual(amplitude._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); + }); + + it('should synchronize event data across multiple amplitude instances that share the same cookie', function() { + // this test fails if logEvent does not reload cookie data every time + var amplitude1 = new AmplitudeClient(); + amplitude1.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + var amplitude2 = new AmplitudeClient(); + amplitude2.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 5}); + + amplitude1.logEvent('test1'); + amplitude2.logEvent('test2'); + amplitude1.logEvent('test3'); + amplitude2.logEvent('test4'); + amplitude2.identify(new amplitude2.Identify().set('key', 'value')); + amplitude1.logEvent('test5'); + + // the event ids should all be sequential since amplitude1 and amplitude2 have synchronized cookies + var eventId = amplitude1._unsentEvents[0]['event_id']; + assert.equal(amplitude2._unsentEvents[0]['event_id'], eventId + 1); + assert.equal(amplitude1._unsentEvents[1]['event_id'], eventId + 2); + assert.equal(amplitude2._unsentEvents[1]['event_id'], eventId + 3); + + var sequenceNumber = amplitude1._unsentEvents[0]['sequence_number']; + assert.equal(amplitude2._unsentIdentifys[0]['sequence_number'], sequenceNumber + 4); + assert.equal(amplitude1._unsentEvents[2]['sequence_number'], sequenceNumber + 5); + }); + + it('should handle groups input', function() { + var counter = 0; + var value = -1; + var message = ''; + var callback = function (status, response) { + counter++; + value = status; + message = response; + }; + + var eventProperties = { + 'key': 'value' + }; + + var groups = { + 10: 1.23, // coerce numbers to strings + 'array': ['test2', false, ['test', 23, null], null], // should ignore nested array and nulls + 'dictionary': {160: 'test3'}, // should ignore dictionaries + 'null': null, // ignore null values + } + + amplitude.logEventWithGroups('Test', eventProperties, groups, callback); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + + // verify event is correctly formatted + var event = events[0]; + assert.equal(event.event_type, 'Test'); + assert.equal(event.event_id, 1); + assert.deepEqual(event.user_properties, {}); + assert.deepEqual(event.event_properties, eventProperties); + assert.deepEqual(event.groups, { + '10': '1.23', + 'array': ['test2', 'false'], + }); + + // verify callback behavior + assert.equal(counter, 0); + assert.equal(value, -1); + assert.equal(message, ''); + server.respondWith('success'); + server.respond(); + assert.equal(counter, 1); + assert.equal(value, 200); + assert.equal(message, 'success'); + }); + }); + + describe('optOut', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should not send events while enabled', function() { + amplitude.setOptOut(true); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 0); + }); + + it('should not send saved events while enabled', function() { + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + + amplitude._sending = false; + amplitude.setOptOut(true); + amplitude.init(apiKey); + assert.lengthOf(server.requests, 1); + }); + + it('should start sending events again when disabled', function() { + amplitude.setOptOut(true); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 0); + + amplitude.setOptOut(false); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + }); + + it('should have state be persisted in the cookie', function() { + var amplitude = new 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 via identify when the includeUtm flag is true', function() { + cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); + reset(); + amplitude.init(apiKey, undefined, {includeUtm: true, batchEvents: true, eventUploadThreshold: 2}); + + amplitude.logEvent('UTM Test Event', {}); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + initial_utm_campaign: 'new', + initial_utm_content: 'top' + }, + '$set': { + utm_campaign: 'new', + utm_content: 'top' + } + }); + + assert.equal(events[1].event_type, 'UTM Test Event'); + assert.deepEqual(events[1].user_properties, {}); + }); + + it('should parse utm params', function() { + cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); + + var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; + amplitude._initUtmData(utmParams); + + var expectedProperties = { + utm_campaign: 'new', + utm_content: 'top', + utm_medium: 'email', + utm_source: 'amplitude', + utm_term: 'terms' + } + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + initial_utm_campaign: 'new', + initial_utm_content: 'top', + initial_utm_medium: 'email', + initial_utm_source: 'amplitude', + initial_utm_term: 'terms' + }, + '$set': expectedProperties + }); + server.respondWith('success'); + server.respond(); + + amplitude.logEvent('UTM Test Event', {}); + assert.lengthOf(server.requests, 2); + var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); + assert.deepEqual(events[0].user_properties, {}); + + // verify session storage set + assert.deepEqual(JSON.parse(sessionStorage.getItem('amplitude_utm_properties')), expectedProperties); + }); + + it('should not set utmProperties if utmProperties data already in session storage', function() { + reset(); + var existingProperties = { + utm_campaign: 'old', + utm_content: 'bottom', + utm_medium: 'texts', + utm_source: 'datamonster', + utm_term: 'conditions' + }; + sessionStorage.setItem('amplitude_utm_properties', JSON.stringify(existingProperties)); + + cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); + var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; + amplitude._initUtmData(utmParams); + + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1); + + // first event should be identify with initial_utm properties and NO existing utm properties + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + initial_utm_campaign: 'new', + initial_utm_content: 'top', + initial_utm_medium: 'email', + initial_utm_source: 'amplitude', + initial_utm_term: 'terms' + } + }); + + // should not override any existing utm properties values in session storage + assert.equal(sessionStorage.getItem('amplitude_utm_properties'), JSON.stringify(existingProperties)); + }); + }); + + describe('gatherReferrer', function() { + beforeEach(function() { + amplitude.init(apiKey); + sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.com/contact'); + }); + + afterEach(function() { + amplitude._getReferrer.restore(); + reset(); + }); + + it('should not send referrer data when the includeReferrer flag is false', function() { + amplitude.init(apiKey, undefined, {}); + + amplitude.setUserProperties({user_prop: true}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events[0].user_properties.referrer, undefined); + assert.equal(events[0].user_properties.referring_domain, undefined); + }); + + it('should only send referrer via identify call when the includeReferrer flag is true', function() { + reset(); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + + var expected = { + 'referrer': 'https://amplitude.com/contact', + 'referring_domain': 'amplitude.com' + }; + + // first event should be identify with initial_referrer and referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$set': expected, + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be the test event with no referrer information + assert.equal(events[1].event_type, 'Referrer Test Event'); + assert.deepEqual(events[1].user_properties, {}); + + // referrer should be propagated to session storage + assert.equal(sessionStorage.getItem('amplitude_referrer'), JSON.stringify(expected)); + }); + + it('should not set referrer if referrer data already in session storage', function() { + reset(); + sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 2}); + amplitude.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 2); + + // first event should be identify with initial_referrer and NO referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be the test event with no referrer information + assert.equal(events[1].event_type, 'Referrer Test Event'); + assert.deepEqual(events[1].user_properties, {}); + }); + + it('should not override any existing initial referrer values in session storage', function() { + reset(); + sessionStorage.setItem('amplitude_referrer', 'https://www.google.com/search?'); + amplitude.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); + amplitude._saveReferrer('https://facebook.com/contact'); + amplitude.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + + // first event should be identify with initial_referrer and NO referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be another identify but with the new referrer + assert.equal(events[1].event_type, '$identify'); + assert.deepEqual(events[1].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://facebook.com/contact', + 'initial_referring_domain': 'facebook.com' + } + }); + + // third event should be the test event with no referrer information + assert.equal(events[2].event_type, 'Referrer Test Event'); + assert.deepEqual(events[2].user_properties, {}); + + // existing value persists + assert.equal(sessionStorage.getItem('amplitude_referrer'), 'https://www.google.com/search?'); + }); + + it('should not override any existing referrer values in session storage for non-default instances', function() { + reset(); + sessionStorage.setItem('amplitude_referrer_new_app', 'https://www.google.com/search?'); + var amplitude2 = new AmplitudeClient('new_app'); + sinon.stub(amplitude2, '_getReferrer').returns('https://amplitude.com/contact'); + amplitude2.init(apiKey, undefined, {includeReferrer: true, batchEvents: true, eventUploadThreshold: 3}); + amplitude2._getReferrer.restore(); + + amplitude2._saveReferrer('https://facebook.com/contact'); + amplitude2.logEvent('Referrer Test Event', {}); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 3); + + // first event should be identify with initial_referrer and NO referrer + assert.equal(events[0].event_type, '$identify'); + assert.deepEqual(events[0].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://amplitude.com/contact', + 'initial_referring_domain': 'amplitude.com' + } + }); + + // second event should be another identify but with the new referrer + assert.equal(events[1].event_type, '$identify'); + assert.deepEqual(events[1].user_properties, { + '$setOnce': { + 'initial_referrer': 'https://facebook.com/contact', + 'initial_referring_domain': 'facebook.com' + } + }); + + // third event should be the test event with no referrer information + assert.equal(events[2].event_type, 'Referrer Test Event'); + assert.deepEqual(events[2].user_properties, {}); + + // existing value persists + assert.equal(sessionStorage.getItem('amplitude_referrer_new_app'), 'https://www.google.com/search?'); + }); + }); + + describe('logRevenue', function() { + beforeEach(function() { + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + /** + * Deep compare an object against the api_properties of the + * event queued for sending. + */ + function revenueEqual(api, event) { + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.deepEqual(events[0].api_properties, api || {}); + assert.deepEqual(events[0].event_properties, event || {}); + } + + it('should log simple amount', function() { + amplitude.logRevenue(10.10); + revenueEqual({ + special: 'revenue_amount', + price: 10.10, + quantity: 1 + }) + }); + + it('should log complex amount', function() { + amplitude.logRevenue(10.10, 7); + revenueEqual({ + special: 'revenue_amount', + price: 10.10, + quantity: 7 + }) + }); + + it('shouldn\'t log invalid price', function() { + amplitude.logRevenue('kitten', 7); + assert.lengthOf(server.requests, 0); + }); + + it('shouldn\'t log invalid quantity', function() { + amplitude.logRevenue(10.00, 'puppy'); + assert.lengthOf(server.requests, 0); + }); + + it('should log complex amount with product id', function() { + amplitude.logRevenue(10.10, 7, 'chicken.dinner'); + revenueEqual({ + special: 'revenue_amount', + price: 10.10, + quantity: 7, + productId: 'chicken.dinner' + }); + }); + }); + + describe('logRevenueV2', function() { + beforeEach(function() { + reset(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + }); + + it('should log with the Revenue object', function () { + // ignore invalid revenue objects + amplitude.logRevenueV2(null); + assert.lengthOf(server.requests, 0); + amplitude.logRevenueV2({}); + assert.lengthOf(server.requests, 0); + amplitude.logRevenueV2(new amplitude.Revenue()); + + // log valid revenue object + var productId = 'testProductId'; + var quantity = 15; + var price = 10.99; + var revenueType = 'testRevenueType' + var properties = {'city': 'San Francisco'}; + + var revenue = new amplitude.Revenue().setProductId(productId).setQuantity(quantity).setPrice(price); + revenue.setRevenueType(revenueType).setEventProperties(properties); + + amplitude.logRevenueV2(revenue); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + var event = events[0]; + assert.equal(event.event_type, 'revenue_amount'); + + assert.deepEqual(event.event_properties, { + '$productId': productId, + '$quantity': quantity, + '$price': price, + '$revenueType': revenueType, + 'city': 'San Francisco' + }); + + // verify user properties empty + assert.deepEqual(event.user_properties, {}); + + // verify no revenue data in api_properties + assert.deepEqual(event.api_properties, {}); + }); + + it('should convert proxied Revenue object into real revenue object', function() { + var fakeRevenue = {'_q':[ + ['setProductId', 'questionable'], + ['setQuantity', 10], + ['setPrice', 'key1'] // invalid price type, this will fail to generate revenue event + ]}; + amplitude.logRevenueV2(fakeRevenue); + assert.lengthOf(server.requests, 0); + + var proxyRevenue = {'_q':[ + ['setProductId', 'questionable'], + ['setQuantity', 15], + ['setPrice', 10.99], + ['setRevenueType', 'purchase'] + ]}; + amplitude.logRevenueV2(proxyRevenue); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + var event = events[0]; + assert.equal(event.event_type, 'revenue_amount'); + + assert.deepEqual(event.event_properties, { + '$productId': 'questionable', + '$quantity': 15, + '$price': 10.99, + '$revenueType': 'purchase' + }); + }); + }); + + describe('sessionId', function() { + var clock; + beforeEach(function() { + clock = sinon.useFakeTimers(); + amplitude.init(apiKey); + }); + + afterEach(function() { + reset(); + clock.restore(); + }); + + it('should create new session IDs on timeout', function() { + var sessionId = amplitude._sessionId; + clock.tick(30 * 60 * 1000 + 1); + amplitude.logEvent('Event Type 1'); + assert.lengthOf(server.requests, 1); + var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); + assert.equal(events.length, 1); + assert.notEqual(events[0].session_id, sessionId); + assert.notEqual(amplitude._sessionId, sessionId); + assert.equal(events[0].session_id, amplitude._sessionId); + }); + + it('should be fetched correctly by getSessionId', function() { + var timestamp = 1000; + clock.tick(timestamp); + var amplitude2 = new AmplitudeClient(); + amplitude2.init(apiKey); + assert.equal(amplitude2._sessionId, timestamp); + assert.equal(amplitude2.getSessionId(), timestamp); + assert.equal(amplitude2.getSessionId(), amplitude2._sessionId); + }); + }); +}); \ No newline at end of file diff --git a/test/amplitude.js b/test/amplitude.js index 6cd941d0..11ae32bd 100644 --- a/test/amplitude.js +++ b/test/amplitude.js @@ -34,9 +34,189 @@ 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.options.apiKey, apiKey); + assert.equal(amplitude.options, amplitude.options); + assert.equal(amplitude.getInstance('$default_instance').options.apiKey, apiKey); + assert.equal(amplitude.getInstance(), amplitude.getInstance('$default_instance')); + assert.equal(amplitude.options.deviceId, amplitude.options.deviceId); + + // test for case insensitivity + assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_INSTANCE')); + assert.equal(amplitude.getInstance(), amplitude.getInstance('$DEFAULT_instance')); + }); + + it('should create two separate instances', function() { + var app1 = amplitude.getInstance('app1'); + app1.init('1'); + var app2 = amplitude.getInstance('app2'); + app2.init('2'); + + assert.notEqual(app1, app2); + assert.equal(app1.options.apiKey, '1'); + assert.equal(app2.options.apiKey, '2'); + + assert.equal(app1, amplitude.getInstance('app1')); + assert.equal(app1, amplitude.getInstance('APP1')); + assert.equal(app1, amplitude.getInstance('aPp1')); + assert.equal(app2, amplitude.getInstance('app2')); + assert.equal(app2, amplitude.getInstance('APP2')); + assert.equal(app2, amplitude.getInstance('aPp2')); + + assert.equal(amplitude.getInstance('APP3')._instanceName, 'app3'); + }); + + it('should return the same instance for same key', function() { + var app = amplitude.getInstance('app'); + app.init('1'); + assert.equal(app, amplitude.getInstance('app')); + assert.equal(amplitude.getInstance('app').options.apiKey, '1'); + }); + + it('instances should have separate event queues and settings', function() { + amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); + var app1 = amplitude.getInstance('app1'); + app1.init('1'); + var app2 = amplitude.getInstance('app2'); + app2.init('2'); + + assert.notEqual(amplitude.options.deviceId, app1.options.deviceId); + assert.notEqual(amplitude.options.deviceId, app2.options.deviceId); + assert.notEqual(app1.options.deviceId, app2.options.deviceId); + + amplitude.logEvent('amplitude event'); + amplitude.logEvent('amplitude event2'); + var identify = new Identify().set('key', 'value'); + app1.identify(identify); + app2.logEvent('app2 event'); + + assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); + + assert.lengthOf(app1._unsentEvents, 0); + assert.lengthOf(app1._unsentIdentifys, 1); + assert.lengthOf(app2._unsentEvents, 1); + assert.lengthOf(app2._unsentIdentifys, 0); + + assert.deepEqual(amplitude.getInstance()._unsentEvents[0].event_type, 'amplitude event'); + assert.deepEqual(amplitude.getInstance()._unsentEvents[1].event_type, 'amplitude event2'); + assert.deepEqual(amplitude.getInstance()._unsentIdentifys, []); + assert.deepEqual(app1._unsentEvents, []); + assert.deepEqual(app1._unsentIdentifys[0].user_properties, {'$set':{'key':'value'}}); + assert.deepEqual(app2._unsentEvents[0].event_type, 'app2 event'); + assert.deepEqual(app2._unsentIdentifys, []); + + assert.equal(amplitude.getInstance()._eventId, 2); + assert.equal(amplitude.getInstance()._identifyId, 0); + assert.equal(amplitude.getInstance()._sequenceNumber, 2); + assert.equal(app1._eventId, 0); + assert.equal(app1._identifyId, 1); + assert.equal(app1._sequenceNumber, 1); + assert.equal(app2._eventId, 1); + assert.equal(app2._identifyId, 0); + assert.equal(app2._sequenceNumber, 1); + + // verify separate localstorages + assert.deepEqual( + JSON.parse(localStorage.getItem('amplitude_unsent'))[0].event_type, 'amplitude event' + ); + assert.deepEqual( + JSON.parse(localStorage.getItem('amplitude_unsent'))[1].event_type, 'amplitude event2' + ); + assert.equal(localStorage.getItem('amplitude_unsent_identify'), JSON.stringify([])); + assert.equal(localStorage.getItem('amplitude_unsent_app1'), JSON.stringify([])); + assert.deepEqual( + JSON.parse(localStorage.getItem('amplitude_unsent_identify_app1'))[0].user_properties, {'$set':{'key':'value'}} + ); + assert.equal( + JSON.parse(localStorage.getItem('amplitude_unsent_app2'))[0].event_type, 'app2 event' + ); + assert.equal(localStorage.getItem('amplitude_unsent_identify_app2'), JSON.stringify([])); + + // verify separate apiKeys in server requests + assert.lengthOf(server.requests, 3); + assert.equal(JSON.parse(querystring.parse(server.requests[1].requestBody).client), 1); + assert.equal(JSON.parse(querystring.parse(server.requests[2].requestBody).client), 2); + + // verify separate cookie data + var cookieData = cookie.get(amplitude.options.cookieName); + assert.equal(cookieData.deviceId, amplitude.options.deviceId); + + var cookieData1 = cookie.get(app1.options.cookieName + '_app1'); + assert.equal(cookieData1.deviceId, app1.options.deviceId); + + var cookieData2 = cookie.get(app2.options.cookieName + '_app2'); + assert.equal(cookieData2.deviceId, app2.options.deviceId); + }); + + it('new instances should not load historical cookie data', function() { + var now = new Date().getTime(); + + var cookieData = { + deviceId: 'test_device_id', + userId: 'test_user_id', + optOut: true, + sessionId: now-500, + lastEventTime: now-500, + eventId: 50, + identifyId: 60, + sequenceNumber: 70 + } + cookie.set(amplitude.options.cookieName, cookieData); + + // default instance loads from existing cookie + var app = amplitude.getInstance(); + app.init(apiKey); + assert.equal(app.options.deviceId, 'test_device_id'); + assert.equal(app.options.userId, 'test_user_id'); + assert.isTrue(app.options.optOut); + assert.equal(app._sessionId, now-500); + assert.isTrue(app._lastEventTime >= now); + assert.equal(app._eventId, 50); + assert.equal(app._identifyId, 60); + assert.equal(app._sequenceNumber, 70); + + var app1 = amplitude.getInstance('app1'); + app1.init('1'); + assert.notEqual(app1.options.deviceId, 'test_device_id'); + assert.isNull(app1.options.userId); + assert.isFalse(app1.options.optOut); + console.log(app1._sessionId); + assert.isTrue(app1._sessionId >= now); + assert.isTrue(app1._lastEventTime >= now); + assert.equal(app1._eventId, 0); + assert.equal(app1._identifyId, 0); + assert.equal(app1._sequenceNumber, 0); + + var app2 = amplitude.getInstance('app2'); + app2.init('2'); + assert.notEqual(app2.options.deviceId, 'test_device_id'); + assert.isNull(app2.options.userId); + assert.isFalse(app2.options.optOut); + assert.isTrue(app2._sessionId >= now); + assert.isTrue(app2._lastEventTime >= now); + assert.equal(app2._eventId, 0); + assert.equal(app2._identifyId, 0); + assert.equal(app2._sequenceNumber, 0); + }); + }); + describe('init', function() { beforeEach(function() { reset(); @@ -156,15 +336,15 @@ describe('Amplitude', function() { 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); + assert.equal(amplitude.getInstance()._sessionId, now); + assert.isTrue(amplitude.getInstance()._lastEventTime >= now); + assert.equal(amplitude.getInstance()._eventId, 3000); + assert.equal(amplitude.getInstance()._identifyId, 4000); + assert.equal(amplitude.getInstance()._sequenceNumber, 5000); var cookieData = cookie.get(amplitude.options.cookieName); assert.equal(cookieData.sessionId, now); - assert.equal(cookieData.lastEventTime, amplitude._lastEventTime); + assert.equal(cookieData.lastEventTime, amplitude.getInstance()._lastEventTime); assert.equal(cookieData.eventId, 3000); assert.equal(cookieData.identifyId, 4000); assert.equal(cookieData.sequenceNumber, 5000); @@ -197,11 +377,11 @@ describe('Amplitude', function() { 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); + assert.equal(amplitude.getInstance()._sessionId, now); + assert.isTrue(amplitude.getInstance()._lastEventTime >= now); + assert.equal(amplitude.getInstance()._eventId, 50); + assert.equal(amplitude.getInstance()._identifyId, 60); + assert.equal(amplitude.getInstance()._sequenceNumber, 40); }); it('should skip the migration if the new cookie already has deviceId, sessionId, lastEventTime', function() { @@ -226,39 +406,11 @@ describe('Amplitude', function() { 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 + assert.isTrue(amplitude.getInstance()._sessionId >= now); + assert.isTrue(amplitude.getInstance()._lastEventTime >= now); + assert.equal(amplitude.getInstance()._eventId, 0); + assert.equal(amplitude.getInstance()._identifyId, 0); + assert.equal(amplitude.getInstance()._sequenceNumber, 0); }); it('should load sessionId, eventId from cookie and ignore the one in localStorage', function() { @@ -297,11 +449,11 @@ describe('Amplitude', function() { amplitude2.init(apiKey); clock.restore(); - assert.equal(amplitude2._sessionId, sessionId); - assert.equal(amplitude2._lastEventTime, sessionId + 10); - assert.equal(amplitude2._eventId, 50); - assert.equal(amplitude2._identifyId, 60); - assert.equal(amplitude2._sequenceNumber, 70); + assert.equal(amplitude2.getInstance()._sessionId, sessionId); + assert.equal(amplitude2.getInstance()._lastEventTime, sessionId + 10); + assert.equal(amplitude2.getInstance()._eventId, 50); + assert.equal(amplitude2.getInstance()._identifyId, 60); + assert.equal(amplitude2.getInstance()._sequenceNumber, 70); }); it('should load sessionId from localStorage if not in cookie', function() { @@ -334,11 +486,11 @@ describe('Amplitude', function() { amplitude2.init(apiKey, userId); clock.restore(); - assert.equal(amplitude2._sessionId, sessionId); - assert.equal(amplitude2._lastEventTime, sessionId + 10); - assert.equal(amplitude2._eventId, 50); - assert.equal(amplitude2._identifyId, 60); - assert.equal(amplitude2._sequenceNumber, 70); + assert.equal(amplitude2.getInstance()._sessionId, sessionId); + assert.equal(amplitude2.getInstance()._lastEventTime, sessionId + 10); + assert.equal(amplitude2.getInstance()._eventId, 50); + assert.equal(amplitude2.getInstance()._identifyId, 60); + assert.equal(amplitude2.getInstance()._sequenceNumber, 70); }); it('should load saved events from localStorage', function() { @@ -360,8 +512,8 @@ describe('Amplitude', function() { amplitude2.init(apiKey, null, {batchEvents: true}); // check event loaded into memory - assert.deepEqual(amplitude2._unsentEvents, JSON.parse(existingEvent)); - assert.deepEqual(amplitude2._unsentIdentifys, JSON.parse(existingIdentify)); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, JSON.parse(existingEvent)); + assert.deepEqual(amplitude2.getInstance()._unsentIdentifys, JSON.parse(existingIdentify)); // check local storage keys are still same for default instance assert.equal(localStorage.getItem('amplitude_unsent'), existingEvent); @@ -397,8 +549,8 @@ describe('Amplitude', function() { } // check that event loaded into memory - assert.deepEqual(amplitude2._unsentEvents[0].event_properties, {}); - assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[0].event_properties, {}); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[1].event_properties, expected); }); it('should validate user properties when loading saved identifys from localStorage', function() { @@ -426,7 +578,7 @@ describe('Amplitude', function() { } // check that event loaded into memory - assert.deepEqual(amplitude2._unsentIdentifys[0].user_properties, {'$set': expected}); + assert.deepEqual(amplitude2.getInstance()._unsentIdentifys[0].user_properties, {'$set': expected}); }); it ('should load saved events from localStorage new keys and send events', function() { @@ -450,8 +602,8 @@ describe('Amplitude', function() { server.respond(); // check event loaded into memory - assert.deepEqual(amplitude2._unsentEvents, []); - assert.deepEqual(amplitude2._unsentIdentifys, []); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, []); + assert.deepEqual(amplitude2.getInstance()._unsentIdentifys, []); // check local storage keys are still same assert.equal(localStorage.getItem('amplitude_unsent'), JSON.stringify([])); @@ -505,8 +657,8 @@ describe('Amplitude', function() { } // check that event loaded into memory - assert.deepEqual(amplitude2._unsentEvents[0].event_properties, {}); - assert.deepEqual(amplitude2._unsentEvents[1].event_properties, expected); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[0].event_properties, {}); + assert.deepEqual(amplitude2.getInstance()._unsentEvents[1].event_properties, expected); }); }); @@ -520,7 +672,7 @@ describe('Amplitude', function() { }); it('should run queued functions', function() { - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); assert.lengthOf(server.requests, 0); var userId = 'testUserId' var eventType = 'test_event' @@ -528,18 +680,18 @@ describe('Amplitude', function() { ['setUserId', userId], ['logEvent', eventType] ]; - amplitude._q = functions; - assert.lengthOf(amplitude._q, 2); + amplitude.getInstance()._q = functions; + assert.lengthOf(amplitude.getInstance()._q, 2); amplitude.runQueuedFunctions(); assert.equal(amplitude.options.userId, userId); - assert.equal(amplitude._unsentCount(), 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); assert.equal(events[0].event_type, eventType); - assert.lengthOf(amplitude._q, 0); + assert.lengthOf(amplitude.getInstance()._q, 0); }); }); @@ -553,12 +705,12 @@ describe('Amplitude', function() { }); it('should log identify call from set user properties', function() { - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._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(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -585,12 +737,12 @@ describe('Amplitude', function() { }); it('should log identify call from clear user properties', function() { - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); amplitude.clearUserProperties(); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -660,12 +812,12 @@ describe('setVersionName', function() { amplitude.init(apiKey, null, {batchEvents: true}); amplitude.setVersionName('testVersionName1'); amplitude.logEvent('testEvent1'); - assert.equal(amplitude._unsentEvents[0].version_name, 'testVersionName1'); + assert.equal(amplitude.getInstance()._unsentEvents[0].version_name, 'testVersionName1'); // should ignore non-string values amplitude.setVersionName(15000); amplitude.logEvent('testEvent2'); - assert.equal(amplitude._unsentEvents[1].version_name, 'testVersionName1'); + assert.equal(amplitude.getInstance()._unsentEvents[1].version_name, 'testVersionName1'); }); }); @@ -740,19 +892,19 @@ describe('setVersionName', function() { it('should ignore inputs that are not identify objects', function() { amplitude.identify('This is a test'); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify(150); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify(['test']); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify({'user_prop': true}); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); @@ -760,9 +912,9 @@ describe('setVersionName', 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(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -786,17 +938,17 @@ describe('setVersionName', function() { it('should ignore empty identify objects', function() { amplitude.identify(new Identify()); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); it('should ignore empty proxy identify objects', function() { amplitude.identify({'_q': {}}); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); amplitude.identify({}); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 0); }); @@ -811,9 +963,9 @@ describe('setVersionName', function() { ]}; amplitude.identify(proxyObject); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 1); @@ -952,11 +1104,11 @@ describe('setVersionName', function() { }); it('should queue events', function() { - amplitude._sending = true; + amplitude.getInstance()._sending = true; amplitude.logEvent('Event', {index: 1}); amplitude.logEvent('Event', {index: 2}); amplitude.logEvent('Event', {index: 3}); - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -970,11 +1122,11 @@ describe('setVersionName', function() { it('should limit events queued', function() { amplitude.init(apiKey, null, {savedMaxCount: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -986,10 +1138,10 @@ describe('setVersionName', function() { }); it('should remove only sent events', function() { - amplitude._sending = true; + amplitude.getInstance()._sending = true; amplitude.logEvent('Event', {index: 1}); amplitude.logEvent('Event', {index: 2}); - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 3}); server.respondWith('success'); @@ -1011,7 +1163,7 @@ describe('setVersionName', function() { var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.deepEqual(amplitude2._unsentEvents, amplitude._unsentEvents); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, amplitude.getInstance()._unsentEvents); }); it('should not save events', function() { @@ -1022,17 +1174,17 @@ describe('setVersionName', function() { var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.deepEqual(amplitude2._unsentEvents, []); + assert.deepEqual(amplitude2.getInstance()._unsentEvents, []); }); it('should limit events sent', function() { amplitude.init(apiKey, null, {uploadBatchSize: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -1074,7 +1226,7 @@ describe('setVersionName', function() { server.respond(); assert.lengthOf(server.requests, 1); - var unsentEvents = amplitude._unsentEvents; + var unsentEvents = amplitude.getInstance()._unsentEvents; assert.lengthOf(unsentEvents, 5); assert.deepEqual(unsentEvents[4].event_properties, {index: 14}); @@ -1083,7 +1235,7 @@ describe('setVersionName', function() { assert.lengthOf(server.requests, 2); server.respondWith('success'); server.respond(); - assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); var events = JSON.parse(querystring.parse(server.requests[1].requestBody).e); assert.lengthOf(events, 5); assert.deepEqual(events[4].event_properties, {index: 14}); @@ -1099,7 +1251,7 @@ describe('setVersionName', function() { amplitude.logEvent('Event'); // saveEvent should not have been called yet - assert.lengthOf(amplitude._unsentEvents, 1); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); assert.lengthOf(server.requests, 0); // saveEvent should be called after delay @@ -1123,7 +1275,7 @@ describe('setVersionName', function() { amplitude.logEvent('Event2'); // saveEvent triggered by 2 event batch threshold - assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); assert.lengthOf(server.requests, 1); server.respondWith('success'); server.respond(); @@ -1132,7 +1284,7 @@ describe('setVersionName', function() { assert.deepEqual(events[1].event_type, 'Event2'); // saveEvent should be called after delay, but no request made - assert.lengthOf(amplitude._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); clock.tick(eventUploadPeriodMillis); assert.lengthOf(server.requests, 1); }); @@ -1149,7 +1301,7 @@ describe('setVersionName', function() { amplitude.logEvent('Event1'); clock.tick(1); amplitude.logEvent('Event2'); - assert.lengthOf(amplitude._unsentEvents, 2); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 2); assert.lengthOf(server.requests, 0); // advance to upload period millis, and should have 1 server request @@ -1174,11 +1326,11 @@ describe('setVersionName', function() { it('should back off on 413 status', function() { amplitude.init(apiKey, null, {uploadBatchSize: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 100}); @@ -1201,11 +1353,11 @@ describe('setVersionName', function() { it('should back off on 413 status all the way to 1 event with drops', function() { amplitude.init(apiKey, null, {uploadBatchSize: 9}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 10; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 100}); for (var i = 0; i < 6; i++) { @@ -1320,11 +1472,11 @@ describe('setVersionName', function() { }; // queue up 15 events, since batchsize 10, need to send in 2 batches - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.logEvent('Event', {index: 100}, callback); @@ -1358,11 +1510,11 @@ describe('setVersionName', function() { }; // queue up 15 events - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { amplitude.logEvent('Event', {index: i}); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; // 16th event with 413 will backoff to batches of 8 amplitude.logEvent('Event', {index: 100}, callback); @@ -1419,18 +1571,18 @@ describe('setVersionName', function() { it('should send 3 identify events', function() { amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); amplitude.identify(new Identify().add('photoCount', 1)); amplitude.identify(new Identify().add('photoCount', 1).set('country', 'USA')); amplitude.identify(new Identify().add('photoCount', 1)); // verify some internal counters - assert.equal(amplitude._eventId, 0); - assert.equal(amplitude._identifyId, 3); - assert.equal(amplitude._unsentCount(), 3); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 3); + assert.equal(amplitude.getInstance()._eventId, 0); + assert.equal(amplitude.getInstance()._identifyId, 3); + assert.equal(amplitude.getInstance()._unsentCount(), 3); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 3); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1446,24 +1598,24 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should send 3 events', function() { amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._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.equal(amplitude.getInstance()._eventId, 3); + assert.equal(amplitude.getInstance()._identifyId, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 3); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 3); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1477,23 +1629,23 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); }); it('should send 1 event and 1 identify event', function() { amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 2}); - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._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.equal(amplitude.getInstance()._eventId, 1); + assert.equal(amplitude.getInstance()._identifyId, 1); + assert.equal(amplitude.getInstance()._unsentCount(), 2); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 1); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1513,14 +1665,14 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should properly coalesce events and identify events into a request', function() { amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 6}); - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); amplitude.logEvent('test1'); clock.tick(1); @@ -1534,11 +1686,11 @@ describe('setVersionName', function() { 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.equal(amplitude.getInstance()._eventId, 4); + assert.equal(amplitude.getInstance()._identifyId, 2); + assert.equal(amplitude.getInstance()._unsentCount(), 6); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 4); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 2); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1569,28 +1721,28 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should merged events supporting backwards compatability', function() { // events logged before v2.5.0 won't have sequence number, should get priority amplitude.init(apiKey, null, {batchEvents: true, eventUploadThreshold: 3}); - assert.equal(amplitude._unsentCount(), 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); amplitude.identify(new Identify().add('photoCount', 1)); amplitude.logEvent('test'); - delete amplitude._unsentEvents[0].sequence_number; // delete sequence number to simulate old event - amplitude._sequenceNumber = 1; // reset sequence number + delete amplitude.getInstance()._unsentEvents[0].sequence_number; // delete sequence number to simulate old event + amplitude.getInstance()._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.equal(amplitude.getInstance()._eventId, 1); + assert.equal(amplitude.getInstance()._identifyId, 2); + assert.equal(amplitude.getInstance()._unsentCount(), 3); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 1); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 2); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -1617,9 +1769,9 @@ describe('setVersionName', function() { // send response and check that remove events works properly server.respondWith('success'); server.respond(); - assert.equal(amplitude._unsentCount(), 0); - assert.lengthOf(amplitude._unsentEvents, 0); - assert.lengthOf(amplitude._unsentIdentifys, 0); + assert.equal(amplitude.getInstance()._unsentCount(), 0); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 0); + assert.lengthOf(amplitude.getInstance()._unsentIdentifys, 0); }); it('should drop event and keep identify on 413 response', function() { @@ -1628,21 +1780,21 @@ describe('setVersionName', function() { clock.tick(1); amplitude.identify(new Identify().add('photoCount', 1)); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 1); server.respondWith([413, {}, '']); server.respond(); // backoff and retry assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 2); server.respondWith([413, {}, '']); server.respond(); // after dropping massive event, only 1 event left assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 3); var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); @@ -1658,21 +1810,21 @@ describe('setVersionName', function() { clock.tick(1); amplitude.logEvent('test'); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 1); server.respondWith([413, {}, '']); server.respond(); // backoff and retry assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 2); + assert.equal(amplitude.getInstance()._unsentCount(), 2); assert.lengthOf(server.requests, 2); server.respondWith([413, {}, '']); server.respond(); // after dropping massive event, only 1 event left assert.equal(amplitude.options.uploadBatchSize, 1); - assert.equal(amplitude._unsentCount(), 1); + assert.equal(amplitude.getInstance()._unsentCount(), 1); assert.lengthOf(server.requests, 3); var events = JSON.parse(querystring.parse(server.requests[2].requestBody).e); @@ -1755,7 +1907,7 @@ describe('setVersionName', function() { }); clock.tick(1); - assert.lengthOf(amplitude._unsentEvents, 5); + assert.lengthOf(amplitude.getInstance()._unsentEvents, 5); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.lengthOf(events, 5); @@ -1781,7 +1933,7 @@ describe('setVersionName', function() { amplitude.init(apiKey, null, {batchEvents: true}); amplitude.identify(identify); - assert.deepEqual(amplitude._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); + assert.deepEqual(amplitude.getInstance()._unsentIdentifys[0].user_properties, {'$set': {'10': 10}}); }); it('should synchronize event data across multiple amplitude instances that share the same cookie', function() { @@ -1799,14 +1951,14 @@ describe('setVersionName', function() { amplitude1.logEvent('test5'); // the event ids should all be sequential since amplitude1 and amplitude2 have synchronized cookies - var eventId = amplitude1._unsentEvents[0]['event_id']; - assert.equal(amplitude2._unsentEvents[0]['event_id'], eventId + 1); - assert.equal(amplitude1._unsentEvents[1]['event_id'], eventId + 2); - assert.equal(amplitude2._unsentEvents[1]['event_id'], eventId + 3); + var eventId = amplitude1.getInstance()._unsentEvents[0]['event_id']; + assert.equal(amplitude2.getInstance()._unsentEvents[0]['event_id'], eventId + 1); + assert.equal(amplitude1.getInstance()._unsentEvents[1]['event_id'], eventId + 2); + assert.equal(amplitude2.getInstance()._unsentEvents[1]['event_id'], eventId + 3); - var sequenceNumber = amplitude1._unsentEvents[0]['sequence_number']; - assert.equal(amplitude2._unsentIdentifys[0]['sequence_number'], sequenceNumber + 4); - assert.equal(amplitude1._unsentEvents[2]['sequence_number'], sequenceNumber + 5); + var sequenceNumber = amplitude1.getInstance()._unsentEvents[0]['sequence_number']; + assert.equal(amplitude2.getInstance()._unsentIdentifys[0]['sequence_number'], sequenceNumber + 4); + assert.equal(amplitude1.getInstance()._unsentEvents[2]['sequence_number'], sequenceNumber + 5); }); it('should handle groups input', function() { @@ -1877,7 +2029,7 @@ describe('setVersionName', function() { amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.setOptOut(true); amplitude.init(apiKey); assert.lengthOf(server.requests, 1); @@ -1911,11 +2063,11 @@ describe('setVersionName', function() { it('should limit identify events queued', function() { amplitude.init(apiKey, null, {savedMaxCount: 10}); - amplitude._sending = true; + amplitude.getInstance()._sending = true; for (var i = 0; i < 15; i++) { amplitude.identify(new Identify().add('test', i)); } - amplitude._sending = false; + amplitude.getInstance()._sending = false; amplitude.identify(new Identify().add('test', 100)); assert.lengthOf(server.requests, 1); @@ -1979,7 +2131,7 @@ describe('setVersionName', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; - amplitude._initUtmData(utmParams); + amplitude.getInstance()._initUtmData(utmParams); var expectedProperties = { utm_campaign: 'new', @@ -2027,7 +2179,7 @@ describe('setVersionName', function() { cookie.set('__utmz', '133232535.1424926227.1.1.utmcct=top&utmccn=new'); var utmParams = '?utm_source=amplitude&utm_medium=email&utm_term=terms'; - amplitude._initUtmData(utmParams); + amplitude.getInstance()._initUtmData(utmParams); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); @@ -2053,11 +2205,11 @@ describe('setVersionName', function() { describe('gatherReferrer', function() { beforeEach(function() { amplitude.init(apiKey); - sinon.stub(amplitude, '_getReferrer').returns('https://amplitude.com/contact'); + sinon.stub(amplitude.getInstance(), '_getReferrer').returns('https://amplitude.com/contact'); }); afterEach(function() { - amplitude._getReferrer.restore(); + amplitude.getInstance()._getReferrer.restore(); reset(); }); @@ -2129,7 +2281,7 @@ describe('setVersionName', 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.getInstance()._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); @@ -2314,15 +2466,15 @@ describe('setVersionName', function() { }); it('should create new session IDs on timeout', function() { - var sessionId = amplitude._sessionId; + var sessionId = amplitude.getInstance()._sessionId; clock.tick(30 * 60 * 1000 + 1); amplitude.logEvent('Event Type 1'); assert.lengthOf(server.requests, 1); var events = JSON.parse(querystring.parse(server.requests[0].requestBody).e); assert.equal(events.length, 1); assert.notEqual(events[0].session_id, sessionId); - assert.notEqual(amplitude._sessionId, sessionId); - assert.equal(events[0].session_id, amplitude._sessionId); + assert.notEqual(amplitude.getInstance()._sessionId, sessionId); + assert.equal(events[0].session_id, amplitude.getInstance()._sessionId); }); it('should be fetched correctly by getSessionId', function() { @@ -2330,9 +2482,9 @@ describe('setVersionName', function() { clock.tick(timestamp); var amplitude2 = new Amplitude(); amplitude2.init(apiKey); - assert.equal(amplitude2._sessionId, timestamp); + assert.equal(amplitude2.getInstance()._sessionId, timestamp); assert.equal(amplitude2.getSessionId(), timestamp); - assert.equal(amplitude2.getSessionId(), amplitude2._sessionId); + assert.equal(amplitude2.getSessionId(), amplitude2.getInstance()._sessionId); }); }); }); diff --git a/test/browser/amplitudejs.html b/test/browser/amplitudejs.html index bba74070..43cce0a3 100644 --- a/test/browser/amplitudejs.html +++ b/test/browser/amplitudejs.html @@ -2,7 +2,7 @@

Amplitude JS Test

diff --git a/test/browser/amplitudejs2.html b/test/browser/amplitudejs2.html index 8cb502c6..f2c1fcfe 100644 --- a/test/browser/amplitudejs2.html +++ b/test/browser/amplitudejs2.html @@ -2,7 +2,7 @@