Created
June 8, 2015 02:49
-
-
Save 1ambda/a751f7083d6e1a07c9ed to your computer and use it in GitHub Desktop.
rake-1.3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* | |
* rake-javascript-skp | |
* =================== | |
* | |
* ## Javascript Library for web application tracking ## | |
* ### based on Mixpanel JS Library ### | |
* | |
* Mixpanel JS Library v2.2.0 | |
* | |
* 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.2.min.js | |
// ==/ClosureCompiler== | |
/* | |
Will export window.mixpanel | |
*/ | |
/* | |
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 | |
*/ | |
(function (mixpanel) { | |
/* | |
* Saved references to long variable names, so that closure compiler can | |
* minimize file size. | |
*/ | |
var ArrayProto = Array.prototype | |
, ObjProto = Object.prototype | |
, slice = ArrayProto.slice | |
, toString = ObjProto.toString | |
, hasOwnProperty = ObjProto.hasOwnProperty | |
, windowConsole = window.console | |
, navigator = window.navigator | |
, document = window.document | |
, userAgent = navigator.userAgent; | |
var rake_lib_ver = 'r0.5.0_c1.3'; | |
/* | |
* Constants | |
*/ | |
/** @const */ var PRIMARY_INSTANCE_NAME = "mixpanel" | |
/** @const */ , SET_QUEUE_KEY = "__mps" | |
/** @const */ , SET_ONCE_QUEUE_KEY = "__mpso" | |
/** @const */ , ADD_QUEUE_KEY = "__mpa" | |
/** @const */ , APPEND_QUEUE_KEY = "__mpap" | |
/** @const */ , SET_ACTION = "$set" | |
/** @const */ , SET_ONCE_ACTION = "$set_once" | |
/** @const */ , ADD_ACTION = "$add" | |
/** @const */ , APPEND_ACTION = "$append" | |
// This key is deprecated, but we want to check for it to see whether aliasing is allowed. | |
/** @const */ , PEOPLE_DISTINCT_ID_KEY = "$people_distinct_id" | |
/** @const */ , ALIAS_ID_KEY = "__alias" | |
/** @const */ , RESERVED_PROPERTIES = [SET_QUEUE_KEY, SET_ONCE_QUEUE_KEY, ADD_QUEUE_KEY, APPEND_QUEUE_KEY, PEOPLE_DISTINCT_ID_KEY, ALIAS_ID_KEY]; | |
/* | |
* Dynamic... constants? Is that an oxymoron? | |
*/ | |
var HTTP_PROTOCOL = (("https:" == document.location.protocol) ? "https://" : "http://") | |
, SNIPPET_VERSION = (mixpanel && mixpanel['__SV']) || 0 | |
// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ | |
// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials | |
, USE_XHR = (window.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 | |
// , ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') == -1); | |
// by lonslonz | |
// script with defer will delay send request until DOMContentLoaded event. so ENQUEUE_REQUESTS is not needed. | |
// IE <= 10 have 'MSIE' string in userAgent. But IE11 doesn't have. | |
// IE <= 9 does not support XMLHttpRequest & withCredentials. | |
// When USE_XHR is true and IE < 10, ENQUEUE_REQUESTS can be set to false. | |
// When USE_XHR is true and IE = 11, XMLHttpRequest's open have problem, access denied (script 5), about all IE 11(?) - need to test. | |
// It's enough as follows. | |
// if(USE_XHR) ENQUEUE_REQUESTS = true; | |
// else ENQUEUE_REQUESTS = false; | |
// Script with defer can be queued, it is not different with XHR. It's just changes of send method. | |
// There is no problem that ENQUEUE_REQUESTS always set to be true. | |
, ENQUEUE_REQUESTS = true; | |
/* | |
* Closure-level globals | |
*/ | |
var _ = {} | |
, DEBUG = false | |
, DEFAULT_CONFIG = { | |
"api_host": 'https://rake.skplanet.com:8443', | |
"cross_subdomain_cookie": true, | |
"cookie_name": "", | |
"loaded": function () {}, | |
"store_google": true, | |
"save_referrer": true, | |
"test": false, | |
"verbose": false, | |
"img": false, | |
"track_pageview": false, | |
"debug": false, | |
"track_links_timeout": 300, | |
"cookie_expiration": 365, | |
"upgrade": false, | |
"disable_cookie": false, | |
"secure_cookie": false, | |
"ip": true | |
} | |
, DOM_LOADED = false; | |
// UNDERSCORE | |
// Embed part of the Underscore Library | |
(function () { | |
var nativeForEach = ArrayProto.forEach, | |
nativeIndexOf = ArrayProto.indexOf, | |
nativeIsArray = Array.isArray, | |
breaker = {}; | |
/** | |
* @param {*=} obj | |
* @param {function(...[*])=} iterator | |
* @param {Object=} context | |
*/ | |
var each = _.each = function (obj, iterator, context) { | |
if (obj == null) return; | |
if (nativeForEach && obj.forEach === nativeForEach) { | |
obj.forEach(iterator, context); | |
} else if (obj.length === +obj.length) { | |
for (var i = 0, l = obj.length; i < l; i++) { | |
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; | |
} | |
} else { | |
for (var key in obj) { | |
if (hasOwnProperty.call(obj, key)) { | |
if (iterator.call(context, obj[key], key, obj) === breaker) return; | |
} | |
} | |
} | |
}; | |
_.extend = function (obj) { | |
each(slice.call(arguments, 1), function (source) { | |
for (var prop in source) { | |
if (source[prop] !== void 0) obj[prop] = source[prop]; | |
} | |
}); | |
return obj; | |
}; | |
_.isArray = nativeIsArray || function (obj) { | |
return toString.call(obj) === '[object Array]'; | |
}; | |
// from a comment on http://dbj.org/dbj/?p=286 | |
// fails on only one very rare and deliberate custom object: | |
// var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; | |
_.isFunction = function (f) { | |
try { | |
return /^\s*\bfunction\b/.test(f); | |
} catch (x) { | |
return false; | |
} | |
}; | |
_.isArguments = function (obj) { | |
return !!(obj && hasOwnProperty.call(obj, 'callee')); | |
}; | |
_.toArray = function (iterable) { | |
if (!iterable) return []; | |
if (iterable.toArray) return iterable.toArray(); | |
if (_.isArray(iterable)) return slice.call(iterable); | |
if (_.isArguments(iterable)) return slice.call(iterable); | |
return _.values(iterable); | |
}; | |
_.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]'; | |
}; | |
_.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; | |
}; | |
_.formatDate = function (d) { | |
// YYYY-MM-DDTHH:MM:SS in UTC | |
function pad(n) { | |
return n < 10 ? '0' + n : n | |
} | |
return d.getUTCFullYear() + '-' | |
+ pad(d.getUTCMonth() + 1) + '-' | |
+ pad(d.getUTCDate()) + 'T' | |
+ pad(d.getUTCHours()) + ':' | |
+ pad(d.getUTCMinutes()) + ':' | |
+ pad(d.getUTCSeconds()); | |
}; | |
_.strip_empty_properties = function (p) { | |
var ret = {}; | |
_.each(p, function (v, k) { | |
if (_.isString(v) && v.length > 0) { | |
ret[k] = v; | |
} | |
}); | |
return ret; | |
}; | |
/* | |
* this function returns a copy of object after truncating it. If | |
* passed an Array or Object it will iterate through obj and | |
* truncate all the values recursively. | |
*/ | |
_.truncate = function (obj, length) { | |
var ret; | |
if (typeof(obj) === "string") { | |
ret = obj.slice(0, length); | |
} else if (_.isArray(obj)) { | |
ret = []; | |
_.each(obj, function (val) { | |
ret.push(_.truncate(val, length)); | |
}); | |
} else if (_.isObject(obj)) { | |
ret = {}; | |
_.each(obj, function (val, key) { | |
ret[key] = _.truncate(val, length); | |
}); | |
} else { | |
ret = obj; | |
} | |
return ret; | |
}; | |
_.JSONEncode = (function () { | |
return function (mixed_val) { | |
var indent; | |
var value = mixed_val; | |
var i; | |
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; | |
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 | |
}); | |
}; | |
})(); | |
_.JSONDecode = (function () { // https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js | |
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) { | |
throw { | |
name: 'SyntaxError', | |
message: m, | |
at: at, | |
text: text | |
}; | |
}, | |
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 (n) { | |
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 () { | |
var a = userAgent; | |
if (/(google web preview|baiduspider|yandexbot)/i.test(a)) { | |
return true; | |
} | |
return false; | |
}; | |
/** | |
* @param {Object=} formdata | |
* @param {string=} arg_separator | |
*/ | |
_.HTTPBuildQuery = function (formdata, arg_separator) { | |
var key, use_val, use_key, tmp_arr = []; | |
if (typeof(arg_separator) === "undefined") { | |
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, ' '); | |
} | |
}; | |
// _.cookie | |
// Methods partially borrowed from quirksmode.org/js/cookies.html | |
_.cookie = { | |
get: function (name) { | |
var nameEQ = name + "="; | |
var ca = document.cookie.split(';'); | |
for (var i = 0; i < ca.length; i++) { | |
var c = ca[i]; | |
while (c.charAt(0) == ' ') c = c.substring(1, c.length); | |
if (c.indexOf(nameEQ) == 0) return decodeURIComponent(c.substring(nameEQ.length, c.length)); | |
} | |
return null; | |
}, | |
parse: function (name) { | |
var cookie; | |
try { | |
cookie = _.JSONDecode(_.cookie.get(name)) || {}; | |
} catch (err) { | |
} | |
return cookie; | |
}, | |
set: function (name, value, days, cross_subdomain, is_secure) { | |
var cdomain = "", expires = "", secure = ""; | |
if (cross_subdomain) { | |
var matches = document.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"; | |
} | |
document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/" + cdomain + secure; | |
}, | |
remove: function (name, cross_subdomain) { | |
_.cookie.set(name, '', -1, cross_subdomain); | |
} | |
}; | |
_.register_event = (function () { | |
// written by Dean Edwards, 2005 | |
// with input from Tino Zijdel - [email protected] | |
// with input from Carl Sverre - [email protected] | |
// 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 | |
*/ | |
var register_event = function (element, type, handler, oldSchool) { | |
if (!element) { | |
console.error("No valid element provided to register_event"); | |
return; | |
} | |
if (element.addEventListener && !oldSchool) { | |
element.addEventListener(type, handler, false); | |
} 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.getElementsByTagName) { | |
return new Array(); | |
} | |
// Split selector in to tokens | |
var tokens = selector.split(' '); | |
var token; | |
var currentContext = new Array(document); | |
for (var i = 0; i < tokens.length; i++) { | |
token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); | |
if (token.indexOf('#') > -1) { | |
// Token is an ID selector | |
var bits = token.split('#'); | |
var tagName = bits[0]; | |
var id = bits[1]; | |
var element = document.getElementById(id); | |
if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { | |
// element not found or tag with that ID not found, return false | |
return new Array(); | |
} | |
// Set currentContext to contain just this element | |
currentContext = new Array(element); | |
continue; // Skip to next token | |
} | |
if (token.indexOf('.') > -1) { | |
// Token contains a class selector | |
var bits = token.split('.'); | |
var tagName = bits[0]; | |
var className = bits[1]; | |
if (!tagName) { | |
tagName = '*'; | |
} | |
// Get elements matching tag, filter them for class selector | |
var found = new Array; | |
var foundCount = 0; | |
for (var h = 0; h < currentContext.length; h++) { | |
var elements; | |
if (tagName == '*') { | |
elements = getAllChildren(currentContext[h]); | |
} else { | |
elements = currentContext[h].getElementsByTagName(tagName); | |
} | |
for (var j = 0; j < elements.length; j++) { | |
found[foundCount++] = elements[j]; | |
} | |
} | |
currentContext = new Array; | |
var currentContextIndex = 0; | |
for (var k = 0; k < found.length; k++) { | |
if (found[k].className | |
&& _.isString(found[k].className) // some SVG elements have classNames which are not strings | |
&& hasClass(found[k], className) | |
) { | |
currentContext[currentContextIndex++] = found[k]; | |
} | |
} | |
continue; // Skip to next token | |
} | |
// Code to deal with attribute selectors | |
if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) { | |
var tagName = RegExp.$1; | |
var attrName = RegExp.$2; | |
var attrOperator = RegExp.$3; | |
var attrValue = RegExp.$4; | |
if (!tagName) { | |
tagName = '*'; | |
} | |
// Grab all of the tagName elements within current context | |
var found = new Array; | |
var foundCount = 0; | |
for (var h = 0; h < currentContext.length; h++) { | |
var elements; | |
if (tagName == '*') { | |
elements = getAllChildren(currentContext[h]); | |
} else { | |
elements = currentContext[h].getElementsByTagName(tagName); | |
} | |
for (var j = 0; j < elements.length; j++) { | |
found[foundCount++] = elements[j]; | |
} | |
} | |
currentContext = new Array; | |
var 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 = new Array; | |
currentContextIndex = 0; | |
for (var k = 0; k < found.length; k++) { | |
if (checkFunction(found[k])) { | |
currentContext[currentContextIndex++] = found[k]; | |
} | |
} | |
// 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; | |
var found = new Array; | |
var foundCount = 0; | |
for (var h = 0; h < currentContext.length; h++) { | |
var elements = currentContext[h].getElementsByTagName(tagName); | |
for (var j = 0; j < elements.length; j++) { | |
found[foundCount++] = elements[j]; | |
} | |
} | |
currentContext = found; | |
} | |
return currentContext; | |
}; | |
return getElementsBySelector; | |
})(); | |
_.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.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. | |
*/ | |
// SKP Browser Detection Spec : IE(01), Chrome(02), Firefox(03), Safari(04), Etc(05) (x) | |
browser_version: function () { | |
var ua = userAgent, | |
M = ua.match(/(opera|chrome|safari|firefox|msie|trident)\/?\s*([\d\.]+)/i) || []; | |
return M[2]; | |
}, | |
browser: function () { | |
var ua = userAgent | |
, vend = navigator.vendor || ''; // vendor is undefined for at least IE9 | |
if (window.opera) { | |
if (_.includes(ua, "Mini")) { | |
return "Opera Mini"; | |
} | |
return "Opera"; | |
} else if (/(BlackBerry|PlayBook|BB10)/i.test(ua)) { | |
return 'BlackBerry'; | |
} else if (_.includes(ua, "Chrome")) { | |
return "Chrome"; | |
} else if (_.includes(vend, "Apple")) { | |
if (_.includes(ua, "Mobile")) { | |
return "Mobile Safari"; | |
} | |
return "Safari"; | |
} else if (_.includes(ua, "Android")) { | |
return "Android Mobile"; | |
} else if (_.includes(ua, "Konqueror")) { | |
return "Konqueror"; | |
} else if (_.includes(ua, "Firefox")) { | |
return "Firefox"; | |
} else if (_.includes(ua, "MSIE")) { | |
return "Internet Explorer"; | |
} else if (_.includes(ua, "Gecko")) { | |
return "Mozilla"; | |
} else { | |
return ""; | |
} | |
}, | |
os: function () { | |
var a = userAgent; | |
if (/Windows/i.test(a)) { | |
if (/Phone/.test(a)) { | |
return 'Windows Mobile'; | |
} | |
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 { | |
return 'unknown'; | |
} | |
}, | |
os_version: function(){ | |
var a = userAgent; | |
if (/Windows/i.test(a)) { | |
if (/Phone/.test(a)) { | |
return navigator.appVersion; | |
} | |
return navigator.appVersion; | |
} else if (/(iPhone|iPad|iPod)/.test(a)) { | |
var v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/); | |
//return v[0].split(' ')[1]; | |
return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)].join('.'); | |
} else if (/Android/.test(a)) { | |
var androidversion = parseFloat(a.slice(a.indexOf("Android")+8)); | |
return androidversion; | |
} else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { | |
return navigator.appVersion; | |
} else if (/Mac/i.test(a)) { | |
return navigator.appVersion; | |
} else if (/Linux/.test(a)) { | |
return navigator.appVersion; | |
} else { | |
return 'unknown'; | |
} | |
}, | |
device: function () { | |
var a = userAgent; | |
if (/iPhone/.test(a)) { | |
return 'iPhone'; | |
} else if (/iPad/.test(a)) { | |
return 'iPad'; | |
} else if (/iPod/.test(a)) { | |
return 'iPod Touch'; | |
} else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { | |
return 'BlackBerry'; | |
} else if (/Windows Phone/i.test(a)) { | |
return 'Windows Phone'; | |
} else if (/Android/.test(a)) { | |
return 'Android'; | |
} else { | |
return ''; | |
} | |
}, | |
referringDomain: function (referrer) { | |
var split = referrer.split("/"); | |
if (split.length >= 3) { | |
return split[2]; | |
} | |
return ""; | |
}, | |
timeStamp: function(date) { | |
var date_str = date.getFullYear().toString(); | |
date_str += (((date.getMonth()+1)<10?'0':''))+(date.getMonth()+1).toString(); | |
date_str += ((date.getDate()<10?'0':'') + date.getDate()).toString(); | |
var hours = (date.getHours()<10?'0':'') + date.getHours() | |
date_str += hours.toString(); | |
date_str += ((date.getMinutes()<10?'0':'') + date.getMinutes()).toString(); | |
date_str += ((date.getSeconds()<10?'0':'') + date.getSeconds()).toString(); | |
var milliseconds = date.getMilliseconds(); | |
if(milliseconds < 10){ | |
date_str += '00'; | |
}else if(milliseconds <100){ | |
date_str += '0'; | |
} | |
date_str += milliseconds.toString(); | |
return date_str; | |
}, | |
timeZone: function(){ | |
return new Date().toTimeString(); | |
}, | |
resolution: function(){ | |
return _.info.screen_width()+'x'+_.info.screen_height(); | |
}, | |
screen_width: function(){ | |
return window.screen.width; | |
}, | |
screen_height: function(){ | |
return window.screen.height; | |
}, | |
locale : function(){ | |
return 'unknown'; | |
}, | |
phone_model: function(){ | |
var ua = navigator.userAgent; | |
if (/iPhone/.test(ua)) { | |
var startIndex = ua.indexOf('('); | |
var endIndex = ua.indexOf(')'); | |
var model = ua.substring(startIndex,endIndex).split(';'); | |
return model[model.length-1].trim().split(' ')[1]; | |
} else if (/iPad/.test(ua)) { | |
return 'iPad'; | |
} else if (/iPod/.test(ua)) { | |
return 'iPod Touch'; | |
} else if (/(BlackBerry|PlayBook|BB10)/i.test(ua)) { | |
return 'BlackBerry'; | |
} else if (/Windows Phone/i.test(ua)) { | |
return 'Windows Phone'; | |
} else if (/Android/.test(ua)) { | |
var startIndex = ua.indexOf('('); | |
var endIndex = ua.indexOf(')'); | |
var model = ua.substring(startIndex,endIndex).split(';'); | |
model = model[model.length-1].trim(); | |
model = model.split(' '); | |
var model_build = model.splice(model.length-1, 1); | |
return model.join(' '); | |
} else { | |
return ''; | |
} | |
}, | |
lang: function(){ | |
return navigator.language; | |
}, | |
platform: function(){ | |
return navigator.platform; | |
}, | |
userAgent: function(){ | |
return navigator.userAgent; | |
}, | |
network_type: function(){ | |
//return navigator.connection; | |
}, | |
properties: function () { | |
// return _.strip_empty_properties({ | |
// don't strip empty prorperties | |
var d = new Date(); | |
return ({ | |
'base_time': _.info.timeStamp(new Date(d.getTime() + (d.getTimezoneOffset() * 60000) + (3600000*9))), | |
'local_time': _.info.timeStamp(new Date()), | |
'os_name': _.info.os(), | |
'os_version': _.info.os_version(), | |
'browser_name': _.info.browser(), | |
'browser_version': _.info.browser_version(), | |
'resolution': _.info.resolution(), | |
'screen_width': _.info.screen_width(), | |
'screen_height': _.info.screen_height(), | |
'language_code':_.info.lang(), | |
'device': _.info.device(), | |
'device_model': _.info.phone_model(), | |
'referrer': document.referrer, | |
'rake_lib': 'web', | |
'rake_lib_version': rake_lib_ver, | |
'platform': _.info.platform(), | |
'document_title': document.title, | |
'url': document.URL | |
}); | |
}, | |
people_properties: function () { | |
//return _.strip_empty_properties({ | |
return ({ | |
'Os': _.info.os(), | |
'Browser': _.info.browser() | |
}); | |
}, | |
pageviewInfo: function (page) { | |
//return _.strip_empty_properties({ | |
return ({ | |
'url':page | |
//'mp_page': page, 'mp_referrer': document.referrer, 'mp_browser': _.info.browser(), 'mp_platform': _.info.os() | |
}); | |
} | |
}; | |
// Console override | |
var console = { | |
/** @type {function(...[*])} */ | |
log: function () { | |
if (DEBUG && !_.isUndefined(windowConsole) && windowConsole) { | |
try { | |
windowConsole.log.apply(windowConsole, arguments); | |
} catch (err) { | |
_.each(arguments, function (arg) { | |
windowConsole.log(arg); | |
}); | |
} | |
} | |
}, | |
/** @type {function(...[*])} */ | |
error: function () { | |
if (DEBUG && !_.isUndefined(windowConsole) && windowConsole) { | |
var args = ["Mixpanel error:"].concat(_.toArray(arguments)); | |
try { | |
windowConsole.error.apply(windowConsole, args); | |
} catch (err) { | |
_.each(args, function (arg) { | |
windowConsole.error(arg); | |
}); | |
} | |
} | |
}, | |
/** @type {function(...[*])} */ | |
critical: function () { | |
if (!_.isUndefined(windowConsole) && windowConsole) { | |
var args = ["Mixpanel error:"].concat(_.toArray(arguments)); | |
try { | |
windowConsole.error.apply(windowConsole, args); | |
} catch (err) { | |
_.each(args, function (arg) { | |
windowConsole.error(arg); | |
}); | |
} | |
} | |
} | |
}; | |
/** | |
* 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 {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 | |
, elements = _.dom_query(query); | |
if (elements.length == 0) { | |
console.error("The DOM query (" + query + ") returned 0 elements"); | |
return; | |
} | |
_.each(elements, function (element) { | |
_.register_event(element, this.override_event, function (e) { | |
var options = {} | |
, props = that.create_properties(properties, this) | |
, 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.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 {string} query | |
* @param {string} event_name | |
* @param {Object=} properties | |
* @param {function(...[*])=} user_callback | |
*/ | |
DomTracker.prototype.trackSimple = function (query, event_name, properties, user_callback) { | |
var that = this | |
, elements = _.dom_query(query); | |
if (elements.length == 0) { | |
console.error("The DOM query (" + query + ") returned 0 elements"); | |
return; | |
} | |
_.each(elements, function (element) { | |
_.register_event(element, this.override_event, function (e) { | |
var options = {} | |
, props = that.create_properties(properties, this) | |
, 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.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 || element.target === "_blank"); | |
options.href = element.href; | |
if (!options.new_tab) { | |
evt.preventDefault(); | |
} | |
}; | |
LinkTracker.prototype.after_track_handler = function (props, options, timeout_occured) { | |
if (options.new_tab) { | |
return; | |
} | |
setTimeout(function () { | |
window.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, timeout_occured) { | |
setTimeout(function () { | |
options.element.submit(); | |
}, 0); | |
}; | |
/** | |
* Mixpanel Cookie Object | |
* @constructor | |
*/ | |
var MixpanelCookie = function (config) { | |
this['props'] = {}; | |
this.campaign_params_saved = false; | |
if (config['cookie_name']) { | |
this.name = "mp_" + config['cookie_name']; | |
} else { | |
this.name = "mp_" + config['token'] + "_mixpanel"; | |
} | |
this.load(); | |
this.update_config(config); | |
this.upgrade(config); | |
this.save(); | |
}; | |
MixpanelCookie.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; | |
}; | |
MixpanelCookie.prototype.load = function () { | |
if (this.disabled) { | |
return; | |
} | |
var cookie = _.cookie.parse(this.name); | |
if (cookie) { | |
this['props'] = _.extend({}, cookie); | |
} | |
}; | |
MixpanelCookie.prototype.upgrade = function (config) { | |
var should_upgrade = config['upgrade'], | |
old_cookie_name, | |
old_cookie; | |
if (should_upgrade) { | |
old_cookie_name = "mp_super_properties"; | |
// Case where they had a custom cookie name before. | |
if (typeof(should_upgrade) === "string") { | |
old_cookie_name = should_upgrade; | |
} | |
old_cookie = _.cookie.parse(old_cookie_name); | |
// remove the cookie | |
_.cookie.remove(old_cookie_name); | |
_.cookie.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 = _.cookie.parse(old_cookie_name); | |
if (old_cookie) { | |
_.cookie.remove(old_cookie_name); | |
_.cookie.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); | |
} | |
} | |
}; | |
MixpanelCookie.prototype.save = function () { | |
if (this.disabled) { | |
return; | |
} | |
_.cookie.set( | |
this.name, | |
_.JSONEncode(this['props']), | |
this.expire_days, | |
this.cross_subdomain, | |
this.secure | |
); | |
}; | |
MixpanelCookie.prototype.remove = function () { | |
// remove both domain and subdomain cookies | |
_.cookie.remove(this.name, false); | |
_.cookie.remove(this.name, true); | |
}; | |
// removes the cookie and deletes all loaded data | |
// forced name for tests | |
MixpanelCookie.prototype.clear = function () { | |
this.remove(); | |
this['props'] = {}; | |
}; | |
/** | |
* @param {Object} props | |
* @param {*=} default_value | |
* @param {number=} days | |
*/ | |
MixpanelCookie.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'][prop] || this['props'][prop] === default_value) { | |
this['props'][prop] = val; | |
} | |
}, this); | |
this.save(); | |
return true; | |
} | |
return false; | |
}; | |
/** | |
* @param {Object} props | |
* @param {number=} days | |
*/ | |
MixpanelCookie.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; | |
}; | |
MixpanelCookie.prototype.unregister = function (prop) { | |
if (prop in this['props']) { | |
delete this['props'][prop]; | |
this.save(); | |
} | |
}; | |
MixpanelCookie.prototype.update_campaign_params = function () { | |
if (!this.campaign_params_saved) { | |
this.register_once(_.info.campaignParams()); | |
this.campaign_params_saved = true; | |
} | |
}; | |
MixpanelCookie.prototype.update_search_keyword = function (referrer) { | |
this.register(_.info.searchInfo(referrer)); | |
}; | |
// EXPORTED METHOD, we test this directly. | |
MixpanelCookie.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" | |
}, ""); | |
}; | |
MixpanelCookie.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 the cookies properties, | |
// does not override any properties defined in both | |
// returns the passed in object | |
MixpanelCookie.prototype.safe_merge = function (props) { | |
_.each(this['props'], function (val, prop) { | |
if (!(prop in props)) { | |
props[prop] = val; | |
} | |
}); | |
return props; | |
}; | |
MixpanelCookie.prototype.update_config = function (config) { | |
this.default_expiry = this.expire_days = config['cookie_expiration']; | |
this.set_disabled(config['disable_cookie']); | |
this.set_cross_subdomain(config['cross_subdomain_cookie']); | |
this.set_secure(config['secure_cookie']); | |
}; | |
MixpanelCookie.prototype.set_disabled = function (disabled) { | |
this.disabled = disabled; | |
if (this.disabled) { | |
this.remove(); | |
} | |
}; | |
MixpanelCookie.prototype.set_cross_subdomain = function (cross_subdomain) { | |
if (cross_subdomain !== this.cross_subdomain) { | |
this.cross_subdomain = cross_subdomain; | |
this.remove(); | |
this.save(); | |
} | |
}; | |
MixpanelCookie.prototype.get_cross_subdomain = function () { | |
return this.cross_subdomain; | |
}; | |
MixpanelCookie.prototype.set_secure = function (secure) { | |
if (secure !== this.secure) { | |
this.secure = secure ? true : false; | |
this.remove(); | |
this.save(); | |
} | |
}; | |
MixpanelCookie.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), | |
add_q = this._get_or_create_queue(ADD_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); | |
} 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; | |
} | |
}); | |
} 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); | |
} else if (q_key === APPEND_QUEUE_KEY) { | |
append_q.push(q_data); | |
} | |
console.log("MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):"); | |
console.log(data); | |
this.save(); | |
}; | |
MixpanelCookie.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(); | |
} | |
}; | |
MixpanelCookie.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 === ADD_ACTION) { | |
return ADD_QUEUE_KEY; | |
} else if (queue === APPEND_ACTION) { | |
return APPEND_QUEUE_KEY; | |
} else { | |
console.error("Invalid queue:", queue); | |
} | |
}; | |
MixpanelCookie.prototype._get_queue = function (queue) { | |
return this['props'][this._get_queue_key(queue)]; | |
}; | |
MixpanelCookie.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); | |
}; | |
/** | |
* 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 : mixpanel[name]; | |
if (target && !_.isArray(target)) { | |
console.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 | |
DEBUG = DEBUG || instance.get_config('debug'); | |
// 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)) { | |
// 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; | |
}; | |
/** | |
* Mixpanel Library Object | |
* @constructor | |
*/ | |
var MixpanelLib = function () { | |
}; | |
// Initialization methods | |
/** | |
* This function initialize a new instance of the Mixpanel tracking object. | |
* All new instances are added to the main mixpanel object as sub properties (such as | |
* mixpanel.your_library_name) and also returned by this function. If you wanted | |
* to define a second instance on the page you would do it like so: | |
* | |
* mixpanel.init("new token", { your: "config" }, "library_name") | |
* | |
* and use it like this: | |
* | |
* mixpanel.library_name.track(...) | |
* | |
* @param {String} token Your Mixpanel API token | |
* @param {Object} config A dictionary of config options to override | |
* @param {String} name The name for the new mixpanel instance that you want created | |
*/ | |
MixpanelLib.prototype.init = function (token, config, name) { | |
if (typeof(name) === "undefined") { | |
console.error("You must name your new library: init(token, config, name)"); | |
return; | |
} | |
if (name === PRIMARY_INSTANCE_NAME) { | |
console.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[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.__track_queue = []; | |
this.__disabled_events = []; | |
this._flags = { | |
"disable_all_events": false, "identify_called": false | |
}; | |
this['cookie'] = new MixpanelCookie(this['config']); | |
//this.register_once({'distinct_id': _.UUID()}, ""); | |
}; | |
// Private methods | |
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); | |
_.each(this.__request_queue, function (item) { | |
this._send_request.apply(this, item); | |
}, this); | |
_.each(this.__track_queue, function (item) { | |
this.track.apply(this, item); | |
}, this); | |
delete this.__dom_loaded_queue; | |
delete this.__request_queue; | |
delete this.__track_queue; | |
}; | |
MixpanelLib.prototype._track_dom = function (DomClass, args) { | |
if (this.get_config('img')) { | |
console.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'] | |
, randomized_cb = '' + Math.floor(Math.random() * 100000000) | |
, 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 (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.createElement("img"); | |
img.src = url; | |
document.body.appendChild(img); | |
} else if (USE_XHR) { | |
var req = new XMLHttpRequest(); | |
// GET or POST (if post : req.send("id=mom&pw=1234");) | |
req.open("GET", url, true); | |
// send the mp_optout cookie | |
// withCredentials cannot be modified until after calling .open on Android and Mobile Safari | |
req.withCredentials = true; | |
req.onreadystatechange = function (e) { | |
if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 | |
if (req.status === 200) { | |
if (callback) { | |
if (verbose_mode) { | |
callback(_.JSONDecode(req.responseText)); | |
} | |
else { | |
callback(Number(req.responseText)); | |
} | |
} | |
} else { | |
var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; | |
console.error(error); | |
if (callback) { | |
if (verbose_mode) { | |
callback({ status: 0, error: error }); | |
} | |
else { | |
callback(0); | |
} | |
} | |
} | |
} | |
}; | |
console.log('url : ' + url); | |
req.send(null); | |
} else { | |
var script = document.createElement("script"); | |
script.type = "text/javascript"; | |
script.async = true; | |
script.defer = true; | |
script.src = url; | |
var s = document.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). Good example is Optimizely. | |
* | |
* ### 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 doesn't stop regular mixpanel functions from | |
* firing such as register and name_tag. | |
* | |
* @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); | |
} | |
}; | |
/** rake track | |
* Track an event. This is the most important Mixpanel function and | |
* the one you will be using the most. | |
* | |
* ### Usage: | |
* | |
* // track an event named "Registered" | |
* mixpanel.track("Registered", {"Gender": "Male", "Age": 21}); | |
* | |
* For tracking link clicks or form submissions, see mixpanel.track_links or mixpanel.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 = function (event_name, properties, callback) { | |
if (typeof(event_name) === "undefined") { | |
console.error("No event name provided to mixpanel.track"); | |
return; | |
} | |
if (_.isBlockedUA() | |
|| this._flags.disable_all_events | |
|| _.include(this.__disabled_events, event_name)) { | |
if (typeof(callback) !== 'undefined') { | |
callback(0); | |
} | |
return; | |
} | |
// by lons : RAKE-104 | |
if (ENQUEUE_REQUESTS) { | |
console.log("enqueue request in track()"); | |
this.__track_queue.push(arguments); | |
return; | |
} | |
// set defaults | |
properties = properties || {}; | |
properties['token'] = properties.token || this.get_config('token'); | |
// update cookie | |
this['cookie'].update_search_keyword(document.referrer); | |
if (this.get_config('store_google')) { | |
this['cookie'].update_campaign_params(); | |
} | |
if (this.get_config('save_referrer')) { | |
this['cookie'].update_referrer_info(document.referrer); | |
} | |
// note: extend writes to the first object, so lets make sure we | |
// don't write to the cookie properties object and info | |
// properties object by passing in a new object | |
// update properties with pageview info and super-properties | |
properties = _.extend( | |
{} | |
, properties | |
, this['cookie'].properties() | |
, _.info.properties() | |
); | |
delete properties['distinct_id']; | |
delete properties['initial_referring_domain']; | |
delete properties['initial_referrer']; | |
delete properties['$initial_referring_domain']; | |
delete properties['$initial_referrer']; | |
var data = [{}]; | |
// sentinel meta | |
if(properties['sentinel_meta']){ | |
data[0]['_$schemaId'] = properties['sentinel_meta']['_$schemaId']; | |
data[0]['_$fieldOrder'] = properties['sentinel_meta']['_$fieldOrder']; | |
data[0]['_$encryptionFields'] = properties['sentinel_meta']['_$encryptionFields']; | |
delete properties['sentinel_meta']; | |
} | |
data[0]['properties'] = properties; | |
var truncated_data = _.truncate(data, 255) | |
, json_data = _.JSONEncode(truncated_data) | |
, encoded_data = _.base64Encode(json_data); | |
// send | |
this._send_request( | |
this.get_config('api_host') + "/log/track", | |
{ 'data': encoded_data, 'compress': 'plain'}, | |
//{ 'data': json_data, 'compress': 'plain'}, | |
this._prepare_callback(callback, truncated_data) | |
//this._prepare_callback(callback, json_data) | |
); | |
return truncated_data; | |
}; | |
/** | |
* Track a page view event. This is most useful for ajax websites | |
* where new page views occur without a new page load. 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. | |
*/ | |
MixpanelLib.prototype.track_pageview = function (page) { | |
if (typeof(page) === "undefined") { | |
page = document.location.href; | |
} | |
this.track("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 | |
* mixpanel.set_config docs below. | |
* | |
* If you pass a function in as the properties argument, the | |
* function will receive the DOMElement which 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 {String} query A valid DOM query | |
* @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); | |
}; | |
/** | |
* Tracks 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 | |
* mixpanel.set_config docs below. | |
* | |
* If you pass a function in as the properties argument, the | |
* function will receive the DOMElement which 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 {String} query A valid DOM query | |
* @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); | |
}; | |
/** | |
* Register a set of super properties, which are included with all | |
* events. This will overwrite previous super property values. | |
* | |
* @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['cookie'].register(props, days); | |
}; | |
/** | |
* Register a set of super properties only once. This will not | |
* overwrite previous super property values, unlike register(). | |
* | |
* ### 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['cookie'].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['cookie'].unregister(property); | |
}; | |
MixpanelLib.prototype._register_single = function (prop, value) { | |
var props = {}; | |
props[prop] = value; | |
this.register(props); | |
}; | |
/** | |
* Identify a user with a unique id. All subsequent | |
* actions caused by this user will be tied to this identity. This | |
* property is used to track unique visitors. If the method is | |
* never called, then unique visitors will be identified by a UUID | |
* generated the first time they visit the site. | |
* | |
* ### Note: | |
* | |
* 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 users id | |
* they will appear to be a new user. | |
* | |
* @param {String} unique_id A string that uniquely identifies a user | |
*/ | |
MixpanelLib.prototype.identify = function (unique_id, _set_callback, _add_callback, _append_callback, _set_once_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 | |
// 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._flags.identify_called = true; | |
// Flush any queued up people requests | |
this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback); | |
}; | |
/** | |
* 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 mixpanel.identify | |
*/ | |
MixpanelLib.prototype.get_distinct_id = function () { | |
return this.get_property('distinct_id'); | |
}; | |
/** | |
* Create an alias. 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. | |
* | |
* @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 the cookie, 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.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 (response) { | |
// Flush the people queue | |
_this.identify(alias); | |
}); | |
} else { | |
console.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 | |
*/ | |
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 span subdomains | |
* cross_subdomain_cookie: true | |
* | |
* // super properties cookie name | |
* cookie_name: "" | |
* | |
* // super properties cookie expiration (in days) | |
* cookie_expiration: 365 | |
* | |
* // should we track a page view on page load | |
* track_pageview: true | |
* | |
* // the amount of time track_links will | |
* // wait for Mixpanel's servers to respond | |
* track_links_timeout: 300 | |
* | |
* // if this is true, the mixpanel cookie will be deleted, | |
* // and no user persistence will take place | |
* disable_cookie: false | |
* | |
* // if this is true, the mixpanel cookie will be marked as | |
* // secure, meaning it will only be transmitted over https | |
* secure_cookie: false | |
* | |
* // 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 | |
* } | |
* | |
* | |
* @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['cookie']) { | |
this['cookie'].update_config(this['config']); | |
} | |
DEBUG = 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. | |
* | |
* @param {String} property_name The name of the super property you want to retrieve | |
*/ | |
MixpanelLib.prototype.get_property = function (property_name) { | |
return this['cookie']['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; | |
}; | |
/** | |
* Mixpanel People Object | |
* @constructor | |
*/ | |
var MixpanelPeople = function () { | |
}; | |
MixpanelPeople.prototype._init = function (mixpanel) { | |
this._mixpanel = mixpanel; | |
}; | |
/* | |
* 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', | |
* 'last_seen': new Date() | |
* }); | |
* // 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 = function (prop, to, callback) { | |
var data = {}; | |
var $set = {}; | |
if (_.isObject(prop)) { | |
_.each(prop, function (v, k) { | |
// We will get these ourselves | |
if (k == '$distinct_id' || k == '$token') { | |
return; | |
} else { | |
$set[k] = v; | |
} | |
}); | |
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.cookie.update_referrer_info(document.referrer); | |
} | |
// update $set object with default people properties | |
$set = _.extend({} | |
, _.info.people_properties() | |
, this._mixpanel.cookie.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. | |
* | |
* ### 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 = function (prop, to, callback) { | |
var data = {}; | |
var $set_once = {}; | |
if (_.isObject(prop)) { | |
_.each(prop, function (v, k) { | |
// We will get these ourselves | |
if (k == '$distinct_id' || k == '$token') { | |
return; | |
} else { | |
$set_once[k] = v; | |
} | |
}); | |
callback = to; | |
} else { | |
$set_once[prop] = to; | |
} | |
data[SET_ONCE_ACTION] = $set_once; | |
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: 1 | |
* }); | |
* | |
* @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 = function (prop, by, callback) { | |
var data = {}; | |
var $add = {}; | |
if (_.isObject(prop)) { | |
_.each(prop, function (v, k) { | |
if (k == '$distinct_id' || k == '$token') { | |
return; | |
} else if (isNaN(parseFloat(v))) { | |
console.error("Invalid increment value passed to mixpanel.people.increment - must be a number"); | |
return; | |
} else { | |
$add[k] = v; | |
} | |
}); | |
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 numeric values. | |
* @param {*} [value] An amount to increment the given property | |
* @param {Function} [callback] If provided, the callback will be called after the tracking event | |
*/ | |
MixpanelPeople.prototype.append = function (list_name, value, callback) { | |
var data = {}; | |
var $append = {}; | |
if (_.isObject(list_name)) { | |
_.each(list_name, function (v, k) { | |
if (k == '$distinct_id' || k == '$token') { | |
return; | |
} else { | |
$append[k] = v; | |
} | |
}); | |
callback = value; | |
} else { | |
$append[list_name] = value; | |
} | |
data[APPEND_ACTION] = $append; | |
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 = function (amount, properties, callback) { | |
if (!_.isNumber(amount)) { | |
amount = parseFloat(amount); | |
if (isNaN(amount)) { | |
console.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.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) | |
, truncated_data = _.truncate(date_encoded_data, 255) | |
, json_data = _.JSONEncode(date_encoded_data) | |
, 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.log("MIXPANEL PEOPLE REQUEST:"); | |
console.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.cookie._add_to_people_queue(SET_ACTION, data); | |
} else if (SET_ONCE_ACTION in data) { | |
this._mixpanel.cookie._add_to_people_queue(SET_ONCE_ACTION, data); | |
} else if (ADD_ACTION in data) { | |
this._mixpanel.cookie._add_to_people_queue(ADD_ACTION, data); | |
} else if (APPEND_ACTION in data) { | |
this._mixpanel.cookie._add_to_people_queue(APPEND_ACTION, data); | |
} else { | |
console.error("Invalid call to _enqueue():", 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) { | |
var _this = this, | |
$set_queue = _.extend({}, this._mixpanel.cookie._get_queue(SET_ACTION)), | |
$set_once_queue = _.extend({}, this._mixpanel.cookie._get_queue(SET_ONCE_ACTION)), | |
$add_queue = _.extend({}, this._mixpanel.cookie._get_queue(ADD_ACTION)), | |
$append_queue = this._mixpanel.cookie._get_queue(APPEND_ACTION); | |
if (!_.isUndefined($set_queue) && _.isObject($set_queue) && !_.isEmptyObject($set_queue)) { | |
_this._mixpanel.cookie._pop_from_people_queue(SET_ACTION, $set_queue); | |
this.set($set_queue, function (response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response == 0) { | |
_this._mixpanel.cookie._add_to_people_queue(SET_ACTION, $set_queue); | |
} | |
if (!_.isUndefined(_set_callback)) { | |
_set_callback(response, data); | |
} | |
}); | |
} | |
if (!_.isUndefined($set_once_queue) && _.isObject($set_once_queue) && !_.isEmptyObject($set_once_queue)) { | |
_this._mixpanel.cookie._pop_from_people_queue(SET_ONCE_ACTION, $set_once_queue); | |
this.set_once($set_once_queue, function (response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response == 0) { | |
_this._mixpanel.cookie._add_to_people_queue(SET_ONCE_ACTION, $set_once_queue); | |
} | |
if (!_.isUndefined(_set_once_callback)) { | |
_set_once_callback(response, data); | |
} | |
}); | |
} | |
if (!_.isUndefined($add_queue) && _.isObject($add_queue) && !_.isEmptyObject($add_queue)) { | |
_this._mixpanel.cookie._pop_from_people_queue(ADD_ACTION, $add_queue); | |
this.increment($add_queue, function (response, data) { | |
// on bad response, we want to add it back to the queue | |
if (response == 0) { | |
_this._mixpanel.cookie._add_to_people_queue(ADD_ACTION, $add_queue); | |
} | |
if (!_.isUndefined(_add_callback)) { | |
_add_callback(response, data); | |
} | |
}); | |
} | |
// 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) { | |
for (var i = $append_queue.length - 1; i >= 0; i--) { | |
var $append_item = $append_queue.pop(); | |
_this.append($append_item, function (response, data) { | |
if (response == 0) { | |
_this._mixpanel.cookie._add_to_people_queue(APPEND_ACTION, $append_item); | |
} | |
if (!_.isUndefined(_append_callback)) { | |
_append_callback(response, data); | |
} | |
}); | |
} | |
; | |
// Save the shortened append queue | |
_this._mixpanel.cookie.save(); | |
} | |
}; | |
// EXPORTS (for closure compiler) | |
// Underscore Exports | |
_['toArray'] = _.toArray; | |
_['isObject'] = _.isObject; | |
_['JSONEncode'] = _.JSONEncode; | |
_['JSONDecode'] = _.JSONDecode; | |
_['isEmptyObject'] = _.isEmptyObject; | |
// MixpanelLib Exports | |
MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; | |
MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; | |
MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; | |
MixpanelLib.prototype['trackSimple'] = MixpanelLib.prototype.trackSimple; | |
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; | |
// MixpanelCookie Exports | |
MixpanelCookie.prototype['properties'] = MixpanelCookie.prototype.properties; | |
MixpanelCookie.prototype['update_search_keyword'] = MixpanelCookie.prototype.update_search_keyword; | |
MixpanelCookie.prototype['update_referrer_info'] = MixpanelCookie.prototype.update_referrer_info; | |
MixpanelCookie.prototype['get_cross_subdomain'] = MixpanelCookie.prototype.get_cross_subdomain; | |
MixpanelCookie.prototype['clear'] = MixpanelCookie.prototype.clear; | |
// MixpanelPeople Exports | |
MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; | |
MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; | |
MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; | |
MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; | |
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; | |
// Initialization | |
if (_.isUndefined(mixpanel)) { | |
// mixpanel wasn't initialized properly, report error and quit | |
console.critical("'mixpanel' object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide."); | |
return; | |
} | |
if (mixpanel['__loaded'] || (mixpanel['config'] && mixpanel['cookie'])) { | |
// lib has already been loaded at least once; we don't want to override the global object this time so bomb early | |
console.error("Mixpanel library has already been downloaded at least once."); | |
return; | |
} | |
if (SNIPPET_VERSION < 1.1) { | |
// mixpanel wasn't initialized properly, report error and quit | |
console.critical("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."); | |
return; | |
} | |
// Load instances of the Mixpanel Library | |
var instances = {}; | |
_.each(mixpanel['_i'], function (item) { | |
var name, instance; | |
if (item && _.isArray(item)) { | |
name = item[item.length - 1]; | |
instance = create_mplib.apply(this, item); | |
instances[name] = instance; | |
} | |
}); | |
var extend_mp = function () { | |
// add all the sub mixpanel instances | |
_.each(instances, function (instance, name) { | |
if (name !== PRIMARY_INSTANCE_NAME) { | |
mixpanel[name] = instance; | |
} | |
}); | |
// add private functions as _ | |
mixpanel['_'] = _; | |
}; | |
// we override the snippets init function to handle the case where a | |
// user initializes the mixpanel library after the script loads & runs | |
mixpanel['init'] = function (token, config, name) { | |
if (name) { | |
// initialize a sub library | |
if (!mixpanel[name]) { | |
mixpanel[name] = instances[name] = create_mplib(token, config, name); | |
mixpanel[name]._loaded(); | |
} | |
} else { | |
var instance = mixpanel; | |
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); | |
} | |
window[PRIMARY_INSTANCE_NAME] = mixpanel = instance; | |
extend_mp(); | |
} | |
}; | |
mixpanel['init'](); | |
// Fire loaded events after updating the window's mixpanel object | |
_.each(instances, function (instance) { | |
instance._loaded(); | |
}); | |
// 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(); | |
}); | |
} | |
if ( document.readyState === "complete" ) { | |
return setTimeout(dom_loaded_handler, 1 ); | |
} | |
if (document.addEventListener) { | |
console.log("document.readyState",document.readyState ); | |
console.log("document.addEventListener(DOMContentLoaded", document); | |
document.addEventListener("DOMContentLoaded", dom_loaded_handler, false); | |
} else if (document.attachEvent) { | |
// IE | |
document.attachEvent("onreadystatechange", dom_loaded_handler); | |
// check to make sure we arn't in a frame | |
var toplevel = false; | |
try { | |
toplevel = window.frameElement == null; | |
} catch (e) { | |
} | |
if (document.documentElement.doScroll && toplevel) { | |
function do_scroll_check() { | |
try { | |
document.documentElement.doScroll("left"); | |
} catch (e) { | |
setTimeout(do_scroll_check, 1); | |
return; | |
} | |
dom_loaded_handler(); | |
}; | |
do_scroll_check(); | |
} | |
} | |
// fallback handler, always will work | |
_.register_event(window, 'load', dom_loaded_handler, true); | |
})(window['mixpanel']); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment