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.
@@ -6,7 +6,7 @@
6
6
 
7
7
  var Config = {
8
8
  DEBUG: false,
9
- LIB_VERSION: '2.40.0'
9
+ LIB_VERSION: '2.42.1'
10
10
  };
11
11
 
12
12
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -59,7 +59,7 @@
59
59
  };
60
60
 
61
61
  // Console override
62
- var console$1 = {
62
+ var console = {
63
63
  /** @type {function(...*)} */
64
64
  log: function() {
65
65
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
@@ -73,6 +73,19 @@
73
73
  }
74
74
  },
75
75
  /** @type {function(...*)} */
76
+ warn: function() {
77
+ if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
78
+ var args = ['Mixpanel warning:'].concat(_.toArray(arguments));
79
+ try {
80
+ windowConsole.warn.apply(windowConsole, args);
81
+ } catch (err) {
82
+ _.each(args, function(arg) {
83
+ windowConsole.warn(arg);
84
+ });
85
+ }
86
+ }
87
+ },
88
+ /** @type {function(...*)} */
76
89
  error: function() {
77
90
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
78
91
  var args = ['Mixpanel error:'].concat(_.toArray(arguments));
@@ -103,14 +116,14 @@
103
116
  var log_func_with_prefix = function(func, prefix) {
104
117
  return function() {
105
118
  arguments[0] = '[' + prefix + '] ' + arguments[0];
106
- return func.apply(console$1, arguments);
119
+ return func.apply(console, arguments);
107
120
  };
108
121
  };
109
122
  var console_with_prefix = function(prefix) {
110
123
  return {
111
- log: log_func_with_prefix(console$1.log, prefix),
112
- error: log_func_with_prefix(console$1.error, prefix),
113
- critical: log_func_with_prefix(console$1.critical, prefix)
124
+ log: log_func_with_prefix(console.log, prefix),
125
+ error: log_func_with_prefix(console.error, prefix),
126
+ critical: log_func_with_prefix(console.critical, prefix)
114
127
  };
115
128
  };
116
129
 
@@ -272,10 +285,6 @@
272
285
  return results;
273
286
  };
274
287
 
275
- _.identity = function(value) {
276
- return value;
277
- };
278
-
279
288
  _.include = function(obj, target) {
280
289
  var found = false;
281
290
  if (obj === null) {
@@ -376,9 +385,9 @@
376
385
  try {
377
386
  return f.apply(this, arguments);
378
387
  } catch (e) {
379
- console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
388
+ console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
380
389
  if (Config.DEBUG){
381
- console$1.critical(e);
390
+ console.critical(e);
382
391
  }
383
392
  }
384
393
  };
@@ -938,9 +947,39 @@
938
947
  // _.isBlockedUA()
939
948
  // This is to block various web spiders from executing our JS and
940
949
  // sending false tracking data
950
+ var BLOCKED_UA_STRS = [
951
+ 'ahrefsbot',
952
+ 'baiduspider',
953
+ 'bingbot',
954
+ 'bingpreview',
955
+ 'facebookexternal',
956
+ 'petalbot',
957
+ 'pinterest',
958
+ 'screaming frog',
959
+ 'yahoo! slurp',
960
+ 'yandexbot',
961
+
962
+ // a whole bunch of goog-specific crawlers
963
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
964
+ 'adsbot-google',
965
+ 'apis-google',
966
+ 'duplexweb-google',
967
+ 'feedfetcher-google',
968
+ 'google favicon',
969
+ 'google web preview',
970
+ 'google-read-aloud',
971
+ 'googlebot',
972
+ 'googleweblight',
973
+ 'mediapartners-google',
974
+ 'storebot-google'
975
+ ];
941
976
  _.isBlockedUA = function(ua) {
942
- if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) {
943
- return true;
977
+ var i;
978
+ ua = ua.toLowerCase();
979
+ for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
980
+ if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
981
+ return true;
982
+ }
944
983
  }
945
984
  return false;
946
985
  };
@@ -979,16 +1018,12 @@
979
1018
  try {
980
1019
  result = decodeURIComponent(result);
981
1020
  } catch(err) {
982
- console$1.error('Skipping decoding for malformed query param: ' + result);
1021
+ console.error('Skipping decoding for malformed query param: ' + result);
983
1022
  }
984
1023
  return result.replace(/\+/g, ' ');
985
1024
  }
986
1025
  };
987
1026
 
988
- _.getHashParam = function(hash, param) {
989
- var matches = hash.match(new RegExp(param + '=([^&]*)'));
990
- return matches ? matches[1] : null;
991
- };
992
1027
 
993
1028
  // _.cookie
994
1029
  // Methods partially borrowed from quirksmode.org/js/cookies.html
@@ -1110,13 +1145,13 @@
1110
1145
  is_supported: function(force_check) {
1111
1146
  var supported = localStorageSupported(null, force_check);
1112
1147
  if (!supported) {
1113
- console$1.error('localStorage unsupported; falling back to cookie store');
1148
+ console.error('localStorage unsupported; falling back to cookie store');
1114
1149
  }
1115
1150
  return supported;
1116
1151
  },
1117
1152
 
1118
1153
  error: function(msg) {
1119
- console$1.error('localStorage error: ' + msg);
1154
+ console.error('localStorage error: ' + msg);
1120
1155
  },
1121
1156
 
1122
1157
  get: function(name) {
@@ -1171,7 +1206,7 @@
1171
1206
  */
1172
1207
  var register_event = function(element, type, handler, oldSchool, useCapture) {
1173
1208
  if (!element) {
1174
- console$1.error('No valid element provided to register_event');
1209
+ console.error('No valid element provided to register_event');
1175
1210
  return;
1176
1211
  }
1177
1212
 
@@ -1655,28 +1690,6 @@
1655
1690
  return maxlen ? guid.substring(0, maxlen) : guid;
1656
1691
  };
1657
1692
 
1658
- /**
1659
- * Check deterministically whether to include or exclude from a feature rollout/test based on the
1660
- * given string and the desired percentage to include.
1661
- * @param {String} str - string to run the check against (for instance a project's token)
1662
- * @param {String} feature - name of feature (for inclusion in hash, to ensure different results
1663
- * for different features)
1664
- * @param {Number} percent_allowed - percentage chance that a given string will be included
1665
- * @returns {Boolean} whether the given string should be included
1666
- */
1667
- var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
1668
- str = str + feature;
1669
-
1670
- // Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
1671
- var hash = 5381;
1672
- for (var i = 0; i < str.length; i++) {
1673
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
1674
- hash = hash & hash;
1675
- }
1676
- var dart = (hash >>> 0) % 100;
1677
- return dart < percent_allowed;
1678
- });
1679
-
1680
1693
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1681
1694
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1682
1695
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1727,539 +1740,6 @@
1727
1740
  _['info']['browserVersion'] = _.info.browserVersion;
1728
1741
  _['info']['properties'] = _.info.properties;
1729
1742
 
1730
- /*
1731
- * Get the className of an element, accounting for edge cases where element.className is an object
1732
- * @param {Element} el - element to get the className of
1733
- * @returns {string} the element's class
1734
- */
1735
- function getClassName(el) {
1736
- switch(typeof el.className) {
1737
- case 'string':
1738
- return el.className;
1739
- case 'object': // handle cases where className might be SVGAnimatedString or some other type
1740
- return el.className.baseVal || el.getAttribute('class') || '';
1741
- default: // future proof
1742
- return '';
1743
- }
1744
- }
1745
-
1746
- /*
1747
- * Get the direct text content of an element, protecting against sensitive data collection.
1748
- * Concats textContent of each of the element's text node children; this avoids potential
1749
- * collection of sensitive data that could happen if we used element.textContent and the
1750
- * element had sensitive child elements, since element.textContent includes child content.
1751
- * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
1752
- * @param {Element} el - element to get the text of
1753
- * @returns {string} the element's direct text content
1754
- */
1755
- function getSafeText(el) {
1756
- var elText = '';
1757
-
1758
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
1759
- _.each(el.childNodes, function(child) {
1760
- if (isTextNode(child) && child.textContent) {
1761
- elText += _.trim(child.textContent)
1762
- // scrub potentially sensitive values
1763
- .split(/(\s+)/).filter(shouldTrackValue).join('')
1764
- // normalize whitespace
1765
- .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
1766
- // truncate
1767
- .substring(0, 255);
1768
- }
1769
- });
1770
- }
1771
-
1772
- return _.trim(elText);
1773
- }
1774
-
1775
- /*
1776
- * Check whether an element has nodeType Node.ELEMENT_NODE
1777
- * @param {Element} el - element to check
1778
- * @returns {boolean} whether el is of the correct nodeType
1779
- */
1780
- function isElementNode(el) {
1781
- return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
1782
- }
1783
-
1784
- /*
1785
- * Check whether an element is of a given tag type.
1786
- * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
1787
- * we want to match tagNames instead of specific references because something like
1788
- * element === document.body won't always work because element might not be a native
1789
- * element.
1790
- * @param {Element} el - element to check
1791
- * @param {string} tag - tag name (e.g., "div")
1792
- * @returns {boolean} whether el is of the given tag type
1793
- */
1794
- function isTag(el, tag) {
1795
- return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
1796
- }
1797
-
1798
- /*
1799
- * Check whether an element has nodeType Node.TEXT_NODE
1800
- * @param {Element} el - element to check
1801
- * @returns {boolean} whether el is of the correct nodeType
1802
- */
1803
- function isTextNode(el) {
1804
- return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
1805
- }
1806
-
1807
- /*
1808
- * Check whether a DOM event should be "tracked" or if it may contain sentitive data
1809
- * using a variety of heuristics.
1810
- * @param {Element} el - element to check
1811
- * @param {Event} event - event to check
1812
- * @returns {boolean} whether the event should be tracked
1813
- */
1814
- function shouldTrackDomEvent(el, event) {
1815
- if (!el || isTag(el, 'html') || !isElementNode(el)) {
1816
- return false;
1817
- }
1818
- var tag = el.tagName.toLowerCase();
1819
- switch (tag) {
1820
- case 'html':
1821
- return false;
1822
- case 'form':
1823
- return event.type === 'submit';
1824
- case 'input':
1825
- if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
1826
- return event.type === 'change';
1827
- } else {
1828
- return event.type === 'click';
1829
- }
1830
- case 'select':
1831
- case 'textarea':
1832
- return event.type === 'change';
1833
- default:
1834
- return event.type === 'click';
1835
- }
1836
- }
1837
-
1838
- /*
1839
- * Check whether a DOM element should be "tracked" or if it may contain sentitive data
1840
- * using a variety of heuristics.
1841
- * @param {Element} el - element to check
1842
- * @returns {boolean} whether the element should be tracked
1843
- */
1844
- function shouldTrackElement(el) {
1845
- for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
1846
- var classes = getClassName(curEl).split(' ');
1847
- if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) {
1848
- return false;
1849
- }
1850
- }
1851
-
1852
- if (_.includes(getClassName(el).split(' '), 'mp-include')) {
1853
- return true;
1854
- }
1855
-
1856
- // don't send data from inputs or similar elements since there will always be
1857
- // a risk of clientside javascript placing sensitive data in attributes
1858
- if (
1859
- isTag(el, 'input') ||
1860
- isTag(el, 'select') ||
1861
- isTag(el, 'textarea') ||
1862
- el.getAttribute('contenteditable') === 'true'
1863
- ) {
1864
- return false;
1865
- }
1866
-
1867
- // don't include hidden or password fields
1868
- var type = el.type || '';
1869
- 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"]
1870
- switch(type.toLowerCase()) {
1871
- case 'hidden':
1872
- return false;
1873
- case 'password':
1874
- return false;
1875
- }
1876
- }
1877
-
1878
- // filter out data from fields that look like sensitive fields
1879
- var name = el.name || el.id || '';
1880
- 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"]
1881
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
1882
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
1883
- return false;
1884
- }
1885
- }
1886
-
1887
- return true;
1888
- }
1889
-
1890
- /*
1891
- * Check whether a string value should be "tracked" or if it may contain sentitive data
1892
- * using a variety of heuristics.
1893
- * @param {string} value - string value to check
1894
- * @returns {boolean} whether the element should be tracked
1895
- */
1896
- function shouldTrackValue(value) {
1897
- if (value === null || _.isUndefined(value)) {
1898
- return false;
1899
- }
1900
-
1901
- if (typeof value === 'string') {
1902
- value = _.trim(value);
1903
-
1904
- // check to see if input value looks like a credit card number
1905
- // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
1906
- 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}))$/;
1907
- if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
1908
- return false;
1909
- }
1910
-
1911
- // check to see if input value looks like a social security number
1912
- var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
1913
- if (ssnRegex.test(value)) {
1914
- return false;
1915
- }
1916
- }
1917
-
1918
- return true;
1919
- }
1920
-
1921
- var autotrack = {
1922
- _initializedTokens: [],
1923
-
1924
- _previousElementSibling: function(el) {
1925
- if (el.previousElementSibling) {
1926
- return el.previousElementSibling;
1927
- } else {
1928
- do {
1929
- el = el.previousSibling;
1930
- } while (el && !isElementNode(el));
1931
- return el;
1932
- }
1933
- },
1934
-
1935
- _loadScript: function(scriptUrlToLoad, callback) {
1936
- var scriptTag = document.createElement('script');
1937
- scriptTag.type = 'text/javascript';
1938
- scriptTag.src = scriptUrlToLoad;
1939
- scriptTag.onload = callback;
1940
-
1941
- var scripts = document.getElementsByTagName('script');
1942
- if (scripts.length > 0) {
1943
- scripts[0].parentNode.insertBefore(scriptTag, scripts[0]);
1944
- } else {
1945
- document.body.appendChild(scriptTag);
1946
- }
1947
- },
1948
-
1949
- _getPropertiesFromElement: function(elem) {
1950
- var props = {
1951
- 'classes': getClassName(elem).split(' '),
1952
- 'tag_name': elem.tagName.toLowerCase()
1953
- };
1954
-
1955
- if (shouldTrackElement(elem)) {
1956
- _.each(elem.attributes, function(attr) {
1957
- if (shouldTrackValue(attr.value)) {
1958
- props['attr__' + attr.name] = attr.value;
1959
- }
1960
- });
1961
- }
1962
-
1963
- var nthChild = 1;
1964
- var nthOfType = 1;
1965
- var currentElem = elem;
1966
- while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
1967
- nthChild++;
1968
- if (currentElem.tagName === elem.tagName) {
1969
- nthOfType++;
1970
- }
1971
- }
1972
- props['nth_child'] = nthChild;
1973
- props['nth_of_type'] = nthOfType;
1974
-
1975
- return props;
1976
- },
1977
-
1978
- _getDefaultProperties: function(eventType) {
1979
- return {
1980
- '$event_type': eventType,
1981
- '$ce_version': 1,
1982
- '$host': window.location.host,
1983
- '$pathname': window.location.pathname
1984
- };
1985
- },
1986
-
1987
- _extractCustomPropertyValue: function(customProperty) {
1988
- var propValues = [];
1989
- _.each(document.querySelectorAll(customProperty['css_selector']), function(matchedElem) {
1990
- var value;
1991
-
1992
- if (['input', 'select'].indexOf(matchedElem.tagName.toLowerCase()) > -1) {
1993
- value = matchedElem['value'];
1994
- } else if (matchedElem['textContent']) {
1995
- value = matchedElem['textContent'];
1996
- }
1997
-
1998
- if (shouldTrackValue(value)) {
1999
- propValues.push(value);
2000
- }
2001
- });
2002
- return propValues.join(', ');
2003
- },
2004
-
2005
- _getCustomProperties: function(targetElementList) {
2006
- var props = {};
2007
- _.each(this._customProperties, function(customProperty) {
2008
- _.each(customProperty['event_selectors'], function(eventSelector) {
2009
- var eventElements = document.querySelectorAll(eventSelector);
2010
- _.each(eventElements, function(eventElement) {
2011
- if (_.includes(targetElementList, eventElement) && shouldTrackElement(eventElement)) {
2012
- props[customProperty['name']] = this._extractCustomPropertyValue(customProperty);
2013
- }
2014
- }, this);
2015
- }, this);
2016
- }, this);
2017
- return props;
2018
- },
2019
-
2020
- _getEventTarget: function(e) {
2021
- // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes
2022
- if (typeof e.target === 'undefined') {
2023
- return e.srcElement;
2024
- } else {
2025
- return e.target;
2026
- }
2027
- },
2028
-
2029
- _trackEvent: function(e, instance) {
2030
- /*** Don't mess with this code without running IE8 tests on it ***/
2031
- var target = this._getEventTarget(e);
2032
- if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
2033
- target = target.parentNode;
2034
- }
2035
-
2036
- if (shouldTrackDomEvent(target, e)) {
2037
- var targetElementList = [target];
2038
- var curEl = target;
2039
- while (curEl.parentNode && !isTag(curEl, 'body')) {
2040
- targetElementList.push(curEl.parentNode);
2041
- curEl = curEl.parentNode;
2042
- }
2043
-
2044
- var elementsJson = [];
2045
- var href, explicitNoTrack = false;
2046
- _.each(targetElementList, function(el) {
2047
- var shouldTrackEl = shouldTrackElement(el);
2048
-
2049
- // if the element or a parent element is an anchor tag
2050
- // include the href as a property
2051
- if (el.tagName.toLowerCase() === 'a') {
2052
- href = el.getAttribute('href');
2053
- href = shouldTrackEl && shouldTrackValue(href) && href;
2054
- }
2055
-
2056
- // allow users to programatically prevent tracking of elements by adding class 'mp-no-track'
2057
- var classes = getClassName(el).split(' ');
2058
- if (_.includes(classes, 'mp-no-track')) {
2059
- explicitNoTrack = true;
2060
- }
2061
-
2062
- elementsJson.push(this._getPropertiesFromElement(el));
2063
- }, this);
2064
-
2065
- if (explicitNoTrack) {
2066
- return false;
2067
- }
2068
-
2069
- // only populate text content from target element (not parents)
2070
- // to prevent text within a sensitive element from being collected
2071
- // as part of a parent's el.textContent
2072
- var elementText;
2073
- var safeElementText = getSafeText(target);
2074
- if (safeElementText && safeElementText.length) {
2075
- elementText = safeElementText;
2076
- }
2077
-
2078
- var props = _.extend(
2079
- this._getDefaultProperties(e.type),
2080
- {
2081
- '$elements': elementsJson,
2082
- '$el_attr__href': href,
2083
- '$el_text': elementText
2084
- },
2085
- this._getCustomProperties(targetElementList)
2086
- );
2087
-
2088
- instance.track('$web_event', props);
2089
- return true;
2090
- }
2091
- },
2092
-
2093
- // only reason is to stub for unit tests
2094
- // since you can't override window.location props
2095
- _navigate: function(href) {
2096
- window.location.href = href;
2097
- },
2098
-
2099
- _addDomEventHandlers: function(instance) {
2100
- var handler = _.bind(function(e) {
2101
- e = e || window.event;
2102
- this._trackEvent(e, instance);
2103
- }, this);
2104
- _.register_event(document, 'submit', handler, false, true);
2105
- _.register_event(document, 'change', handler, false, true);
2106
- _.register_event(document, 'click', handler, false, true);
2107
- },
2108
-
2109
- _customProperties: {},
2110
- init: function(instance) {
2111
- if (!(document && document.body)) {
2112
- console.log('document not ready yet, trying again in 500 milliseconds...');
2113
- var that = this;
2114
- setTimeout(function() { that.init(instance); }, 500);
2115
- return;
2116
- }
2117
-
2118
- var token = instance.get_config('token');
2119
- if (this._initializedTokens.indexOf(token) > -1) {
2120
- console.log('autotrack already initialized for token "' + token + '"');
2121
- return;
2122
- }
2123
- this._initializedTokens.push(token);
2124
-
2125
- if (!this._maybeLoadEditor(instance)) { // don't autotrack actions when the editor is enabled
2126
- var parseDecideResponse = _.bind(function(response) {
2127
- if (response && response['config'] && response['config']['enable_collect_everything'] === true) {
2128
-
2129
- if (response['custom_properties']) {
2130
- this._customProperties = response['custom_properties'];
2131
- }
2132
-
2133
- instance.track('$web_event', _.extend({
2134
- '$title': document.title
2135
- }, this._getDefaultProperties('pageview')));
2136
-
2137
- this._addDomEventHandlers(instance);
2138
-
2139
- } else {
2140
- instance['__autotrack_enabled'] = false;
2141
- }
2142
- }, this);
2143
-
2144
- instance._send_request(
2145
- instance.get_config('api_host') + '/decide/', {
2146
- 'verbose': true,
2147
- 'version': '1',
2148
- 'lib': 'web',
2149
- 'token': token
2150
- },
2151
- {method: 'GET', transport: 'XHR'},
2152
- instance._prepare_callback(parseDecideResponse)
2153
- );
2154
- }
2155
- },
2156
-
2157
- _editorParamsFromHash: function(instance, hash) {
2158
- var editorParams;
2159
- try {
2160
- var state = _.getHashParam(hash, 'state');
2161
- state = JSON.parse(decodeURIComponent(state));
2162
- var expiresInSeconds = _.getHashParam(hash, 'expires_in');
2163
- editorParams = {
2164
- 'accessToken': _.getHashParam(hash, 'access_token'),
2165
- 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000),
2166
- 'bookmarkletMode': !!state['bookmarkletMode'],
2167
- 'projectId': state['projectId'],
2168
- 'projectOwnerId': state['projectOwnerId'],
2169
- 'projectToken': state['token'],
2170
- 'readOnly': state['readOnly'],
2171
- 'userFlags': state['userFlags'],
2172
- 'userId': state['userId']
2173
- };
2174
- window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams));
2175
-
2176
- if (state['desiredHash']) {
2177
- window.location.hash = state['desiredHash'];
2178
- } else if (window.history) {
2179
- history.replaceState('', document.title, window.location.pathname + window.location.search); // completely remove hash
2180
- } else {
2181
- window.location.hash = ''; // clear hash (but leaves # unfortunately)
2182
- }
2183
- } catch (e) {
2184
- console.error('Unable to parse data from hash', e);
2185
- }
2186
- return editorParams;
2187
- },
2188
-
2189
- /**
2190
- * To load the visual editor, we need an access token and other state. That state comes from one of three places:
2191
- * 1. In the URL hash params if the customer is using an old snippet
2192
- * 2. From session storage under the key `_mpcehash` if the snippet already parsed the hash
2193
- * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page
2194
- */
2195
- _maybeLoadEditor: function(instance) {
2196
- try {
2197
- var parseFromUrl = false;
2198
- if (_.getHashParam(window.location.hash, 'state')) {
2199
- var state = _.getHashParam(window.location.hash, 'state');
2200
- state = JSON.parse(decodeURIComponent(state));
2201
- parseFromUrl = state['action'] === 'mpeditor';
2202
- }
2203
- var parseFromStorage = !!window.sessionStorage.getItem('_mpcehash');
2204
- var editorParams;
2205
-
2206
- if (parseFromUrl) { // happens if they are initializing the editor using an old snippet
2207
- editorParams = this._editorParamsFromHash(instance, window.location.hash);
2208
- } else if (parseFromStorage) { // happens if they are initialized the editor and using the new snippet
2209
- editorParams = this._editorParamsFromHash(instance, window.sessionStorage.getItem('_mpcehash'));
2210
- window.sessionStorage.removeItem('_mpcehash');
2211
- } else { // get credentials from sessionStorage from a previous initialzation
2212
- editorParams = JSON.parse(window.sessionStorage.getItem('editorParams') || '{}');
2213
- }
2214
-
2215
- if (editorParams['projectToken'] && instance.get_config('token') === editorParams['projectToken']) {
2216
- this._loadEditor(instance, editorParams);
2217
- return true;
2218
- } else {
2219
- return false;
2220
- }
2221
- } catch (e) {
2222
- return false;
2223
- }
2224
- },
2225
-
2226
- _loadEditor: function(instance, editorParams) {
2227
- if (!window['_mpEditorLoaded']) { // only load the codeless event editor once, even if there are multiple instances of MixpanelLib
2228
- window['_mpEditorLoaded'] = true;
2229
- var editorUrl = instance.get_config('app_host')
2230
- + '/js-bundle/reports/collect-everything/editor.js?_ts='
2231
- + (new Date()).getTime();
2232
- this._loadScript(editorUrl, function() {
2233
- window['mp_load_editor'](editorParams);
2234
- });
2235
- return true;
2236
- }
2237
- return false;
2238
- },
2239
-
2240
- // this is a mechanism to ramp up CE with no server-side interaction.
2241
- // when CE is active, every page load results in a decide request. we
2242
- // need to gently ramp this up so we don't overload decide. this decides
2243
- // deterministically if CE is enabled for this project by modding the char
2244
- // value of the project token.
2245
- enabledForProject: function(token, numBuckets, numEnabledBuckets) {
2246
- numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10;
2247
- numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10;
2248
- var charCodeSum = 0;
2249
- for (var i = 0; i < token.length; i++) {
2250
- charCodeSum += token.charCodeAt(i);
2251
- }
2252
- return (charCodeSum % numBuckets) < numEnabledBuckets;
2253
- },
2254
-
2255
- isBrowserSupported: function() {
2256
- return _.isFunction(document.querySelectorAll);
2257
- }
2258
- };
2259
-
2260
- _.bind_instance_methods(autotrack);
2261
- _.safewrap_instance_methods(autotrack);
2262
-
2263
1743
  /**
2264
1744
  * DomTracker Object
2265
1745
  * @constructor
@@ -2288,7 +1768,7 @@
2288
1768
  var elements = _.dom_query(query);
2289
1769
 
2290
1770
  if (elements.length === 0) {
2291
- console$1.error('The DOM query (' + query + ') returned 0 elements');
1771
+ console.error('The DOM query (' + query + ') returned 0 elements');
2292
1772
  return;
2293
1773
  }
2294
1774
 
@@ -2948,9 +2428,9 @@
2948
2428
  } else if (
2949
2429
  _.isObject(res) &&
2950
2430
  res.xhr_req &&
2951
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
2431
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
2952
2432
  ) {
2953
- // network or API error, retry
2433
+ // network or API error, or 429 Too Many Requests, retry
2954
2434
  var retryMS = this.flushInterval * 2;
2955
2435
  var headers = res.xhr_req['responseHeaders'];
2956
2436
  if (headers) {
@@ -3080,9 +2560,14 @@
3080
2560
  */
3081
2561
  function hasOptedOut(token, options) {
3082
2562
  if (_hasDoNotTrackFlagOn(options)) {
2563
+ 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"');
3083
2564
  return true;
3084
2565
  }
3085
- return _getStorageValue(token, options) === '0';
2566
+ var optedOut = _getStorageValue(token, options) === '0';
2567
+ if (optedOut) {
2568
+ console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.');
2569
+ }
2570
+ return optedOut;
3086
2571
  }
3087
2572
 
3088
2573
  /**
@@ -3515,9 +3000,13 @@
3515
3000
  * Permanently delete a group.
3516
3001
  *
3517
3002
  * ### Usage:
3003
+ *
3518
3004
  * mixpanel.get_group('company', 'mixpanel').delete();
3005
+ *
3006
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
3519
3007
  */
3520
3008
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
3009
+ // bracket notation above prevents a minification error related to reserved words
3521
3010
  var data = this.delete_action();
3522
3011
  return this._send_request(data, callback);
3523
3012
  });
@@ -3617,7 +3106,7 @@
3617
3106
 
3618
3107
  var storage_type = config['persistence'];
3619
3108
  if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
3620
- console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3109
+ console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3621
3110
  storage_type = config['persistence'] = 'cookie';
3622
3111
  }
3623
3112
 
@@ -3975,8 +3464,8 @@
3975
3464
  this._pop_from_people_queue(UNSET_ACTION, q_data);
3976
3465
  }
3977
3466
 
3978
- console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3979
- console$1.log(data);
3467
+ console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3468
+ console.log(data);
3980
3469
 
3981
3470
  this.save();
3982
3471
  };
@@ -4019,7 +3508,7 @@
4019
3508
  } else if (queue === UNION_ACTION) {
4020
3509
  return UNION_QUEUE_KEY;
4021
3510
  } else {
4022
- console$1.error('Invalid queue:', queue);
3511
+ console.error('Invalid queue:', queue);
4023
3512
  }
4024
3513
  };
4025
3514
 
@@ -5987,7 +5476,7 @@
5987
5476
  _.each(prop, function(v, k) {
5988
5477
  if (!this._is_reserved_property(k)) {
5989
5478
  if (isNaN(parseFloat(v))) {
5990
- console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5479
+ console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5991
5480
  return;
5992
5481
  } else {
5993
5482
  $add[k] = v;
@@ -6111,7 +5600,7 @@
6111
5600
  if (!_.isNumber(amount)) {
6112
5601
  amount = parseFloat(amount);
6113
5602
  if (isNaN(amount)) {
6114
- console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
5603
+ console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
6115
5604
  return;
6116
5605
  }
6117
5606
  }
@@ -6147,7 +5636,7 @@
6147
5636
  */
6148
5637
  MixpanelPeople.prototype.delete_user = function() {
6149
5638
  if (!this._identify_called()) {
6150
- console$1.error('mixpanel.people.delete_user() requires you to call identify() first');
5639
+ console.error('mixpanel.people.delete_user() requires you to call identify() first');
6151
5640
  return;
6152
5641
  }
6153
5642
  var data = {'$delete': this._mixpanel.get_distinct_id()};
@@ -6221,7 +5710,7 @@
6221
5710
  } else if (UNION_ACTION in data) {
6222
5711
  this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
6223
5712
  } else {
6224
- console$1.error('Invalid call to _enqueue():', data);
5713
+ console.error('Invalid call to _enqueue():', data);
6225
5714
  }
6226
5715
  };
6227
5716
 
@@ -6389,7 +5878,6 @@
6389
5878
  'api_method': 'POST',
6390
5879
  'api_transport': 'XHR',
6391
5880
  'app_host': 'https://mixpanel.com',
6392
- 'autotrack': true,
6393
5881
  'cdn': 'https://cdn.mxpnl.com',
6394
5882
  'cross_site_cookie': false,
6395
5883
  'cross_subdomain_cookie': true,
@@ -6420,7 +5908,7 @@
6420
5908
  'inapp_protocol': '//',
6421
5909
  'inapp_link_new_window': false,
6422
5910
  'ignore_dnt': false,
6423
- 'batch_requests': false, // for now
5911
+ 'batch_requests': true,
6424
5912
  'batch_size': 50,
6425
5913
  'batch_flush_interval_ms': 5000,
6426
5914
  'batch_request_timeout_ms': 90000,
@@ -6453,7 +5941,7 @@
6453
5941
  instance = target;
6454
5942
  } else {
6455
5943
  if (target && !_.isArray(target)) {
6456
- console$1.error('You have already initialized ' + name);
5944
+ console.error('You have already initialized ' + name);
6457
5945
  return;
6458
5946
  }
6459
5947
  instance = new MixpanelLib();
@@ -6472,21 +5960,6 @@
6472
5960
  // global debug to be true
6473
5961
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
6474
5962
 
6475
- instance['__autotrack_enabled'] = instance.get_config('autotrack');
6476
- if (instance.get_config('autotrack')) {
6477
- var num_buckets = 100;
6478
- var num_enabled_buckets = 100;
6479
- if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
6480
- instance['__autotrack_enabled'] = false;
6481
- console$1.log('Not in active bucket: disabling Automatic Event Collection.');
6482
- } else if (!autotrack.isBrowserSupported()) {
6483
- instance['__autotrack_enabled'] = false;
6484
- console$1.log('Disabling Automatic Event Collection because this browser is not supported');
6485
- } else {
6486
- autotrack.init(instance);
6487
- }
6488
- }
6489
-
6490
5963
  // if target is not defined, we called init after the lib already
6491
5964
  // loaded, so there won't be an array of things to execute
6492
5965
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -6525,11 +5998,11 @@
6525
5998
  */
6526
5999
  MixpanelLib.prototype.init = function (token, config, name) {
6527
6000
  if (_.isUndefined(name)) {
6528
- console$1.error('You must name your new library: init(token, config, name)');
6001
+ console.error('You must name your new library: init(token, config, name)');
6529
6002
  return;
6530
6003
  }
6531
6004
  if (name === PRIMARY_INSTANCE_NAME) {
6532
- console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6005
+ console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6533
6006
  return;
6534
6007
  }
6535
6008
 
@@ -6554,17 +6027,7 @@
6554
6027
  this['config'] = {};
6555
6028
  this['_triggered_notifs'] = [];
6556
6029
 
6557
- // rollout: enable batch_requests by default for 60% of projects
6558
- // (only if they have not specified a value in their init config
6559
- // and they aren't using a custom API host)
6560
- var variable_features = {};
6561
- var api_host = config['api_host'];
6562
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
6563
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 60)) {
6564
- variable_features['batch_requests'] = true;
6565
- }
6566
-
6567
- this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
6030
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
6568
6031
  'name': name,
6569
6032
  'token': token,
6570
6033
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
@@ -6586,19 +6049,36 @@
6586
6049
  if (this._batch_requests) {
6587
6050
  if (!_.localStorage.is_supported(true) || !USE_XHR) {
6588
6051
  this._batch_requests = false;
6589
- console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6052
+ console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6590
6053
  } else {
6591
6054
  this.init_batchers();
6592
6055
  if (sendBeacon && window$1.addEventListener) {
6593
- window$1.addEventListener('unload', _.bind(function() {
6594
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
6595
- // Since sendBeacon doesn't report success/failure, events will not be removed from
6596
- // the persistent store; if the site is loaded again, the events will be flushed again
6597
- // on startup and deduplicated on the Mixpanel server side.
6056
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
6057
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
6058
+ // events will not be removed from the persistent store; if the site is loaded again,
6059
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
6060
+ // side.
6061
+ // There is no reliable way to capture only page close events, so we lean on the
6062
+ // visibilitychange and pagehide events as recommended at
6063
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
6064
+ // These events fire when the user clicks away from the current page/tab, so will occur
6065
+ // more frequently than page unload, but are the only mechanism currently for capturing
6066
+ // this scenario somewhat reliably.
6067
+ var flush_on_unload = _.bind(function() {
6598
6068
  if (!this.request_batchers.events.stopped) {
6599
6069
  this.request_batchers.events.flush({unloading: true});
6600
6070
  }
6601
- }, this));
6071
+ }, this);
6072
+ window$1.addEventListener('pagehide', function(ev) {
6073
+ if (ev['persisted']) {
6074
+ flush_on_unload();
6075
+ }
6076
+ });
6077
+ window$1.addEventListener('visibilitychange', function() {
6078
+ if (document$1['visibilityState'] === 'hidden') {
6079
+ flush_on_unload();
6080
+ }
6081
+ });
6602
6082
  }
6603
6083
  }
6604
6084
  }
@@ -6654,7 +6134,7 @@
6654
6134
 
6655
6135
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
6656
6136
  if (this.get_config('img')) {
6657
- console$1.error('You can\'t use DOM tracking functions with img = true.');
6137
+ console.error('You can\'t use DOM tracking functions with img = true.');
6658
6138
  return false;
6659
6139
  }
6660
6140
 
@@ -6764,7 +6244,7 @@
6764
6244
  try {
6765
6245
  succeeded = sendBeacon(url, body_data);
6766
6246
  } catch (e) {
6767
- console$1.error(e);
6247
+ console.error(e);
6768
6248
  succeeded = false;
6769
6249
  }
6770
6250
  try {
@@ -6772,7 +6252,7 @@
6772
6252
  callback(succeeded ? 1 : 0);
6773
6253
  }
6774
6254
  } catch (e) {
6775
- console$1.error(e);
6255
+ console.error(e);
6776
6256
  }
6777
6257
  } else if (USE_XHR) {
6778
6258
  try {
@@ -6804,7 +6284,7 @@
6804
6284
  try {
6805
6285
  response = _.JSONDecode(req.responseText);
6806
6286
  } catch (e) {
6807
- console$1.error(e);
6287
+ console.error(e);
6808
6288
  if (options.ignore_json_errors) {
6809
6289
  response = req.responseText;
6810
6290
  } else {
@@ -6827,7 +6307,7 @@
6827
6307
  } else {
6828
6308
  error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
6829
6309
  }
6830
- console$1.error(error);
6310
+ console.error(error);
6831
6311
  if (callback) {
6832
6312
  if (verbose_mode) {
6833
6313
  callback({status: 0, error: error, xhr_req: req});
@@ -6840,7 +6320,7 @@
6840
6320
  };
6841
6321
  req.send(body_data);
6842
6322
  } catch (e) {
6843
- console$1.error(e);
6323
+ console.error(e);
6844
6324
  succeeded = false;
6845
6325
  }
6846
6326
  } else {
@@ -7012,8 +6492,8 @@
7012
6492
  truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
7013
6493
  }
7014
6494
  if (truncated_data) {
7015
- console$1.log('MIXPANEL REQUEST:');
7016
- console$1.log(truncated_data);
6495
+ console.log('MIXPANEL REQUEST:');
6496
+ console.log(truncated_data);
7017
6497
  return this._send_request(
7018
6498
  endpoint,
7019
6499
  encode_data_for_request(truncated_data),
@@ -7079,7 +6559,7 @@
7079
6559
  }
7080
6560
 
7081
6561
  if (_.isUndefined(event_name)) {
7082
- console$1.error('No event name provided to mixpanel.track');
6562
+ console.error('No event name provided to mixpanel.track');
7083
6563
  return;
7084
6564
  }
7085
6565
 
@@ -7120,7 +6600,7 @@
7120
6600
  delete properties[blacklisted_prop];
7121
6601
  });
7122
6602
  } else {
7123
- console$1.error('Invalid value for property_blacklist config: ' + property_blacklist);
6603
+ console.error('Invalid value for property_blacklist config: ' + property_blacklist);
7124
6604
  }
7125
6605
 
7126
6606
  var data = {
@@ -7365,7 +6845,7 @@
7365
6845
  */
7366
6846
  MixpanelLib.prototype.time_event = function(event_name) {
7367
6847
  if (_.isUndefined(event_name)) {
7368
- console$1.error('No event name provided to mixpanel.time_event');
6848
+ console.error('No event name provided to mixpanel.time_event');
7369
6849
  return;
7370
6850
  }
7371
6851
 
@@ -7638,7 +7118,7 @@
7638
7118
  // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
7639
7119
  // this ID, as it will duplicate users.
7640
7120
  if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
7641
- console$1.critical('Attempting to create alias for existing People user - aborting.');
7121
+ console.critical('Attempting to create alias for existing People user - aborting.');
7642
7122
  return -2;
7643
7123
  }
7644
7124
 
@@ -7658,7 +7138,7 @@
7658
7138
  _this.identify(alias);
7659
7139
  });
7660
7140
  } else {
7661
- console$1.error('alias matches current distinct_id - skipping api call.');
7141
+ console.error('alias matches current distinct_id - skipping api call.');
7662
7142
  this.identify(alias);
7663
7143
  return -1;
7664
7144
  }
@@ -7841,7 +7321,7 @@
7841
7321
  MixpanelLib.prototype._run_hook = function(hook_name) {
7842
7322
  var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
7843
7323
  if (typeof ret === 'undefined') {
7844
- console$1.error(hook_name + ' hook did not return a value');
7324
+ console.error(hook_name + ' hook did not return a value');
7845
7325
  ret = null;
7846
7326
  }
7847
7327
  return ret;
@@ -7907,7 +7387,7 @@
7907
7387
  return;
7908
7388
  }
7909
7389
 
7910
- console$1.log('MIXPANEL NOTIFICATION CHECK');
7390
+ console.log('MIXPANEL NOTIFICATION CHECK');
7911
7391
 
7912
7392
  var data = {
7913
7393
  'verbose': true,