var events = []; function trackMixpanelEvent(event,prop,callBckFun){ if(typeof mixpanel != null && typeof mixpanel == 'object' && typeof(mixpanel.track) === "function" ) { mixpanel.track(event, prop, function(){ if(callBckFun!=undefined){ callBckFun(); } mixpanel.people.set('$last_seen',new Date()); }); }else{ events.push({"event":event,"prop":prop,"callBckFun":callBckFun}); } } function trackArticleViewed(feedId, companyId, source, origin, companyName) { var articleViewedProperty = {}; articleViewedProperty["Article Id"] = feedId; articleViewedProperty["Company Id"] = companyId; articleViewedProperty["Feed Url"] = source; if (source == undefined) { articleViewedProperty["Feed Url"] = 'N/A'; } articleViewedProperty["Location"] = origin; articleViewedProperty["Company Name"] = companyName; trackMixpanelEvent("Article Viewed", articleViewedProperty); mixpanel.people.increment("Articles Viewed", 1); } function trackPollResponse(companyId,companyName,insightCategory,insightType,origin,option) { var property={}; property["Company Id"] = companyId; property["Company Name"] = companyName; property["Type of Survey"] = insightCategory; property["Survey Name"] = insightType; property["Location of Survey"] = origin; property["Don't Know"] = "False"; if(option === "dont_know") { property["Don't Know"] = "True"; } trackMixpanelEvent("SurveyResponse",property); mixpanel.people.set({'$last_seen':new Date()}); mixpanel.people.increment("Polls Taken", 1); } function flushEvents(){ if(typeof mixpanel != null){ for(var i=0;i this.options.sessionTimeout) { this._newSession = true; this._sessionId = now; // only capture UTM params and referrer if new session if (this.options.saveParamsReferrerOncePerSession) { this._trackParamsAndReferrer(); } } if (!this.options.saveParamsReferrerOncePerSession) { this._trackParamsAndReferrer(); } this._lastEventTime = now; _saveCookieData(this); this._sendEventsIfReady(); // try sending unsent events } catch (e) { utils.log(e); } finally { if (type(opt_callback) === 'function') { opt_callback(this); } } }; /** * @private */ AmplitudeClient.prototype._trackParamsAndReferrer = function _trackParamsAndReferrer() { if (this.options.includeUtm) { this._initUtmData(); } if (this.options.includeReferrer) { this._saveReferrer(this._getReferrer()); } if (this.options.includeGclid) { this._saveGclid(this._getUrlParams()); } }; /** * 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 || this._getUrlParams(); cookieParams = cookieParams || this.cookieStorage.get('__utmz'); var utmProperties = getUtmData(cookieParams, queryParams); _sendParamsReferrerUserProperties(this, utmProperties); }; /** * The calling function should determine when it is appropriate to send these user properties. This function * will no longer contain any session storage checking logic. * @private */ var _sendParamsReferrerUserProperties = function _sendParamsReferrerUserProperties(scope, 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]); identify.set(key, userProperties[key]); } } scope.identify(identify); }; /** * @private */ AmplitudeClient.prototype._getReferrer = function _getReferrer() { return document.referrer; }; /** * @private */ AmplitudeClient.prototype._getUrlParams = function _getUrlParams() { return location.search; }; /** * Try to fetch Google Gclid from url params. * @private */ AmplitudeClient.prototype._saveGclid = function _saveGclid(urlParams) { var gclid = utils.getQueryParam('gclid', urlParams); if (utils.isEmptyString(gclid)) { return; } var gclidProperties = {'gclid': gclid}; _sendParamsReferrerUserProperties(this, gclidProperties); }; /** * Try to fetch Amplitude device id from url params. * @private */ AmplitudeClient.prototype._getDeviceIdFromUrlParam = function _getDeviceIdFromUrlParam(urlParams) { return utils.getQueryParam(Constants.AMP_DEVICE_ID_PARAM, urlParams); }; /** * 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) }; _sendParamsReferrerUserProperties(this, 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, 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; } // sanitize the userProperties dict before converting into identify var sanitized = utils.truncate(utils.validateProperties(userProperties)); if (Object.keys(sanitized).length === 0) { return; } // convert userProperties into an identify call var identify = new Identify(); for (var property in sanitized) { if (sanitized.hasOwnProperty(property)) { identify.set(property, sanitized[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, 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, timestamp, 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 = (type(timestamp) === 'number') ? timestamp : 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)), user_agent: this._userAgent // 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) { return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback); }; /** * Log an event with eventType and eventProperties and a custom timestamp * @public * @param {string} eventType - name of event * @param {object} eventProperties - (optional) an object with string keys and values for the event properties. * @param {number} timesatmp - (optional) the custom timestamp as milliseconds since epoch. * @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.logEventWithTimestamp = function logEvent(eventType, eventProperties, timestamp, 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, timestamp, 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, null, opt_callback); }; /** * Test that n is a number or a numeric value. * @private */ var _isNumber = function _isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; /** * Log revenue 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, 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 protocol = this.options.forceHttps ? 'https' : ('https:' === window.location.protocol ? 'https' : 'http'); var url = protocol + '://' + 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; }, {"./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, MAX_PROPERTY_KEYS: 1000, IDENTIFY_EVENT: '$identify', // localStorageKeys LAST_EVENT_ID: 'amplitude_lastEventId', LAST_EVENT_TIME: 'amplitude_lastEventTime', LAST_IDENTIFY_ID: 'amplitude_lastIdentifyId', LAST_SEQUENCE_NUMBER: 'amplitude_lastSequenceNumber', SESSION_ID: 'amplitude_sessionId', // Used in cookie as well DEVICE_ID: 'amplitude_deviceId', OPT_OUT: 'amplitude_optOut', USER_ID: 'amplitude_userId', COOKIE_TEST: 'amplitude_cookie_test', // revenue keys REVENUE_EVENT: 'revenue_amount', REVENUE_PRODUCT_ID: '$productId', REVENUE_QUANTITY: '$quantity', REVENUE_PRICE: '$price', REVENUE_REVENUE_TYPE: '$revenueType', AMP_DEVICE_ID_PARAM: 'amp_device_id' // url param }; }, {}], 12: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* * Abstraction layer for cookie storage. * Uses cookie if available, otherwise fallback to localstorage. */ var Constants = require('./constants'); var Cookie = require('./cookie'); var JSON = require('json'); // jshint ignore:line var localStorage = require('./localstorage'); // jshint ignore:line var cookieStorage = function() { this.storage = null; }; // test that cookies are enabled - navigator.cookiesEnabled yields false positives in IE, need to test directly cookieStorage.prototype._cookiesEnabled = function() { var uid = String(new Date()); var result; try { Cookie.set(Constants.COOKIE_TEST, uid); result = Cookie.get(Constants.COOKIE_TEST) === uid; Cookie.remove(Constants.COOKIE_TEST); return result; } catch (e) { // cookies are not enabled } return false; }; cookieStorage.prototype.getStorage = function() { if (this.storage !== null) { return this.storage; } if (this._cookiesEnabled()) { this.storage = Cookie; } else { // if cookies disabled, fallback to localstorage // note: localstorage does not persist across subdomains var keyPrefix = 'amp_cookiestore_'; this.storage = { _options: { expirationDays: undefined, domain: undefined }, reset: function() { this._options = { expirationDays: undefined, domain: undefined }; }, options: function(opts) { if (arguments.length === 0) { return this._options; } opts = opts || {}; this._options.expirationDays = opts.expirationDays || this._options.expirationDays; // localStorage is specific to subdomains this._options.domain = opts.domain || this._options.domain || window.location.hostname; return this._options; }, get: function(name) { try { return JSON.parse(localStorage.getItem(keyPrefix + name)); } catch (e) { } return null; }, set: function(name, value) { try { localStorage.setItem(keyPrefix + name, JSON.stringify(value)); return true; } catch (e) { } return false; }, remove: function(name) { try { localStorage.removeItem(keyPrefix + name); } catch (e) { return false; } } }; } return this.storage; }; module.exports = cookieStorage; }, {"./constants":4,"./cookie":20,"json":14,"./localstorage":15}], 20: [function(require, module, exports) { /* * Cookie data */ var Base64 = require('./base64'); var JSON = require('json'); // jshint ignore:line var topDomain = require('top-domain'); var utils = require('./utils'); var _options = { expirationDays: undefined, domain: undefined }; var reset = function() { _options = { expirationDays: undefined, domain: undefined }; }; var options = function(opts) { if (arguments.length === 0) { return _options; } opts = opts || {}; _options.expirationDays = opts.expirationDays; var domain = (!utils.isEmptyString(opts.domain)) ? opts.domain : '.' + topDomain(window.location.href); var token = Math.random(); _options.domain = domain; set('amplitude_test', token); var stored = get('amplitude_test'); if (!stored || stored !== token) { domain = null; } remove('amplitude_test'); _options.domain = domain; }; var _domainSpecific = function(name) { // differentiate between cookies on different domains var suffix = ''; if (_options.domain) { suffix = _options.domain.charAt(0) === '.' ? _options.domain.substring(1) : _options.domain; } return name + suffix; }; var get = function(name) { try { var nameEq = _domainSpecific(name) + '='; var ca = document.cookie.split(';'); var value = null; for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) === ' ') { c = c.substring(1, c.length); } if (c.indexOf(nameEq) === 0) { value = c.substring(nameEq.length, c.length); break; } } if (value) { return JSON.parse(Base64.decode(value)); } return null; } catch (e) { return null; } }; var set = function(name, value) { try { _set(_domainSpecific(name), Base64.encode(JSON.stringify(value)), _options); return true; } catch (e) { return false; } }; var _set = function(name, value, opts) { var expires = value !== null ? opts.expirationDays : -1 ; if (expires) { var date = new Date(); date.setTime(date.getTime() + (expires * 24 * 60 * 60 * 1000)); expires = date; } var str = name + '=' + value; if (expires) { str += '; expires=' + expires.toUTCString(); } str += '; path=/'; if (opts.domain) { str += '; domain=' + opts.domain; } document.cookie = str; }; var remove = function(name) { try { _set(_domainSpecific(name), null, _options); return true; } catch (e) { return false; } }; module.exports = { reset: reset, options: options, get: get, set: set, remove: remove }; }, {"./base64":21,"json":14,"top-domain":22,"./utils":9}], 21: [function(require, module, exports) { /* jshint bitwise: false */ /* global escape, unescape */ var UTF8 = require('./utf8'); /* * Base64 encoder/decoder * http://www.webtoolkit.info/ */ var Base64 = { _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', encode: function (input) { try { if (window.btoa && window.atob) { return window.btoa(unescape(encodeURIComponent(input))); } } catch (e) { //log(e); } return Base64._encode(input); }, _encode: function (input) { var output = ''; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = UTF8.encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 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) { //log(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 < input.length) { enc1 = Base64._keyStr.indexOf(input.charAt(i++)); enc2 = Base64._keyStr.indexOf(input.charAt(i++)); enc3 = Base64._keyStr.indexOf(input.charAt(i++)); enc4 = Base64._keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 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) { /* jshint bitwise: false */ /* * UTF-8 encoder/decoder * http://www.webtoolkit.info/ */ var UTF8 = { encode: function (s) { var utftext = ''; for (var n = 0; n < s.length; n++) { var c = s.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if((c > 127) && (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 ( i < utftext.length ) { c = utftext.charCodeAt(i); if (c < 128) { s += String.fromCharCode(c); i++; } else if((c > 191) && (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) { /* json2.js 2014-02-04 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. (function () { 'use strict'; var JSON = module.exports = {}; function f(n) { // Format integers to have at least two digits. 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) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. 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) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }()); }, {}], 22: [function(require, module, exports) { /** * Module dependencies. */ var parse = require('url').parse; /** * Expose `domain` */ module.exports = domain; /** * RegExp */ var regexp = /[a-z0-9][a-z0-9\-]*[a-z0-9]\.[a-z\.]{2,6}$/i; /** * Get the top domain. * * Official Grammar: http://tools.ietf.org/html/rfc883#page-56 * Look for tlds with up to 2-6 characters. * * Example: * * domain('http://localhost:3000/baz'); * // => '' * domain('http://dev:3000/baz'); * // => '' * domain('http://127.0.0.1:3000/baz'); * // => '' * domain('http://segment.io/baz'); * // => 'segment.io' * * @param {String} url * @return {String} * @api public */ function domain(url){ var host = parse(url).hostname; var match = host.match(regexp); return match ? match[0] : ''; }; }, {"url":25}], 25: [function(require, module, exports) { /** * Parse the given `url`. * * @param {String} str * @return {Object} * @api public */ exports.parse = function(url){ var a = document.createElement('a'); a.href = url; return { href: a.href, host: a.host || location.host, port: ('0' === a.port || '' === a.port) ? port(a.protocol) : a.port, hash: a.hash, hostname: a.hostname || location.hostname, pathname: a.pathname.charAt(0) != '/' ? '/' + a.pathname : a.pathname, protocol: !a.protocol || ':' == a.protocol ? location.protocol : a.protocol, search: a.search, query: a.search.slice(1) }; }; /** * Check if `url` is absolute. * * @param {String} url * @return {Boolean} * @api public */ exports.isAbsolute = function(url){ return 0 == url.indexOf('//') || !!~url.indexOf('://'); }; /** * Check if `url` is relative. * * @param {String} url * @return {Boolean} * @api public */ exports.isRelative = function(url){ return !exports.isAbsolute(url); }; /** * Check if `url` is cross domain. * * @param {String} url * @return {Boolean} * @api public */ exports.isCrossDomain = function(url){ url = exports.parse(url); var location = exports.parse(window.location.href); return url.hostname !== location.hostname || url.port !== location.port || url.protocol !== location.protocol; }; /** * Return default port for `protocol`. * * @param {String} protocol * @return {String} * @api private */ function port (protocol){ switch (protocol) { case 'http:': return 80; case 'https:': return 443; default: return location.port; } } }, {}], 9: [function(require, module, exports) { var constants = require('./constants'); var type = require('./type'); var log = function log(s) { try { console.log('[Amplitude] ' + s); } catch (e) { // console logging not available } }; var isEmptyString = function isEmptyString(str) { return (!str || str.length === 0); }; var sessionStorageEnabled = function sessionStorageEnabled() { try { if (window.sessionStorage) { return true; } } catch (e) {} // sessionStorage disabled return false; }; // truncate string values in event and user properties so that request size does not get too large var truncate = function truncate(value) { if (type(value) === 'array') { for (var i = 0; i < value.length; i++) { value[i] = truncate(value[i]); } } else if (type(value) === 'object') { for (var key in value) { if (value.hasOwnProperty(key)) { value[key] = truncate(value[key]); } } } else { value = _truncateValue(value); } return value; }; var _truncateValue = function _truncateValue(value) { if (type(value) === 'string') { return value.length > constants.MAX_STRING_LENGTH ? value.substring(0, constants.MAX_STRING_LENGTH) : value; } return value; }; var validateInput = function validateInput(input, name, expectedType) { if (type(input) !== expectedType) { log('Invalid ' + name + ' input type. Expected ' + expectedType + ' but received ' + type(input)); return false; } return true; }; // do some basic sanitization and type checking, also catch property dicts with more than 1000 key/value pairs var validateProperties = function validateProperties(properties) { var propsType = type(properties); if (propsType !== 'object') { log('Error: invalid properties format. Expecting Javascript object, received ' + propsType + ', ignoring'); return {}; } if (Object.keys(properties).length > constants.MAX_PROPERTY_KEYS) { log('Error: too many properties (more than 1000), ignoring'); return {}; } var copy = {}; // create a copy with all of the valid properties for (var property in properties) { if (!properties.hasOwnProperty(property)) { continue; } // validate key var key = property; var keyType = type(key); if (keyType !== 'string') { key = String(key); log('WARNING: Non-string property key, received type ' + keyType + ', coercing to string "' + key + '"'); } // validate value var value = validatePropertyValue(key, properties[property]); if (value === null) { continue; } copy[key] = value; } return copy; }; var invalidValueTypes = [ 'null', 'nan', 'undefined', 'function', 'arguments', 'regexp', 'element' ]; var validatePropertyValue = function validatePropertyValue(key, value) { var valueType = type(value); if (invalidValueTypes.indexOf(valueType) !== -1) { log('WARNING: Property key "' + key + '" with invalid value type ' + valueType + ', ignoring'); value = null; } else if (valueType === 'error') { value = String(value); log('WARNING: Property key "' + key + '" with value type error, coercing to ' + value); } else if (valueType === 'array') { // check for nested arrays or objects var arrayCopy = []; for (var i = 0; i < value.length; i++) { var element = value[i]; var elemType = type(element); if (elemType === 'array' || elemType === 'object') { log('WARNING: Cannot have ' + elemType + ' nested in an array property value, skipping'); continue; } arrayCopy.push(validatePropertyValue(key, element)); } value = arrayCopy; } else if (valueType === 'object') { value = validateProperties(value); } return value; }; var validateGroups = function validateGroups(groups) { var groupsType = type(groups); if (groupsType !== 'object') { log('Error: invalid groups format. Expecting Javascript object, received ' + groupsType + ', ignoring'); return {}; } var copy = {}; // create a copy with all of the valid properties for (var group in groups) { if (!groups.hasOwnProperty(group)) { continue; } // validate key var key = group; var keyType = type(key); if (keyType !== 'string') { key = String(key); log('WARNING: Non-string groupType, received type ' + keyType + ', coercing to string "' + key + '"'); } // validate value var value = validateGroupName(key, groups[group]); if (value === null) { continue; } copy[key] = value; } return copy; }; var validateGroupName = function validateGroupName(key, groupName) { var groupNameType = type(groupName); if (groupNameType === 'string') { return groupName; } if (groupNameType === 'date' || groupNameType === 'number' || groupNameType === 'boolean') { groupName = String(groupName); log('WARNING: Non-string groupName, received type ' + groupNameType + ', coercing to string "' + groupName + '"'); return groupName; } if (groupNameType === 'array') { // check for nested arrays or objects var arrayCopy = []; for (var i = 0; i < groupName.length; i++) { var element = groupName[i]; var elemType = type(element); if (elemType === 'array' || elemType === 'object') { log('WARNING: Skipping nested ' + elemType + ' in array groupName'); continue; } else if (elemType === 'string') { arrayCopy.push(element); } else if (elemType === 'date' || elemType === 'number' || elemType === 'boolean') { element = String(element); log('WARNING: Non-string groupName, received type ' + elemType + ', coercing to string "' + element + '"'); arrayCopy.push(element); } } return arrayCopy; } log('WARNING: Non-string groupName, received type ' + groupNameType + '. Please use strings or array of strings for groupName'); }; // parses the value of a url param (for example ?gclid=1234&...) var getQueryParam = function getQueryParam(name, query) { name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); var results = regex.exec(query); return results === null ? undefined : decodeURIComponent(results[1].replace(/\+/g, " ")); }; module.exports = { log: log, isEmptyString: isEmptyString, getQueryParam: getQueryParam, sessionStorageEnabled: sessionStorageEnabled, truncate: truncate, validateGroups: validateGroups, validateInput: validateInput, validateProperties: validateProperties }; }, {"./constants":4,"./type":8}], 8: [function(require, module, exports) { /** * toString ref. * @private */ var toString = Object.prototype.toString; /** * Return the type of `val`. * @private * @param {Mixed} val * @return {String} * @api public */ module.exports = function(val){ switch (toString.call(val)) { case '[object Date]': return 'date'; case '[object RegExp]': return 'regexp'; case '[object Arguments]': return 'arguments'; case '[object Array]': return 'array'; case '[object Error]': return 'error'; } if (val === null) { return 'null'; } if (val === undefined) { return 'undefined'; } if (val !== val) { return 'nan'; } if (val && val.nodeType === 1) { return 'element'; } if (typeof Buffer !== 'undefined' && Buffer.isBuffer(val)) { return 'buffer'; } val = val.valueOf ? val.valueOf() : Object.prototype.valueOf.apply(val); return typeof val; }; }, {}], 15: [function(require, module, exports) { /* jshint -W020, unused: false, noempty: false, boss: true */ /* * Implement localStorage to support Firefox 2-3 and IE 5-7 */ var localStorage; // jshint ignore:line // test that Window.localStorage is available and works function windowLocalStorageAvailable() { var uid = new Date(); var result; try { window.localStorage.setItem(uid, uid); result = window.localStorage.getItem(uid) === String(uid); window.localStorage.removeItem(uid); return result; } catch (e) { // localStorage not available } return false; } if (windowLocalStorageAvailable()) { localStorage = window.localStorage; } else if (window.globalStorage) { // Firefox 2-3 use globalStorage // See https://developer.mozilla.org/en/dom/storage#globalStorage try { localStorage = window.globalStorage[window.location.hostname]; } catch (e) { // Something bad happened... } } else { // IE 5-7 use userData // See http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx var div = document.createElement('div'), attrKey = 'localStorage'; div.style.display = 'none'; document.getElementsByTagName('head')[0].appendChild(div); if (div.addBehavior) { div.addBehavior('#default#userdata'); localStorage = { length: 0, setItem: function(k, v) { div.load(attrKey); if (!div.getAttribute(k)) { this.length++; } div.setAttribute(k, v); div.save(attrKey); }, getItem: function(k) { div.load(attrKey); return div.getAttribute(k); }, removeItem: function(k) { div.load(attrKey); if (div.getAttribute(k)) { this.length--; } div.removeAttribute(k); div.save(attrKey); }, clear: function() { div.load(attrKey); var i = 0; var attr; while (attr = div.XMLDocument.documentElement.attributes[i++]) { div.removeAttribute(attr.name); } div.save(attrKey); this.length = 0; }, key: function(k) { div.load(attrKey); return div.XMLDocument.documentElement.attributes[k]; } }; div.load(attrKey); localStorage.length = div.XMLDocument.documentElement.attributes.length; } else { /* Nothing we can do ... */ } } if (!localStorage) { localStorage = { length: 0, setItem: function(k, v) { }, getItem: function(k) { }, removeItem: function(k) { }, clear: function() { }, key: function(k) { } }; } module.exports = localStorage; }, {}], 13: [function(require, module, exports) { var utils = require('./utils'); var getUtmData = function getUtmData(rawCookie, query) { // Translate the utmz cookie format into url query string format. var cookie = rawCookie ? '?' + rawCookie.split('.').slice(-1)[0].replace(/\|/g, '&') : ''; var fetchParam = function fetchParam(queryName, query, cookieName, cookie) { return utils.getQueryParam(queryName, query) || utils.getQueryParam(cookieName, cookie); }; var utmSource = fetchParam('utm_source', query, 'utmcsr', cookie); var utmMedium = fetchParam('utm_medium', query, 'utmcmd', cookie); var utmCampaign = fetchParam('utm_campaign', query, 'utmccn', cookie); var utmTerm = fetchParam('utm_term', query, 'utmctr', cookie); var utmContent = fetchParam('utm_content', query, 'utmcct', cookie); var utmData = {}; var addIfNotNull = function addIfNotNull(key, value) { if (!utils.isEmptyString(value)) { utmData[key] = value; } }; addIfNotNull('utm_source', utmSource); addIfNotNull('utm_medium', utmMedium); addIfNotNull('utm_campaign', utmCampaign); addIfNotNull('utm_term', utmTerm); addIfNotNull('utm_content', utmContent); return utmData; }; module.exports = getUtmData; }, {"./utils":9}], 5: [function(require, module, exports) { var type = require('./type'); var utils = require('./utils'); /* * Wrapper for a user properties JSON object that supports operations. * Note: if a user property is used in multiple operations on the same Identify object, * only the first operation will be saved, and the rest will be ignored. */ var AMP_OP_ADD = '$add'; var AMP_OP_APPEND = '$append'; var AMP_OP_CLEAR_ALL = '$clearAll'; var AMP_OP_PREPEND = '$prepend'; var AMP_OP_SET = '$set'; var AMP_OP_SET_ONCE = '$setOnce'; var AMP_OP_UNSET = '$unset'; /** * Identify API - instance constructor. Identify objects are a wrapper for user property operations. * Each method adds a user property operation to the Identify object, and returns the same Identify object, * allowing you to chain multiple method calls together. * Note: if the same user property is used in multiple operations on a single Identify object, * only the first operation on that property will be saved, and the rest will be ignored. * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#user-properties-and-user-property-operations} * for more information on the Identify API and user property operations. * @constructor Identify * @public * @example var identify = new amplitude.Identify(); */ var Identify = function() { this.userPropertiesOperations = {}; this.properties = []; // keep track of keys that have been added }; /** * Increment a user property by a given value (can also be negative to decrement). * If the user property does not have a value set yet, it will be initialized to 0 before being incremented. * @public * @param {string} property - The user property key. * @param {number|string} value - The amount by which to increment the user property. Allows numbers as strings (ex: '123'). * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. * @example var identify = new amplitude.Identify().add('karma', 1).add('friends', 1); * amplitude.identify(identify); // send the Identify call */ Identify.prototype.add = function(property, value) { if (type(value) === 'number' || type(value) === 'string') { this._addOperation(AMP_OP_ADD, property, value); } else { utils.log('Unsupported type for value: ' + type(value) + ', expecting number or string'); } return this; }; /** * Append a value or values to a user property. * If the user property does not have a value set yet, * it will be initialized to an empty list before the new values are appended. * If the user property has an existing value and it is not a list, * the existing value will be converted into a list with the new values appended. * @public * @param {string} property - The user property key. * @param {number|string|list|object} value - A value or values to append. * Values can be numbers, strings, lists, or object (key:value dict will be flattened). * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. * @example var identify = new amplitude.Identify().append('ab-tests', 'new-user-tests'); * identify.append('some_list', [1, 2, 3, 4, 'values']); * amplitude.identify(identify); // send the Identify call */ Identify.prototype.append = function(property, value) { this._addOperation(AMP_OP_APPEND, property, value); return this; }; /** * Clear all user properties for the current user. * SDK user should instead call amplitude.clearUserProperties() instead of using this. * $clearAll needs to be sent on its own Identify object. If there are already other operations, then don't add $clearAll. * If $clearAll already in an Identify object, don't allow other operations to be added. * @private */ Identify.prototype.clearAll = function() { if (Object.keys(this.userPropertiesOperations).length > 0) { if (!this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) { utils.log('Need to send $clearAll on its own Identify object without any other operations, skipping $clearAll'); } return this; } this.userPropertiesOperations[AMP_OP_CLEAR_ALL] = '-'; return this; }; /** * Prepend a value or values to a user property. * Prepend means inserting the value or values at the front of a list. * If the user property does not have a value set yet, * it will be initialized to an empty list before the new values are prepended. * If the user property has an existing value and it is not a list, * the existing value will be converted into a list with the new values prepended. * @public * @param {string} property - The user property key. * @param {number|string|list|object} value - A value or values to prepend. * Values can be numbers, strings, lists, or object (key:value dict will be flattened). * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. * @example var identify = new amplitude.Identify().prepend('ab-tests', 'new-user-tests'); * identify.prepend('some_list', [1, 2, 3, 4, 'values']); * amplitude.identify(identify); // send the Identify call */ Identify.prototype.prepend = function(property, value) { this._addOperation(AMP_OP_PREPEND, property, value); return this; }; /** * Sets the value of a given user property. If a value already exists, it will be overwriten with the new value. * @public * @param {string} property - The user property key. * @param {number|string|list|object} value - A value or values to set. * Values can be numbers, strings, lists, or object (key:value dict will be flattened). * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. * @example var identify = new amplitude.Identify().set('user_type', 'beta'); * identify.set('name', {'first': 'John', 'last': 'Doe'}); // dict is flattened and becomes name.first: John, name.last: Doe * amplitude.identify(identify); // send the Identify call */ Identify.prototype.set = function(property, value) { this._addOperation(AMP_OP_SET, property, value); return this; }; /** * Sets the value of a given user property only once. Subsequent setOnce operations on that user property will be ignored; * however, that user property can still be modified through any of the other operations. * Useful for capturing properties such as 'initial_signup_date', 'initial_referrer', etc. * @public * @param {string} property - The user property key. * @param {number|string|list|object} value - A value or values to set once. * Values can be numbers, strings, lists, or object (key:value dict will be flattened). * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. * @example var identify = new amplitude.Identify().setOnce('sign_up_date', '2016-04-01'); * amplitude.identify(identify); // send the Identify call */ Identify.prototype.setOnce = function(property, value) { this._addOperation(AMP_OP_SET_ONCE, property, value); return this; }; /** * Unset and remove a user property. This user property will no longer show up in a user's profile. * @public * @param {string} property - The user property key. * @return {Identify} Returns the same Identify object, allowing you to chain multiple method calls together. * @example var identify = new amplitude.Identify().unset('user_type').unset('age'); * amplitude.identify(identify); // send the Identify call */ Identify.prototype.unset = function(property) { this._addOperation(AMP_OP_UNSET, property, '-'); return this; }; /** * Helper function that adds operation to the Identify's object * Handle's filtering of duplicate user property keys, and filtering for clearAll. * @private */ Identify.prototype._addOperation = function(operation, property, value) { // check that the identify doesn't already contain a clearAll if (this.userPropertiesOperations.hasOwnProperty(AMP_OP_CLEAR_ALL)) { utils.log('This identify already contains a $clearAll operation, skipping operation ' + operation); return; } // check that property wasn't already used in this Identify if (this.properties.indexOf(property) !== -1) { utils.log('User property "' + property + '" already used in this identify, skipping operation ' + operation); return; } if (!this.userPropertiesOperations.hasOwnProperty(operation)){ this.userPropertiesOperations[operation] = {}; } this.userPropertiesOperations[operation][property] = value; this.properties.push(property); }; module.exports = Identify; }, {"./type":8,"./utils":9}], 16: [function(require, module, exports) { /* * JavaScript MD5 1.0.1 * https://github.com/blueimp/JavaScript-MD5 * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: * http://www.opensource.org/licenses/MIT * * Based on * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet * Distributed under the BSD License * See http://pajhome.org.uk/crypt/md5 for more info. */ /*jslint bitwise: true */ /*global unescape, define */ (function ($) { 'use strict'; /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. */ function safe_add(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF), msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } /* * Bitwise rotate a 32-bit number to the left. */ function bit_rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } /* * These functions implement the four basic operations the algorithm uses. */ 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); } /* * Calculate the MD5 of an array of little-endian words, and a bit length. */ function binl_md5(x, len) { /* append padding */ x[len >> 5] |= 0x80 << (len % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; var i, olda, oldb, oldc, oldd, a = 1732584193, b = -271733879, c = -1732584194, d = 271733878; for (i = 0; i < x.length; i += 16) { olda = a; oldb = b; oldc = c; oldd = d; a = md5_ff(a, b, c, d, x[i], 7, -680876936); d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5_gg(b, c, d, a, x[i], 20, -373897302); a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5_hh(d, a, b, c, x[i], 11, -358537222); c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5_ii(a, b, c, d, x[i], 6, -198630844); d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); a = safe_add(a, olda); b = safe_add(b, oldb); c = safe_add(c, oldc); d = safe_add(d, oldd); } return [a, b, c, d]; } /* * Convert an array of little-endian words to a string */ function binl2rstr(input) { var i, output = ''; for (i = 0; i < input.length * 32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); } return output; } /* * Convert a raw string to an array of little-endian words * Characters >255 have their high-byte silently ignored. */ function rstr2binl(input) { var i, output = []; output[(input.length >> 2) - 1] = undefined; for (i = 0; i < output.length; i += 1) { output[i] = 0; } for (i = 0; i < input.length * 8; i += 8) { output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); } return output; } /* * Calculate the MD5 of a raw string */ function rstr_md5(s) { return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); } /* * Calculate the HMAC-MD5, of a key and some data (raw strings) */ function rstr_hmac_md5(key, data) { var i, bkey = rstr2binl(key), ipad = [], opad = [], hash; ipad[15] = opad[15] = undefined; if (bkey.length > 16) { bkey = binl_md5(bkey, key.length * 8); } for (i = 0; i < 16; i += 1) { ipad[i] = bkey[i] ^ 0x36363636; opad[i] = bkey[i] ^ 0x5C5C5C5C; } hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); } /* * Convert a raw string to a hex string */ function rstr2hex(input) { var hex_tab = '0123456789abcdef', output = '', x, i; for (i = 0; i < input.length; i += 1) { x = input.charCodeAt(i); output += hex_tab.charAt((x >>> 4) & 0x0F) + hex_tab.charAt(x & 0x0F); } return output; } /* * Encode a string as utf-8 */ function str2rstr_utf8(input) { return unescape(encodeURIComponent(input)); } /* * Take string arguments and return either raw or hex encoded strings */ 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); } // check js environment if (typeof(exports) !== 'undefined') { // nodejs env if (typeof module !== 'undefined' && module.exports) { exports = module.exports = md5; } exports.md5 = md5; } else { // requirejs env (optional) if (typeof(define) === 'function' && define.amd) { define(function () { return md5; }); } else { // browser env $.md5 = md5; } } }(this)); }, {}], 6: [function(require, module, exports) { /** * HOP ref. */ var has = Object.prototype.hasOwnProperty; /** * Return own keys in `obj`. * * @param {Object} obj * @return {Array} * @api public */ exports.keys = Object.keys || function(obj){ var keys = []; for (var key in obj) { if (has.call(obj, key)) { keys.push(key); } } return keys; }; /** * Return own values in `obj`. * * @param {Object} obj * @return {Array} * @api public */ exports.values = function(obj){ var vals = []; for (var key in obj) { if (has.call(obj, key)) { vals.push(obj[key]); } } return vals; }; /** * Merge `b` into `a`. * * @param {Object} a * @param {Object} b * @return {Object} a * @api public */ exports.merge = function(a, b){ for (var key in b) { if (has.call(b, key)) { a[key] = b[key]; } } return a; }; /** * Return length of `obj`. * * @param {Object} obj * @return {Number} * @api public */ exports.length = function(obj){ return exports.keys(obj).length; }; /** * Check if `obj` is empty. * * @param {Object} obj * @return {Boolean} * @api public */ exports.isEmpty = function(obj){ return 0 == exports.length(obj); }; }, {}], 17: [function(require, module, exports) { var querystring = require('querystring'); /* * Simple AJAX request object */ 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 () { // status code not available from xdr, try string matching on responseText 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)); } //log('sent request to ' + this.url + ' with data ' + decodeURIComponent(queryString(this.data))); }; module.exports = Request; }, {"querystring":26}], 26: [function(require, module, exports) { /** * Module dependencies. */ var encode = encodeURIComponent; var decode = decodeURIComponent; var trim = require('trim'); var type = require('type'); /** * Parse the given query `str`. * * @param {String} str * @return {Object} * @api public */ 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; i < pairs.length; i++) { var parts = pairs[i].split('='); var key = decode(parts[0]); var m; if (m = /(\w+)\[(\d+)\]/.exec(key)) { obj[m[1]] = obj[m[1]] || []; obj[m[1]][m[2]] = decode(parts[1]); continue; } obj[parts[0]] = null == parts[1] ? '' : decode(parts[1]); } return obj; }; /** * Stringify the given `obj`. * * @param {Object} obj * @return {String} * @api public */ exports.stringify = function(obj){ if (!obj) return ''; var pairs = []; for (var key in obj) { var value = obj[key]; if ('array' == type(value)) { for (var i = 0; i < value.length; ++i) { pairs.push(encode(key + '[' + i + ']') + '=' + encode(value[i])); } continue; } pairs.push(encode(key) + '=' + encode(obj[key])); } return pairs.join('&'); }; }, {"trim":27,"type":28}], 27: [function(require, module, exports) { exports = module.exports = trim; function trim(str){ if (str.trim) return str.trim(); return str.replace(/^\s*|\s*$/g, ''); } exports.left = function(str){ if (str.trimLeft) return str.trimLeft(); return str.replace(/^\s*/, ''); }; exports.right = function(str){ if (str.trimRight) return str.trimRight(); return str.replace(/\s*$/, ''); }; }, {}], 28: [function(require, module, exports) { /** * toString ref. */ var toString = Object.prototype.toString; /** * Return the type of `val`. * * @param {Mixed} val * @return {String} * @api public */ module.exports = function(val){ switch (toString.call(val)) { case '[object Date]': return 'date'; case '[object RegExp]': return 'regexp'; case '[object Arguments]': return 'arguments'; case '[object Array]': return 'array'; case '[object Error]': return 'error'; } if (val === null) return 'null'; if (val === undefined) return 'undefined'; if (val !== val) return 'nan'; if (val && val.nodeType === 1) return 'element'; if (isBuffer(val)) return 'buffer'; val = val.valueOf ? val.valueOf() : Object.prototype.valueOf.apply(val); return typeof val; }; // code borrowed from https://github.com/feross/is-buffer/blob/master/index.js function isBuffer(obj) { return !!(obj != null && (obj._isBuffer || // For Safari 5-7 (missing Object.prototype.constructor) (obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)) )) } }, {}], 7: [function(require, module, exports) { var constants = require('./constants'); var type = require('./type'); var utils = require('./utils'); /* * Wrapper for logging Revenue data. Revenue objects get passed to amplitude.logRevenueV2 to send to Amplitude servers. * Note: price is the only required field. If quantity is not specified, then defaults to 1. */ /** * Revenue API - instance constructor. Revenue objects are a wrapper for revenue data. * Each method updates a revenue property in the Revenue object, and returns the same Revenue object, * allowing you to chain multiple method calls together. * Note: price is a required field to log revenue events. * If quantity is not specified then defaults to 1. * See [Readme]{@link https://github.com/amplitude/Amplitude-Javascript#tracking-revenue} for more information * about logging Revenue. * @constructor Revenue * @public * @example var revenue = new amplitude.Revenue(); */ var Revenue = function Revenue() { // required fields this._price = null; // optional fields this._productId = null; this._quantity = 1; this._revenueType = null; this._properties = null; }; /** * Set a value for the product identifer. * @public * @param {string} productId - The value for the product identifier. Empty and invalid strings are ignored. * @return {Revenue} Returns the same Revenue object, allowing you to chain multiple method calls together. * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ Revenue.prototype.setProductId = function setProductId(productId) { if (type(productId) !== 'string') { utils.log('Unsupported type for productId: ' + type(productId) + ', expecting string'); } else if (utils.isEmptyString(productId)) { utils.log('Invalid empty productId'); } else { this._productId = productId; } return this; }; /** * Set a value for the quantity. Note revenue amount is calculated as price * quantity. * @public * @param {number} quantity - Integer value for the quantity. If not set, quantity defaults to 1. * @return {Revenue} Returns the same Revenue object, allowing you to chain multiple method calls together. * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99).setQuantity(5); * amplitude.logRevenueV2(revenue); */ Revenue.prototype.setQuantity = function setQuantity(quantity) { if (type(quantity) !== 'number') { utils.log('Unsupported type for quantity: ' + type(quantity) + ', expecting number'); } else { this._quantity = parseInt(quantity); } return this; }; /** * Set a value for the price. This field is required for all revenue being logged. * Note revenue amount is calculated as price * quantity. * @public * @param {number} price - Double value for the quantity. * @return {Revenue} Returns the same Revenue object, allowing you to chain multiple method calls together. * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); * amplitude.logRevenueV2(revenue); */ Revenue.prototype.setPrice = function setPrice(price) { if (type(price) !== 'number') { utils.log('Unsupported type for price: ' + type(price) + ', expecting number'); } else { this._price = price; } return this; }; /** * Set a value for the revenueType (for example purchase, cost, tax, refund, etc). * @public * @param {string} revenueType - RevenueType to designate. * @return {Revenue} Returns the same Revenue object, allowing you to chain multiple method calls together. * @example var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99).setRevenueType('purchase'); * amplitude.logRevenueV2(revenue); */ Revenue.prototype.setRevenueType = function setRevenueType(revenueType) { if (type(revenueType) !== 'string') { utils.log('Unsupported type for revenueType: ' + type(revenueType) + ', expecting string'); } else { this._revenueType = revenueType; } return this; }; /** * Set event properties for the revenue event. * @public * @param {object} eventProperties - Revenue event properties to set. * @return {Revenue} Returns the same Revenue object, allowing you to chain multiple method calls together. * @example var event_properties = {'city': 'San Francisco'}; * var revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99).setEventProperties(event_properties); * amplitude.logRevenueV2(revenue); */ Revenue.prototype.setEventProperties = function setEventProperties(eventProperties) { if (type(eventProperties) !== 'object') { utils.log('Unsupported type for eventProperties: ' + type(eventProperties) + ', expecting object'); } else { this._properties = utils.validateProperties(eventProperties); } return this; }; /** * @private */ Revenue.prototype._isValidRevenue = function _isValidRevenue() { if (type(this._price) !== 'number') { utils.log('Invalid revenue, need to set price field'); return false; } return true; }; /** * @private */ Revenue.prototype._toJSONObject = function _toJSONObject() { var obj = type(this._properties) === 'object' ? this._properties : {}; if (this._productId !== null) { obj[constants.REVENUE_PRODUCT_ID] = this._productId; } if (this._quantity !== null) { obj[constants.REVENUE_QUANTITY] = this._quantity; } if (this._price !== null) { obj[constants.REVENUE_PRICE] = this._price; } if (this._revenueType !== null) { obj[constants.REVENUE_REVENUE_TYPE] = this._revenueType; } return obj; }; module.exports = Revenue; }, {"./constants":4,"./type":8,"./utils":9}], 18: [function(require, module, exports) { /* jshint eqeqeq: false, forin: false */ /* global define */ /** * UAParser.js v0.7.10 * Lightweight JavaScript-based User-Agent string parser * https://github.com/faisalman/ua-parser-js * * Copyright © 2012-2015 Faisal Salman * Dual licensed under GPLv2 & MIT */ (function (window, undefined) { 'use strict'; ////////////// // Constants ///////////// var LIBVERSION = '0.7.10', EMPTY = '', UNKNOWN = '?', FUNC_TYPE = 'function', UNDEF_TYPE = 'undefined', OBJ_TYPE = 'object', STR_TYPE = 'string', MAJOR = 'major', // deprecated MODEL = 'model', NAME = 'name', TYPE = 'type', VENDOR = 'vendor', VERSION = 'version', ARCHITECTURE= 'architecture', CONSOLE = 'console', MOBILE = 'mobile', TABLET = 'tablet', SMARTTV = 'smarttv', WEARABLE = 'wearable', EMBEDDED = 'embedded'; /////////// // Helper ////////// var util = { extend : function (regexes, extensions) { var margedRegexes = {}; for (var i in regexes) { if (extensions[i] && extensions[i].length % 2 === 0) { margedRegexes[i] = extensions[i].concat(regexes[i]); } else { margedRegexes[i] = regexes[i]; } } return margedRegexes; }, has : function (str1, str2) { if (typeof str1 === "string") { return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1; } else { return false; } }, lowerize : function (str) { return str.toLowerCase(); }, major : function (version) { return typeof(version) === STR_TYPE ? version.split(".")[0] : undefined; } }; /////////////// // Map helper ////////////// var mapper = { rgx : function () { var result, i = 0, j, k, p, q, matches, match, args = arguments; // loop through all regexes maps while (i < args.length && !matches) { var regex = args[i], // even sequence (0,2,4,..) props = args[i + 1]; // odd sequence (1,3,5,..) // construct object barebones if (typeof result === UNDEF_TYPE) { result = {}; for (p in props) { if (props.hasOwnProperty(p)){ q = props[p]; if (typeof q === OBJ_TYPE) { result[q[0]] = undefined; } else { result[q] = undefined; } } } } // try matching uastring with regexes j = k = 0; while (j < regex.length && !matches) { matches = regex[j++].exec(this.getUA()); if (!!matches) { for (p = 0; p < props.length; p++) { match = matches[++k]; q = props[p]; // check if given property is actually array if (typeof q === OBJ_TYPE && q.length > 0) { if (q.length == 2) { if (typeof q[1] == FUNC_TYPE) { // assign modified match result[q[0]] = q[1].call(this, match); } else { // assign given value, ignore regex match result[q[0]] = q[1]; } } else if (q.length == 3) { // check whether function or regex if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) { // call function (usually string mapper) result[q[0]] = match ? q[1].call(this, match, q[2]) : undefined; } else { // sanitize match using given regex 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) { // check if array if (typeof map[i] === OBJ_TYPE && map[i].length > 0) { for (var j = 0; j < map[i].length; j++) { if (util.has(map[i][j], str)) { return (i === UNKNOWN) ? undefined : i; } } } else if (util.has(map[i], str)) { return (i === UNKNOWN) ? undefined : i; } } return str; } }; /////////////// // String map ////////////// var maps = { browser : { oldsafari : { version : { '1.0' : '/8', '1.2' : '/1', '1.3' : '/3', '2.0' : '/412', '2.0.2' : '/416', '2.0.3' : '/417', '2.0.4' : '/419', '?' : '/' } }, name : { 'Opera Mobile' : 'Opera Mobi', 'IE Mobile' : 'IEMobile' } }, device : { amazon : { model : { 'Fire Phone' : ['SD', 'KF'] } }, sprint : { model : { 'Evo Shift 4G' : '7373KT' }, vendor : { 'HTC' : 'APA', 'Sprint' : 'Sprint' } } }, os : { windows : { version : { 'ME' : '4.90', 'NT 3.11' : 'NT3.51', 'NT 4.0' : 'NT4.0', '2000' : 'NT 5.0', 'XP' : ['NT 5.1', 'NT 5.2'], 'Vista' : 'NT 6.0', '7' : 'NT 6.1', '8' : 'NT 6.2', '8.1' : 'NT 6.3', '10' : ['NT 6.4', 'NT 10.0'], 'RT' : 'ARM' }, name : { 'Windows Phone' : 'Windows Phone OS', } } } }; ////////////// // Regex map ///////////// var regexes = { browser : [[ // Presto based /(opera\smini)\/([\w\.-]+)/i, // Opera Mini /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet /(opera).+version\/([\w\.]+)/i, // Opera > 9.80 /(opera)[\/\s]+([\w\.]+)/i // Opera < 9.80 ], [NAME, VERSION], [ /(OPiOS)[\/\s]+([\w\.]+)/i // Opera mini on iphone >= 8.0 ], [[NAME, 'Opera Mini'], VERSION], [ /\s(opr)\/([\w\.]+)/i // Opera Webkit ], [[NAME, 'Opera'], VERSION], [ // Mixed /(kindle)\/([\w\.]+)/i, // Kindle /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i, // Lunascape/Maxthon/Netfront/Jasmine/Blazer // Trident based /(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i, // Avant/IEMobile/SlimBrowser/Baidu /(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer // Webkit/KHTML based /(rekonq)\/([\w\.]+)*/i, // Rekonq /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs)\/([\w\.-]+)/i // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS ], [NAME, VERSION], [ /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i // IE11 ], [[NAME, 'IE'], VERSION], [ /(edge)\/((\d+)?[\w\.]+)/i // Microsoft Edge ], [NAME, VERSION], [ /(yabrowser)\/([\w\.]+)/i // Yandex ], [[NAME, 'Yandex'], VERSION], [ /(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon ], [[NAME, /_/g, ' '], VERSION], [ /((?:android.+)crmo|crios)\/([\w\.]+)/i, /android.+(chrome)\/([\w\.]+)\s+(?:mobile\s?safari)/i // Chrome for Android/iOS ], [[NAME, 'Chrome Mobile'], VERSION], [ /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i, // Chrome/OmniWeb/Arora/Tizen/Nokia /(qqbrowser)[\/\s]?([\w\.]+)/i // QQBrowser ], [NAME, VERSION], [ /(uc\s?browser)[\/\s]?([\w\.]+)/i, /ucweb.+(ucbrowser)[\/\s]?([\w\.]+)/i, /JUC.+(ucweb)[\/\s]?([\w\.]+)/i // UCBrowser ], [[NAME, 'UCBrowser'], VERSION], [ /(dolfin)\/([\w\.]+)/i // Dolphin ], [[NAME, 'Dolphin'], VERSION], [ /XiaoMi\/MiuiBrowser\/([\w\.]+)/i // MIUI Browser ], [VERSION, [NAME, 'MIUI Browser']], [ /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)/i // Android Browser ], [VERSION, [NAME, 'Android Browser']], [ /FBAV\/([\w\.]+);/i // Facebook App for iOS ], [VERSION, [NAME, 'Facebook']], [ /fxios\/([\w\.-]+)/i // Firefox for iOS ], [VERSION, [NAME, 'Firefox']], [ /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i // Mobile Safari ], [VERSION, [NAME, 'Mobile Safari']], [ /version\/([\w\.]+).+?(mobile\s?safari|safari)/i // Safari & Safari Mobile ], [VERSION, NAME], [ /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Safari < 3.0 ], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [ /(konqueror)\/([\w\.]+)/i, // Konqueror /(webkit|khtml)\/([\w\.]+)/i ], [NAME, VERSION], [ /(blackberry)\\s?\/([\w\.]+)/i // Blackberry ], [[NAME, "BlackBerry"], VERSION], [ // Gecko based /(navigator|netscape)\/([\w\.-]+)/i // Netscape ], [[NAME, 'Netscape'], VERSION], [ /(swiftfox)/i, // Swiftfox /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i, // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i, // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i, // Mozilla // Other /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i, // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir /(links)\s\(([\w\.]+)/i, // Links /(gobrowser)\/?([\w\.]+)*/i, // GoBrowser /(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser /(mosaic)[\/\s]([\w\.]+)/i // Mosaic ], [NAME, VERSION] /* ///////////////////// // Media players BEGIN //////////////////////// , [ /(apple(?:coremedia|))\/((\d+)[\w\._]+)/i, // Generic Apple CoreMedia /(coremedia) v((\d+)[\w\._]+)/i ], [NAME, VERSION], [ /(aqualung|lyssna|bsplayer)\/((\d+)?[\w\.-]+)/i // Aqualung/Lyssna/BSPlayer ], [NAME, VERSION], [ /(ares|ossproxy)\s((\d+)[\w\.-]+)/i // Ares/OSSProxy ], [NAME, VERSION], [ /(audacious|audimusicstream|amarok|bass|core|dalvik|gnomemplayer|music on console|nsplayer|psp-internetradioplayer|videos)\/((\d+)[\w\.-]+)/i, // Audacious/AudiMusicStream/Amarok/BASS/OpenCORE/Dalvik/GnomeMplayer/MoC // NSPlayer/PSP-InternetRadioPlayer/Videos /(clementine|music player daemon)\s((\d+)[\w\.-]+)/i, // Clementine/MPD /(lg player|nexplayer)\s((\d+)[\d\.]+)/i, /player\/(nexplayer|lg player)\s((\d+)[\w\.-]+)/i // NexPlayer/LG Player ], [NAME, VERSION], [ /(nexplayer)\s((\d+)[\w\.-]+)/i // Nexplayer ], [NAME, VERSION], [ /(flrp)\/((\d+)[\w\.-]+)/i // Flip Player ], [[NAME, 'Flip Player'], VERSION], [ /(fstream|nativehost|queryseekspider|ia-archiver|facebookexternalhit)/i // FStream/NativeHost/QuerySeekSpider/IA Archiver/facebookexternalhit ], [NAME], [ /(gstreamer) souphttpsrc (?:\([^\)]+\)){0,1} libsoup\/((\d+)[\w\.-]+)/i // Gstreamer ], [NAME, VERSION], [ /(htc streaming player)\s[\w_]+\s\/\s((\d+)[\d\.]+)/i, // HTC Streaming Player /(java|python-urllib|python-requests|wget|libcurl)\/((\d+)[\w\.-_]+)/i, // Java/urllib/requests/wget/cURL /(lavf)((\d+)[\d\.]+)/i // Lavf (FFMPEG) ], [NAME, VERSION], [ /(htc_one_s)\/((\d+)[\d\.]+)/i // HTC One S ], [[NAME, /_/g, ' '], VERSION], [ /(mplayer)(?:\s|\/)(?:(?:sherpya-){0,1}svn)(?:-|\s)(r\d+(?:-\d+[\w\.-]+){0,1})/i // MPlayer SVN ], [NAME, VERSION], [ /(mplayer)(?:\s|\/|[unkow-]+)((\d+)[\w\.-]+)/i // MPlayer ], [NAME, VERSION], [ /(mplayer)/i, // MPlayer (no other info) /(yourmuze)/i, // YourMuze /(media player classic|nero showtime)/i // Media Player Classic/Nero ShowTime ], [NAME], [ /(nero (?:home|scout))\/((\d+)[\w\.-]+)/i // Nero Home/Nero Scout ], [NAME, VERSION], [ /(nokia\d+)\/((\d+)[\w\.-]+)/i // Nokia ], [NAME, VERSION], [ /\s(songbird)\/((\d+)[\w\.-]+)/i // Songbird/Philips-Songbird ], [NAME, VERSION], [ /(winamp)3 version ((\d+)[\w\.-]+)/i, // Winamp /(winamp)\s((\d+)[\w\.-]+)/i, /(winamp)mpeg\/((\d+)[\w\.-]+)/i ], [NAME, VERSION], [ /(ocms-bot|tapinradio|tunein radio|unknown|winamp|inlight radio)/i // OCMS-bot/tap in radio/tunein/unknown/winamp (no other info) // inlight radio ], [NAME], [ /(quicktime|rma|radioapp|radioclientapplication|soundtap|totem|stagefright|streamium)\/((\d+)[\w\.-]+)/i // QuickTime/RealMedia/RadioApp/RadioClientApplication/ // SoundTap/Totem/Stagefright/Streamium ], [NAME, VERSION], [ /(smp)((\d+)[\d\.]+)/i // SMP ], [NAME, VERSION], [ /(vlc) media player - version ((\d+)[\w\.]+)/i, // VLC Videolan /(vlc)\/((\d+)[\w\.-]+)/i, /(xbmc|gvfs|xine|xmms|irapp)\/((\d+)[\w\.-]+)/i, // XBMC/gvfs/Xine/XMMS/irapp /(foobar2000)\/((\d+)[\d\.]+)/i, // Foobar2000 /(itunes)\/((\d+)[\d\.]+)/i // iTunes ], [NAME, VERSION], [ /(wmplayer)\/((\d+)[\w\.-]+)/i, // Windows Media Player /(windows-media-player)\/((\d+)[\w\.-]+)/i ], [[NAME, /-/g, ' '], VERSION], [ /windows\/((\d+)[\w\.-]+) upnp\/[\d\.]+ dlnadoc\/[\d\.]+ (home media server)/i // Windows Media Server ], [VERSION, [NAME, 'Windows']], [ /(com\.riseupradioalarm)\/((\d+)[\d\.]*)/i // RiseUP Radio Alarm ], [NAME, VERSION], [ /(rad.io)\s((\d+)[\d\.]+)/i, // Rad.io /(radio.(?:de|at|fr))\s((\d+)[\d\.]+)/i ], [[NAME, 'rad.io'], VERSION] ////////////////////// // Media players END ////////////////////*/ ], cpu : [[ /(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i // AMD64 ], [[ARCHITECTURE, 'amd64']], [ /(ia32(?=;))/i // IA32 (quicktime) ], [[ARCHITECTURE, util.lowerize]], [ /((?:i[346]|x)86)[;\)]/i // IA32 ], [[ARCHITECTURE, 'ia32']], [ // PocketPC mistakenly identified as PowerPC /windows\s(ce|mobile);\sppc;/i ], [[ARCHITECTURE, 'arm']], [ /((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i // PowerPC ], [[ARCHITECTURE, /ower/, '', util.lowerize]], [ /(sun4\w)[;\)]/i // SPARC ], [[ARCHITECTURE, 'sparc']], [ /((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC ], [[ARCHITECTURE, util.lowerize]] ], device : [[ /\((ipad|playbook);[\w\s\);-]+(rim|apple)/i // iPad/PlayBook ], [MODEL, VENDOR, [TYPE, TABLET]], [ /applecoremedia\/[\w\.]+ \((ipad)/ // iPad ], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [ /(apple\s{0,1}tv)/i // Apple TV ], [[MODEL, 'Apple TV'], [VENDOR, 'Apple']], [ /(archos)\s(gamepad2?)/i, // Archos /(hp).+(touchpad)/i, // HP TouchPad /(kindle)\/([\w\.]+)/i, // Kindle /\s(nook)[\w\s]+build\/(\w+)/i, // Nook /(dell)\s(strea[kpr\s\d]*[\dko])/i // Dell Streak ], [VENDOR, MODEL, [TYPE, TABLET]], [ /(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i // Kindle Fire HD ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [ /(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i // Fire Phone ], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [ /\((ip[honed|\s\w*]+);.+(apple)/i // iPod/iPhone ], [MODEL, VENDOR, [TYPE, MOBILE]], [ /\((ip[honed|\s\w*]+);/i // iPod/iPhone ], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [ /(blackberry)[\s-]?(\w+)/i, // BlackBerry /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|huawei|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i, // BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Huawei/Meizu/Motorola/Polytron /(hp)\s([\w\s]+\w)/i, // HP iPAQ /(asus)-?(\w+)/i // Asus ], [VENDOR, MODEL, [TYPE, MOBILE]], [ /\(bb10;\s(\w+)/i // BlackBerry 10 ], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [ // Asus Tablets /android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7)/i ], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [ /(sony)\s(tablet\s[ps])\sbuild\//i, // Sony /(sony)?(?:sgp.+)\sbuild\//i ], [[VENDOR, 'Sony'], [MODEL, 'Xperia Tablet'], [TYPE, TABLET]], [ /(?:sony)?(?:(?:(?:c|d)\d{4})|(?:so[-l].+))\sbuild\//i ], [[VENDOR, 'Sony'], [MODEL, 'Xperia Phone'], [TYPE, MOBILE]], [ /\s(ouya)\s/i, // Ouya /(nintendo)\s([wids3u]+)/i // Nintendo ], [VENDOR, MODEL, [TYPE, CONSOLE]], [ /android.+;\s(shield)\sbuild/i // Nvidia ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [ /(playstation\s[34portablevi]+)/i // Playstation ], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [ /(sprint\s(\w+))/i // Sprint Phones ], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [ /(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i // Lenovo tablets ], [VENDOR, MODEL, [TYPE, TABLET]], [ /(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i, // HTC /(zte)-(\w+)*/i, // ZTE /(alcatel|geeksphone|huawei|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i // Alcatel/GeeksPhone/Huawei/Lenovo/Nexian/Panasonic/Sony ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [ /(nexus\s9)/i // HTC Nexus 9 ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [ /[\s\(;](xbox(?:\sone)?)[\s\);]/i // Microsoft Xbox ], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [ /(kin\.[onetw]{3})/i // Microsoft Kin ], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [ // Motorola /\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i, /mot[\s-]?(\w+)*/i, /(XT\d{3,4}) build\//i, /(nexus\s[6])/i ], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [ /android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i ], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [ /android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n8000|sgh-t8[56]9|nexus 10))/i, /((SM-T\w+))/i ], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [ // Samsung /((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-n900))/i, /(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i, /sec-((sgh\w+))/i ], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [ /(samsung);smarttv/i ], [VENDOR, MODEL, [TYPE, SMARTTV]], [ /\(dtv[\);].+(aquos)/i // Sharp ], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [ /sie-(\w+)*/i // Siemens ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [ /(maemo|nokia).*(n900|lumia\s\d+)/i, // Nokia /(nokia)[\s_-]?([\w-]+)*/i ], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [ /android\s3\.[\s\w;-]{10}(a\d{3})/i // Acer ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [ /android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i // LG Tablet ], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [ /(lg) netcast\.tv/i // LG SmartTV ], [VENDOR, MODEL, [TYPE, SMARTTV]], [ /(nexus\s[45])/i, // LG /lg[e;\s\/-]+(\w+)*/i ], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [ /android.+(ideatab[a-z0-9\-\s]+)/i // Lenovo ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [ /linux;.+((jolla));/i // Jolla ], [VENDOR, MODEL, [TYPE, MOBILE]], [ /((pebble))app\/[\d\.]+\s/i // Pebble ], [VENDOR, MODEL, [TYPE, WEARABLE]], [ /android.+;\s(glass)\s\d/i // Google Glass ], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [ /android.+(\w+)\s+build\/hm\1/i, // Xiaomi Hongmi 'numeric' models /android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i, // Xiaomi Hongmi /android.+(mi[\s\-_]*(?:one|one[\s_]plus)?[\s_]*(?:\d\w)?)\s+build/i // Xiaomi Mi ], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [ /\s(tablet)[;\/\s]/i, // Unidentifiable Tablet /\s(mobile)[;\/\s]/i // Unidentifiable Mobile ], [[TYPE, util.lowerize], VENDOR, MODEL] /*////////////////////////// // TODO: move to string map //////////////////////////// /(C6603)/i // Sony Xperia Z C6603 ], [[MODEL, 'Xperia Z C6603'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [ /(C6903)/i // Sony Xperia Z 1 ], [[MODEL, 'Xperia Z 1'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [ /(SM-G900[F|H])/i // Samsung Galaxy S5 ], [[MODEL, 'Galaxy S5'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ /(SM-G7102)/i // Samsung Galaxy Grand 2 ], [[MODEL, 'Galaxy Grand 2'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ /(SM-G530H)/i // Samsung Galaxy Grand Prime ], [[MODEL, 'Galaxy Grand Prime'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ /(SM-G313HZ)/i // Samsung Galaxy V ], [[MODEL, 'Galaxy V'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ /(SM-T805)/i // Samsung Galaxy Tab S 10.5 ], [[MODEL, 'Galaxy Tab S 10.5'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [ /(SM-G800F)/i // Samsung Galaxy S5 Mini ], [[MODEL, 'Galaxy S5 Mini'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ /(SM-T311)/i // Samsung Galaxy Tab 3 8.0 ], [[MODEL, 'Galaxy Tab 3 8.0'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [ /(R1001)/i // Oppo R1001 ], [MODEL, [VENDOR, 'OPPO'], [TYPE, MOBILE]], [ /(X9006)/i // Oppo Find 7a ], [[MODEL, 'Find 7a'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [ /(R2001)/i // Oppo YOYO R2001 ], [[MODEL, 'Yoyo R2001'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [ /(R815)/i // Oppo Clover R815 ], [[MODEL, 'Clover R815'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [ /(U707)/i // Oppo Find Way S ], [[MODEL, 'Find Way S'], [VENDOR, 'Oppo'], [TYPE, MOBILE]], [ /(T3C)/i // Advan Vandroid T3C ], [MODEL, [VENDOR, 'Advan'], [TYPE, TABLET]], [ /(ADVAN T1J\+)/i // Advan Vandroid T1J+ ], [[MODEL, 'Vandroid T1J+'], [VENDOR, 'Advan'], [TYPE, TABLET]], [ /(ADVAN S4A)/i // Advan Vandroid S4A ], [[MODEL, 'Vandroid S4A'], [VENDOR, 'Advan'], [TYPE, MOBILE]], [ /(V972M)/i // ZTE V972M ], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [ /(i-mobile)\s(IQ\s[\d\.]+)/i // i-mobile IQ ], [VENDOR, MODEL, [TYPE, MOBILE]], [ /(IQ6.3)/i // i-mobile IQ IQ 6.3 ], [[MODEL, 'IQ 6.3'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [ /(i-mobile)\s(i-style\s[\d\.]+)/i // i-mobile i-STYLE ], [VENDOR, MODEL, [TYPE, MOBILE]], [ /(i-STYLE2.1)/i // i-mobile i-STYLE 2.1 ], [[MODEL, 'i-STYLE 2.1'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [ /(mobiistar touch LAI 512)/i // mobiistar touch LAI 512 ], [[MODEL, 'Touch LAI 512'], [VENDOR, 'mobiistar'], [TYPE, MOBILE]], [ ///////////// // END TODO ///////////*/ ], engine : [[ /windows.+\sedge\/([\w\.]+)/i // EdgeHTML ], [VERSION, [NAME, 'EdgeHTML']], [ /(presto)\/([\w\.]+)/i, // Presto /(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links /(icab)[\/\s]([23]\.[\d\.]+)/i // iCab ], [NAME, VERSION], [ /rv\:([\w\.]+).*(gecko)/i // Gecko ], [VERSION, NAME] ], os : [[ // Windows based /microsoft\s(windows)\s(vista|xp)/i // Windows (iTunes) ], [NAME, VERSION], [ /(windows)\snt\s6\.2;\s(arm)/i, // Windows RT /(windows\sphone(?:\sos)*|windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i ], [[NAME, mapper.str, maps.os.windows.name], [VERSION, mapper.str, maps.os.windows.version]], [ /(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i ], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [ // Mobile/Embedded OS /\((bb)(10);/i // BlackBerry 10 ], [[NAME, 'BlackBerry'], VERSION], [ /(blackberry)\w*\/?([\w\.]+)*/i, // Blackberry /(tizen)[\/\s]([\w\.]+)/i, // Tizen /(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i, // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki /linux;.+(sailfish);/i // Sailfish OS ], [NAME, VERSION], [ /(symbian\s?o?s?|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i // Symbian ], [[NAME, 'Symbian'], VERSION], [ /\((series40);/i // Series 40 ], [NAME], [ /mozilla.+\(mobile;.+gecko.+firefox/i // Firefox OS ], [[NAME, 'Firefox OS'], VERSION], [ // Console /(nintendo|playstation)\s([wids34portablevu]+)/i, // Nintendo/Playstation // GNU/Linux based /(mint)[\/\s\(]?(\w+)*/i, // Mint /(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux /(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?([\w\.-]+)*/i, // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus /(hurd|linux)\s?([\w\.]+)*/i, // Hurd/Linux /(gnu)\s?([\w\.]+)*/i // GNU ], [[NAME, 'Linux'], VERSION], [ /(cros)\s[\w]+\s([\w\.]+\w)/i // Chromium OS ], [[NAME, 'Chromium OS'], VERSION],[ // Solaris /(sunos)\s?([\w\.]+\d)*/i // Solaris ], [[NAME, 'Solaris'], VERSION], [ // BSD based /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly ], [[NAME, 'Linux'], VERSION],[ /(iphone)(?:.*os\s*([\w]+)*\slike\smac|;\sopera)/i // iOS ], [[NAME, 'iPhone'], [VERSION, /_/g, '.']], [ /(ipad)(?:.*os\s*([\w]+)*\slike\smac|;\sopera)/i // iOS ], [[NAME, 'iPad'], [VERSION, /_/g, '.']], [ /(ip[honead]+)(?:.*os\s([\w]+)*\slike\smac|;\sopera)/i // iOS ], [[NAME, 'iOS'], [VERSION, /_/g, '.']], [ /(mac\sos\sx)\s?([\w\s\.]+\w)*/i, /(macintosh|mac(?=_powerpc)\s)/i // Mac OS ], [[NAME, 'Mac'], [VERSION, /_/g, '.']], [ // Other /((?:open)?solaris)[\/\s-]?([\w\.]+)*/i, // Solaris /(haiku)\s(\w+)/i, // Haiku /(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i, // AIX /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i, // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS /(unix)\s?([\w\.]+)*/i // UNIX ], [NAME, VERSION] ] }; ///////////////// // Constructor //////////////// var UAParser = function (uastring, extensions) { if (!(this instanceof UAParser)) { return new UAParser(uastring, extensions).getResult(); } var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY); var rgxmap = extensions ? util.extend(regexes, extensions) : regexes; this.getBrowser = function () { var browser = mapper.rgx.apply(this, rgxmap.browser); browser.major = util.major(browser.version); return browser; }; this.getCPU = function () { return mapper.rgx.apply(this, rgxmap.cpu); }; this.getDevice = function () { return mapper.rgx.apply(this, rgxmap.device); }; this.getEngine = function () { return mapper.rgx.apply(this, rgxmap.engine); }; this.getOS = function () { return mapper.rgx.apply(this, rgxmap.os); }; this.getResult = function() { return { ua : this.getUA(), browser : this.getBrowser(), engine : this.getEngine(), os : this.getOS(), device : this.getDevice(), cpu : this.getCPU() }; }; this.getUA = function () { return ua; }; this.setUA = function (uastring) { ua = uastring; return this; }; return this; }; UAParser.VERSION = LIBVERSION; UAParser.BROWSER = { NAME : NAME, MAJOR : MAJOR, // deprecated VERSION : VERSION }; UAParser.CPU = { ARCHITECTURE : ARCHITECTURE }; UAParser.DEVICE = { MODEL : MODEL, VENDOR : VENDOR, TYPE : TYPE, CONSOLE : CONSOLE, MOBILE : MOBILE, SMARTTV : SMARTTV, TABLET : TABLET, WEARABLE: WEARABLE, EMBEDDED: EMBEDDED }; UAParser.ENGINE = { NAME : NAME, VERSION : VERSION }; UAParser.OS = { NAME : NAME, VERSION : VERSION }; /////////// // Export ////////// // check js environment if (typeof(exports) !== UNDEF_TYPE) { // nodejs env if (typeof module !== UNDEF_TYPE && module.exports) { exports = module.exports = UAParser; } exports.UAParser = UAParser; } else { // requirejs env (optional) if (typeof(define) === FUNC_TYPE && define.amd) { define("ua-parser-js", [], function () { return UAParser; }); } else { // browser env window.UAParser = UAParser; } } // jQuery/Zepto specific (optional) // Note: // In AMD env the global scope should be kept clean, but jQuery is an exception. // jQuery always exports to global scope, unless jQuery.noConflict(true) is used, // and we should catch that. var $ = window.jQuery || window.Zepto; if (typeof $ !== UNDEF_TYPE) { var parser = new UAParser(); $.ua = parser.getResult(); $.ua.get = function() { return parser.getUA(); }; $.ua.set = function (uastring) { parser.setUA(uastring); var result = parser.getResult(); for (var prop in result) { $.ua[prop] = result[prop]; } }; } })(typeof window === 'object' ? window : this); }, {}], 19: [function(require, module, exports) { /* jshint bitwise: false, laxbreak: true */ /** * Source: [jed's gist]{@link https://gist.github.com/982883}. * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, * where each x is replaced with a random hexadecimal digit from 0 to f, and * y is replaced with a random hexadecimal digit from 8 to b. * Used to generate UUIDs for deviceIds. * @private */ var uuid = function(a) { return a // if the placeholder was passed, return ? ( // a random number from 0 to 15 a ^ // unless b is 8, Math.random() // in which case * 16 // a random number from >> a / 4 // 8 to 11 ).toString(16) // in hexadecimal : ( // or otherwise a concatenated string: [1e7] + // 10000000 + -1e3 + // -1000 + -4e3 + // -4000 + -8e3 + // -80000000 + -1e11 // -100000000000, ).replace( // replacing /[018]/g, // zeroes, ones, and eights with uuid // random hex digits ); }; module.exports = uuid; }, {}], 10: [function(require, module, exports) { module.exports = '3.4.0'; }, {}], 11: [function(require, module, exports) { var language = require('./language'); // default options 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: 1000, saveEvents: true, sessionTimeout: 30 * 60 * 1000, unsentKey: 'amplitude_unsent', unsentIdentifyKey: 'amplitude_unsent_identify', uploadBatchSize: 100, batchEvents: false, eventUploadThreshold: 30, eventUploadPeriodMillis: 30 * 1000, // 30s forceHttps: false, includeGclid: false, saveParamsReferrerOncePerSession: true, deviceIdFromUrlParam: false, }; }, {"./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":""}) ); try { if (amplitudeToken) { amplitude.getInstance().init(amplitudeToken,null,{ includeUtm: true, includeReferrer:true, saveParamsReferrerOncePerSession:false }); }else{ console.error("No token to initialize analytics") } } catch (e) { console.error("not able to initialize analytics") } var mixpanel = {}; var analytics = {global:{properties:{}}}; if(document.referrer && document.referrer!=""){ analytics.global.properties['referrer'] = document.referrer; } mixpanel.track = function(eventName,property,callBack){ if(property == undefined){ property ={}; } for (var attrname in analytics.global.properties) { property[attrname] = analytics.global.properties[attrname]; } amplitude.getInstance().logEvent(eventName, property,callBack); } mixpanel.register = function(userproperties){ if(userproperties!=undefined){ for (var attrname in userproperties) { analytics.global.properties[attrname] = userproperties[attrname]; } } } mixpanel.identify = function(userId){ amplitude.getInstance().setUserId(userId); } mixpanel.time_event = function(){ //TODO } mixpanel.alias = function(){ //TODO } mixpanel.cookie = {}; mixpanel.cookie.clear = function(){ //TODO } mixpanel.people = {}; mixpanel.people.set = function(userPropObj,value){ if(value != undefined){ var identify = new amplitude.Identify().set(userPropObj, value); amplitude.getInstance().identify(identify); }else{ var identifyObj = new amplitude.Identify(); for (var key in userPropObj) { if (userPropObj.hasOwnProperty(key)) { identifyObj.set(key, userPropObj[key]); } } amplitude.getInstance().identify(identifyObj); } } mixpanel.people.set_once = function(key,value){ var identify = new amplitude.Identify().setOnce(key, value); amplitude.getInstance().identify(identify); } mixpanel.people.increment = function(property,value){ var identify = new amplitude.Identify().add(property, value); amplitude.getInstance().identify(identify); }