UNPKG

amplitude-js

Version:
1,696 lines (1,411 loc) 121 kB
import _objectSpread from '@babel/runtime/helpers/objectSpread'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _typeof from '@babel/runtime/helpers/typeof'; import _classCallCheck from '@babel/runtime/helpers/classCallCheck'; import _createClass from '@babel/runtime/helpers/createClass'; import md5 from 'blueimp-md5'; import queryString from 'query-string'; import UAParser from '@amplitude/ua-parser-js'; var Constants = { DEFAULT_INSTANCE: '$default_instance', API_VERSION: 2, MAX_STRING_LENGTH: 4096, MAX_PROPERTY_KEYS: 1000, IDENTIFY_EVENT: '$identify', GROUP_IDENTIFY_EVENT: '$groupidentify', // 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_PREFIX: 'amp_cookie_test', COOKIE_PREFIX: "amp", // 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 REFERRER: 'referrer', // UTM Params UTM_SOURCE: 'utm_source', UTM_MEDIUM: 'utm_medium', UTM_CAMPAIGN: 'utm_campaign', UTM_TERM: 'utm_term', UTM_CONTENT: 'utm_content', ATTRIBUTION_EVENT: '[Amplitude] Attribution Captured' }; /* jshint bitwise: false */ /* * UTF-8 encoder/decoder * http://www.webtoolkit.info/ */ var UTF8 = { encode: function encode(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 decode(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; } }; /* jshint bitwise: false */ /* * Base64 encoder/decoder * http://www.webtoolkit.info/ */ var Base64 = { _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', encode: function encode(input) { try { if (window.btoa && window.atob) { return window.btoa(unescape(encodeURIComponent(input))); } } catch (e) {//log(e); } return Base64._encode(input); }, _encode: function _encode(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 decode(input) { try { if (window.btoa && window.atob) { return decodeURIComponent(escape(window.atob(input))); } } catch (e) {//log(e); } return Base64._decode(input); }, _decode: function _decode(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; } }; /** * toString ref. * @private */ var toString = Object.prototype.toString; /** * Return the type of `val`. * @private * @param {Mixed} val * @return {String} * @api public */ function type (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' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(val)) { return 'buffer'; } val = val.valueOf ? val.valueOf() : Object.prototype.valueOf.apply(val); return _typeof(val); } var logLevels = { DISABLE: 0, ERROR: 1, WARN: 2, INFO: 3 }; var logLevel = logLevels.WARN; var setLogLevel = function setLogLevel(logLevelName) { if (logLevels.hasOwnProperty(logLevelName)) { logLevel = logLevels[logLevelName]; } }; var getLogLevel = function getLogLevel() { return logLevel; }; var log = { error: function error(s) { if (logLevel >= logLevels.ERROR) { _log(s); } }, warn: function warn(s) { if (logLevel >= logLevels.WARN) { _log(s); } }, info: function info(s) { if (logLevel >= logLevels.INFO) { _log(s); } } }; 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.error('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('Error: invalid properties format. Expecting Javascript object, received ' + propsType + ', ignoring'); return {}; } if (Object.keys(properties).length > Constants.MAX_PROPERTY_KEYS) { log.error('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.warn('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 = ['nan', 'function', 'arguments', 'regexp', 'element']; var validatePropertyValue = function validatePropertyValue(key, value) { var valueType = type(value); if (invalidValueTypes.indexOf(valueType) !== -1) { log.warn('WARNING: Property key "' + key + '" with invalid value type ' + valueType + ', ignoring'); value = null; } else if (valueType === 'undefined') { value = null; } else if (valueType === 'error') { value = String(value); log.warn('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') { log.warn('WARNING: Cannot have ' + elemType + ' nested in an array property value, skipping'); continue; } else if (elemType === 'object') { arrayCopy.push(validateProperties(element)); } else { 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('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.warn('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.warn('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.warn('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.warn('WARNING: Non-string groupName, received type ' + elemType + ', coercing to string "' + element + '"'); arrayCopy.push(element); } } return arrayCopy; } log.warn('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, " ")); }; var utils = { setLogLevel: setLogLevel, getLogLevel: getLogLevel, logLevels: logLevels, log: log, isEmptyString: isEmptyString, getQueryParam: getQueryParam, sessionStorageEnabled: sessionStorageEnabled, truncate: truncate, validateGroups: validateGroups, validateInput: validateInput, validateProperties: validateProperties }; var getLocation = function getLocation() { return window.location; }; // A URL safe variation on the the list of Base64 characters var base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; var base64Id = function base64Id() { var str = ''; for (var i = 0; i < 22; ++i) { str += base64Chars.charAt(Math.floor(Math.random() * 64)); } return str; }; var get = function get(name) { try { 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(name) === 0) { value = c.substring(name.length, c.length); break; } } return value; } catch (e) { return null; } }; var set = function set(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; } if (opts.secure) { str += '; Secure'; } if (opts.sameSite) { str += '; SameSite=' + opts.sameSite; } document.cookie = str; }; // test that cookies are enabled - navigator.cookiesEnabled yields false positives in IE, need to test directly var areCookiesEnabled = function areCookiesEnabled() { var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var uid = String(new Date()); try { var cookieName = Constants.COOKIE_TEST_PREFIX + base64Id(); set(cookieName, uid, opts); var _areCookiesEnabled = get(cookieName + '=') === uid; set(cookieName, null, opts); return _areCookiesEnabled; } catch (e) {} return false; }; var baseCookie = { set: set, get: get, areCookiesEnabled: areCookiesEnabled }; var getHost = function getHost(url) { var a = document.createElement('a'); a.href = url; return a.hostname || location.hostname; }; var topDomain = function topDomain(url) { var host = getHost(url); var parts = host.split('.'); var levels = []; var cname = '_tldtest_' + base64Id(); for (var i = parts.length - 2; i >= 0; --i) { levels.push(parts.slice(i).join('.')); } for (var _i = 0; _i < levels.length; ++_i) { var domain = levels[_i]; var opts = { domain: '.' + domain }; baseCookie.set(cname, 1, opts); if (baseCookie.get(cname)) { baseCookie.set(cname, null, opts); return domain; } } return ''; }; /* * Cookie data */ var _options = { expirationDays: undefined, domain: undefined }; var reset = function reset() { _options = { expirationDays: undefined, domain: undefined }; }; var options = function options(opts) { if (arguments.length === 0) { return _options; } opts = opts || {}; _options.expirationDays = opts.expirationDays; _options.secure = opts.secure; _options.sameSite = opts.sameSite; var domain = !utils.isEmptyString(opts.domain) ? opts.domain : '.' + topDomain(getLocation().href); var token = Math.random(); _options.domain = domain; set$1('amplitude_test', token); var stored = get$1('amplitude_test'); if (!stored || stored !== token) { domain = null; } remove('amplitude_test'); _options.domain = domain; return _options; }; var _domainSpecific = function _domainSpecific(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$1 = function get(name) { var nameEq = _domainSpecific(name) + '='; var value = baseCookie.get(nameEq); try { if (value) { return JSON.parse(Base64.decode(value)); } } catch (e) { return null; } return null; }; var set$1 = function set(name, value) { try { baseCookie.set(_domainSpecific(name), Base64.encode(JSON.stringify(value)), _options); return true; } catch (e) { return false; } }; var setRaw = function setRaw(name, value) { try { baseCookie.set(_domainSpecific(name), value, _options); return true; } catch (e) { return false; } }; var getRaw = function getRaw(name) { var nameEq = _domainSpecific(name) + '='; return baseCookie.get(nameEq); }; var remove = function remove(name) { try { baseCookie.set(_domainSpecific(name), null, _options); return true; } catch (e) { return false; } }; var Cookie = { reset: reset, options: options, get: get$1, set: set$1, remove: remove, setRaw: setRaw, getRaw: getRaw }; /* 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 var windowLocalStorageAvailable = 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 if (typeof document !== 'undefined') { // 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 setItem(k, v) { div.load(attrKey); if (!div.getAttribute(k)) { this.length++; } div.setAttribute(k, v); div.save(attrKey); }, getItem: function getItem(k) { div.load(attrKey); return div.getAttribute(k); }, removeItem: function removeItem(k) { div.load(attrKey); if (div.getAttribute(k)) { this.length--; } div.removeAttribute(k); div.save(attrKey); }, clear: function clear() { 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 key(k) { div.load(attrKey); return div.XMLDocument.documentElement.attributes[k]; } }; div.load(attrKey); localStorage.length = div.XMLDocument.documentElement.attributes.length; } } if (!localStorage) { localStorage = { length: 0, setItem: function setItem(k, v) {}, getItem: function getItem(k) {}, removeItem: function removeItem(k) {}, clear: function clear() {}, key: function key(k) {} }; } } var localStorage$1 = localStorage; /* jshint -W020, unused: false, noempty: false, boss: true */ var cookieStorage = function cookieStorage() { this.storage = null; }; cookieStorage.prototype.getStorage = function () { if (this.storage !== null) { return this.storage; } if (baseCookie.areCookiesEnabled()) { 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, secure: false }, reset: function reset() { this._options = { expirationDays: undefined, domain: undefined, secure: false }; }, options: function options(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 && window.location && window.location.hostname; return this._options.secure = opts.secure || false; }, get: function get(name) { try { return JSON.parse(localStorage$1.getItem(keyPrefix + name)); } catch (e) {} return null; }, set: function set(name, value) { try { localStorage$1.setItem(keyPrefix + name, JSON.stringify(value)); return true; } catch (e) {} return false; }, remove: function remove(name) { try { localStorage$1.removeItem(keyPrefix + name); } catch (e) { return false; } } }; } return this.storage; }; /** * MetadataStorage involves SDK data persistance * storage priority: cookies -> localStorage -> in memory * if in localStorage, unable track users between subdomains * if in memory, then memory can't be shared between different tabs */ var MetadataStorage = /*#__PURE__*/ function () { function MetadataStorage(_ref) { var storageKey = _ref.storageKey, disableCookies = _ref.disableCookies, domain = _ref.domain, secure = _ref.secure, sameSite = _ref.sameSite, expirationDays = _ref.expirationDays; _classCallCheck(this, MetadataStorage); this.storageKey = storageKey; this.domain = domain; this.secure = secure; this.sameSite = sameSite; this.expirationDays = expirationDays; this.cookieDomain = ''; { var writableTopDomain = topDomain(getLocation().href); this.cookieDomain = domain || (writableTopDomain ? '.' + writableTopDomain : null); } this.disableCookieStorage = disableCookies || !baseCookie.areCookiesEnabled({ domain: this.cookieDomain, secure: this.secure, sameSite: this.sameSite, expirationDays: this.expirationDays }); } _createClass(MetadataStorage, [{ key: "getCookieStorageKey", value: function getCookieStorageKey() { if (!this.domain) { return this.storageKey; } var suffix = this.domain.charAt(0) === '.' ? this.domain.substring(1) : this.domain; return "".concat(this.storageKey).concat(suffix ? "_".concat(suffix) : ''); } /* * Data is saved as delimited values rather than JSO to minimize cookie space * Should not change order of the items */ }, { key: "save", value: function save(_ref2) { var deviceId = _ref2.deviceId, userId = _ref2.userId, optOut = _ref2.optOut, sessionId = _ref2.sessionId, lastEventTime = _ref2.lastEventTime, eventId = _ref2.eventId, identifyId = _ref2.identifyId, sequenceNumber = _ref2.sequenceNumber; var value = [deviceId, Base64.encode(userId || ''), // used to convert not unicode to alphanumeric since cookies only use alphanumeric optOut ? '1' : '', sessionId ? sessionId.toString(32) : '0', // generated when instantiated, timestamp (but re-uses session id in cookie if not expired) @TODO clients may want custom session id lastEventTime ? lastEventTime.toString(32) : '0', // last time an event was set eventId ? eventId.toString(32) : '0', identifyId ? identifyId.toString(32) : '0', sequenceNumber ? sequenceNumber.toString(32) : '0'].join('.'); if (this.disableCookieStorage) { localStorage$1.setItem(this.storageKey, value); } else { baseCookie.set(this.getCookieStorageKey(), value, { domain: this.cookieDomain, secure: this.secure, sameSite: this.sameSite, expirationDays: this.expirationDays }); } } }, { key: "load", value: function load() { var str; if (!this.disableCookieStorage) { str = baseCookie.get(this.getCookieStorageKey() + '='); } if (!str) { str = localStorage$1.getItem(this.storageKey); } if (!str) { return null; } var values = str.split('.'); var userId = null; if (values[1]) { try { userId = Base64.decode(values[1]); } catch (e) { userId = null; } } return { deviceId: values[0], userId: userId, optOut: values[2] === '1', sessionId: parseInt(values[3], 32), lastEventTime: parseInt(values[4], 32), eventId: parseInt(values[5], 32), identifyId: parseInt(values[6], 32), sequenceNumber: parseInt(values[7], 32) }; } }]); return MetadataStorage; }(); 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(Constants.UTM_SOURCE, query, 'utmcsr', cookie); var utmMedium = fetchParam(Constants.UTM_MEDIUM, query, 'utmcmd', cookie); var utmCampaign = fetchParam(Constants.UTM_CAMPAIGN, query, 'utmccn', cookie); var utmTerm = fetchParam(Constants.UTM_TERM, query, 'utmctr', cookie); var utmContent = fetchParam(Constants.UTM_CONTENT, query, 'utmcct', cookie); var utmData = {}; var addIfNotNull = function addIfNotNull(key, value) { if (!utils.isEmptyString(value)) { utmData[key] = value; } }; addIfNotNull(Constants.UTM_SOURCE, utmSource); addIfNotNull(Constants.UTM_MEDIUM, utmMedium); addIfNotNull(Constants.UTM_CAMPAIGN, utmCampaign); addIfNotNull(Constants.UTM_TERM, utmTerm); addIfNotNull(Constants.UTM_CONTENT, utmContent); return utmData; }; /* * 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. * @constructor Identify * @public * @example var identify = new amplitude.Identify(); */ var Identify = function Identify() { 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.error('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.error('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|boolean|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|boolean|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.error('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.error('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); }; /* * Simple AJAX request object */ var Request = function Request(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))); }; /** * Revenue API - instance constructor. Wrapper for logging Revenue data. Revenue objects get passed to amplitude.logRevenueV2 to send to Amplitude servers. * 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. * @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.error('Unsupported type for productId: ' + type(productId) + ', expecting string'); } else if (utils.isEmptyString(productId)) { utils.log.error('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.error('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.error('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.error('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.error('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.error('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; }; /* 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 uuid(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 ); }; var version = "7.2.2"; var getLanguage = function getLanguage() { return navigator && (navigator.languages && navigator.languages[0] || navigator.language || navigator.userLanguage) || ''; }; var language = { getLanguage: getLanguage }; var platform = 'Web'; var DEFAULT_OPTIONS = { apiEndpoint: 'api.amplitude.com', batchEvents: false, cookieExpiration: 365 * 10, cookieName: 'amplitude_id', // this is a deprecated option sameSiteCookie: 'Lax', // cookie privacy policy cookieForceUpgrade: false, deferInitialization: false, disableCookies: false, deviceIdFromUrlParam: false, domain: '', eventUploadPeriodMillis: 30 * 1000, // 30s eventUploadThreshold: 30, forceHttps: true, includeGclid: false, includeReferrer: false, includeUtm: false, language: language.getLanguage(), logLevel: 'WARN', logAttributionCapturedEvent: false, optOut: false, onError: function onError() {}, platform: platform, savedMaxCount: 1000, saveEvents: true, saveParamsReferrerOncePerSession: true, secureCookie: false, sessionTimeout: 30 * 60 * 1000, trackingOptions: { city: true, country: true, carrier: true, device_manufacturer: true, device_model: true, dma: true, ip_address: true, language: true, os_name: true, os_version: true, platform: true, region: true, version_name: true }, unsetParamsReferrerOnNewSession: false, unsentKey: 'amplitude_unsent', unsentIdentifyKey: 'amplitude_unsent_identify', uploadBatchSize: 100 }; var AsyncStorage; var DeviceInfo; /** * AmplitudeClient SDK API - instance constructor. * The Amplitude class handles creation of client instances, all you need to do is call amplitude.getInstance() * @constructor AmplitudeClient * @public * @example var amplitudeClient = new AmplitudeClient(); */ var AmplitudeClient = function AmplitudeClient(instanceName) { this._instanceName = utils.isEmptyString(instanceName) ? Constants.DEFAULT_INSTANCE : instanceName.toLowerCase(); this._unsentEvents = []; this._unsentIdentifys = []; this._ua = new UAParser(navigator.userAgent).getResult(); this.options = _objectSpread({}, DEFAULT_OPTIONS, { trackingOptions: _objectSpread({}, DEFAULT_OPTIONS.trackingOptions) }); this.cookieStorage = new cookieStorage().getStorage(); this._q = []; // queue for proxied functions before script load this._sending = false; this._updateScheduled = false; this._onInit = []; // event meta data this._eventId = 0; this._identifyId = 0; this._lastEventTime = null; this._newSession = false; // sequence used for by frontend for prioritizing event send retries this._sequenceNumber = 0; this._sessionId = null; this._isInitialized = false; this._userAgent = navigator && navigator.userAgent || null; }; AmplitudeClient.prototype.Identify = Identify; AmplitudeClient.prototype.Revenue = Revenue; /** * Initializes the Amplitude Javascript SDK with your apiKey and any optional configurations. * This is required before any other methods can be called. * @public * @param {string} apiKey - The API key for your app. * @param {string} opt_userId - (optional) An identifier for this user. * @param {object} opt_config - (optional) Configuration options. * See [options.js](https://github.com/amplitude/Amplitude-JavaScript/blob/master/src/options.js#L14) for list of options and default values. * @param {function} opt_callback - (optional) Provide a callback function to run after initialization is complete. * @example amplitudeClient.init('API_KEY', 'USER_ID', {includeReferrer: true, includeUtm: true}, function() { alert('init complete'); }); */ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, opt_callback) { var _this = this; if (type(apiKey) !== 'string' || utils.isEmptyString(apiKey)) { utils.log.error('Invalid apiKey. Please re-initialize with a valid apiKey'); return; } try { _parseConfig(this.options, opt_config); if (this.options.cookieName !== DEFAULT_OPTIONS.cookieName) { utils.log.warn('The cookieName option is deprecated. We will be ignoring it for newer cookies'); } this.options.apiKey = apiKey; this._storageSuffix = '_' + apiKey + (this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName); this._storageSuffixV5 = apiKey.slice(0, 6); this._oldCookiename = this.options.cookieName + this._storageSuffix; this._unsentKey = this.options.unsentKey + this._storageSuffix; this._unsentIdentifyKey = this.options.unsentIdentifyKey + this._storageSuffix; this._cookieName = Constants.COOKIE_PREFIX + '_' + this._storageSuffixV5; this.cookieStorage.options({ expirationDays: this.options.cookieExpiration, domain: this.options.domain, secure: this.options.secureCookie, sameSite: this.options.sameSiteCookie }); this._metadataStorage = new MetadataStorage({ storageKey: this._cookieName, disableCookies: this.options.disableCookies, expirationDays: this.options.cookieExpiration, domain: this.options.domain, secure: this.options.secureCookie, sameSite: this.options.sameSiteCookie }); var hasOldCookie = !!this.cookieStorage.get(this._oldCookiename); var hasNewCookie = !!this._metadataStorage.load(); this._useOldCookie = !hasNewCookie && hasOldCookie && !this.options.cookieForceUpgrade; var hasCookie = hasNewCookie || hasOldCookie; this.options.domain = this.cookieStorage.options().domain; if (this.options.deferInitialization && !hasCookie) { this._deferInitialization(apiKey, opt_userId, opt_config, opt_callback); return; } if (type(this.options.logLevel) === 'string') { utils.setLogLevel(this.options.logLevel); } var trackingOptions = _generateApiPropertiesTrackingConfig(this); this._apiPropertiesTrackingOptions = Object.keys(trackingOptions).length > 0 ? { tracking_options: trackingOptions } : {}; if (this.options.cookieForceUpgrade && hasOldCookie) { if (!hasNewCookie) { _upgradeCookieData(this); } this.cookieStorage.remove