PageRenderTime 36ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 1ms

/HelloWorld/platforms/ios/www/js/mixpanel.js

https://github.com/ABensick/cordova
JavaScript | 3027 lines | 2149 code | 250 blank | 628 comment | 386 complexity | f7c6e4057d25b02392e543c3814517ab MD5 | raw file
  1. /*
  2. * Mixpanel JS Library v2.2.1
  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(ua) {
  702. if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) {
  703. return true;
  704. }
  705. return false;
  706. };
  707. /**
  708. * @param {Object=} formdata
  709. * @param {string=} arg_separator
  710. */
  711. _.HTTPBuildQuery = function(formdata, arg_separator) {
  712. var key, use_val, use_key, tmp_arr = [];
  713. if (typeof(arg_separator) === "undefined") {
  714. arg_separator = '&';
  715. }
  716. _.each(formdata, function(val, key) {
  717. use_val = encodeURIComponent(val.toString());
  718. use_key = encodeURIComponent(key);
  719. tmp_arr[tmp_arr.length] = use_key + '=' + use_val;
  720. });
  721. return tmp_arr.join(arg_separator);
  722. };
  723. _.getQueryParam = function(url, param) {
  724. // Expects a raw URL
  725. param = param.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
  726. var regexS = "[\\?&]" + param + "=([^&#]*)",
  727. regex = new RegExp( regexS ),
  728. results = regex.exec(url);
  729. if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) {
  730. return '';
  731. } else {
  732. return decodeURIComponent(results[1]).replace(/\+/g, ' ');
  733. }
  734. };
  735. // _.cookie
  736. // Methods partially borrowed from quirksmode.org/js/cookies.html
  737. _.cookie = {
  738. get: function(name) {
  739. var nameEQ = name + "=";
  740. var ca = document.cookie.split(';');
  741. for(var i=0;i < ca.length;i++) {
  742. var c = ca[i];
  743. while (c.charAt(0)==' ') c = c.substring(1,c.length);
  744. if (c.indexOf(nameEQ) == 0) return decodeURIComponent(c.substring(nameEQ.length,c.length));
  745. }
  746. return null;
  747. },
  748. parse: function(name) {
  749. var cookie;
  750. try {
  751. cookie = _.JSONDecode(_.cookie.get(name)) || {};
  752. } catch (err) {}
  753. return cookie;
  754. },
  755. set: function(name, value, days, cross_subdomain, is_secure) {
  756. var cdomain = "", expires = "", secure = "";
  757. if (cross_subdomain) {
  758. var matches = document.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i)
  759. , domain = matches ? matches[0] : '';
  760. cdomain = ((domain) ? "; domain=." + domain : "");
  761. }
  762. if (days) {
  763. var date = new Date();
  764. date.setTime(date.getTime()+(days*24*60*60*1000));
  765. expires = "; expires=" + date.toGMTString();
  766. }
  767. if (is_secure) {
  768. secure = "; secure";
  769. }
  770. document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/" + cdomain + secure;
  771. },
  772. remove: function(name, cross_subdomain) {
  773. _.cookie.set(name, '', -1, cross_subdomain);
  774. }
  775. };
  776. _.register_event = (function() {
  777. // written by Dean Edwards, 2005
  778. // with input from Tino Zijdel - crisp@xs4all.nl
  779. // with input from Carl Sverre - mail@carlsverre.com
  780. // with input from Mixpanel
  781. // http://dean.edwards.name/weblog/2005/10/add-event/
  782. // https://gist.github.com/1930440
  783. /**
  784. * @param {Object} element
  785. * @param {string} type
  786. * @param {function(...[*])} handler
  787. * @param {boolean=} oldSchool
  788. */
  789. var register_event = function(element, type, handler, oldSchool) {
  790. if (!element) {
  791. console.error("No valid element provided to register_event");
  792. return;
  793. }
  794. if (element.addEventListener && !oldSchool) {
  795. element.addEventListener(type, handler, false);
  796. } else {
  797. var ontype = 'on' + type;
  798. var old_handler = element[ontype]; // can be undefined
  799. element[ontype] = makeHandler(element, handler, old_handler);
  800. }
  801. };
  802. function makeHandler(element, new_handler, old_handlers) {
  803. var handler = function(event) {
  804. event = event || fixEvent(window.event);
  805. // this basically happens in firefox whenever another script
  806. // overwrites the onload callback and doesn't pass the event
  807. // object to previously defined callbacks. All the browsers
  808. // that don't define window.event implement addEventListener
  809. // so the dom_loaded handler will still be fired as usual.
  810. if (!event) { return undefined; }
  811. var ret = true;
  812. var old_result, new_result;
  813. if (_.isFunction(old_handlers)) {
  814. old_result = old_handlers(event);
  815. }
  816. new_result = new_handler.call(element, event);
  817. if ((false === old_result) || (false === new_result)) {
  818. ret = false;
  819. }
  820. return ret;
  821. };
  822. return handler;
  823. };
  824. function fixEvent(event) {
  825. if (event) {
  826. event.preventDefault = fixEvent.preventDefault;
  827. event.stopPropagation = fixEvent.stopPropagation;
  828. }
  829. return event;
  830. };
  831. fixEvent.preventDefault = function() {
  832. this.returnValue = false;
  833. };
  834. fixEvent.stopPropagation = function() {
  835. this.cancelBubble = true;
  836. };
  837. return register_event;
  838. })();
  839. _.dom_query = (function() {
  840. /* document.getElementsBySelector(selector)
  841. - returns an array of element objects from the current document
  842. matching the CSS selector. Selectors can contain element names,
  843. class names and ids and can be nested. For example:
  844. elements = document.getElementsBySelector('div#main p a.external')
  845. Will return an array of all 'a' elements with 'external' in their
  846. class attribute that are contained inside 'p' elements that are
  847. contained inside the 'div' element which has id="main"
  848. New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
  849. See http://www.w3.org/TR/css3-selectors/#attribute-selectors
  850. Version 0.4 - Simon Willison, March 25th 2003
  851. -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
  852. -- Opera 7 fails
  853. Version 0.5 - Carl Sverre, Jan 7th 2013
  854. -- Now uses jQuery-esque `hasClass` for testing class name
  855. equality. This fixes a bug related to '-' characters being
  856. considered not part of a 'word' in regex.
  857. */
  858. function getAllChildren(e) {
  859. // Returns all children of element. Workaround required for IE5/Windows. Ugh.
  860. return e.all ? e.all : e.getElementsByTagName('*');
  861. }
  862. var bad_whitespace = /[\t\r\n]/g;
  863. function hasClass(elem, selector) {
  864. var className = " " + selector + " ";
  865. return ((" " + elem.className + " ").replace(bad_whitespace, " ").indexOf(className) >= 0);
  866. }
  867. function getElementsBySelector(selector) {
  868. // Attempt to fail gracefully in lesser browsers
  869. if (!document.getElementsByTagName) {
  870. return new Array();
  871. }
  872. // Split selector in to tokens
  873. var tokens = selector.split(' ');
  874. var token;
  875. var currentContext = new Array(document);
  876. for (var i = 0; i < tokens.length; i++) {
  877. token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');
  878. if (token.indexOf('#') > -1) {
  879. // Token is an ID selector
  880. var bits = token.split('#');
  881. var tagName = bits[0];
  882. var id = bits[1];
  883. var element = document.getElementById(id);
  884. if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
  885. // element not found or tag with that ID not found, return false
  886. return new Array();
  887. }
  888. // Set currentContext to contain just this element
  889. currentContext = new Array(element);
  890. continue; // Skip to next token
  891. }
  892. if (token.indexOf('.') > -1) {
  893. // Token contains a class selector
  894. var bits = token.split('.');
  895. var tagName = bits[0];
  896. var className = bits[1];
  897. if (!tagName) {
  898. tagName = '*';
  899. }
  900. // Get elements matching tag, filter them for class selector
  901. var found = new Array;
  902. var foundCount = 0;
  903. for (var h = 0; h < currentContext.length; h++) {
  904. var elements;
  905. if (tagName == '*') {
  906. elements = getAllChildren(currentContext[h]);
  907. } else {
  908. elements = currentContext[h].getElementsByTagName(tagName);
  909. }
  910. for (var j = 0; j < elements.length; j++) {
  911. found[foundCount++] = elements[j];
  912. }
  913. }
  914. currentContext = new Array;
  915. var currentContextIndex = 0;
  916. for (var k = 0; k < found.length; k++) {
  917. if (found[k].className
  918. && _.isString(found[k].className) // some SVG elements have classNames which are not strings
  919. && hasClass(found[k], className)
  920. ) {
  921. currentContext[currentContextIndex++] = found[k];
  922. }
  923. }
  924. continue; // Skip to next token
  925. }
  926. // Code to deal with attribute selectors
  927. if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
  928. var tagName = RegExp.$1;
  929. var attrName = RegExp.$2;
  930. var attrOperator = RegExp.$3;
  931. var attrValue = RegExp.$4;
  932. if (!tagName) {
  933. tagName = '*';
  934. }
  935. // Grab all of the tagName elements within current context
  936. var found = new Array;
  937. var foundCount = 0;
  938. for (var h = 0; h < currentContext.length; h++) {
  939. var elements;
  940. if (tagName == '*') {
  941. elements = getAllChildren(currentContext[h]);
  942. } else {
  943. elements = currentContext[h].getElementsByTagName(tagName);
  944. }
  945. for (var j = 0; j < elements.length; j++) {
  946. found[foundCount++] = elements[j];
  947. }
  948. }
  949. currentContext = new Array;
  950. var currentContextIndex = 0;
  951. var checkFunction; // This function will be used to filter the elements
  952. switch (attrOperator) {
  953. case '=': // Equality
  954. checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
  955. break;
  956. case '~': // Match one of space seperated words
  957. checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
  958. break;
  959. case '|': // Match start with value followed by optional hyphen
  960. checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
  961. break;
  962. case '^': // Match starts with value
  963. checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
  964. break;
  965. case '$': // Match ends with value - fails with "Warning" in Opera 7
  966. checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
  967. break;
  968. case '*': // Match ends with value
  969. checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
  970. break;
  971. default :
  972. // Just test for existence of attribute
  973. checkFunction = function(e) { return e.getAttribute(attrName); };
  974. }
  975. currentContext = new Array;
  976. currentContextIndex = 0;
  977. for (var k = 0; k < found.length; k++) {
  978. if (checkFunction(found[k])) {
  979. currentContext[currentContextIndex++] = found[k];
  980. }
  981. }
  982. // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
  983. continue; // Skip to next token
  984. }
  985. // If we get here, token is JUST an element (not a class or ID selector)
  986. tagName = token;
  987. var found = new Array;
  988. var foundCount = 0;
  989. for (var h = 0; h < currentContext.length; h++) {
  990. var elements = currentContext[h].getElementsByTagName(tagName);
  991. for (var j = 0; j < elements.length; j++) {
  992. found[foundCount++] = elements[j];
  993. }
  994. }
  995. currentContext = found;
  996. }
  997. return currentContext;
  998. };
  999. return getElementsBySelector;
  1000. })();
  1001. _.info = {
  1002. campaignParams: function() {
  1003. var campaign_keywords = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' ')
  1004. , kw = ''
  1005. , params = {};
  1006. _.each(campaign_keywords, function(kwkey) {
  1007. kw = _.getQueryParam(document.URL, kwkey);
  1008. if (kw.length) {
  1009. params[kwkey] = kw;
  1010. }
  1011. });
  1012. return params;
  1013. },
  1014. searchEngine: function(referrer) {
  1015. if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
  1016. return 'google';
  1017. } else if (referrer.search('https?://(.*)bing.com') === 0) {
  1018. return 'bing';
  1019. } else if (referrer.search('https?://(.*)yahoo.com') === 0) {
  1020. return 'yahoo';
  1021. } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) {
  1022. return 'duckduckgo';
  1023. } else {
  1024. return null;
  1025. }
  1026. },
  1027. searchInfo: function(referrer) {
  1028. var search = _.info.searchEngine(referrer)
  1029. , param = (search != "yahoo") ? "q" : "p"
  1030. , ret = {};
  1031. if (search !== null) {
  1032. ret["$search_engine"] = search;
  1033. var keyword = _.getQueryParam(referrer, param);
  1034. if (keyword.length) {
  1035. ret["mp_keyword"] = keyword;
  1036. }
  1037. }
  1038. return ret;
  1039. },
  1040. /**
  1041. * This function detects which browser is running this script.
  1042. * The order of the checks are important since many user agents
  1043. * include key words used in later checks.
  1044. */
  1045. browser: function(user_agent, vendor, opera) {
  1046. var vendor = vendor || ''; // vendor is undefined for at least IE9
  1047. if (opera) {
  1048. if (_.includes(user_agent, "Mini")) {
  1049. return "Opera Mini";
  1050. }
  1051. return "Opera";
  1052. } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
  1053. return 'BlackBerry';
  1054. } else if (_.includes(user_agent, "FBIOS")) {
  1055. return "Facebook Mobile";
  1056. } else if (_.includes(user_agent, "Chrome")) {
  1057. return "Chrome";
  1058. } else if (_.includes(user_agent, "CriOS")) {
  1059. return "Chrome iOS";
  1060. } else if (_.includes(vendor, "Apple")) {
  1061. if (_.includes(user_agent, "Mobile")) {
  1062. return "Mobile Safari";
  1063. }
  1064. return "Safari";
  1065. } else if (_.includes(user_agent, "Android")) {
  1066. return "Android Mobile";
  1067. } else if (_.includes(user_agent, "Konqueror")) {
  1068. return "Konqueror";
  1069. } else if (_.includes(user_agent, "Firefox")) {
  1070. return "Firefox";
  1071. } else if (_.includes(user_agent, "MSIE") || _.includes(user_agent, "Trident/")) {
  1072. return "Internet Explorer";
  1073. } else if (_.includes(user_agent, "Gecko")) {
  1074. return "Mozilla";
  1075. } else {
  1076. return "";
  1077. }
  1078. },
  1079. os: function() {
  1080. var a = userAgent;
  1081. if (/Windows/i.test(a)) {
  1082. if (/Phone/.test(a)) { return 'Windows Mobile'; }
  1083. return 'Windows';
  1084. } else if (/(iPhone|iPad|iPod)/.test(a)) {
  1085. return 'iOS';
  1086. } else if (/Android/.test(a)) {
  1087. return 'Android';
  1088. } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) {
  1089. return 'BlackBerry';
  1090. } else if (/Mac/i.test(a)) {
  1091. return 'Mac OS X';
  1092. } else if (/Linux/.test(a)) {
  1093. return 'Linux';
  1094. } else {
  1095. return '';
  1096. }
  1097. },
  1098. device: function(user_agent) {
  1099. if (/iPad/.test(user_agent)) {
  1100. return 'iPad';
  1101. } else if (/iPod/.test(user_agent)) {
  1102. return 'iPod Touch';
  1103. } else if (/iPhone/.test(user_agent)) {
  1104. return 'iPhone';
  1105. } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
  1106. return 'BlackBerry';
  1107. } else if (/Windows Phone/i.test(user_agent)) {
  1108. return 'Windows Phone';
  1109. } else if (/Android/.test(user_agent)) {
  1110. return 'Android';
  1111. } else {
  1112. return '';
  1113. }
  1114. },
  1115. referringDomain: function(referrer) {
  1116. var split = referrer.split("/");
  1117. if (split.length >= 3) {
  1118. return split[2];
  1119. }
  1120. return "";
  1121. },
  1122. properties: function() {
  1123. return _.extend(_.strip_empty_properties({
  1124. '$os': _.info.os(),
  1125. '$browser': _.info.browser(userAgent, navigator.vendor, window.opera),
  1126. '$referrer': document.referrer,
  1127. '$referring_domain': _.info.referringDomain(document.referrer),
  1128. '$device': _.info.device(userAgent)
  1129. }), {
  1130. '$screen_height': screen.height,
  1131. '$screen_width': screen.width,
  1132. 'mp_lib': 'web'
  1133. });
  1134. },
  1135. people_properties: function() {
  1136. return _.strip_empty_properties({
  1137. '$os': _.info.os(),
  1138. '$browser': _.info.browser(userAgent, navigator.vendor, window.opera)
  1139. });
  1140. },
  1141. pageviewInfo: function(page) {
  1142. return _.strip_empty_properties({
  1143. 'mp_page': page
  1144. , 'mp_referrer': document.referrer
  1145. , 'mp_browser': _.info.browser(userAgent, navigator.vendor, window.opera)
  1146. , 'mp_platform': _.info.os()
  1147. });
  1148. }
  1149. };
  1150. // Console override
  1151. var console = {
  1152. /** @type {function(...[*])} */
  1153. log: function() {
  1154. if (DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
  1155. try {
  1156. windowConsole.log.apply(windowConsole, arguments);
  1157. } catch(err) {
  1158. _.each(arguments, function(arg) {
  1159. windowConsole.log(arg);
  1160. });
  1161. }
  1162. }
  1163. },
  1164. /** @type {function(...[*])} */
  1165. error: function() {
  1166. if (DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
  1167. var args = ["Mixpanel error:"].concat(_.toArray(arguments));
  1168. try {
  1169. windowConsole.error.apply(windowConsole, args);
  1170. } catch(err) {
  1171. _.each(args, function(arg) {
  1172. windowConsole.error(arg);
  1173. });
  1174. }
  1175. }
  1176. },
  1177. /** @type {function(...[*])} */
  1178. critical: function() {
  1179. if (!_.isUndefined(windowConsole) && windowConsole) {
  1180. var args = ["Mixpanel error:"].concat(_.toArray(arguments));
  1181. try {
  1182. windowConsole.error.apply(windowConsole, args);
  1183. } catch(err) {
  1184. _.each(args, function(arg) {
  1185. windowConsole.error(arg);
  1186. });
  1187. }
  1188. }
  1189. }
  1190. };
  1191. /**
  1192. * DomTracker Object
  1193. * @constructor
  1194. */
  1195. var DomTracker = function() {};
  1196. // interface
  1197. DomTracker.prototype.create_properties = function() {};
  1198. DomTracker.prototype.event_handler = function() {};
  1199. DomTracker.prototype.after_track_handler = function() {};
  1200. DomTracker.prototype.init = function(mixpanel_instance) {
  1201. this.mp = mixpanel_instance;
  1202. return this;
  1203. };
  1204. /**
  1205. * @param {string} query
  1206. * @param {string} event_name
  1207. * @param {Object=} properties
  1208. * @param {function(...[*])=} user_callback
  1209. */
  1210. DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
  1211. var that = this
  1212. , elements = _.dom_query(query);
  1213. if (elements.length == 0) {
  1214. console.error("The DOM query (" + query + ") returned 0 elements");
  1215. return;
  1216. }
  1217. _.each(elements, function(element) {
  1218. _.register_event(element, this.override_event, function(e) {
  1219. var options = {}
  1220. , props = that.create_properties(properties, this)
  1221. , timeout = that.mp.get_config("track_links_timeout");
  1222. that.event_handler(e, this, options);
  1223. // in case the mixpanel servers don't get back to us in time
  1224. window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
  1225. // fire the tracking event
  1226. that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
  1227. });
  1228. }, this);
  1229. return true;
  1230. };
  1231. /**
  1232. * @param {function(...[*])} user_callback
  1233. * @param {Object} props
  1234. * @param {boolean=} timeout_occured
  1235. */
  1236. DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
  1237. timeout_occured = timeout_occured || false;
  1238. var that = this;
  1239. return function() {
  1240. // options is referenced from both callbacks, so we can have
  1241. // a "lock" of sorts to ensure only one fires
  1242. if (options.callback_fired) { return; }
  1243. options.callback_fired = true;
  1244. if (user_callback && user_callback(timeout_occured, props) === false) {
  1245. // user can prevent the default functionality by
  1246. // returning false from their callback
  1247. return;
  1248. }
  1249. that.after_track_handler(props, options, timeout_occured);
  1250. };
  1251. };
  1252. DomTracker.prototype.create_properties = function(properties, element) {
  1253. var props;
  1254. if (typeof(properties) === "function") {
  1255. props = properties(element);
  1256. } else {
  1257. props = _.extend({}, properties);
  1258. }
  1259. return props;
  1260. };
  1261. /**
  1262. * LinkTracker Object
  1263. * @constructor
  1264. * @extends DomTracker
  1265. */
  1266. var LinkTracker = function() {
  1267. this.override_event = "click";
  1268. };
  1269. _.inherit(LinkTracker, DomTracker);
  1270. LinkTracker.prototype.create_properties = function(properties, element) {
  1271. var props = LinkTracker.superclass.create_properties.apply(this, arguments);
  1272. if (element.href) { props["url"] = element.href; }
  1273. return props;
  1274. };
  1275. LinkTracker.prototype.event_handler = function(evt, element, options) {
  1276. options.new_tab = (evt.which === 2 || evt.metaKey || element.target === "_blank");
  1277. options.href = element.href;
  1278. if (!options.new_tab) {
  1279. evt.preventDefault();
  1280. }
  1281. };
  1282. LinkTracker.prototype.after_track_handler = function(props, options, timeout_occured) {
  1283. if (options.new_tab) { return; }
  1284. setTimeout(function() {
  1285. window.location = options.href;
  1286. }, 0);
  1287. };
  1288. /**
  1289. * FormTracker Object
  1290. * @constructor
  1291. * @extends DomTracker
  1292. */
  1293. var FormTracker = function() {
  1294. this.override_event = "submit";
  1295. };
  1296. _.inherit(FormTracker, DomTracker);
  1297. FormTracker.prototype.event_handler = function(evt, element, options) {
  1298. options.element = element;
  1299. evt.preventDefault();
  1300. };
  1301. FormTracker.prototype.after_track_handler = function(props, options, timeout_occured) {
  1302. setTimeout(function() {
  1303. options.element.submit();
  1304. }, 0);
  1305. };
  1306. /**
  1307. * Mixpanel Cookie Object
  1308. * @constructor
  1309. */
  1310. var MixpanelCookie = function(config) {
  1311. this['props'] = {};
  1312. this.campaign_params_saved = false;
  1313. if (config['cookie_name']) {
  1314. this.name = "mp_" + config['cookie_name'];
  1315. } else {
  1316. this.name = "mp_" + config['token'] + "_mixpanel";
  1317. }
  1318. this.load();
  1319. this.update_config(config);
  1320. this.upgrade(config);
  1321. this.save();
  1322. };
  1323. MixpanelCookie.prototype.properties = function() {
  1324. var p = {};
  1325. // Filter out reserved properties
  1326. _.each(this['props'], function(v, k) {
  1327. if (!_.include(RESERVED_PROPERTIES, k)) {
  1328. p[k] = v;
  1329. }
  1330. });
  1331. return p;
  1332. };
  1333. MixpanelCookie.prototype.load = function() {
  1334. if (this.disabled) { return; }
  1335. var cookie = _.cookie.parse(this.name);
  1336. if (cookie) {
  1337. this['props'] = _.extend({}, cookie);
  1338. }
  1339. };
  1340. MixpanelCookie.prototype.upgrade = function(config) {
  1341. var should_upgrade = config['upgrade'],
  1342. old_cookie_name,
  1343. old_cookie;
  1344. if (should_upgrade) {
  1345. old_cookie_name = "mp_super_properties";
  1346. // Case where they had a custom cookie name before.
  1347. if (typeof(should_upgrade) === "string") {
  1348. old_cookie_name = should_upgrade;
  1349. }
  1350. old_cookie = _.cookie.parse(old_cookie_name);
  1351. // remove the cookie
  1352. _.cookie.remove(old_cookie_name);
  1353. _.cookie.remove(old_cookie_name, true);
  1354. if (old_cookie) {
  1355. this['props'] = _.extend(
  1356. this['props'],
  1357. old_cookie['all'],
  1358. old_cookie['events']
  1359. );
  1360. }
  1361. }
  1362. if (!config['cookie_name'] && config['name'] !== 'mixpanel') {
  1363. // special case to handle people with cookies of the form
  1364. // mp_TOKEN_INSTANCENAME from the first release of this library
  1365. old_cookie_name = "mp_" + config['token'] + "_" + config['name'];
  1366. old_cookie = _.cookie.parse(old_cookie_name);
  1367. if (old_cookie) {
  1368. _.cookie.remove(old_cookie_name);
  1369. _.cookie.remove(old_cookie_name, true);
  1370. // Save the prop values that were in the cookie from before -
  1371. // this should only happen once as we delete the old one.
  1372. this.register_once(old_cookie);
  1373. }
  1374. }
  1375. };
  1376. MixpanelCookie.prototype.save = function() {
  1377. if (this.disabled) { return; }
  1378. _.cookie.set(
  1379. this.name,
  1380. _.JSONEncode(this['props']),
  1381. this.expire_days,
  1382. this.cross_subdomain,
  1383. this.secure
  1384. );
  1385. };
  1386. MixpanelCookie.prototype.remove = function() {
  1387. // remove both domain and subdomain cookies
  1388. _.cookie.remove(this.name, false);
  1389. _.cookie.remove(this.name, true);
  1390. };
  1391. // removes the cookie and deletes all loaded data
  1392. // forced name for tests
  1393. MixpanelCookie.prototype.clear = function() {
  1394. this.remove();
  1395. this['props'] = {};
  1396. };
  1397. /**
  1398. * @param {Object} props
  1399. * @param {*=} default_value
  1400. * @param {number=} days
  1401. */
  1402. MixpanelCookie.prototype.register_once = function(props, default_value, days) {
  1403. if (_.isObject(props)) {
  1404. if (typeof(default_value) === 'undefined') { default_value = "None"; }
  1405. this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
  1406. _.each(props, function(val, prop) {
  1407. if (!this['props'][prop] || this['props'][prop] === default_value) {
  1408. this['props'][prop] = val;
  1409. }
  1410. }, this);
  1411. this.save();
  1412. return true;
  1413. }
  1414. return false;
  1415. };
  1416. /**
  1417. * @param {Object} props
  1418. * @param {number=} days
  1419. */
  1420. MixpanelCookie.prototype.register = function(props, days) {
  1421. if (_.isObject(props)) {
  1422. this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days;
  1423. _.extend(this['props'], props);
  1424. this.save();
  1425. return true;
  1426. }
  1427. return false;
  1428. };
  1429. MixpanelCookie.prototype.unregister = function(prop) {
  1430. if (prop in this['props']) {
  1431. delete this['props'][prop];
  1432. this.save();
  1433. }
  1434. };
  1435. MixpanelCookie.prototype.update_campaign_params = function() {
  1436. if (!this.campaign_params_saved) {
  1437. this.register_once(_.info.campaignParams());
  1438. this.campaign_params_saved = true;
  1439. }
  1440. };
  1441. MixpanelCookie.prototype.update_search_keyword = function(referrer) {
  1442. this.register(_.info.searchInfo(referrer));
  1443. };
  1444. // EXPORTED METHOD, we test this directly.
  1445. MixpanelCookie.prototype.update_referrer_info = function(referrer) {
  1446. // If referrer doesn't exist, we want to note the fact that it was type-in traffic.
  1447. this.register_once({
  1448. "$initial_referrer": referrer || "$direct",
  1449. "$initial_referring_domain": _.info.referringDomain(referrer) || "$direct"
  1450. }, "");
  1451. };
  1452. MixpanelCookie.prototype.get_referrer_info = function() {
  1453. return _.strip_empty_properties({
  1454. '$initial_referrer': this['props']['$initial_referrer'],
  1455. '$initial_referring_domain': this['props']['$initial_referring_domain']
  1456. });
  1457. };
  1458. // safely fills the passed in object with the cookies properties,
  1459. // does not override any properties defined in both
  1460. // returns the passed in object
  1461. MixpanelCookie.prototype.safe_merge = function(props) {
  1462. _.each(this['props'], function(val, prop) {
  1463. if (!(prop in props)) {
  1464. props[prop] = val;
  1465. }
  1466. });
  1467. return props;
  1468. };
  1469. MixpanelCookie.prototype.update_config = function(config) {
  1470. this.default_expiry = this.expire_days = config['cookie_expiration'];
  1471. this.set_disabled(config['disable_cookie']);
  1472. this.set_cross_subdomain(config['cross_subdomain_cookie']);
  1473. this.set_secure(config['secure_cookie']);
  1474. };
  1475. MixpanelCookie.prototype.set_disabled = function(disabled) {
  1476. this.disabled = disabled;
  1477. if (this.disabled) {
  1478. this.remove();
  1479. }
  1480. };
  1481. MixpanelCookie.prototype.set_cross_subdomain = function(cross_subdomain) {
  1482. if (cross_subdomain !== this.cross_subdomain) {
  1483. this.cross_subdomain = cross_subdomain;
  1484. this.remove();
  1485. this.save();
  1486. }
  1487. };
  1488. MixpanelCookie.prototype.get_cross_subdomain = function() {
  1489. return this.cross_subdomain;
  1490. };
  1491. MixpanelCookie.prototype.set_secure = function(secure) {
  1492. if (secure !== this.secure) {
  1493. this.secure = secure ? true : false;
  1494. this.remove();
  1495. this.save();
  1496. }
  1497. };
  1498. MixpanelCookie.prototype._add_to_people_queue = function(queue, data) {
  1499. var q_key = this._get_queue_key(queue),
  1500. q_data = data[queue],
  1501. set_q = this._get_or_create_queue(SET_ACTION),
  1502. set_once_q = this._get_or_create_queue(SET_ONCE_ACTION),
  1503. add_q = this._get_or_create_queue(ADD_ACTION),
  1504. append_q = this._get_or_create_queue(APPEND_ACTION, []);
  1505. if (q_key === SET_QUEUE_KEY) {
  1506. // Update the set queue - we can override any existing values
  1507. _.extend(set_q, q_data);
  1508. // if there was a pending increment, override it
  1509. // with the set.
  1510. this._pop_from_people_queue(ADD_ACTION, q_data);
  1511. } else if (q_key === SET_ONCE_QUEUE_KEY) {
  1512. // only queue the data if there is not already a set_once call for it.
  1513. _.each(q_data, function(v, k) {
  1514. if (!(k in set_once_q)) {
  1515. set_once_q[k] = v;
  1516. }
  1517. });
  1518. } else if (q_key === ADD_QUEUE_KEY) {
  1519. _.each(q_data, function(v, k) {
  1520. // If it exists in the set queue, increment
  1521. // the value
  1522. if (k in set_q) {
  1523. set_q[k] += v;
  1524. } else {
  1525. // If it doesn't exist, update the add
  1526. // queue
  1527. if (!(k in add_q)) {
  1528. add_q[k] = 0;
  1529. }
  1530. add_q[k] += v;
  1531. }
  1532. }, this);
  1533. } else if (q_key === APPEND_QUEUE_KEY) {
  1534. append_q.push(q_data);
  1535. }
  1536. console.log("MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):");
  1537. console.log(data);
  1538. this.save();
  1539. };
  1540. MixpanelCookie.prototype._pop_from_people_queue = function(queue, data) {
  1541. var q = this._get_queue(queue);
  1542. if (!_.isUndefined(q)) {
  1543. _.each(data, function(v, k) {
  1544. delete q[k];
  1545. }, this);
  1546. this.save();
  1547. }
  1548. };
  1549. MixpanelCookie.prototype._get_queue_key = function(queue) {
  1550. if (queue === SET_ACTION) {
  1551. return SET_QUEUE_KEY;
  1552. } else if (queue === SET_ONCE_ACTION) {
  1553. return SET_ONCE_QUEUE_KEY;
  1554. } else if (queue === ADD_ACTION) {
  1555. return ADD_QUEUE_KEY;
  1556. } else if (queue === APPEND_ACTION) {
  1557. return APPEND_QUEUE_KEY;
  1558. } else {
  1559. console.error("Invalid queue:", queue);
  1560. }
  1561. };
  1562. MixpanelCookie.prototype._get_queue = function(queue) {
  1563. return this['props'][this._get_queue_key(queue)];
  1564. };
  1565. MixpanelCookie.prototype._get_or_create_queue = function(queue, default_val) {
  1566. var key = this._get_queue_key(queue),
  1567. default_val = _.isUndefined(default_val) ? {} : default_val;
  1568. return this['props'][key] || (this['props'][key] = default_val);
  1569. };
  1570. /**
  1571. * create_mplib(token:string, config:object, name:string)
  1572. *
  1573. * This function is used by the init method of MixpanelLib objects
  1574. * as well as the main initializer at the end of the JSLib (that
  1575. * initializes document.mixpanel as well as any additional instances
  1576. * declared before this file has loaded).
  1577. */
  1578. var create_mplib = function(token, config, name) {
  1579. var instance, target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel : mixpanel[name];
  1580. if (target && !_.isArray(target)) {
  1581. console.error("You have already initialized " + name);
  1582. return;
  1583. }
  1584. instance = new MixpanelLib();
  1585. instance._init(token, config, name);
  1586. instance['people'] = new MixpanelPeople();
  1587. instance['people']._init(instance);
  1588. // if any instance on the page has debug = true, we set the
  1589. // global debug to be true
  1590. DEBUG = DEBUG || instance.get_config('debug');
  1591. // if target is not defined, we called init after the lib already
  1592. // loaded, so there won't be an array of things to execute
  1593. if (!_.isUndefined(target)) {
  1594. // Crunch through the people queue first - we queue this data up &
  1595. // flush on identify, so it's better to do all these operations first
  1596. instance._execute_array.call(instance['people'], target['people']);
  1597. instance._execute_array(target);
  1598. }
  1599. return instance;
  1600. };
  1601. /**
  1602. * Mixpanel Library Object
  1603. * @constructor
  1604. */
  1605. var MixpanelLib = function() { };
  1606. // Initialization methods
  1607. /**
  1608. * This function initialize a new instance of the Mixpanel tracking object.
  1609. * All new instances are added to the main mixpanel object as sub properties (such as
  1610. * mixpanel.your_library_name) and also returned by this function. If you wanted
  1611. * to define a second instance on the page you would do it like so:
  1612. *
  1613. * mixpanel.init("new token", { your: "config" }, "library_name")
  1614. *
  1615. * and use it like this:
  1616. *
  1617. * mixpanel.library_name.track(...)
  1618. *
  1619. * @param {String} token Your Mixpanel API token
  1620. * @param {Object} [config] A dictionary of config options to override
  1621. * @param {String} [name] The name for the new mixpanel instance that you want created
  1622. */
  1623. MixpanelLib.prototype.init = function (token, config, name) {
  1624. if (typeof(name) === "undefined") {
  1625. console.error("You must name your new library: init(token, config, name)");
  1626. return;
  1627. }
  1628. if (name === PRIMARY_INSTANCE_NAME) {
  1629. console.error("You must initialize the main mixpanel object right after you include the Mixpanel js snippet");
  1630. return;
  1631. }
  1632. var instance = create_mplib(token, config, name);
  1633. mixpanel[name] = instance;
  1634. instance._loaded();
  1635. return instance;
  1636. };
  1637. // mixpanel._init(token:string, config:object, name:string)
  1638. //
  1639. // This function sets up the current instance of the mixpanel
  1640. // library. The difference between this method and the init(...)
  1641. // method is this one initializes the actual instance, whereas the
  1642. // init(...) method sets up a new library and calls _init on it.
  1643. //
  1644. MixpanelLib.prototype._init = function(token, config, name) {
  1645. this['__loaded'] = true;
  1646. this['config'] = {};
  1647. this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
  1648. "name": name
  1649. , "token": token
  1650. , "callback_fn": ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
  1651. }));
  1652. this['_jsc'] = function() {};
  1653. this.__dom_loaded_queue = [];
  1654. this.__request_queue = [];
  1655. this.__disabled_events = [];
  1656. this._flags = {
  1657. "disable_all_events": false
  1658. , "identify_called": false
  1659. };
  1660. this['cookie'] = new MixpanelCookie(this['config']);
  1661. this.register_once({'distinct_id': _.UUID()}, "");
  1662. };
  1663. // Private methods
  1664. MixpanelLib.prototype._loaded = function() {
  1665. this.get_config('loaded')(this);
  1666. // this happens after so a user can call identify/name_tag in
  1667. // the loaded callback
  1668. if (this.get_config('track_pageview')) {
  1669. this.track_pageview();
  1670. }
  1671. };
  1672. MixpanelLib.prototype._dom_loaded = function() {
  1673. _.each(this.__dom_loaded_queue, function(item) {
  1674. this._track_dom.apply(this, item);
  1675. }, this);
  1676. _.each(this.__request_queue, function(item) {
  1677. this._send_request.apply(this, item);
  1678. }, this);
  1679. delete this.__dom_loaded_queue;
  1680. delete this.__request_queue;
  1681. };
  1682. MixpanelLib.prototype._track_dom = function(DomClass, args) {
  1683. if (this.get_config('img')) {
  1684. console.error("You can't use DOM tracking functions with img = true.");
  1685. return false;
  1686. }
  1687. if (!DOM_LOADED) {
  1688. this.__dom_loaded_queue.push([DomClass, args]);
  1689. return false;
  1690. }
  1691. var dt = new DomClass().init(this);
  1692. return dt.track.apply(dt, args);
  1693. };
  1694. /**
  1695. * _prepare_callback() should be called by callers of _send_request for use
  1696. * as the callback argument.
  1697. *
  1698. * If there is no callback, this returns null.
  1699. * If we are going to make XHR/XDR requests, this returns a function.
  1700. * If we are going to use script tags, this returns a string to use as the
  1701. * callback GET param.
  1702. */
  1703. MixpanelLib.prototype._prepare_callback = function(callback, data) {
  1704. if (_.isUndefined(callback)) {
  1705. return null;
  1706. }
  1707. if (USE_XHR) {
  1708. var callback_function = function(response) {
  1709. callback(response, data);
  1710. };
  1711. return callback_function;
  1712. } else {
  1713. // if the user gives us a callback, we store as a random
  1714. // property on this instances jsc function and update our
  1715. // callback string to reflect that.
  1716. var jsc = this['_jsc']
  1717. , randomized_cb = '' + Math.floor(Math.random() * 100000000)
  1718. , callback_string = this.get_config('callback_fn') + '["' + randomized_cb + '"]';
  1719. jsc[randomized_cb] = function(response) {
  1720. delete jsc[randomized_cb];
  1721. callback(response, data);
  1722. };
  1723. return callback_string;
  1724. }
  1725. };
  1726. MixpanelLib.prototype._send_request = function(url, data, callback) {
  1727. if (ENQUEUE_REQUESTS) {
  1728. this.__request_queue.push(arguments);
  1729. return;
  1730. }
  1731. // needed to correctly format responses
  1732. var verbose_mode = this.get_config('verbose');
  1733. if (this.get_config('test')) { data['test'] = 1; }
  1734. if (verbose_mode) { data['verbose'] = 1; }
  1735. if (this.get_config('img')) { data['img'] = 1; }
  1736. if (!USE_XHR) {
  1737. if (callback) {
  1738. data['callback'] = callback;
  1739. } else if (verbose_mode || this.get_config('test')) {
  1740. // Verbose output (from verbose mode, or an error in test mode) is a json blob,
  1741. // which by itself is not valid javascript. Without a callback, this verbose output will
  1742. // cause an error when returned via jsonp, so we force a no-op callback param.
  1743. // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4
  1744. data['callback'] = '(function(){})';
  1745. }
  1746. }
  1747. data['ip'] = this.get_config('ip')?1:0;
  1748. data['_'] = new Date().getTime().toString();
  1749. url += '?' + _.HTTPBuildQuery(data);
  1750. if ('img' in data) {
  1751. var img = document.createElement("img");
  1752. img.src = url;
  1753. document.body.appendChild(img);
  1754. } else if (USE_XHR) {
  1755. var req = new XMLHttpRequest();
  1756. req.open("GET", url, true);
  1757. // send the mp_optout cookie
  1758. // withCredentials cannot be modified until after calling .open on Android and Mobile Safari
  1759. req.withCredentials = true;
  1760. req.onreadystatechange = function (e) {
  1761. if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4
  1762. if (req.status === 200) {
  1763. if (callback) {
  1764. if (verbose_mode) { callback(_.JSONDecode(req.responseText)); }
  1765. else { callback(Number(req.responseText)); }
  1766. }
  1767. } else {
  1768. var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
  1769. console.error(error);
  1770. if (callback) {
  1771. if (verbose_mode) { callback({ status: 0, error: error }); }
  1772. else { callback(0); }
  1773. }
  1774. }
  1775. }
  1776. };
  1777. req.send(null);
  1778. } else {
  1779. var script = document.createElement("script");
  1780. script.type = "text/javascript";
  1781. script.async = true;
  1782. script.defer = true;
  1783. script.src = url;
  1784. var s = document.getElementsByTagName("script")[0];
  1785. s.parentNode.insertBefore(script, s);
  1786. }
  1787. };
  1788. /**
  1789. * _execute_array() deals with processing any mixpanel function
  1790. * calls that were called before the Mixpanel library were loaded
  1791. * (and are thus stored in an array so they can be called later)
  1792. *
  1793. * Note: we fire off all the mixpanel function calls && user defined
  1794. * functions BEFORE we fire off mixpanel tracking calls. This is so
  1795. * identify/register/set_config calls can properly modify early
  1796. * tracking calls.
  1797. *
  1798. * @param {Array} array
  1799. */
  1800. MixpanelLib.prototype._execute_array = function(array) {
  1801. var fn_name, alias_calls = [], other_calls = [], tracking_calls = [];
  1802. _.each(array, function(item) {
  1803. if (item) {
  1804. fn_name = item[0];
  1805. if (typeof(item) === "function") {
  1806. item.call(this);
  1807. } else if (_.isArray(item) && fn_name === 'alias') {
  1808. alias_calls.push(item);
  1809. } else if (_.isArray(item) && fn_name.indexOf('track') != -1 && typeof(this[fn_name]) === "function") {
  1810. tracking_calls.push(item);
  1811. } else {
  1812. other_calls.push(item);
  1813. }
  1814. }
  1815. }, this);
  1816. var execute = function(calls, context) {
  1817. _.each(calls, function(item) {
  1818. this[item[0]].apply(this, item.slice(1));
  1819. }, context);
  1820. };
  1821. execute(alias_calls, this);
  1822. execute(other_calls, this);
  1823. execute(tracking_calls, this);
  1824. };
  1825. /**
  1826. * push() keeps the standard async-array-push
  1827. * behavior around after the lib is loaded.
  1828. * This is only useful for external integrations that
  1829. * do not wish to rely on our convenience methods
  1830. * (created in the snippet). Good example is Optimizely.
  1831. *
  1832. * ### Usage:
  1833. * mixpanel.push(['register', { a: 'b' }]);
  1834. *
  1835. * @param {Array} item A [function_name, args...] array to be executed
  1836. */
  1837. MixpanelLib.prototype.push = function(item) {
  1838. this._execute_array([item]);
  1839. };
  1840. /**
  1841. * Disable events on the Mixpanel object. If passed no arguments,
  1842. * this function disables tracking of any event. If passed an
  1843. * array of event names, those events will be disabled, but other
  1844. * events will continue to be tracked.
  1845. *
  1846. * Note: this function doesn't stop regular mixpanel functions from
  1847. * firing such as register and name_tag.
  1848. *
  1849. * @param {Array} [events] An array of event names to disable
  1850. */
  1851. MixpanelLib.prototype.disable = function(events) {
  1852. if (typeof(events) === 'undefined') {
  1853. this._flags.disable_all_events = true;
  1854. } else {
  1855. this.__disabled_events = this.__disabled_events.concat(events);
  1856. }
  1857. };
  1858. /**
  1859. * Track an event. This is the most important Mixpanel function and
  1860. * the one you will be using the most.
  1861. *
  1862. * ### Usage:
  1863. *
  1864. * // track an event named "Registered"
  1865. * mixpanel.track("Registered", {"Gender": "Male", "Age": 21});
  1866. *
  1867. * For tracking link clicks or form submissions, see mixpanel.track_links or mixpanel.track_forms.
  1868. *
  1869. * @param {String} event_name The name of the event. This can be anything the user does - "Button Click", "Sign Up", "Item Purchased", etc.
  1870. * @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.
  1871. * @param {Function} [callback] If provided, the callback function will be called after tracking the event.
  1872. */
  1873. MixpanelLib.prototype.track = function(event_name, properties, callback) {
  1874. if (typeof(event_name) === "undefined") {
  1875. console.error("No event name provided to mixpanel.track");
  1876. return;
  1877. }
  1878. if (_.isBlockedUA(userAgent)
  1879. || this._flags.disable_all_events
  1880. || _.include(this.__disabled_events, event_name)) {
  1881. if (typeof(callback) !== 'undefined') { callback(0); }
  1882. return;
  1883. }
  1884. // set defaults
  1885. properties = properties || {};
  1886. properties['token'] = properties.token || this.get_config('token');
  1887. // update cookie
  1888. this['cookie'].update_search_keyword(document.referrer);
  1889. if (this.get_config('store_google')) { this['cookie'].update_campaign_params(); }
  1890. if (this.get_config('save_referrer')) { this['cookie'].update_referrer_info(document.referrer); }
  1891. // note: extend writes to the first object, so lets make sure we
  1892. // don't write to the cookie properties object and info
  1893. // properties object by passing in a new object
  1894. // update properties with pageview info and super-properties
  1895. properties = _.extend(
  1896. {}
  1897. , _.info.properties()
  1898. , this['cookie'].properties()
  1899. , properties
  1900. );
  1901. var data = {
  1902. 'event': event_name
  1903. , 'properties': properties
  1904. };
  1905. var truncated_data = _.truncate(data, 255)
  1906. , json_data = _.JSONEncode(truncated_data)
  1907. , encoded_data = _.base64Encode(json_data);
  1908. console.log("MIXPANEL REQUEST:");
  1909. console.log(truncated_data);
  1910. this._send_request(
  1911. this.get_config('api_host') + "/track/",
  1912. { 'data': encoded_data },
  1913. this._prepare_callback(callback, truncated_data)
  1914. );
  1915. return truncated_data;
  1916. };
  1917. /**
  1918. * Track a page view event, which is currently ignored by the server.
  1919. * This function is called by default on page load unless the
  1920. * track_pageview configuration variable is false.
  1921. *
  1922. * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url.
  1923. * @api private
  1924. */
  1925. MixpanelLib.prototype.track_pageview = function(page) {
  1926. if (typeof(page) === "undefined") { page = document.location.href; }
  1927. this.track("mp_page_view", _.info.pageviewInfo(page));
  1928. };
  1929. /**
  1930. * Track clicks on a set of document elements. Selector must be a
  1931. * valid query. Elements must exist on the page at the time track_links is called.
  1932. *
  1933. * ### Usage:
  1934. *
  1935. * // track click for link id #nav
  1936. * mixpanel.track_links("#nav", "Clicked Nav Link");
  1937. *
  1938. * ### Notes:
  1939. *
  1940. * This function will wait up to 300 ms for the Mixpanel
  1941. * servers to respond. If they have not responded by that time
  1942. * it will head to the link without ensuring that your event
  1943. * has been tracked. To configure this timeout please see the
  1944. * mixpanel.set_config docs below.
  1945. *
  1946. * If you pass a function in as the properties argument, the
  1947. * function will receive the DOMElement which triggered the
  1948. * event as an argument. You are expected to return an object
  1949. * from the function; any properties defined on this object
  1950. * will be sent to mixpanel as event properties.
  1951. *
  1952. * @type {Function}
  1953. * @param {String} query A valid DOM query
  1954. * @param {String} event_name The name of the event to track
  1955. * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement
  1956. */
  1957. MixpanelLib.prototype.track_links = function() {
  1958. return this._track_dom.call(this, LinkTracker, arguments);
  1959. };
  1960. /**
  1961. * Tracks form submissions. Selector must be a valid query.
  1962. *
  1963. * ### Usage:
  1964. *
  1965. * // track submission for form id "register"
  1966. * mixpanel.track_forms("#register", "Created Account");
  1967. *
  1968. * ### Notes:
  1969. *
  1970. * This function will wait up to 300 ms for the mixpanel
  1971. * servers to respond, if they have not responded by that time
  1972. * it will head to the link without ensuring that your event
  1973. * has been tracked. To configure this timeout please see the
  1974. * mixpanel.set_config docs below.
  1975. *
  1976. * If you pass a function in as the properties argument, the
  1977. * function will receive the DOMElement which triggered the
  1978. * event as an argument. You are expected to return an object
  1979. * from the function; any properties defined on this object
  1980. * will be sent to mixpanel as event properties.
  1981. *
  1982. * @type {Function}
  1983. * @param {String} query A valid DOM query
  1984. * @param {String} event_name The name of the event to track
  1985. * @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
  1986. */
  1987. MixpanelLib.prototype.track_forms = function() {
  1988. return this._track_dom.call(this, FormTracker, arguments);
  1989. };
  1990. /**
  1991. * Register a set of super properties, which are included with all
  1992. * events. This will overwrite previous super property values.
  1993. *
  1994. * @param {Object} properties An associative array of properties to store about the user
  1995. * @param {Number} [days] How many days since the user's last visit to store the super properties
  1996. */
  1997. MixpanelLib.prototype.register = function(props, days) {
  1998. this['cookie'].register(props, days);
  1999. };
  2000. /**
  2001. * Register a set of super properties only once. This will not
  2002. * overwrite previous super property values, unlike register().
  2003. *
  2004. * ### Notes:
  2005. *
  2006. * If default_value is specified, current super properties
  2007. * with that value will be overwritten.
  2008. *
  2009. * @param {Object} properties An associative array of properties to store about the user
  2010. * @param {*} [default_value] Value to override if already set in super properties (ex: "False") Default: "None"
  2011. * @param {Number} [days] How many days since the users last visit to store the super properties
  2012. */
  2013. MixpanelLib.prototype.register_once = function(props, default_value, days) {
  2014. this['cookie'].register_once(props, default_value, days);
  2015. };
  2016. /**
  2017. * Delete a super property stored with the current user.
  2018. *
  2019. * @param {String} property The name of the super property to remove
  2020. */
  2021. MixpanelLib.prototype.unregister = function(property) {
  2022. this['cookie'].unregister(property);
  2023. };
  2024. MixpanelLib.prototype._register_single = function(prop, value) {
  2025. var props = {};
  2026. props[prop] = value;
  2027. this.register(props);
  2028. };
  2029. /**
  2030. * Identify a user with a unique id. All subsequent
  2031. * actions caused by this user will be tied to this identity. This
  2032. * property is used to track unique visitors. If the method is
  2033. * never called, then unique visitors will be identified by a UUID
  2034. * generated the first time they visit the site.
  2035. *
  2036. * ### Note:
  2037. *
  2038. * You can call this function to overwrite a previously set
  2039. * unique id for the current user. Mixpanel cannot translate
  2040. * between ids at this time, so when you change a users id
  2041. * they will appear to be a new user.
  2042. *
  2043. * @param {String} unique_id A string that uniquely identifies a user
  2044. */
  2045. MixpanelLib.prototype.identify = function(unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback) {
  2046. // Optional Parameters
  2047. // _set_callback:function A callback to be run if and when the People set queue is flushed
  2048. // _add_callback:function A callback to be run if and when the People add queue is flushed
  2049. // _append_callback:function A callback to be run if and when the People append queue is flushed
  2050. // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed
  2051. // identify only changes the distinct id if it doesn't match either the existing or the alias;
  2052. // if it's new, blow away the alias as well.
  2053. if (unique_id != this.get_distinct_id() && unique_id != this.get_property(ALIAS_ID_KEY)) {
  2054. this.unregister(ALIAS_ID_KEY);
  2055. this._register_single('distinct_id', unique_id);
  2056. }
  2057. this._flags.identify_called = true;
  2058. // Flush any queued up people requests
  2059. this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback);
  2060. };
  2061. /**
  2062. * Returns the current distinct id of the user. This is either the id automatically
  2063. * generated by the library or the id that has been passed by a call to mixpanel.identify
  2064. */
  2065. MixpanelLib.prototype.get_distinct_id = function() {
  2066. return this.get_property('distinct_id');
  2067. };
  2068. /**
  2069. * Create an alias, which Mixpanel will use to link two distinct_ids going forward (not retroactively).
  2070. * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the
  2071. * following is a valid scenario:
  2072. *
  2073. * mixpanel.alias("new_id", "existing_id");
  2074. * ...
  2075. * mixpanel.alias("newer_id", "new_id");
  2076. *
  2077. * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID.
  2078. *
  2079. * @param {String} alias A unique identifier that you want to use for this user in the future.
  2080. * @param {String} [original] The current identifier being used for this user.
  2081. */
  2082. MixpanelLib.prototype.alias = function(alias, original) {
  2083. // If the $people_distinct_id key exists in the cookie, there has been a previous
  2084. // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
  2085. // this ID, as it will duplicate users.
  2086. if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
  2087. console.critical("Attempting to create alias for existing People user - aborting.");
  2088. return -2;
  2089. }
  2090. var _this = this;
  2091. if (_.isUndefined(original)) {
  2092. original = this.get_distinct_id();
  2093. }
  2094. if (alias !== original) {
  2095. this._register_single(ALIAS_ID_KEY, alias);
  2096. return this.track("$create_alias", { "alias": alias, "distinct_id": original }, function(response) {
  2097. // Flush the people queue
  2098. _this.identify(alias);
  2099. });
  2100. } else {
  2101. console.error("alias matches current distinct_id - skipping api call.");
  2102. this.identify(alias);
  2103. return -1;
  2104. }
  2105. };
  2106. /**
  2107. * Provide a string to recognize the user by. The string passed to
  2108. * this method will appear in the Mixpanel Streams product rather
  2109. * than an automatically generated name. Name tags do not have to
  2110. * be unique.
  2111. *
  2112. * This value will only be included in Streams data.
  2113. *
  2114. * @param {String} name_tag A human readable name for the user
  2115. * @api private
  2116. */
  2117. MixpanelLib.prototype.name_tag = function(name_tag) {
  2118. this._register_single('mp_name_tag', name_tag);
  2119. };
  2120. /**
  2121. * Update the configuration of a mixpanel library instance.
  2122. *
  2123. * The default config is:
  2124. *
  2125. * {
  2126. * // super properties span subdomains
  2127. * cross_subdomain_cookie: true
  2128. *
  2129. * // super properties cookie name
  2130. * cookie_name: ""
  2131. *
  2132. * // super properties cookie expiration (in days)
  2133. * cookie_expiration: 365
  2134. *
  2135. * // should we track a page view on page load
  2136. * track_pageview: true
  2137. *
  2138. * // the amount of time track_links will
  2139. * // wait for Mixpanel's servers to respond
  2140. * track_links_timeout: 300
  2141. *
  2142. * // if this is true, the mixpanel cookie will be deleted,
  2143. * // and no user persistence will take place
  2144. * disable_cookie: false
  2145. *
  2146. * // if this is true, the mixpanel cookie will be marked as
  2147. * // secure, meaning it will only be transmitted over https
  2148. * secure_cookie: false
  2149. *
  2150. * // if you set upgrade to be true, the library will check for a
  2151. * // cookie from our old js library and import super
  2152. * // properties from it, then the old cookie is deleted
  2153. * // The upgrade config option only works in the initialization, so
  2154. * // make sure you set it when you create the library.
  2155. * upgrade: false
  2156. * }
  2157. *
  2158. *
  2159. * @param {Object} config A dictionary of new configuration values to update
  2160. */
  2161. MixpanelLib.prototype.set_config = function(config) {
  2162. if (_.isObject(config)) {
  2163. _.extend(this['config'], config);
  2164. if (this['cookie']) { this['cookie'].update_config(this['config']); }
  2165. DEBUG = DEBUG || this.get_config('debug');
  2166. }
  2167. };
  2168. /**
  2169. * returns the current config object for the library.
  2170. */
  2171. MixpanelLib.prototype.get_config = function(prop_name) {
  2172. return this['config'][prop_name];
  2173. };
  2174. /**
  2175. * Returns the value of the super property named property_name. If no such
  2176. * property is set, get_property will return the undefined value.
  2177. *
  2178. * @param {String} property_name The name of the super property you want to retrieve
  2179. */
  2180. MixpanelLib.prototype.get_property = function(property_name) {
  2181. return this['cookie']['props'][property_name];
  2182. };
  2183. MixpanelLib.prototype.toString = function() {
  2184. var name = this.get_config("name");
  2185. if (name !== PRIMARY_INSTANCE_NAME) {
  2186. name = PRIMARY_INSTANCE_NAME + "." + name;
  2187. }
  2188. return name;
  2189. };
  2190. /**
  2191. * Mixpanel People Object
  2192. * @constructor
  2193. */
  2194. var MixpanelPeople = function(){ };
  2195. MixpanelPeople.prototype._init = function(mixpanel) {
  2196. this._mixpanel = mixpanel;
  2197. };
  2198. /*
  2199. * Set properties on a user record.
  2200. *
  2201. * ### Usage:
  2202. *
  2203. * mixpanel.people.set('gender', 'm');
  2204. *
  2205. * // or set multiple properties at once
  2206. * mixpanel.people.set({
  2207. * 'Company': 'Acme',
  2208. * 'Plan': 'Premium',
  2209. * 'Upgrade date': new Date()
  2210. * });
  2211. * // properties can be strings, integers, dates, or lists
  2212. *
  2213. * @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.
  2214. * @param {*} [to] A value to set on the given property name
  2215. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2216. */
  2217. MixpanelPeople.prototype.set = function(prop, to, callback) {
  2218. var data = {};
  2219. var $set = {};
  2220. if (_.isObject(prop)) {
  2221. _.each(prop, function(v, k) {
  2222. // We will get these ourselves
  2223. if (k == '$distinct_id' || k == '$token') {
  2224. return;
  2225. } else {
  2226. $set[k] = v;
  2227. }
  2228. });
  2229. callback = to;
  2230. } else {
  2231. $set[prop] = to;
  2232. }
  2233. // make sure that the referrer info has been updated and saved
  2234. if (this._get_config('save_referrer')) {
  2235. this._mixpanel.cookie.update_referrer_info(document.referrer);
  2236. }
  2237. // update $set object with default people properties
  2238. $set = _.extend({}
  2239. , _.info.people_properties()
  2240. , this._mixpanel.cookie.get_referrer_info()
  2241. , $set
  2242. );
  2243. data[SET_ACTION] = $set;
  2244. return this._send_request(data, callback);
  2245. };
  2246. /*
  2247. * Set properties on a user record, only if they do not yet exist.
  2248. *
  2249. * ### Usage:
  2250. *
  2251. * mixpanel.people.set_once('First Login Date', new Date());
  2252. *
  2253. * // or set multiple properties at once
  2254. * mixpanel.people.set_once({
  2255. * 'First Login Date': new Date(),
  2256. * 'Starting Plan': 'Premium'
  2257. * });
  2258. *
  2259. * // properties can be strings, integers or dates
  2260. *
  2261. * @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.
  2262. * @param {*} [to] A value to set on the given property name
  2263. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2264. */
  2265. MixpanelPeople.prototype.set_once = function(prop, to, callback) {
  2266. var data = {};
  2267. var $set_once = {};
  2268. if (_.isObject(prop)) {
  2269. _.each(prop, function(v, k) {
  2270. // We will get these ourselves
  2271. if (k == '$distinct_id' || k == '$token') {
  2272. return;
  2273. } else {
  2274. $set_once[k] = v;
  2275. }
  2276. });
  2277. callback = to;
  2278. } else {
  2279. $set_once[prop] = to;
  2280. }
  2281. data[SET_ONCE_ACTION] = $set_once;
  2282. return this._send_request(data, callback);
  2283. };
  2284. /*
  2285. * Increment/decrement numeric people analytics properties.
  2286. *
  2287. * ### Usage:
  2288. *
  2289. * mixpanel.people.increment('page_views', 1);
  2290. *
  2291. * // or, for convenience, if you're just incrementing a counter by 1, you can
  2292. * // simply do
  2293. * mixpanel.people.increment('page_views');
  2294. *
  2295. * // to decrement a counter, pass a negative number
  2296. * mixpanel.people.increment('credits_left': -1);
  2297. *
  2298. * // like mixpanel.people.set(), you can increment multiple properties at once:
  2299. * mixpanel.people.increment({
  2300. * counter1: 1,
  2301. * counter2: 1
  2302. * });
  2303. *
  2304. * @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.
  2305. * @param {Number} [by] An amount to increment the given property
  2306. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2307. */
  2308. MixpanelPeople.prototype.increment = function(prop, by, callback) {
  2309. var data = {};
  2310. var $add = {};
  2311. if (_.isObject(prop)) {
  2312. _.each(prop, function(v, k) {
  2313. if (k == '$distinct_id' || k == '$token') {
  2314. return;
  2315. } else if (isNaN(parseFloat(v))) {
  2316. console.error("Invalid increment value passed to mixpanel.people.increment - must be a number");
  2317. return;
  2318. } else {
  2319. $add[k] = v;
  2320. }
  2321. });
  2322. callback = by;
  2323. } else {
  2324. // convenience: mixpanel.people.increment('property'); will
  2325. // increment 'property' by 1
  2326. if (_.isUndefined(by)) {
  2327. by = 1;
  2328. }
  2329. $add[prop] = by;
  2330. }
  2331. data[ADD_ACTION] = $add;
  2332. return this._send_request(data, callback);
  2333. };
  2334. /*
  2335. * Append a value to a list-valued people analytics property.
  2336. *
  2337. * ### Usage:
  2338. *
  2339. * // append a value to a list, creating it if needed
  2340. * mixpanel.people.append('pages_visited', 'homepage');
  2341. *
  2342. * // like mixpanel.people.set(), you can append multiple properties at once:
  2343. * mixpanel.people.append({
  2344. * list1: 'bob',
  2345. * list2: 123
  2346. * });
  2347. *
  2348. * @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.
  2349. * @param {*} [value] An item to append to the list
  2350. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2351. */
  2352. MixpanelPeople.prototype.append = function(list_name, value, callback) {
  2353. var data = {};
  2354. var $append = {};
  2355. if (_.isObject(list_name)) {
  2356. _.each(list_name, function(v, k) {
  2357. if (k == '$distinct_id' || k == '$token') {
  2358. return;
  2359. } else {
  2360. $append[k] = v;
  2361. }
  2362. });
  2363. callback = value;
  2364. } else {
  2365. $append[list_name] = value;
  2366. }
  2367. data[APPEND_ACTION] = $append;
  2368. return this._send_request(data, callback);
  2369. };
  2370. /*
  2371. * Record that you have charged the current user a certain amount
  2372. * of money. Charges recorded with track_charge will appear in the
  2373. * Mixpanel revenue report.
  2374. *
  2375. * ### Usage:
  2376. *
  2377. * // charge a user $50
  2378. * mixpanel.people.track_charge(50);
  2379. *
  2380. * // charge a user $30.50 on the 2nd of january
  2381. * mixpanel.people.track_charge(30.50, { '$time': new Date('jan 1 2012') });
  2382. *
  2383. * @param {Number} amount The amount of money charged to the current user
  2384. * @param {Object} [properties] An associative array of properties associated with the charge
  2385. * @param {Function} [callback] If provided, the callback will be called when the server responds
  2386. */
  2387. MixpanelPeople.prototype.track_charge = function(amount, properties, callback) {
  2388. if (!_.isNumber(amount)) {
  2389. amount = parseFloat(amount);
  2390. if (isNaN(amount)) {
  2391. console.error("Invalid value passed to mixpanel.people.track_charge - must be a number");
  2392. return;
  2393. }
  2394. }
  2395. return this.append('$transactions', _.extend({
  2396. '$amount': amount
  2397. }, properties), callback);
  2398. };
  2399. /*
  2400. * Permanently clear all revenue report transactions from the
  2401. * current user's people analytics profile.
  2402. *
  2403. * ### Usage:
  2404. *
  2405. * mixpanel.people.clear_charges();
  2406. *
  2407. * @param {Function} [callback] If provided, the callback will be called after the tracking event
  2408. */
  2409. MixpanelPeople.prototype.clear_charges = function(callback) {
  2410. return this.set('$transactions', [], callback);
  2411. };
  2412. /*
  2413. * Permanently deletes the current people analytics profile from
  2414. * Mixpanel (using the current distinct_id).
  2415. *
  2416. * ### Usage:
  2417. *
  2418. * // remove the all data you have stored about the current user
  2419. * mixpanel.people.delete_user();
  2420. *
  2421. */
  2422. MixpanelPeople.prototype.delete_user = function() {
  2423. if (!this._identify_called()) {
  2424. console.error('mixpanel.people.delete_user() requires you to call identify() first');
  2425. return;
  2426. }
  2427. var data = {'$delete': this._mixpanel.get_distinct_id()};
  2428. return this._send_request(data);
  2429. };
  2430. MixpanelPeople.prototype.toString = function() {
  2431. return this._mixpanel.toString() + ".people";
  2432. };
  2433. MixpanelPeople.prototype._send_request = function(data, callback) {
  2434. data['$token'] = this._get_config('token');
  2435. data['$distinct_id'] = this._mixpanel.get_distinct_id();
  2436. var date_encoded_data = _.encodeDates(data)
  2437. , truncated_data = _.truncate(date_encoded_data, 255)
  2438. , json_data = _.JSONEncode(date_encoded_data)
  2439. , encoded_data = _.base64Encode(json_data);
  2440. if (!this._identify_called()) {
  2441. this._enqueue(data);
  2442. if (!_.isUndefined(callback)) {
  2443. if (this._get_config('verbose')) {
  2444. callback({ status: -1, error: null });
  2445. } else {
  2446. callback(-1);
  2447. }
  2448. }
  2449. return truncated_data;
  2450. }
  2451. console.log("MIXPANEL PEOPLE REQUEST:");
  2452. console.log(truncated_data);
  2453. this._mixpanel._send_request(
  2454. this._get_config('api_host') + '/engage/',
  2455. { 'data': encoded_data },
  2456. this._mixpanel._prepare_callback(callback, truncated_data)
  2457. );
  2458. return truncated_data;
  2459. };
  2460. MixpanelPeople.prototype._get_config = function(conf_var) {
  2461. return this._mixpanel.get_config(conf_var);
  2462. };
  2463. MixpanelPeople.prototype._identify_called = function() {
  2464. return this._mixpanel._flags.identify_called === true;
  2465. };
  2466. // Queue up engage operations if identify hasn't been called yet.
  2467. MixpanelPeople.prototype._enqueue = function(data) {
  2468. if (SET_ACTION in data) {
  2469. this._mixpanel.cookie._add_to_people_queue(SET_ACTION, data);
  2470. } else if (SET_ONCE_ACTION in data) {
  2471. this._mixpanel.cookie._add_to_people_queue(SET_ONCE_ACTION, data);
  2472. } else if (ADD_ACTION in data) {
  2473. this._mixpanel.cookie._add_to_people_queue(ADD_ACTION, data);
  2474. } else if (APPEND_ACTION in data) {
  2475. this._mixpanel.cookie._add_to_people_queue(APPEND_ACTION, data);
  2476. } else {
  2477. console.error("Invalid call to _enqueue():", data);
  2478. }
  2479. };
  2480. // Flush queued engage operations - order does not matter,
  2481. // and there are network level race conditions anyway
  2482. MixpanelPeople.prototype._flush = function(_set_callback, _add_callback, _append_callback, _set_once_callback) {
  2483. var _this = this,
  2484. $set_queue = _.extend({}, this._mixpanel.cookie._get_queue(SET_ACTION)),
  2485. $set_once_queue = _.extend({}, this._mixpanel.cookie._get_queue(SET_ONCE_ACTION)),
  2486. $add_queue = _.extend({}, this._mixpanel.cookie._get_queue(ADD_ACTION)),
  2487. $append_queue = this._mixpanel.cookie._get_queue(APPEND_ACTION);
  2488. if (!_.isUndefined($set_queue) && _.isObject($set_queue) && !_.isEmptyObject($set_queue)) {
  2489. _this._mixpanel.cookie._pop_from_people_queue(SET_ACTION, $set_queue);
  2490. this.set($set_queue, function(response, data) {
  2491. // on bad response, we want to add it back to the queue
  2492. if (response == 0) {
  2493. _this._mixpanel.cookie._add_to_people_queue(SET_ACTION, $set_queue);
  2494. }
  2495. if (!_.isUndefined(_set_callback)) {
  2496. _set_callback(response, data);
  2497. }
  2498. });
  2499. }
  2500. if (!_.isUndefined($set_once_queue) && _.isObject($set_once_queue) && !_.isEmptyObject($set_once_queue)) {
  2501. _this._mixpanel.cookie._pop_from_people_queue(SET_ONCE_ACTION, $set_once_queue);
  2502. this.set_once($set_once_queue, function(response, data) {
  2503. // on bad response, we want to add it back to the queue
  2504. if (response == 0) {
  2505. _this._mixpanel.cookie._add_to_people_queue(SET_ONCE_ACTION, $set_once_queue);
  2506. }
  2507. if (!_.isUndefined(_set_once_callback)) {
  2508. _set_once_callback(response, data);
  2509. }
  2510. });
  2511. }
  2512. if (!_.isUndefined($add_queue) && _.isObject($add_queue) && !_.isEmptyObject($add_queue)) {
  2513. _this._mixpanel.cookie._pop_from_people_queue(ADD_ACTION, $add_queue);
  2514. this.increment($add_queue, function(response, data) {
  2515. // on bad response, we want to add it back to the queue
  2516. if (response == 0) {
  2517. _this._mixpanel.cookie._add_to_people_queue(ADD_ACTION, $add_queue);
  2518. }
  2519. if (!_.isUndefined(_add_callback)) {
  2520. _add_callback(response, data);
  2521. }
  2522. });
  2523. }
  2524. // we have to fire off each $append individually since there is
  2525. // no concat method server side
  2526. if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) {
  2527. for (var i = $append_queue.length - 1; i >= 0; i--) {
  2528. var $append_item = $append_queue.pop();
  2529. _this.append($append_item, function(response, data) {
  2530. if (response == 0) {
  2531. _this._mixpanel.cookie._add_to_people_queue(APPEND_ACTION, $append_item);
  2532. }
  2533. if (!_.isUndefined(_append_callback)) { _append_callback(response, data); }
  2534. });
  2535. };
  2536. // Save the shortened append queue
  2537. _this._mixpanel.cookie.save();
  2538. }
  2539. };
  2540. // EXPORTS (for closure compiler)
  2541. // Underscore Exports
  2542. _['toArray'] = _.toArray;
  2543. _['isObject'] = _.isObject;
  2544. _['JSONEncode'] = _.JSONEncode;
  2545. _['JSONDecode'] = _.JSONDecode;
  2546. _['isBlockedUA'] = _.isBlockedUA;
  2547. _['isEmptyObject'] = _.isEmptyObject;
  2548. _['info'] = _.info;
  2549. _['info']['device'] = _.info.device;
  2550. _['info']['browser'] = _.info.browser;
  2551. // MixpanelLib Exports
  2552. MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
  2553. MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
  2554. MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
  2555. MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links;
  2556. MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms;
  2557. MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview;
  2558. MixpanelLib.prototype['register'] = MixpanelLib.prototype.register;
  2559. MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once;
  2560. MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister;
  2561. MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify;
  2562. MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias;
  2563. MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag;
  2564. MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config;
  2565. MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config;
  2566. MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property;
  2567. MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id;
  2568. MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString;
  2569. // MixpanelCookie Exports
  2570. MixpanelCookie.prototype['properties'] = MixpanelCookie.prototype.properties;
  2571. MixpanelCookie.prototype['update_search_keyword'] = MixpanelCookie.prototype.update_search_keyword;
  2572. MixpanelCookie.prototype['update_referrer_info'] = MixpanelCookie.prototype.update_referrer_info;
  2573. MixpanelCookie.prototype['get_cross_subdomain'] = MixpanelCookie.prototype.get_cross_subdomain;
  2574. MixpanelCookie.prototype['clear'] = MixpanelCookie.prototype.clear;
  2575. // MixpanelPeople Exports
  2576. MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set;
  2577. MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once;
  2578. MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment;
  2579. MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append;
  2580. MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge;
  2581. MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges;
  2582. MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user;
  2583. MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString;
  2584. // Initialization
  2585. if (_.isUndefined(mixpanel)) {
  2586. // mixpanel wasn't initialized properly, report error and quit
  2587. console.critical("'mixpanel' object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.");
  2588. return;
  2589. }
  2590. if (mixpanel['__loaded'] || (mixpanel['config'] && mixpanel['cookie'])) {
  2591. // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
  2592. console.error("Mixpanel library has already been downloaded at least once.");
  2593. return;
  2594. }
  2595. if (SNIPPET_VERSION < 1.1) {
  2596. // mixpanel wasn't initialized properly, report error and quit
  2597. console.critical("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet.");
  2598. return;
  2599. }
  2600. // Load instances of the Mixpanel Library
  2601. var instances = {};
  2602. _.each(mixpanel['_i'], function(item) {
  2603. var name, instance;
  2604. if (item && _.isArray(item)) {
  2605. name = item[item.length-1];
  2606. instance = create_mplib.apply(this, item);
  2607. instances[name] = instance;
  2608. }
  2609. });
  2610. var extend_mp = function() {
  2611. // add all the sub mixpanel instances
  2612. _.each(instances, function(instance, name) {
  2613. if (name !== PRIMARY_INSTANCE_NAME) { mixpanel[name] = instance; }
  2614. });
  2615. // add private functions as _
  2616. mixpanel['_'] = _;
  2617. };
  2618. // we override the snippets init function to handle the case where a
  2619. // user initializes the mixpanel library after the script loads & runs
  2620. mixpanel['init'] = function(token, config, name) {
  2621. if (name) {
  2622. // initialize a sub library
  2623. if (!mixpanel[name]) {
  2624. mixpanel[name] = instances[name] = create_mplib(token, config, name);
  2625. mixpanel[name]._loaded();
  2626. }
  2627. } else {
  2628. var instance = mixpanel;
  2629. if (instances[PRIMARY_INSTANCE_NAME]) {
  2630. // main mixpanel lib already initialized
  2631. instance = instances[PRIMARY_INSTANCE_NAME];
  2632. } else if (token) {
  2633. // intialize the main mixpanel lib
  2634. instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME);
  2635. }
  2636. window[PRIMARY_INSTANCE_NAME] = mixpanel = instance;
  2637. extend_mp();
  2638. }
  2639. };
  2640. mixpanel['init']();
  2641. // Fire loaded events after updating the window's mixpanel object
  2642. _.each(instances, function(instance) {
  2643. instance._loaded();
  2644. });
  2645. // Cross browser DOM Loaded support
  2646. function dom_loaded_handler() {
  2647. // function flag since we only want to execute this once
  2648. if (dom_loaded_handler.done) { return; }
  2649. dom_loaded_handler.done = true;
  2650. DOM_LOADED = true;
  2651. ENQUEUE_REQUESTS = false;
  2652. _.each(instances, function(inst) {
  2653. inst._dom_loaded();
  2654. });
  2655. }
  2656. if (document.addEventListener) {
  2657. if (document.readyState == "complete") {
  2658. // safari 4 can fire the DOMContentLoaded event before loading all
  2659. // external JS (including this file). you will see some copypasta
  2660. // on the internet that checks for 'complete' and 'loaded', but
  2661. // 'loaded' is an IE thing
  2662. dom_loaded_handler();
  2663. } else {
  2664. document.addEventListener("DOMContentLoaded", dom_loaded_handler, false);
  2665. }
  2666. } else if (document.attachEvent) {
  2667. // IE
  2668. document.attachEvent("onreadystatechange", dom_loaded_handler);
  2669. // check to make sure we arn't in a frame
  2670. var toplevel = false;
  2671. try {
  2672. toplevel = window.frameElement == null;
  2673. } catch(e) {}
  2674. if (document.documentElement.doScroll && toplevel) {
  2675. function do_scroll_check() {
  2676. try {
  2677. document.documentElement.doScroll("left");
  2678. } catch(e) {
  2679. setTimeout(do_scroll_check, 1);
  2680. return;
  2681. }
  2682. dom_loaded_handler();
  2683. };
  2684. do_scroll_check();
  2685. }
  2686. }
  2687. // fallback handler, always will work
  2688. _.register_event(window, 'load', dom_loaded_handler, true);
  2689. })(window['mixpanel']);