(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.bitanalytics = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } return escaped; }; _.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); }; _.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; }; _.identity = function(value) { return value; }; _.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()); }; _.safewrap = function(f) { return function() { try { return f.apply(this, arguments); } catch (e) { console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.'); if (Config.DEBUG){ console$1.critical(e); } } }; }; _.safewrap_class = function(klass, functions) { for (var i = 0; i < functions.length; i++) { klass.prototype[functions[i]] = _.safewrap(klass.prototype[functions[i]]); } }; _.safewrap_instance_methods = function(obj) { for (var func in obj) { if (typeof(obj[func]) === 'function') { obj[func] = _.safewrap(obj[func]); } } }; _.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() { // Time/ticks information // 1*new Date() is a cross browser version of Date.now() var T = function() { var d = 1 * new Date(), i = 0; // this while loop figures how many browser ticks go by // before 1*new Date() returns a new number, ie the amount // of ticks that go by per millisecond while (d == 1 * new Date()) { i++; } return d.toString(16) + i.toString(16); }; // Math.Random entropy var R = function() { return Math.random().toString(16).replace('.', ''); }; // User agent entropy // This function takes the user agent string, and then xors // together each sequence of 8 bytes. This produces a final // sequence of 8 bytes which it returns as hex. var UA = function() { var ua = userAgent, i, ch, buffer = [], ret = 0; function xor(result, byte_array) { var j, tmp = 0; for (j = 0; j < byte_array.length; j++) { tmp |= (buffer[j] << j * 8); } return result ^ tmp; } for (i = 0; i < ua.length; i++) { ch = ua.charCodeAt(i); buffer.unshift(ch & 0xFF); if (buffer.length >= 4) { ret = xor(ret, buffer); buffer = []; } } if (buffer.length > 0) { ret = xor(ret, buffer); } return ret.toString(16); }; return function() { var se = (screen.height * screen.width).toString(16); return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); }; })(); // _.isBlockedUA() // This is to block various web spiders from executing our JS and // sending false tracking data _.isBlockedUA = function(ua) { if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) { 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(/[\[]/, '\\\[').replace(/[\]]/, '\\\]'); var regexS = '[\\?&]' + param + '=([^&#]*)', regex = new RegExp(regexS), results = regex.exec(url); if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { return ''; } else { return decodeURIComponent(results[1]).replace(/\+/g, ' '); } }; _.getHashParam = function(hash, param) { var matches = hash.match(new RegExp(param + '=([^&]*)')); return matches ? matches[1] : null; }; // _.cookie // Methods partially borrowed from quirksmode.org/js/cookies.html _.cookie = { get: function(name) { var nameEQ = name + '='; var ca = document$1.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, cross_subdomain, is_secure) { var cdomain = '', expires = '', secure = ''; if (cross_subdomain) { var matches = document$1.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i), domain = matches ? matches[0] : ''; cdomain = ((domain) ? '; domain=.' + domain : ''); } if (seconds) { var date = new Date(); date.setTime(date.getTime() + (seconds * 1000)); expires = '; expires=' + date.toGMTString(); } if (is_secure) { secure = '; secure'; } document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; }, set: function(name, value, days, cross_subdomain, is_secure) { var cdomain = '', expires = '', secure = ''; if (cross_subdomain) { var matches = document$1.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i), domain = matches ? matches[0] : ''; 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_secure) { secure = '; secure'; } var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; document$1.cookie = new_cookie_val; return new_cookie_val; }, remove: function(name, cross_subdomain) { _.cookie.set(name, '', -1, cross_subdomain); } }; // _.localStorage var _localStorage_supported = null; _.localStorage = { is_supported: function() { if (_localStorage_supported !== null) { return _localStorage_supported; } var supported = true; try { var key = '__mplssupport__', val = 'xyz'; _.localStorage.set(key, val); if (_.localStorage.get(key) !== val) { supported = false; } _.localStorage.remove(key); } catch (err) { supported = false; } if (!supported) { console$1.error('localStorage unsupported; falling back to cookie store'); } _localStorage_supported = supported; return supported; }, error: function(msg) { console$1.error('localStorage error: ' + msg); }, get: function(name) { try { return window.localStorage.getItem(name); } catch (err) { _.localStorage.error(err); } return null; }, parse: function(name) { try { return _.JSONDecode(_.localStorage.get(name)) || {}; } catch (err) { // noop } return null; }, set: function(name, value) { try { window.localStorage.setItem(name, value); } catch (err) { _.localStorage.error(err); } }, remove: function(name) { try { window.localStorage.removeItem(name); } catch (err) { _.localStorage.error(err); } } }; _.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$1.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; })(); _.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$1.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$1]; 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$1.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(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/); 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); } }; })(); _.info = { campaignParams: function() { var campaign_keywords = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' '), kw = '', params = {}; _.each(campaign_keywords, function(kwkey) { kw = _.getQueryParam(document$1.URL, kwkey); if (kw.length) { params[kwkey] = kw; } }); return params; }, 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, 'Edge')) { return 'Microsoft Edge'; } else if (_.includes(user_agent, 'FBIOS')) { return 'Facebook Mobile'; } 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+)?)/, 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, 'Mozilla': /rv:(\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) || /WPDesktop/.test(a)) { return 'Windows Phone'; } return 'Windows'; } else if (/(iPhone|iPad|iPod)/.test(a)) { return 'iOS'; } else if (/Android/.test(a)) { return 'Android'; } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { return 'BlackBerry'; } else if (/Mac/i.test(a)) { return 'Mac OS X'; } else if (/Linux/.test(a)) { return 'Linux'; } else if (/CrOS/.test(a)) { return 'Chrome OS'; } else { return ''; } }, device: function(user_agent) { if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { return 'Windows Phone'; } else if (/iPad/.test(user_agent)) { return 'iPad'; } else if (/iPod/.test(user_agent)) { return 'iPod Touch'; } else if (/iPhone/.test(user_agent)) { return 'iPhone'; } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { return 'BlackBerry'; } else if (/Android/.test(user_agent)) { return 'Android'; } else { return ''; } }, referringDomain: function(referrer) { var split = referrer.split('/'); if (split.length >= 3) { return split[2]; } return ''; }, properties: function() { return _.extend(_.strip_empty_properties({ '$os': _.info.os(), '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera), '$referrer': document$1.referrer, '$referring_domain': _.info.referringDomain(document$1.referrer), '$device': _.info.device(userAgent) }), { '$current_url': window$1.location.href, '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera), '$screen_height': screen.height, '$screen_width': screen.width, 'mp_lib': 'web', '$lib_version': Config.LIB_VERSION }); }, people_properties: function() { return _.extend(_.strip_empty_properties({ '$os': _.info.os(), '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera) }), { '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera) }); }, pageviewInfo: function(page) { return _.strip_empty_properties({ 'mp_page': page, 'mp_referrer': document$1.referrer, 'mp_browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera), 'mp_platform': _.info.os() }); } }; // EXPORTS (for closure compiler) _['toArray'] = _.toArray; _['isObject'] = _.isObject; _['JSONEncode'] = _.JSONEncode; _['JSONDecode'] = _.JSONDecode; _['isBlockedUA'] = _.isBlockedUA; _['isEmptyObject'] = _.isEmptyObject; _['info'] = _.info; _['info']['device'] = _.info.device; _['info']['browser'] = _.info.browser; _['info']['properties'] = _.info.properties; /* * Get the className of an element, accounting for edge cases where element.className is an object * @param {Element} el - element to get the className of * @returns {string} the element's class */ function getClassName(el) { switch(typeof el.className) { case 'string': return el.className; case 'object': // handle cases where className might be SVGAnimatedString or some other type return el.className.baseVal || el.getAttribute('class') || ''; default: // future proof return ''; } } /* * Get the direct text content of an element, protecting against sensitive data collection. * Concats textContent of each of the element's text node children; this avoids potential * collection of sensitive data that could happen if we used element.textContent and the * element had sensitive child elements, since element.textContent includes child content. * Scrubs values that look like they could be sensitive (i.e. cc or ssn number). * @param {Element} el - element to get the text of * @returns {string} the element's direct text content */ function getSafeText(el) { var elText = ''; if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) { _.each(el.childNodes, function(child) { if (isTextNode(child) && child.textContent) { elText += _.trim(child.textContent) // scrub potentially sensitive values .split(/(\s+)/).filter(shouldTrackValue).join('') // normalize whitespace .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ') // truncate .substring(0, 255); } }); } return _.trim(elText); } /* * Check whether an element has nodeType Node.ELEMENT_NODE * @param {Element} el - element to check * @returns {boolean} whether el is of the correct nodeType */ function isElementNode(el) { return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability } /* * Check whether an element is of a given tag type. * Due to potential reference discrepancies (such as the webcomponents.js polyfill), * we want to match tagNames instead of specific references because something like * element === document.body won't always work because element might not be a native * element. * @param {Element} el - element to check * @param {string} tag - tag name (e.g., "div") * @returns {boolean} whether el is of the given tag type */ function isTag(el, tag) { return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase(); } /* * Check whether an element has nodeType Node.TEXT_NODE * @param {Element} el - element to check * @returns {boolean} whether el is of the correct nodeType */ function isTextNode(el) { return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability } /* * Check whether a DOM event should be "tracked" or if it may contain sentitive data * using a variety of heuristics. * @param {Element} el - element to check * @param {Event} event - event to check * @returns {boolean} whether the event should be tracked */ function shouldTrackDomEvent(el, event) { if (!el || isTag(el, 'html') || !isElementNode(el)) { return false; } var tag = el.tagName.toLowerCase(); switch (tag) { case 'html': return false; case 'form': return event.type === 'submit'; case 'input': if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) { return event.type === 'change'; } else { return event.type === 'click'; } case 'select': case 'textarea': return event.type === 'change'; default: return event.type === 'click'; } } /* * Check whether a DOM element should be "tracked" or if it may contain sentitive data * using a variety of heuristics. * @param {Element} el - element to check * @returns {boolean} whether the element should be tracked */ function shouldTrackElement(el) { for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) { var classes = getClassName(curEl).split(' '); if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) { return false; } } if (_.includes(getClassName(el).split(' '), 'mp-include')) { return true; } // don't send data from inputs or similar elements since there will always be // a risk of clientside javascript placing sensitive data in attributes if ( isTag(el, 'input') || isTag(el, 'select') || isTag(el, 'textarea') || el.getAttribute('contenteditable') === 'true' ) { return false; } // don't include hidden or password fields var type = el.type || ''; if (typeof type === 'string') { // it's possible for el.type to be a DOM element if el is a form with a child input[name="type"] switch(type.toLowerCase()) { case 'hidden': return false; case 'password': return false; } } // filter out data from fields that look like sensitive fields var name = el.name || el.id || ''; if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"] var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i; if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) { return false; } } return true; } /* * Check whether a string value should be "tracked" or if it may contain sentitive data * using a variety of heuristics. * @param {string} value - string value to check * @returns {boolean} whether the element should be tracked */ function shouldTrackValue(value) { if (value === null || _.isUndefined(value)) { return false; } if (typeof value === 'string') { value = _.trim(value); // check to see if input value looks like a credit card number // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html var ccRegex = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/; if (ccRegex.test((value || '').replace(/[\- ]/g, ''))) { return false; } // check to see if input value looks like a social security number var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/; if (ssnRegex.test(value)) { return false; } } return true; } var autotrack = { _initializedTokens: [], _previousElementSibling: function(el) { if (el.previousElementSibling) { return el.previousElementSibling; } else { do { el = el.previousSibling; } while (el && !isElementNode(el)); return el; } }, _loadScript: function(scriptUrlToLoad, callback) { var scriptTag = document.createElement('script'); scriptTag.type = 'text/javascript'; scriptTag.src = scriptUrlToLoad; scriptTag.onload = callback; var scripts = document.getElementsByTagName('script'); if (scripts.length > 0) { scripts[0].parentNode.insertBefore(scriptTag, scripts[0]); } else { document.body.appendChild(scriptTag); } }, _getPropertiesFromElement: function(elem) { var props = { 'classes': getClassName(elem).split(' '), 'tag_name': elem.tagName.toLowerCase() }; if (shouldTrackElement(elem)) { _.each(elem.attributes, function(attr) { if (shouldTrackValue(attr.value)) { props['attr__' + attr.name] = attr.value; } }); } var nthChild = 1; var nthOfType = 1; var currentElem = elem; while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign nthChild++; if (currentElem.tagName === elem.tagName) { nthOfType++; } } props['nth_child'] = nthChild; props['nth_of_type'] = nthOfType; return props; }, _getDefaultProperties: function(eventType) { return { '$event_type': eventType, '$ce_version': 1, '$host': window.location.host, '$pathname': window.location.pathname }; }, _extractCustomPropertyValue: function(customProperty) { var propValues = []; _.each(document.querySelectorAll(customProperty['css_selector']), function(matchedElem) { var value; if (['input', 'select'].indexOf(matchedElem.tagName.toLowerCase()) > -1) { value = matchedElem['value']; } else if (matchedElem['textContent']) { value = matchedElem['textContent']; } if (shouldTrackValue(value)) { propValues.push(value); } }); return propValues.join(', '); }, _getCustomProperties: function(targetElementList) { var props = {}; _.each(this._customProperties, function(customProperty) { _.each(customProperty['event_selectors'], function(eventSelector) { var eventElements = document.querySelectorAll(eventSelector); _.each(eventElements, function(eventElement) { if (_.includes(targetElementList, eventElement) && shouldTrackElement(eventElement)) { props[customProperty['name']] = this._extractCustomPropertyValue(customProperty); } }, this); }, this); }, this); return props; }, _getEventTarget: function(e) { // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes if (typeof e.target === 'undefined') { return e.srcElement; } else { return e.target; } }, _trackEvent: function(e, instance) { /*** Don't mess with this code without running IE8 tests on it ***/ var target = this._getEventTarget(e); if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html) target = target.parentNode; } if (shouldTrackDomEvent(target, e)) { var targetElementList = [target]; var curEl = target; while (curEl.parentNode && !isTag(curEl, 'body')) { targetElementList.push(curEl.parentNode); curEl = curEl.parentNode; } var elementsJson = []; var href, explicitNoTrack = false; _.each(targetElementList, function(el) { var shouldTrackEl = shouldTrackElement(el); // if the element or a parent element is an anchor tag // include the href as a property if (el.tagName.toLowerCase() === 'a') { href = el.getAttribute('href'); href = shouldTrackEl && shouldTrackValue(href) && href; } // allow users to programatically prevent tracking of elements by adding class 'mp-no-track' var classes = getClassName(el).split(' '); if (_.includes(classes, 'mp-no-track')) { explicitNoTrack = true; } elementsJson.push(this._getPropertiesFromElement(el)); }, this); if (explicitNoTrack) { return false; } // only populate text content from target element (not parents) // to prevent text within a sensitive element from being collected // as part of a parent's el.textContent var elementText; var safeElementText = getSafeText(target); if (safeElementText && safeElementText.length) { elementText = safeElementText; } var props = _.extend( this._getDefaultProperties(e.type), { '$elements': elementsJson, '$el_attr__href': href, '$el_text': elementText }, this._getCustomProperties(targetElementList) ); instance.track('$web_event', props); return true; } }, // only reason is to stub for unit tests // since you can't override window.location props _navigate: function(href) { window.location.href = href; }, _addDomEventHandlers: function(instance) { var handler = _.bind(function(e) { e = e || window.event; this._trackEvent(e, instance); }, this); _.register_event(document, 'submit', handler, false, true); _.register_event(document, 'change', handler, false, true); _.register_event(document, 'click', handler, false, true); }, _customProperties: {}, init: function(instance) { if (!(document && document.body)) { console.log('document not ready yet, trying again in 500 milliseconds...'); var that = this; setTimeout(function() { that.init(instance); }, 500); return; } var token = instance.get_config('token'); if (this._initializedTokens.indexOf(token) > -1) { console.log('autotrack already initialized for token "' + token + '"'); return; } this._initializedTokens.push(token); if (!this._maybeLoadEditor(instance)) { // don't autotrack actions when the editor is enabled var parseDecideResponse = _.bind(function(response) { if (response && response['config'] && response['config']['enable_collect_everything'] === true) { if (response['custom_properties']) { this._customProperties = response['custom_properties']; } instance.track('$web_event', _.extend({ '$title': document.title }, this._getDefaultProperties('pageview'))); this._addDomEventHandlers(instance); } else { instance['__autotrack_enabled'] = false; } }, this); instance._send_request( instance.get_config('api_host') + '/decide/', { 'verbose': true, 'version': '1', 'lib': 'web', 'token': token }, instance._prepare_callback(parseDecideResponse) ); } }, _editorParamsFromHash: function(instance, hash) { var editorParams; try { var state = _.getHashParam(hash, 'state'); state = JSON.parse(decodeURIComponent(state)); var expiresInSeconds = _.getHashParam(hash, 'expires_in'); editorParams = { 'accessToken': _.getHashParam(hash, 'access_token'), 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000), 'bookmarkletMode': !!state['bookmarkletMode'], 'projectId': state['projectId'], 'projectOwnerId': state['projectOwnerId'], 'projectToken': state['token'], 'readOnly': state['readOnly'], 'userFlags': state['userFlags'], 'userId': state['userId'] }; window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams)); if (state['desiredHash']) { window.location.hash = state['desiredHash']; } else if (window.history) { history.replaceState('', document.title, window.location.pathname + window.location.search); // completely remove hash } else { window.location.hash = ''; // clear hash (but leaves # unfortunately) } } catch (e) { console.error('Unable to parse data from hash', e); } return editorParams; }, /** * To load the visual editor, we need an access token and other state. That state comes from one of three places: * 1. In the URL hash params if the customer is using an old snippet * 2. From session storage under the key `_mpcehash` if the snippet already parsed the hash * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page */ _maybeLoadEditor: function(instance) { try { var parseFromUrl = false; if (_.getHashParam(window.location.hash, 'state')) { var state = _.getHashParam(window.location.hash, 'state'); state = JSON.parse(decodeURIComponent(state)); parseFromUrl = state['action'] === 'mpeditor'; } var parseFromStorage = !!window.sessionStorage.getItem('_mpcehash'); var editorParams; if (parseFromUrl) { // happens if they are initializing the editor using an old snippet editorParams = this._editorParamsFromHash(instance, window.location.hash); } else if (parseFromStorage) { // happens if they are initialized the editor and using the new snippet editorParams = this._editorParamsFromHash(instance, window.sessionStorage.getItem('_mpcehash')); window.sessionStorage.removeItem('_mpcehash'); } else { // get credentials from sessionStorage from a previous initialzation editorParams = JSON.parse(window.sessionStorage.getItem('editorParams') || '{}'); } if (editorParams['projectToken'] && instance.get_config('token') === editorParams['projectToken']) { this._loadEditor(instance, editorParams); return true; } else { return false; } } catch (e) { return false; } }, _loadEditor: function(instance, editorParams) { if (!window['_mpEditorLoaded']) { // only load the codeless event editor once, even if there are multiple instances of MixpanelLib window['_mpEditorLoaded'] = true; var editorUrl = instance.get_config('app_host') + '/js-bundle/reports/collect-everything/editor.js?_ts=' + (new Date()).getTime(); this._loadScript(editorUrl, function() { window['mp_load_editor'](editorParams); }); return true; } return false; }, // this is a mechanism to ramp up CE with no server-side interaction. // when CE is active, every page load results in a decide request. we // need to gently ramp this up so we don't overload decide. this decides // deterministically if CE is enabled for this project by modding the char // value of the project token. enabledForProject: function(token, numBuckets, numEnabledBuckets) { numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10; numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10; var charCodeSum = 0; for (var i = 0; i < token.length; i++) { charCodeSum += token.charCodeAt(i); } return (charCodeSum % numBuckets) < numEnabledBuckets; }, isBrowserSupported: function() { return _.isFunction(document.querySelectorAll); } }; _.bind_instance_methods(autotrack); _.safewrap_instance_methods(autotrack); /** * A function used to track a Mixpanel event (e.g. MixpanelLib.track) * @callback trackFunction * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ /** Public **/ var GDPR_DEFAULT_PERSISTENCE_PREFIX = '__mp_opt_in_out_'; /** * Opt the user in to data tracking and cookies/localstorage for the given token * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not */ function optIn(token, options) { _optInOut(true, token, options); } /** * Opt the user out of data tracking and cookies/localstorage for the given token * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookieExpiration] - number of days until the opt-out cookie expires * @param {boolean} [options.crossSubdomainCookie] - whether the opt-out cookie is set as cross-subdomain or not * @param {boolean} [options.secureCookie] - whether the opt-out cookie is set as secure or not */ function optOut(token, options) { _optInOut(false, token, options); } /** * Check whether the user has opted in to data tracking and cookies/localstorage for the given token * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @returns {boolean} whether the user has opted in to the given opt type */ function hasOptedIn(token, options) { return _getStorageValue(token, options) === '1'; } /** * Check whether the user has opted out of data tracking and cookies/localstorage for the given token * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @returns {boolean} whether the user has opted out of the given opt type */ function hasOptedOut(token, options) { if (_hasDoNotTrackFlagOn()) { return true; } return _getStorageValue(token, options) === '0'; } /** * Wrap a MixpanelLib method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token * If the user has opted out, return early instead of executing the method. * If a callback argument was provided, execute it passing the 0 error code. * @param {function} method - wrapped method to be executed if the user has not opted out * @returns {*} the result of executing method OR undefined if the user has opted out */ function addOptOutCheckMixpanelLib(method) { return _addOptOutCheck(method, function(name) { return this.get_config(name); }); } /** * Wrap a MixpanelPeople method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token * If the user has opted out, return early instead of executing the method. * If a callback argument was provided, execute it passing the 0 error code. * @param {function} method - wrapped method to be executed if the user has not opted out * @returns {*} the result of executing method OR undefined if the user has opted out */ function addOptOutCheckMixpanelPeople(method) { return _addOptOutCheck(method, function(name) { return this._get_config(name); }); } /** * Clear the user's opt in/out status of data tracking and cookies/localstorage for the given token * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {string} [options.persistenceType] Persistence mechanism used - cookie or localStorage * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not */ function clearOptInOut(token, options) { options = options || {}; _getStorage(options).remove(_getStorageKey(token, options), !!options.crossSubdomainCookie); } /** Private **/ /** * Get storage util * @param {Object} [options] * @param {string} [options.persistenceType] * @returns {object} either _.cookie or _.localstorage */ function _getStorage(options) { options = options || {}; return options.persistenceType === 'localStorage' ? _.localStorage : _.cookie; } /** * Get the name of the cookie that is used for the given opt type (tracking, cookie, etc.) * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @returns {string} the name of the cookie for the given opt type */ function _getStorageKey(token, options) { options = options || {}; return (options.persistencePrefix || GDPR_DEFAULT_PERSISTENCE_PREFIX) + token; } /** * Get the value of the cookie that is used for the given opt type (tracking, cookie, etc.) * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @returns {string} the value of the cookie for the given opt type */ function _getStorageValue(token, options) { return _getStorage(options).get(_getStorageKey(token, options)); } /** * Check whether the user has set the DNT/doNotTrack setting to true in their browser * @returns {boolean} whether the DNT setting is true */ function _hasDoNotTrackFlagOn() { return !!(window$1.navigator && window$1.navigator.doNotTrack === '1'); } /** * Set cookie/localstorage for the user indicating that they are opted in or out for the given opt type * @param {boolean} optValue - whether to opt the user in or out for the given opt type * @param {string} token - Mixpanel project tracking token * @param {Object} [options] * @param {trackFunction} [options.track] - function used for tracking a Mixpanel event to record the opt-in action * @param {string} [options.trackEventName] - event name to be used for tracking the opt-in action * @param {Object} [options.trackProperties] - set of properties to be tracked along with the opt-in action * @param {string} [options.persistencePrefix=__mp_opt_in_out] - custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookieExpiration] - number of days until the opt-in cookie expires * @param {boolean} [options.crossSubdomainCookie] - whether the opt-in cookie is set as cross-subdomain or not * @param {boolean} [options.secureCookie] - whether the opt-in cookie is set as secure or not */ function _optInOut(optValue, token, options) { if (!_.isString(token) || !token.length) { console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token'); return; } options = options || {}; _getStorage(options).set( _getStorageKey(token, options), optValue ? 1 : 0, _.isNumber(options.cookieExpiration) ? options.cookieExpiration : null, !!options.crossSubdomainCookie, !!options.secureCookie ); if (options.track && optValue) { // only track event if opting in (optValue=true) options.track(options.trackEventName || '$opt_in', options.trackProperties); } } /** * Wrap a method with a check for whether the user is opted out of data tracking and cookies/localstorage for the given token * If the user has opted out, return early instead of executing the method. * If a callback argument was provided, execute it passing the 0 error code. * @param {function} method - wrapped method to be executed if the user has not opted out * @param {function} getConfigValue - getter function for the Mixpanel API token and other options to be used with opt-out check * @returns {*} the result of executing method OR undefined if the user has opted out */ function _addOptOutCheck(method, getConfigValue) { return function() { var optedOut = false; try { var token = getConfigValue.call(this, 'token'); var persistenceType = getConfigValue.call(this, 'opt_out_tracking_persistence_type'); var persistencePrefix = getConfigValue.call(this, 'opt_out_tracking_cookie_prefix'); if (token) { // if there was an issue getting the token, continue method execution as normal optedOut = hasOptedOut(token, { persistenceType: persistenceType, persistencePrefix: persistencePrefix }); } } catch(err) { console.error('Unexpected error when checking tracking opt-out status: ' + err); } if (!optedOut) { return method.apply(this, arguments); } var callback = arguments[arguments.length - 1]; if (typeof(callback) === 'function') { callback(0); } return; }; } /* * Mixpanel JS Library * * Copyright 2012, Mixpanel, Inc. All Rights Reserved * http://mixpanel.com/ * * Includes portions of Underscore.js * http://documentcloud.github.com/underscore/ * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. * Released under the MIT License. */ // ==ClosureCompiler== // @compilation_level ADVANCED_OPTIMIZATIONS // @output_file_name mixpanel-2.8.min.js // ==/ClosureCompiler== /* SIMPLE STYLE GUIDE: this.x === public function this._x === internal - only use within this file this.__x === private - only use within the class Globals should be all caps */ var init_type; // MODULE or SNIPPET loader var mixpanel_master; // main mixpanel instance / object var INIT_MODULE = 0; var INIT_SNIPPET = 1; /* * Constants */ /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; /** @const */ var SET_QUEUE_KEY = '__mps'; /** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; /** @const */ var UNSET_QUEUE_KEY = '__mpus'; /** @const */ var ADD_QUEUE_KEY = '__mpa'; /** @const */ var APPEND_QUEUE_KEY = '__mpap'; /** @const */ var UNION_QUEUE_KEY = '__mpu'; /** @const */ var SET_ACTION = '$set'; /** @const */ var SET_ONCE_ACTION = '$set_once'; /** @const */ var UNSET_ACTION = '$unset'; /** @const */ var ADD_ACTION = '$add'; /** @const */ var APPEND_ACTION = '$append'; /** @const */ var UNION_ACTION = '$union'; // This key is deprecated, but we want to check for it to see whether aliasing is allowed. /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; /** @const */ var ALIAS_ID_KEY = '__alias'; /** @const */ var CAMPAIGN_IDS_KEY = '__cmpns'; /** @const */ var EVENT_TIMERS_KEY = '__timers'; /** @const */ var RESERVED_PROPERTIES = [ SET_QUEUE_KEY, SET_ONCE_QUEUE_KEY, UNSET_QUEUE_KEY, ADD_QUEUE_KEY, APPEND_QUEUE_KEY, UNION_QUEUE_KEY, PEOPLE_DISTINCT_ID_KEY, ALIAS_ID_KEY, CAMPAIGN_IDS_KEY, EVENT_TIMERS_KEY ]; /* * Dynamic... constants? Is that an oxymoron? */ // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials var USE_XHR = (window$1.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); // IE<10 does not support cross-origin XHR's but script tags // with defer won't block window.onload; ENQUEUE_REQUESTS // should only be true for Opera<12 var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); /* * Module-level globals */ var DEFAULT_CONFIG = { 'api_host': 'https://api.mixpanel.com', 'app_host': 'https://mixpanel.com', 'autotrack': true, 'cdn': 'https://cdn.mxpnl.com', 'cross_subdomain_cookie': true, 'persistence': 'cookie', 'persistence_name': '', 'cookie_name': '', 'loaded': function() {}, 'store_google': true, 'save_referrer': true, 'test': false, 'verbose': false, 'img': false, 'track_pageview': true, 'debug': false, 'track_links_timeout': 300, 'cookie_expiration': 365, 'upgrade': false, 'disable_persistence': false, 'disable_cookie': false, 'secure_cookie': false, 'ip': true, 'opt_out_tracking_by_default': false, 'opt_out_tracking_persistence_type': 'localStorage', 'opt_out_tracking_cookie_prefix': null, 'property_blacklist': [], 'xhr_headers': {} // { header: value, header2: value } }; var DOM_LOADED = false; /** * DomTracker Object * @constructor */ var DomTracker = function() {}; // interface DomTracker.prototype.create_properties = function() {}; DomTracker.prototype.event_handler = function() {}; DomTracker.prototype.after_track_handler = function() {}; DomTracker.prototype.init = function(mixpanel_instance) { this.mp = mixpanel_instance; return this; }; /** * @param {Object|string} query * @param {string} event_name * @param {Object=} properties * @param {function(...[*])=} user_callback */ DomTracker.prototype.track = function(query, event_name, properties, user_callback) { var that = this; var elements = _.dom_query(query); if (elements.length === 0) { console$1.error('The DOM query (' + query + ') returned 0 elements'); return; } _.each(elements, function(element) { _.register_event(element, this.override_event, function(e) { var options = {}; var props = that.create_properties(properties, this); var timeout = that.mp.get_config('track_links_timeout'); that.event_handler(e, this, options); // in case the mixpanel servers don't get back to us in time window$1.setTimeout(that.track_callback(user_callback, props, options, true), timeout); // fire the tracking event that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); }); }, this); return true; }; /** * @param {function(...[*])} user_callback * @param {Object} props * @param {boolean=} timeout_occured */ DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { timeout_occured = timeout_occured || false; var that = this; return function() { // options is referenced from both callbacks, so we can have // a 'lock' of sorts to ensure only one fires if (options.callback_fired) { return; } options.callback_fired = true; if (user_callback && user_callback(timeout_occured, props) === false) { // user can prevent the default functionality by // returning false from their callback return; } that.after_track_handler(props, options, timeout_occured); }; }; DomTracker.prototype.create_properties = function(properties, element) { var props; if (typeof(properties) === 'function') { props = properties(element); } else { props = _.extend({}, properties); } return props; }; /** * LinkTracker Object * @constructor * @extends DomTracker */ var LinkTracker = function() { this.override_event = 'click'; }; _.inherit(LinkTracker, DomTracker); LinkTracker.prototype.create_properties = function(properties, element) { var props = LinkTracker.superclass.create_properties.apply(this, arguments); if (element.href) { props['url'] = element.href; } return props; }; LinkTracker.prototype.event_handler = function(evt, element, options) { options.new_tab = ( evt.which === 2 || evt.metaKey || evt.ctrlKey || element.target === '_blank' ); options.href = element.href; if (!options.new_tab) { evt.preventDefault(); } }; LinkTracker.prototype.after_track_handler = function(props, options) { if (options.new_tab) { return; } setTimeout(function() { window$1.location = options.href; }, 0); }; /** * FormTracker Object * @constructor * @extends DomTracker */ var FormTracker = function() { this.override_event = 'submit'; }; _.inherit(FormTracker, DomTracker); FormTracker.prototype.event_handler = function(evt, element, options) { options.element = element; evt.preventDefault(); }; FormTracker.prototype.after_track_handler = function(props, options) { setTimeout(function() { options.element.submit(); }, 0); }; /** * Mixpanel Persistence Object * @constructor */ var MixpanelPersistence = function(config) { this['props'] = {}; this.campaign_params_saved = false; if (config['persistence_name']) { this.name = 'mp_' + config['persistence_name']; } else { this.name = 'mp_' + config['token'] + '_mixpanel'; } var storage_type = config['persistence']; if (storage_type !== 'cookie' && storage_type !== 'localStorage') { console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); storage_type = config['persistence'] = 'cookie'; } if (storage_type === 'localStorage' && _.localStorage.is_supported()) { this.storage = _.localStorage; } else { this.storage = _.cookie; } this.load(); this.update_config(config); this.upgrade(config); this.save(); }; MixpanelPersistence.prototype.properties = function() { var p = {}; // Filter out reserved properties _.each(this['props'], function(v, k) { if (!_.include(RESERVED_PROPERTIES, k)) { p[k] = v; } }); return p; }; MixpanelPersistence.prototype.load = function() { if (this.disabled) { return; } var entry = this.storage.parse(this.name); if (entry) { this['props'] = _.extend({}, entry); } }; MixpanelPersistence.prototype.upgrade = function(config) { var upgrade_from_old_lib = config['upgrade'], old_cookie_name, old_cookie; if (upgrade_from_old_lib) { old_cookie_name = 'mp_super_properties'; // Case where they had a custom cookie name before. if (typeof(upgrade_from_old_lib) === 'string') { old_cookie_name = upgrade_from_old_lib; } old_cookie = this.storage.parse(old_cookie_name); // remove the cookie this.storage.remove(old_cookie_name); this.storage.remove(old_cookie_name, true); if (old_cookie) { this['props'] = _.extend( this['props'], old_cookie['all'], old_cookie['events'] ); } } if (!config['cookie_name'] && config['name'] !== 'mixpanel') { // special case to handle people with cookies of the form // mp_TOKEN_INSTANCENAME from the first release of this library old_cookie_name = 'mp_' + config['token'] + '_' + config['name']; old_cookie = this.storage.parse(old_cookie_name); if (old_cookie) { this.storage.remove(old_cookie_name); this.storage.remove(old_cookie_name, true); // Save the prop values that were in the cookie from before - // this should only happen once as we delete the old one. this.register_once(old_cookie); } } if (this.storage === _.localStorage) { old_cookie = _.cookie.parse(this.name); _.cookie.remove(this.name); _.cookie.remove(this.name, true); if (old_cookie) { this.register_once(old_cookie); } } }; MixpanelPersistence.prototype.save = function() { if (this.disabled) { return; } this._expire_notification_campaigns(); this.storage.set( this.name, _.JSONEncode(this['props']), this.expire_days, this.cross_subdomain, this.secure ); }; MixpanelPersistence.prototype.remove = function() { // remove both domain and subdomain cookies this.storage.remove(this.name, false); this.storage.remove(this.name, true); }; // removes the storage entry and deletes all loaded data // forced name for tests MixpanelPersistence.prototype.clear = function() { this.remove(); this['props'] = {}; }; /** * @param {Object} props * @param {*=} default_value * @param {number=} days */ MixpanelPersistence.prototype.register_once = function(props, default_value, days) { if (_.isObject(props)) { if (typeof(default_value) === 'undefined') { default_value = 'None'; } this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; _.each(props, function(val, prop) { if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { this['props'][prop] = val; } }, this); this.save(); return true; } return false; }; /** * @param {Object} props * @param {number=} days */ MixpanelPersistence.prototype.register = function(props, days) { if (_.isObject(props)) { this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; _.extend(this['props'], props); this.save(); return true; } return false; }; MixpanelPersistence.prototype.unregister = function(prop) { if (prop in this['props']) { delete this['props'][prop]; this.save(); } }; MixpanelPersistence.prototype._expire_notification_campaigns = _.safewrap(function() { var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY], EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN) if (!campaigns_shown) { return; } for (var campaign_id in campaigns_shown) { if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) { delete campaigns_shown[campaign_id]; } } if (_.isEmptyObject(campaigns_shown)) { delete this['props'][CAMPAIGN_IDS_KEY]; } }); MixpanelPersistence.prototype.update_campaign_params = function() { if (!this.campaign_params_saved) { this.register_once(_.info.campaignParams()); this.campaign_params_saved = true; } }; MixpanelPersistence.prototype.update_search_keyword = function(referrer) { this.register(_.info.searchInfo(referrer)); }; // EXPORTED METHOD, we test this directly. MixpanelPersistence.prototype.update_referrer_info = function(referrer) { // If referrer doesn't exist, we want to note the fact that it was type-in traffic. this.register_once({ '$initial_referrer': referrer || '$direct', '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' }, ''); }; MixpanelPersistence.prototype.get_referrer_info = function() { return _.strip_empty_properties({ '$initial_referrer': this['props']['$initial_referrer'], '$initial_referring_domain': this['props']['$initial_referring_domain'] }); }; // safely fills the passed in object with stored properties, // does not override any properties defined in both // returns the passed in object MixpanelPersistence.prototype.safe_merge = function(props) { _.each(this['props'], function(val, prop) { if (!(prop in props)) { props[prop] = val; } }); return props; }; MixpanelPersistence.prototype.update_config = function(config) { this.default_expiry = this.expire_days = config['cookie_expiration']; this.set_disabled(config['disable_persistence']); this.set_cross_subdomain(config['cross_subdomain_cookie']); this.set_secure(config['secure_cookie']); }; MixpanelPersistence.prototype.set_disabled = function(disabled) { this.disabled = disabled; if (this.disabled) { this.remove(); } else { this.save(); } }; MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { if (cross_subdomain !== this.cross_subdomain) { this.cross_subdomain = cross_subdomain; this.remove(); this.save(); } }; MixpanelPersistence.prototype.get_cross_subdomain = function() { return this.cross_subdomain; }; MixpanelPersistence.prototype.set_secure = function(secure) { if (secure !== this.secure) { this.secure = secure ? true : false; this.remove(); this.save(); } }; MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { var q_key = this._get_queue_key(queue), q_data = data[queue], set_q = this._get_or_create_queue(SET_ACTION), set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), unset_q = this._get_or_create_queue(UNSET_ACTION), add_q = this._get_or_create_queue(ADD_ACTION), union_q = this._get_or_create_queue(UNION_ACTION), append_q = this._get_or_create_queue(APPEND_ACTION, []); if (q_key === SET_QUEUE_KEY) { // Update the set queue - we can override any existing values _.extend(set_q, q_data); // if there was a pending increment, override it // with the set. this._pop_from_people_queue(ADD_ACTION, q_data); // if there was a pending union, override it // with the set. this._pop_from_people_queue(UNION_ACTION, q_data); this._pop_from_people_queue(UNSET_ACTION, q_data); } else if (q_key === SET_ONCE_QUEUE_KEY) { // only queue the data if there is not already a set_once call for it. _.each(q_data, function(v, k) { if (!(k in set_once_q)) { set_once_q[k] = v; } }); this._pop_from_people_queue(UNSET_ACTION, q_data); } else if (q_key === UNSET_QUEUE_KEY) { _.each(q_data, function(prop) { // undo previously-queued actions on this key _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { if (prop in enqueued_obj) { delete enqueued_obj[prop]; } }); _.each(append_q, function(append_obj) { if (prop in append_obj) { delete append_obj[prop]; } }); unset_q[prop] = true; }); } else if (q_key === ADD_QUEUE_KEY) { _.each(q_data, function(v, k) { // If it exists in the set queue, increment // the value if (k in set_q) { set_q[k] += v; } else { // If it doesn't exist, update the add // queue if (!(k in add_q)) { add_q[k] = 0; } add_q[k] += v; } }, this); this._pop_from_people_queue(UNSET_ACTION, q_data); } else if (q_key === UNION_QUEUE_KEY) { _.each(q_data, function(v, k) { if (_.isArray(v)) { if (!(k in union_q)) { union_q[k] = []; } // We may send duplicates, the server will dedup them. union_q[k] = union_q[k].concat(v); } }); this._pop_from_people_queue(UNSET_ACTION, q_data); } else if (q_key === APPEND_QUEUE_KEY) { append_q.push(q_data); this._pop_from_people_queue(UNSET_ACTION, q_data); } console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); console$1.log(data); this.save(); }; MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { var q = this._get_queue(queue); if (!_.isUndefined(q)) { _.each(data, function(v, k) { delete q[k]; }, this); this.save(); } }; MixpanelPersistence.prototype._get_queue_key = function(queue) { if (queue === SET_ACTION) { return SET_QUEUE_KEY; } else if (queue === SET_ONCE_ACTION) { return SET_ONCE_QUEUE_KEY; } else if (queue === UNSET_ACTION) { return UNSET_QUEUE_KEY; } else if (queue === ADD_ACTION) { return ADD_QUEUE_KEY; } else if (queue === APPEND_ACTION) { return APPEND_QUEUE_KEY; } else if (queue === UNION_ACTION) { return UNION_QUEUE_KEY; } else { console$1.error('Invalid queue:', queue); } }; MixpanelPersistence.prototype._get_queue = function(queue) { return this['props'][this._get_queue_key(queue)]; }; MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { var key = this._get_queue_key(queue); default_val = _.isUndefined(default_val) ? {} : default_val; return this['props'][key] || (this['props'][key] = default_val); }; MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { var timers = this['props'][EVENT_TIMERS_KEY] || {}; timers[event_name] = timestamp; this['props'][EVENT_TIMERS_KEY] = timers; this.save(); }; MixpanelPersistence.prototype.remove_event_timer = function(event_name) { var timers = this['props'][EVENT_TIMERS_KEY] || {}; var timestamp = timers[event_name]; if (!_.isUndefined(timestamp)) { delete this['props'][EVENT_TIMERS_KEY][event_name]; this.save(); } return timestamp; }; /** * Mixpanel Library Object * @constructor */ var MixpanelLib = function() {}; /** * Mixpanel People Object * @constructor */ var MixpanelPeople = function() {}; var MPNotif; /** * create_mplib(token:string, config:object, name:string) * * This function is used by the init method of MixpanelLib objects * as well as the main initializer at the end of the JSLib (that * initializes document.mixpanel as well as any additional instances * declared before this file has loaded). */ var create_mplib = function(token, config, name) { var instance, target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; if (target && init_type === INIT_MODULE) { instance = target; } else { if (target && !_.isArray(target)) { console$1.error('You have already initialized ' + name); return; } instance = new MixpanelLib(); } instance._init(token, config, name); instance['people'] = new MixpanelPeople(); instance['people']._init(instance); // if any instance on the page has debug = true, we set the // global debug to be true Config.DEBUG = Config.DEBUG || instance.get_config('debug'); instance['__autotrack_enabled'] = instance.get_config('autotrack'); if (instance.get_config('autotrack')) { var num_buckets = 100; var num_enabled_buckets = 100; if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) { instance['__autotrack_enabled'] = false; console$1.log('Not in active bucket: disabling Automatic Event Collection.'); } else if (!autotrack.isBrowserSupported()) { instance['__autotrack_enabled'] = false; console$1.log('Disabling Automatic Event Collection because this browser is not supported'); } else { autotrack.init(instance); } } // if target is not defined, we called init after the lib already // loaded, so there won't be an array of things to execute if (!_.isUndefined(target) && _.isArray(target)) { // Crunch through the people queue first - we queue this data up & // flush on identify, so it's better to do all these operations first instance._execute_array.call(instance['people'], target['people']); instance._execute_array(target); } return instance; }; // Initialization methods /** * This function initializes a new instance of the Mixpanel tracking object. * All new instances are added to the main mixpanel object as sub properties (such as * mixpanel.library_name) and also returned by this function. To define a * second instance on the page, you would call: * * mixpanel.init('new token', { your: 'config' }, 'library_name'); * * and use it like so: * * mixpanel.library_name.track(...); * * @param {String} token Your Mixpanel API token * @param {Object} [config] A dictionary of config options to override. See a list of default config options. * @param {String} [name] The name for the new mixpanel instance that you want created */ MixpanelLib.prototype.init = function (token, config, name) { if (_.isUndefined(name)) { console$1.error('You must name your new library: init(token, config, name)'); return; } if (name === PRIMARY_INSTANCE_NAME) { console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); return; } var instance = create_mplib(token, config, name); mixpanel_master[name] = instance; instance._loaded(); return instance; }; // mixpanel._init(token:string, config:object, name:string) // // This function sets up the current instance of the mixpanel // library. The difference between this method and the init(...) // method is this one initializes the actual instance, whereas the // init(...) method sets up a new library and calls _init on it. // MixpanelLib.prototype._init = function(token, config, name) { this['__loaded'] = true; this['config'] = {}; this.set_config(_.extend({}, DEFAULT_CONFIG, config, { 'name': name, 'token': token, 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' })); this['_jsc'] = function() {}; this.__dom_loaded_queue = []; this.__request_queue = []; this.__disabled_events = []; this._flags = { 'disable_all_events': false, 'identify_called': false }; this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); this._init_gdpr_persistence(); this.register_once({'distinct_id': _.UUID()}, ''); }; // Private methods MixpanelLib.prototype._update_persistence = function() { var disablePersistence = this.get_config('disable_persistence') || this.has_opted_out_tracking(); if (this['persistence'].disabled !== disablePersistence) { this['persistence'].set_disabled(disablePersistence); } }; MixpanelLib.prototype._loaded = function() { this.get_config('loaded')(this); // this happens after so a user can call identify/name_tag in // the loaded callback if (this.get_config('track_pageview')) { this.track_pageview(); } }; MixpanelLib.prototype._dom_loaded = function() { _.each(this.__dom_loaded_queue, function(item) { this._track_dom.apply(this, item); }, this); if (!this.has_opted_out_tracking()) { _.each(this.__request_queue, function(item) { this._send_request.apply(this, item); }, this); } delete this.__dom_loaded_queue; delete this.__request_queue; }; MixpanelLib.prototype._track_dom = function(DomClass, args) { if (this.get_config('img')) { console$1.error('You can\'t use DOM tracking functions with img = true.'); return false; } if (!DOM_LOADED) { this.__dom_loaded_queue.push([DomClass, args]); return false; } var dt = new DomClass().init(this); return dt.track.apply(dt, args); }; /** * _prepare_callback() should be called by callers of _send_request for use * as the callback argument. * * If there is no callback, this returns null. * If we are going to make XHR/XDR requests, this returns a function. * If we are going to use script tags, this returns a string to use as the * callback GET param. */ MixpanelLib.prototype._prepare_callback = function(callback, data) { if (_.isUndefined(callback)) { return null; } if (USE_XHR) { var callback_function = function(response) { callback(response, data); }; return callback_function; } else { // if the user gives us a callback, we store as a random // property on this instances jsc function and update our // callback string to reflect that. var jsc = this['_jsc']; var randomized_cb = '' + Math.floor(Math.random() * 100000000); var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; jsc[randomized_cb] = function(response) { delete jsc[randomized_cb]; callback(response, data); }; return callback_string; } }; MixpanelLib.prototype._send_request = function(url, data, callback) { if (ENQUEUE_REQUESTS) { this.__request_queue.push(arguments); return; } // needed to correctly format responses var verbose_mode = this.get_config('verbose'); if (data['verbose']) { verbose_mode = true; } if (this.get_config('test')) { data['test'] = 1; } if (verbose_mode) { data['verbose'] = 1; } if (this.get_config('img')) { data['img'] = 1; } if (!USE_XHR) { if (callback) { data['callback'] = callback; } else if (verbose_mode || this.get_config('test')) { // Verbose output (from verbose mode, or an error in test mode) is a json blob, // which by itself is not valid javascript. Without a callback, this verbose output will // cause an error when returned via jsonp, so we force a no-op callback param. // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 data['callback'] = '(function(){})'; } } data['ip'] = this.get_config('ip')?1:0; data['_'] = new Date().getTime().toString(); url += '?' + _.HTTPBuildQuery(data); if ('img' in data) { var img = document$1.createElement('img'); img.src = url; document$1.body.appendChild(img); } else if (USE_XHR) { try { var req = new XMLHttpRequest(); req.open('GET', url, true); var headers = this.get_config('xhr_headers'); _.each(headers, function(headerValue, headerName) { req.setRequestHeader(headerName, headerValue); }); // send the mp_optout cookie // withCredentials cannot be modified until after calling .open on Android and Mobile Safari req.withCredentials = true; req.onreadystatechange = function () { if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 if (req.status === 200) { if (callback) { if (verbose_mode) { var response; try { response = _.JSONDecode(req.responseText); } catch (e) { console$1.error(e); return; } callback(response); } else { callback(Number(req.responseText)); } } } else { var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; console$1.error(error); if (callback) { if (verbose_mode) { callback({status: 0, error: error}); } else { callback(0); } } } } }; req.send(null); } catch (e) { console$1.error(e); } } else { var script = document$1.createElement('script'); script.type = 'text/javascript'; script.async = true; script.defer = true; script.src = url; var s = document$1.getElementsByTagName('script')[0]; s.parentNode.insertBefore(script, s); } }; /** * _execute_array() deals with processing any mixpanel function * calls that were called before the Mixpanel library were loaded * (and are thus stored in an array so they can be called later) * * Note: we fire off all the mixpanel function calls && user defined * functions BEFORE we fire off mixpanel tracking calls. This is so * identify/register/set_config calls can properly modify early * tracking calls. * * @param {Array} array */ MixpanelLib.prototype._execute_array = function(array) { var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; _.each(array, function(item) { if (item) { fn_name = item[0]; if (typeof(item) === 'function') { item.call(this); } else if (_.isArray(item) && fn_name === 'alias') { alias_calls.push(item); } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { tracking_calls.push(item); } else { other_calls.push(item); } } }, this); var execute = function(calls, context) { _.each(calls, function(item) { this[item[0]].apply(this, item.slice(1)); }, context); }; execute(alias_calls, this); execute(other_calls, this); execute(tracking_calls, this); }; /** * push() keeps the standard async-array-push * behavior around after the lib is loaded. * This is only useful for external integrations that * do not wish to rely on our convenience methods * (created in the snippet). * * ### Usage: * mixpanel.push(['register', { a: 'b' }]); * * @param {Array} item A [function_name, args...] array to be executed */ MixpanelLib.prototype.push = function(item) { this._execute_array([item]); }; /** * Disable events on the Mixpanel object. If passed no arguments, * this function disables tracking of any event. If passed an * array of event names, those events will be disabled, but other * events will continue to be tracked. * * Note: this function does not stop other mixpanel functions from * firing, such as register() or people.set(). * * @param {Array} [events] An array of event names to disable */ MixpanelLib.prototype.disable = function(events) { if (typeof(events) === 'undefined') { this._flags.disable_all_events = true; } else { this.__disabled_events = this.__disabled_events.concat(events); } }; /** * Track an event. This is the most important and * frequently used Mixpanel function. * * ### Usage: * * // track an event named 'Registered' * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); * * To track link clicks or form submissions, see track_links() or track_forms(). * * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. * @param {Function} [callback] If provided, the callback function will be called after tracking the event. */ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, properties, callback) { if (typeof(callback) !== 'function') { callback = function() {}; } if (_.isUndefined(event_name)) { console$1.error('No event name provided to mixpanel.track'); return; } if (this._event_is_disabled(event_name)) { callback(0); return; } // set defaults properties = properties || {}; properties['token'] = this.get_config('token'); // set $duration if time_event was previously called for this event var start_timestamp = this['persistence'].remove_event_timer(event_name); if (!_.isUndefined(start_timestamp)) { var duration_in_ms = new Date().getTime() - start_timestamp; properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); } // update persistence this['persistence'].update_search_keyword(document$1.referrer); if (this.get_config('store_google')) { this['persistence'].update_campaign_params(); } if (this.get_config('save_referrer')) { this['persistence'].update_referrer_info(document$1.referrer); } // note: extend writes to the first object, so lets make sure we // don't write to the persistence properties object and info // properties object by passing in a new object // update properties with pageview info and super-properties properties = _.extend( {}, _.info.properties(), this['persistence'].properties(), properties ); var property_blacklist = this.get_config('property_blacklist'); if (_.isArray(property_blacklist)) { _.each(property_blacklist, function(blacklisted_prop) { delete properties[blacklisted_prop]; }); } else { console$1.error('Invalid value for property_blacklist config: ' + property_blacklist); } var data = { 'event': event_name, 'properties': properties }; var truncated_data = _.truncate(data, 255); var json_data = _.JSONEncode(truncated_data); var encoded_data = _.base64Encode(json_data); console$1.log('MIXPANEL REQUEST:'); console$1.log(truncated_data); this._send_request( this.get_config('api_host') + '/track/', { 'data': encoded_data }, this._prepare_callback(callback, truncated_data) ); return truncated_data; }); /** * Track a page view event, which is currently ignored by the server. * This function is called by default on page load unless the * track_pageview configuration variable is false. * * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url. * @api private */ MixpanelLib.prototype.track_pageview = function(page) { if (_.isUndefined(page)) { page = document$1.location.href; } this.track('mp_page_view', _.info.pageviewInfo(page)); }; /** * Track clicks on a set of document elements. Selector must be a * valid query. Elements must exist on the page at the time track_links is called. * * ### Usage: * * // track click for link id #nav * mixpanel.track_links('#nav', 'Clicked Nav Link'); * * ### Notes: * * This function will wait up to 300 ms for the Mixpanel * servers to respond. If they have not responded by that time * it will head to the link without ensuring that your event * has been tracked. To configure this timeout please see the * set_config() documentation below. * * If you pass a function in as the properties argument, the * function will receive the DOMElement that triggered the * event as an argument. You are expected to return an object * from the function; any properties defined on this object * will be sent to mixpanel as event properties. * * @type {Function} * @param {Object|String} query A valid DOM query, element or jQuery-esque list * @param {String} event_name The name of the event to track * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement */ MixpanelLib.prototype.track_links = function() { return this._track_dom.call(this, LinkTracker, arguments); }; /** * Track form submissions. Selector must be a valid query. * * ### Usage: * * // track submission for form id 'register' * mixpanel.track_forms('#register', 'Created Account'); * * ### Notes: * * This function will wait up to 300 ms for the mixpanel * servers to respond, if they have not responded by that time * it will head to the link without ensuring that your event * has been tracked. To configure this timeout please see the * set_config() documentation below. * * If you pass a function in as the properties argument, the * function will receive the DOMElement that triggered the * event as an argument. You are expected to return an object * from the function; any properties defined on this object * will be sent to mixpanel as event properties. * * @type {Function} * @param {Object|String} query A valid DOM query, element or jQuery-esque list * @param {String} event_name The name of the event to track * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement */ MixpanelLib.prototype.track_forms = function() { return this._track_dom.call(this, FormTracker, arguments); }; /** * Time an event by including the time between this call and a * later 'track' call for the same event in the properties sent * with the event. * * ### Usage: * * // time an event named 'Registered' * mixpanel.time_event('Registered'); * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); * * When called for a particular event name, the next track call for that event * name will include the elapsed time between the 'time_event' and 'track' * calls. This value is stored as seconds in the '$duration' property. * * @param {String} event_name The name of the event. */ MixpanelLib.prototype.time_event = function(event_name) { if (_.isUndefined(event_name)) { console$1.error('No event name provided to mixpanel.time_event'); return; } if (this._event_is_disabled(event_name)) { return; } this['persistence'].set_event_timer(event_name, new Date().getTime()); }; /** * Register a set of super properties, which are included with all * events. This will overwrite previous super property values. * * ### Usage: * * // register 'Gender' as a super property * mixpanel.register({'Gender': 'Female'}); * * // register several super properties when a user signs up * mixpanel.register({ * 'Email': 'jdoe@example.com', * 'Account Type': 'Free' * }); * * @param {Object} properties An associative array of properties to store about the user * @param {Number} [days] How many days since the user's last visit to store the super properties */ MixpanelLib.prototype.register = function(props, days) { this['persistence'].register(props, days); }; /** * Register a set of super properties only once. This will not * overwrite previous super property values, unlike register(). * * ### Usage: * * // register a super property for the first time only * mixpanel.register_once({ * 'First Login Date': new Date().toISOString() * }); * * ### Notes: * * If default_value is specified, current super properties * with that value will be overwritten. * * @param {Object} properties An associative array of properties to store about the user * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' * @param {Number} [days] How many days since the users last visit to store the super properties */ MixpanelLib.prototype.register_once = function(props, default_value, days) { this['persistence'].register_once(props, default_value, days); }; /** * Delete a super property stored with the current user. * * @param {String} property The name of the super property to remove */ MixpanelLib.prototype.unregister = function(property) { this['persistence'].unregister(property); }; MixpanelLib.prototype._register_single = function(prop, value) { var props = {}; props[prop] = value; this.register(props); }; /** * Identify a user with a unique ID instead of a Mixpanel * randomly generated distinct_id. If the method is never called, * then unique visitors will be identified by a UUID generated * the first time they visit the site. * * ### Notes: * * You can call this function to overwrite a previously set * unique ID for the current user. Mixpanel cannot translate * between IDs at this time, so when you change a user's ID * they will appear to be a new user. * * When used alone, mixpanel.identify will change the user's * distinct_id to the unique ID provided. When used in tandem * with mixpanel.alias, it will allow you to identify based on * unique ID and map that back to the original, anonymous * distinct_id given to the user upon her first arrival to your * site (thus connecting anonymous pre-signup activity to * post-signup activity). Though the two work together, do not * call identify() at the same time as alias(). Calling the two * at the same time can cause a race condition, so it is best * practice to call identify on the original, anonymous ID * right after you've aliased it. * Learn more about how mixpanel.identify and mixpanel.alias can be used. * * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. */ MixpanelLib.prototype.identify = function( unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback ) { // Optional Parameters // _set_callback:function A callback to be run if and when the People set queue is flushed // _add_callback:function A callback to be run if and when the People add queue is flushed // _append_callback:function A callback to be run if and when the People append queue is flushed // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed // _union_callback:function A callback to be run if and when the People union queue is flushed // _unset_callback:function A callback to be run if and when the People unset queue is flushed // identify only changes the distinct id if it doesn't match either the existing or the alias; // if it's new, blow away the alias as well. if (unique_id !== this.get_distinct_id() && unique_id !== this.get_property(ALIAS_ID_KEY)) { this.unregister(ALIAS_ID_KEY); this._register_single('distinct_id', unique_id); } this._check_and_handle_notifications(this.get_distinct_id()); this._flags.identify_called = true; // Flush any queued up people requests this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback); }; /** * Clears super properties and generates a new random distinct_id for this instance. * Useful for clearing data when a user logs out. */ MixpanelLib.prototype.reset = function() { this['persistence'].clear(); this._flags.identify_called = false; this.register_once({'distinct_id': _.UUID()}, ''); }; /** * Returns the current distinct id of the user. This is either the id automatically * generated by the library or the id that has been passed by a call to identify(). * * ### Notes: * * get_distinct_id() can only be called after the Mixpanel library has finished loading. * init() has a loaded function available to handle this automatically. For example: * * // set distinct_id after the mixpanel library has loaded * mixpanel.init('YOUR PROJECT TOKEN', { * loaded: function(mixpanel) { * distinct_id = mixpanel.get_distinct_id(); * } * }); */ MixpanelLib.prototype.get_distinct_id = function() { return this.get_property('distinct_id'); }; /** * Create an alias, which Mixpanel will use to link two distinct_ids going forward (not retroactively). * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the * following is a valid scenario: * * mixpanel.alias('new_id', 'existing_id'); * ... * mixpanel.alias('newer_id', 'new_id'); * * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID. * * ### Notes: * * The best practice is to call alias() when a unique ID is first created for a user * (e.g., when a user first registers for an account and provides an email address). * alias() should never be called more than once for a given user, except to * chain a newer ID to a previously new ID, as described above. * * @param {String} alias A unique identifier that you want to use for this user in the future. * @param {String} [original] The current identifier being used for this user. */ MixpanelLib.prototype.alias = function(alias, original) { // If the $people_distinct_id key exists in persistence, there has been a previous // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with // this ID, as it will duplicate users. if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { console$1.critical('Attempting to create alias for existing People user - aborting.'); return -2; } var _this = this; if (_.isUndefined(original)) { original = this.get_distinct_id(); } if (alias !== original) { this._register_single(ALIAS_ID_KEY, alias); return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() { // Flush the people queue _this.identify(alias); }); } else { console$1.error('alias matches current distinct_id - skipping api call.'); this.identify(alias); return -1; } }; /** * Provide a string to recognize the user by. The string passed to * this method will appear in the Mixpanel Streams product rather * than an automatically generated name. Name tags do not have to * be unique. * * This value will only be included in Streams data. * * @param {String} name_tag A human readable name for the user * @api private */ MixpanelLib.prototype.name_tag = function(name_tag) { this._register_single('mp_name_tag', name_tag); }; /** * Update the configuration of a mixpanel library instance. * * The default config is: * * { * // super properties cookie expiration (in days) * cookie_expiration: 365 * * // super properties span subdomains * cross_subdomain_cookie: true * * // debug mode * debug: false * * // if this is true, the mixpanel cookie or localStorage entry * // will be deleted, and no user persistence will take place * disable_persistence: false * * // if this is true, Mixpanel will automatically determine * // City, Region and Country data using the IP address of * //the client * ip: true * * // opt users out of tracking by this Mixpanel instance by default * opt_out_tracking_by_default: false * * // persistence mechanism used by opt-in/opt-out methods - cookie * // or localStorage - falls back to cookie if localStorage is unavailable * opt_out_tracking_persistence_type: 'localStorage' * * // customize the name of cookie/localStorage set by opt-in/opt-out methods * opt_out_tracking_cookie_prefix: null * * // type of persistent store for super properties (cookie/ * // localStorage) if set to 'localStorage', any existing * // mixpanel cookie value with the same persistence_name * // will be transferred to localStorage and deleted * persistence: 'cookie' * * // name for super properties persistent store * persistence_name: '' * * // names of properties/superproperties which should never * // be sent with track() calls * property_blacklist: [] * * // if this is true, mixpanel cookies will be marked as * // secure, meaning they will only be transmitted over https * secure_cookie: false * * // the amount of time track_links will * // wait for Mixpanel's servers to respond * track_links_timeout: 300 * * // should we track a page view on page load * track_pageview: true * * // if you set upgrade to be true, the library will check for * // a cookie from our old js library and import super * // properties from it, then the old cookie is deleted * // The upgrade config option only works in the initialization, * // so make sure you set it when you create the library. * upgrade: false * * // extra HTTP request headers to set for each API request, in * // the format {'Header-Name': value} * xhr_headers: {} * } * * * @param {Object} config A dictionary of new configuration values to update */ MixpanelLib.prototype.set_config = function(config) { if (_.isObject(config)) { _.extend(this['config'], config); if (!this.get_config('persistence_name')) { this['config']['persistence_name'] = this['config']['cookie_name']; } if (!this.get_config('disable_persistence')) { this['config']['disable_persistence'] = this['config']['disable_cookie']; } if (this['persistence']) { this['persistence'].update_config(this['config']); } Config.DEBUG = Config.DEBUG || this.get_config('debug'); } }; /** * returns the current config object for the library. */ MixpanelLib.prototype.get_config = function(prop_name) { return this['config'][prop_name]; }; /** * Returns the value of the super property named property_name. If no such * property is set, get_property() will return the undefined value. * * ### Notes: * * get_property() can only be called after the Mixpanel library has finished loading. * init() has a loaded function available to handle this automatically. For example: * * // grab value for 'user_id' after the mixpanel library has loaded * mixpanel.init('YOUR PROJECT TOKEN', { * loaded: function(mixpanel) { * user_id = mixpanel.get_property('user_id'); * } * }); * * @param {String} property_name The name of the super property you want to retrieve */ MixpanelLib.prototype.get_property = function(property_name) { return this['persistence']['props'][property_name]; }; MixpanelLib.prototype.toString = function() { var name = this.get_config('name'); if (name !== PRIMARY_INSTANCE_NAME) { name = PRIMARY_INSTANCE_NAME + '.' + name; } return name; }; MixpanelLib.prototype._event_is_disabled = function(event_name) { return _.isBlockedUA(userAgent) || this._flags.disable_all_events || _.include(this.__disabled_events, event_name); }; MixpanelLib.prototype._check_and_handle_notifications = addOptOutCheckMixpanelLib(function(distinct_id) { if ( !distinct_id || this._flags.identify_called || this.get_config('disable_notifications') ) { return; } console$1.log('MIXPANEL NOTIFICATION CHECK'); var data = { 'verbose': true, 'version': '2', 'lib': 'web', 'token': this.get_config('token'), 'distinct_id': distinct_id }; var self = this; this._send_request( this.get_config('api_host') + '/decide/', data, this._prepare_callback(function(r) { if (r['notifications'] && r['notifications'].length > 0) { self._show_notification.call(self, r['notifications'][0]); } }) ); }); MixpanelLib.prototype._show_notification = function(notification_data) { var notification = new MPNotif(notification_data, this); notification.show(); }; // perform some housekeeping around GDPR persistence of opt-in/out state MixpanelLib.prototype._init_gdpr_persistence = function() { var is_localStorage_requested = this.get_config('opt_out_tracking_persistence_type') === 'localStorage'; // try to convert opt-in/out cookies to localStorage if possible if (is_localStorage_requested && _.localStorage.is_supported()) { if (!this.has_opted_in_tracking() && this.has_opted_in_tracking({'persistence_type': 'cookie'})) { this.opt_in_tracking(); } if (!this.has_opted_out_tracking() && this.has_opted_out_tracking({'persistence_type': 'cookie'})) { this.opt_out_tracking(); } this.clear_opt_in_out_tracking({'persistence_type': 'cookie'}); } // check whether we should opt out by default and update persistence accordingly if (this.get_config('opt_out_tracking_by_default') || _.cookie.get('mp_optout')) { _.cookie.remove('mp_optout'); this.opt_out_tracking(); } this._update_persistence(); }; // call a base gdpr function after constructing the appropriate token and options args MixpanelLib.prototype._call_gdpr_func = function(func, options) { options = _.extend({ 'track': _.bind(this.track, this), 'persistence_type': this.get_config('opt_out_tracking_persistence_type'), 'cookie_prefix': this.get_config('opt_out_tracking_cookie_prefix'), 'cookie_expiration': this.get_config('cookie_expiration'), 'cross_subdomain_cookie': this.get_config('cross_subdomain_cookie'), 'secure_cookie': this.get_config('secure_cookie') }, options); // check if localStorage can be used for recording opt out status, fall back to cookie if not if (!_.localStorage.is_supported()) { options['persistence_type'] = 'cookie'; } return func(this.get_config('token'), { track: options['track'], trackEventName: options['track_event_name'], trackProperties: options['track_properties'], persistenceType: options['persistence_type'], persistencePrefix: options['cookie_prefix'], cookieExpiration: options['cookie_expiration'], crossSubdomainCookie: options['cross_subdomain_cookie'], secureCookie: options['secure_cookie'] }); }; /** * Opt the user in to data tracking and cookies/localstorage for this Mixpanel instance * * ### Usage * * // opt user in * mixpanel.opt_in_tracking(); * * // opt user in with specific event name, properties, cookie configuration * mixpanel.opt_in_tracking({ * track_event_name: 'User opted in', * track_event_properties: { * 'Email': 'jdoe@example.com' * }, * cookie_expiration: 30, * secure_cookie: true * }); * * @param {Object} [options] A dictionary of config options to override * @param {function} [options.track] Function used for tracking a Mixpanel event to record the opt-in action (default is this Mixpanel instance's track method) * @param {string} [options.track_event_name=$opt_in] Event name to be used for tracking the opt-in action * @param {Object} [options.track_properties] Set of properties to be tracked along with the opt-in action * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) */ MixpanelLib.prototype.opt_in_tracking = function(options) { this._call_gdpr_func(optIn, options); this._update_persistence(); }; /** * Opt the user out of data tracking and cookies/localstorage for this Mixpanel instance * * ### Usage * * // opt user out * mixpanel.opt_out_tracking(); * * // opt user out with different cookie configuration from Mixpanel instance * mixpanel.opt_out_tracking({ * cookie_expiration: 30, * secure_cookie: true * }); * * @param {Object} [options] A dictionary of config options to override * @param {boolean} [options.delete_user=true] If true, will delete the currently identified user's profile and clear all charges after opting the user out * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) */ MixpanelLib.prototype.opt_out_tracking = function(options) { // delete use and clear charges since these methods may be disabled by opt-out options = _.extend({'delete_user': true}, options); if (options['delete_user'] && this['people'] && this['people']._identify_called()) { this['people'].delete_user(); this['people'].clear_charges(); } this._call_gdpr_func(optOut, options); this._update_persistence(); }; /** * Check whether the user has opted in to data tracking and cookies/localstorage for this Mixpanel instance * * ### Usage * * var has_opted_in = mixpanel.has_opted_in_tracking(); * // use has_opted_in value * * @param {Object} [options] A dictionary of config options to override * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name * @returns {boolean} current opt-in status */ MixpanelLib.prototype.has_opted_in_tracking = function(options) { return this._call_gdpr_func(hasOptedIn, options); }; /** * Check whether the user has opted out of data tracking and cookies/localstorage for this Mixpanel instance * * ### Usage * * var has_opted_out = mixpanel.has_opted_out_tracking(); * // use has_opted_out value * * @param {Object} [options] A dictionary of config options to override * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name * @returns {boolean} current opt-out status */ MixpanelLib.prototype.has_opted_out_tracking = function(options) { return this._call_gdpr_func(hasOptedOut, options); }; /** * Clear the user's opt in/out status of data tracking and cookies/localstorage for this Mixpanel instance * * ### Usage * * // clear user's opt-in/out status * mixpanel.clear_opt_in_out_tracking(); * * // clear user's opt-in/out status with specific cookie configuration - should match * // configuration used when opt_in_tracking/opt_out_tracking methods were called. * mixpanel.clear_opt_in_out_tracking({ * cookie_expiration: 30, * secure_cookie: true * }); * * @param {Object} [options] A dictionary of config options to override * @param {string} [options.persistence_type=localStorage] Persistence mechanism used - cookie or localStorage - falls back to cookie if localStorage is unavailable * @param {string} [options.cookie_prefix=__mp_opt_in_out] Custom prefix to be used in the cookie/localstorage name * @param {Number} [options.cookie_expiration] Number of days until the opt-in cookie expires (overrides value specified in this Mixpanel instance's config) * @param {boolean} [options.cross_subdomain_cookie] Whether the opt-in cookie is set as cross-subdomain or not (overrides value specified in this Mixpanel instance's config) * @param {boolean} [options.secure_cookie] Whether the opt-in cookie is set as secure or not (overrides value specified in this Mixpanel instance's config) */ MixpanelLib.prototype.clear_opt_in_out_tracking = function(options) { this._call_gdpr_func(clearOptInOut, options); this._update_persistence(); }; MixpanelPeople.prototype._init = function(mixpanel_instance) { this._mixpanel = mixpanel_instance; }; /* * Set properties on a user record. * * ### Usage: * * mixpanel.people.set('gender', 'm'); * * // or set multiple properties at once * mixpanel.people.set({ * 'Company': 'Acme', * 'Plan': 'Premium', * 'Upgrade date': new Date() * }); * // properties can be strings, integers, dates, or lists * * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. * @param {*} [to] A value to set on the given property name * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.set = addOptOutCheckMixpanelPeople(function(prop, to, callback) { var data = {}; var $set = {}; if (_.isObject(prop)) { _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { $set[k] = v; } }, this); callback = to; } else { $set[prop] = to; } // make sure that the referrer info has been updated and saved if (this._get_config('save_referrer')) { this._mixpanel['persistence'].update_referrer_info(document$1.referrer); } // update $set object with default people properties $set = _.extend( {}, _.info.people_properties(), this._mixpanel['persistence'].get_referrer_info(), $set ); data[SET_ACTION] = $set; return this._send_request(data, callback); }); /* * Set properties on a user record, only if they do not yet exist. * This will not overwrite previous people property values, unlike * people.set(). * * ### Usage: * * mixpanel.people.set_once('First Login Date', new Date()); * * // or set multiple properties at once * mixpanel.people.set_once({ * 'First Login Date': new Date(), * 'Starting Plan': 'Premium' * }); * * // properties can be strings, integers or dates * * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. * @param {*} [to] A value to set on the given property name * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.set_once = addOptOutCheckMixpanelPeople(function(prop, to, callback) { var data = {}; var $set_once = {}; if (_.isObject(prop)) { _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { $set_once[k] = v; } }, this); callback = to; } else { $set_once[prop] = to; } data[SET_ONCE_ACTION] = $set_once; return this._send_request(data, callback); }); /* * Unset properties on a user record (permanently removes the properties and their values from a profile). * * ### Usage: * * mixpanel.people.unset('gender'); * * // or unset multiple properties at once * mixpanel.people.unset(['gender', 'Company']); * * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.unset = function(prop, callback) { var data = {}; var $unset = []; if (!_.isArray(prop)) { prop = [prop]; } _.each(prop, function(k) { if (!this._is_reserved_property(k)) { $unset.push(k); } }, this); data[UNSET_ACTION] = $unset; return this._send_request(data, callback); }; /* * Increment/decrement numeric people analytics properties. * * ### Usage: * * mixpanel.people.increment('page_views', 1); * * // or, for convenience, if you're just incrementing a counter by * // 1, you can simply do * mixpanel.people.increment('page_views'); * * // to decrement a counter, pass a negative number * mixpanel.people.increment('credits_left', -1); * * // like mixpanel.people.set(), you can increment multiple * // properties at once: * mixpanel.people.increment({ * counter1: 1, * counter2: 6 * }); * * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. * @param {Number} [by] An amount to increment the given property * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop, by, callback) { var data = {}; var $add = {}; if (_.isObject(prop)) { _.each(prop, function(v, k) { if (!this._is_reserved_property(k)) { if (isNaN(parseFloat(v))) { console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); return; } else { $add[k] = v; } } }, this); callback = by; } else { // convenience: mixpanel.people.increment('property'); will // increment 'property' by 1 if (_.isUndefined(by)) { by = 1; } $add[prop] = by; } data[ADD_ACTION] = $add; return this._send_request(data, callback); }); /* * Append a value to a list-valued people analytics property. * * ### Usage: * * // append a value to a list, creating it if needed * mixpanel.people.append('pages_visited', 'homepage'); * * // like mixpanel.people.set(), you can append multiple * // properties at once: * mixpanel.people.append({ * list1: 'bob', * list2: 123 * }); * * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. * @param {*} [value] An item to append to the list * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.append = addOptOutCheckMixpanelPeople(function(list_name, value, callback) { var data = {}; var $append = {}; if (_.isObject(list_name)) { _.each(list_name, function(v, k) { if (!this._is_reserved_property(k)) { $append[k] = v; } }, this); callback = value; } else { $append[list_name] = value; } data[APPEND_ACTION] = $append; return this._send_request(data, callback); }); /* * Merge a given list with a list-valued people analytics property, * excluding duplicate values. * * ### Usage: * * // merge a value to a list, creating it if needed * mixpanel.people.union('pages_visited', 'homepage'); * * // like mixpanel.people.set(), you can append multiple * // properties at once: * mixpanel.people.union({ * list1: 'bob', * list2: 123 * }); * * // like mixpanel.people.append(), you can append multiple * // values to the same list: * mixpanel.people.union({ * list1: ['bob', 'billy'] * }); * * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. * @param {*} [value] Value / values to merge with the given property * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.union = addOptOutCheckMixpanelPeople(function(list_name, values, callback) { var data = {}; var $union = {}; if (_.isObject(list_name)) { _.each(list_name, function(v, k) { if (!this._is_reserved_property(k)) { $union[k] = _.isArray(v) ? v : [v]; } }, this); callback = values; } else { $union[list_name] = _.isArray(values) ? values : [values]; } data[UNION_ACTION] = $union; return this._send_request(data, callback); }); /* * Record that you have charged the current user a certain amount * of money. Charges recorded with track_charge() will appear in the * Mixpanel revenue report. * * ### Usage: * * // charge a user $50 * mixpanel.people.track_charge(50); * * // charge a user $30.50 on the 2nd of january * mixpanel.people.track_charge(30.50, { * '$time': new Date('jan 1 2012') * }); * * @param {Number} amount The amount of money charged to the current user * @param {Object} [properties] An associative array of properties associated with the charge * @param {Function} [callback] If provided, the callback will be called when the server responds */ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(amount, properties, callback) { if (!_.isNumber(amount)) { amount = parseFloat(amount); if (isNaN(amount)) { console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); return; } } return this.append('$transactions', _.extend({ '$amount': amount }, properties), callback); }); /* * Permanently clear all revenue report transactions from the * current user's people analytics profile. * * ### Usage: * * mixpanel.people.clear_charges(); * * @param {Function} [callback] If provided, the callback will be called after the tracking event */ MixpanelPeople.prototype.clear_charges = function(callback) { return this.set('$transactions', [], callback); }; /* * Permanently deletes the current people analytics profile from * Mixpanel (using the current distinct_id). * * ### Usage: * * // remove the all data you have stored about the current user * mixpanel.people.delete_user(); * */ MixpanelPeople.prototype.delete_user = function() { if (!this._identify_called()) { console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); return; } var data = {'$delete': this._mixpanel.get_distinct_id()}; return this._send_request(data); }; MixpanelPeople.prototype.toString = function() { return this._mixpanel.toString() + '.people'; }; MixpanelPeople.prototype._send_request = function(data, callback) { data['$token'] = this._get_config('token'); data['$distinct_id'] = this._mixpanel.get_distinct_id(); var date_encoded_data = _.encodeDates(data); var truncated_data = _.truncate(date_encoded_data, 255); var json_data = _.JSONEncode(date_encoded_data); var encoded_data = _.base64Encode(json_data); if (!this._identify_called()) { this._enqueue(data); if (!_.isUndefined(callback)) { if (this._get_config('verbose')) { callback({status: -1, error: null}); } else { callback(-1); } } return truncated_data; } console$1.log('MIXPANEL PEOPLE REQUEST:'); console$1.log(truncated_data); this._mixpanel._send_request( this._get_config('api_host') + '/engage/', {'data': encoded_data}, this._mixpanel._prepare_callback(callback, truncated_data) ); return truncated_data; }; MixpanelPeople.prototype._get_config = function(conf_var) { return this._mixpanel.get_config(conf_var); }; MixpanelPeople.prototype._identify_called = function() { return this._mixpanel._flags.identify_called === true; }; // Queue up engage operations if identify hasn't been called yet. MixpanelPeople.prototype._enqueue = function(data) { if (SET_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); } else if (SET_ONCE_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); } else if (UNSET_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); } else if (ADD_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); } else if (APPEND_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); } else if (UNION_ACTION in data) { this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); } else { console$1.error('Invalid call to _enqueue():', data); } }; MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { var _this = this; var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action)); var action_params = queued_data; if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); if (queue_to_params_fn) { action_params = queue_to_params_fn(queued_data); } action_method.call(_this, action_params, function(response, data) { // on bad response, we want to add it back to the queue if (response === 0) { _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); } if (!_.isUndefined(callback)) { callback(response, data); } }); } }; // Flush queued engage operations - order does not matter, // and there are network level race conditions anyway MixpanelPeople.prototype._flush = function( _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback ) { var _this = this; var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION); this._flush_one_queue(SET_ACTION, this.set, _set_callback); this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); this._flush_one_queue(UNION_ACTION, this.union, _union_callback); // we have to fire off each $append individually since there is // no concat method server side if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { var $append_item; var callback = function(response, data) { if (response === 0) { _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); } if (!_.isUndefined(_append_callback)) { _append_callback(response, data); } }; for (var i = $append_queue.length - 1; i >= 0; i--) { $append_item = $append_queue.pop(); _this.append($append_item, callback); } // Save the shortened append queue _this._mixpanel['persistence'].save(); } }; MixpanelPeople.prototype._is_reserved_property = function(prop) { return prop === '$distinct_id' || prop === '$token'; }; // Internal class for notification display MixpanelLib._Notification = function(notif_data, mixpanel_instance) { _.bind_instance_methods(this); this.mixpanel = mixpanel_instance; this.persistence = this.mixpanel['persistence']; this.campaign_id = _.escapeHTML(notif_data['id']); this.message_id = _.escapeHTML(notif_data['message_id']); this.body = (_.escapeHTML(notif_data['body']) || '').replace(/\n/g, '
'); this.cta = _.escapeHTML(notif_data['cta']) || 'Close'; this.notif_type = _.escapeHTML(notif_data['type']) || 'takeover'; this.style = _.escapeHTML(notif_data['style']) || 'light'; this.title = _.escapeHTML(notif_data['title']) || ''; this.video_width = MPNotif.VIDEO_WIDTH; this.video_height = MPNotif.VIDEO_HEIGHT; // These fields are url-sanitized in the backend already. this.dest_url = notif_data['cta_url'] || null; this.image_url = notif_data['image_url'] || null; this.thumb_image_url = notif_data['thumb_image_url'] || null; this.video_url = notif_data['video_url'] || null; this.clickthrough = true; if (!this.dest_url) { this.dest_url = '#dismiss'; this.clickthrough = false; } this.mini = this.notif_type === 'mini'; if (!this.mini) { this.notif_type = 'takeover'; } this.notif_width = !this.mini ? MPNotif.NOTIF_WIDTH : MPNotif.NOTIF_WIDTH_MINI; this._set_client_config(); this.imgs_to_preload = this._init_image_html(); this._init_video(); }; MPNotif = MixpanelLib._Notification; MPNotif.ANIM_TIME = 200; MPNotif.MARKUP_PREFIX = 'mixpanel-notification'; MPNotif.BG_OPACITY = 0.6; MPNotif.NOTIF_TOP = 25; MPNotif.NOTIF_START_TOP = 200; MPNotif.NOTIF_WIDTH = 388; MPNotif.NOTIF_WIDTH_MINI = 420; MPNotif.NOTIF_HEIGHT_MINI = 85; MPNotif.THUMB_BORDER_SIZE = 5; MPNotif.THUMB_IMG_SIZE = 60; MPNotif.THUMB_OFFSET = Math.round(MPNotif.THUMB_IMG_SIZE / 2); MPNotif.VIDEO_WIDTH = 595; MPNotif.VIDEO_HEIGHT = 334; MPNotif.prototype.show = function() { var self = this; this._set_client_config(); // don't display until HTML body exists if (!this.body_el) { setTimeout(function() { self.show(); }, 300); return; } this._init_styles(); this._init_notification_el(); // wait for any images to load before showing notification this._preload_images(this._attach_and_animate); }; MPNotif.prototype.dismiss = _.safewrap(function() { if (!this.marked_as_shown) { // unexpected condition: user interacted with notif even though we didn't consider it // visible (see _mark_as_shown()); send tracking signals to mark delivery this._mark_delivery({'invisible': true}); } var exiting_el = this.showing_video ? this._get_el('video') : this._get_notification_display_el(); if (this.use_transitions) { this._remove_class('bg', 'visible'); this._add_class(exiting_el, 'exiting'); setTimeout(this._remove_notification_el, MPNotif.ANIM_TIME); } else { var notif_attr, notif_start, notif_goal; if (this.mini) { notif_attr = 'right'; notif_start = 20; notif_goal = -100; } else { notif_attr = 'top'; notif_start = MPNotif.NOTIF_TOP; notif_goal = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; } this._animate_els([ { el: this._get_el('bg'), attr: 'opacity', start: MPNotif.BG_OPACITY, goal: 0.0 }, { el: exiting_el, attr: 'opacity', start: 1.0, goal: 0.0 }, { el: exiting_el, attr: notif_attr, start: notif_start, goal: notif_goal } ], MPNotif.ANIM_TIME, this._remove_notification_el); } }); MPNotif.prototype._add_class = _.safewrap(function(el, class_name) { class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; if (typeof el === 'string') { el = this._get_el(el); } if (!el.className) { el.className = class_name; } else if (!~(' ' + el.className + ' ').indexOf(' ' + class_name + ' ')) { el.className += ' ' + class_name; } }); MPNotif.prototype._remove_class = _.safewrap(function(el, class_name) { class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; if (typeof el === 'string') { el = this._get_el(el); } if (el.className) { el.className = (' ' + el.className + ' ') .replace(' ' + class_name + ' ', '') .replace(/^[\s\xA0]+/, '') .replace(/[\s\xA0]+$/, ''); } }); MPNotif.prototype._animate_els = _.safewrap(function(anims, mss, done_cb, start_time) { var self = this, in_progress = false, ai, anim, cur_time = 1 * new Date(), time_diff; start_time = start_time || cur_time; time_diff = cur_time - start_time; for (ai = 0; ai < anims.length; ai++) { anim = anims[ai]; if (typeof anim.val === 'undefined') { anim.val = anim.start; } if (anim.val !== anim.goal) { in_progress = true; var anim_diff = anim.goal - anim.start, anim_dir = anim.goal >= anim.start ? 1 : -1; anim.val = anim.start + anim_diff * time_diff / mss; if (anim.attr !== 'opacity') { anim.val = Math.round(anim.val); } if ((anim_dir > 0 && anim.val >= anim.goal) || (anim_dir < 0 && anim.val <= anim.goal)) { anim.val = anim.goal; } } } if (!in_progress) { if (done_cb) { done_cb(); } return; } for (ai = 0; ai < anims.length; ai++) { anim = anims[ai]; if (anim.el) { var suffix = anim.attr === 'opacity' ? '' : 'px'; anim.el.style[anim.attr] = String(anim.val) + suffix; } } setTimeout(function() { self._animate_els(anims, mss, done_cb, start_time); }, 10); }); MPNotif.prototype._attach_and_animate = _.safewrap(function() { var self = this; // no possibility to double-display if (this.shown || this._get_shown_campaigns()[this.campaign_id]) { return; } this.shown = true; this.body_el.appendChild(this.notification_el); setTimeout(function() { var notif_el = self._get_notification_display_el(); if (self.use_transitions) { if (!self.mini) { self._add_class('bg', 'visible'); } self._add_class(notif_el, 'visible'); self._mark_as_shown(); } else { var notif_attr, notif_start, notif_goal; if (self.mini) { notif_attr = 'right'; notif_start = -100; notif_goal = 20; } else { notif_attr = 'top'; notif_start = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; notif_goal = MPNotif.NOTIF_TOP; } self._animate_els([ { el: self._get_el('bg'), attr: 'opacity', start: 0.0, goal: MPNotif.BG_OPACITY }, { el: notif_el, attr: 'opacity', start: 0.0, goal: 1.0 }, { el: notif_el, attr: notif_attr, start: notif_start, goal: notif_goal } ], MPNotif.ANIM_TIME, self._mark_as_shown); } }, 100); _.register_event(self._get_el('cancel'), 'click', function(e) { e.preventDefault(); self.dismiss(); }); var click_el = self._get_el('button') || self._get_el('mini-content'); _.register_event(click_el, 'click', function(e) { e.preventDefault(); if (self.show_video) { self._track_event('$campaign_open', {'$resource_type': 'video'}); self._switch_to_video(); } else { self.dismiss(); if (self.clickthrough) { self._track_event('$campaign_open', {'$resource_type': 'link'}, function() { window$1.location.href = self.dest_url; }); } } }); }); MPNotif.prototype._get_el = function(id) { return document$1.getElementById(MPNotif.MARKUP_PREFIX + '-' + id); }; MPNotif.prototype._get_notification_display_el = function() { return this._get_el(this.notif_type); }; MPNotif.prototype._get_shown_campaigns = function() { return this.persistence['props'][CAMPAIGN_IDS_KEY] || (this.persistence['props'][CAMPAIGN_IDS_KEY] = {}); }; MPNotif.prototype._browser_lte = function(browser, version) { return this.browser_versions[browser] && this.browser_versions[browser] <= version; }; MPNotif.prototype._init_image_html = function() { var imgs_to_preload = []; if (!this.mini) { if (this.image_url) { imgs_to_preload.push(this.image_url); this.img_html = ''; } else { this.img_html = ''; } if (this.thumb_image_url) { imgs_to_preload.push(this.thumb_image_url); this.thumb_img_html = '
' + '' + '
'; } else { this.thumb_img_html = ''; } } else { this.thumb_image_url = this.thumb_image_url || '//cdn.mxpnl.com/site_media/images/icons/notifications/mini-news-dark.png'; imgs_to_preload.push(this.thumb_image_url); } return imgs_to_preload; }; MPNotif.prototype._init_notification_el = function() { var notification_html = ''; var video_src = ''; var video_html = ''; var cancel_html = '
' + '
' + '
'; this.notification_el = document$1.createElement('div'); this.notification_el.id = MPNotif.MARKUP_PREFIX + '-wrapper'; if (!this.mini) { // TAKEOVER notification var close_html = (this.clickthrough || this.show_video) ? '' : '
', play_html = this.show_video ? '
' : ''; if (this._browser_lte('ie', 7)) { close_html = ''; play_html = ''; } notification_html = '
' + this.thumb_img_html + '
' + cancel_html + '
' + this.img_html + '
' + this.title + '
' + '
' + this.body + '
' + '' + '
' + '
' + close_html + '' + this.cta + '' + play_html + '
' + '
' + '
'; } else { // MINI notification notification_html = '
' + '
' + cancel_html + '
' + '
' + '
' + '
' + '
' + '
' + this.body + '
' + '
' + '
' + '
' + '
' + '
'; } if (this.youtube_video) { video_src = '//www.youtube.com/embed/' + this.youtube_video + '?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080'; if (this.yt_custom) { video_src += '&enablejsapi=1&html5=1&controls=0'; video_html = '
' + '
' + '
' + '
' + '
' + '
' + '
'; } } else if (this.vimeo_video) { video_src = '//player.vimeo.com/video/' + this.vimeo_video + '?autoplay=1&title=0&byline=0&portrait=0'; } if (this.show_video) { this.video_iframe = ''; video_html = '
' + '
' + '
' + video_html + '
' + '
'; } var main_html = video_html + notification_html; if (this.flip_animate) { main_html = (this.mini ? notification_html : '') + '
' + (this.mini ? video_html : main_html) + '
'; } this.notification_el.innerHTML = ('
' + '
' + '
' + '
' + main_html + '
' + '
' + '
') .replace(/class=\"/g, 'class="' + MPNotif.MARKUP_PREFIX + '-') .replace(/id=\"/g, 'id="' + MPNotif.MARKUP_PREFIX + '-'); }; MPNotif.prototype._init_styles = function() { if (this.style === 'dark') { this.style_vals = { bg: '#1d1f25', bg_actions: '#282b32', bg_hover: '#3a4147', bg_light: '#4a5157', border_gray: '#32353c', cancel_opacity: '0.4', mini_hover: '#2a3137', text_title: '#fff', text_main: '#9498a3', text_tagline: '#464851', text_hover: '#ddd' }; } else { this.style_vals = { bg: '#fff', bg_actions: '#e7eaee', bg_hover: '#eceff3', bg_light: '#f5f5f5', border_gray: '#e4ecf2', cancel_opacity: '1.0', mini_hover: '#fafafa', text_title: '#5c6578', text_main: '#8b949b', text_tagline: '#ced9e6', text_hover: '#7c8598' }; } var shadow = '0px 0px 35px 0px rgba(45, 49, 56, 0.7)', video_shadow = shadow, mini_shadow = shadow, thumb_total_size = MPNotif.THUMB_IMG_SIZE + MPNotif.THUMB_BORDER_SIZE * 2, anim_seconds = (MPNotif.ANIM_TIME / 1000) + 's'; if (this.mini) { shadow = 'none'; } // don't display on small viewports var notif_media_queries = {}, min_width = MPNotif.NOTIF_WIDTH_MINI + 20; notif_media_queries['@media only screen and (max-width: ' + (min_width - 1) + 'px)'] = { '#overlay': { 'display': 'none' } }; var notif_styles = { '.flipped': { 'transform': 'rotateY(180deg)' }, '#overlay': { 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%', 'overflow': 'auto', 'text-align': 'center', 'z-index': '10000', 'font-family': '"Helvetica", "Arial", sans-serif', '-webkit-font-smoothing': 'antialiased', '-moz-osx-font-smoothing': 'grayscale' }, '#overlay.mini': { 'height': '0', 'overflow': 'visible' }, '#overlay a': { 'width': 'initial', 'padding': '0', 'text-decoration': 'none', 'text-transform': 'none', 'color': 'inherit' }, '#bgwrapper': { 'position': 'relative', 'width': '100%', 'height': '100%' }, '#bg': { 'position': 'fixed', 'top': '0', 'left': '0', 'width': '100%', 'height': '100%', 'min-width': this.doc_width * 4 + 'px', 'min-height': this.doc_height * 4 + 'px', 'background-color': 'black', 'opacity': '0.0', '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)', // IE8 'filter': 'alpha(opacity=60)', // IE5-7 'transition': 'opacity ' + anim_seconds }, '#bg.visible': { 'opacity': MPNotif.BG_OPACITY }, '.mini #bg': { 'width': '0', 'height': '0', 'min-width': '0' }, '#flipcontainer': { 'perspective': '1000px', 'position': 'absolute', 'width': '100%' }, '#flipper': { 'position': 'relative', 'transform-style': 'preserve-3d', 'transition': '0.3s' }, '#takeover': { 'position': 'absolute', 'left': '50%', 'width': MPNotif.NOTIF_WIDTH + 'px', 'margin-left': Math.round(-MPNotif.NOTIF_WIDTH / 2) + 'px', 'backface-visibility': 'hidden', 'transform': 'rotateY(0deg)', 'opacity': '0.0', 'top': MPNotif.NOTIF_START_TOP + 'px', 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds }, '#takeover.visible': { 'opacity': '1.0', 'top': MPNotif.NOTIF_TOP + 'px' }, '#takeover.exiting': { 'opacity': '0.0', 'top': MPNotif.NOTIF_START_TOP + 'px' }, '#thumbspacer': { 'height': MPNotif.THUMB_OFFSET + 'px' }, '#thumbborder-wrapper': { 'position': 'absolute', 'top': (-MPNotif.THUMB_BORDER_SIZE) + 'px', 'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET - MPNotif.THUMB_BORDER_SIZE) + 'px', 'width': thumb_total_size + 'px', 'height': (thumb_total_size / 2) + 'px', 'overflow': 'hidden' }, '#thumbborder': { 'position': 'absolute', 'width': thumb_total_size + 'px', 'height': thumb_total_size + 'px', 'border-radius': thumb_total_size + 'px', 'background-color': this.style_vals.bg_actions, 'opacity': '0.5' }, '#thumbnail': { 'position': 'absolute', 'top': '0px', 'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET) + 'px', 'width': MPNotif.THUMB_IMG_SIZE + 'px', 'height': MPNotif.THUMB_IMG_SIZE + 'px', 'overflow': 'hidden', 'z-index': '100', 'border-radius': MPNotif.THUMB_IMG_SIZE + 'px' }, '#mini': { 'position': 'absolute', 'right': '20px', 'top': MPNotif.NOTIF_TOP + 'px', 'width': this.notif_width + 'px', 'height': MPNotif.NOTIF_HEIGHT_MINI * 2 + 'px', 'margin-top': 20 - MPNotif.NOTIF_HEIGHT_MINI + 'px', 'backface-visibility': 'hidden', 'opacity': '0.0', 'transform': 'rotateX(90deg)', 'transition': 'opacity 0.3s, transform 0.3s, right 0.3s' }, '#mini.visible': { 'opacity': '1.0', 'transform': 'rotateX(0deg)' }, '#mini.exiting': { 'opacity': '0.0', 'right': '-150px' }, '#mainbox': { 'border-radius': '4px', 'box-shadow': shadow, 'text-align': 'center', 'background-color': this.style_vals.bg, 'font-size': '14px', 'color': this.style_vals.text_main }, '#mini #mainbox': { 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', 'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', 'border-radius': '3px', 'transition': 'background-color ' + anim_seconds }, '#mini-border': { 'height': (MPNotif.NOTIF_HEIGHT_MINI + 6) + 'px', 'width': (MPNotif.NOTIF_WIDTH_MINI + 6) + 'px', 'position': 'absolute', 'top': '-3px', 'left': '-3px', 'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', 'border-radius': '6px', 'opacity': '0.25', 'background-color': '#fff', 'z-index': '-1', 'box-shadow': mini_shadow }, '#mini-icon': { 'position': 'relative', 'display': 'inline-block', 'width': '75px', 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', 'border-radius': '3px 0 0 3px', 'background-color': this.style_vals.bg_actions, 'background': 'linear-gradient(135deg, ' + this.style_vals.bg_light + ' 0%, ' + this.style_vals.bg_actions + ' 100%)', 'transition': 'background-color ' + anim_seconds }, '#mini:hover #mini-icon': { 'background-color': this.style_vals.mini_hover }, '#mini:hover #mainbox': { 'background-color': this.style_vals.mini_hover }, '#mini-icon-img': { 'position': 'absolute', 'background-image': 'url(' + this.thumb_image_url + ')', 'width': '48px', 'height': '48px', 'top': '20px', 'left': '12px' }, '#content': { 'padding': '30px 20px 0px 20px' }, '#mini-content': { 'text-align': 'left', 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', 'cursor': 'pointer' }, '#img': { 'width': '328px', 'margin-top': '30px', 'border-radius': '5px' }, '#title': { 'max-height': '600px', 'overflow': 'hidden', 'word-wrap': 'break-word', 'padding': '25px 0px 20px 0px', 'font-size': '19px', 'font-weight': 'bold', 'color': this.style_vals.text_title }, '#body': { 'max-height': '600px', 'margin-bottom': '25px', 'overflow': 'hidden', 'word-wrap': 'break-word', 'line-height': '21px', 'font-size': '15px', 'font-weight': 'normal', 'text-align': 'left' }, '#mini #body': { 'display': 'inline-block', 'max-width': '250px', 'margin': '0 0 0 30px', 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', 'font-size': '16px', 'letter-spacing': '0.8px', 'color': this.style_vals.text_title }, '#mini #body-text': { 'display': 'table', 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px' }, '#mini #body-text div': { 'display': 'table-cell', 'vertical-align': 'middle' }, '#tagline': { 'margin-bottom': '15px', 'font-size': '10px', 'font-weight': '600', 'letter-spacing': '0.8px', 'color': '#ccd7e0', 'text-align': 'left' }, '#tagline a': { 'color': this.style_vals.text_tagline, 'transition': 'color ' + anim_seconds }, '#tagline a:hover': { 'color': this.style_vals.text_hover }, '#cancel': { 'position': 'absolute', 'right': '0', 'width': '8px', 'height': '8px', 'padding': '10px', 'border-radius': '20px', 'margin': '12px 12px 0 0', 'box-sizing': 'content-box', 'cursor': 'pointer', 'transition': 'background-color ' + anim_seconds }, '#mini #cancel': { 'margin': '7px 7px 0 0' }, '#cancel-icon': { 'width': '8px', 'height': '8px', 'overflow': 'hidden', 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/cancel-x.png)', 'opacity': this.style_vals.cancel_opacity }, '#cancel:hover': { 'background-color': this.style_vals.bg_hover }, '#button': { 'display': 'block', 'height': '60px', 'line-height': '60px', 'text-align': 'center', 'background-color': this.style_vals.bg_actions, 'border-radius': '0 0 4px 4px', 'overflow': 'hidden', 'cursor': 'pointer', 'transition': 'background-color ' + anim_seconds }, '#button-close': { 'display': 'inline-block', 'width': '9px', 'height': '60px', 'margin-right': '8px', 'vertical-align': 'top', 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/close-x-' + this.style + '.png)', 'background-repeat': 'no-repeat', 'background-position': '0px 25px' }, '#button-play': { 'display': 'inline-block', 'width': '30px', 'height': '60px', 'margin-left': '15px', 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/play-' + this.style + '-small.png)', 'background-repeat': 'no-repeat', 'background-position': '0px 15px' }, 'a#button-link': { 'display': 'inline-block', 'vertical-align': 'top', 'text-align': 'center', 'font-size': '17px', 'font-weight': 'bold', 'overflow': 'hidden', 'word-wrap': 'break-word', 'color': this.style_vals.text_title, 'transition': 'color ' + anim_seconds }, '#button:hover': { 'background-color': this.style_vals.bg_hover, 'color': this.style_vals.text_hover }, '#button:hover a': { 'color': this.style_vals.text_hover }, '#video-noflip': { 'position': 'relative', 'top': (-this.video_height * 2) + 'px' }, '#video-flip': { 'backface-visibility': 'hidden', 'transform': 'rotateY(180deg)' }, '#video': { 'position': 'absolute', 'width': (this.video_width - 1) + 'px', 'height': this.video_height + 'px', 'top': MPNotif.NOTIF_TOP + 'px', 'margin-top': '100px', 'left': '50%', 'margin-left': Math.round(-this.video_width / 2) + 'px', 'overflow': 'hidden', 'border-radius': '5px', 'box-shadow': video_shadow, 'transform': 'translateZ(1px)', // webkit rendering bug http://stackoverflow.com/questions/18167981/clickable-link-area-unexpectedly-smaller-after-css-transform 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds }, '#video.exiting': { 'opacity': '0.0', 'top': this.video_height + 'px' }, '#video-holder': { 'position': 'absolute', 'width': (this.video_width - 1) + 'px', 'height': this.video_height + 'px', 'overflow': 'hidden', 'border-radius': '5px' }, '#video-frame': { 'margin-left': '-1px', 'width': this.video_width + 'px' }, '#video-controls': { 'opacity': '0', 'transition': 'opacity 0.5s' }, '#video:hover #video-controls': { 'opacity': '1.0' }, '#video .video-progress-el': { 'position': 'absolute', 'bottom': '0', 'height': '25px', 'border-radius': '0 0 0 5px' }, '#video-progress': { 'width': '90%' }, '#video-progress-total': { 'width': '100%', 'background-color': this.style_vals.bg, 'opacity': '0.7' }, '#video-elapsed': { 'width': '0', 'background-color': '#6cb6f5', 'opacity': '0.9' }, '#video #video-time': { 'width': '10%', 'right': '0', 'font-size': '11px', 'line-height': '25px', 'color': this.style_vals.text_main, 'background-color': '#666', 'border-radius': '0 0 5px 0' } }; // IE hacks if (this._browser_lte('ie', 8)) { _.extend(notif_styles, { '* html #overlay': { 'position': 'absolute' }, '* html #bg': { 'position': 'absolute' }, 'html, body': { 'height': '100%' } }); } if (this._browser_lte('ie', 7)) { _.extend(notif_styles, { '#mini #body': { 'display': 'inline', 'zoom': '1', 'border': '1px solid ' + this.style_vals.bg_hover }, '#mini #body-text': { 'padding': '20px' }, '#mini #mini-icon': { 'display': 'none' } }); } // add vendor-prefixed style rules var VENDOR_STYLES = ['backface-visibility', 'border-radius', 'box-shadow', 'opacity', 'perspective', 'transform', 'transform-style', 'transition'], VENDOR_PREFIXES = ['khtml', 'moz', 'ms', 'o', 'webkit']; for (var selector in notif_styles) { for (var si = 0; si < VENDOR_STYLES.length; si++) { var prop = VENDOR_STYLES[si]; if (prop in notif_styles[selector]) { var val = notif_styles[selector][prop]; for (var pi = 0; pi < VENDOR_PREFIXES.length; pi++) { notif_styles[selector]['-' + VENDOR_PREFIXES[pi] + '-' + prop] = val; } } } } var inject_styles = function(styles, media_queries) { var create_style_text = function(style_defs) { var st = ''; for (var selector in style_defs) { var mp_selector = selector .replace(/#/g, '#' + MPNotif.MARKUP_PREFIX + '-') .replace(/\./g, '.' + MPNotif.MARKUP_PREFIX + '-'); st += '\n' + mp_selector + ' {'; var props = style_defs[selector]; for (var k in props) { st += k + ':' + props[k] + ';'; } st += '}'; } return st; }; var create_media_query_text = function(mq_defs) { var mqt = ''; for (var mq in mq_defs) { mqt += '\n' + mq + ' {' + create_style_text(mq_defs[mq]) + '\n}'; } return mqt; }; var style_text = create_style_text(styles) + create_media_query_text(media_queries), head_el = document$1.head || document$1.getElementsByTagName('head')[0] || document$1.documentElement, style_el = document$1.createElement('style'); head_el.appendChild(style_el); style_el.setAttribute('type', 'text/css'); if (style_el.styleSheet) { // IE style_el.styleSheet.cssText = style_text; } else { style_el.textContent = style_text; } }; inject_styles(notif_styles, notif_media_queries); }; MPNotif.prototype._init_video = _.safewrap(function() { if (!this.video_url) { return; } var self = this; // Youtube iframe API compatibility self.yt_custom = 'postMessage' in window$1; self.dest_url = self.video_url; var youtube_match = self.video_url.match( // http://stackoverflow.com/questions/2936467/parse-youtube-video-id-using-preg-match /(?:youtube(?:-nocookie)?\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i ), vimeo_match = self.video_url.match( /vimeo\.com\/.*?(\d+)/i ); if (youtube_match) { self.show_video = true; self.youtube_video = youtube_match[1]; if (self.yt_custom) { window$1['onYouTubeIframeAPIReady'] = function() { if (self._get_el('video-frame')) { self._yt_video_ready(); } }; // load Youtube iframe API; see https://developers.google.com/youtube/iframe_api_reference var tag = document$1.createElement('script'); tag.src = '//www.youtube.com/iframe_api'; var firstScriptTag = document$1.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); } } else if (vimeo_match) { self.show_video = true; self.vimeo_video = vimeo_match[1]; } // IE <= 7, FF <= 3: fall through to video link rather than embedded player if (self._browser_lte('ie', 7) || self._browser_lte('firefox', 3)) { self.show_video = false; self.clickthrough = true; } }); MPNotif.prototype._mark_as_shown = _.safewrap(function() { // click on background to dismiss var self = this; _.register_event(self._get_el('bg'), 'click', function() { self.dismiss(); }); var get_style = function(el, style_name) { var styles = {}; if (document$1.defaultView && document$1.defaultView.getComputedStyle) { styles = document$1.defaultView.getComputedStyle(el, null); // FF3 requires both args } else if (el.currentStyle) { // IE styles = el.currentStyle; } return styles[style_name]; }; if (this.campaign_id) { var notif_el = this._get_el('overlay'); if (notif_el && get_style(notif_el, 'visibility') !== 'hidden' && get_style(notif_el, 'display') !== 'none') { this._mark_delivery(); } } }); MPNotif.prototype._mark_delivery = _.safewrap(function(extra_props) { if (!this.marked_as_shown) { this.marked_as_shown = true; if (this.campaign_id) { // mark notification shown (local cache) this._get_shown_campaigns()[this.campaign_id] = 1 * new Date(); this.persistence.save(); } // track delivery this._track_event('$campaign_delivery', extra_props); // mark notification shown (mixpanel property) this.mixpanel['people']['append']({ '$campaigns': this.campaign_id, '$notifications': { 'campaign_id': this.campaign_id, 'message_id': this.message_id, 'type': 'web', 'time': new Date() } }); } }); MPNotif.prototype._preload_images = function(all_loaded_cb) { var self = this; if (this.imgs_to_preload.length === 0) { all_loaded_cb(); return; } var preloaded_imgs = 0; var img_objs = []; var onload = function() { preloaded_imgs++; if (preloaded_imgs === self.imgs_to_preload.length && all_loaded_cb) { all_loaded_cb(); all_loaded_cb = null; } }; for (var i = 0; i < this.imgs_to_preload.length; i++) { var img = new Image(); img.onload = onload; img.src = this.imgs_to_preload[i]; if (img.complete) { onload(); } img_objs.push(img); } // IE6/7 doesn't fire onload reliably if (this._browser_lte('ie', 7)) { setTimeout(function() { var imgs_loaded = true; for (i = 0; i < img_objs.length; i++) { if (!img_objs[i].complete) { imgs_loaded = false; } } if (imgs_loaded && all_loaded_cb) { all_loaded_cb(); all_loaded_cb = null; } }, 500); } }; MPNotif.prototype._remove_notification_el = _.safewrap(function() { window$1.clearInterval(this._video_progress_checker); this.notification_el.style.visibility = 'hidden'; this.body_el.removeChild(this.notification_el); }); MPNotif.prototype._set_client_config = function() { var get_browser_version = function(browser_ex) { var match = navigator.userAgent.match(browser_ex); return match && match[1]; }; this.browser_versions = {}; this.browser_versions['chrome'] = get_browser_version(/Chrome\/(\d+)/); this.browser_versions['firefox'] = get_browser_version(/Firefox\/(\d+)/); this.browser_versions['ie'] = get_browser_version(/MSIE (\d+).+/); if (!this.browser_versions['ie'] && !(window$1.ActiveXObject) && 'ActiveXObject' in window$1) { this.browser_versions['ie'] = 11; } this.body_el = document$1.body || document$1.getElementsByTagName('body')[0]; if (this.body_el) { this.doc_width = Math.max( this.body_el.scrollWidth, document$1.documentElement.scrollWidth, this.body_el.offsetWidth, document$1.documentElement.offsetWidth, this.body_el.clientWidth, document$1.documentElement.clientWidth ); this.doc_height = Math.max( this.body_el.scrollHeight, document$1.documentElement.scrollHeight, this.body_el.offsetHeight, document$1.documentElement.offsetHeight, this.body_el.clientHeight, document$1.documentElement.clientHeight ); } // detect CSS compatibility var ie_ver = this.browser_versions['ie']; var sample_styles = document$1.createElement('div').style, is_css_compatible = function(rule) { if (rule in sample_styles) { return true; } if (!ie_ver) { rule = rule[0].toUpperCase() + rule.slice(1); var props = ['O' + rule, 'Webkit' + rule, 'Moz' + rule]; for (var i = 0; i < props.length; i++) { if (props[i] in sample_styles) { return true; } } } return false; }; this.use_transitions = this.body_el && is_css_compatible('transition') && is_css_compatible('transform'); this.flip_animate = (this.browser_versions['chrome'] >= 33 || this.browser_versions['firefox'] >= 15) && this.body_el && is_css_compatible('backfaceVisibility') && is_css_compatible('perspective') && is_css_compatible('transform'); }; MPNotif.prototype._switch_to_video = _.safewrap(function() { var self = this, anims = [ { el: self._get_notification_display_el(), attr: 'opacity', start: 1.0, goal: 0.0 }, { el: self._get_notification_display_el(), attr: 'top', start: MPNotif.NOTIF_TOP, goal: -500 }, { el: self._get_el('video-noflip'), attr: 'opacity', start: 0.0, goal: 1.0 }, { el: self._get_el('video-noflip'), attr: 'top', start: -self.video_height * 2, goal: 0 } ]; if (self.mini) { var bg = self._get_el('bg'), overlay = self._get_el('overlay'); bg.style.width = '100%'; bg.style.height = '100%'; overlay.style.width = '100%'; self._add_class(self._get_notification_display_el(), 'exiting'); self._add_class(bg, 'visible'); anims.push({ el: self._get_el('bg'), attr: 'opacity', start: 0.0, goal: MPNotif.BG_OPACITY }); } var video_el = self._get_el('video-holder'); video_el.innerHTML = self.video_iframe; var video_ready = function() { if (window$1['YT'] && window$1['YT']['loaded']) { self._yt_video_ready(); } self.showing_video = true; self._get_notification_display_el().style.visibility = 'hidden'; }; if (self.flip_animate) { self._add_class('flipper', 'flipped'); setTimeout(video_ready, MPNotif.ANIM_TIME); } else { self._animate_els(anims, MPNotif.ANIM_TIME, video_ready); } }); MPNotif.prototype._track_event = function(event_name, properties, cb) { if (this.campaign_id) { properties = properties || {}; properties = _.extend(properties, { 'campaign_id': this.campaign_id, 'message_id': this.message_id, 'message_type': 'web_inapp', 'message_subtype': this.notif_type }); this.mixpanel['track'](event_name, properties, cb); } else if (cb) { cb.call(); } }; MPNotif.prototype._yt_video_ready = _.safewrap(function() { var self = this; if (self.video_inited) { return; } self.video_inited = true; var progress_bar = self._get_el('video-elapsed'), progress_time = self._get_el('video-time'), progress_el = self._get_el('video-progress'); new window$1['YT']['Player'](MPNotif.MARKUP_PREFIX + '-video-frame', { 'events': { 'onReady': function(event) { var ytplayer = event['target'], video_duration = ytplayer['getDuration'](), pad = function(i) { return ('00' + i).slice(-2); }, update_video_time = function(current_time) { var secs = Math.round(video_duration - current_time), mins = Math.floor(secs / 60), hours = Math.floor(mins / 60); secs -= mins * 60; mins -= hours * 60; progress_time.innerHTML = '-' + (hours ? hours + ':' : '') + pad(mins) + ':' + pad(secs); }; update_video_time(0); self._video_progress_checker = window$1.setInterval(function() { var current_time = ytplayer['getCurrentTime'](); progress_bar.style.width = (current_time / video_duration * 100) + '%'; update_video_time(current_time); }, 250); _.register_event(progress_el, 'click', function(e) { var clickx = Math.max(0, e.pageX - progress_el.getBoundingClientRect().left); ytplayer['seekTo'](video_duration * clickx / progress_el.clientWidth, true); }); } } }); }); // EXPORTS (for closure compiler) // MixpanelLib Exports MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; MixpanelLib.prototype['_check_and_handle_notifications'] = MixpanelLib.prototype._check_and_handle_notifications; MixpanelLib.prototype['_show_notification'] = MixpanelLib.prototype._show_notification; MixpanelLib.prototype['opt_out_tracking'] = MixpanelLib.prototype.opt_out_tracking; MixpanelLib.prototype['opt_in_tracking'] = MixpanelLib.prototype.opt_in_tracking; MixpanelLib.prototype['has_opted_out_tracking'] = MixpanelLib.prototype.has_opted_out_tracking; MixpanelLib.prototype['has_opted_in_tracking'] = MixpanelLib.prototype.has_opted_in_tracking; MixpanelLib.prototype['clear_opt_in_out_tracking'] = MixpanelLib.prototype.clear_opt_in_out_tracking; // MixpanelPersistence Exports MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; // MixpanelPeople Exports MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; _.safewrap_class(MixpanelLib, ['identify', '_check_and_handle_notifications', '_show_notification']); var instances = {}; var extend_mp = function() { // add all the sub mixpanel instances _.each(instances, function(instance, name) { if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } }); // add private functions as _ mixpanel_master['_'] = _; }; var override_mp_init_func = function() { // we override the snippets init function to handle the case where a // user initializes the mixpanel library after the script loads & runs mixpanel_master['init'] = function(token, config, name) { if (name) { // initialize a sub library if (!mixpanel_master[name]) { mixpanel_master[name] = instances[name] = create_mplib(token, config, name); mixpanel_master[name]._loaded(); } return mixpanel_master[name]; } else { var instance = mixpanel_master; if (instances[PRIMARY_INSTANCE_NAME]) { // main mixpanel lib already initialized instance = instances[PRIMARY_INSTANCE_NAME]; } else if (token) { // intialize the main mixpanel lib instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); instance._loaded(); instances[PRIMARY_INSTANCE_NAME] = instance; } mixpanel_master = instance; if (init_type === INIT_SNIPPET) { window$1[PRIMARY_INSTANCE_NAME] = mixpanel_master; } extend_mp(); } }; }; var add_dom_loaded_handler = function() { // Cross browser DOM Loaded support function dom_loaded_handler() { // function flag since we only want to execute this once if (dom_loaded_handler.done) { return; } dom_loaded_handler.done = true; DOM_LOADED = true; ENQUEUE_REQUESTS = false; _.each(instances, function(inst) { inst._dom_loaded(); }); } function do_scroll_check() { try { document$1.documentElement.doScroll('left'); } catch(e) { setTimeout(do_scroll_check, 1); return; } dom_loaded_handler(); } if (document$1.addEventListener) { if (document$1.readyState === 'complete') { // safari 4 can fire the DOMContentLoaded event before loading all // external JS (including this file). you will see some copypasta // on the internet that checks for 'complete' and 'loaded', but // 'loaded' is an IE thing dom_loaded_handler(); } else { document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); } } else if (document$1.attachEvent) { // IE document$1.attachEvent('onreadystatechange', dom_loaded_handler); // check to make sure we arn't in a frame var toplevel = false; try { toplevel = window$1.frameElement === null; } catch(e) { // noop } if (document$1.documentElement.doScroll && toplevel) { do_scroll_check(); } } // fallback handler, always will work _.register_event(window$1, 'load', dom_loaded_handler, true); }; function init_as_module() { init_type = INIT_MODULE; mixpanel_master = new MixpanelLib(); override_mp_init_func(); mixpanel_master['init'](); add_dom_loaded_handler(); return mixpanel_master; } var mixpanel = init_as_module(); module.exports = mixpanel; },{}],2:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var click_action_1 = __importDefault(require("./actions/click-action")); var ActionFactory = /** @class */ (function () { function ActionFactory() { } ActionFactory.createAction = function (type, config) { // Check if the action is available if (ActionFactory.classDictionary[type] == undefined) { throw new Error('[BitAnalytics] ' + type + ' is not available.'); } else { // Create a action var action = Object.create(ActionFactory.classDictionary[type].prototype); action.constructor.apply(action, [config]); action = action; return action; } }; ActionFactory.classDictionary = { 'click': click_action_1.default }; return ActionFactory; }()); exports.default = ActionFactory; },{"./actions/click-action":5}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var ActionHandlers = /** @class */ (function () { function ActionHandlers() { this.actions = []; var self = this; var callback = function () { if (self.timeout) { clearTimeout(self.timeout); } self.timeout = setTimeout(function () { console.log('[BitAnalytics] Content modified, refreshing trackers'); self.refreshTrackers(); self.timeout = undefined; }, 300); }; if (MutationObserver) { var targetNode = document.getElementsByTagName('body'); var config = { attributes: true, childList: true, subtree: true }; // Create an observer instance linked to the callback function var observer = new MutationObserver(callback); // Start observing the target node for configured mutations observer.observe(targetNode[0], config); } else { window.addEventListener("DOMSubtreeModified", callback); } } /** * * Public methods * */ ActionHandlers.prototype.refreshTrackers = function () { this.actions.map(function (action) { try { action.stopTracking(); } catch (err) { console.log(err); } try { action.startTracking(); } catch (err) { console.log(err); } }); }; ActionHandlers.prototype.trackAction = function (action) { this.actions.push(action); }; return ActionHandlers; }()); exports.default = ActionHandlers; },{}],4:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var Action = /** @class */ (function () { function Action(config) { if (!config.name) { throw new Error('[BitAnalytics] Action should have a name config : { name : ... }'); } this.name = config.name; this.isTracking = false; } return Action; }()); exports.default = Action; },{}],5:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var action_1 = __importDefault(require("../action")); var log_event_handlers_1 = __importDefault(require("../log-event-handlers")); var log_event_1 = __importDefault(require("../log-event")); var ClickAction = /** @class */ (function (_super) { __extends(ClickAction, _super); function ClickAction(config) { var _this = _super.call(this, config) || this; _this.params = []; if (!config.class || !config.channels) { throw new Error('[BitAnalytics] ClickAction should have a config like this : { class : ..., channels: ... }'); } if (config.params) { _this.params = config.params; } _this.class = config.class; _this.channels = config.channels; var self = _this; _this.listener = function (event) { /*console.log('on click'); console.log(event.target.id); console.log(event.target.outerHTML); console.log(event.target.outerText);*/ var params = {}; var target = _this.searchTarget(event.srcElement); // If I found my element, that should happen 100% if (target) { self.params.map(function (param) { var value = target[param]; if (value) { params[param] = value; } else { var item = target.attributes.getNamedItem(param); if (item) { params[param] = item.value; } } }); } var logEvent = new log_event_1.default(self.name, [params], self.channels); log_event_handlers_1.default.sharedInstance().postEvent(logEvent); }; _this.isTracking = false; return _this; } /** * * Private methods * */ ClickAction.prototype.searchTarget = function (element) { if (element && element.classList && element.classList.contains(this.class)) { return element; } else if (element.parentElement) { return this.searchTarget(element.parentElement); } else { return undefined; } }; /** * * Public methods * */ ClickAction.prototype.startTracking = function () { if (this.isTracking) { throw new Error('[BitAnalytics] The tacking is already started'); } this.isTracking = true; var elements = document.getElementsByClassName(this.class); // Add event listener to all the elements found for (var i = 0; i < elements.length; i++) { var element = elements[i]; console.log('init ' + this.name); element.addEventListener('click', this.listener); } }; ClickAction.prototype.stopTracking = function () { if (!this.isTracking) { throw new Error('[BitAnalytics] The tacking is already stopped'); } var elements = document.getElementsByClassName(this.class); // Add event listener to all the elements found for (var i = 0; i < elements.length; i++) { var element = elements[i]; element.removeEventListener('click', this.listener); } this.isTracking = false; }; return ClickAction; }(action_1.default)); exports.default = ClickAction; },{"../action":4,"../log-event":15,"../log-event-handlers":14}],6:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var log_event_handlers_1 = __importDefault(require("./log-event-handlers")); var log_event_1 = __importDefault(require("./log-event")); var action_factory_1 = __importDefault(require("./action-factory")); var adjust_channel_1 = __importDefault(require("./channels/adjust-channel")); var mixpanel_channel_1 = __importDefault(require("./channels/mixpanel-channel")); var action_handlers_1 = __importDefault(require("./action-handlers")); var channels; (function (channels) { channels.AdjustChannel = adjust_channel_1.default; channels.MixpanelChannel = mixpanel_channel_1.default; })(channels = exports.channels || (exports.channels = {})); var BitAnalytics = /** @class */ (function () { function BitAnalytics() { } BitAnalytics.initialize = function (os, appVersion, channelConfigs) { if (window == undefined) { console.error('[BitAnalytics] BitAnalytics cannot be integrated in window.'); } BitAnalytics.LogEventHandlers = new log_event_handlers_1.default(os, appVersion, channelConfigs); BitAnalytics.ActionHandlers = new action_handlers_1.default(); BitAnalytics.LogEvent = log_event_1.default; BitAnalytics.ActionFactory = action_factory_1.default; }; BitAnalytics.main = function () { if (window) { window.BitAnalytics = BitAnalytics; } }; return BitAnalytics; }()); exports.default = BitAnalytics; BitAnalytics.main(); },{"./action-factory":2,"./action-handlers":3,"./channels/adjust-channel":9,"./channels/mixpanel-channel":12,"./log-event":15,"./log-event-handlers":14}],7:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var adjust_channel_1 = __importDefault(require("./channels/adjust-channel")); var firebase_channel_1 = __importDefault(require("./channels/firebase-channel")); var ga_channel_1 = __importDefault(require("./channels/ga-channel")); var mixpanel_channel_1 = __importDefault(require("./channels/mixpanel-channel")); var ChannelFactory = /** @class */ (function () { function ChannelFactory() { } ChannelFactory.createChannel = function (name, config) { // Check if the channel is available if (ChannelFactory.classDictionary[name] == undefined) { throw new Error('[BitAnalytics] ' + name + ' is not available.'); } else { // Create a channel var channel = Object.create(ChannelFactory.classDictionary[name].prototype); channel.constructor.apply(channel, [name, config]); channel = channel; return channel; } }; ChannelFactory.classDictionary = { 'adjust': adjust_channel_1.default, 'firebase': firebase_channel_1.default, 'ga': ga_channel_1.default, 'mixpanel': mixpanel_channel_1.default }; return ChannelFactory; }()); exports.default = ChannelFactory; },{"./channels/adjust-channel":9,"./channels/firebase-channel":10,"./channels/ga-channel":11,"./channels/mixpanel-channel":12}],8:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var Channel = /** @class */ (function () { function Channel(name) { this.isReady = false; this.queue = new Array(); this.name = name; } /** * * Protected methods * */ Channel.prototype.flush = function () { this.queue.forEach(function (f) { f(); }); this.queue = new Array(); }; Channel.prototype.enqueue = function (f) { this.queue.push(f); }; return Channel; }()); exports.default = Channel; },{}],9:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var channel_1 = __importDefault(require("../channel")); // Loading Adjust websdk require('../external-libs/adjust'); var AdjustChannel = /** @class */ (function (_super) { __extends(AdjustChannel, _super); function AdjustChannel(name, config) { var _this = _super.call(this, name) || this; if (!config.token) { throw new Error('[BitAnalytics] Adjust config is missing token.'); } if (!config.eventTypes) { throw new Error('[BitAnalytics] Adjust config is missing event types.'); } if (!Adjust) { throw new Error('[BitAnalytics] Adjust cordova plugin is not installed correctly.'); } _this.eventTypes = config.eventTypes; var os = _this.adjustedOs(config.os); _this.advertisingId = _this.getAdvertisingId(os); console.log('Advertising ID for adjust: ' + _this.advertisingId); // TODO: Different initialisation for Cordova. var sessionParams = { app_version: config.appVersion, app_version_short: config.appVersion, os_name: os }; _this.addAdvertisingId(os, sessionParams); var environment = config.environment || 'production'; _this.adjustInstance = new Adjust(config.token, environment, os); _this.adjustInstance.trackSession(sessionParams); _this.isReady = true; return _this; } /** * * Public methods * */ AdjustChannel.prototype.postEvent = function (name, params) { if (this.isReady) { var eventType = this.eventTypes[name]; // Each event needs to be added on adjust, and config for adjust. if (!eventType) { throw new Error('This event name does not exist on Adjust.'); } params.os = this.adjustedOs(params.os); this.addAdvertisingId(params.os, params); this.adjustInstance.trackEvent(eventType, params); } }; /** * * Private methods * */ AdjustChannel.prototype.addAdvertisingId = function (os, params) { if (os === 'ios') { params.idfa = this.advertisingId; } else if (os === 'android') { params.gps_adid = this.advertisingId; } else { params.win_hwid = this.advertisingId; params.win_naid = this.advertisingId; params.win_adid = this.advertisingId; } }; // Desktop version will pretend to be Windows AdjustChannel.prototype.adjustedOs = function (os) { if (os === 'ios' || os === 'android') { return os; } else { return 'wstore'; } }; AdjustChannel.prototype.generateRandomGuid = function () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; // Example: 107e8ea14329d4a2194ebbb6dc0c0fd7 AdjustChannel.prototype.generateWindowsAdvertisingId = function () { var id = ''; for (var i = 0; i < 32; i++) { id += Math.floor(Math.random() * 16).toString(16); } return id; }; // https://docs.adjust.com/en/event-tracking/ AdjustChannel.prototype.getAdvertisingId = function (os) { var adid = localStorage.getItem('adid'); if (!adid) { if (os === 'ios') { adid = this.generateRandomGuid().toUpperCase(); } else if (os === 'android') { adid = this.generateRandomGuid(); } else { adid = this.generateWindowsAdvertisingId(); } localStorage.setItem('adid', adid); } return adid; }; return AdjustChannel; }(channel_1.default)); exports.default = AdjustChannel; },{"../channel":8,"../external-libs/adjust":13}],10:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var channel_1 = __importDefault(require("../channel")); var FirebaseChannel = /** @class */ (function (_super) { __extends(FirebaseChannel, _super); function FirebaseChannel(name, config) { var _this = _super.call(this, name) || this; /** * Firebase available only on ios and android */ if (config.os != 'android' && config.os != 'ios') { throw new Error('[BitAnalytics] Firebase is not supported on ' + config.os); } if (!window.FirebasePlugin) { throw new Error('[BitAnalytics] Firebase cordova plugin is not installed correctly.'); } _this.firebaseInstance = window.FirebasePlugin; _this.isReady = true; return _this; } /** * * Public methods * */ FirebaseChannel.prototype.postEvent = function (name, params) { var _this = this; var sanitizedParams = this.sanitizeParams(params); if (!this.isReady) { this.enqueue(function () { _this.postEvent(name, sanitizedParams); }); } else { this.firebaseInstance.logEvent(name, sanitizedParams); } }; // [Firebase/Analytics][I-ACS013002] Event parameter name must contain only letters, numbers, or underscores FirebaseChannel.prototype.sanitizeParams = function (params) { var keys = Object.keys(params); var keysLength = keys.length; var sanitized = {}; for (var i = 0; i < keysLength; i++) { var key = keys[i]; var cleanKey = key.replace('-', '_').replace(/[\W]+/g, ''); sanitized[cleanKey] = params[key]; } return sanitized; }; return FirebaseChannel; }(channel_1.default)); exports.default = FirebaseChannel; },{"../channel":8}],11:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var channel_1 = __importDefault(require("../channel")); var GoogleAnalyticsChannel = /** @class */ (function (_super) { __extends(GoogleAnalyticsChannel, _super); function GoogleAnalyticsChannel(name, config) { var _this = _super.call(this, name) || this; _this.dataLayer = null; _this.gaInstance = null; _this.trackingId = ''; _this.eventLabels = ['id']; if (!config.trackingId) { throw new Error('[BitAnalytics] Google Analytics config is missing tracking ID.'); } if (config.eventLabels) { _this.eventLabels = config.eventLabels; } _this.trackingId = config.trackingId; _this.setUpGa(); return _this; } /** * * Public methods * */ GoogleAnalyticsChannel.prototype.postEvent = function (name, params) { // Default Google Analytics Events // https://developers.google.com/analytics/devguides/collection/gtagjs/events // Useful to convert to these, or start with these? if (this.isReady) { params.event_category = name; for (var _i = 0, _a = this.eventLabels; _i < _a.length; _i++) { var eventLabel = _a[_i]; if (params[eventLabel]) { params.event_label = params[eventLabel]; break; } } this.gtag('event', name, params); } }; /** * * Private methods * */ /** * Mimics function in the tracking snippet */ GoogleAnalyticsChannel.prototype.gtag = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } console.log(arguments); window.dataLayer.push(arguments); }; GoogleAnalyticsChannel.prototype.setUpGa = function () { // From what GA recommends to insert into page window.dataLayer = window.dataLayer || []; this.gtag('js', new Date()); this.gtag('config', this.trackingId); this.isReady = true; }; return GoogleAnalyticsChannel; }(channel_1.default)); exports.default = GoogleAnalyticsChannel; },{"../channel":8}],12:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var channel_1 = __importDefault(require("../channel")); var mixpanel = require('mixpanel-browser'); var MixpanelChannel = /** @class */ (function (_super) { __extends(MixpanelChannel, _super); function MixpanelChannel(name, config) { var _this = _super.call(this, name) || this; if (!config.token) { throw new DOMException('[BitAnalytics] Config incorrect.'); } _this.mixpanelInstance = mixpanel; mixpanel.init(config.token, config.config); return _this; } /** * * Public methods * */ MixpanelChannel.prototype.postEvent = function (name, params) { var result = this.mixpanelInstance.track(name); }; return MixpanelChannel; }(channel_1.default)); exports.default = MixpanelChannel; },{"../channel":8,"mixpanel-browser":1}],13:[function(require,module,exports){ "use strict"; (function (window) { var sendRequest = function (method, url, data, success_cb, error_cb) { var req = new XMLHttpRequest(); req.open(method, url, !0); req.setRequestHeader("Client-SDK", "js4.0.0"); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status >= 200 && req.status < 400) { !!success_cb && success_cb(req.responseText); } else if (!!error_cb) { !!error_cb && error_cb(new Error("Server responded with HTTP " + req.status), xhr); } } }; if (!!error_cb) { req.onerror = error_cb; } req.send(data); }; var encodeQueryString = function (params) { var pairs = []; for (var k in params) { if (!params.hasOwnProperty(k)) { continue; } pairs.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k])); } return pairs.join("&"); }; var cloneObj = function (obj) { var copy = {}; if (typeof (obj) != "object" || !obj) { return copy; } for (var k in obj) { if (!obj.hasOwnProperty(k)) { continue; } copy[k] = obj[k]; } return copy; }; if (!'withCredentials' in new XMLHttpRequest()) { sendRequest = function () { }; } window.Adjust = function (app_token, environment, os_name) { this.trackSession = function (device_ids) { var params = cloneObj(device_ids); params.app_token = app_token; params.os_name = os_name; params.environment = environment; sendRequest("GET", "https://app.adjust.com/session?" + encodeQueryString(params)); }; this.trackEvent = function (event_token, device_ids) { var params = cloneObj(device_ids); params.app_token = app_token; params.event_token = event_token; params.os_name = os_name; params.environment = environment; sendRequest("GET", "https://app.adjust.com/event?" + encodeQueryString(params)); }; }; })(window); },{}],14:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var channel_factory_1 = __importDefault(require("./channel-factory")); var LogEventHandlers = /** @class */ (function () { function LogEventHandlers(os, appVersion, channelConfigs) { this.os = os; this.appVersion = appVersion; this.channels = []; this.isReady = false; // Ready once all channels are ready or one channel is ready? this.initializeChannels(channelConfigs); LogEventHandlers.instance = this; } /** * * Public methods * */ LogEventHandlers.sharedInstance = function () { if (LogEventHandlers.instance) { return LogEventHandlers.instance; } else { throw new Error('[BitAnalytics] LogEventHandlers need to be initialized'); } }; LogEventHandlers.prototype.postEvent = function (logEvent) { var _this = this; /** * 0 is shared params * 1 is first channel * 2 is second channel * ... */ var logEventParams = logEvent.params; // params var params = { 'os': this.os, 'appVersion': this.appVersion }; // Concat the shared params if (logEventParams.length > 0) { // concat specific params needed params = this.concatObject(logEventParams[0], params); } // Post event depending of the channel logEvent.channelNames.map(function (channelName, i) { var channel = _this.getChannelByName(channelName); if (channel) { // Real index (first param is shared by all channels) var index = i + 1; // concat if needed if (logEventParams.length > index) { // concat specific params needed params = _this.concatObject(logEventParams[index], params); } console.log('[BitAnalytics] Params: ' + JSON.stringify(params)); try { channel.postEvent(logEvent.name, params); console.log('[BitAnalytics] LogEvent "' + logEvent.name + '" sent to ' + channelName + '.'); } catch (e) { console.error('[BitAnalytics] LogEvent "' + logEvent.name + '" failed to send with "' + channelName + '. '); console.log(e); } } else { // Channel not available console.log('[BitAnalytics] LogEvent "' + logEvent.name + '" cannot send to ' + channelName + ', this channel is not available.'); } }); }; /** * * Private methods * */ LogEventHandlers.prototype.concatObject = function (from, to) { var keys = Object.keys(from); keys.map(function (key) { if (!to[key]) { to[key] = from[key]; } }); return to; }; LogEventHandlers.prototype.getChannelByName = function (channelName) { var channels = this.channels.filter(function (channel) { return channel.name == channelName; }); if (channels.length > 0) { return channels[0]; } else { return undefined; } }; LogEventHandlers.prototype.initializeChannels = function (channelConfigs) { var _this = this; // Get the channel names by the keys var channelNames = Object.keys(channelConfigs); // Iterate to init the several channels given in the config channelNames.map(function (channelName) { var channelConfig = channelConfigs[channelName]; // OS shared to check the availability of this channel on this OS. channelConfig.os = _this.os; channelConfig.appVersion = _this.appVersion; try { var channel = channel_factory_1.default.createChannel(channelName, channelConfig); _this.channels.push(channel); } catch (error) { console.log('[BitAnalytics] ' + error.name + ': ' + error.message); } }); }; return LogEventHandlers; }()); exports.default = LogEventHandlers; },{"./channel-factory":7}],15:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var LogEvent = /** @class */ (function () { function LogEvent(name, params, channelNames) { if (channelNames.length == 0) { throw new Error('[BitAnalytics] Minimum one channel is needed.'); } this.name = name; this.params = params; this.channelNames = channelNames; } return LogEvent; }()); exports.default = LogEvent; },{}]},{},[6])(6) });