UNPKG

mixpanel-browser

Version:

The official Mixpanel JavaScript browser client library

1,557 lines (1,408 loc) 57.2 kB
/* eslint camelcase: "off", eqeqeq: "off" */ import Config from './config'; import { NpoPromise, Promise } from './promise-polyfill'; import { window } from './window'; // Maximum allowed session recording length var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours // Maximum allowed value for minimum session recording length var MAX_VALUE_FOR_MIN_RECORDING_MS = 8 * 1000; // 8 seconds /* * Saved references to long variable names, so that closure compiler can * minimize file size. */ var ArrayProto = Array.prototype, FuncProto = Function.prototype, ObjProto = Object.prototype, slice = ArrayProto.slice, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty, windowConsole = window.console, navigator = window.navigator, document = window.document, windowOpera = window.opera, screen = window.screen, userAgent = navigator.userAgent; var nativeBind = FuncProto.bind, nativeForEach = ArrayProto.forEach, nativeIndexOf = ArrayProto.indexOf, nativeMap = ArrayProto.map, nativeIsArray = Array.isArray, breaker = {}; var _ = { trim: function(str) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); } }; // Console override var console = { /** @type {function(...*)} */ log: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { try { windowConsole.log.apply(windowConsole, arguments); } catch (err) { _.each(arguments, function(arg) { windowConsole.log(arg); }); } } }, /** @type {function(...*)} */ warn: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { var args = ['Mixpanel warning:'].concat(_.toArray(arguments)); try { windowConsole.warn.apply(windowConsole, args); } catch (err) { _.each(args, function(arg) { windowConsole.warn(arg); }); } } }, /** @type {function(...*)} */ error: function() { if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { var args = ['Mixpanel error:'].concat(_.toArray(arguments)); try { windowConsole.error.apply(windowConsole, args); } catch (err) { _.each(args, function(arg) { windowConsole.error(arg); }); } } }, /** @type {function(...*)} */ critical: function() { if (!_.isUndefined(windowConsole) && windowConsole) { var args = ['Mixpanel error:'].concat(_.toArray(arguments)); try { windowConsole.error.apply(windowConsole, args); } catch (err) { _.each(args, function(arg) { windowConsole.error(arg); }); } } } }; var log_func_with_prefix = function(func, prefix) { return function() { arguments[0] = '[' + prefix + '] ' + arguments[0]; return func.apply(console, arguments); }; }; var console_with_prefix = function(prefix) { return { log: log_func_with_prefix(console.log, prefix), error: log_func_with_prefix(console.error, prefix), critical: log_func_with_prefix(console.critical, prefix) }; }; var safewrap = function(f) { return function() { try { return f.apply(this, arguments); } catch (e) { console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.'); if (Config.DEBUG){ console.critical(e); } } }; }; var safewrapClass = function(klass) { var proto = klass.prototype; for (var func in proto) { if (typeof(proto[func]) === 'function') { proto[func] = safewrap(proto[func]); } } }; // UNDERSCORE // Embed part of the Underscore Library _.bind = function(func, context) { var args, bound; if (nativeBind && func.bind === nativeBind) { return nativeBind.apply(func, slice.call(arguments, 1)); } if (!_.isFunction(func)) { throw new TypeError(); } args = slice.call(arguments, 2); bound = function() { if (!(this instanceof bound)) { return func.apply(context, args.concat(slice.call(arguments))); } var ctor = {}; ctor.prototype = func.prototype; var self = new ctor(); ctor.prototype = null; var result = func.apply(self, args.concat(slice.call(arguments))); if (Object(result) === result) { return result; } return self; }; return bound; }; /** * @param {*=} obj * @param {function(...*)=} iterator * @param {Object=} context */ _.each = function(obj, iterator, context) { if (obj === null || obj === undefined) { return; } if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { return; } } } else { for (var key in obj) { if (hasOwnProperty.call(obj, key)) { if (iterator.call(context, obj[key], key, obj) === breaker) { return; } } } } }; _.extend = function(obj) { _.each(slice.call(arguments, 1), function(source) { for (var prop in source) { if (source[prop] !== void 0) { obj[prop] = source[prop]; } } }); return obj; }; _.isArray = nativeIsArray || function(obj) { return toString.call(obj) === '[object Array]'; }; // from a comment on http://dbj.org/dbj/?p=286 // fails on only one very rare and deliberate custom object: // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; _.isFunction = function(f) { try { return /^\s*\bfunction\b/.test(f); } catch (x) { return false; } }; _.isArguments = function(obj) { return !!(obj && hasOwnProperty.call(obj, 'callee')); }; _.toArray = function(iterable) { if (!iterable) { return []; } if (iterable.toArray) { return iterable.toArray(); } if (_.isArray(iterable)) { return slice.call(iterable); } if (_.isArguments(iterable)) { return slice.call(iterable); } return _.values(iterable); }; _.map = function(arr, callback, context) { if (nativeMap && arr.map === nativeMap) { return arr.map(callback, context); } else { var results = []; _.each(arr, function(item) { results.push(callback.call(context, item)); }); return results; } }; _.keys = function(obj) { var results = []; if (obj === null) { return results; } _.each(obj, function(value, key) { results[results.length] = key; }); return results; }; _.values = function(obj) { var results = []; if (obj === null) { return results; } _.each(obj, function(value) { results[results.length] = value; }); return results; }; _.include = function(obj, target) { var found = false; if (obj === null) { return found; } if (nativeIndexOf && obj.indexOf === nativeIndexOf) { return obj.indexOf(target) != -1; } _.each(obj, function(value) { if (found || (found = (value === target))) { return breaker; } }); return found; }; _.includes = function(str, needle) { return str.indexOf(needle) !== -1; }; // Underscore Addons _.inherit = function(subclass, superclass) { subclass.prototype = new superclass(); subclass.prototype.constructor = subclass; subclass.superclass = superclass.prototype; return subclass; }; _.isObject = function(obj) { return (obj === Object(obj) && !_.isArray(obj)); }; _.isEmptyObject = function(obj) { if (_.isObject(obj)) { for (var key in obj) { if (hasOwnProperty.call(obj, key)) { return false; } } return true; } return false; }; _.isUndefined = function(obj) { return obj === void 0; }; _.isString = function(obj) { return toString.call(obj) == '[object String]'; }; _.isDate = function(obj) { return toString.call(obj) == '[object Date]'; }; _.isNumber = function(obj) { return toString.call(obj) == '[object Number]'; }; _.isElement = function(obj) { return !!(obj && obj.nodeType === 1); }; _.encodeDates = function(obj) { _.each(obj, function(v, k) { if (_.isDate(v)) { obj[k] = _.formatDate(v); } else if (_.isObject(v)) { obj[k] = _.encodeDates(v); // recurse } }); return obj; }; _.timestamp = function() { Date.now = Date.now || function() { return +new Date; }; return Date.now(); }; _.formatDate = function(d) { // YYYY-MM-DDTHH:MM:SS in UTC function pad(n) { return n < 10 ? '0' + n : n; } return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) + 'T' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()); }; _.strip_empty_properties = function(p) { var ret = {}; _.each(p, function(v, k) { if (_.isString(v) && v.length > 0) { ret[k] = v; } }); return ret; }; /* * this function returns a copy of object after truncating it. If * passed an Array or Object it will iterate through obj and * truncate all the values recursively. */ _.truncate = function(obj, length) { var ret; if (typeof(obj) === 'string') { ret = obj.slice(0, length); } else if (_.isArray(obj)) { ret = []; _.each(obj, function(val) { ret.push(_.truncate(val, length)); }); } else if (_.isObject(obj)) { ret = {}; _.each(obj, function(val, key) { ret[key] = _.truncate(val, length); }); } else { ret = obj; } return ret; }; _.JSONEncode = (function() { return function(mixed_val) { var value = mixed_val; var quote = function(string) { var escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex var meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"': '\\"', '\\': '\\\\' }; 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 + '"'; }; var str = function(key, holder) { var gap = ''; var indent = ' '; var i = 0; // The loop counter. var k = ''; // The member key. var v = ''; // The member value. var length = 0; var mind = gap; var partial = []; var 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); } // 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); case 'object': // If the type is 'object', we might be dealing with an object or an array or // null. // 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 (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; } // Iterate through all of the keys in the object. for (k in value) { if (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 ? '{' + partial.join(',') + '' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } }; // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', { '': value }); }; })(); /** * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js * Slightly modified to throw a real Error rather than a POJO */ _.JSONDecode = (function() { var at, // The index of the current character ch, // The current character escapee = { '"': '"', '\\': '\\', '/': '/', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t' }, text, error = function(m) { var e = new SyntaxError(m); e.at = at; e.text = text; throw e; }, next = function(c) { // If a c parameter is provided, verify that it matches the current character. if (c && c !== ch) { error('Expected \'' + c + '\' instead of \'' + ch + '\''); } // Get the next character. When there are no more characters, // return the empty string. ch = text.charAt(at); at += 1; return ch; }, number = function() { // Parse a number value. var number, string = ''; if (ch === '-') { string = '-'; next('-'); } while (ch >= '0' && ch <= '9') { string += ch; next(); } if (ch === '.') { string += '.'; while (next() && ch >= '0' && ch <= '9') { string += ch; } } if (ch === 'e' || ch === 'E') { string += ch; next(); if (ch === '-' || ch === '+') { string += ch; next(); } while (ch >= '0' && ch <= '9') { string += ch; next(); } } number = +string; if (!isFinite(number)) { error('Bad number'); } else { return number; } }, string = function() { // Parse a string value. var hex, i, string = '', uffff; // When parsing for string values, we must look for " and \ characters. if (ch === '"') { while (next()) { if (ch === '"') { next(); return string; } if (ch === '\\') { next(); if (ch === 'u') { uffff = 0; for (i = 0; i < 4; i += 1) { hex = parseInt(next(), 16); if (!isFinite(hex)) { break; } uffff = uffff * 16 + hex; } string += String.fromCharCode(uffff); } else if (typeof escapee[ch] === 'string') { string += escapee[ch]; } else { break; } } else { string += ch; } } } error('Bad string'); }, white = function() { // Skip whitespace. while (ch && ch <= ' ') { next(); } }, word = function() { // true, false, or null. switch (ch) { case 't': next('t'); next('r'); next('u'); next('e'); return true; case 'f': next('f'); next('a'); next('l'); next('s'); next('e'); return false; case 'n': next('n'); next('u'); next('l'); next('l'); return null; } error('Unexpected "' + ch + '"'); }, value, // Placeholder for the value function. array = function() { // Parse an array value. var array = []; if (ch === '[') { next('['); white(); if (ch === ']') { next(']'); return array; // empty array } while (ch) { array.push(value()); white(); if (ch === ']') { next(']'); return array; } next(','); white(); } } error('Bad array'); }, object = function() { // Parse an object value. var key, object = {}; if (ch === '{') { next('{'); white(); if (ch === '}') { next('}'); return object; // empty object } while (ch) { key = string(); white(); next(':'); if (Object.hasOwnProperty.call(object, key)) { error('Duplicate key "' + key + '"'); } object[key] = value(); white(); if (ch === '}') { next('}'); return object; } next(','); white(); } } error('Bad object'); }; value = function() { // Parse a JSON value. It could be an object, an array, a string, // a number, or a word. white(); switch (ch) { case '{': return object(); case '[': return array(); case '"': return string(); case '-': return number(); default: return ch >= '0' && ch <= '9' ? number() : word(); } }; // Return the json_parse function. It will have access to all of the // above functions and variables. return function(source) { var result; text = source; at = 0; ch = ' '; result = value(); white(); if (ch) { error('Syntax error'); } return result; }; })(); _.base64Encode = function(data) { var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = '', tmp_arr = []; if (!data) { return data; } data = _.utf8Encode(data); do { // pack three octets into four hexets o1 = data.charCodeAt(i++); o2 = data.charCodeAt(i++); o3 = data.charCodeAt(i++); bits = o1 << 16 | o2 << 8 | o3; h1 = bits >> 18 & 0x3f; h2 = bits >> 12 & 0x3f; h3 = bits >> 6 & 0x3f; h4 = bits & 0x3f; // use hexets to index into b64, and append result to encoded string tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); } while (i < data.length); enc = tmp_arr.join(''); switch (data.length % 3) { case 1: enc = enc.slice(0, -2) + '=='; break; case 2: enc = enc.slice(0, -1) + '='; break; } return enc; }; _.utf8Encode = function(string) { string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); var utftext = '', start, end; var stringl = 0, n; start = end = 0; stringl = string.length; for (n = 0; n < stringl; n++) { var c1 = string.charCodeAt(n); var enc = null; if (c1 < 128) { end++; } else if ((c1 > 127) && (c1 < 2048)) { enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); } else { enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); } if (enc !== null) { if (end > start) { utftext += string.substring(start, end); } utftext += enc; start = end = n + 1; } } if (end > start) { utftext += string.substring(start, string.length); } return utftext; }; _.UUID = function() { try { // use native Crypto API when available return window['crypto']['randomUUID'](); } catch (err) { // fall back to generating our own UUID // based on https://gist.github.com/scwood/3bff42cc005cc20ab7ec98f0d8e1d59d var uuid = new Array(36); for (var i = 0; i < 36; i++) { uuid[i] = Math.floor(Math.random() * 16); } uuid[14] = 4; // set bits 12-15 of time-high-and-version to 0100 uuid[19] = uuid[19] &= ~(1 << 2); // set bit 6 of clock-seq-and-reserved to zero uuid[19] = uuid[19] |= (1 << 3); // set bit 7 of clock-seq-and-reserved to one uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; return _.map(uuid, function(x) { return x.toString(16); }).join(''); } }; // _.isBlockedUA() // This is to block various web spiders from executing our JS and // sending false tracking data var BLOCKED_UA_STRS = [ 'ahrefsbot', 'ahrefssiteaudit', 'amazonbot', 'baiduspider', 'bingbot', 'bingpreview', 'chrome-lighthouse', 'facebookexternal', 'petalbot', 'pinterest', 'screaming frog', 'yahoo! slurp', 'yandex', // a whole bunch of goog-specific crawlers // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers 'adsbot-google', 'apis-google', 'duplexweb-google', 'feedfetcher-google', 'google favicon', 'google web preview', 'google-read-aloud', 'googlebot', 'googleweblight', 'mediapartners-google', 'storebot-google' ]; _.isBlockedUA = function(ua) { var i; ua = ua.toLowerCase(); for (i = 0; i < BLOCKED_UA_STRS.length; i++) { if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) { return true; } } return false; }; /** * @param {Object=} formdata * @param {string=} arg_separator */ _.HTTPBuildQuery = function(formdata, arg_separator) { var use_val, use_key, tmp_arr = []; if (_.isUndefined(arg_separator)) { arg_separator = '&'; } _.each(formdata, function(val, key) { use_val = encodeURIComponent(val.toString()); use_key = encodeURIComponent(key); tmp_arr[tmp_arr.length] = use_key + '=' + use_val; }); return tmp_arr.join(arg_separator); }; _.getQueryParam = function(url, param) { // Expects a raw URL param = param.replace(/[[]/g, '\\[').replace(/[\]]/g, '\\]'); var regexS = '[\\?&]' + param + '=([^&#]*)', regex = new RegExp(regexS), results = regex.exec(url); if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { return ''; } else { var result = results[1]; try { result = decodeURIComponent(result); } catch(err) { console.error('Skipping decoding for malformed query param: ' + result); } return result.replace(/\+/g, ' '); } }; // _.cookie // Methods partially borrowed from quirksmode.org/js/cookies.html _.cookie = { get: function(name) { var nameEQ = name + '='; var ca = document.cookie.split(';'); 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) { return decodeURIComponent(c.substring(nameEQ.length, c.length)); } } return null; }, parse: function(name) { var cookie; try { cookie = _.JSONDecode(_.cookie.get(name)) || {}; } catch (err) { // noop } return cookie; }, set_seconds: function(name, value, seconds, is_cross_subdomain, is_secure, is_cross_site, domain_override) { var cdomain = '', expires = '', secure = ''; if (domain_override) { cdomain = '; domain=' + domain_override; } else if (is_cross_subdomain) { var domain = extract_domain(document.location.hostname); cdomain = domain ? '; domain=.' + domain : ''; } if (seconds) { var date = new Date(); date.setTime(date.getTime() + (seconds * 1000)); expires = '; expires=' + date.toGMTString(); } if (is_cross_site) { is_secure = true; secure = '; SameSite=None'; } if (is_secure) { secure += '; secure'; } document.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; }, set: function(name, value, days, is_cross_subdomain, is_secure, is_cross_site, domain_override) { var cdomain = '', expires = '', secure = ''; if (domain_override) { cdomain = '; domain=' + domain_override; } else if (is_cross_subdomain) { var domain = extract_domain(document.location.hostname); cdomain = domain ? '; domain=.' + domain : ''; } if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = '; expires=' + date.toGMTString(); } if (is_cross_site) { is_secure = true; secure = '; SameSite=None'; } if (is_secure) { secure += '; secure'; } var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; document.cookie = new_cookie_val; return new_cookie_val; }, remove: function(name, is_cross_subdomain, domain_override) { _.cookie.set(name, '', -1, is_cross_subdomain, false, false, domain_override); } }; var _testStorageSupported = function (storage) { var supported = true; try { var key = '__mplss_' + cheap_guid(8), val = 'xyz'; storage.setItem(key, val); if (storage.getItem(key) !== val) { supported = false; } storage.removeItem(key); } catch (err) { supported = false; } return supported; }; var _localStorageSupported = null; var localStorageSupported = function(storage, forceCheck) { if (_localStorageSupported !== null && !forceCheck) { return _localStorageSupported; } return _localStorageSupported = _testStorageSupported(storage || window.localStorage); }; var _sessionStorageSupported = null; var sessionStorageSupported = function(storage, forceCheck) { if (_sessionStorageSupported !== null && !forceCheck) { return _sessionStorageSupported; } return _sessionStorageSupported = _testStorageSupported(storage || window.sessionStorage); }; function _storageWrapper(storage, name, is_supported_fn) { var log_error = function(msg) { console.error(name + ' error: ' + msg); }; return { is_supported: function(forceCheck) { var supported = is_supported_fn(storage, forceCheck); if (!supported) { console.error(name + ' unsupported'); } return supported; }, error: log_error, get: function(key) { try { return storage.getItem(key); } catch (err) { log_error(err); } return null; }, parse: function(key) { try { return _.JSONDecode(storage.getItem(key)) || {}; } catch (err) { // noop } return null; }, set: function(key, value) { try { storage.setItem(key, value); } catch (err) { log_error(err); } }, remove: function(key) { try { storage.removeItem(key); } catch (err) { log_error(err); } } }; } _.localStorage = _storageWrapper(window.localStorage, 'localStorage', localStorageSupported); _.sessionStorage = _storageWrapper(window.sessionStorage, 'sessionStorage', sessionStorageSupported); _.register_event = (function() { // written by Dean Edwards, 2005 // with input from Tino Zijdel - crisp@xs4all.nl // with input from Carl Sverre - mail@carlsverre.com // with input from Mixpanel // http://dean.edwards.name/weblog/2005/10/add-event/ // https://gist.github.com/1930440 /** * @param {Object} element * @param {string} type * @param {function(...*)} handler * @param {boolean=} oldSchool * @param {boolean=} useCapture */ var register_event = function(element, type, handler, oldSchool, useCapture) { if (!element) { console.error('No valid element provided to register_event'); return; } if (element.addEventListener && !oldSchool) { element.addEventListener(type, handler, !!useCapture); } else { var ontype = 'on' + type; var old_handler = element[ontype]; // can be undefined element[ontype] = makeHandler(element, handler, old_handler); } }; function makeHandler(element, new_handler, old_handlers) { var handler = function(event) { event = event || fixEvent(window.event); // this basically happens in firefox whenever another script // overwrites the onload callback and doesn't pass the event // object to previously defined callbacks. All the browsers // that don't define window.event implement addEventListener // so the dom_loaded handler will still be fired as usual. if (!event) { return undefined; } var ret = true; var old_result, new_result; if (_.isFunction(old_handlers)) { old_result = old_handlers(event); } new_result = new_handler.call(element, event); if ((false === old_result) || (false === new_result)) { ret = false; } return ret; }; return handler; } function fixEvent(event) { if (event) { event.preventDefault = fixEvent.preventDefault; event.stopPropagation = fixEvent.stopPropagation; } return event; } fixEvent.preventDefault = function() { this.returnValue = false; }; fixEvent.stopPropagation = function() { this.cancelBubble = true; }; return register_event; })(); var TOKEN_MATCH_REGEX = new RegExp('^(\\w*)\\[(\\w+)([=~\\|\\^\\$\\*]?)=?"?([^\\]"]*)"?\\]$'); _.dom_query = (function() { /* document.getElementsBySelector(selector) - returns an array of element objects from the current document matching the CSS selector. Selectors can contain element names, class names and ids and can be nested. For example: elements = document.getElementsBySelector('div#main p a.external') Will return an array of all 'a' elements with 'external' in their class attribute that are contained inside 'p' elements that are contained inside the 'div' element which has id="main" New in version 0.4: Support for CSS2 and CSS3 attribute selectors: See http://www.w3.org/TR/css3-selectors/#attribute-selectors Version 0.4 - Simon Willison, March 25th 2003 -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows -- Opera 7 fails Version 0.5 - Carl Sverre, Jan 7th 2013 -- Now uses jQuery-esque `hasClass` for testing class name equality. This fixes a bug related to '-' characters being considered not part of a 'word' in regex. */ function getAllChildren(e) { // Returns all children of element. Workaround required for IE5/Windows. Ugh. return e.all ? e.all : e.getElementsByTagName('*'); } var bad_whitespace = /[\t\r\n]/g; function hasClass(elem, selector) { var className = ' ' + selector + ' '; return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); } function getElementsBySelector(selector) { // Attempt to fail gracefully in lesser browsers if (!document.getElementsByTagName) { return []; } // Split selector in to tokens var tokens = selector.split(' '); var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; var currentContext = [document]; for (i = 0; i < tokens.length; i++) { token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); if (token.indexOf('#') > -1) { // Token is an ID selector bits = token.split('#'); tagName = bits[0]; var id = bits[1]; var element = document.getElementById(id); if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { // element not found or tag with that ID not found, return false return []; } // Set currentContext to contain just this element currentContext = [element]; continue; // Skip to next token } if (token.indexOf('.') > -1) { // Token contains a class selector bits = token.split('.'); tagName = bits[0]; var className = bits[1]; if (!tagName) { tagName = '*'; } // Get elements matching tag, filter them for class selector found = []; foundCount = 0; for (j = 0; j < currentContext.length; j++) { if (tagName == '*') { elements = getAllChildren(currentContext[j]); } else { elements = currentContext[j].getElementsByTagName(tagName); } for (k = 0; k < elements.length; k++) { found[foundCount++] = elements[k]; } } currentContext = []; currentContextIndex = 0; for (j = 0; j < found.length; j++) { if (found[j].className && _.isString(found[j].className) && // some SVG elements have classNames which are not strings hasClass(found[j], className) ) { currentContext[currentContextIndex++] = found[j]; } } continue; // Skip to next token } // Code to deal with attribute selectors var token_match = token.match(TOKEN_MATCH_REGEX); if (token_match) { tagName = token_match[1]; var attrName = token_match[2]; var attrOperator = token_match[3]; var attrValue = token_match[4]; if (!tagName) { tagName = '*'; } // Grab all of the tagName elements within current context found = []; foundCount = 0; for (j = 0; j < currentContext.length; j++) { if (tagName == '*') { elements = getAllChildren(currentContext[j]); } else { elements = currentContext[j].getElementsByTagName(tagName); } for (k = 0; k < elements.length; k++) { found[foundCount++] = elements[k]; } } currentContext = []; currentContextIndex = 0; var checkFunction; // This function will be used to filter the elements switch (attrOperator) { case '=': // Equality checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); }; break; case '~': // Match one of space seperated words checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); }; break; case '|': // Match start with value followed by optional hyphen checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); }; break; case '^': // Match starts with value checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) === 0); }; break; case '$': // Match ends with value - fails with "Warning" in Opera 7 checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); }; break; case '*': // Match ends with value checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); }; break; default: // Just test for existence of attribute checkFunction = function(e) { return e.getAttribute(attrName); }; } currentContext = []; currentContextIndex = 0; for (j = 0; j < found.length; j++) { if (checkFunction(found[j])) { currentContext[currentContextIndex++] = found[j]; } } // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); continue; // Skip to next token } // If we get here, token is JUST an element (not a class or ID selector) tagName = token; found = []; foundCount = 0; for (j = 0; j < currentContext.length; j++) { elements = currentContext[j].getElementsByTagName(tagName); for (k = 0; k < elements.length; k++) { found[foundCount++] = elements[k]; } } currentContext = found; } return currentContext; } return function(query) { if (_.isElement(query)) { return [query]; } else if (_.isObject(query) && !_.isUndefined(query.length)) { return query; } else { return getElementsBySelector.call(this, query); } }; })(); var CAMPAIGN_KEYWORDS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform','utm_campaign_id', 'utm_creative_format', 'utm_marketing_tactic']; var CLICK_IDS = ['dclid', 'fbclid', 'gclid', 'ko_click_id', 'li_fat_id', 'msclkid', 'sccid', 'ttclid', 'twclid', 'wbraid']; _.info = { campaignParams: function(default_value) { var kw = '', params = {}; _.each(CAMPAIGN_KEYWORDS, function(kwkey) { kw = _.getQueryParam(document.URL, kwkey); if (kw.length) { params[kwkey] = kw; } else if (default_value !== undefined) { params[kwkey] = default_value; } }); return params; }, clickParams: function() { var id = '', params = {}; _.each(CLICK_IDS, function(idkey) { id = _.getQueryParam(document.URL, idkey); if (id.length) { params[idkey] = id; } }); return params; }, marketingParams: function() { return _.extend(_.info.campaignParams(), _.info.clickParams()); }, searchEngine: function(referrer) { if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { return 'google'; } else if (referrer.search('https?://(.*)bing.com') === 0) { return 'bing'; } else if (referrer.search('https?://(.*)yahoo.com') === 0) { return 'yahoo'; } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { return 'duckduckgo'; } else { return null; } }, searchInfo: function(referrer) { var search = _.info.searchEngine(referrer), param = (search != 'yahoo') ? 'q' : 'p', ret = {}; if (search !== null) { ret['$search_engine'] = search; var keyword = _.getQueryParam(referrer, param); if (keyword.length) { ret['mp_keyword'] = keyword; } } return ret; }, /** * This function detects which browser is running this script. * The order of the checks are important since many user agents * include key words used in later checks. */ browser: function(user_agent, vendor, opera) { vendor = vendor || ''; // vendor is undefined for at least IE9 if (opera || _.includes(user_agent, ' OPR/')) { if (_.includes(user_agent, 'Mini')) { return 'Opera Mini'; } return 'Opera'; } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { return 'BlackBerry'; } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { return 'Internet Explorer Mobile'; } else if (_.includes(user_agent, 'SamsungBrowser/')) { // https://developer.samsung.com/internet/user-agent-string-format return 'Samsung Internet'; } else if (_.includes(user_agent, 'Edge') || _.includes(user_agent, 'Edg/')) { return 'Microsoft Edge'; } else if (_.includes(user_agent, 'FBIOS')) { return 'Facebook Mobile'; } else if (_.includes(user_agent, 'Whale/')) { // https://user-agents.net/browsers/whale-browser return 'Whale Browser'; } else if (_.includes(user_agent, 'Chrome')) { return 'Chrome'; } else if (_.includes(user_agent, 'CriOS')) { return 'Chrome iOS'; } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { return 'UC Browser'; } else if (_.includes(user_agent, 'FxiOS')) { return 'Firefox iOS'; } else if (_.includes(vendor, 'Apple')) { if (_.includes(user_agent, 'Mobile')) { return 'Mobile Safari'; } return 'Safari'; } else if (_.includes(user_agent, 'Android')) { return 'Android Mobile'; } else if (_.includes(user_agent, 'Konqueror')) { return 'Konqueror'; } else if (_.includes(user_agent, 'Firefox')) { return 'Firefox'; } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { return 'Internet Explorer'; } else if (_.includes(user_agent, 'Gecko')) { return 'Mozilla'; } else { return ''; } }, /** * This function detects which browser version is running this script, * parsing major and minor version (e.g., 42.1). User agent strings from: * http://www.useragentstring.com/pages/useragentstring.php */ browserVersion: function(userAgent, vendor, opera) { var browser = _.info.browser(userAgent, vendor, opera); var versionRegexs = { 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/, 'Chrome': /Chrome\/(\d+(\.\d+)?)/, 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, 'Safari': /Version\/(\d+(\.\d+)?)/, 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, 'Firefox': /Firefox\/(\d+(\.\d+)?)/, 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, 'Android Mobile': /android\s(\d+(\.\d+)?)/, 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/, 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, 'Mozilla': /rv:(\d+(\.\d+)?)/, 'Whale Browser': /Whale\/(\d+(\.\d+)?)/ }; var regex = versionRegexs[browser]; if (regex === undefined) { return null; } var matches = userAgent.match(regex); if (!matches) { return null; } return parseFloat(matches[matches.length - 2]); }, os: function() { var a = userAgent; if (/Windows/i.test(a)) { if (/Phone/.test(a