mixpanel-browser 2.39.0 → 2.42.0

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.
@@ -3,7 +3,7 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.39.0'
6
+ LIB_VERSION: '2.42.0'
7
7
  };
8
8
 
9
9
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -56,7 +56,7 @@
56
56
  };
57
57
 
58
58
  // Console override
59
- var console$1 = {
59
+ var console = {
60
60
  /** @type {function(...*)} */
61
61
  log: function() {
62
62
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
@@ -70,6 +70,19 @@
70
70
  }
71
71
  },
72
72
  /** @type {function(...*)} */
73
+ warn: function() {
74
+ if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
75
+ var args = ['Mixpanel warning:'].concat(_.toArray(arguments));
76
+ try {
77
+ windowConsole.warn.apply(windowConsole, args);
78
+ } catch (err) {
79
+ _.each(args, function(arg) {
80
+ windowConsole.warn(arg);
81
+ });
82
+ }
83
+ }
84
+ },
85
+ /** @type {function(...*)} */
73
86
  error: function() {
74
87
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
75
88
  var args = ['Mixpanel error:'].concat(_.toArray(arguments));
@@ -100,14 +113,14 @@
100
113
  var log_func_with_prefix = function(func, prefix) {
101
114
  return function() {
102
115
  arguments[0] = '[' + prefix + '] ' + arguments[0];
103
- return func.apply(console$1, arguments);
116
+ return func.apply(console, arguments);
104
117
  };
105
118
  };
106
119
  var console_with_prefix = function(prefix) {
107
120
  return {
108
- log: log_func_with_prefix(console$1.log, prefix),
109
- error: log_func_with_prefix(console$1.error, prefix),
110
- critical: log_func_with_prefix(console$1.critical, prefix)
121
+ log: log_func_with_prefix(console.log, prefix),
122
+ error: log_func_with_prefix(console.error, prefix),
123
+ critical: log_func_with_prefix(console.critical, prefix)
111
124
  };
112
125
  };
113
126
 
@@ -235,13 +248,13 @@
235
248
  return _.values(iterable);
236
249
  };
237
250
 
238
- _.map = function(arr, callback) {
251
+ _.map = function(arr, callback, context) {
239
252
  if (nativeMap && arr.map === nativeMap) {
240
- return arr.map(callback);
253
+ return arr.map(callback, context);
241
254
  } else {
242
255
  var results = [];
243
256
  _.each(arr, function(item) {
244
- results.push(callback(item));
257
+ results.push(callback.call(context, item));
245
258
  });
246
259
  return results;
247
260
  }
@@ -269,10 +282,6 @@
269
282
  return results;
270
283
  };
271
284
 
272
- _.identity = function(value) {
273
- return value;
274
- };
275
-
276
285
  _.include = function(obj, target) {
277
286
  var found = false;
278
287
  if (obj === null) {
@@ -373,9 +382,9 @@
373
382
  try {
374
383
  return f.apply(this, arguments);
375
384
  } catch (e) {
376
- console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
385
+ console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
377
386
  if (Config.DEBUG){
378
- console$1.critical(e);
387
+ console.critical(e);
379
388
  }
380
389
  }
381
390
  };
@@ -935,9 +944,37 @@
935
944
  // _.isBlockedUA()
936
945
  // This is to block various web spiders from executing our JS and
937
946
  // sending false tracking data
947
+ var BLOCKED_UA_STRS = [
948
+ 'baiduspider',
949
+ 'bingbot',
950
+ 'bingpreview',
951
+ 'facebookexternal',
952
+ 'pinterest',
953
+ 'screaming frog',
954
+ 'yahoo! slurp',
955
+ 'yandexbot',
956
+
957
+ // a whole bunch of goog-specific crawlers
958
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
959
+ 'adsbot-google',
960
+ 'apis-google',
961
+ 'duplexweb-google',
962
+ 'feedfetcher-google',
963
+ 'google favicon',
964
+ 'google web preview',
965
+ 'google-read-aloud',
966
+ 'googlebot',
967
+ 'googleweblight',
968
+ 'mediapartners-google',
969
+ 'storebot-google'
970
+ ];
938
971
  _.isBlockedUA = function(ua) {
939
- if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) {
940
- return true;
972
+ var i;
973
+ ua = ua.toLowerCase();
974
+ for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
975
+ if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
976
+ return true;
977
+ }
941
978
  }
942
979
  return false;
943
980
  };
@@ -976,16 +1013,12 @@
976
1013
  try {
977
1014
  result = decodeURIComponent(result);
978
1015
  } catch(err) {
979
- console$1.error('Skipping decoding for malformed query param: ' + result);
1016
+ console.error('Skipping decoding for malformed query param: ' + result);
980
1017
  }
981
1018
  return result.replace(/\+/g, ' ');
982
1019
  }
983
1020
  };
984
1021
 
985
- _.getHashParam = function(hash, param) {
986
- var matches = hash.match(new RegExp(param + '=([^&]*)'));
987
- return matches ? matches[1] : null;
988
- };
989
1022
 
990
1023
  // _.cookie
991
1024
  // Methods partially borrowed from quirksmode.org/js/cookies.html
@@ -1107,13 +1140,13 @@
1107
1140
  is_supported: function(force_check) {
1108
1141
  var supported = localStorageSupported(null, force_check);
1109
1142
  if (!supported) {
1110
- console$1.error('localStorage unsupported; falling back to cookie store');
1143
+ console.error('localStorage unsupported; falling back to cookie store');
1111
1144
  }
1112
1145
  return supported;
1113
1146
  },
1114
1147
 
1115
1148
  error: function(msg) {
1116
- console$1.error('localStorage error: ' + msg);
1149
+ console.error('localStorage error: ' + msg);
1117
1150
  },
1118
1151
 
1119
1152
  get: function(name) {
@@ -1168,7 +1201,7 @@
1168
1201
  */
1169
1202
  var register_event = function(element, type, handler, oldSchool, useCapture) {
1170
1203
  if (!element) {
1171
- console$1.error('No valid element provided to register_event');
1204
+ console.error('No valid element provided to register_event');
1172
1205
  return;
1173
1206
  }
1174
1207
 
@@ -1652,28 +1685,6 @@
1652
1685
  return maxlen ? guid.substring(0, maxlen) : guid;
1653
1686
  };
1654
1687
 
1655
- /**
1656
- * Check deterministically whether to include or exclude from a feature rollout/test based on the
1657
- * given string and the desired percentage to include.
1658
- * @param {String} str - string to run the check against (for instance a project's token)
1659
- * @param {String} feature - name of feature (for inclusion in hash, to ensure different results
1660
- * for different features)
1661
- * @param {Number} percent_allowed - percentage chance that a given string will be included
1662
- * @returns {Boolean} whether the given string should be included
1663
- */
1664
- var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
1665
- str = str + feature;
1666
-
1667
- // Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
1668
- var hash = 5381;
1669
- for (var i = 0; i < str.length; i++) {
1670
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
1671
- hash = hash & hash;
1672
- }
1673
- var dart = (hash >>> 0) % 100;
1674
- return dart < percent_allowed;
1675
- });
1676
-
1677
1688
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1678
1689
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1679
1690
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1724,539 +1735,6 @@
1724
1735
  _['info']['browserVersion'] = _.info.browserVersion;
1725
1736
  _['info']['properties'] = _.info.properties;
1726
1737
 
1727
- /*
1728
- * Get the className of an element, accounting for edge cases where element.className is an object
1729
- * @param {Element} el - element to get the className of
1730
- * @returns {string} the element's class
1731
- */
1732
- function getClassName(el) {
1733
- switch(typeof el.className) {
1734
- case 'string':
1735
- return el.className;
1736
- case 'object': // handle cases where className might be SVGAnimatedString or some other type
1737
- return el.className.baseVal || el.getAttribute('class') || '';
1738
- default: // future proof
1739
- return '';
1740
- }
1741
- }
1742
-
1743
- /*
1744
- * Get the direct text content of an element, protecting against sensitive data collection.
1745
- * Concats textContent of each of the element's text node children; this avoids potential
1746
- * collection of sensitive data that could happen if we used element.textContent and the
1747
- * element had sensitive child elements, since element.textContent includes child content.
1748
- * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
1749
- * @param {Element} el - element to get the text of
1750
- * @returns {string} the element's direct text content
1751
- */
1752
- function getSafeText(el) {
1753
- var elText = '';
1754
-
1755
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
1756
- _.each(el.childNodes, function(child) {
1757
- if (isTextNode(child) && child.textContent) {
1758
- elText += _.trim(child.textContent)
1759
- // scrub potentially sensitive values
1760
- .split(/(\s+)/).filter(shouldTrackValue).join('')
1761
- // normalize whitespace
1762
- .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
1763
- // truncate
1764
- .substring(0, 255);
1765
- }
1766
- });
1767
- }
1768
-
1769
- return _.trim(elText);
1770
- }
1771
-
1772
- /*
1773
- * Check whether an element has nodeType Node.ELEMENT_NODE
1774
- * @param {Element} el - element to check
1775
- * @returns {boolean} whether el is of the correct nodeType
1776
- */
1777
- function isElementNode(el) {
1778
- return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
1779
- }
1780
-
1781
- /*
1782
- * Check whether an element is of a given tag type.
1783
- * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
1784
- * we want to match tagNames instead of specific references because something like
1785
- * element === document.body won't always work because element might not be a native
1786
- * element.
1787
- * @param {Element} el - element to check
1788
- * @param {string} tag - tag name (e.g., "div")
1789
- * @returns {boolean} whether el is of the given tag type
1790
- */
1791
- function isTag(el, tag) {
1792
- return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
1793
- }
1794
-
1795
- /*
1796
- * Check whether an element has nodeType Node.TEXT_NODE
1797
- * @param {Element} el - element to check
1798
- * @returns {boolean} whether el is of the correct nodeType
1799
- */
1800
- function isTextNode(el) {
1801
- return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
1802
- }
1803
-
1804
- /*
1805
- * Check whether a DOM event should be "tracked" or if it may contain sentitive data
1806
- * using a variety of heuristics.
1807
- * @param {Element} el - element to check
1808
- * @param {Event} event - event to check
1809
- * @returns {boolean} whether the event should be tracked
1810
- */
1811
- function shouldTrackDomEvent(el, event) {
1812
- if (!el || isTag(el, 'html') || !isElementNode(el)) {
1813
- return false;
1814
- }
1815
- var tag = el.tagName.toLowerCase();
1816
- switch (tag) {
1817
- case 'html':
1818
- return false;
1819
- case 'form':
1820
- return event.type === 'submit';
1821
- case 'input':
1822
- if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
1823
- return event.type === 'change';
1824
- } else {
1825
- return event.type === 'click';
1826
- }
1827
- case 'select':
1828
- case 'textarea':
1829
- return event.type === 'change';
1830
- default:
1831
- return event.type === 'click';
1832
- }
1833
- }
1834
-
1835
- /*
1836
- * Check whether a DOM element should be "tracked" or if it may contain sentitive data
1837
- * using a variety of heuristics.
1838
- * @param {Element} el - element to check
1839
- * @returns {boolean} whether the element should be tracked
1840
- */
1841
- function shouldTrackElement(el) {
1842
- for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
1843
- var classes = getClassName(curEl).split(' ');
1844
- if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) {
1845
- return false;
1846
- }
1847
- }
1848
-
1849
- if (_.includes(getClassName(el).split(' '), 'mp-include')) {
1850
- return true;
1851
- }
1852
-
1853
- // don't send data from inputs or similar elements since there will always be
1854
- // a risk of clientside javascript placing sensitive data in attributes
1855
- if (
1856
- isTag(el, 'input') ||
1857
- isTag(el, 'select') ||
1858
- isTag(el, 'textarea') ||
1859
- el.getAttribute('contenteditable') === 'true'
1860
- ) {
1861
- return false;
1862
- }
1863
-
1864
- // don't include hidden or password fields
1865
- var type = el.type || '';
1866
- 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"]
1867
- switch(type.toLowerCase()) {
1868
- case 'hidden':
1869
- return false;
1870
- case 'password':
1871
- return false;
1872
- }
1873
- }
1874
-
1875
- // filter out data from fields that look like sensitive fields
1876
- var name = el.name || el.id || '';
1877
- 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"]
1878
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
1879
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
1880
- return false;
1881
- }
1882
- }
1883
-
1884
- return true;
1885
- }
1886
-
1887
- /*
1888
- * Check whether a string value should be "tracked" or if it may contain sentitive data
1889
- * using a variety of heuristics.
1890
- * @param {string} value - string value to check
1891
- * @returns {boolean} whether the element should be tracked
1892
- */
1893
- function shouldTrackValue(value) {
1894
- if (value === null || _.isUndefined(value)) {
1895
- return false;
1896
- }
1897
-
1898
- if (typeof value === 'string') {
1899
- value = _.trim(value);
1900
-
1901
- // check to see if input value looks like a credit card number
1902
- // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
1903
- 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}))$/;
1904
- if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
1905
- return false;
1906
- }
1907
-
1908
- // check to see if input value looks like a social security number
1909
- var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
1910
- if (ssnRegex.test(value)) {
1911
- return false;
1912
- }
1913
- }
1914
-
1915
- return true;
1916
- }
1917
-
1918
- var autotrack = {
1919
- _initializedTokens: [],
1920
-
1921
- _previousElementSibling: function(el) {
1922
- if (el.previousElementSibling) {
1923
- return el.previousElementSibling;
1924
- } else {
1925
- do {
1926
- el = el.previousSibling;
1927
- } while (el && !isElementNode(el));
1928
- return el;
1929
- }
1930
- },
1931
-
1932
- _loadScript: function(scriptUrlToLoad, callback) {
1933
- var scriptTag = document.createElement('script');
1934
- scriptTag.type = 'text/javascript';
1935
- scriptTag.src = scriptUrlToLoad;
1936
- scriptTag.onload = callback;
1937
-
1938
- var scripts = document.getElementsByTagName('script');
1939
- if (scripts.length > 0) {
1940
- scripts[0].parentNode.insertBefore(scriptTag, scripts[0]);
1941
- } else {
1942
- document.body.appendChild(scriptTag);
1943
- }
1944
- },
1945
-
1946
- _getPropertiesFromElement: function(elem) {
1947
- var props = {
1948
- 'classes': getClassName(elem).split(' '),
1949
- 'tag_name': elem.tagName.toLowerCase()
1950
- };
1951
-
1952
- if (shouldTrackElement(elem)) {
1953
- _.each(elem.attributes, function(attr) {
1954
- if (shouldTrackValue(attr.value)) {
1955
- props['attr__' + attr.name] = attr.value;
1956
- }
1957
- });
1958
- }
1959
-
1960
- var nthChild = 1;
1961
- var nthOfType = 1;
1962
- var currentElem = elem;
1963
- while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
1964
- nthChild++;
1965
- if (currentElem.tagName === elem.tagName) {
1966
- nthOfType++;
1967
- }
1968
- }
1969
- props['nth_child'] = nthChild;
1970
- props['nth_of_type'] = nthOfType;
1971
-
1972
- return props;
1973
- },
1974
-
1975
- _getDefaultProperties: function(eventType) {
1976
- return {
1977
- '$event_type': eventType,
1978
- '$ce_version': 1,
1979
- '$host': window.location.host,
1980
- '$pathname': window.location.pathname
1981
- };
1982
- },
1983
-
1984
- _extractCustomPropertyValue: function(customProperty) {
1985
- var propValues = [];
1986
- _.each(document.querySelectorAll(customProperty['css_selector']), function(matchedElem) {
1987
- var value;
1988
-
1989
- if (['input', 'select'].indexOf(matchedElem.tagName.toLowerCase()) > -1) {
1990
- value = matchedElem['value'];
1991
- } else if (matchedElem['textContent']) {
1992
- value = matchedElem['textContent'];
1993
- }
1994
-
1995
- if (shouldTrackValue(value)) {
1996
- propValues.push(value);
1997
- }
1998
- });
1999
- return propValues.join(', ');
2000
- },
2001
-
2002
- _getCustomProperties: function(targetElementList) {
2003
- var props = {};
2004
- _.each(this._customProperties, function(customProperty) {
2005
- _.each(customProperty['event_selectors'], function(eventSelector) {
2006
- var eventElements = document.querySelectorAll(eventSelector);
2007
- _.each(eventElements, function(eventElement) {
2008
- if (_.includes(targetElementList, eventElement) && shouldTrackElement(eventElement)) {
2009
- props[customProperty['name']] = this._extractCustomPropertyValue(customProperty);
2010
- }
2011
- }, this);
2012
- }, this);
2013
- }, this);
2014
- return props;
2015
- },
2016
-
2017
- _getEventTarget: function(e) {
2018
- // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes
2019
- if (typeof e.target === 'undefined') {
2020
- return e.srcElement;
2021
- } else {
2022
- return e.target;
2023
- }
2024
- },
2025
-
2026
- _trackEvent: function(e, instance) {
2027
- /*** Don't mess with this code without running IE8 tests on it ***/
2028
- var target = this._getEventTarget(e);
2029
- if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
2030
- target = target.parentNode;
2031
- }
2032
-
2033
- if (shouldTrackDomEvent(target, e)) {
2034
- var targetElementList = [target];
2035
- var curEl = target;
2036
- while (curEl.parentNode && !isTag(curEl, 'body')) {
2037
- targetElementList.push(curEl.parentNode);
2038
- curEl = curEl.parentNode;
2039
- }
2040
-
2041
- var elementsJson = [];
2042
- var href, explicitNoTrack = false;
2043
- _.each(targetElementList, function(el) {
2044
- var shouldTrackEl = shouldTrackElement(el);
2045
-
2046
- // if the element or a parent element is an anchor tag
2047
- // include the href as a property
2048
- if (el.tagName.toLowerCase() === 'a') {
2049
- href = el.getAttribute('href');
2050
- href = shouldTrackEl && shouldTrackValue(href) && href;
2051
- }
2052
-
2053
- // allow users to programatically prevent tracking of elements by adding class 'mp-no-track'
2054
- var classes = getClassName(el).split(' ');
2055
- if (_.includes(classes, 'mp-no-track')) {
2056
- explicitNoTrack = true;
2057
- }
2058
-
2059
- elementsJson.push(this._getPropertiesFromElement(el));
2060
- }, this);
2061
-
2062
- if (explicitNoTrack) {
2063
- return false;
2064
- }
2065
-
2066
- // only populate text content from target element (not parents)
2067
- // to prevent text within a sensitive element from being collected
2068
- // as part of a parent's el.textContent
2069
- var elementText;
2070
- var safeElementText = getSafeText(target);
2071
- if (safeElementText && safeElementText.length) {
2072
- elementText = safeElementText;
2073
- }
2074
-
2075
- var props = _.extend(
2076
- this._getDefaultProperties(e.type),
2077
- {
2078
- '$elements': elementsJson,
2079
- '$el_attr__href': href,
2080
- '$el_text': elementText
2081
- },
2082
- this._getCustomProperties(targetElementList)
2083
- );
2084
-
2085
- instance.track('$web_event', props);
2086
- return true;
2087
- }
2088
- },
2089
-
2090
- // only reason is to stub for unit tests
2091
- // since you can't override window.location props
2092
- _navigate: function(href) {
2093
- window.location.href = href;
2094
- },
2095
-
2096
- _addDomEventHandlers: function(instance) {
2097
- var handler = _.bind(function(e) {
2098
- e = e || window.event;
2099
- this._trackEvent(e, instance);
2100
- }, this);
2101
- _.register_event(document, 'submit', handler, false, true);
2102
- _.register_event(document, 'change', handler, false, true);
2103
- _.register_event(document, 'click', handler, false, true);
2104
- },
2105
-
2106
- _customProperties: {},
2107
- init: function(instance) {
2108
- if (!(document && document.body)) {
2109
- console.log('document not ready yet, trying again in 500 milliseconds...');
2110
- var that = this;
2111
- setTimeout(function() { that.init(instance); }, 500);
2112
- return;
2113
- }
2114
-
2115
- var token = instance.get_config('token');
2116
- if (this._initializedTokens.indexOf(token) > -1) {
2117
- console.log('autotrack already initialized for token "' + token + '"');
2118
- return;
2119
- }
2120
- this._initializedTokens.push(token);
2121
-
2122
- if (!this._maybeLoadEditor(instance)) { // don't autotrack actions when the editor is enabled
2123
- var parseDecideResponse = _.bind(function(response) {
2124
- if (response && response['config'] && response['config']['enable_collect_everything'] === true) {
2125
-
2126
- if (response['custom_properties']) {
2127
- this._customProperties = response['custom_properties'];
2128
- }
2129
-
2130
- instance.track('$web_event', _.extend({
2131
- '$title': document.title
2132
- }, this._getDefaultProperties('pageview')));
2133
-
2134
- this._addDomEventHandlers(instance);
2135
-
2136
- } else {
2137
- instance['__autotrack_enabled'] = false;
2138
- }
2139
- }, this);
2140
-
2141
- instance._send_request(
2142
- instance.get_config('api_host') + '/decide/', {
2143
- 'verbose': true,
2144
- 'version': '1',
2145
- 'lib': 'web',
2146
- 'token': token
2147
- },
2148
- {method: 'GET', transport: 'XHR'},
2149
- instance._prepare_callback(parseDecideResponse)
2150
- );
2151
- }
2152
- },
2153
-
2154
- _editorParamsFromHash: function(instance, hash) {
2155
- var editorParams;
2156
- try {
2157
- var state = _.getHashParam(hash, 'state');
2158
- state = JSON.parse(decodeURIComponent(state));
2159
- var expiresInSeconds = _.getHashParam(hash, 'expires_in');
2160
- editorParams = {
2161
- 'accessToken': _.getHashParam(hash, 'access_token'),
2162
- 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000),
2163
- 'bookmarkletMode': !!state['bookmarkletMode'],
2164
- 'projectId': state['projectId'],
2165
- 'projectOwnerId': state['projectOwnerId'],
2166
- 'projectToken': state['token'],
2167
- 'readOnly': state['readOnly'],
2168
- 'userFlags': state['userFlags'],
2169
- 'userId': state['userId']
2170
- };
2171
- window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams));
2172
-
2173
- if (state['desiredHash']) {
2174
- window.location.hash = state['desiredHash'];
2175
- } else if (window.history) {
2176
- history.replaceState('', document.title, window.location.pathname + window.location.search); // completely remove hash
2177
- } else {
2178
- window.location.hash = ''; // clear hash (but leaves # unfortunately)
2179
- }
2180
- } catch (e) {
2181
- console.error('Unable to parse data from hash', e);
2182
- }
2183
- return editorParams;
2184
- },
2185
-
2186
- /**
2187
- * To load the visual editor, we need an access token and other state. That state comes from one of three places:
2188
- * 1. In the URL hash params if the customer is using an old snippet
2189
- * 2. From session storage under the key `_mpcehash` if the snippet already parsed the hash
2190
- * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page
2191
- */
2192
- _maybeLoadEditor: function(instance) {
2193
- try {
2194
- var parseFromUrl = false;
2195
- if (_.getHashParam(window.location.hash, 'state')) {
2196
- var state = _.getHashParam(window.location.hash, 'state');
2197
- state = JSON.parse(decodeURIComponent(state));
2198
- parseFromUrl = state['action'] === 'mpeditor';
2199
- }
2200
- var parseFromStorage = !!window.sessionStorage.getItem('_mpcehash');
2201
- var editorParams;
2202
-
2203
- if (parseFromUrl) { // happens if they are initializing the editor using an old snippet
2204
- editorParams = this._editorParamsFromHash(instance, window.location.hash);
2205
- } else if (parseFromStorage) { // happens if they are initialized the editor and using the new snippet
2206
- editorParams = this._editorParamsFromHash(instance, window.sessionStorage.getItem('_mpcehash'));
2207
- window.sessionStorage.removeItem('_mpcehash');
2208
- } else { // get credentials from sessionStorage from a previous initialzation
2209
- editorParams = JSON.parse(window.sessionStorage.getItem('editorParams') || '{}');
2210
- }
2211
-
2212
- if (editorParams['projectToken'] && instance.get_config('token') === editorParams['projectToken']) {
2213
- this._loadEditor(instance, editorParams);
2214
- return true;
2215
- } else {
2216
- return false;
2217
- }
2218
- } catch (e) {
2219
- return false;
2220
- }
2221
- },
2222
-
2223
- _loadEditor: function(instance, editorParams) {
2224
- if (!window['_mpEditorLoaded']) { // only load the codeless event editor once, even if there are multiple instances of MixpanelLib
2225
- window['_mpEditorLoaded'] = true;
2226
- var editorUrl = instance.get_config('app_host')
2227
- + '/js-bundle/reports/collect-everything/editor.js?_ts='
2228
- + (new Date()).getTime();
2229
- this._loadScript(editorUrl, function() {
2230
- window['mp_load_editor'](editorParams);
2231
- });
2232
- return true;
2233
- }
2234
- return false;
2235
- },
2236
-
2237
- // this is a mechanism to ramp up CE with no server-side interaction.
2238
- // when CE is active, every page load results in a decide request. we
2239
- // need to gently ramp this up so we don't overload decide. this decides
2240
- // deterministically if CE is enabled for this project by modding the char
2241
- // value of the project token.
2242
- enabledForProject: function(token, numBuckets, numEnabledBuckets) {
2243
- numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10;
2244
- numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10;
2245
- var charCodeSum = 0;
2246
- for (var i = 0; i < token.length; i++) {
2247
- charCodeSum += token.charCodeAt(i);
2248
- }
2249
- return (charCodeSum % numBuckets) < numEnabledBuckets;
2250
- },
2251
-
2252
- isBrowserSupported: function() {
2253
- return _.isFunction(document.querySelectorAll);
2254
- }
2255
- };
2256
-
2257
- _.bind_instance_methods(autotrack);
2258
- _.safewrap_instance_methods(autotrack);
2259
-
2260
1738
  /**
2261
1739
  * DomTracker Object
2262
1740
  * @constructor
@@ -2285,7 +1763,7 @@
2285
1763
  var elements = _.dom_query(query);
2286
1764
 
2287
1765
  if (elements.length === 0) {
2288
- console$1.error('The DOM query (' + query + ') returned 0 elements');
1766
+ console.error('The DOM query (' + query + ') returned 0 elements');
2289
1767
  return;
2290
1768
  }
2291
1769
 
@@ -2650,6 +2128,7 @@
2650
2128
  for (var i = 0; i < storedQueue.length; i++) {
2651
2129
  var item = storedQueue[i];
2652
2130
  if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
2131
+ item.orphaned = true;
2653
2132
  batch.push(item);
2654
2133
  if (batch.length >= batchSize) {
2655
2134
  break;
@@ -2706,6 +2185,52 @@
2706
2185
  }, this.pid);
2707
2186
  };
2708
2187
 
2188
+ // internal helper for RequestQueue.updatePayloads
2189
+ var updatePayloads = function(existingItems, itemsToUpdate) {
2190
+ var newItems = [];
2191
+ _.each(existingItems, function(item) {
2192
+ var id = item['id'];
2193
+ if (id in itemsToUpdate) {
2194
+ var newPayload = itemsToUpdate[id];
2195
+ if (newPayload !== null) {
2196
+ item['payload'] = newPayload;
2197
+ newItems.push(item);
2198
+ }
2199
+ } else {
2200
+ // no update
2201
+ newItems.push(item);
2202
+ }
2203
+ });
2204
+ return newItems;
2205
+ };
2206
+
2207
+ /**
2208
+ * Update payloads of given items in both in-memory queue and
2209
+ * persisted queue. Items set to null are removed from queues.
2210
+ */
2211
+ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
2212
+ this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
2213
+ this.lock.withLock(_.bind(function lockAcquired() {
2214
+ var succeeded;
2215
+ try {
2216
+ var storedQueue = this.readFromStorage();
2217
+ storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2218
+ succeeded = this.saveToStorage(storedQueue);
2219
+ } catch(err) {
2220
+ logger$1.error('Error updating items', itemsToUpdate);
2221
+ succeeded = false;
2222
+ }
2223
+ if (cb) {
2224
+ cb(succeeded);
2225
+ }
2226
+ }, this), function lockFailure(err) {
2227
+ logger$1.error('Error acquiring storage lock', err);
2228
+ if (cb) {
2229
+ cb(false);
2230
+ }
2231
+ }, this.pid);
2232
+ };
2233
+
2709
2234
  /**
2710
2235
  * Read and parse items array from localStorage entry, handling
2711
2236
  * malformed/missing data if necessary.
@@ -2762,18 +2287,18 @@
2762
2287
  * Uses RequestQueue to manage the backing store.
2763
2288
  * @constructor
2764
2289
  */
2765
- var RequestBatcher = function(storageKey, endpoint, options) {
2290
+ var RequestBatcher = function(storageKey, options) {
2766
2291
  this.queue = new RequestQueue(storageKey, {storage: options.storage});
2767
- this.endpoint = endpoint;
2768
2292
 
2769
2293
  this.libConfig = options.libConfig;
2770
2294
  this.sendRequest = options.sendRequestFunc;
2295
+ this.beforeSendHook = options.beforeSendHook;
2771
2296
 
2772
2297
  // seed variable batch size + flush interval with configured values
2773
2298
  this.batchSize = this.libConfig['batch_size'];
2774
2299
  this.flushInterval = this.libConfig['batch_flush_interval_ms'];
2775
2300
 
2776
- this.stopped = false;
2301
+ this.stopped = !this.libConfig['batch_autostart'];
2777
2302
  };
2778
2303
 
2779
2304
  /**
@@ -2853,18 +2378,29 @@
2853
2378
  }
2854
2379
 
2855
2380
  options = options || {};
2381
+ var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2382
+ var startTime = new Date().getTime();
2856
2383
  var currentBatchSize = this.batchSize;
2857
2384
  var batch = this.queue.fillBatch(currentBatchSize);
2858
- if (batch.length < 1) {
2385
+ var dataForRequest = [];
2386
+ var transformedItems = {};
2387
+ _.each(batch, function(item) {
2388
+ var payload = item['payload'];
2389
+ if (this.beforeSendHook && !item.orphaned) {
2390
+ payload = this.beforeSendHook(payload);
2391
+ }
2392
+ if (payload) {
2393
+ dataForRequest.push(payload);
2394
+ }
2395
+ transformedItems[item['id']] = payload;
2396
+ }, this);
2397
+ if (dataForRequest.length < 1) {
2859
2398
  this.resetFlush();
2860
2399
  return; // nothing to do
2861
2400
  }
2862
2401
 
2863
2402
  this.requestInProgress = true;
2864
2403
 
2865
- var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2866
- var startTime = new Date().getTime();
2867
- var dataForRequest = _.map(batch, function(item) { return item['payload']; });
2868
2404
  var batchSendCallback = _.bind(function(res) {
2869
2405
  this.requestInProgress = false;
2870
2406
 
@@ -2874,7 +2410,10 @@
2874
2410
  // flush operation if something goes wrong
2875
2411
 
2876
2412
  var removeItemsFromQueue = false;
2877
- if (
2413
+ if (options.unloading) {
2414
+ // update persisted data to include hook transformations
2415
+ this.queue.updatePayloads(transformedItems);
2416
+ } else if (
2878
2417
  _.isObject(res) &&
2879
2418
  res.error === 'timeout' &&
2880
2419
  new Date().getTime() - startTime >= timeoutMS
@@ -2884,9 +2423,9 @@
2884
2423
  } else if (
2885
2424
  _.isObject(res) &&
2886
2425
  res.xhr_req &&
2887
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
2426
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
2888
2427
  ) {
2889
- // network or API error, retry
2428
+ // network or API error, or 429 Too Many Requests, retry
2890
2429
  var retryMS = this.flushInterval * 2;
2891
2430
  var headers = res.xhr_req['responseHeaders'];
2892
2431
  if (headers) {
@@ -2934,11 +2473,11 @@
2934
2473
  ignore_json_errors: true, // eslint-disable-line camelcase
2935
2474
  timeout_ms: timeoutMS // eslint-disable-line camelcase
2936
2475
  };
2937
- if (options.sendBeacon) {
2476
+ if (options.unloading) {
2938
2477
  requestOptions.transport = 'sendBeacon';
2939
2478
  }
2940
- logger.log('MIXPANEL REQUEST:', this.endpoint, dataForRequest);
2941
- this.sendRequest(this.endpoint, dataForRequest, requestOptions, batchSendCallback);
2479
+ logger.log('MIXPANEL REQUEST:', dataForRequest);
2480
+ this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
2942
2481
 
2943
2482
  } catch(err) {
2944
2483
  logger.error('Error flushing request queue', err);
@@ -3016,9 +2555,14 @@
3016
2555
  */
3017
2556
  function hasOptedOut(token, options) {
3018
2557
  if (_hasDoNotTrackFlagOn(options)) {
2558
+ 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"');
3019
2559
  return true;
3020
2560
  }
3021
- return _getStorageValue(token, options) === '0';
2561
+ var optedOut = _getStorageValue(token, options) === '0';
2562
+ if (optedOut) {
2563
+ console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.');
2564
+ }
2565
+ return optedOut;
3022
2566
  }
3023
2567
 
3024
2568
  /**
@@ -3451,9 +2995,13 @@
3451
2995
  * Permanently delete a group.
3452
2996
  *
3453
2997
  * ### Usage:
2998
+ *
3454
2999
  * mixpanel.get_group('company', 'mixpanel').delete();
3000
+ *
3001
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
3455
3002
  */
3456
3003
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
3004
+ // bracket notation above prevents a minification error related to reserved words
3457
3005
  var data = this.delete_action();
3458
3006
  return this._send_request(data, callback);
3459
3007
  });
@@ -3481,7 +3029,8 @@
3481
3029
 
3482
3030
  var date_encoded_data = _.encodeDates(data);
3483
3031
  return this._mixpanel._track_or_batch({
3484
- truncated_data: _.truncate(date_encoded_data, 255),
3032
+ type: 'groups',
3033
+ data: date_encoded_data,
3485
3034
  endpoint: this._get_config('api_host') + '/groups/',
3486
3035
  batcher: this._mixpanel.request_batchers.groups
3487
3036
  }, callback);
@@ -3552,7 +3101,7 @@
3552
3101
 
3553
3102
  var storage_type = config['persistence'];
3554
3103
  if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
3555
- console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3104
+ console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3556
3105
  storage_type = config['persistence'] = 'cookie';
3557
3106
  }
3558
3107
 
@@ -3910,8 +3459,8 @@
3910
3459
  this._pop_from_people_queue(UNSET_ACTION, q_data);
3911
3460
  }
3912
3461
 
3913
- console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3914
- console$1.log(data);
3462
+ console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3463
+ console.log(data);
3915
3464
 
3916
3465
  this.save();
3917
3466
  };
@@ -3954,7 +3503,7 @@
3954
3503
  } else if (queue === UNION_ACTION) {
3955
3504
  return UNION_QUEUE_KEY;
3956
3505
  } else {
3957
- console$1.error('Invalid queue:', queue);
3506
+ console.error('Invalid queue:', queue);
3958
3507
  }
3959
3508
  };
3960
3509
 
@@ -5922,7 +5471,7 @@
5922
5471
  _.each(prop, function(v, k) {
5923
5472
  if (!this._is_reserved_property(k)) {
5924
5473
  if (isNaN(parseFloat(v))) {
5925
- console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5474
+ console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5926
5475
  return;
5927
5476
  } else {
5928
5477
  $add[k] = v;
@@ -6046,7 +5595,7 @@
6046
5595
  if (!_.isNumber(amount)) {
6047
5596
  amount = parseFloat(amount);
6048
5597
  if (isNaN(amount)) {
6049
- console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
5598
+ console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
6050
5599
  return;
6051
5600
  }
6052
5601
  }
@@ -6082,7 +5631,7 @@
6082
5631
  */
6083
5632
  MixpanelPeople.prototype.delete_user = function() {
6084
5633
  if (!this._identify_called()) {
6085
- console$1.error('mixpanel.people.delete_user() requires you to call identify() first');
5634
+ console.error('mixpanel.people.delete_user() requires you to call identify() first');
6086
5635
  return;
6087
5636
  }
6088
5637
  var data = {'$delete': this._mixpanel.get_distinct_id()};
@@ -6110,7 +5659,6 @@
6110
5659
  }
6111
5660
 
6112
5661
  var date_encoded_data = _.encodeDates(data);
6113
- var truncated_data = _.truncate(date_encoded_data, 255);
6114
5662
 
6115
5663
  if (!this._identify_called()) {
6116
5664
  this._enqueue(data);
@@ -6121,11 +5669,12 @@
6121
5669
  callback(-1);
6122
5670
  }
6123
5671
  }
6124
- return truncated_data;
5672
+ return _.truncate(date_encoded_data, 255);
6125
5673
  }
6126
5674
 
6127
5675
  return this._mixpanel._track_or_batch({
6128
- truncated_data: truncated_data,
5676
+ type: 'people',
5677
+ data: date_encoded_data,
6129
5678
  endpoint: this._get_config('api_host') + '/engage/',
6130
5679
  batcher: this._mixpanel.request_batchers.people
6131
5680
  }, callback);
@@ -6156,7 +5705,7 @@
6156
5705
  } else if (UNION_ACTION in data) {
6157
5706
  this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
6158
5707
  } else {
6159
- console$1.error('Invalid call to _enqueue():', data);
5708
+ console.error('Invalid call to _enqueue():', data);
6160
5709
  }
6161
5710
  };
6162
5711
 
@@ -6289,6 +5838,9 @@
6289
5838
  var INIT_MODULE = 0;
6290
5839
  var INIT_SNIPPET = 1;
6291
5840
 
5841
+ var IDENTITY_FUNC = function(x) {return x;};
5842
+ var NOOP_FUNC = function() {};
5843
+
6292
5844
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
6293
5845
 
6294
5846
 
@@ -6321,7 +5873,6 @@
6321
5873
  'api_method': 'POST',
6322
5874
  'api_transport': 'XHR',
6323
5875
  'app_host': 'https://mixpanel.com',
6324
- 'autotrack': true,
6325
5876
  'cdn': 'https://cdn.mxpnl.com',
6326
5877
  'cross_site_cookie': false,
6327
5878
  'cross_subdomain_cookie': true,
@@ -6329,7 +5880,7 @@
6329
5880
  'persistence_name': '',
6330
5881
  'cookie_domain': '',
6331
5882
  'cookie_name': '',
6332
- 'loaded': function() {},
5883
+ 'loaded': NOOP_FUNC,
6333
5884
  'store_google': true,
6334
5885
  'save_referrer': true,
6335
5886
  'test': false,
@@ -6352,10 +5903,12 @@
6352
5903
  'inapp_protocol': '//',
6353
5904
  'inapp_link_new_window': false,
6354
5905
  'ignore_dnt': false,
6355
- 'batch_requests': false, // for now
5906
+ 'batch_requests': true,
6356
5907
  'batch_size': 50,
6357
5908
  'batch_flush_interval_ms': 5000,
6358
- 'batch_request_timeout_ms': 90000
5909
+ 'batch_request_timeout_ms': 90000,
5910
+ 'batch_autostart': true,
5911
+ 'hooks': {}
6359
5912
  };
6360
5913
 
6361
5914
  var DOM_LOADED = false;
@@ -6383,7 +5936,7 @@
6383
5936
  instance = target;
6384
5937
  } else {
6385
5938
  if (target && !_.isArray(target)) {
6386
- console$1.error('You have already initialized ' + name);
5939
+ console.error('You have already initialized ' + name);
6387
5940
  return;
6388
5941
  }
6389
5942
  instance = new MixpanelLib();
@@ -6402,21 +5955,6 @@
6402
5955
  // global debug to be true
6403
5956
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
6404
5957
 
6405
- instance['__autotrack_enabled'] = instance.get_config('autotrack');
6406
- if (instance.get_config('autotrack')) {
6407
- var num_buckets = 100;
6408
- var num_enabled_buckets = 100;
6409
- if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
6410
- instance['__autotrack_enabled'] = false;
6411
- console$1.log('Not in active bucket: disabling Automatic Event Collection.');
6412
- } else if (!autotrack.isBrowserSupported()) {
6413
- instance['__autotrack_enabled'] = false;
6414
- console$1.log('Disabling Automatic Event Collection because this browser is not supported');
6415
- } else {
6416
- autotrack.init(instance);
6417
- }
6418
- }
6419
-
6420
5958
  // if target is not defined, we called init after the lib already
6421
5959
  // loaded, so there won't be an array of things to execute
6422
5960
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -6455,11 +5993,11 @@
6455
5993
  */
6456
5994
  MixpanelLib.prototype.init = function (token, config, name) {
6457
5995
  if (_.isUndefined(name)) {
6458
- console$1.error('You must name your new library: init(token, config, name)');
5996
+ console.error('You must name your new library: init(token, config, name)');
6459
5997
  return;
6460
5998
  }
6461
5999
  if (name === PRIMARY_INSTANCE_NAME) {
6462
- console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6000
+ console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6463
6001
  return;
6464
6002
  }
6465
6003
 
@@ -6484,23 +6022,13 @@
6484
6022
  this['config'] = {};
6485
6023
  this['_triggered_notifs'] = [];
6486
6024
 
6487
- // rollout: enable batch_requests by default for 30% of projects
6488
- // (only if they have not specified a value in their init config
6489
- // and they aren't using a custom API host)
6490
- var variable_features = {};
6491
- var api_host = config['api_host'];
6492
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
6493
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 30)) {
6494
- variable_features['batch_requests'] = true;
6495
- }
6496
-
6497
- this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
6025
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
6498
6026
  'name': name,
6499
6027
  'token': token,
6500
6028
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
6501
6029
  }));
6502
6030
 
6503
- this['_jsc'] = function() {};
6031
+ this['_jsc'] = NOOP_FUNC;
6504
6032
 
6505
6033
  this.__dom_loaded_queue = [];
6506
6034
  this.__request_queue = [];
@@ -6516,22 +6044,42 @@
6516
6044
  if (this._batch_requests) {
6517
6045
  if (!_.localStorage.is_supported(true) || !USE_XHR) {
6518
6046
  this._batch_requests = false;
6519
- console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6047
+ console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6520
6048
  } else {
6521
- this.start_batch_requests();
6049
+ this.init_batchers();
6522
6050
  if (sendBeacon && window$1.addEventListener) {
6523
- window$1.addEventListener('unload', _.bind(function() {
6524
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
6525
- // Since sendBeacon doesn't report success/failure, events will not be removed from
6526
- // the persistent store; if the site is loaded again, the events will be flushed again
6527
- // on startup and deduplicated on the Mixpanel server side.
6528
- this.request_batchers.events.flush({sendBeacon: true});
6529
- }, this));
6051
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
6052
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
6053
+ // events will not be removed from the persistent store; if the site is loaded again,
6054
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
6055
+ // side.
6056
+ // There is no reliable way to capture only page close events, so we lean on the
6057
+ // visibilitychange and pagehide events as recommended at
6058
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
6059
+ // These events fire when the user clicks away from the current page/tab, so will occur
6060
+ // more frequently than page unload, but are the only mechanism currently for capturing
6061
+ // this scenario somewhat reliably.
6062
+ var flush_on_unload = _.bind(function() {
6063
+ if (!this.request_batchers.events.stopped) {
6064
+ this.request_batchers.events.flush({unloading: true});
6065
+ }
6066
+ }, this);
6067
+ window$1.addEventListener('pagehide', function(ev) {
6068
+ if (ev['persisted']) {
6069
+ flush_on_unload();
6070
+ }
6071
+ });
6072
+ window$1.addEventListener('visibilitychange', function() {
6073
+ if (document$1['visibilityState'] === 'hidden') {
6074
+ flush_on_unload();
6075
+ }
6076
+ });
6530
6077
  }
6531
6078
  }
6532
6079
  }
6533
6080
 
6534
6081
  this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']);
6082
+ this.unpersisted_superprops = {};
6535
6083
  this._gdpr_init();
6536
6084
 
6537
6085
  var uuid = _.UUID();
@@ -6581,7 +6129,7 @@
6581
6129
 
6582
6130
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
6583
6131
  if (this.get_config('img')) {
6584
- console$1.error('You can\'t use DOM tracking functions with img = true.');
6132
+ console.error('You can\'t use DOM tracking functions with img = true.');
6585
6133
  return false;
6586
6134
  }
6587
6135
 
@@ -6691,9 +6239,16 @@
6691
6239
  try {
6692
6240
  succeeded = sendBeacon(url, body_data);
6693
6241
  } catch (e) {
6694
- console$1.error(e);
6242
+ console.error(e);
6695
6243
  succeeded = false;
6696
6244
  }
6245
+ try {
6246
+ if (callback) {
6247
+ callback(succeeded ? 1 : 0);
6248
+ }
6249
+ } catch (e) {
6250
+ console.error(e);
6251
+ }
6697
6252
  } else if (USE_XHR) {
6698
6253
  try {
6699
6254
  var req = new XMLHttpRequest();
@@ -6724,7 +6279,7 @@
6724
6279
  try {
6725
6280
  response = _.JSONDecode(req.responseText);
6726
6281
  } catch (e) {
6727
- console$1.error(e);
6282
+ console.error(e);
6728
6283
  if (options.ignore_json_errors) {
6729
6284
  response = req.responseText;
6730
6285
  } else {
@@ -6747,7 +6302,7 @@
6747
6302
  } else {
6748
6303
  error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
6749
6304
  }
6750
- console$1.error(error);
6305
+ console.error(error);
6751
6306
  if (callback) {
6752
6307
  if (verbose_mode) {
6753
6308
  callback({status: 0, error: error, xhr_req: req});
@@ -6760,7 +6315,7 @@
6760
6315
  };
6761
6316
  req.send(body_data);
6762
6317
  } catch (e) {
6763
- console$1.error(e);
6318
+ console.error(e);
6764
6319
  succeeded = false;
6765
6320
  }
6766
6321
  } else {
@@ -6828,32 +6383,53 @@
6828
6383
 
6829
6384
  // request queueing utils
6830
6385
 
6831
- MixpanelLib.prototype.start_batch_requests = function() {
6386
+ MixpanelLib.prototype.are_batchers_initialized = function() {
6387
+ return !!this.request_batchers.events;
6388
+ };
6389
+
6390
+ MixpanelLib.prototype.init_batchers = function() {
6832
6391
  var token = this.get_config('token');
6833
- if (!this.request_batchers.events) { // no batchers initialized yet
6834
- var batcher_config = {
6835
- libConfig: this['config'],
6836
- sendRequestFunc: _.bind(function(endpoint, data, options, cb) {
6837
- this._send_request(
6838
- this.get_config('api_host') + endpoint,
6839
- encode_data_for_request(data),
6840
- options,
6841
- this._prepare_callback(cb, data)
6842
- );
6843
- }, this)
6844
- };
6392
+ if (!this.are_batchers_initialized()) {
6393
+ var batcher_for = _.bind(function(attrs) {
6394
+ return new RequestBatcher(
6395
+ '__mpq_' + token + attrs.queue_suffix,
6396
+ {
6397
+ libConfig: this['config'],
6398
+ sendRequestFunc: _.bind(function(data, options, cb) {
6399
+ this._send_request(
6400
+ this.get_config('api_host') + attrs.endpoint,
6401
+ encode_data_for_request(data),
6402
+ options,
6403
+ this._prepare_callback(cb, data)
6404
+ );
6405
+ }, this),
6406
+ beforeSendHook: _.bind(function(item) {
6407
+ return this._run_hook('before_send_' + attrs.type, item);
6408
+ }, this)
6409
+ }
6410
+ );
6411
+ }, this);
6845
6412
  this.request_batchers = {
6846
- events: new RequestBatcher('__mpq_' + token + '_ev', '/track/', batcher_config),
6847
- people: new RequestBatcher('__mpq_' + token + '_pp', '/engage/', batcher_config),
6848
- groups: new RequestBatcher('__mpq_' + token + '_gr', '/groups/', batcher_config)
6413
+ events: batcher_for({type: 'events', endpoint: '/track/', queue_suffix: '_ev'}),
6414
+ people: batcher_for({type: 'people', endpoint: '/engage/', queue_suffix: '_pp'}),
6415
+ groups: batcher_for({type: 'groups', endpoint: '/groups/', queue_suffix: '_gr'})
6849
6416
  };
6850
6417
  }
6851
- _.each(this.request_batchers, function(batcher) {
6852
- batcher.start();
6853
- });
6418
+ if (this.get_config('batch_autostart')) {
6419
+ this.start_batch_senders();
6420
+ }
6854
6421
  };
6855
6422
 
6856
- MixpanelLib.prototype.stop_batch_requests = function() {
6423
+ MixpanelLib.prototype.start_batch_senders = function() {
6424
+ if (this.are_batchers_initialized()) {
6425
+ this._batch_requests = true;
6426
+ _.each(this.request_batchers, function(batcher) {
6427
+ batcher.start();
6428
+ });
6429
+ }
6430
+ };
6431
+
6432
+ MixpanelLib.prototype.stop_batch_senders = function() {
6857
6433
  this._batch_requests = false;
6858
6434
  _.each(this.request_batchers, function(batcher) {
6859
6435
  batcher.stop();
@@ -6898,23 +6474,30 @@
6898
6474
 
6899
6475
  // internal method for handling track vs batch-enqueue logic
6900
6476
  MixpanelLib.prototype._track_or_batch = function(options, callback) {
6901
- var truncated_data = options.truncated_data;
6477
+ var truncated_data = _.truncate(options.data, 255);
6902
6478
  var endpoint = options.endpoint;
6903
6479
  var batcher = options.batcher;
6904
6480
  var should_send_immediately = options.should_send_immediately;
6905
6481
  var send_request_options = options.send_request_options || {};
6906
- callback = callback || function() {};
6482
+ callback = callback || NOOP_FUNC;
6907
6483
 
6908
6484
  var request_enqueued_or_initiated = true;
6909
6485
  var send_request_immediately = _.bind(function() {
6910
- console$1.log('MIXPANEL REQUEST:');
6911
- console$1.log(truncated_data);
6912
- return this._send_request(
6913
- endpoint,
6914
- encode_data_for_request(truncated_data),
6915
- send_request_options,
6916
- this._prepare_callback(callback, truncated_data)
6917
- );
6486
+ if (!send_request_options.skip_hooks) {
6487
+ truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
6488
+ }
6489
+ if (truncated_data) {
6490
+ console.log('MIXPANEL REQUEST:');
6491
+ console.log(truncated_data);
6492
+ return this._send_request(
6493
+ endpoint,
6494
+ encode_data_for_request(truncated_data),
6495
+ send_request_options,
6496
+ this._prepare_callback(callback, truncated_data)
6497
+ );
6498
+ } else {
6499
+ return null;
6500
+ }
6918
6501
  }, this);
6919
6502
 
6920
6503
  if (this._batch_requests && !should_send_immediately) {
@@ -6967,11 +6550,11 @@
6967
6550
  }
6968
6551
  var should_send_immediately = options['send_immediately'];
6969
6552
  if (typeof callback !== 'function') {
6970
- callback = function() {};
6553
+ callback = NOOP_FUNC;
6971
6554
  }
6972
6555
 
6973
6556
  if (_.isUndefined(event_name)) {
6974
- console$1.error('No event name provided to mixpanel.track');
6557
+ console.error('No event name provided to mixpanel.track');
6975
6558
  return;
6976
6559
  }
6977
6560
 
@@ -7002,6 +6585,7 @@
7002
6585
  {},
7003
6586
  _.info.properties(),
7004
6587
  this['persistence'].properties(),
6588
+ this.unpersisted_superprops,
7005
6589
  properties
7006
6590
  );
7007
6591
 
@@ -7011,7 +6595,7 @@
7011
6595
  delete properties[blacklisted_prop];
7012
6596
  });
7013
6597
  } else {
7014
- console$1.error('Invalid value for property_blacklist config: ' + property_blacklist);
6598
+ console.error('Invalid value for property_blacklist config: ' + property_blacklist);
7015
6599
  }
7016
6600
 
7017
6601
  var data = {
@@ -7019,7 +6603,8 @@
7019
6603
  'properties': properties
7020
6604
  };
7021
6605
  var ret = this._track_or_batch({
7022
- truncated_data: _.truncate(data, 255),
6606
+ type: 'events',
6607
+ data: data,
7023
6608
  endpoint: this.get_config('api_host') + '/track/',
7024
6609
  batcher: this.request_batchers.events,
7025
6610
  should_send_immediately: should_send_immediately,
@@ -7255,7 +6840,7 @@
7255
6840
  */
7256
6841
  MixpanelLib.prototype.time_event = function(event_name) {
7257
6842
  if (_.isUndefined(event_name)) {
7258
- console$1.error('No event name provided to mixpanel.time_event');
6843
+ console.error('No event name provided to mixpanel.time_event');
7259
6844
  return;
7260
6845
  }
7261
6846
 
@@ -7266,6 +6851,27 @@
7266
6851
  this['persistence'].set_event_timer(event_name, new Date().getTime());
7267
6852
  };
7268
6853
 
6854
+ var REGISTER_DEFAULTS = {
6855
+ 'persistent': true
6856
+ };
6857
+ /**
6858
+ * Helper to parse options param for register methods, maintaining
6859
+ * legacy support for plain "days" param instead of options object
6860
+ * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods
6861
+ * @returns {Object} options object
6862
+ */
6863
+ var options_for_register = function(days_or_options) {
6864
+ var options;
6865
+ if (_.isObject(days_or_options)) {
6866
+ options = days_or_options;
6867
+ } else if (!_.isUndefined(days_or_options)) {
6868
+ options = {'days': days_or_options};
6869
+ } else {
6870
+ options = {};
6871
+ }
6872
+ return _.extend({}, REGISTER_DEFAULTS, options);
6873
+ };
6874
+
7269
6875
  /**
7270
6876
  * Register a set of super properties, which are included with all
7271
6877
  * events. This will overwrite previous super property values.
@@ -7281,11 +6887,21 @@
7281
6887
  * 'Account Type': 'Free'
7282
6888
  * });
7283
6889
  *
6890
+ * // register only for the current pageload
6891
+ * mixpanel.register({'Name': 'Pat'}, {persistent: false});
6892
+ *
7284
6893
  * @param {Object} properties An associative array of properties to store about the user
7285
- * @param {Number} [days] How many days since the user's last visit to store the super properties
6894
+ * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props)
6895
+ * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props)
6896
+ * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
7286
6897
  */
7287
- MixpanelLib.prototype.register = function(props, days) {
7288
- this['persistence'].register(props, days);
6898
+ MixpanelLib.prototype.register = function(props, days_or_options) {
6899
+ var options = options_for_register(days_or_options);
6900
+ if (options['persistent']) {
6901
+ this['persistence'].register(props, options['days']);
6902
+ } else {
6903
+ _.extend(this.unpersisted_superprops, props);
6904
+ }
7289
6905
  };
7290
6906
 
7291
6907
  /**
@@ -7299,6 +6915,11 @@
7299
6915
  * 'First Login Date': new Date().toISOString()
7300
6916
  * });
7301
6917
  *
6918
+ * // register once, only for the current pageload
6919
+ * mixpanel.register_once({
6920
+ * 'First interaction time': new Date().toISOString()
6921
+ * }, 'None', {persistent: false});
6922
+ *
7302
6923
  * ### Notes:
7303
6924
  *
7304
6925
  * If default_value is specified, current super properties
@@ -7306,19 +6927,40 @@
7306
6927
  *
7307
6928
  * @param {Object} properties An associative array of properties to store about the user
7308
6929
  * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
7309
- * @param {Number} [days] How many days since the users last visit to store the super properties
6930
+ * @param {Number|Object} [days_or_options] Options object or number of days since the user's last visit to store the super properties (only valid for persisted props)
6931
+ * @param {boolean} [days_or_options.days] - number of days since the user's last visit to store the super properties (only valid for persisted props)
6932
+ * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
7310
6933
  */
7311
- MixpanelLib.prototype.register_once = function(props, default_value, days) {
7312
- this['persistence'].register_once(props, default_value, days);
6934
+ MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
6935
+ var options = options_for_register(days_or_options);
6936
+ if (options['persistent']) {
6937
+ this['persistence'].register_once(props, default_value, options['days']);
6938
+ } else {
6939
+ if (typeof(default_value) === 'undefined') {
6940
+ default_value = 'None';
6941
+ }
6942
+ _.each(props, function(val, prop) {
6943
+ if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) {
6944
+ this.unpersisted_superprops[prop] = val;
6945
+ }
6946
+ }, this);
6947
+ }
7313
6948
  };
7314
6949
 
7315
6950
  /**
7316
6951
  * Delete a super property stored with the current user.
7317
6952
  *
7318
6953
  * @param {String} property The name of the super property to remove
6954
+ * @param {Object} [options]
6955
+ * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
7319
6956
  */
7320
- MixpanelLib.prototype.unregister = function(property) {
7321
- this['persistence'].unregister(property);
6957
+ MixpanelLib.prototype.unregister = function(property, options) {
6958
+ options = options_for_register(options);
6959
+ if (options['persistent']) {
6960
+ this['persistence'].unregister(property);
6961
+ } else {
6962
+ delete this.unpersisted_superprops[property];
6963
+ }
7322
6964
  };
7323
6965
 
7324
6966
  MixpanelLib.prototype._register_single = function(prop, value) {
@@ -7389,7 +7031,10 @@
7389
7031
  // send an $identify event any time the distinct_id is changing - logic on the server
7390
7032
  // will determine whether or not to do anything with it.
7391
7033
  if (new_distinct_id !== previous_distinct_id) {
7392
- this.track('$identify', { 'distinct_id': new_distinct_id, '$anon_distinct_id': previous_distinct_id });
7034
+ this.track('$identify', {
7035
+ 'distinct_id': new_distinct_id,
7036
+ '$anon_distinct_id': previous_distinct_id
7037
+ }, {skip_hooks: true});
7393
7038
  }
7394
7039
  };
7395
7040
 
@@ -7468,7 +7113,7 @@
7468
7113
  // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
7469
7114
  // this ID, as it will duplicate users.
7470
7115
  if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
7471
- console$1.critical('Attempting to create alias for existing People user - aborting.');
7116
+ console.critical('Attempting to create alias for existing People user - aborting.');
7472
7117
  return -2;
7473
7118
  }
7474
7119
 
@@ -7478,12 +7123,17 @@
7478
7123
  }
7479
7124
  if (alias !== original) {
7480
7125
  this._register_single(ALIAS_ID_KEY, alias);
7481
- return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() {
7126
+ return this.track('$create_alias', {
7127
+ 'alias': alias,
7128
+ 'distinct_id': original
7129
+ }, {
7130
+ skip_hooks: true
7131
+ }, function() {
7482
7132
  // Flush the people queue
7483
7133
  _this.identify(alias);
7484
7134
  });
7485
7135
  } else {
7486
- console$1.error('alias matches current distinct_id - skipping api call.');
7136
+ console.error('alias matches current distinct_id - skipping api call.');
7487
7137
  this.identify(alias);
7488
7138
  return -1;
7489
7139
  }
@@ -7657,6 +7307,21 @@
7657
7307
  return this['config'][prop_name];
7658
7308
  };
7659
7309
 
7310
+ /**
7311
+ * Fetch a hook function from config, with safe default, and run it
7312
+ * against the given arguments
7313
+ * @param {string} hook_name which hook to retrieve
7314
+ * @returns {any|null} return value of user-provided hook, or null if nothing was returned
7315
+ */
7316
+ MixpanelLib.prototype._run_hook = function(hook_name) {
7317
+ var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
7318
+ if (typeof ret === 'undefined') {
7319
+ console.error(hook_name + ' hook did not return a value');
7320
+ ret = null;
7321
+ }
7322
+ return ret;
7323
+ };
7324
+
7660
7325
  /**
7661
7326
  * Returns the value of the super property named property_name. If no such
7662
7327
  * property is set, get_property() will return the undefined value.
@@ -7717,7 +7382,7 @@
7717
7382
  return;
7718
7383
  }
7719
7384
 
7720
- console$1.log('MIXPANEL NOTIFICATION CHECK');
7385
+ console.log('MIXPANEL NOTIFICATION CHECK');
7721
7386
 
7722
7387
  var data = {
7723
7388
  'verbose': true,
@@ -8042,7 +7707,8 @@
8042
7707
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
8043
7708
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
8044
7709
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
8045
- MixpanelLib.prototype['stop_batch_requests'] = MixpanelLib.prototype.stop_batch_requests;
7710
+ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
7711
+ MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
8046
7712
 
8047
7713
  // MixpanelPersistence Exports
8048
7714
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
@@ -8162,18 +7828,18 @@
8162
7828
  // Initialization
8163
7829
  if (_.isUndefined(mixpanel_master)) {
8164
7830
  // mixpanel wasn't initialized properly, report error and quit
8165
- console$1.critical('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.');
7831
+ console.critical('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.');
8166
7832
  return;
8167
7833
  }
8168
7834
  if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) {
8169
7835
  // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
8170
- console$1.error('Mixpanel library has already been downloaded at least once.');
7836
+ console.critical('The Mixpanel library has already been downloaded at least once. Ensure that the Mixpanel code snippet only appears once on the page (and is not double-loaded by a tag manager) in order to avoid errors.');
8171
7837
  return;
8172
7838
  }
8173
7839
  var snippet_version = mixpanel_master['__SV'] || 0;
8174
7840
  if (snippet_version < 1.1) {
8175
7841
  // mixpanel wasn't initialized properly, report error and quit
8176
- console$1.critical('Version mismatch; please ensure you\'re using the latest version of the Mixpanel code snippet.');
7842
+ console.critical('Version mismatch; please ensure you\'re using the latest version of the Mixpanel code snippet.');
8177
7843
  return;
8178
7844
  }
8179
7845