PageRenderTime 156ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/deps/js/mixpanel.js

https://github.com/ShiftFinancial/ripple-client
JavaScript | 3017 lines | 2141 code | 250 blank | 626 comment | 381 complexity | 78ccf84f17ebe11f46b2bb814d958186 MD5 | raw file
Possible License(s): 0BSD, Apache-2.0
  1. /*
  2. * Mixpanel JS Library v2.2.0
  3. *
  4. * Copyright 2012, Mixpanel, Inc. All Rights Reserved
  5. * http://mixpanel.com/
  6. *
  7. * Includes portions of Underscore.js
  8. * http://documentcloud.github.com/underscore/
  9. * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
  10. * Released under the MIT License.
  11. */
  12. // ==ClosureCompiler==
  13. // @compilation_level ADVANCED_OPTIMIZATIONS
  14. // @output_file_name mixpanel-2.2.min.js
  15. // ==/ClosureCompiler==
  16. /*
  17. Will export window.mixpanel
  18. */
  19. /*
  20. SIMPLE STYLE GUIDE:
  21. this.x == public function
  22. this._x == internal - only use within this file
  23. this.__x == private - only use within the class
  24. Globals should be all caps
  25. */
  26. (function (mixpanel) {
  27. /*
  28. * Saved references to long variable names, so that closure compiler can
  29. * minimize file size.
  30. */
  31. var ArrayProto = Array.prototype
  32. , ObjProto = Object.prototype
  33. , slice = ArrayProto.slice
  34. , toString = ObjProto.toString
  35. , hasOwnProperty = ObjProto.hasOwnProperty
  36. , windowConsole = window.console
  37. , navigator = window.navigator
  38. , document = window.document
  39. , userAgent = navigator.userAgent;
  40. /*
  41. * Constants
  42. */
  43. /** @const */ var PRIMARY_INSTANCE_NAME = "mixpanel"
  44. /** @const */ , SET_QUEUE_KEY = "__mps"
  45. /** @const */ , SET_ONCE_QUEUE_KEY = "__mpso"
  46. /** @const */ , ADD_QUEUE_KEY = "__mpa"
  47. /** @const */ , APPEND_QUEUE_KEY = "__mpap"
  48. /** @const */ , SET_ACTION = "$set"
  49. /** @const */ , SET_ONCE_ACTION = "$set_once"
  50. /** @const */ , ADD_ACTION = "$add"
  51. /** @const */ , APPEND_ACTION = "$append"
  52. // This key is deprecated, but we want to check for it to see whether aliasing is allowed.
  53. /** @const */ , PEOPLE_DISTINCT_ID_KEY = "$people_distinct_id"
  54. /** @const */ , ALIAS_ID_KEY = "__alias"
  55. /** @const */ , RESERVED_PROPERTIES = [SET_QUEUE_KEY, SET_ONCE_QUEUE_KEY, ADD_QUEUE_KEY, APPEND_QUEUE_KEY, PEOPLE_DISTINCT_ID_KEY, ALIAS_ID_KEY];
  56. /*
  57. * Dynamic... constants? Is that an oxymoron?
  58. */
  59. var HTTP_PROTOCOL = (("https:" == document.location.protocol) ? "https://" : "http://")
  60. , SNIPPET_VERSION = (mixpanel && mixpanel['__SV']) || 0
  61. // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
  62. // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials
  63. , USE_XHR = (window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest())
  64. // IE<10 does not support cross-origin XHR's but script tags
  65. // with defer won't block window.onload; ENQUEUE_REQUESTS
  66. // should only be true for Opera<12
  67. , ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') == -1);
  68. /*
  69. * Closure-level globals
  70. */
  71. var _ = {}
  72. , DEBUG = false
  73. , DEFAULT_CONFIG = {
  74. "api_host": HTTP_PROTOCOL + 'api.mixpanel.com'
  75. , "cross_subdomain_cookie": true
  76. , "cookie_name": ""
  77. , "loaded": function() {}
  78. , "store_google": true
  79. , "save_referrer": true
  80. , "test": false
  81. , "verbose": false
  82. , "img": false
  83. , "track_pageview": true
  84. , "debug": false
  85. , "track_links_timeout": 300
  86. , "cookie_expiration": 365
  87. , "upgrade": false
  88. , "disable_cookie": false
  89. , "secure_cookie": false
  90. , "ip": true
  91. }
  92. , DOM_LOADED = false;
  93. // UNDERSCORE
  94. // Embed part of the Underscore Library
  95. (function() {
  96. var nativeForEach = ArrayProto.forEach,
  97. nativeIndexOf = ArrayProto.indexOf,
  98. nativeIsArray = Array.isArray,
  99. breaker = {};
  100. /**
  101. * @param {*=} obj
  102. * @param {function(...[*])=} iterator
  103. * @param {Object=} context
  104. */
  105. var each = _.each = function(obj, iterator, context) {
  106. if (obj == null) return;
  107. if (nativeForEach && obj.forEach === nativeForEach) {
  108. obj.forEach(iterator, context);
  109. } else if (obj.length === +obj.length) {
  110. for (var i = 0, l = obj.length; i < l; i++) {
  111. if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
  112. }
  113. } else {
  114. for (var key in obj) {
  115. if (hasOwnProperty.call(obj, key)) {
  116. if (iterator.call(context, obj[key], key, obj) === breaker) return;
  117. }
  118. }
  119. }
  120. };
  121. _.extend = function(obj) {
  122. each(slice.call(arguments, 1), function(source) {
  123. for (var prop in source) {
  124. if (source[prop] !== void 0) obj[prop] = source[prop];
  125. }
  126. });
  127. return obj;
  128. };
  129. _.isArray = nativeIsArray || function(obj) {
  130. return toString.call(obj) === '[object Array]';
  131. };
  132. // from a comment on http://dbj.org/dbj/?p=286
  133. // fails on only one very rare and deliberate custom object:
  134. // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
  135. _.isFunction = function (f) {
  136. try {
  137. return /^\s*\bfunction\b/.test(f);
  138. } catch (x) {
  139. return false;
  140. }
  141. };
  142. _.isArguments = function(obj) {
  143. return !!(obj && hasOwnProperty.call(obj, 'callee'));
  144. };
  145. _.toArray = function(iterable) {
  146. if (!iterable) return [];
  147. if (iterable.toArray) return iterable.toArray();
  148. if (_.isArray(iterable)) return slice.call(iterable);
  149. if (_.isArguments(iterable)) return slice.call(iterable);
  150. return _.values(iterable);
  151. };
  152. _.values = function(obj) {
  153. var results = [];
  154. if (obj == null) return results;
  155. each(obj, function(value) {
  156. results[results.length] = value;
  157. });
  158. return results;
  159. };
  160. _.identity = function(value) {
  161. return value;
  162. };
  163. _.include = function(obj, target) {
  164. var found = false;
  165. if (obj == null) return found;
  166. if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
  167. each(obj, function(value) {
  168. if (found || (found = (value === target))) { return breaker; }
  169. });
  170. return found;
  171. };
  172. _.includes = function(str, needle) {
  173. return str.indexOf(needle) !== -1;
  174. };
  175. })();
  176. // Underscore Addons
  177. _.inherit = function(subclass, superclass) {
  178. subclass.prototype = new superclass();
  179. subclass.prototype.constructor = subclass;
  180. subclass.superclass = superclass.prototype;
  181. return subclass;
  182. };
  183. _.isObject = function(obj) {
  184. return (obj === Object(obj) && !_.isArray(obj));
  185. };
  186. _.isEmptyObject = function(obj) {
  187. if (_.isObject(obj)) {
  188. for (var key in obj) {
  189. if (hasOwnProperty.call(obj, key)) {
  190. return false;
  191. }
  192. }
  193. return true;
  194. }
  195. return false;
  196. };
  197. _.isUndefined = function(obj) {
  198. return obj === void 0;
  199. };
  200. _.isString = function(obj) {
  201. return toString.call(obj) == '[object String]';
  202. };
  203. _.isDate = function(obj) {
  204. return toString.call(obj) == '[object Date]';
  205. };
  206. _.isNumber = function(obj) {
  207. return toString.call(obj) == '[object Number]';
  208. };
  209. _.encodeDates = function(obj) {
  210. _.each(obj, function(v, k) {
  211. if (_.isDate(v)) {
  212. obj[k] = _.formatDate(v);
  213. } else if (_.isObject(v)) {
  214. obj[k] = _.encodeDates(v); // recurse
  215. }
  216. });
  217. return obj;
  218. };
  219. _.formatDate = function(d) {
  220. // YYYY-MM-DDTHH:MM:SS in UTC
  221. function pad(n) {return n < 10 ? '0' + n : n}
  222. return d.getUTCFullYear() + '-'
  223. + pad(d.getUTCMonth() + 1) + '-'
  224. + pad(d.getUTCDate()) + 'T'
  225. + pad(d.getUTCHours()) + ':'
  226. + pad(d.getUTCMinutes()) + ':'
  227. + pad(d.getUTCSeconds());
  228. };
  229. _.strip_empty_properties = function(p) {
  230. var ret = {};
  231. _.each(p, function(v, k) {
  232. if (_.isString(v) && v.length > 0) { ret[k] = v; }
  233. });
  234. return ret;
  235. };
  236. /*
  237. * this function returns a copy of object after truncating it. If
  238. * passed an Array or Object it will iterate through obj and
  239. * truncate all the values recursively.
  240. */
  241. _.truncate = function(obj, length) {
  242. var ret;
  243. if (typeof(obj) === "string") {
  244. ret = obj.slice(0, length);
  245. } else if (_.isArray(obj)) {
  246. ret = [];
  247. _.each(obj, function(val) {
  248. ret.push(_.truncate(val, length));
  249. });
  250. } else if (_.isObject(obj)) {
  251. ret = {};
  252. _.each(obj, function(val, key) {
  253. ret[key] = _.truncate(val, length);
  254. });
  255. } else {
  256. ret = obj;
  257. }
  258. return ret;
  259. };
  260. _.JSONEncode = (function() {
  261. return function(mixed_val) {
  262. var indent;
  263. var value = mixed_val;
  264. var i;
  265. var quote = function (string) {
  266. var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
  267. var meta = { // table of character substitutions
  268. '\b': '\\b',
  269. '\t': '\\t',
  270. '\n': '\\n',
  271. '\f': '\\f',
  272. '\r': '\\r',
  273. '"' : '\\"',
  274. '\\': '\\\\'
  275. };
  276. escapable.lastIndex = 0;
  277. return escapable.test(string) ?
  278. '"' + string.replace(escapable, function (a) {
  279. var c = meta[a];
  280. return typeof c === 'string' ? c :
  281. '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
  282. }) + '"' :
  283. '"' + string + '"';
  284. };
  285. var str = function(key, holder) {
  286. var gap = '';
  287. var indent = ' ';
  288. var i = 0; // The loop counter.
  289. var k = ''; // The member key.
  290. var v = ''; // The member value.
  291. var length = 0;
  292. var mind = gap;
  293. var partial = [];
  294. var value = holder[key];
  295. // If the value has a toJSON method, call it to obtain a replacement value.
  296. if (value && typeof value === 'object' &&
  297. typeof value.toJSON === 'function') {
  298. value = value.toJSON(key);
  299. }
  300. // What happens next depends on the value's type.
  301. switch (typeof value) {
  302. case 'string':
  303. return quote(value);
  304. case 'number':
  305. // JSON numbers must be finite. Encode non-finite numbers as null.
  306. return isFinite(value) ? String(value) : 'null';
  307. case 'boolean':
  308. case 'null':
  309. // If the value is a boolean or null, convert it to a string. Note:
  310. // typeof null does not produce 'null'. The case is included here in
  311. // the remote chance that this gets fixed someday.
  312. return String(value);
  313. case 'object':
  314. // If the type is 'object', we might be dealing with an object or an array or
  315. // null.
  316. // Due to a specification blunder in ECMAScript, typeof null is 'object',
  317. // so watch out for that case.
  318. if (!value) {
  319. return 'null';
  320. }
  321. // Make an array to hold the partial results of stringifying this object value.
  322. gap += indent;
  323. partial = [];
  324. // Is the value an array?
  325. if (toString.apply(value) === '[object Array]') {
  326. // The value is an array. Stringify every element. Use null as a placeholder
  327. // for non-JSON values.
  328. length = value.length;
  329. for (i = 0; i < length; i += 1) {
  330. partial[i] = str(i, value) || 'null';
  331. }
  332. // Join all of the elements together, separated with commas, and wrap them in
  333. // brackets.
  334. v = partial.length === 0 ? '[]' :
  335. gap ? '[\n' + gap +
  336. partial.join(',\n' + gap) + '\n' +
  337. mind + ']' :
  338. '[' + partial.join(',') + ']';
  339. gap = mind;
  340. return v;
  341. }
  342. // Iterate through all of the keys in the object.
  343. for (k in value) {
  344. if (hasOwnProperty.call(value, k)) {
  345. v = str(k, value);
  346. if (v) {
  347. partial.push(quote(k) + (gap ? ': ' : ':') + v);
  348. }
  349. }
  350. }
  351. // Join all of the member texts together, separated with commas,
  352. // and wrap them in braces.
  353. v = partial.length === 0 ? '{}' :
  354. gap ? '{' + partial.join(',') + '' +
  355. mind + '}' : '{' + partial.join(',') + '}';
  356. gap = mind;
  357. return v;
  358. }
  359. };
  360. // Make a fake root object containing our value under the key of ''.
  361. // Return the result of stringifying the value.
  362. return str('', {
  363. '': value
  364. });
  365. };
  366. })();
  367. _.JSONDecode = (function() { // https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
  368. var at, // The index of the current character
  369. ch, // The current character
  370. escapee = {
  371. '"': '"',
  372. '\\': '\\',
  373. '/': '/',
  374. 'b': '\b',
  375. 'f': '\f',
  376. 'n': '\n',
  377. 'r': '\r',
  378. 't': '\t'
  379. },
  380. text,
  381. error = function (m) {
  382. throw {
  383. name: 'SyntaxError',
  384. message: m,
  385. at: at,
  386. text: text
  387. };
  388. },
  389. next = function (c) {
  390. // If a c parameter is provided, verify that it matches the current character.
  391. if (c && c !== ch) {
  392. error("Expected '" + c + "' instead of '" + ch + "'");
  393. }
  394. // Get the next character. When there are no more characters,
  395. // return the empty string.
  396. ch = text.charAt(at);
  397. at += 1;
  398. return ch;
  399. },
  400. number = function () {
  401. // Parse a number value.
  402. var number,
  403. string = '';
  404. if (ch === '-') {
  405. string = '-';
  406. next('-');
  407. }
  408. while (ch >= '0' && ch <= '9') {
  409. string += ch;
  410. next();
  411. }
  412. if (ch === '.') {
  413. string += '.';
  414. while (next() && ch >= '0' && ch <= '9') {
  415. string += ch;
  416. }
  417. }
  418. if (ch === 'e' || ch === 'E') {
  419. string += ch;
  420. next();
  421. if (ch === '-' || ch === '+') {
  422. string += ch;
  423. next();
  424. }
  425. while (ch >= '0' && ch <= '9') {
  426. string += ch;
  427. next();
  428. }
  429. }
  430. number = +string;
  431. if (!isFinite(number)) {
  432. error("Bad number");
  433. } else {
  434. return number;
  435. }
  436. },
  437. string = function () {
  438. // Parse a string value.
  439. var hex,
  440. i,
  441. string = '',
  442. uffff;
  443. // When parsing for string values, we must look for " and \ characters.
  444. if (ch === '"') {
  445. while (next()) {
  446. if (ch === '"') {
  447. next();
  448. return string;
  449. }
  450. if (ch === '\\') {
  451. next();
  452. if (ch === 'u') {
  453. uffff = 0;
  454. for (i = 0; i < 4; i += 1) {
  455. hex = parseInt(next(), 16);
  456. if (!isFinite(hex)) {
  457. break;
  458. }
  459. uffff = uffff * 16 + hex;
  460. }
  461. string += String.fromCharCode(uffff);
  462. } else if (typeof escapee[ch] === 'string') {
  463. string += escapee[ch];
  464. } else {
  465. break;
  466. }
  467. } else {
  468. string += ch;
  469. }
  470. }
  471. }
  472. error("Bad string");
  473. },
  474. white = function () {
  475. // Skip whitespace.
  476. while (ch && ch <= ' ') {
  477. next();
  478. }
  479. },
  480. word = function () {
  481. // true, false, or null.
  482. switch (ch) {
  483. case 't':
  484. next('t');
  485. next('r');
  486. next('u');
  487. next('e');
  488. return true;
  489. case 'f':
  490. next('f');
  491. next('a');
  492. next('l');
  493. next('s');
  494. next('e');
  495. return false;
  496. case 'n':
  497. next('n');
  498. next('u');
  499. next('l');
  500. next('l');
  501. return null;
  502. }
  503. error("Unexpected '" + ch + "'");
  504. },
  505. value, // Placeholder for the value function.
  506. array = function () {
  507. // Parse an array value.
  508. var array = [];
  509. if (ch === '[') {
  510. next('[');
  511. white();
  512. if (ch === ']') {
  513. next(']');
  514. return array; // empty array
  515. }
  516. while (ch) {
  517. array.push(value());
  518. white();
  519. if (ch === ']') {
  520. next(']');
  521. return array;
  522. }
  523. next(',');
  524. white();
  525. }
  526. }
  527. error("Bad array");
  528. },
  529. object = function () {
  530. // Parse an object value.
  531. var key,
  532. object = {};
  533. if (ch === '{') {
  534. next('{');
  535. white();
  536. if (ch === '}') {
  537. next('}');
  538. return object; // empty object
  539. }
  540. while (ch) {
  541. key = string();
  542. white();
  543. next(':');
  544. if (Object.hasOwnProperty.call(object, key)) {
  545. error('Duplicate key "' + key + '"');
  546. }
  547. object[key] = value();
  548. white();
  549. if (ch === '}') {
  550. next('}');
  551. return object;
  552. }
  553. next(',');
  554. white();
  555. }
  556. }
  557. error("Bad object");
  558. };
  559. value = function () {
  560. // Parse a JSON value. It could be an object, an array, a string,
  561. // a number, or a word.
  562. white();
  563. switch (ch) {
  564. case '{':
  565. return object();
  566. case '[':
  567. return array();
  568. case '"':
  569. return string();
  570. case '-':
  571. return number();
  572. default:
  573. return ch >= '0' && ch <= '9' ? number() : word();
  574. }
  575. };
  576. // Return the json_parse function. It will have access to all of the
  577. // above functions and variables.
  578. return function (source) {
  579. var result;
  580. text = source;
  581. at = 0;
  582. ch = ' ';
  583. result = value();
  584. white();
  585. if (ch) {
  586. error("Syntax error");
  587. }
  588. return result;
  589. };
  590. })();
  591. _.base64Encode = function(data) {
  592. var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  593. var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc="", tmp_arr = [];
  594. if (!data) {
  595. return data;
  596. }
  597. data = _.utf8Encode(data);
  598. do { // pack three octets into four hexets
  599. o1 = data.charCodeAt(i++);
  600. o2 = data.charCodeAt(i++);
  601. o3 = data.charCodeAt(i++);
  602. bits = o1<<16 | o2<<8 | o3;
  603. h1 = bits>>18 & 0x3f;
  604. h2 = bits>>12 & 0x3f;
  605. h3 = bits>>6 & 0x3f;
  606. h4 = bits & 0x3f;
  607. // use hexets to index into b64, and append result to encoded string
  608. tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
  609. } while (i < data.length);
  610. enc = tmp_arr.join('');
  611. switch( data.length % 3 ){
  612. case 1:
  613. enc = enc.slice(0, -2) + '==';
  614. break;
  615. case 2:
  616. enc = enc.slice(0, -1) + '=';
  617. break;
  618. }
  619. return enc;
  620. };
  621. _.utf8Encode = function(string) {
  622. string = (string+'').replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  623. var utftext = "",
  624. start,
  625. end;
  626. var stringl = 0,
  627. n;
  628. start = end = 0;
  629. stringl = string.length;
  630. for (n = 0; n < stringl; n++) {
  631. var c1 = string.charCodeAt(n);
  632. var enc = null;
  633. if (c1 < 128) {
  634. end++;
  635. } else if((c1 > 127) && (c1 < 2048)) {
  636. enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128);
  637. } else {
  638. enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128);
  639. }
  640. if (enc !== null) {
  641. if (end > start) {
  642. utftext += string.substring(start, end);
  643. }
  644. utftext += enc;
  645. start = end = n+1;
  646. }
  647. }
  648. if (end > start) {
  649. utftext += string.substring(start, string.length);
  650. }
  651. return utftext;
  652. };
  653. _.UUID = (function() {
  654. // Time/ticks information
  655. // 1*new Date() is a cross browser version of Date.now()
  656. var T = function() {
  657. var d = 1*new Date()
  658. , i = 0;
  659. // this while loop figures how many browser ticks go by
  660. // before 1*new Date() returns a new number, ie the amount
  661. // of ticks that go by per millisecond
  662. while (d == 1*new Date()) { i++; }
  663. return d.toString(16) + i.toString(16);
  664. };
  665. // Math.Random entropy
  666. var R = function() {
  667. return Math.random().toString(16).replace('.','');
  668. };
  669. // User agent entropy
  670. // This function takes the user agent string, and then xors
  671. // together each sequence of 8 bytes. This produces a final
  672. // sequence of 8 bytes which it returns as hex.
  673. var UA = function(n) {
  674. var ua = userAgent, i, ch, buffer = [], ret = 0;
  675. function xor(result, byte_array) {
  676. var j, tmp = 0;
  677. for (j = 0; j < byte_array.length; j++) {
  678. tmp |= (buffer[j] << j*8);
  679. }
  680. return result ^ tmp;
  681. }
  682. for (i = 0; i < ua.length; i++) {
  683. ch = ua.charCodeAt(i);
  684. buffer.unshift(ch & 0xFF);
  685. if (buffer.length >= 4) {
  686. ret = xor(ret, buffer);
  687. buffer = [];
  688. }
  689. }
  690. if (buffer.length > 0) { ret = xor(ret, buffer); }
  691. return ret.toString(16);
  692. };
  693. return function() {
  694. var se = (screen.height*screen.width).toString(16);
  695. return (T()+"-"+R()+"-"+UA()+"-"+se+"-"+T());
  696. };
  697. })();
  698. // _.isBlockedUA()
  699. // This is to block various web spiders from executing our JS and
  700. // sending false tracking data
  701. _.isBlockedUA = function() {
  702. var a = userAgent;
  703. if (/(google web preview|baiduspider|yandexbot)/i.test(a)) {
  704. return true;
  705. }
  706. return false;
  707. };
  708. /**
  709. * @param {Object=} formdata
  710. * @param {string=} arg_separator
  711. */
  712. _.HTTPBuildQuery = function(formdata, arg_separator) {
  713. var key, use_val, use_key, tmp_arr = [];
  714. if (typeof(arg_separator) === "undefined") {
  715. arg_separator = '&';
  716. }
  717. _.each(formdata, function(val, key) {
  718. use_val = encodeURIComponent(val.toString());
  719. use_key = encodeURIComponent(key);
  720. tmp_arr[tmp_arr.length] = use_key + '=' + use_val;
  721. });
  722. return tmp_arr.join(arg_separator);
  723. };
  724. _.getQueryParam = function(url, param) {
  725. // Expects a raw URL
  726. param = param.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
  727. var regexS = "[\\?&]" + param + "=([^&#]*)",
  728. regex = new RegExp( regexS ),
  729. results = regex.exec(url);
  730. if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) {
  731. return '';
  732. } else {
  733. return decodeURIComponent(results[1]).replace(/\+/g, ' ');
  734. }
  735. };
  736. // _.cookie
  737. // Methods partially borrowed from quirksmode.org/js/cookies.html
  738. _.cookie = {
  739. get: function(name) {
  740. var nameEQ = name + "=";
  741. var ca = document.cookie.split(';');
  742. for(var i=0;i < ca.length;i++) {
  743. var c = ca[i];
  744. while (c.charAt(0)==' ') c = c.substring(1,c.length);
  745. if (c.indexOf(nameEQ) == 0) return decodeURIComponent(c.substring(nameEQ.length,c.length));
  746. }
  747. return null;
  748. },
  749. parse: function(name) {
  750. var cookie;
  751. try {
  752. cookie = _.JSONDecode(_.cookie.get(name)) || {};
  753. } catch (err) {}
  754. return cookie;
  755. },
  756. set: function(name, value, days, cross_subdomain, is_secure) {
  757. var cdomain = "", expires = "", secure = "";
  758. if (cross_subdomain) {
  759. var matches = document.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i)
  760. , domain = matches ? matches[0] : '';
  761. cdomain = ((domain) ? "; domain=." + domain : "");
  762. }
  763. if (days) {
  764. var date = new Date();
  765. date.setTime(date.getTime()+(days*24*60*60*1000));
  766. expires = "; expires=" + date.toGMTString();
  767. }
  768. if (is_secure) {
  769. secure = "; secure";
  770. }
  771. document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/" + cdomain + secure;
  772. },
  773. remove: function(name, cross_subdomain) {
  774. _.cookie.set(name, '', -1, cross_subdomain);
  775. }
  776. };
  777. _.register_event = (function() {
  778. // written by Dean Edwards, 2005
  779. // with input from Tino Zijdel - crisp@xs4all.nl
  780. // with input from Carl Sverre - mail@carlsverre.com
  781. // with input from Mixpanel
  782. // http://dean.edwards.name/weblog/2005/10/add-event/
  783. // https://gist.github.com/1930440
  784. /**
  785. * @param {Object} element
  786. * @param {string} type
  787. * @param {function(...[*])} handler
  788. * @param {boolean=} oldSchool
  789. */
  790. var register_event = function(element, type, handler, oldSchool) {
  791. if (!element) {
  792. console.error("No valid element provided to register_event");
  793. return;
  794. }
  795. if (element.addEventListener && !oldSchool) {
  796. element.addEventListener(type, handler, false);
  797. } else {
  798. var ontype = 'on' + type;
  799. var old_handler = element[ontype]; // can be undefined
  800. element[ontype] = makeHandler(element, handler, old_handler);
  801. }
  802. };
  803. function makeHandler(element, new_handler, old_handlers) {
  804. var handler = function(event) {
  805. event = event || fixEvent(window.event);
  806. // this basically happens in firefox whenever another script
  807. // overwrites the onload callback and doesn't pass the event
  808. // object to previously defined callbacks. All the browsers
  809. // that don't define window.event implement addEventListener
  810. // so the dom_loaded handler will still be fired as usual.
  811. if (!event) { return undefined; }
  812. var ret = true;
  813. var old_result, new_result;
  814. if (_.isFunction(old_handlers)) {
  815. old_result = old_handlers(event);
  816. }
  817. new_result = new_handler.call(element, event);
  818. if ((false === old_result) || (false === new_result)) {
  819. ret = false;
  820. }
  821. return ret;
  822. };
  823. return handler;
  824. };
  825. function fixEvent(event) {
  826. if (event) {
  827. event.preventDefault = fixEvent.preventDefault;
  828. event.stopPropagation = fixEvent.stopPropagation;
  829. }
  830. return event;
  831. };
  832. fixEvent.preventDefault = function() {
  833. this.returnValue = false;
  834. };
  835. fixEvent.stopPropagation = function() {
  836. this.cancelBubble = true;
  837. };
  838. return register_event;
  839. })();
  840. _.dom_query = (function() {
  841. /* document.getElementsBySelector(selector)
  842. - returns an array of element objects from the current document
  843. matching the CSS selector. Selectors can contain element names,
  844. class names and ids and can be nested. For example:
  845. elements = document.getElementsBySelector('div#main p a.external')
  846. Will return an array of all 'a' elements with 'external' in their
  847. class attribute that are contained inside 'p' elements that are
  848. contained inside the 'div' element which has id="main"
  849. New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
  850. See http://www.w3.org/TR/css3-selectors/#attribute-selectors
  851. Version 0.4 - Simon Willison, March 25th 2003
  852. -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
  853. -- Opera 7 fails
  854. Version 0.5 - Carl Sverre, Jan 7th 2013
  855. -- Now uses jQuery-esque `hasClass` for testing class name
  856. equality. This fixes a bug related to '-' characters being
  857. considered not part of a 'word' in regex.
  858. */
  859. function getAllChildren(e) {
  860. // Returns all children of element. Workaround required for IE5/Windows. Ugh.
  861. return e.all ? e.all : e.getElementsByTagName('*');
  862. }
  863. var bad_whitespace = /[\t\r\n]/g;
  864. function hasClass(elem, selector) {
  865. var className = " " + selector + " ";
  866. return ((" " + elem.className + " ").replace(bad_whitespace, " ").indexOf(className) >= 0);
  867. }
  868. function getElementsBySelector(selector) {
  869. // Attempt to fail gracefully in lesser browsers
  870. if (!document.getElementsByTagName) {
  871. return new Array();
  872. }
  873. // Split selector in to tokens
  874. var tokens = selector.split(' ');
  875. var token;
  876. var currentContext = new Array(document);
  877. for (var i = 0; i < tokens.length; i++) {
  878. token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');
  879. if (token.indexOf('#') > -1) {
  880. // Token is an ID selector
  881. var bits = token.split('#');
  882. var tagName = bits[0];
  883. var id = bits[1];
  884. var element = document.getElementById(id);
  885. if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
  886. // element not found or tag with that ID not found, return false
  887. return new Array();
  888. }
  889. // Set currentContext to contain just this element
  890. currentContext = new Array(element);
  891. continue; // Skip to next token
  892. }
  893. if (token.indexOf('.') > -1) {
  894. // Token contains a class selector
  895. var bits = token.split('.');
  896. var tagName = bits[0];
  897. var className = bits[1];
  898. if (!tagName) {
  899. tagName = '*';
  900. }
  901. // Get elements matching tag, filter them for class selector
  902. var found = new Array;
  903. var foundCount = 0;
  904. for (var h = 0; h < currentContext.length; h++) {
  905. var elements;
  906. if (tagName == '*') {
  907. elements = getAllChildren(currentContext[h]);
  908. } else {
  909. elements = currentContext[h].getElementsByTagName(tagName);
  910. }
  911. for (var j = 0; j < elements.length; j++) {
  912. found[foundCount++] = elements[j];
  913. }
  914. }
  915. currentContext = new Array;
  916. var currentContextIndex = 0;
  917. for (var k = 0; k < found.length; k++) {
  918. if (found[k].className
  919. && _.isString(found[k].className) // some SVG elements have classNames which are not strings
  920. && hasClass(found[k], className)
  921. ) {
  922. currentContext[currentContextIndex++] = found[k];
  923. }
  924. }
  925. continue; // Skip to next token
  926. }
  927. // Code to deal with attribute selectors
  928. if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
  929. var tagName = RegExp.$1;
  930. var attrName = RegExp.$2;
  931. var attrOperator = RegExp.$3;
  932. var attrValue = RegExp.$4;
  933. if (!tagName) {
  934. tagName = '*';
  935. }
  936. // Grab all of the tagName elements within current context
  937. var found = new Array;
  938. var foundCount = 0;
  939. for (var h = 0; h < currentContext.length; h++) {
  940. var elements;
  941. if (tagName == '*') {
  942. elements = getAllChildren(currentContext[h]);
  943. } else {
  944. elements = currentContext[h].getElementsByTagName(tagName);
  945. }
  946. for (var j = 0; j < elements.length; j++) {
  947. found[foundCount++] = elements[j];
  948. }
  949. }
  950. currentContext = new Array;
  951. var currentContextIndex = 0;
  952. var checkFunction; // This function will be used to filter the elements
  953. switch (attrOperator) {
  954. case '=': // Equality
  955. checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
  956. break;
  957. case '~': // Match one of space seperated words
  958. checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
  959. break;
  960. case '|': // Match start with value followed by optional hyphen
  961. checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
  962. break;
  963. case '^': // Match starts with value
  964. checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
  965. break;
  966. case '$': // Match ends with value - fails with "Warning" in Opera 7
  967. checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
  968. break;
  969. case '*': // Match ends with value
  970. checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
  971. break;
  972. default :
  973. // Just test for existence of attribute
  974. checkFunction = function(e) { return e.getAttribute(attrName); };
  975. }
  976. currentContext = new Array;
  977. currentContextIndex = 0;
  978. for (var k = 0; k < found.length; k++) {
  979. if (checkFunction(found[k])) {
  980. currentContext[currentContextIndex++] = found[k];
  981. }
  982. }
  983. // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
  984. continue; // Skip to next token
  985. }
  986. // If we get here, token is JUST an element (not a class or ID selector)
  987. tagName = token;
  988. var found = new Array;
  989. var foundCount = 0;
  990. for (var h = 0; h < currentContext.length; h++) {
  991. var elements = currentContext[h].getElementsByTagName(tagName);
  992. for (var j = 0; j < elements.length; j++) {
  993. found[foundCount++] = elements[j];
  994. }
  995. }
  996. currentContext = found;
  997. }
  998. return currentContext;
  999. };
  1000. return getElementsBySelector;
  1001. })();
  1002. _.info = {
  1003. campaignParams: function() {
  1004. var campaign_keywords = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' ')
  1005. , kw = ''
  1006. , params = {};
  1007. _.each(campaign_keywords, function(kwkey) {
  1008. kw = _.getQueryParam(document.URL, kwkey);
  1009. if (kw.length) {
  1010. params[kwkey] = kw;
  1011. }
  1012. });
  1013. return params;
  1014. },
  1015. searchEngine: function(referrer) {
  1016. if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
  1017. return 'google';
  1018. } else if (referrer.search('https?://(.*)bing.com') === 0) {
  1019. return 'bing';
  1020. } else if (referrer.search('https?://(.*)yahoo.com') === 0) {
  1021. return 'yahoo';
  1022. } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) {
  1023. return 'duckduckgo';
  1024. } else {
  1025. return null;
  1026. }
  1027. },
  1028. searchInfo: function(referrer) {
  1029. var search = _.info.searchEngine(referrer)
  1030. , param = (search != "yahoo") ? "q" : "p"
  1031. , ret = {};
  1032. if (search !== null) {
  1033. ret["$search_engine"] = search;
  1034. var keyword = _.getQueryParam(referrer, param);
  1035. if (keyword.length) {
  1036. ret["mp_keyword"] = keyword;
  1037. }
  1038. }
  1039. return ret;
  1040. },
  1041. /**
  1042. * This function detects which browser is running this script.
  1043. * The order of the checks are important since many user agents
  1044. * include key words used in later checks.
  1045. */
  1046. browser: function() {
  1047. var ua = userAgent
  1048. , vend = navigator.vendor || ''; // vendor is undefined for at least IE9
  1049. if (window.opera) {
  1050. if (_.includes(ua, "Mini")) {
  1051. return "Opera Mini";
  1052. }
  1053. return "Opera";
  1054. } else if (/(BlackBerry|PlayBook|BB10)/i.test(ua)) {
  1055. return 'BlackBerry';
  1056. } else if (_.includes(ua, "Chrome")) {
  1057. return "Chrome";
  1058. } else if (_.includes(vend, "Apple")) {
  1059. if (_.includes(ua, "Mobile")) {
  1060. return "Mobile Safari";
  1061. }
  1062. return "Safari";
  1063. } else if (_.includes(ua, "Android")) {
  1064. return "Android Mobile";
  1065. } else if (_.includes(ua, "Konqueror")) {
  1066. return "Konqueror";
  1067. } else if (_.includes(ua, "Firefox")) {
  1068. return "Firefox";
  1069. } else if (_.includes(ua, "MSIE")) {
  1070. return "Internet Explorer";
  1071. } else if (_.includes(ua, "Gecko")) {
  1072. return "Mozilla";
  1073. } else {
  1074. return "";
  1075. }
  1076. },
  1077. os: function() {
  1078. var a = userAgent;
  1079. if (/Windows/i.test(a)) {
  1080. if (/Phone/.test(a)) { return 'Windows Mobile'; }
  1081. return 'Windows';
  1082. } else if (/(iPhone|iPad|iPod)/.test(a)) {
  1083. return 'iOS';
  1084. } else if (/Android/.test(a)) {
  1085. return 'Android';
  1086. } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) {
  1087. return 'BlackBerry';
  1088. } else if (/Mac/i.test(a)) {
  1089. return 'Mac OS X';
  1090. } else if (/Linux/.test(a)) {
  1091. return 'Linux';
  1092. } else {
  1093. return '';
  1094. }
  1095. },
  1096. device: function() {
  1097. var a = userAgent;
  1098. if (/iPhone/.test(a)) {
  1099. return 'iPhone';
  1100. } else if (/iPad/.test(a)) {
  1101. return 'iPad';
  1102. } else if (/iPod/.test(a)) {
  1103. return 'iPod Touch';
  1104. } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) {
  1105. return 'BlackBerry';
  1106. } else if (/Windows Phone/i.test(a)) {
  1107. return 'Windows Phone';
  1108. } else if (/Android/.test(a)) {
  1109. return 'Android';
  1110. } else {
  1111. return '';
  1112. }
  1113. },
  1114. referringDomain: function(referrer) {
  1115. var split = referrer.split("/");
  1116. if (split.length >= 3) {
  1117. return split[2];
  1118. }
  1119. return "";
  1120. },
  1121. properties: function() {
  1122. return _.strip_empty_properties({
  1123. '$os': _.info.os(),
  1124. '$browser': _.info.browser(),
  1125. '$referrer': document.referrer,
  1126. '$referring_domain': _.info.referringDomain(document.referrer),
  1127. '$device': _.info.device(),
  1128. 'mp_lib': 'web'
  1129. });
  1130. },
  1131. people_properties: function() {
  1132. return _.strip_empty_properties({
  1133. '$os': _.info.os(),
  1134. '$browser': _.info.browser()
  1135. });
  1136. },
  1137. pageviewInfo: function(page) {
  1138. return _.strip_empty_properties({
  1139. 'mp_page': page
  1140. , 'mp_referrer': document.referrer
  1141. , 'mp_browser': _.info.browser()
  1142. , 'mp_platform': _.info.os()
  1143. });
  1144. }
  1145. };
  1146. // Console override
  1147. var console = {
  1148. /** @type {function(...[*])} */
  1149. log: function() {
  1150. if (DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
  1151. try {
  1152. windowConsole.log.apply(windowConsole, arguments);
  1153. } catch(err) {
  1154. _.each(arguments, function(arg) {
  1155. windowConsole.log(arg);
  1156. });
  1157. }
  1158. }
  1159. },
  1160. /** @type {function(...[*])} */
  1161. error: function() {
  1162. if (DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
  1163. var args = ["Mixpanel error:"].concat(_.toArray(arguments));
  1164. try {
  1165. windowConsole.error.apply(windowConsole, args);
  1166. } catch(err) {
  1167. _.each(args, function(arg) {
  1168. windowConsole.error(arg);
  1169. });
  1170. }
  1171. }
  1172. },
  1173. /** @type {function(...[*])} */
  1174. critical: function() {
  1175. if (!_.isUndefined(windowConsole) && windowConsole) {
  1176. var args = ["Mixpanel error:"].concat(_.toArray(arguments));
  1177. try {
  1178. windowConsole.error.apply(windowConsole, args);
  1179. } catch(err) {
  1180. _.each(args, function(arg) {
  1181. windowConsole.error(arg);
  1182. });
  1183. }
  1184. }
  1185. }
  1186. };
  1187. /**
  1188. * DomTracker Object
  1189. * @constructor
  1190. */
  1191. var DomTracker = function() {};
  1192. // interface
  1193. DomTracker.prototype.create_properties = function() {};
  1194. DomTracker.prototype.event_handler = function() {};
  1195. DomTracker.prototype.after_track_handler = function() {};
  1196. DomTracker.prototype.init = function(mixpanel_instance) {
  1197. this.mp = mixpanel_instance;
  1198. return this;
  1199. };
  1200. /**
  1201. * @param {string} query
  1202. * @param {string} event_name
  1203. * @param {Object=} properties
  1204. * @param {function(...[*])=} user_callback
  1205. */
  1206. DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
  1207. var that = this
  1208. , elements = _.dom_query(query);
  1209. if (elements.length == 0) {
  1210. console.error("The DOM query (" + query + ") returned 0 elements");
  1211. return;
  1212. }
  1213. _.each(elements, function(element) {
  1214. _.register_event(element, this.override_event, function(e) {
  1215. var options = {}
  1216. , props = that.create_properties(properties, this)
  1217. , timeout = that.mp.get_config("track_links_timeout");
  1218. that.event_handler(e, this, options);
  1219. // in case the mixpanel servers don't get back to us in time
  1220. window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
  1221. // fire the tracking event
  1222. that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
  1223. });
  1224. }, this);
  1225. return true;
  1226. };
  1227. /**
  1228. * @param {function(...[*])} user_callback
  1229. * @param {Object} props
  1230. * @param {boolean=} timeout_occured
  1231. */
  1232. DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
  1233. timeout_occured = timeout_occured || false;
  1234. var that = this;
  1235. return function() {
  1236. // options is referenced from both callbacks, so we can have
  1237. // a "lock" of sorts to ensure only one fires
  1238. if (options.callback_fired) { return; }
  1239. options.callback_fired = true;
  1240. if (user_callback && user_callback(timeout_occured, props) === false) {
  1241. // user can prevent the default functionality by
  1242. // returning false from their callback
  1243. return;
  1244. }
  1245. that.after_track_handler(props, options, timeout_occured);
  1246. };
  1247. };
  1248. DomTracker.prototype.create_properties = function(properties, element) {
  1249. var props;
  1250. if (typeof(properties) === "function") {
  1251. props = properties(element);
  1252. } else {
  1253. props = _.extend({}, properties);
  1254. }
  1255. return props;
  1256. };
  1257. /**
  1258. * LinkTracker Object
  1259. * @constructor
  1260. * @extends DomTracker
  1261. */
  1262. var LinkTracker = function() {
  1263. this.override_event = "click";
  1264. };
  1265. _.inherit(LinkTracker, DomTracker);
  1266. LinkTracker.prototype.create_properties = function(properties, element) {
  1267. var props = LinkTracker.superclass.create_properties.apply(this, arguments);
  1268. if (element.href) { props["url"] = element.href; }
  1269. return props;
  1270. };
  1271. LinkTracker.prototype.event_handler = function(evt, element, options) {
  1272. options.new_tab = (evt.which === 2 || evt.metaKey || element.target === "_blank");
  1273. options.href = element.href;
  1274. if (!options.new_tab) {
  1275. evt.preventDefault();
  1276. }
  1277. };
  1278. LinkTracker.prototype.after_track_handler = function(props, options, timeout_occured) {
  1279. if (options.new_tab) { return; }
  1280. setTimeout(function() {
  1281. window.location = options.href;
  1282. }, 0);
  1283. };
  1284. /**
  1285. * FormTracker Object
  1286. * @constructor
  1287. * @extends DomTracker
  1288. */
  1289. var FormTracker = function() {
  1290. this.override_event = "submit";
  1291. };
  1292. _.inherit(FormTracker, DomTracker);
  1293. FormTracker.prototype.event_handler = function(evt, element, options) {
  1294. options.element = element;
  1295. evt.preventDefault();
  1296. };
  1297. FormTracker.prototype.after_track_handler = function(props, options, timeout_occured) {
  1298. setTimeout(function() {
  1299. options.element.submit();
  1300. }, 0);
  1301. };
  1302. /**
  1303. * Mixpanel Cookie Object
  1304. * @constructor
  1305. */
  1306. var MixpanelCookie = function(config) {
  1307. this['props'] = {};
  1308. this.campaign_params_saved = false;
  1309. if (config['cookie_name']) {
  1310. this.name = "mp_" + config['cookie_name'];
  1311. } else {
  1312. this.name = "mp_" + config['token'] + "_mixpanel";
  1313. }
  1314. this.load();
  1315. this.update_config(config);
  1316. this.upgrade(config);
  1317. this.save();
  1318. };
  1319. MixpanelCookie.prototype.properties = function() {
  1320. var p = {};
  1321. // Filter out reserved properties
  1322. _.each(this['props'], function(v, k) {
  1323. if (!_.include(RESERVED_PROPERTIES, k)) {
  1324. p[k] = v;
  1325. }
  1326. });
  1327. return p;
  1328. };
  1329. MixpanelCookie.prototype.load = function() {
  1330. if (this.disabled) { return; }
  1331. var cookie = _.cookie.parse(this.name);
  1332. if (cookie) {
  1333. this['props'] = _.extend({}, cookie);
  1334. }
  1335. };
  1336. MixpanelCookie.prototype.upgrade = function(config) {
  1337. var should_upgrade = config['upgrade'],
  1338. old_cookie_name,
  1339. old_cookie;
  1340. if (should_upgrade) {
  1341. old_cookie_name = "mp_super_properties";
  1342. // Case where they had a custom cookie name before.
  1343. if (typeof(should_upgrade) === "string") {
  1344. old_cookie_name = should_upgrade;
  1345. }
  1346. old_cookie = _.cookie.parse(old_cookie_name);
  1347. // remove the cookie
  1348. _.cookie.remove(old_cookie_name);
  1349. _.cookie.remove(old_cookie_name, true);
  1350. if (old_cookie) {
  1351. this['props'] = _.extend(
  1352. this['props'],
  1353. old_cookie['all'],
  1354. old_cookie['events']
  1355. );
  1356. }
  1357. }
  1358. if (!config['cookie_name'] && config['name'] !== 'mixpanel') {
  1359. // special case to handle people with cookies of the form
  1360. // mp_TOKEN_INSTANCENAME from the first release of this library
  1361. old_cookie_name = "mp_" + config['token'] + "_" + config['name'];
  1362. old_cookie = _.cookie.parse(old_cookie_name);
  1363. if (old_cookie) {
  1364. _.cookie.remove(old_cookie_name);
  1365. _.cookie.remove(old_cookie_name, true);
  1366. // Save the prop values that were in the cookie from before -
  1367. // this should only happen once as we delete the old one.
  1368. this.register_once(old_cookie);
  1369. }
  1370. }
  1371. };
  1372. MixpanelCookie.prototype.save = function() {
  1373. if (this.disabled) { return; }
  1374. _.cookie.set(
  1375. this.name,
  1376. _.JSONEncode(this['props']),
  1377. this.expire_days,
  1378. this.cross_subdomain,
  1379. this.secure
  1380. );
  1381. };
  1382. MixpanelCookie.prototype.remove = function() {
  1383. // remove both domain and subdomain cookies
  1384. _.cookie.remove(this.name, false);
  1385. _.cookie.remove(this.name, true);
  1386. };
  1387. // removes the cookie and deletes all loaded data
  1388. // forced name for tests
  1389. MixpanelCookie.prototype.clear = function() {
  1390. this.remove();
  1391. this['props'] = {};
  1392. };
  1393. /**
  1394. * @param {Object} props
  1395. * @param {*=} default_value
  1396. * @param {number=} days
  1397. */
  1398. MixpanelCookie.prototype.register_once = function(props, default_value, days) {
  1399. if (_.isObject(props)) {
  1400. if (typeof(default_value) === 'undefined') { default_value = "None"; }
  1401. this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
  1402. _.each(props, function(val, prop) {
  1403. if (!this['props'][prop] || this['props'][prop] === default_value) {
  1404. this['props'][prop] = val;
  1405. }
  1406. }, this);
  1407. this.save();
  1408. return true;
  1409. }
  1410. return false;
  1411. };
  1412. /**
  1413. * @param {Object} props
  1414. * @param {number=} days
  1415. */
  1416. MixpanelCookie.prototype.register = function(props, days) {
  1417. if (_.isObject(props)) {
  1418. this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
  1419. _.extend(this['props'], props);
  1420. this.save();
  1421. return true;
  1422. }
  1423. return false;
  1424. };
  1425. MixpanelCookie.prototype.unregister = function(prop) {
  1426. if (prop in this['props']) {
  1427. delete this['props'][prop];
  1428. this.save();
  1429. }
  1430. };
  1431. MixpanelCookie.prototype.update_campaign_params = function() {
  1432. if (!this.campaign_params_saved) {
  1433. this.register_once(_.info.campaignParams());
  1434. this.campaign_params_saved = true;
  1435. }
  1436. };
  1437. MixpanelCookie.prototype.update_search_keyword = function(referrer) {
  1438. this.register(_.info.searchInfo(referrer));
  1439. };
  1440. // EXPORTED METHOD, we test this directly.
  1441. MixpanelCookie.prototype.update_referrer_info = function(referrer) {
  1442. // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
  1443. this.register_once({
  1444. "$initial_referrer": referrer || "$direct",
  1445. "$initial_referring_domain": _.info.referringDomain(referrer) || "$direct"
  1446. }, "");
  1447. };
  1448. MixpanelCookie.prototype.get_referrer_info = function() {
  1449. return _.strip_empty_properties({
  1450. '$initial_referrer': this['props']['$initial_referrer'],
  1451. '$initial_referring_domain': this['props']['$initial_referring_domain']
  1452. });
  1453. };
  1454. // safely fills the passed in object with the cookies properties,
  1455. // does not override any properties defined in both
  1456. // returns the passed in object
  1457. MixpanelCookie.prototype.safe_merge = function(props) {
  1458. _.each(this['props'], function(val, prop) {
  1459. if (!(prop in props)) {
  1460. props[prop] = val;
  1461. }
  1462. });
  1463. return props;
  1464. };
  1465. MixpanelCookie.prototype.update_config = function(config) {
  1466. this.default_expiry = this.expire_days = config['cookie_expiration'];
  1467. this.set_disabled(config['disable_cookie']);
  1468. this.set_cross_subdomain(config['cross_subdomain_cookie']);
  1469. this.set_secure(config['secure_cookie']);
  1470. };
  1471. MixpanelCookie.prototype.set_disabled = function(disabled) {
  1472. this.disabled = disabled;
  1473. if (this.disabled) {
  1474. this.remove();
  1475. }
  1476. };
  1477. MixpanelCookie.prototype.set_cross_subdomain = function(cross_subdomain) {
  1478. if (cross_subdomain !== this.cross_subdomain) {
  1479. this.cross_subdomain = cross_subdomain;
  1480. this.remove();
  1481. this.save();
  1482. }
  1483. };
  1484. MixpanelCookie.prototype.get_cross_subdomain = function() {
  1485. return this.cross_subdomain;
  1486. };
  1487. MixpanelCookie.prototype.set_secure = function(secure) {
  1488. if (secure !== this.secure) {
  1489. this.secure = secure ? true : false;
  1490. this.remove();
  1491. this.save();
  1492. }
  1493. };
  1494. MixpanelCookie.prototype._add_to_people_queue = function(queue, data) {
  1495. var q_key = this._get_queue_key(queue),
  1496. q_data = data[queue],
  1497. set_q = this._get_or_create_queue(SET_ACTION),
  1498. set_once_q = this._get_or_create_queue(SET_ONCE_ACTION),
  1499. add_q = this._get_or_create_queue(ADD_ACTION),
  1500. append_q = this._get_or_create_queue(APPEND_ACTION, []);
  1501. if (q_key === SET_QUEUE_KEY) {
  1502. // Update the set queue - we can override any existing values
  1503. _.extend(set_q, q_data);
  1504. // if there was a pending increment, override it
  1505. // with the set.
  1506. this._pop_from_people_queue(ADD_ACTION, q_data);
  1507. } else if (q_key === SET_ONCE_QUEUE_KEY) {
  1508. // only queue the data if there is not already a set_once call for it.
  1509. _.each(q_data, function(v, k) {
  1510. if (!(k in set_once_q)) {
  1511. set_once_q[k] = v;
  1512. }
  1513. });
  1514. } else if (q_key === ADD_QUEUE_KEY) {
  1515. _.each(q_data, function(v, k) {
  1516. // If it exists in the set queue, increment
  1517. // the value
  1518. if (k in set_q) {
  1519. set_q[k] += v;
  1520. } else {
  1521. // If it doesn't exist, update the add
  1522. // queue
  1523. if (!(k in add_q)) {
  1524. add_q[k] = 0;
  1525. }
  1526. add_q[k] += v;
  1527. }
  1528. }, this);
  1529. } else if (q_key === APPEND_QUEUE_KEY) {
  1530. append_q.push(q_data);
  1531. }
  1532. console.log("MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):");
  1533. console.log(data);
  1534. this.save();
  1535. };
  1536. MixpanelCookie.prototype._pop_from_people_queue = function(queue, data) {
  1537. var q = this._get_queue(queue);
  1538. if (!_.isUndefined(q)) {
  1539. _.each(data, function(v, k) {
  1540. delete q[k];
  1541. }, this);
  1542. this.save();
  1543. }
  1544. };
  1545. MixpanelCookie.prototype._get_queue_key = function(queue) {
  1546. if (queue === SET_ACTION) {
  1547. return SET_QUEUE_KEY;
  1548. } else if (queue === SET_ONCE_ACTION) {
  1549. return SET_ONCE_QUEUE_KEY;
  1550. } else if (queue === ADD_ACTION) {
  1551. return ADD_QUEUE_KEY;
  1552. } else if (queue === APPEND_ACTION) {
  1553. return APPEND_QUEUE_KEY;
  1554. } else {
  1555. console.error("Invalid queue:", queue);
  1556. }
  1557. };
  1558. MixpanelCookie.prototype._get_queue = function(queue) {
  1559. return this['props'][this._get_queue_key(queue)];
  1560. };
  1561. MixpanelCookie.prototype._get_or_create_queue = function(queue, default_val) {
  1562. var key = this._get_queue_key(queue),
  1563. default_val = _.isUndefined(default_val) ? {} : default_val;
  1564. return this['props'][key] || (this['props'][key] = default_val);
  1565. };
  1566. /**
  1567. * create_mplib(token:string, config:object, name:string)
  1568. *
  1569. * This function is used by the init method of MixpanelLib objects
  1570. * as well as the main initializer at the end of the JSLib (that
  1571. * initializes document.mixpanel as well as any additional instances
  1572. * declared before this file has loaded).
  1573. */
  1574. var create_mplib = function(token, config, name) {
  1575. var instance, target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel : mixpanel[name];
  1576. if (target && !_.isArray(target)) {
  1577. console.error("You have already initialized " + name);
  1578. return;
  1579. }
  1580. instance = new MixpanelLib();
  1581. instance._init(token, config, name);
  1582. instance['people'] = new MixpanelPeople();
  1583. instance['people']._init(instance);
  1584. // if any instance on the page has debug = true, we set the
  1585. // global debug to be true
  1586. DEBUG = DEBUG || instance.get_config('debug');
  1587. // if target is not defined, we called init after the lib already
  1588. // loaded, so there won't be an array of things to execute
  1589. if (!_.isUndefined(target)) {
  1590. // Crunch through the people queue first - we queue this data up &
  1591. // flush on identify, so it's better to do all these operations first
  1592. instance._execute_array.call(instance['people'], target['people']);
  1593. instance._execute_array(target);
  1594. }
  1595. return instance;
  1596. };
  1597. /**
  1598. * Mixpanel Library Object
  1599. * @constructor
  1600. */
  1601. var MixpanelLib = function() { };
  1602. // Initialization methods
  1603. /**
  1604. * This function initialize a new instance of the Mixpanel tracking object.
  1605. * All new instances are added to the main mixpanel object as sub properties (such as
  1606. * mixpanel.your_library_name) and also returned by this function. If you wanted
  1607. * to define a second instance on the page you would do it like so:
  1608. *
  1609. * mixpanel.init("new token", { your: "config" }, "library_name")
  1610. *
  1611. * and use it like this:
  1612. *
  1613. * mixpanel.library_name.track(...)
  1614. *
  1615. * @param {String} token Your Mixpanel API token
  1616. * @param {Object} config A dictionary of config options to override
  1617. * @param {String} name The name for the new mixpanel instance that you want created
  1618. */
  1619. MixpanelLib.prototype.init = function (token, config, name) {
  1620. if (typeof(name) === "undefined") {
  1621. console.error("You must name your new library: init(token, config, name)");
  1622. return;
  1623. }
  1624. if (name === PRIMARY_INSTANCE_NAME) {
  1625. console.error("You must initialize the main mixpanel object right after you include the Mixpanel js snippet");
  1626. return;
  1627. }
  1628. var instance = create_mplib(token, config, name);
  1629. mixpanel[name] = instance;
  1630. instance._loaded();
  1631. return instance;
  1632. };
  1633. // mixpanel._init(token:string, config:object, name:string)
  1634. //
  1635. // This function sets up the current instance of the mixpanel
  1636. // library. The difference between this method and the init(...)
  1637. // method is this one initializes the actual instance, whereas the
  1638. // init(...) method sets up a new library and calls _init on it.
  1639. //
  1640. MixpanelLib.prototype._init = function(token, config, name) {
  1641. this['__loaded'] = true;
  1642. this['config'] = {};
  1643. this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
  1644. "name": name
  1645. , "token": token
  1646. , "callback_fn": ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
  1647. }));
  1648. this['_jsc'] = function() {};
  1649. this.__dom_loaded_queue = [];
  1650. this.__request_queue = [];
  1651. this.__disabled_events = [];
  1652. this._flags = {
  1653. "disable_all_events": false
  1654. , "identify_called": false
  1655. };
  1656. this['cookie'] = new MixpanelCookie(this['config']);
  1657. this.register_once({'distinct_id': _.UUID()}, "");
  1658. };
  1659. // Private methods
  1660. MixpanelLib.prototype._loaded = function() {
  1661. this.get_config('loaded')(this);
  1662. // this happens after so a user can call identify/name_tag in
  1663. // the loaded callback
  1664. if (this.get_config('track_pageview')) {
  1665. this.track_pageview();
  1666. }
  1667. };
  1668. MixpanelLib.prototype._dom_loaded = function() {
  1669. _.each(this.__dom_loaded_queue, function(item) {
  1670. this._track_dom.apply(this, item);
  1671. }, this);
  1672. _.each(this.__request_queue, function(item) {
  1673. this._send_request.apply(this, item);
  1674. }, this);
  1675. delete this.__dom_loaded_queue;
  1676. delete this.__request_queue;
  1677. };
  1678. MixpanelLib.prototype._track_dom = function(DomClass, args) {
  1679. if (this.get_config('img')) {
  1680. console.error("You can't use DOM tracking functions with img = true.");
  1681. return false;
  1682. }
  1683. if (!DOM_LOADED) {
  1684. this.__dom_loaded_queue.push([DomClass, args]);
  1685. return false;
  1686. }
  1687. var dt = new DomClass().init(this);
  1688. return dt.track.apply(dt, args);
  1689. };
  1690. /**
  1691. * _prepare_callback() should be called by callers of _send_request for use
  1692. * as the callback argument.
  1693. *
  1694. * If there is no callback, this returns null.
  1695. * If we are going to make XHR/XDR requests, this returns a function.
  1696. * If we are going to use script tags, this returns a string to use as the
  1697. * callback GET param.
  1698. */
  1699. MixpanelLib.prototype._prepare_callback = function(callback, data) {
  1700. if (_.isUndefined(callback)) {
  1701. return null;
  1702. }
  1703. if (USE_XHR) {
  1704. var callback_function = function(response) {
  1705. callback(response, data);
  1706. };
  1707. return callback_function;
  1708. } else {
  1709. // if the user gives us a callback, we store as a random
  1710. // property on this instances jsc function and update our
  1711. // callback string to reflect that.
  1712. var jsc = this['_jsc']
  1713. , randomized_cb = '' + Math.floor(Math.random() * 100000000)
  1714. , callback_string = this.get_config('callback_fn') + '["' + randomized_cb + '"]';
  1715. jsc[randomized_cb] = function(response) {
  1716. delete jsc[randomized_cb];
  1717. callback(response, data);
  1718. };
  1719. return callback_string;
  1720. }
  1721. };
  1722. MixpanelLib.prototype._send_request = function(url, data, callback) {
  1723. if (ENQUEUE_REQUESTS) {
  1724. this.__request_queue.push(arguments);
  1725. return;
  1726. }
  1727. // needed to correctly format responses
  1728. var verbose_mode = this.get_config('verbose');
  1729. if (this.get_config('test')) { data['test'] = 1; }
  1730. if (verbose_mode) { data['verbose'] = 1; }
  1731. if (this.get_config('img')) { data['img'] = 1; }
  1732. if (!USE_XHR) {
  1733. if (callback) {
  1734. data['callback'] = callback;
  1735. } else if (verbose_mode || this.get_config('test')) {
  1736. // Verbose output (from verbose mode, or an error in test mode) is a json blob,
  1737. // which by itself is not valid javascript. Without a callback, this verbose output will
  1738. // cause an error when returned via jsonp, so we force a no-op callback param.
  1739. // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4
  1740. data['callback'] = '(function(){})';
  1741. }
  1742. }
  1743. data['ip'] = this.get_config('ip')?1:0;
  1744. data['_'] = new Date().getTime().toString();
  1745. url += '?' + _.HTTPBuildQuery(data);
  1746. if ('img' in data) {
  1747. var img = document.createElement("img");
  1748. img.src = url;
  1749. document.body.appendChild(img);
  1750. } else if (USE_XHR) {
  1751. var req = new XMLHttpRequest();
  1752. req.open("GET", url, true);
  1753. // send the mp_optout cookie
  1754. // withCredentials cannot be modified until after calling .open on Android and Mobile Safari
  1755. req.withCredentials = true;
  1756. req.onreadystatechange = function (e) {
  1757. if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4
  1758. if (req.status === 200) {
  1759. if (callback) {
  1760. if (verbose_mode) { callback(_.JSONDecode(req.responseText)); }
  1761. else { callback(Number(req.responseText)); }
  1762. }
  1763. } else {
  1764. var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
  1765. console.error(error);
  1766. if (callback) {
  1767. if (verbose_mode) { callback({ status: 0, error: error }); }
  1768. else { callback(0); }
  1769. }
  1770. }
  1771. }
  1772. };
  1773. req.send(null);
  1774. } else {
  1775. var script = document.createElement("script");
  1776. script.type = "text/javascript";
  1777. script.async = true;
  1778. script.defer = true;
  1779. script.src = url;
  1780. var s = document.getElementsByTagName("script")[0];
  1781. s.parentNode.insertBefore(script, s);
  1782. }
  1783. };
  1784. /**
  1785. * _execute_array() deals with processing any mixpanel function
  1786. * calls that were called before the Mixpanel library were loaded
  1787. * (and are thus stored in an array so they can be called later)
  1788. *
  1789. * Note: we fire off all the mixpanel function calls && user defined
  1790. * functions BEFORE we fire off mixpanel tracking calls. This is so
  1791. * identify/register/set_config calls can properly modify early
  1792. * tracking calls.
  1793. *
  1794. * @param {Array} array
  1795. */
  1796. MixpanelLib.prototype._execute_array = function(array) {
  1797. var fn_name, alias_calls = [], other_calls = [], tracking_calls = [];
  1798. _.each(array, function(item) {
  1799. if (item) {
  1800. fn_name = item[0];
  1801. if (typeof(item) === "function") {
  1802. item.call(this);
  1803. } else if (_.isArray(item) && fn_name === 'alias') {
  1804. alias_calls.push(item);
  1805. } else if (_.isArray(item) && fn_name.indexOf('track') != -1 && typeof(this[fn_name]) === "function") {
  1806. tracking_calls.push(item);
  1807. } else {
  1808. other_calls.push(item);
  1809. }
  1810. }
  1811. }, this);
  1812. var execute = function(calls, context) {
  1813. _.each(calls, function(item) {
  1814. this[item[0]].apply(this, item.slice(1));
  1815. }, context);
  1816. };
  1817. execute(alias_calls, this);
  1818. execute(other_calls, this);
  1819. execute(tracking_calls, this);
  1820. };
  1821. /**
  1822. * push() keeps the standard async-array-push
  1823. * behavior around after the lib is loaded.
  1824. * This is only useful for external integrations that
  1825. * do not wish to rely on our convenience methods
  1826. * (created in the snippet). Good example is Optimizely.
  1827. *
  1828. * ### Usage:
  1829. * mixpanel.push(['register', { a: 'b' }]);
  1830. *
  1831. * @param {Array} item A [function_name, args...] array to be executed
  1832. */
  1833. MixpanelLib.prototype.push = function(item) {
  1834. this._execute_array([item]);
  1835. };
  1836. /**
  1837. * Disable events on the Mixpanel object. If passed no arguments,
  1838. * this function disables tracking of any event. If passed an
  1839. * array of event names, those events will be disabled, but other
  1840. * events will continue to be tracked.
  1841. *
  1842. * Note: this function doesn't stop regular mixpanel functions from
  1843. * firing such as register and name_tag.
  1844. *
  1845. * @param {Array} [events] An array of event names to disable
  1846. */
  1847. MixpanelLib.prototype.disable = function(events) {
  1848. if (typeof(events) === 'undefined') {
  1849. this._flags.disable_all_events = true;
  1850. } else {
  1851. this.__disabled_events = this.__disabled_events.concat(events);
  1852. }
  1853. };
  1854. /**
  1855. * Track an event. This is the most important Mixpanel function and
  1856. * the one you will be using the most.
  1857. *
  1858. * ### Usage:
  1859. *
  1860. * // track an event named "Registered"
  1861. * mixpanel.track("Registered", {"Gender": "Male", "Age": 21});
  1862. *
  1863. * For tracking link clicks or form submissions, see mixpanel.track_links or mixpanel.track_forms.
  1864. *
  1865. * @param {String} event_name The name of the event. This can be anything the user does - "Button Click", "Sign Up", "Item Purchased", etc.
  1866. * @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.
  1867. * @param {Function} [callback] If provided, the callback function will be called after tracking the event.
  1868. */
  1869. MixpanelLib.prototype.track = function(event_name, properties, callback) {
  1870. if (typeof(event_name) === "undefined") {
  1871. console.error("No event name provided to mixpanel.track");
  1872. return;
  1873. }
  1874. if (_.isBlockedUA()
  1875. || this._flags.disable_all_events
  1876. || _.include(this.__disabled_events, event_name)) {
  1877. if (typeof(callback) !== 'undefined') { callback(0); }
  1878. return;
  1879. }
  1880. // set defaults
  1881. properties = properties || {};
  1882. properties['token'] = properties.token || this.get_config('token');
  1883. // update cookie
  1884. this['cookie'].update_search_keyword(document.referrer);
  1885. if (this.get_config('store_google')) { this['cookie'].update_campaign_params(); }
  1886. if (this.get_config('save_referrer')) { this['cookie'].update_referrer_info(document.referrer); }
  1887. // note: extend writes to the first object, so lets make sure we
  1888. // don't write to the cookie properties object and info
  1889. // properties object by passing in a new object
  1890. // update properties with pageview info and super-properties
  1891. properties = _.extend(
  1892. {}
  1893. , _.info.properties()
  1894. , this['cookie'].properties()
  1895. , properties
  1896. );
  1897. var data = {
  1898. 'event': event_name
  1899. , 'properties': properties
  1900. };
  1901. var truncated_data = _.truncate(data, 255)
  1902. , json_data = _.JSONEncode(truncated_data)
  1903. , encoded_data = _.base64Encode(json_data);
  1904. console.log("MIXPANEL REQUEST:");
  1905. console.log(truncated_data);
  1906. this._send_request(
  1907. this.get_config('api_host') + "/track/",
  1908. { 'data': encoded_data },
  1909. this._prepare_callback(callback, truncated_data)
  1910. );
  1911. return truncated_data;
  1912. };
  1913. /**
  1914. * Track a page view event. This is most useful for ajax websites
  1915. * where new page views occur without a new page load. This
  1916. * function is called by default on page load unless the
  1917. * track_pageview configuration variable is false.
  1918. *
  1919. * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
  1920. */
  1921. MixpanelLib.prototype.track_pageview = function(page) {
  1922. if (typeof(page) === "undefined") { page = document.location.href; }
  1923. this.track("mp_page_view", _.info.pageviewInfo(page));
  1924. };
  1925. /**
  1926. * Track clicks on a set of document elements. Selector must be a
  1927. * valid query. Elements must exist on the page at the time track_links is called.
  1928. *
  1929. * ### Usage:
  1930. *
  1931. * // track click for link id #nav
  1932. * mixpanel.track_links("#nav", "Clicked Nav Link");
  1933. *
  1934. * ### Notes:
  1935. *
  1936. * This function will wait up to 300 ms for the Mixpanel
  1937. * servers to respond, if they have not responded by that time
  1938. * it will head to the link without ensuring that your event
  1939. * has been tracked. To configure this timeout please see the
  1940. * mixpanel.set_config docs below.
  1941. *
  1942. * If you pass a function in as the properties argument, the
  1943. * function will receive the DOMElement which triggered the
  1944. * event as an argument. You are expected to return an object
  1945. * from the function; any properties defined on this object
  1946. * will be sent to mixpanel as event properties.
  1947. *
  1948. * @type {Function}
  1949. * @param {String} query A valid DOM query
  1950. * @param {String} event_name The name of the event to track
  1951. * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement
  1952. */
  1953. MixpanelLib.prototype.track_links = function() {
  1954. return this._track_dom.call(this, LinkTracker, arguments);
  1955. };
  1956. /**
  1957. * Tracks form submissions. Selector must be a valid query.
  1958. *
  1959. * ### Usage:
  1960. *
  1961. * // track submission for form id "register"
  1962. * mixpanel.track_forms("#register", "Created Account");
  1963. *
  1964. * ### Notes:
  1965. *
  1966. * This function will wait up to 300 ms for the mixpanel
  1967. * servers to respond, if they have not responded by that time
  1968. * it will head to the link without ensuring that your event
  1969. * has been tracked. To configure this timeout please see the
  1970. * mixpanel.set_config docs below.
  1971. *
  1972. * If you pass a function in as the properties argument, the
  1973. * function will receive the DOMElement which triggered the
  1974. * event as an argument. You are expected to return an object
  1975. * from the function; any properties defined on this object
  1976. * will be sent to mixpanel as event properties.
  1977. *
  1978. * @type {Function}
  1979. * @param {String} query A valid DOM query
  1980. * @param {String} event_name The name of the event to track
  1981. * @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
  1982. */
  1983. MixpanelLib.prototype.track_forms = function() {
  1984. return this._track_dom.call(this, FormTracker, arguments);
  1985. };
  1986. /**
  1987. * Register a set of super properties, which are included with all
  1988. * events. This will overwrite previous super property values.
  1989. *
  1990. * @param {Object} properties An associative array of properties to store about the user
  1991. * @param {Number} [days] How many days since the user's last visit to store the super properties
  1992. */
  1993. MixpanelLib.prototype.register = function(props, days) {
  1994. this['cookie'].register(props, days);
  1995. };
  1996. /**
  1997. * Register a set of super properties only once. This will not
  1998. * overwrite previous super property values, unlike register().
  1999. *
  2000. * ### Notes:
  2001. *
  2002. * If default_value is specified, current super properties
  2003. * with that value will be overwritten.
  2004. *
  2005. * @param {Object} properties An associative array of properties to store about the user
  2006. * @param {*} [default_value] Value to override if already set in super properties (ex: "False") Default: "None"
  2007. * @param {Number} [days] How many days since the users last visit to store the super properties
  2008. */
  2009. MixpanelLib.prototype.register_once = function(props, default_value, days) {
  2010. this['cookie'].register_once(props, default_value, days);
  2011. };
  2012. /**
  2013. * Delete a super property stored with the current user.
  2014. *
  2015. * @param {String} property The name of the super property to remove
  2016. */
  2017. MixpanelLib.prototype.unregister = function(property) {
  2018. this['cookie'].unregister(property);
  2019. };
  2020. MixpanelLib.prototype._register_single = function(prop, value) {
  2021. var props = {};
  2022. props[prop] = value;
  2023. this.register(props);
  2024. };
  2025. /**
  2026. * Identify a user with a unique id. All subsequent
  2027. * actions caused by this user will be tied to this identity. This
  2028. * property is used to track unique visitors. If the method is
  2029. * never called, then unique visitors will be identified by a UUID
  2030. * generated the first time they visit the site.
  2031. *
  2032. * ### Note:
  2033. *
  2034. * You can call this function to overwrite a previously set
  2035. * unique id for the current user. Mixpanel cannot translate
  2036. * between ids at this time, so when you change a users id
  2037. * they will appear to be a new user.
  2038. *
  2039. * @param {String} unique_id A string that uniquely identifies a user
  2040. */
  2041. MixpanelLib.prototype.identify = function(unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback) {
  2042. // Optional Parameters
  2043. // _set_callback:function A callback to be run if and when the People set queue is flushed
  2044. // _add_callback:function A callback to be run if and when the People add queue is flushed
  2045. // _append_callback:function A callback to be run if and when the People append queue is flushed
  2046. // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
  2047. // identify only changes the distinct id if it doesn't match either the existing or the alias;
  2048. // if it's new, blow away the alias as well.
  2049. if (unique_id != this.get_distinct_id() && unique_id != this.get_property(ALIAS_ID_KEY)) {
  2050. this.unregister(ALIAS_ID_KEY);
  2051. this._register_single('distinct_id', unique_id);
  2052. }
  2053. this._flags.identify_called = true;
  2054. // Flush any queued up people requests
  2055. this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback);
  2056. };
  2057. /**
  2058. * Returns the current distinct id of the user. This is either the id automatically
  2059. * generated by the library or the id that has been passed by a call to mixpanel.identify
  2060. */
  2061. MixpanelLib.prototype.get_distinct_id = function() {
  2062. return this.get_property('distinct_id');
  2063. };
  2064. /**
  2065. * Create an alias. Multiple aliases can map to the same original ID, but not vice-versa.
  2066. * Aliases can also be chained - the following is a valid scenario:
  2067. *
  2068. * mixpanel.alias("new_id", "existing_id");
  2069. * ...
  2070. * mixpanel.alias("newer_id", "new_id");
  2071. *
  2072. * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID.
  2073. *
  2074. * @param {String} alias A unique identifier that you want to use for this user in the future.
  2075. * @param {String} [original] The current identifier being used for this user.
  2076. */
  2077. MixpanelLib.prototype.alias = function(alias, original) {
  2078. // If the $people_distinct_id key exists in the cookie, there has been a previous
  2079. // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
  2080. // this ID, as it will duplicate users.
  2081. if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
  2082. console.critical("Attempting to create alias for existing People user - aborting.");
  2083. return -2;
  2084. }
  2085. var _this = this;
  2086. if (_.isUndefined(original)) {
  2087. original = this.get_distinct_id();
  2088. }
  2089. if (alias !== original) {
  2090. this._register_single(ALIAS_ID_KEY, alias);
  2091. return this.track("$create_alias", { "alias": alias, "distinct_id": original }, function(response) {
  2092. // Flush the people queue
  2093. _this.identify(alias);
  2094. });
  2095. } else {
  2096. console.error("alias matches current distinct_id - skipping api call.");
  2097. this.identify(alias);
  2098. return -1;
  2099. }
  2100. };
  2101. /**
  2102. * Provide a string to recognize the user by. The string passed to
  2103. * this method will appear in the Mixpanel Streams product rather
  2104. * than an automatically generated name. Name tags do not have to
  2105. * be unique.
  2106. *
  2107. * This value will only be included in Streams data.
  2108. *
  2109. * @param {String} name_tag A human readable name for the user
  2110. */
  2111. MixpanelLib.prototype.name_tag = function(name_tag) {
  2112. this._register_single('mp_name_tag', name_tag);
  2113. };
  2114. /**
  2115. * Update the configuration of a mixpanel library instance.
  2116. *
  2117. * The default config is:
  2118. *
  2119. * {
  2120. * // super properties span subdomains
  2121. * cross_subdomain_cookie: true
  2122. *
  2123. * // super properties cookie name
  2124. * cookie_name: ""
  2125. *
  2126. * // super properties cookie expiration (in days)
  2127. * cookie_expiration: 365
  2128. *
  2129. * // should we track a page view on page load
  2130. * track_pageview: true
  2131. *
  2132. * // the amount of time track_links will
  2133. * // wait for Mixpanel's servers to respond
  2134. * track_links_timeout: 300
  2135. *
  2136. * // if this is true, the mixpanel cookie will be deleted,
  2137. * // and no user persistence will take place
  2138. * disable_cookie: false
  2139. *
  2140. * // if this is true, the mixpanel cookie will be marked as
  2141. * // secure, meaning it will only be transmitted over https
  2142. * secure_cookie: false
  2143. *
  2144. * // if you set upgrade to be true, the library will check for a
  2145. * // cookie from our old js library and import super
  2146. * // properties from it, then the old cookie is deleted
  2147. * // The upgrade config option only works in the initialization, so
  2148. * // make sure you set it when you create the library.
  2149. * upgrade: false
  2150. * }
  2151. *
  2152. *
  2153. * @param {Object} config A dictionary of new configuration values to update
  2154. */
  2155. MixpanelLib.prototype.set_config = function(config) {
  2156. if (_.isObject(config)) {
  2157. _.extend(this['config'], config);
  2158. if (this['cookie']) { this['cookie'].update_config(this['config']); }
  2159. DEBUG = DEBUG || this.get_config('debug');
  2160. }
  2161. };
  2162. /**
  2163. * returns the current config object for the library.
  2164. */
  2165. MixpanelLib.prototype.get_config = function(prop_name) {
  2166. return this['config'][prop_name];
  2167. };
  2168. /**
  2169. * Returns the value of the super property named property_name. If no such
  2170. * property is set, get_property will return the undefined value.
  2171. *
  2172. * @param {String} property_name The name of the super property you want to retrieve
  2173. */
  2174. MixpanelLib.prototype.get_property = function(property_name) {
  2175. return this['cookie']['props'][property_name];
  2176. };
  2177. MixpanelLib.prototype.toString = function() {
  2178. var name = this.get_config("name");
  2179. if (name !== PRIMARY_INSTANCE_NAME) {
  2180. name = PRIMARY_INSTANCE_NAME + "." + name;
  2181. }
  2182. return name;
  2183. };
  2184. /**
  2185. * Mixpanel People Object
  2186. * @constructor
  2187. */
  2188. var MixpanelPeople = function(){ };
  2189. MixpanelPeople.prototype._init = function(mixpanel) {
  2190. this._mixpanel = mixpanel;
  2191. };
  2192. /*
  2193. * Set properties on a user record.
  2194. *
  2195. * ### Usage:
  2196. *
  2197. * mixpanel.people.set('gender', 'm');
  2198. *
  2199. * // or set multiple properties at once
  2200. * mixpanel.people.set({
  2201. * 'company': 'Acme',
  2202. * 'plan': 'Premium',
  2203. * 'last_seen': new Date()
  2204. * });
  2205. * // properties can be strings, integers or dates
  2206. *
  2207. * @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.
  2208. * @param {*} [to] A value to set on the given property name
  2209. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2210. */
  2211. MixpanelPeople.prototype.set = function(prop, to, callback) {
  2212. var data = {};
  2213. var $set = {};
  2214. if (_.isObject(prop)) {
  2215. _.each(prop, function(v, k) {
  2216. // We will get these ourselves
  2217. if (k == '$distinct_id' || k == '$token') {
  2218. return;
  2219. } else {
  2220. $set[k] = v;
  2221. }
  2222. });
  2223. callback = to;
  2224. } else {
  2225. $set[prop] = to;
  2226. }
  2227. // make sure that the referrer info has been updated and saved
  2228. if (this._get_config('save_referrer')) {
  2229. this._mixpanel.cookie.update_referrer_info(document.referrer);
  2230. }
  2231. // update $set object with default people properties
  2232. $set = _.extend({}
  2233. , _.info.people_properties()
  2234. , this._mixpanel.cookie.get_referrer_info()
  2235. , $set
  2236. );
  2237. data[SET_ACTION] = $set;
  2238. return this._send_request(data, callback);
  2239. };
  2240. /*
  2241. * Set properties on a user record, only if they do not yet exist.
  2242. *
  2243. * ### Usage:
  2244. *
  2245. * mixpanel.people.set_once('First Login Date', new Date());
  2246. *
  2247. * // or set multiple properties at once
  2248. * mixpanel.people.set_once({
  2249. * 'First Login Date': new Date(),
  2250. * 'Starting Plan': 'Premium'
  2251. * });
  2252. *
  2253. * // properties can be strings, integers or dates
  2254. *
  2255. * @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.
  2256. * @param {*} [to] A value to set on the given property name
  2257. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2258. */
  2259. MixpanelPeople.prototype.set_once = function(prop, to, callback) {
  2260. var data = {};
  2261. var $set_once = {};
  2262. if (_.isObject(prop)) {
  2263. _.each(prop, function(v, k) {
  2264. // We will get these ourselves
  2265. if (k == '$distinct_id' || k == '$token') {
  2266. return;
  2267. } else {
  2268. $set_once[k] = v;
  2269. }
  2270. });
  2271. callback = to;
  2272. } else {
  2273. $set_once[prop] = to;
  2274. }
  2275. data[SET_ONCE_ACTION] = $set_once;
  2276. return this._send_request(data, callback);
  2277. };
  2278. /*
  2279. * Increment/decrement numeric people analytics properties.
  2280. *
  2281. * ### Usage:
  2282. *
  2283. * mixpanel.people.increment('page_views', 1);
  2284. *
  2285. * // or, for convenience, if you're just incrementing a counter by 1, you can
  2286. * // simply do
  2287. * mixpanel.people.increment('page_views');
  2288. *
  2289. * // to decrement a counter, pass a negative number
  2290. * mixpanel.people.increment('credits_left': -1);
  2291. *
  2292. * // like mixpanel.people.set(), you can increment multiple properties at once:
  2293. * mixpanel.people.increment({
  2294. * counter1: 1,
  2295. * counter2: 1
  2296. * });
  2297. *
  2298. * @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.
  2299. * @param {Number} [by] An amount to increment the given property
  2300. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2301. */
  2302. MixpanelPeople.prototype.increment = function(prop, by, callback) {
  2303. var data = {};
  2304. var $add = {};
  2305. if (_.isObject(prop)) {
  2306. _.each(prop, function(v, k) {
  2307. if (k == '$distinct_id' || k == '$token') {
  2308. return;
  2309. } else if (isNaN(parseFloat(v))) {
  2310. console.error("Invalid increment value passed to mixpanel.people.increment - must be a number");
  2311. return;
  2312. } else {
  2313. $add[k] = v;
  2314. }
  2315. });
  2316. callback = by;
  2317. } else {
  2318. // convenience: mixpanel.people.increment('property'); will
  2319. // increment 'property' by 1
  2320. if (_.isUndefined(by)) {
  2321. by = 1;
  2322. }
  2323. $add[prop] = by;
  2324. }
  2325. data[ADD_ACTION] = $add;
  2326. return this._send_request(data, callback);
  2327. };
  2328. /*
  2329. * Append a value to a list-valued people analytics property.
  2330. *
  2331. * ### Usage:
  2332. *
  2333. * // append a value to a list, creating it if needed
  2334. * mixpanel.people.append('pages_visited', 'homepage');
  2335. *
  2336. * // like mixpanel.people.set(), you can append multiple properties at once:
  2337. * mixpanel.people.append({
  2338. * list1: 'bob',
  2339. * list2: 123
  2340. * });
  2341. *
  2342. * @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.
  2343. * @param {*} [value] An amount to increment the given property
  2344. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2345. */
  2346. MixpanelPeople.prototype.append = function(list_name, value, callback) {
  2347. var data = {};
  2348. var $append = {};
  2349. if (_.isObject(list_name)) {
  2350. _.each(list_name, function(v, k) {
  2351. if (k == '$distinct_id' || k == '$token') {
  2352. return;
  2353. } else {
  2354. $append[k] = v;
  2355. }
  2356. });
  2357. callback = value;
  2358. } else {
  2359. $append[list_name] = value;
  2360. }
  2361. data[APPEND_ACTION] = $append;
  2362. return this._send_request(data, callback);
  2363. };
  2364. /*
  2365. * Record that you have charged the current user a certain amount
  2366. * of money. Charges recorded with track_charge will appear in the
  2367. * Mixpanel revenue report.
  2368. *
  2369. * ### Usage:
  2370. *
  2371. * // charge a user $50
  2372. * mixpanel.people.track_charge(50);
  2373. *
  2374. * // charge a user $30.50 on the 2nd of january
  2375. * mixpanel.people.track_charge(30.50, { '$time': new Date('jan 1 2012') });
  2376. *
  2377. * @param {Number} amount The amount of money charged to the current user
  2378. * @param {Object} [properties] An associative array of properties associated with the charge
  2379. * @param {Function} [callback] If provided, the callback will be called when the server responds
  2380. */
  2381. MixpanelPeople.prototype.track_charge = function(amount, properties, callback) {
  2382. if (!_.isNumber(amount)) {
  2383. amount = parseFloat(amount);
  2384. if (isNaN(amount)) {
  2385. console.error("Invalid value passed to mixpanel.people.track_charge - must be a number");
  2386. return;
  2387. }
  2388. }
  2389. return this.append('$transactions', _.extend({
  2390. '$amount': amount
  2391. }, properties), callback);
  2392. };
  2393. /*
  2394. * Permanently clear all revenue report transactions from the
  2395. * current user's people analytics profile.
  2396. *
  2397. * ### Usage:
  2398. *
  2399. * mixpanel.people.clear_charges();
  2400. *
  2401. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2402. */
  2403. MixpanelPeople.prototype.clear_charges = function(callback) {
  2404. return this.set('$transactions', [], callback);
  2405. };
  2406. /*
  2407. * Permanently deletes the current people analytics profile from
  2408. * Mixpanel (using the current distinct_id).
  2409. *
  2410. * ### Usage:
  2411. *
  2412. * // remove the all data you have stored about the current user
  2413. * mixpanel.people.delete_user();
  2414. *
  2415. */
  2416. MixpanelPeople.prototype.delete_user = function() {
  2417. if (!this._identify_called()) {
  2418. console.error('mixpanel.people.delete_user() requires you to call identify() first');
  2419. return;
  2420. }
  2421. var data = {'$delete': this._mixpanel.get_distinct_id()};
  2422. return this._send_request(data);
  2423. };
  2424. MixpanelPeople.prototype.toString = function() {
  2425. return this._mixpanel.toString() + ".people";
  2426. };
  2427. MixpanelPeople.prototype._send_request = function(data, callback) {
  2428. data['$token'] = this._get_config('token');
  2429. data['$distinct_id'] = this._mixpanel.get_distinct_id();
  2430. var date_encoded_data = _.encodeDates(data)
  2431. , truncated_data = _.truncate(date_encoded_data, 255)
  2432. , json_data = _.JSONEncode(date_encoded_data)
  2433. , encoded_data = _.base64Encode(json_data);
  2434. if (!this._identify_called()) {
  2435. this._enqueue(data);
  2436. if (!_.isUndefined(callback)) {
  2437. if (this._get_config('verbose')) {
  2438. callback({ status: -1, error: null });
  2439. } else {
  2440. callback(-1);
  2441. }
  2442. }
  2443. return truncated_data;
  2444. }
  2445. console.log("MIXPANEL PEOPLE REQUEST:");
  2446. console.log(truncated_data);
  2447. this._mixpanel._send_request(
  2448. this._get_config('api_host') + '/engage/',
  2449. { 'data': encoded_data },
  2450. this._mixpanel._prepare_callback(callback, truncated_data)
  2451. );
  2452. return truncated_data;
  2453. };
  2454. MixpanelPeople.prototype._get_config = function(conf_var) {
  2455. return this._mixpanel.get_config(conf_var);
  2456. };
  2457. MixpanelPeople.prototype._identify_called = function() {
  2458. return this._mixpanel._flags.identify_called === true;
  2459. };
  2460. // Queue up engage operations if identify hasn't been called yet.
  2461. MixpanelPeople.prototype._enqueue = function(data) {
  2462. if (SET_ACTION in data) {
  2463. this._mixpanel.cookie._add_to_people_queue(SET_ACTION, data);
  2464. } else if (SET_ONCE_ACTION in data) {
  2465. this._mixpanel.cookie._add_to_people_queue(SET_ONCE_ACTION, data);
  2466. } else if (ADD_ACTION in data) {
  2467. this._mixpanel.cookie._add_to_people_queue(ADD_ACTION, data);
  2468. } else if (APPEND_ACTION in data) {
  2469. this._mixpanel.cookie._add_to_people_queue(APPEND_ACTION, data);
  2470. } else {
  2471. console.error("Invalid call to _enqueue():", data);
  2472. }
  2473. };
  2474. // Flush queued engage operations - order does not matter,
  2475. // and there are network level race conditions anyway
  2476. MixpanelPeople.prototype._flush = function(_set_callback, _add_callback, _append_callback, _set_once_callback) {
  2477. var _this = this,
  2478. $set_queue = _.extend({}, this._mixpanel.cookie._get_queue(SET_ACTION)),
  2479. $set_once_queue = _.extend({}, this._mixpanel.cookie._get_queue(SET_ONCE_ACTION)),
  2480. $add_queue = _.extend({}, this._mixpanel.cookie._get_queue(ADD_ACTION)),
  2481. $append_queue = this._mixpanel.cookie._get_queue(APPEND_ACTION);
  2482. if (!_.isUndefined($set_queue) && _.isObject($set_queue) && !_.isEmptyObject($set_queue)) {
  2483. _this._mixpanel.cookie._pop_from_people_queue(SET_ACTION, $set_queue);
  2484. this.set($set_queue, function(response, data) {
  2485. // on bad response, we want to add it back to the queue
  2486. if (response == 0) {
  2487. _this._mixpanel.cookie._add_to_people_queue(SET_ACTION, $set_queue);
  2488. }
  2489. if (!_.isUndefined(_set_callback)) {
  2490. _set_callback(response, data);
  2491. }
  2492. });
  2493. }
  2494. if (!_.isUndefined($set_once_queue) && _.isObject($set_once_queue) && !_.isEmptyObject($set_once_queue)) {
  2495. _this._mixpanel.cookie._pop_from_people_queue(SET_ONCE_ACTION, $set_once_queue);
  2496. this.set_once($set_once_queue, function(response, data) {
  2497. // on bad response, we want to add it back to the queue
  2498. if (response == 0) {
  2499. _this._mixpanel.cookie._add_to_people_queue(SET_ONCE_ACTION, $set_once_queue);
  2500. }
  2501. if (!_.isUndefined(_set_once_callback)) {
  2502. _set_once_callback(response, data);
  2503. }
  2504. });
  2505. }
  2506. if (!_.isUndefined($add_queue) && _.isObject($add_queue) && !_.isEmptyObject($add_queue)) {
  2507. _this._mixpanel.cookie._pop_from_people_queue(ADD_ACTION, $add_queue);
  2508. this.increment($add_queue, function(response, data) {
  2509. // on bad response, we want to add it back to the queue
  2510. if (response == 0) {
  2511. _this._mixpanel.cookie._add_to_people_queue(ADD_ACTION, $add_queue);
  2512. }
  2513. if (!_.isUndefined(_add_callback)) {
  2514. _add_callback(response, data);
  2515. }
  2516. });
  2517. }
  2518. // we have to fire off each $append individually since there is
  2519. // no concat method server side
  2520. if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
  2521. for (var i = $append_queue.length - 1; i >= 0; i--) {
  2522. var $append_item = $append_queue.pop();
  2523. _this.append($append_item, function(response, data) {
  2524. if (response == 0) {
  2525. _this._mixpanel.cookie._add_to_people_queue(APPEND_ACTION, $append_item);
  2526. }
  2527. if (!_.isUndefined(_append_callback)) { _append_callback(response, data); }
  2528. });
  2529. };
  2530. // Save the shortened append queue
  2531. _this._mixpanel.cookie.save();
  2532. }
  2533. };
  2534. // EXPORTS (for closure compiler)
  2535. // Underscore Exports
  2536. _['toArray'] = _.toArray;
  2537. _['isObject'] = _.isObject;
  2538. _['JSONEncode'] = _.JSONEncode;
  2539. _['JSONDecode'] = _.JSONDecode;
  2540. _['isEmptyObject'] = _.isEmptyObject;
  2541. // MixpanelLib Exports
  2542. MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
  2543. MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
  2544. MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
  2545. MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links;
  2546. MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms;
  2547. MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview;
  2548. MixpanelLib.prototype['register'] = MixpanelLib.prototype.register;
  2549. MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once;
  2550. MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister;
  2551. MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify;
  2552. MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias;
  2553. MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag;
  2554. MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config;
  2555. MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config;
  2556. MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property;
  2557. MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id;
  2558. MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString;
  2559. // MixpanelCookie Exports
  2560. MixpanelCookie.prototype['properties'] = MixpanelCookie.prototype.properties;
  2561. MixpanelCookie.prototype['update_search_keyword'] = MixpanelCookie.prototype.update_search_keyword;
  2562. MixpanelCookie.prototype['update_referrer_info'] = MixpanelCookie.prototype.update_referrer_info;
  2563. MixpanelCookie.prototype['get_cross_subdomain'] = MixpanelCookie.prototype.get_cross_subdomain;
  2564. MixpanelCookie.prototype['clear'] = MixpanelCookie.prototype.clear;
  2565. // MixpanelPeople Exports
  2566. MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set;
  2567. MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once;
  2568. MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment;
  2569. MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append;
  2570. MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge;
  2571. MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
  2572. MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user;
  2573. MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString;
  2574. // Initialization
  2575. if (_.isUndefined(mixpanel)) {
  2576. // mixpanel wasn't initialized properly, report error and quit
  2577. console.critical("'mixpanel' object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.");
  2578. return;
  2579. }
  2580. if (mixpanel['__loaded'] || (mixpanel['config'] && mixpanel['cookie'])) {
  2581. // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
  2582. console.error("Mixpanel library has already been downloaded at least once.");
  2583. return;
  2584. }
  2585. if (SNIPPET_VERSION < 1.1) {
  2586. // mixpanel wasn't initialized properly, report error and quit
  2587. console.critical("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet.");
  2588. return;
  2589. }
  2590. // Load instances of the Mixpanel Library
  2591. var instances = {};
  2592. _.each(mixpanel['_i'], function(item) {
  2593. var name, instance;
  2594. if (item && _.isArray(item)) {
  2595. name = item[item.length-1];
  2596. instance = create_mplib.apply(this, item);
  2597. instances[name] = instance;
  2598. }
  2599. });
  2600. var extend_mp = function() {
  2601. // add all the sub mixpanel instances
  2602. _.each(instances, function(instance, name) {
  2603. if (name !== PRIMARY_INSTANCE_NAME) { mixpanel[name] = instance; }
  2604. });
  2605. // add private functions as _
  2606. mixpanel['_'] = _;
  2607. };
  2608. // we override the snippets init function to handle the case where a
  2609. // user initializes the mixpanel library after the script loads & runs
  2610. mixpanel['init'] = function(token, config, name) {
  2611. if (name) {
  2612. // initialize a sub library
  2613. if (!mixpanel[name]) {
  2614. mixpanel[name] = instances[name] = create_mplib(token, config, name);
  2615. mixpanel[name]._loaded();
  2616. }
  2617. } else {
  2618. var instance = mixpanel;
  2619. if (instances[PRIMARY_INSTANCE_NAME]) {
  2620. // main mixpanel lib already initialized
  2621. instance = instances[PRIMARY_INSTANCE_NAME];
  2622. } else if (token) {
  2623. // intialize the main mixpanel lib
  2624. instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME);
  2625. }
  2626. window[PRIMARY_INSTANCE_NAME] = mixpanel = instance;
  2627. extend_mp();
  2628. }
  2629. };
  2630. mixpanel['init']();
  2631. // Fire loaded events after updating the window's mixpanel object
  2632. _.each(instances, function(instance) {
  2633. instance._loaded();
  2634. });
  2635. // Cross browser DOM Loaded support
  2636. function dom_loaded_handler() {
  2637. // function flag since we only want to execute this once
  2638. if (dom_loaded_handler.done) { return; }
  2639. dom_loaded_handler.done = true;
  2640. DOM_LOADED = true;
  2641. ENQUEUE_REQUESTS = false;
  2642. _.each(instances, function(inst) {
  2643. inst._dom_loaded();
  2644. });
  2645. }
  2646. if (document.addEventListener) {
  2647. if (document.readyState == "complete") {
  2648. // safari 4 can fire the DOMContentLoaded event before loading all
  2649. // external JS (including this file). you will see some copypasta
  2650. // on the internet that checks for 'complete' and 'loaded', but
  2651. // 'loaded' is an IE thing
  2652. dom_loaded_handler();
  2653. } else {
  2654. document.addEventListener("DOMContentLoaded", dom_loaded_handler, false);
  2655. }
  2656. } else if (document.attachEvent) {
  2657. // IE
  2658. document.attachEvent("onreadystatechange", dom_loaded_handler);
  2659. // check to make sure we arn't in a frame
  2660. var toplevel = false;
  2661. try {
  2662. toplevel = window.frameElement == null;
  2663. } catch(e) {}
  2664. if (document.documentElement.doScroll && toplevel) {
  2665. function do_scroll_check() {
  2666. try {
  2667. document.documentElement.doScroll("left");
  2668. } catch(e) {
  2669. setTimeout(do_scroll_check, 1);
  2670. return;
  2671. }
  2672. dom_loaded_handler();
  2673. };
  2674. do_scroll_check();
  2675. }
  2676. }
  2677. // fallback handler, always will work
  2678. _.register_event(window, 'load', dom_loaded_handler, true);
  2679. })(window['mixpanel']);