mixpanel-browser 2.40.0 → 2.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.40.0'
5
+ LIB_VERSION: '2.42.1'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -55,7 +55,7 @@ var _ = {
55
55
  };
56
56
 
57
57
  // Console override
58
- var console$1 = {
58
+ var console = {
59
59
  /** @type {function(...*)} */
60
60
  log: function() {
61
61
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
@@ -69,6 +69,19 @@ var console$1 = {
69
69
  }
70
70
  },
71
71
  /** @type {function(...*)} */
72
+ warn: function() {
73
+ if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
74
+ var args = ['Mixpanel warning:'].concat(_.toArray(arguments));
75
+ try {
76
+ windowConsole.warn.apply(windowConsole, args);
77
+ } catch (err) {
78
+ _.each(args, function(arg) {
79
+ windowConsole.warn(arg);
80
+ });
81
+ }
82
+ }
83
+ },
84
+ /** @type {function(...*)} */
72
85
  error: function() {
73
86
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
74
87
  var args = ['Mixpanel error:'].concat(_.toArray(arguments));
@@ -99,14 +112,14 @@ var console$1 = {
99
112
  var log_func_with_prefix = function(func, prefix) {
100
113
  return function() {
101
114
  arguments[0] = '[' + prefix + '] ' + arguments[0];
102
- return func.apply(console$1, arguments);
115
+ return func.apply(console, arguments);
103
116
  };
104
117
  };
105
118
  var console_with_prefix = function(prefix) {
106
119
  return {
107
- log: log_func_with_prefix(console$1.log, prefix),
108
- error: log_func_with_prefix(console$1.error, prefix),
109
- critical: log_func_with_prefix(console$1.critical, prefix)
120
+ log: log_func_with_prefix(console.log, prefix),
121
+ error: log_func_with_prefix(console.error, prefix),
122
+ critical: log_func_with_prefix(console.critical, prefix)
110
123
  };
111
124
  };
112
125
 
@@ -268,10 +281,6 @@ _.values = function(obj) {
268
281
  return results;
269
282
  };
270
283
 
271
- _.identity = function(value) {
272
- return value;
273
- };
274
-
275
284
  _.include = function(obj, target) {
276
285
  var found = false;
277
286
  if (obj === null) {
@@ -372,9 +381,9 @@ _.safewrap = function(f) {
372
381
  try {
373
382
  return f.apply(this, arguments);
374
383
  } catch (e) {
375
- console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
384
+ console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
376
385
  if (Config.DEBUG){
377
- console$1.critical(e);
386
+ console.critical(e);
378
387
  }
379
388
  }
380
389
  };
@@ -934,9 +943,39 @@ _.UUID = (function() {
934
943
  // _.isBlockedUA()
935
944
  // This is to block various web spiders from executing our JS and
936
945
  // sending false tracking data
946
+ var BLOCKED_UA_STRS = [
947
+ 'ahrefsbot',
948
+ 'baiduspider',
949
+ 'bingbot',
950
+ 'bingpreview',
951
+ 'facebookexternal',
952
+ 'petalbot',
953
+ 'pinterest',
954
+ 'screaming frog',
955
+ 'yahoo! slurp',
956
+ 'yandexbot',
957
+
958
+ // a whole bunch of goog-specific crawlers
959
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
960
+ 'adsbot-google',
961
+ 'apis-google',
962
+ 'duplexweb-google',
963
+ 'feedfetcher-google',
964
+ 'google favicon',
965
+ 'google web preview',
966
+ 'google-read-aloud',
967
+ 'googlebot',
968
+ 'googleweblight',
969
+ 'mediapartners-google',
970
+ 'storebot-google'
971
+ ];
937
972
  _.isBlockedUA = function(ua) {
938
- if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) {
939
- return true;
973
+ var i;
974
+ ua = ua.toLowerCase();
975
+ for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
976
+ if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
977
+ return true;
978
+ }
940
979
  }
941
980
  return false;
942
981
  };
@@ -975,16 +1014,12 @@ _.getQueryParam = function(url, param) {
975
1014
  try {
976
1015
  result = decodeURIComponent(result);
977
1016
  } catch(err) {
978
- console$1.error('Skipping decoding for malformed query param: ' + result);
1017
+ console.error('Skipping decoding for malformed query param: ' + result);
979
1018
  }
980
1019
  return result.replace(/\+/g, ' ');
981
1020
  }
982
1021
  };
983
1022
 
984
- _.getHashParam = function(hash, param) {
985
- var matches = hash.match(new RegExp(param + '=([^&]*)'));
986
- return matches ? matches[1] : null;
987
- };
988
1023
 
989
1024
  // _.cookie
990
1025
  // Methods partially borrowed from quirksmode.org/js/cookies.html
@@ -1106,13 +1141,13 @@ _.localStorage = {
1106
1141
  is_supported: function(force_check) {
1107
1142
  var supported = localStorageSupported(null, force_check);
1108
1143
  if (!supported) {
1109
- console$1.error('localStorage unsupported; falling back to cookie store');
1144
+ console.error('localStorage unsupported; falling back to cookie store');
1110
1145
  }
1111
1146
  return supported;
1112
1147
  },
1113
1148
 
1114
1149
  error: function(msg) {
1115
- console$1.error('localStorage error: ' + msg);
1150
+ console.error('localStorage error: ' + msg);
1116
1151
  },
1117
1152
 
1118
1153
  get: function(name) {
@@ -1167,7 +1202,7 @@ _.register_event = (function() {
1167
1202
  */
1168
1203
  var register_event = function(element, type, handler, oldSchool, useCapture) {
1169
1204
  if (!element) {
1170
- console$1.error('No valid element provided to register_event');
1205
+ console.error('No valid element provided to register_event');
1171
1206
  return;
1172
1207
  }
1173
1208
 
@@ -1651,28 +1686,6 @@ var cheap_guid = function(maxlen) {
1651
1686
  return maxlen ? guid.substring(0, maxlen) : guid;
1652
1687
  };
1653
1688
 
1654
- /**
1655
- * Check deterministically whether to include or exclude from a feature rollout/test based on the
1656
- * given string and the desired percentage to include.
1657
- * @param {String} str - string to run the check against (for instance a project's token)
1658
- * @param {String} feature - name of feature (for inclusion in hash, to ensure different results
1659
- * for different features)
1660
- * @param {Number} percent_allowed - percentage chance that a given string will be included
1661
- * @returns {Boolean} whether the given string should be included
1662
- */
1663
- var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
1664
- str = str + feature;
1665
-
1666
- // Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
1667
- var hash = 5381;
1668
- for (var i = 0; i < str.length; i++) {
1669
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
1670
- hash = hash & hash;
1671
- }
1672
- var dart = (hash >>> 0) % 100;
1673
- return dart < percent_allowed;
1674
- });
1675
-
1676
1689
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1677
1690
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1678
1691
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1723,539 +1736,6 @@ _['info']['browser'] = _.info.browser;
1723
1736
  _['info']['browserVersion'] = _.info.browserVersion;
1724
1737
  _['info']['properties'] = _.info.properties;
1725
1738
 
1726
- /*
1727
- * Get the className of an element, accounting for edge cases where element.className is an object
1728
- * @param {Element} el - element to get the className of
1729
- * @returns {string} the element's class
1730
- */
1731
- function getClassName(el) {
1732
- switch(typeof el.className) {
1733
- case 'string':
1734
- return el.className;
1735
- case 'object': // handle cases where className might be SVGAnimatedString or some other type
1736
- return el.className.baseVal || el.getAttribute('class') || '';
1737
- default: // future proof
1738
- return '';
1739
- }
1740
- }
1741
-
1742
- /*
1743
- * Get the direct text content of an element, protecting against sensitive data collection.
1744
- * Concats textContent of each of the element's text node children; this avoids potential
1745
- * collection of sensitive data that could happen if we used element.textContent and the
1746
- * element had sensitive child elements, since element.textContent includes child content.
1747
- * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
1748
- * @param {Element} el - element to get the text of
1749
- * @returns {string} the element's direct text content
1750
- */
1751
- function getSafeText(el) {
1752
- var elText = '';
1753
-
1754
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
1755
- _.each(el.childNodes, function(child) {
1756
- if (isTextNode(child) && child.textContent) {
1757
- elText += _.trim(child.textContent)
1758
- // scrub potentially sensitive values
1759
- .split(/(\s+)/).filter(shouldTrackValue).join('')
1760
- // normalize whitespace
1761
- .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
1762
- // truncate
1763
- .substring(0, 255);
1764
- }
1765
- });
1766
- }
1767
-
1768
- return _.trim(elText);
1769
- }
1770
-
1771
- /*
1772
- * Check whether an element has nodeType Node.ELEMENT_NODE
1773
- * @param {Element} el - element to check
1774
- * @returns {boolean} whether el is of the correct nodeType
1775
- */
1776
- function isElementNode(el) {
1777
- return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
1778
- }
1779
-
1780
- /*
1781
- * Check whether an element is of a given tag type.
1782
- * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
1783
- * we want to match tagNames instead of specific references because something like
1784
- * element === document.body won't always work because element might not be a native
1785
- * element.
1786
- * @param {Element} el - element to check
1787
- * @param {string} tag - tag name (e.g., "div")
1788
- * @returns {boolean} whether el is of the given tag type
1789
- */
1790
- function isTag(el, tag) {
1791
- return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
1792
- }
1793
-
1794
- /*
1795
- * Check whether an element has nodeType Node.TEXT_NODE
1796
- * @param {Element} el - element to check
1797
- * @returns {boolean} whether el is of the correct nodeType
1798
- */
1799
- function isTextNode(el) {
1800
- return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
1801
- }
1802
-
1803
- /*
1804
- * Check whether a DOM event should be "tracked" or if it may contain sentitive data
1805
- * using a variety of heuristics.
1806
- * @param {Element} el - element to check
1807
- * @param {Event} event - event to check
1808
- * @returns {boolean} whether the event should be tracked
1809
- */
1810
- function shouldTrackDomEvent(el, event) {
1811
- if (!el || isTag(el, 'html') || !isElementNode(el)) {
1812
- return false;
1813
- }
1814
- var tag = el.tagName.toLowerCase();
1815
- switch (tag) {
1816
- case 'html':
1817
- return false;
1818
- case 'form':
1819
- return event.type === 'submit';
1820
- case 'input':
1821
- if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
1822
- return event.type === 'change';
1823
- } else {
1824
- return event.type === 'click';
1825
- }
1826
- case 'select':
1827
- case 'textarea':
1828
- return event.type === 'change';
1829
- default:
1830
- return event.type === 'click';
1831
- }
1832
- }
1833
-
1834
- /*
1835
- * Check whether a DOM element should be "tracked" or if it may contain sentitive data
1836
- * using a variety of heuristics.
1837
- * @param {Element} el - element to check
1838
- * @returns {boolean} whether the element should be tracked
1839
- */
1840
- function shouldTrackElement(el) {
1841
- for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
1842
- var classes = getClassName(curEl).split(' ');
1843
- if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) {
1844
- return false;
1845
- }
1846
- }
1847
-
1848
- if (_.includes(getClassName(el).split(' '), 'mp-include')) {
1849
- return true;
1850
- }
1851
-
1852
- // don't send data from inputs or similar elements since there will always be
1853
- // a risk of clientside javascript placing sensitive data in attributes
1854
- if (
1855
- isTag(el, 'input') ||
1856
- isTag(el, 'select') ||
1857
- isTag(el, 'textarea') ||
1858
- el.getAttribute('contenteditable') === 'true'
1859
- ) {
1860
- return false;
1861
- }
1862
-
1863
- // don't include hidden or password fields
1864
- var type = el.type || '';
1865
- if (typeof type === 'string') { // it's possible for el.type to be a DOM element if el is a form with a child input[name="type"]
1866
- switch(type.toLowerCase()) {
1867
- case 'hidden':
1868
- return false;
1869
- case 'password':
1870
- return false;
1871
- }
1872
- }
1873
-
1874
- // filter out data from fields that look like sensitive fields
1875
- var name = el.name || el.id || '';
1876
- if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
1877
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
1878
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
1879
- return false;
1880
- }
1881
- }
1882
-
1883
- return true;
1884
- }
1885
-
1886
- /*
1887
- * Check whether a string value should be "tracked" or if it may contain sentitive data
1888
- * using a variety of heuristics.
1889
- * @param {string} value - string value to check
1890
- * @returns {boolean} whether the element should be tracked
1891
- */
1892
- function shouldTrackValue(value) {
1893
- if (value === null || _.isUndefined(value)) {
1894
- return false;
1895
- }
1896
-
1897
- if (typeof value === 'string') {
1898
- value = _.trim(value);
1899
-
1900
- // check to see if input value looks like a credit card number
1901
- // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
1902
- var ccRegex = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/;
1903
- if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
1904
- return false;
1905
- }
1906
-
1907
- // check to see if input value looks like a social security number
1908
- var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
1909
- if (ssnRegex.test(value)) {
1910
- return false;
1911
- }
1912
- }
1913
-
1914
- return true;
1915
- }
1916
-
1917
- var autotrack = {
1918
- _initializedTokens: [],
1919
-
1920
- _previousElementSibling: function(el) {
1921
- if (el.previousElementSibling) {
1922
- return el.previousElementSibling;
1923
- } else {
1924
- do {
1925
- el = el.previousSibling;
1926
- } while (el && !isElementNode(el));
1927
- return el;
1928
- }
1929
- },
1930
-
1931
- _loadScript: function(scriptUrlToLoad, callback) {
1932
- var scriptTag = document.createElement('script');
1933
- scriptTag.type = 'text/javascript';
1934
- scriptTag.src = scriptUrlToLoad;
1935
- scriptTag.onload = callback;
1936
-
1937
- var scripts = document.getElementsByTagName('script');
1938
- if (scripts.length > 0) {
1939
- scripts[0].parentNode.insertBefore(scriptTag, scripts[0]);
1940
- } else {
1941
- document.body.appendChild(scriptTag);
1942
- }
1943
- },
1944
-
1945
- _getPropertiesFromElement: function(elem) {
1946
- var props = {
1947
- 'classes': getClassName(elem).split(' '),
1948
- 'tag_name': elem.tagName.toLowerCase()
1949
- };
1950
-
1951
- if (shouldTrackElement(elem)) {
1952
- _.each(elem.attributes, function(attr) {
1953
- if (shouldTrackValue(attr.value)) {
1954
- props['attr__' + attr.name] = attr.value;
1955
- }
1956
- });
1957
- }
1958
-
1959
- var nthChild = 1;
1960
- var nthOfType = 1;
1961
- var currentElem = elem;
1962
- while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
1963
- nthChild++;
1964
- if (currentElem.tagName === elem.tagName) {
1965
- nthOfType++;
1966
- }
1967
- }
1968
- props['nth_child'] = nthChild;
1969
- props['nth_of_type'] = nthOfType;
1970
-
1971
- return props;
1972
- },
1973
-
1974
- _getDefaultProperties: function(eventType) {
1975
- return {
1976
- '$event_type': eventType,
1977
- '$ce_version': 1,
1978
- '$host': window.location.host,
1979
- '$pathname': window.location.pathname
1980
- };
1981
- },
1982
-
1983
- _extractCustomPropertyValue: function(customProperty) {
1984
- var propValues = [];
1985
- _.each(document.querySelectorAll(customProperty['css_selector']), function(matchedElem) {
1986
- var value;
1987
-
1988
- if (['input', 'select'].indexOf(matchedElem.tagName.toLowerCase()) > -1) {
1989
- value = matchedElem['value'];
1990
- } else if (matchedElem['textContent']) {
1991
- value = matchedElem['textContent'];
1992
- }
1993
-
1994
- if (shouldTrackValue(value)) {
1995
- propValues.push(value);
1996
- }
1997
- });
1998
- return propValues.join(', ');
1999
- },
2000
-
2001
- _getCustomProperties: function(targetElementList) {
2002
- var props = {};
2003
- _.each(this._customProperties, function(customProperty) {
2004
- _.each(customProperty['event_selectors'], function(eventSelector) {
2005
- var eventElements = document.querySelectorAll(eventSelector);
2006
- _.each(eventElements, function(eventElement) {
2007
- if (_.includes(targetElementList, eventElement) && shouldTrackElement(eventElement)) {
2008
- props[customProperty['name']] = this._extractCustomPropertyValue(customProperty);
2009
- }
2010
- }, this);
2011
- }, this);
2012
- }, this);
2013
- return props;
2014
- },
2015
-
2016
- _getEventTarget: function(e) {
2017
- // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes
2018
- if (typeof e.target === 'undefined') {
2019
- return e.srcElement;
2020
- } else {
2021
- return e.target;
2022
- }
2023
- },
2024
-
2025
- _trackEvent: function(e, instance) {
2026
- /*** Don't mess with this code without running IE8 tests on it ***/
2027
- var target = this._getEventTarget(e);
2028
- if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
2029
- target = target.parentNode;
2030
- }
2031
-
2032
- if (shouldTrackDomEvent(target, e)) {
2033
- var targetElementList = [target];
2034
- var curEl = target;
2035
- while (curEl.parentNode && !isTag(curEl, 'body')) {
2036
- targetElementList.push(curEl.parentNode);
2037
- curEl = curEl.parentNode;
2038
- }
2039
-
2040
- var elementsJson = [];
2041
- var href, explicitNoTrack = false;
2042
- _.each(targetElementList, function(el) {
2043
- var shouldTrackEl = shouldTrackElement(el);
2044
-
2045
- // if the element or a parent element is an anchor tag
2046
- // include the href as a property
2047
- if (el.tagName.toLowerCase() === 'a') {
2048
- href = el.getAttribute('href');
2049
- href = shouldTrackEl && shouldTrackValue(href) && href;
2050
- }
2051
-
2052
- // allow users to programatically prevent tracking of elements by adding class 'mp-no-track'
2053
- var classes = getClassName(el).split(' ');
2054
- if (_.includes(classes, 'mp-no-track')) {
2055
- explicitNoTrack = true;
2056
- }
2057
-
2058
- elementsJson.push(this._getPropertiesFromElement(el));
2059
- }, this);
2060
-
2061
- if (explicitNoTrack) {
2062
- return false;
2063
- }
2064
-
2065
- // only populate text content from target element (not parents)
2066
- // to prevent text within a sensitive element from being collected
2067
- // as part of a parent's el.textContent
2068
- var elementText;
2069
- var safeElementText = getSafeText(target);
2070
- if (safeElementText && safeElementText.length) {
2071
- elementText = safeElementText;
2072
- }
2073
-
2074
- var props = _.extend(
2075
- this._getDefaultProperties(e.type),
2076
- {
2077
- '$elements': elementsJson,
2078
- '$el_attr__href': href,
2079
- '$el_text': elementText
2080
- },
2081
- this._getCustomProperties(targetElementList)
2082
- );
2083
-
2084
- instance.track('$web_event', props);
2085
- return true;
2086
- }
2087
- },
2088
-
2089
- // only reason is to stub for unit tests
2090
- // since you can't override window.location props
2091
- _navigate: function(href) {
2092
- window.location.href = href;
2093
- },
2094
-
2095
- _addDomEventHandlers: function(instance) {
2096
- var handler = _.bind(function(e) {
2097
- e = e || window.event;
2098
- this._trackEvent(e, instance);
2099
- }, this);
2100
- _.register_event(document, 'submit', handler, false, true);
2101
- _.register_event(document, 'change', handler, false, true);
2102
- _.register_event(document, 'click', handler, false, true);
2103
- },
2104
-
2105
- _customProperties: {},
2106
- init: function(instance) {
2107
- if (!(document && document.body)) {
2108
- console.log('document not ready yet, trying again in 500 milliseconds...');
2109
- var that = this;
2110
- setTimeout(function() { that.init(instance); }, 500);
2111
- return;
2112
- }
2113
-
2114
- var token = instance.get_config('token');
2115
- if (this._initializedTokens.indexOf(token) > -1) {
2116
- console.log('autotrack already initialized for token "' + token + '"');
2117
- return;
2118
- }
2119
- this._initializedTokens.push(token);
2120
-
2121
- if (!this._maybeLoadEditor(instance)) { // don't autotrack actions when the editor is enabled
2122
- var parseDecideResponse = _.bind(function(response) {
2123
- if (response && response['config'] && response['config']['enable_collect_everything'] === true) {
2124
-
2125
- if (response['custom_properties']) {
2126
- this._customProperties = response['custom_properties'];
2127
- }
2128
-
2129
- instance.track('$web_event', _.extend({
2130
- '$title': document.title
2131
- }, this._getDefaultProperties('pageview')));
2132
-
2133
- this._addDomEventHandlers(instance);
2134
-
2135
- } else {
2136
- instance['__autotrack_enabled'] = false;
2137
- }
2138
- }, this);
2139
-
2140
- instance._send_request(
2141
- instance.get_config('api_host') + '/decide/', {
2142
- 'verbose': true,
2143
- 'version': '1',
2144
- 'lib': 'web',
2145
- 'token': token
2146
- },
2147
- {method: 'GET', transport: 'XHR'},
2148
- instance._prepare_callback(parseDecideResponse)
2149
- );
2150
- }
2151
- },
2152
-
2153
- _editorParamsFromHash: function(instance, hash) {
2154
- var editorParams;
2155
- try {
2156
- var state = _.getHashParam(hash, 'state');
2157
- state = JSON.parse(decodeURIComponent(state));
2158
- var expiresInSeconds = _.getHashParam(hash, 'expires_in');
2159
- editorParams = {
2160
- 'accessToken': _.getHashParam(hash, 'access_token'),
2161
- 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000),
2162
- 'bookmarkletMode': !!state['bookmarkletMode'],
2163
- 'projectId': state['projectId'],
2164
- 'projectOwnerId': state['projectOwnerId'],
2165
- 'projectToken': state['token'],
2166
- 'readOnly': state['readOnly'],
2167
- 'userFlags': state['userFlags'],
2168
- 'userId': state['userId']
2169
- };
2170
- window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams));
2171
-
2172
- if (state['desiredHash']) {
2173
- window.location.hash = state['desiredHash'];
2174
- } else if (window.history) {
2175
- history.replaceState('', document.title, window.location.pathname + window.location.search); // completely remove hash
2176
- } else {
2177
- window.location.hash = ''; // clear hash (but leaves # unfortunately)
2178
- }
2179
- } catch (e) {
2180
- console.error('Unable to parse data from hash', e);
2181
- }
2182
- return editorParams;
2183
- },
2184
-
2185
- /**
2186
- * To load the visual editor, we need an access token and other state. That state comes from one of three places:
2187
- * 1. In the URL hash params if the customer is using an old snippet
2188
- * 2. From session storage under the key `_mpcehash` if the snippet already parsed the hash
2189
- * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page
2190
- */
2191
- _maybeLoadEditor: function(instance) {
2192
- try {
2193
- var parseFromUrl = false;
2194
- if (_.getHashParam(window.location.hash, 'state')) {
2195
- var state = _.getHashParam(window.location.hash, 'state');
2196
- state = JSON.parse(decodeURIComponent(state));
2197
- parseFromUrl = state['action'] === 'mpeditor';
2198
- }
2199
- var parseFromStorage = !!window.sessionStorage.getItem('_mpcehash');
2200
- var editorParams;
2201
-
2202
- if (parseFromUrl) { // happens if they are initializing the editor using an old snippet
2203
- editorParams = this._editorParamsFromHash(instance, window.location.hash);
2204
- } else if (parseFromStorage) { // happens if they are initialized the editor and using the new snippet
2205
- editorParams = this._editorParamsFromHash(instance, window.sessionStorage.getItem('_mpcehash'));
2206
- window.sessionStorage.removeItem('_mpcehash');
2207
- } else { // get credentials from sessionStorage from a previous initialzation
2208
- editorParams = JSON.parse(window.sessionStorage.getItem('editorParams') || '{}');
2209
- }
2210
-
2211
- if (editorParams['projectToken'] && instance.get_config('token') === editorParams['projectToken']) {
2212
- this._loadEditor(instance, editorParams);
2213
- return true;
2214
- } else {
2215
- return false;
2216
- }
2217
- } catch (e) {
2218
- return false;
2219
- }
2220
- },
2221
-
2222
- _loadEditor: function(instance, editorParams) {
2223
- if (!window['_mpEditorLoaded']) { // only load the codeless event editor once, even if there are multiple instances of MixpanelLib
2224
- window['_mpEditorLoaded'] = true;
2225
- var editorUrl = instance.get_config('app_host')
2226
- + '/js-bundle/reports/collect-everything/editor.js?_ts='
2227
- + (new Date()).getTime();
2228
- this._loadScript(editorUrl, function() {
2229
- window['mp_load_editor'](editorParams);
2230
- });
2231
- return true;
2232
- }
2233
- return false;
2234
- },
2235
-
2236
- // this is a mechanism to ramp up CE with no server-side interaction.
2237
- // when CE is active, every page load results in a decide request. we
2238
- // need to gently ramp this up so we don't overload decide. this decides
2239
- // deterministically if CE is enabled for this project by modding the char
2240
- // value of the project token.
2241
- enabledForProject: function(token, numBuckets, numEnabledBuckets) {
2242
- numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10;
2243
- numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10;
2244
- var charCodeSum = 0;
2245
- for (var i = 0; i < token.length; i++) {
2246
- charCodeSum += token.charCodeAt(i);
2247
- }
2248
- return (charCodeSum % numBuckets) < numEnabledBuckets;
2249
- },
2250
-
2251
- isBrowserSupported: function() {
2252
- return _.isFunction(document.querySelectorAll);
2253
- }
2254
- };
2255
-
2256
- _.bind_instance_methods(autotrack);
2257
- _.safewrap_instance_methods(autotrack);
2258
-
2259
1739
  /**
2260
1740
  * DomTracker Object
2261
1741
  * @constructor
@@ -2284,7 +1764,7 @@ DomTracker.prototype.track = function(query, event_name, properties, user_callba
2284
1764
  var elements = _.dom_query(query);
2285
1765
 
2286
1766
  if (elements.length === 0) {
2287
- console$1.error('The DOM query (' + query + ') returned 0 elements');
1767
+ console.error('The DOM query (' + query + ') returned 0 elements');
2288
1768
  return;
2289
1769
  }
2290
1770
 
@@ -2944,9 +2424,9 @@ RequestBatcher.prototype.flush = function(options) {
2944
2424
  } else if (
2945
2425
  _.isObject(res) &&
2946
2426
  res.xhr_req &&
2947
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
2427
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
2948
2428
  ) {
2949
- // network or API error, retry
2429
+ // network or API error, or 429 Too Many Requests, retry
2950
2430
  var retryMS = this.flushInterval * 2;
2951
2431
  var headers = res.xhr_req['responseHeaders'];
2952
2432
  if (headers) {
@@ -3076,9 +2556,14 @@ function hasOptedIn(token, options) {
3076
2556
  */
3077
2557
  function hasOptedOut(token, options) {
3078
2558
  if (_hasDoNotTrackFlagOn(options)) {
2559
+ console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"');
3079
2560
  return true;
3080
2561
  }
3081
- return _getStorageValue(token, options) === '0';
2562
+ var optedOut = _getStorageValue(token, options) === '0';
2563
+ if (optedOut) {
2564
+ console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.');
2565
+ }
2566
+ return optedOut;
3082
2567
  }
3083
2568
 
3084
2569
  /**
@@ -3511,9 +2996,13 @@ MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name,
3511
2996
  * Permanently delete a group.
3512
2997
  *
3513
2998
  * ### Usage:
2999
+ *
3514
3000
  * mixpanel.get_group('company', 'mixpanel').delete();
3001
+ *
3002
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
3515
3003
  */
3516
3004
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
3005
+ // bracket notation above prevents a minification error related to reserved words
3517
3006
  var data = this.delete_action();
3518
3007
  return this._send_request(data, callback);
3519
3008
  });
@@ -3613,7 +3102,7 @@ var MixpanelPersistence = function(config) {
3613
3102
 
3614
3103
  var storage_type = config['persistence'];
3615
3104
  if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
3616
- console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3105
+ console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3617
3106
  storage_type = config['persistence'] = 'cookie';
3618
3107
  }
3619
3108
 
@@ -3971,8 +3460,8 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
3971
3460
  this._pop_from_people_queue(UNSET_ACTION, q_data);
3972
3461
  }
3973
3462
 
3974
- console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3975
- console$1.log(data);
3463
+ console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3464
+ console.log(data);
3976
3465
 
3977
3466
  this.save();
3978
3467
  };
@@ -4015,7 +3504,7 @@ MixpanelPersistence.prototype._get_queue_key = function(queue) {
4015
3504
  } else if (queue === UNION_ACTION) {
4016
3505
  return UNION_QUEUE_KEY;
4017
3506
  } else {
4018
- console$1.error('Invalid queue:', queue);
3507
+ console.error('Invalid queue:', queue);
4019
3508
  }
4020
3509
  };
4021
3510
 
@@ -5983,7 +5472,7 @@ MixpanelPeople.prototype.increment = addOptOutCheckMixpanelPeople(function(prop,
5983
5472
  _.each(prop, function(v, k) {
5984
5473
  if (!this._is_reserved_property(k)) {
5985
5474
  if (isNaN(parseFloat(v))) {
5986
- console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5475
+ console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5987
5476
  return;
5988
5477
  } else {
5989
5478
  $add[k] = v;
@@ -6107,7 +5596,7 @@ MixpanelPeople.prototype.track_charge = addOptOutCheckMixpanelPeople(function(am
6107
5596
  if (!_.isNumber(amount)) {
6108
5597
  amount = parseFloat(amount);
6109
5598
  if (isNaN(amount)) {
6110
- console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
5599
+ console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
6111
5600
  return;
6112
5601
  }
6113
5602
  }
@@ -6143,7 +5632,7 @@ MixpanelPeople.prototype.clear_charges = function(callback) {
6143
5632
  */
6144
5633
  MixpanelPeople.prototype.delete_user = function() {
6145
5634
  if (!this._identify_called()) {
6146
- console$1.error('mixpanel.people.delete_user() requires you to call identify() first');
5635
+ console.error('mixpanel.people.delete_user() requires you to call identify() first');
6147
5636
  return;
6148
5637
  }
6149
5638
  var data = {'$delete': this._mixpanel.get_distinct_id()};
@@ -6217,7 +5706,7 @@ MixpanelPeople.prototype._enqueue = function(data) {
6217
5706
  } else if (UNION_ACTION in data) {
6218
5707
  this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
6219
5708
  } else {
6220
- console$1.error('Invalid call to _enqueue():', data);
5709
+ console.error('Invalid call to _enqueue():', data);
6221
5710
  }
6222
5711
  };
6223
5712
 
@@ -6385,7 +5874,6 @@ var DEFAULT_CONFIG = {
6385
5874
  'api_method': 'POST',
6386
5875
  'api_transport': 'XHR',
6387
5876
  'app_host': 'https://mixpanel.com',
6388
- 'autotrack': true,
6389
5877
  'cdn': 'https://cdn.mxpnl.com',
6390
5878
  'cross_site_cookie': false,
6391
5879
  'cross_subdomain_cookie': true,
@@ -6416,7 +5904,7 @@ var DEFAULT_CONFIG = {
6416
5904
  'inapp_protocol': '//',
6417
5905
  'inapp_link_new_window': false,
6418
5906
  'ignore_dnt': false,
6419
- 'batch_requests': false, // for now
5907
+ 'batch_requests': true,
6420
5908
  'batch_size': 50,
6421
5909
  'batch_flush_interval_ms': 5000,
6422
5910
  'batch_request_timeout_ms': 90000,
@@ -6449,7 +5937,7 @@ var create_mplib = function(token, config, name) {
6449
5937
  instance = target;
6450
5938
  } else {
6451
5939
  if (target && !_.isArray(target)) {
6452
- console$1.error('You have already initialized ' + name);
5940
+ console.error('You have already initialized ' + name);
6453
5941
  return;
6454
5942
  }
6455
5943
  instance = new MixpanelLib();
@@ -6468,21 +5956,6 @@ var create_mplib = function(token, config, name) {
6468
5956
  // global debug to be true
6469
5957
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
6470
5958
 
6471
- instance['__autotrack_enabled'] = instance.get_config('autotrack');
6472
- if (instance.get_config('autotrack')) {
6473
- var num_buckets = 100;
6474
- var num_enabled_buckets = 100;
6475
- if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
6476
- instance['__autotrack_enabled'] = false;
6477
- console$1.log('Not in active bucket: disabling Automatic Event Collection.');
6478
- } else if (!autotrack.isBrowserSupported()) {
6479
- instance['__autotrack_enabled'] = false;
6480
- console$1.log('Disabling Automatic Event Collection because this browser is not supported');
6481
- } else {
6482
- autotrack.init(instance);
6483
- }
6484
- }
6485
-
6486
5959
  // if target is not defined, we called init after the lib already
6487
5960
  // loaded, so there won't be an array of things to execute
6488
5961
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -6521,11 +5994,11 @@ var encode_data_for_request = function(data) {
6521
5994
  */
6522
5995
  MixpanelLib.prototype.init = function (token, config, name) {
6523
5996
  if (_.isUndefined(name)) {
6524
- console$1.error('You must name your new library: init(token, config, name)');
5997
+ console.error('You must name your new library: init(token, config, name)');
6525
5998
  return;
6526
5999
  }
6527
6000
  if (name === PRIMARY_INSTANCE_NAME) {
6528
- console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6001
+ console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6529
6002
  return;
6530
6003
  }
6531
6004
 
@@ -6550,17 +6023,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
6550
6023
  this['config'] = {};
6551
6024
  this['_triggered_notifs'] = [];
6552
6025
 
6553
- // rollout: enable batch_requests by default for 60% of projects
6554
- // (only if they have not specified a value in their init config
6555
- // and they aren't using a custom API host)
6556
- var variable_features = {};
6557
- var api_host = config['api_host'];
6558
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
6559
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 60)) {
6560
- variable_features['batch_requests'] = true;
6561
- }
6562
-
6563
- this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
6026
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
6564
6027
  'name': name,
6565
6028
  'token': token,
6566
6029
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
@@ -6582,19 +6045,36 @@ MixpanelLib.prototype._init = function(token, config, name) {
6582
6045
  if (this._batch_requests) {
6583
6046
  if (!_.localStorage.is_supported(true) || !USE_XHR) {
6584
6047
  this._batch_requests = false;
6585
- console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6048
+ console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6586
6049
  } else {
6587
6050
  this.init_batchers();
6588
6051
  if (sendBeacon && window$1.addEventListener) {
6589
- window$1.addEventListener('unload', _.bind(function() {
6590
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
6591
- // Since sendBeacon doesn't report success/failure, events will not be removed from
6592
- // the persistent store; if the site is loaded again, the events will be flushed again
6593
- // on startup and deduplicated on the Mixpanel server side.
6052
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
6053
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
6054
+ // events will not be removed from the persistent store; if the site is loaded again,
6055
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
6056
+ // side.
6057
+ // There is no reliable way to capture only page close events, so we lean on the
6058
+ // visibilitychange and pagehide events as recommended at
6059
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
6060
+ // These events fire when the user clicks away from the current page/tab, so will occur
6061
+ // more frequently than page unload, but are the only mechanism currently for capturing
6062
+ // this scenario somewhat reliably.
6063
+ var flush_on_unload = _.bind(function() {
6594
6064
  if (!this.request_batchers.events.stopped) {
6595
6065
  this.request_batchers.events.flush({unloading: true});
6596
6066
  }
6597
- }, this));
6067
+ }, this);
6068
+ window$1.addEventListener('pagehide', function(ev) {
6069
+ if (ev['persisted']) {
6070
+ flush_on_unload();
6071
+ }
6072
+ });
6073
+ window$1.addEventListener('visibilitychange', function() {
6074
+ if (document$1['visibilityState'] === 'hidden') {
6075
+ flush_on_unload();
6076
+ }
6077
+ });
6598
6078
  }
6599
6079
  }
6600
6080
  }
@@ -6650,7 +6130,7 @@ MixpanelLib.prototype._dom_loaded = function() {
6650
6130
 
6651
6131
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
6652
6132
  if (this.get_config('img')) {
6653
- console$1.error('You can\'t use DOM tracking functions with img = true.');
6133
+ console.error('You can\'t use DOM tracking functions with img = true.');
6654
6134
  return false;
6655
6135
  }
6656
6136
 
@@ -6760,7 +6240,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
6760
6240
  try {
6761
6241
  succeeded = sendBeacon(url, body_data);
6762
6242
  } catch (e) {
6763
- console$1.error(e);
6243
+ console.error(e);
6764
6244
  succeeded = false;
6765
6245
  }
6766
6246
  try {
@@ -6768,7 +6248,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
6768
6248
  callback(succeeded ? 1 : 0);
6769
6249
  }
6770
6250
  } catch (e) {
6771
- console$1.error(e);
6251
+ console.error(e);
6772
6252
  }
6773
6253
  } else if (USE_XHR) {
6774
6254
  try {
@@ -6800,7 +6280,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
6800
6280
  try {
6801
6281
  response = _.JSONDecode(req.responseText);
6802
6282
  } catch (e) {
6803
- console$1.error(e);
6283
+ console.error(e);
6804
6284
  if (options.ignore_json_errors) {
6805
6285
  response = req.responseText;
6806
6286
  } else {
@@ -6823,7 +6303,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
6823
6303
  } else {
6824
6304
  error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
6825
6305
  }
6826
- console$1.error(error);
6306
+ console.error(error);
6827
6307
  if (callback) {
6828
6308
  if (verbose_mode) {
6829
6309
  callback({status: 0, error: error, xhr_req: req});
@@ -6836,7 +6316,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
6836
6316
  };
6837
6317
  req.send(body_data);
6838
6318
  } catch (e) {
6839
- console$1.error(e);
6319
+ console.error(e);
6840
6320
  succeeded = false;
6841
6321
  }
6842
6322
  } else {
@@ -7008,8 +6488,8 @@ MixpanelLib.prototype._track_or_batch = function(options, callback) {
7008
6488
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
7009
6489
  }
7010
6490
  if (truncated_data) {
7011
- console$1.log('MIXPANEL REQUEST:');
7012
- console$1.log(truncated_data);
6491
+ console.log('MIXPANEL REQUEST:');
6492
+ console.log(truncated_data);
7013
6493
  return this._send_request(
7014
6494
  endpoint,
7015
6495
  encode_data_for_request(truncated_data),
@@ -7075,7 +6555,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
7075
6555
  }
7076
6556
 
7077
6557
  if (_.isUndefined(event_name)) {
7078
- console$1.error('No event name provided to mixpanel.track');
6558
+ console.error('No event name provided to mixpanel.track');
7079
6559
  return;
7080
6560
  }
7081
6561
 
@@ -7116,7 +6596,7 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
7116
6596
  delete properties[blacklisted_prop];
7117
6597
  });
7118
6598
  } else {
7119
- console$1.error('Invalid value for property_blacklist config: ' + property_blacklist);
6599
+ console.error('Invalid value for property_blacklist config: ' + property_blacklist);
7120
6600
  }
7121
6601
 
7122
6602
  var data = {
@@ -7361,7 +6841,7 @@ MixpanelLib.prototype.track_forms = function() {
7361
6841
  */
7362
6842
  MixpanelLib.prototype.time_event = function(event_name) {
7363
6843
  if (_.isUndefined(event_name)) {
7364
- console$1.error('No event name provided to mixpanel.time_event');
6844
+ console.error('No event name provided to mixpanel.time_event');
7365
6845
  return;
7366
6846
  }
7367
6847
 
@@ -7634,7 +7114,7 @@ MixpanelLib.prototype.alias = function(alias, original) {
7634
7114
  // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
7635
7115
  // this ID, as it will duplicate users.
7636
7116
  if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
7637
- console$1.critical('Attempting to create alias for existing People user - aborting.');
7117
+ console.critical('Attempting to create alias for existing People user - aborting.');
7638
7118
  return -2;
7639
7119
  }
7640
7120
 
@@ -7654,7 +7134,7 @@ MixpanelLib.prototype.alias = function(alias, original) {
7654
7134
  _this.identify(alias);
7655
7135
  });
7656
7136
  } else {
7657
- console$1.error('alias matches current distinct_id - skipping api call.');
7137
+ console.error('alias matches current distinct_id - skipping api call.');
7658
7138
  this.identify(alias);
7659
7139
  return -1;
7660
7140
  }
@@ -7837,7 +7317,7 @@ MixpanelLib.prototype.get_config = function(prop_name) {
7837
7317
  MixpanelLib.prototype._run_hook = function(hook_name) {
7838
7318
  var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
7839
7319
  if (typeof ret === 'undefined') {
7840
- console$1.error(hook_name + ' hook did not return a value');
7320
+ console.error(hook_name + ' hook did not return a value');
7841
7321
  ret = null;
7842
7322
  }
7843
7323
  return ret;
@@ -7903,7 +7383,7 @@ MixpanelLib.prototype._check_and_handle_notifications = addOptOutCheckMixpanelLi
7903
7383
  return;
7904
7384
  }
7905
7385
 
7906
- console$1.log('MIXPANEL NOTIFICATION CHECK');
7386
+ console.log('MIXPANEL NOTIFICATION CHECK');
7907
7387
 
7908
7388
  var data = {
7909
7389
  'verbose': true,