From a2bd6fc31906ec56f14c0b7a263dc2783e614a2c Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Tue, 10 Jul 2018 16:39:30 +0900 Subject: [PATCH 1/4] Init BitAnalytics --- Gruntfile.js | 9 +- app-template/index.html | 3 +- bitanalytics/bitanalytics-0.1.0.js | 6493 ++++++++++++++++++++++++++++ src/js/routes.js | 6 +- www/index.html | 3 +- 5 files changed, 6510 insertions(+), 4 deletions(-) create mode 100644 bitanalytics/bitanalytics-0.1.0.js diff --git a/Gruntfile.js b/Gruntfile.js index 7ae74ed17..342cc85e7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -161,6 +161,12 @@ module.exports = function(grunt) { ], dest: 'www/lib/bitcoin-cash-js.js' }, + bitanalytics: { + src: [ + 'bitanalytics/bitanalytics-0.1.0.js' + ], + dest: 'www/lib/bitanalytics.js' + }, js: { src: [ 'src/js/app.js', @@ -202,7 +208,8 @@ module.exports = function(grunt) { files: { 'www/js/app.js': ['www/js/app.js'], 'www/lib/angular-components.js': ['www/lib/angular-components.js'], - 'www/lib/bitcoin-cash-js.js': ['www/lib/bitcoin-cash-js.js'] + 'www/lib/bitcoin-cash-js.js': ['www/lib/bitcoin-cash-js.js'], + 'www/lib/bitanalytics.js': ['www/lib/bitanalytics.js'] } } }, diff --git a/app-template/index.html b/app-template/index.html index 2946d7928..fe5262f52 100644 --- a/app-template/index.html +++ b/app-template/index.html @@ -11,7 +11,7 @@ - *USERVISIBLENAME* - *PURPOSELINE* + *USERVISIBLENAME* @@ -31,6 +31,7 @@ + diff --git a/bitanalytics/bitanalytics-0.1.0.js b/bitanalytics/bitanalytics-0.1.0.js new file mode 100644 index 000000000..c1907d7a5 --- /dev/null +++ b/bitanalytics/bitanalytics-0.1.0.js @@ -0,0 +1,6493 @@ +(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 + '
' + + '
' + + 'POWERED BY MIXPANEL' + + '
' + + '
' + + '
' + + 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 log_event_handlers_1 = __importDefault(require("./log-event-handlers")); +var log_event_1 = __importDefault(require("./log-event")); +var adjust_channel_1 = __importDefault(require("./channels/adjust-channel")); +var mixpanel_channel_1 = __importDefault(require("./channels/mixpanel-channel")); +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 (typeof window === 'undefined') { + console.error('BitAnalytics can be used only in a web browser.'); + return; + } + BitAnalytics.LogEventHandlers = new log_event_handlers_1.default(os, appVersion, channelConfigs); + BitAnalytics.LogEvent = log_event_1.default; + }; + BitAnalytics.main = function () { + if (window) { + window.BitAnalytics = BitAnalytics; + } + }; + return BitAnalytics; +}()); +exports.default = BitAnalytics; +BitAnalytics.main(); + +},{"./channels/adjust-channel":5,"./channels/mixpanel-channel":7,"./log-event":10,"./log-event-handlers":9}],3:[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 mixpanel_channel_1 = __importDefault(require("./channels/mixpanel-channel")); +var firebase_channel_1 = __importDefault(require("./channels/firebase-channel")); +var adjust_channel_1 = __importDefault(require("./channels/adjust-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 DOMException(name + ' is not available.'); + } + else { + // Create a channel + var channel = Object.create(ChannelFactory.classDictionary[name].prototype); + channel.constructor.apply(channel, [name, config]); + channel = channel; + console.log(channel); + return channel; + } + }; + ChannelFactory.classDictionary = { + 'mixpanel': mixpanel_channel_1.default, + 'firebase': firebase_channel_1.default, + 'adjust': adjust_channel_1.default + }; + return ChannelFactory; +}()); +exports.default = ChannelFactory; + +},{"./channels/adjust-channel":5,"./channels/firebase-channel":6,"./channels/mixpanel-channel":7}],4:[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; + } + /** + * + * Public methods + * + */ + Channel.prototype.getName = function () { + return this.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; + +},{}],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 }); +require('../lib/adjust'); +var channel_1 = __importDefault(require("../channel")); +var AdjustChannel = /** @class */ (function (_super) { + __extends(AdjustChannel, _super); + function AdjustChannel(name, config) { + var _this = _super.call(this, name) || this; + _this.device_ids = { + "gps_adid": "3ea5fac8-cf01-47d5-8aec-9a1354f5e84a" + }; + _this.eventTypes = { + "wallet_created": "nd3dg5" + }; + if (!config.token) { + throw new DOMException('Config incorrect.'); + } + if (Adjust) { + _this.adjust = new Adjust("au1onbhgg5q8", "sandbox", "android"); + //this.adjust.trackSession(this.device_ids); + _this.isReady = true; + console.log('Adjust initialised.'); + } + else { + console.log('Adjust missing.'); + } + return _this; + } + AdjustChannel.prototype.postEvent = function (name, params) { + //var result = this.mixpanelInstance.track(logEvent.name); + if (this.isReady) { + this.adjust.trackEvent('nd3dg5', this.device_ids); + } + }; + return AdjustChannel; +}(channel_1.default)); +exports.default = AdjustChannel; + +},{"../channel":4,"../lib/adjust":8}],6:[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 DOMException('Firebase is not supported on ' + config['os']); + } + if (!window.FirebasePlugin) { + throw new DOMException('Firebase cordova plugin is not installed correctly.'); + } + _this.firebaseInstance = window.FirebasePlugin; + _this.isReady = true; + return _this; + } + FirebaseChannel.prototype.postEvent = function (name, params) { + var _this = this; + if (!this.isReady) { + this.enqueue(function () { _this.postEvent(name, params); }); + } + else { + this.firebaseInstance.logEvent(name, params); + } + }; + return FirebaseChannel; +}(channel_1.default)); +exports.default = FirebaseChannel; + +},{"../channel":4}],7:[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('Config incorrect.'); + } + _this.mixpanelInstance = mixpanel; + mixpanel.init(config.token, config.config); + return _this; + } + MixpanelChannel.prototype.postEvent = function (name, params) { + var result = this.mixpanelInstance.track(name); + }; + return MixpanelChannel; +}(channel_1.default)); +exports.default = MixpanelChannel; + +},{"../channel":4,"mixpanel-browser":1}],8:[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); + +},{}],9:[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.initialize(channelConfigs); + LogEventHandlers.instance = this; + } + /** + * + * Public methods + * + */ + LogEventHandlers.sharedInstance = function () { + if (LogEventHandlers.instance) { + return LogEventHandlers.instance; + } + else { + throw new DOMException('LogEventHandlers need to be initialized'); + } + }; + LogEventHandlers.prototype.initialize = 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; + try { + var channel = channel_factory_1.default.createChannel(channelName, channelConfig); + _this.channels.push(channel); + } + catch (error) { + console.log(error.name + ': ' + error.message); + } + }); + }; + LogEventHandlers.prototype.postEvent = function (logEvent) { + var _this = this; + /** + * 0 is shared params + * 1 is first channel + * 2 is second channel + * ... + */ + var logEventParams = logEvent.getParams(); + // params + var params = { + 'os': this.os, + 'appVersion': this.appVersion + }; + // Concat the shared params + if (logEventParams.length > 0) { + // concat specific params needed + params = Object.assign({}, logEventParams[0], params); + } + // Post event depending of the channel + logEvent.getChannelNames().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 = Object.assign({}, logEventParams[index], params); + } + channel.postEvent(logEvent.getName(), params); + console.log('LogEvent "' + logEvent.getName() + '" sent to ' + channelName + '.'); + } + else { + // Channel not available + console.log('LogEvent "' + logEvent.getName() + '" cannot send to ' + channelName + ', this channel is not available.'); + } + }); + }; + /** + * + * Private methods + * + */ + LogEventHandlers.prototype.getChannelByName = function (channelName) { + console.log(channelName); + console.log(this.channels); + var channels = this.channels.filter(function (channel) { return channel.getName() == channelName; }); + if (channels.length > 0) { + return channels[0]; + } + else { + return undefined; + } + }; + return LogEventHandlers; +}()); +exports.default = LogEventHandlers; + +},{"./channel-factory":3}],10:[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 DOMException('Minimum one channel is needed.'); + } + this.name = name; + this.params = params; + this.channelNames = channelNames; + } + /** + * + * Public methods + * + */ + LogEvent.prototype.getName = function () { + return this.name; + }; + LogEvent.prototype.getParams = function () { + return this.params; + }; + LogEvent.prototype.getChannelNames = function () { + return this.channelNames; + }; + return LogEvent; +}()); +exports.default = LogEvent; + +},{}]},{},[2])(2) +}); diff --git a/src/js/routes.js b/src/js/routes.js index 4e72246ba..dabebdf65 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1211,7 +1211,11 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr .run(function($rootScope, $state, $location, $log, $timeout, startupService, ionicToast, fingerprintService, $ionicHistory, $ionicPlatform, $window, appConfigService, lodash, platformInfo, profileService, uxLanguage, gettextCatalog, openURLService, storageService, scannerService, configService, emailService, /* plugins START HERE => */ buydotbitcoindotcomService, glideraService, amazonService, bitpayCardService, applicationService, mercadoLibreService, rateService) { $ionicPlatform.ready(function() { - + + // Init BitAnalytics + var os = platformInfo.isAndroid ? 'android' : platformInfo.isIOS ? 'ios' : 'desktop'; + window.BitAnalytics.initialize(os, $window.fullVersion, {"firebase": {}}); + // Init language uxLanguage.init(function (lang) { diff --git a/www/index.html b/www/index.html index 47dc27d2a..a4ab55678 100644 --- a/www/index.html +++ b/www/index.html @@ -11,7 +11,7 @@ - Bitcoin.com Wallet + Bitcoin.com Wallet - Bitcoin.com Wallet @@ -31,6 +31,7 @@ + From 4e1ddfe88388d334ebc8582b0417a4cd0e47d59f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Fri, 13 Jul 2018 16:15:04 +0900 Subject: [PATCH 2/4] Integration BitAnalytics --- bitanalytics/bitanalytics-0.1.0.js | 563 +++++++++++++++++++---- src/js/routes.js | 63 ++- www/index.html | 5 +- www/views/includes/community.html | 6 +- www/views/includes/homeIntegrations.html | 9 +- www/views/includes/nextSteps.html | 4 +- www/views/includes/services.html | 2 +- www/views/tab-home.html | 2 +- www/views/tab-send.html | 2 +- www/views/tabs.html | 11 +- 10 files changed, 555 insertions(+), 112 deletions(-) diff --git a/bitanalytics/bitanalytics-0.1.0.js b/bitanalytics/bitanalytics-0.1.0.js index c1907d7a5..cf85b3c14 100644 --- a/bitanalytics/bitanalytics-0.1.0.js +++ b/bitanalytics/bitanalytics-0.1.0.js @@ -6069,10 +6069,225 @@ 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; @@ -6082,12 +6297,13 @@ var BitAnalytics = /** @class */ (function () { function BitAnalytics() { } BitAnalytics.initialize = function (os, appVersion, channelConfigs) { - if (typeof window === 'undefined') { - console.error('BitAnalytics can be used only in a web browser.'); - return; + 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) { @@ -6099,42 +6315,43 @@ var BitAnalytics = /** @class */ (function () { exports.default = BitAnalytics; BitAnalytics.main(); -},{"./channels/adjust-channel":5,"./channels/mixpanel-channel":7,"./log-event":10,"./log-event-handlers":9}],3:[function(require,module,exports){ +},{"./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 mixpanel_channel_1 = __importDefault(require("./channels/mixpanel-channel")); -var firebase_channel_1 = __importDefault(require("./channels/firebase-channel")); 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 DOMException(name + ' is not available.'); + 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; - console.log(channel); return channel; } }; ChannelFactory.classDictionary = { - 'mixpanel': mixpanel_channel_1.default, + 'adjust': adjust_channel_1.default, 'firebase': firebase_channel_1.default, - 'adjust': adjust_channel_1.default + 'ga': ga_channel_1.default, + 'mixpanel': mixpanel_channel_1.default }; return ChannelFactory; }()); exports.default = ChannelFactory; -},{"./channels/adjust-channel":5,"./channels/firebase-channel":6,"./channels/mixpanel-channel":7}],4:[function(require,module,exports){ +},{"./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 () { @@ -6143,14 +6360,6 @@ var Channel = /** @class */ (function () { this.queue = new Array(); this.name = name; } - /** - * - * Public methods - * - */ - Channel.prototype.getName = function () { - return this.name; - }; /** * * Protected methods @@ -6169,7 +6378,7 @@ var Channel = /** @class */ (function () { }()); exports.default = Channel; -},{}],5:[function(require,module,exports){ +},{}],9:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || @@ -6185,43 +6394,119 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -require('../lib/adjust'); 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; - _this.device_ids = { - "gps_adid": "3ea5fac8-cf01-47d5-8aec-9a1354f5e84a" - }; - _this.eventTypes = { - "wallet_created": "nd3dg5" - }; if (!config.token) { - throw new DOMException('Config incorrect.'); + throw new Error('[BitAnalytics] Adjust config is missing token.'); } - if (Adjust) { - _this.adjust = new Adjust("au1onbhgg5q8", "sandbox", "android"); - //this.adjust.trackSession(this.device_ids); - _this.isReady = true; - console.log('Adjust initialised.'); + if (!config.eventTypes) { + throw new Error('[BitAnalytics] Adjust config is missing event types.'); } - else { - console.log('Adjust missing.'); + 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) { - //var result = this.mixpanelInstance.track(logEvent.name); if (this.isReady) { - this.adjust.trackEvent('nd3dg5', this.device_ids); + 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":4,"../lib/adjust":8}],6:[function(require,module,exports){ +},{"../channel":8,"../external-libs/adjust":13}],10:[function(require,module,exports){ "use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || @@ -6245,16 +6530,21 @@ var FirebaseChannel = /** @class */ (function (_super) { /** * Firebase available only on ios and android */ - if (config['os'] != 'android' && config['os'] != 'ios') { - throw new DOMException('Firebase is not supported on ' + config['os']); + if (config.os != 'android' && config.os != 'ios') { + throw new Error('[BitAnalytics] Firebase is not supported on ' + config.os); } if (!window.FirebasePlugin) { - throw new DOMException('Firebase cordova plugin is not installed correctly.'); + 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; if (!this.isReady) { @@ -6268,7 +6558,90 @@ var FirebaseChannel = /** @class */ (function (_super) { }(channel_1.default)); exports.default = FirebaseChannel; -},{"../channel":4}],7:[function(require,module,exports){ +},{"../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 || @@ -6291,12 +6664,17 @@ var MixpanelChannel = /** @class */ (function (_super) { function MixpanelChannel(name, config) { var _this = _super.call(this, name) || this; if (!config.token) { - throw new DOMException('Config incorrect.'); + 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); }; @@ -6304,7 +6682,7 @@ var MixpanelChannel = /** @class */ (function (_super) { }(channel_1.default)); exports.default = MixpanelChannel; -},{"../channel":4,"mixpanel-browser":1}],8:[function(require,module,exports){ +},{"../channel":8,"mixpanel-browser":1}],13:[function(require,module,exports){ "use strict"; (function (window) { var sendRequest = function (method, url, data, success_cb, error_cb) { @@ -6353,7 +6731,7 @@ exports.default = MixpanelChannel; 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); -},{}],9:[function(require,module,exports){ +},{}],14:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; @@ -6366,7 +6744,7 @@ var LogEventHandlers = /** @class */ (function () { this.appVersion = appVersion; this.channels = []; this.isReady = false; // Ready once all channels are ready or one channel is ready? - this.initialize(channelConfigs); + this.initializeChannels(channelConfigs); LogEventHandlers.instance = this; } /** @@ -6379,27 +6757,9 @@ var LogEventHandlers = /** @class */ (function () { return LogEventHandlers.instance; } else { - throw new DOMException('LogEventHandlers need to be initialized'); + throw new Error('[BitAnalytics] LogEventHandlers need to be initialized'); } }; - LogEventHandlers.prototype.initialize = 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; - try { - var channel = channel_factory_1.default.createChannel(channelName, channelConfig); - _this.channels.push(channel); - } - catch (error) { - console.log(error.name + ': ' + error.message); - } - }); - }; LogEventHandlers.prototype.postEvent = function (logEvent) { var _this = this; /** @@ -6408,7 +6768,7 @@ var LogEventHandlers = /** @class */ (function () { * 2 is second channel * ... */ - var logEventParams = logEvent.getParams(); + var logEventParams = logEvent.params; // params var params = { 'os': this.os, @@ -6417,10 +6777,10 @@ var LogEventHandlers = /** @class */ (function () { // Concat the shared params if (logEventParams.length > 0) { // concat specific params needed - params = Object.assign({}, logEventParams[0], params); + params = this.concatObject(logEventParams[0], params); } // Post event depending of the channel - logEvent.getChannelNames().map(function (channelName, i) { + logEvent.channelNames.map(function (channelName, i) { var channel = _this.getChannelByName(channelName); if (channel) { // Real index (first param is shared by all channels) @@ -6428,14 +6788,21 @@ var LogEventHandlers = /** @class */ (function () { // concat if needed if (logEventParams.length > index) { // concat specific params needed - params = Object.assign({}, logEventParams[index], params); + 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); } - channel.postEvent(logEvent.getName(), params); - console.log('LogEvent "' + logEvent.getName() + '" sent to ' + channelName + '.'); } else { // Channel not available - console.log('LogEvent "' + logEvent.getName() + '" cannot send to ' + channelName + ', this channel is not available.'); + console.log('[BitAnalytics] LogEvent "' + logEvent.name + '" cannot send to ' + channelName + ', this channel is not available.'); } }); }; @@ -6444,10 +6811,17 @@ var LogEventHandlers = /** @class */ (function () { * 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) { - console.log(channelName); - console.log(this.channels); - var channels = this.channels.filter(function (channel) { return channel.getName() == channelName; }); + var channels = this.channels.filter(function (channel) { return channel.name == channelName; }); if (channels.length > 0) { return channels[0]; } @@ -6455,39 +6829,44 @@ var LogEventHandlers = /** @class */ (function () { 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":3}],10:[function(require,module,exports){ +},{"./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 DOMException('Minimum one channel is needed.'); + throw new Error('[BitAnalytics] Minimum one channel is needed.'); } this.name = name; this.params = params; this.channelNames = channelNames; } - /** - * - * Public methods - * - */ - LogEvent.prototype.getName = function () { - return this.name; - }; - LogEvent.prototype.getParams = function () { - return this.params; - }; - LogEvent.prototype.getChannelNames = function () { - return this.channelNames; - }; return LogEvent; }()); exports.default = LogEvent; -},{}]},{},[2])(2) +},{}]},{},[6])(6) }); diff --git a/src/js/routes.js b/src/js/routes.js index dabebdf65..ee7abba58 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1214,8 +1214,65 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr // Init BitAnalytics var os = platformInfo.isAndroid ? 'android' : platformInfo.isIOS ? 'ios' : 'desktop'; - window.BitAnalytics.initialize(os, $window.fullVersion, {"firebase": {}}); - + window.BitAnalytics.initialize(os, $window.fullVersion, {"firebase": {}, + "ga": { + "trackingId": "UA-59964190-23", + "eventLabels": ["id", "icon-off"] + }, + "adjust": { + "token": "au1onbhgg5q8", + "environment" : "production", + "eventTypes": { + "banner_click": "sc5i8u", + "buy_bitcoin_click": "t1vcdz", + "transfer_success": "f68evo", + "wallet_created": "nd3dg5", + "wallet_opened": "4n39l7" + } + } + }); + + var channel = "ga"; + if (platformInfo.isNW) { + channel = "ga"; + } + + // Send a log to test + var log = new window.BitAnalytics.LogEvent("wallet_opened", [], [channel, "adjust"]); + window.BitAnalytics.LogEventHandlers.postEvent(log); + + var actionBanner = new window.BitAnalytics.ActionFactory.createAction('click', { + name: 'banner_click', + class: 'track_banner_click', + params: ['href-banner', 'id'], + channels: [channel, 'adjust'] + }); + window.BitAnalytics.ActionHandlers.trackAction(actionBanner); + + var actionBuyBitcoin = new window.BitAnalytics.ActionFactory.createAction('click', { + name: 'buy_bitcoin_click', + class: 'track_buy_bitcoin_click', + params: ['href', 'id'], + channels: [channel, 'adjust'] + }); + window.BitAnalytics.ActionHandlers.trackAction(actionBuyBitcoin); + + var actionLinkClickOut = new window.BitAnalytics.ActionFactory.createAction('click', { + name: 'link_click_out', + class: 'track_link_click_out', + params: ['href', 'id'], + channels: [channel] + }); + window.BitAnalytics.ActionHandlers.trackAction(actionLinkClickOut); + + var actionTabOpen = new window.BitAnalytics.ActionFactory.createAction('click', { + name: 'tab_open', + class: 'track_tab_open', + params: ['href', 'title', 'icon-off'], + channels: [channel] + }); + window.BitAnalytics.ActionHandlers.trackAction(actionTabOpen); + // Init language uxLanguage.init(function (lang) { @@ -1385,7 +1442,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } win.menu = nativeMenuBar; } - + $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) { if (document.body.classList.contains('keyboard-open')) { document.body.classList.remove('keyboard-open'); diff --git a/www/index.html b/www/index.html index a4ab55678..4c73317e3 100644 --- a/www/index.html +++ b/www/index.html @@ -12,7 +12,8 @@ Bitcoin.com Wallet - Bitcoin.com Wallet - + + @@ -30,7 +31,7 @@ - + diff --git a/www/views/includes/community.html b/www/views/includes/community.html index a8c021745..86841a77c 100644 --- a/www/views/includes/community.html +++ b/www/views/includes/community.html @@ -6,14 +6,14 @@
- +
{{service.title || service.name}}
- +
@@ -22,7 +22,7 @@
- +
diff --git a/www/views/includes/homeIntegrations.html b/www/views/includes/homeIntegrations.html index 7278eff3f..1c63ab090 100644 --- a/www/views/includes/homeIntegrations.html +++ b/www/views/includes/homeIntegrations.html @@ -6,7 +6,14 @@
- +
diff --git a/www/views/tab-home.html b/www/views/tab-home.html index 4d494e9ec..97dd2bd16 100644 --- a/www/views/tab-home.html +++ b/www/views/tab-home.html @@ -92,7 +92,7 @@
- + diff --git a/www/views/tab-send.html b/www/views/tab-send.html index 838c04299..ab40e7e42 100644 --- a/www/views/tab-send.html +++ b/www/views/tab-send.html @@ -32,7 +32,7 @@ To get started, buy bitcoin or share your address. You can receive bitcoin from any wallet or service. To get started, you'll need to create a bitcoin wallet and get some bitcoin.
- +
diff --git a/www/views/tabs.html b/www/views/tabs.html index 69cf93309..3c2683a13 100644 --- a/www/views/tabs.html +++ b/www/views/tabs.html @@ -1,22 +1,21 @@ - - + - + - + - + - + From 8abaf184aaa3ca3599420f63e1f9b74bdb624933 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Fri, 13 Jul 2018 17:19:31 +0900 Subject: [PATCH 3/4] 458 - Add Statistic --- src/js/controllers/addressbookAdd.js | 10 ++++++++++ src/js/controllers/confirm.js | 13 +++++++++++++ src/js/controllers/preferencesNotifications.js | 9 +++++++++ src/js/controllers/tab-receive.js | 10 ++++++++++ src/js/controllers/walletDetails.js | 7 +++++++ src/js/routes.js | 2 +- src/js/services/profileService.js | 9 +++++++++ 7 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/js/controllers/addressbookAdd.js b/src/js/controllers/addressbookAdd.js index a57839aa1..dcdb43b9f 100644 --- a/src/js/controllers/addressbookAdd.js +++ b/src/js/controllers/addressbookAdd.js @@ -35,6 +35,16 @@ angular.module('copayApp.controllers').controller('addressbookAddController', fu var translated = bitcoinCashJsService.readAddress(addressbook.address); addressbook.address = translated.legacy; } + + var channel = "firebase"; + if (platformInfo.isNW) { + channel = "ga"; + } + var log = new window.BitAnalytics.LogEvent("contact_created", [{ + "coin": $scope.addressbookEntry.coin + }], [channel]); + window.BitAnalytics.LogEventHandlers.postEvent(log); + $timeout(function() { addressbookService.add(addressbook, function(err, ab) { if (err) { diff --git a/src/js/controllers/confirm.js b/src/js/controllers/confirm.js index f1fab5b02..03af26fd1 100644 --- a/src/js/controllers/confirm.js +++ b/src/js/controllers/confirm.js @@ -643,6 +643,19 @@ angular.module('copayApp.controllers').controller('confirmController', function( soundService.play('misc/payment_sent.mp3'); } + var channel = "firebase"; + if (platformInfo.isNW) { + channel = "ga"; + } + var log = new window.BitAnalytics.LogEvent("transfer_success", [{ + "coin": $scope.wallet.coin, + "type": "outgoing", + "amount": $scope.amount, + "fees": $scope.fee + }], [channel, "adjust"]); + window.BitAnalytics.LogEventHandlers.postEvent(log); + + // Should be removed firebaseEventsService.logEvent('sent_bitcoin', { coin: $scope.wallet.coin }); $timeout(function() { $scope.$digest(); diff --git a/src/js/controllers/preferencesNotifications.js b/src/js/controllers/preferencesNotifications.js index 7351a9f23..edfb983b5 100644 --- a/src/js/controllers/preferencesNotifications.js +++ b/src/js/controllers/preferencesNotifications.js @@ -75,6 +75,15 @@ angular.module('copayApp.controllers').controller('preferencesNotificationsContr }; emailService.updateEmail(opts); + + var channel = "firebase"; + if (platformInfo.isNW) { + channel = "ga"; + } + var log = new window.BitAnalytics.LogEvent("settings_email_notification_toggle", [{ + "toggle": $scope.emailNotifications.value + }], [channel]); + window.BitAnalytics.LogEventHandlers.postEvent(log); }; $scope.soundNotificationsChange = function() { diff --git a/src/js/controllers/tab-receive.js b/src/js/controllers/tab-receive.js index 629e59b51..6c11c2dbd 100644 --- a/src/js/controllers/tab-receive.js +++ b/src/js/controllers/tab-receive.js @@ -143,6 +143,16 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi } $scope.paymentReceivedCoin = $scope.wallet.coin; + var channel = "firebase"; + if (platformInfo.isNW) { + channel = "ga"; + } + var log = new window.BitAnalytics.LogEvent("transfer_success", [{ + "coin": $scope.wallet.coin, + "type": "incoming" + }], [channel, "adjust"]); + window.BitAnalytics.LogEventHandlers.postEvent(log); + if ($state.current.name === "tabs.receive") { soundService.play('misc/payment_received.mp3'); } diff --git a/src/js/controllers/walletDetails.js b/src/js/controllers/walletDetails.js index 88ee871ff..24237f6c9 100644 --- a/src/js/controllers/walletDetails.js +++ b/src/js/controllers/walletDetails.js @@ -12,6 +12,13 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun $scope.isAndroid = platformInfo.isAndroid; $scope.isIOS = platformInfo.isIOS; + var channel = "firebase"; + if (platformInfo.isNW) { + channel = "ga"; + } + var log = new window.BitAnalytics.LogEvent("wallet_details_open", [], [channel]); + window.BitAnalytics.LogEventHandlers.postEvent(log); + $scope.amountIsCollapsible = !$scope.isAndroid; $scope.openExternalLink = function(url, target) { diff --git a/src/js/routes.js b/src/js/routes.js index ee7abba58..63b436a85 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -1232,7 +1232,7 @@ angular.module('copayApp').config(function(historicLogProvider, $provide, $logPr } }); - var channel = "ga"; + var channel = "firebase"; if (platformInfo.isNW) { channel = "ga"; } diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index 25f2a6852..4f8710c28 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -427,6 +427,15 @@ angular.module('copayApp.services') }, function(err, secret) { if (err) return bwcError.cb(err, gettextCatalog.getString('Error creating wallet'), cb); + var channel = "firebase"; + if (platformInfo.isNW) { + channel = "ga"; + } + var log = new window.BitAnalytics.LogEvent("wallet_created", [{ + "coin": opts.coin + }], [channel]); + window.BitAnalytics.LogEventHandlers.postEvent(log); + return cb(null, walletClient, secret); }); }); From b46b719498d32bc24b731093ebfe8b45403d1318 Mon Sep 17 00:00:00 2001 From: Brendon Duncan Date: Mon, 16 Jul 2018 12:40:17 +1200 Subject: [PATCH 4/4] BitAnalytics with Firebase parameter name fix. --- bitanalytics/bitanalytics-0.1.0.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/bitanalytics/bitanalytics-0.1.0.js b/bitanalytics/bitanalytics-0.1.0.js index cf85b3c14..db149e481 100644 --- a/bitanalytics/bitanalytics-0.1.0.js +++ b/bitanalytics/bitanalytics-0.1.0.js @@ -6547,13 +6547,26 @@ var FirebaseChannel = /** @class */ (function (_super) { */ FirebaseChannel.prototype.postEvent = function (name, params) { var _this = this; + var sanitizedParams = this.sanitizeParams(params); if (!this.isReady) { - this.enqueue(function () { _this.postEvent(name, params); }); + this.enqueue(function () { _this.postEvent(name, sanitizedParams); }); } else { - this.firebaseInstance.logEvent(name, params); + 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;