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.
@@ -6,7 +6,7 @@
6
6
 
7
7
  var Config = {
8
8
  DEBUG: false,
9
- LIB_VERSION: '2.39.0'
9
+ LIB_VERSION: '2.42.0'
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
 
@@ -238,13 +251,13 @@
238
251
  return _.values(iterable);
239
252
  };
240
253
 
241
- _.map = function(arr, callback) {
254
+ _.map = function(arr, callback, context) {
242
255
  if (nativeMap && arr.map === nativeMap) {
243
- return arr.map(callback);
256
+ return arr.map(callback, context);
244
257
  } else {
245
258
  var results = [];
246
259
  _.each(arr, function(item) {
247
- results.push(callback(item));
260
+ results.push(callback.call(context, item));
248
261
  });
249
262
  return results;
250
263
  }
@@ -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,37 @@
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
+ 'baiduspider',
952
+ 'bingbot',
953
+ 'bingpreview',
954
+ 'facebookexternal',
955
+ 'pinterest',
956
+ 'screaming frog',
957
+ 'yahoo! slurp',
958
+ 'yandexbot',
959
+
960
+ // a whole bunch of goog-specific crawlers
961
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
962
+ 'adsbot-google',
963
+ 'apis-google',
964
+ 'duplexweb-google',
965
+ 'feedfetcher-google',
966
+ 'google favicon',
967
+ 'google web preview',
968
+ 'google-read-aloud',
969
+ 'googlebot',
970
+ 'googleweblight',
971
+ 'mediapartners-google',
972
+ 'storebot-google'
973
+ ];
941
974
  _.isBlockedUA = function(ua) {
942
- if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) {
943
- return true;
975
+ var i;
976
+ ua = ua.toLowerCase();
977
+ for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
978
+ if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
979
+ return true;
980
+ }
944
981
  }
945
982
  return false;
946
983
  };
@@ -979,16 +1016,12 @@
979
1016
  try {
980
1017
  result = decodeURIComponent(result);
981
1018
  } catch(err) {
982
- console$1.error('Skipping decoding for malformed query param: ' + result);
1019
+ console.error('Skipping decoding for malformed query param: ' + result);
983
1020
  }
984
1021
  return result.replace(/\+/g, ' ');
985
1022
  }
986
1023
  };
987
1024
 
988
- _.getHashParam = function(hash, param) {
989
- var matches = hash.match(new RegExp(param + '=([^&]*)'));
990
- return matches ? matches[1] : null;
991
- };
992
1025
 
993
1026
  // _.cookie
994
1027
  // Methods partially borrowed from quirksmode.org/js/cookies.html
@@ -1110,13 +1143,13 @@
1110
1143
  is_supported: function(force_check) {
1111
1144
  var supported = localStorageSupported(null, force_check);
1112
1145
  if (!supported) {
1113
- console$1.error('localStorage unsupported; falling back to cookie store');
1146
+ console.error('localStorage unsupported; falling back to cookie store');
1114
1147
  }
1115
1148
  return supported;
1116
1149
  },
1117
1150
 
1118
1151
  error: function(msg) {
1119
- console$1.error('localStorage error: ' + msg);
1152
+ console.error('localStorage error: ' + msg);
1120
1153
  },
1121
1154
 
1122
1155
  get: function(name) {
@@ -1171,7 +1204,7 @@
1171
1204
  */
1172
1205
  var register_event = function(element, type, handler, oldSchool, useCapture) {
1173
1206
  if (!element) {
1174
- console$1.error('No valid element provided to register_event');
1207
+ console.error('No valid element provided to register_event');
1175
1208
  return;
1176
1209
  }
1177
1210
 
@@ -1655,28 +1688,6 @@
1655
1688
  return maxlen ? guid.substring(0, maxlen) : guid;
1656
1689
  };
1657
1690
 
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
1691
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1681
1692
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1682
1693
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1727,539 +1738,6 @@
1727
1738
  _['info']['browserVersion'] = _.info.browserVersion;
1728
1739
  _['info']['properties'] = _.info.properties;
1729
1740
 
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
1741
  /**
2264
1742
  * DomTracker Object
2265
1743
  * @constructor
@@ -2288,7 +1766,7 @@
2288
1766
  var elements = _.dom_query(query);
2289
1767
 
2290
1768
  if (elements.length === 0) {
2291
- console$1.error('The DOM query (' + query + ') returned 0 elements');
1769
+ console.error('The DOM query (' + query + ') returned 0 elements');
2292
1770
  return;
2293
1771
  }
2294
1772
 
@@ -2653,6 +2131,7 @@
2653
2131
  for (var i = 0; i < storedQueue.length; i++) {
2654
2132
  var item = storedQueue[i];
2655
2133
  if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
2134
+ item.orphaned = true;
2656
2135
  batch.push(item);
2657
2136
  if (batch.length >= batchSize) {
2658
2137
  break;
@@ -2709,6 +2188,52 @@
2709
2188
  }, this.pid);
2710
2189
  };
2711
2190
 
2191
+ // internal helper for RequestQueue.updatePayloads
2192
+ var updatePayloads = function(existingItems, itemsToUpdate) {
2193
+ var newItems = [];
2194
+ _.each(existingItems, function(item) {
2195
+ var id = item['id'];
2196
+ if (id in itemsToUpdate) {
2197
+ var newPayload = itemsToUpdate[id];
2198
+ if (newPayload !== null) {
2199
+ item['payload'] = newPayload;
2200
+ newItems.push(item);
2201
+ }
2202
+ } else {
2203
+ // no update
2204
+ newItems.push(item);
2205
+ }
2206
+ });
2207
+ return newItems;
2208
+ };
2209
+
2210
+ /**
2211
+ * Update payloads of given items in both in-memory queue and
2212
+ * persisted queue. Items set to null are removed from queues.
2213
+ */
2214
+ RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
2215
+ this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
2216
+ this.lock.withLock(_.bind(function lockAcquired() {
2217
+ var succeeded;
2218
+ try {
2219
+ var storedQueue = this.readFromStorage();
2220
+ storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2221
+ succeeded = this.saveToStorage(storedQueue);
2222
+ } catch(err) {
2223
+ logger$1.error('Error updating items', itemsToUpdate);
2224
+ succeeded = false;
2225
+ }
2226
+ if (cb) {
2227
+ cb(succeeded);
2228
+ }
2229
+ }, this), function lockFailure(err) {
2230
+ logger$1.error('Error acquiring storage lock', err);
2231
+ if (cb) {
2232
+ cb(false);
2233
+ }
2234
+ }, this.pid);
2235
+ };
2236
+
2712
2237
  /**
2713
2238
  * Read and parse items array from localStorage entry, handling
2714
2239
  * malformed/missing data if necessary.
@@ -2765,18 +2290,18 @@
2765
2290
  * Uses RequestQueue to manage the backing store.
2766
2291
  * @constructor
2767
2292
  */
2768
- var RequestBatcher = function(storageKey, endpoint, options) {
2293
+ var RequestBatcher = function(storageKey, options) {
2769
2294
  this.queue = new RequestQueue(storageKey, {storage: options.storage});
2770
- this.endpoint = endpoint;
2771
2295
 
2772
2296
  this.libConfig = options.libConfig;
2773
2297
  this.sendRequest = options.sendRequestFunc;
2298
+ this.beforeSendHook = options.beforeSendHook;
2774
2299
 
2775
2300
  // seed variable batch size + flush interval with configured values
2776
2301
  this.batchSize = this.libConfig['batch_size'];
2777
2302
  this.flushInterval = this.libConfig['batch_flush_interval_ms'];
2778
2303
 
2779
- this.stopped = false;
2304
+ this.stopped = !this.libConfig['batch_autostart'];
2780
2305
  };
2781
2306
 
2782
2307
  /**
@@ -2856,18 +2381,29 @@
2856
2381
  }
2857
2382
 
2858
2383
  options = options || {};
2384
+ var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2385
+ var startTime = new Date().getTime();
2859
2386
  var currentBatchSize = this.batchSize;
2860
2387
  var batch = this.queue.fillBatch(currentBatchSize);
2861
- if (batch.length < 1) {
2388
+ var dataForRequest = [];
2389
+ var transformedItems = {};
2390
+ _.each(batch, function(item) {
2391
+ var payload = item['payload'];
2392
+ if (this.beforeSendHook && !item.orphaned) {
2393
+ payload = this.beforeSendHook(payload);
2394
+ }
2395
+ if (payload) {
2396
+ dataForRequest.push(payload);
2397
+ }
2398
+ transformedItems[item['id']] = payload;
2399
+ }, this);
2400
+ if (dataForRequest.length < 1) {
2862
2401
  this.resetFlush();
2863
2402
  return; // nothing to do
2864
2403
  }
2865
2404
 
2866
2405
  this.requestInProgress = true;
2867
2406
 
2868
- var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2869
- var startTime = new Date().getTime();
2870
- var dataForRequest = _.map(batch, function(item) { return item['payload']; });
2871
2407
  var batchSendCallback = _.bind(function(res) {
2872
2408
  this.requestInProgress = false;
2873
2409
 
@@ -2877,7 +2413,10 @@
2877
2413
  // flush operation if something goes wrong
2878
2414
 
2879
2415
  var removeItemsFromQueue = false;
2880
- if (
2416
+ if (options.unloading) {
2417
+ // update persisted data to include hook transformations
2418
+ this.queue.updatePayloads(transformedItems);
2419
+ } else if (
2881
2420
  _.isObject(res) &&
2882
2421
  res.error === 'timeout' &&
2883
2422
  new Date().getTime() - startTime >= timeoutMS
@@ -2887,9 +2426,9 @@
2887
2426
  } else if (
2888
2427
  _.isObject(res) &&
2889
2428
  res.xhr_req &&
2890
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
2429
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
2891
2430
  ) {
2892
- // network or API error, retry
2431
+ // network or API error, or 429 Too Many Requests, retry
2893
2432
  var retryMS = this.flushInterval * 2;
2894
2433
  var headers = res.xhr_req['responseHeaders'];
2895
2434
  if (headers) {
@@ -2937,11 +2476,11 @@
2937
2476
  ignore_json_errors: true, // eslint-disable-line camelcase
2938
2477
  timeout_ms: timeoutMS // eslint-disable-line camelcase
2939
2478
  };
2940
- if (options.sendBeacon) {
2479
+ if (options.unloading) {
2941
2480
  requestOptions.transport = 'sendBeacon';
2942
2481
  }
2943
- logger.log('MIXPANEL REQUEST:', this.endpoint, dataForRequest);
2944
- this.sendRequest(this.endpoint, dataForRequest, requestOptions, batchSendCallback);
2482
+ logger.log('MIXPANEL REQUEST:', dataForRequest);
2483
+ this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
2945
2484
 
2946
2485
  } catch(err) {
2947
2486
  logger.error('Error flushing request queue', err);
@@ -3019,9 +2558,14 @@
3019
2558
  */
3020
2559
  function hasOptedOut(token, options) {
3021
2560
  if (_hasDoNotTrackFlagOn(options)) {
2561
+ 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"');
3022
2562
  return true;
3023
2563
  }
3024
- return _getStorageValue(token, options) === '0';
2564
+ var optedOut = _getStorageValue(token, options) === '0';
2565
+ if (optedOut) {
2566
+ console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.');
2567
+ }
2568
+ return optedOut;
3025
2569
  }
3026
2570
 
3027
2571
  /**
@@ -3454,9 +2998,13 @@
3454
2998
  * Permanently delete a group.
3455
2999
  *
3456
3000
  * ### Usage:
3001
+ *
3457
3002
  * mixpanel.get_group('company', 'mixpanel').delete();
3003
+ *
3004
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
3458
3005
  */
3459
3006
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
3007
+ // bracket notation above prevents a minification error related to reserved words
3460
3008
  var data = this.delete_action();
3461
3009
  return this._send_request(data, callback);
3462
3010
  });
@@ -3484,7 +3032,8 @@
3484
3032
 
3485
3033
  var date_encoded_data = _.encodeDates(data);
3486
3034
  return this._mixpanel._track_or_batch({
3487
- truncated_data: _.truncate(date_encoded_data, 255),
3035
+ type: 'groups',
3036
+ data: date_encoded_data,
3488
3037
  endpoint: this._get_config('api_host') + '/groups/',
3489
3038
  batcher: this._mixpanel.request_batchers.groups
3490
3039
  }, callback);
@@ -3555,7 +3104,7 @@
3555
3104
 
3556
3105
  var storage_type = config['persistence'];
3557
3106
  if (storage_type !== 'cookie' && storage_type !== 'localStorage') {
3558
- console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3107
+ console.critical('Unknown persistence type ' + storage_type + '; falling back to cookie');
3559
3108
  storage_type = config['persistence'] = 'cookie';
3560
3109
  }
3561
3110
 
@@ -3913,8 +3462,8 @@
3913
3462
  this._pop_from_people_queue(UNSET_ACTION, q_data);
3914
3463
  }
3915
3464
 
3916
- console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3917
- console$1.log(data);
3465
+ console.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):');
3466
+ console.log(data);
3918
3467
 
3919
3468
  this.save();
3920
3469
  };
@@ -3957,7 +3506,7 @@
3957
3506
  } else if (queue === UNION_ACTION) {
3958
3507
  return UNION_QUEUE_KEY;
3959
3508
  } else {
3960
- console$1.error('Invalid queue:', queue);
3509
+ console.error('Invalid queue:', queue);
3961
3510
  }
3962
3511
  };
3963
3512
 
@@ -5925,7 +5474,7 @@
5925
5474
  _.each(prop, function(v, k) {
5926
5475
  if (!this._is_reserved_property(k)) {
5927
5476
  if (isNaN(parseFloat(v))) {
5928
- console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5477
+ console.error('Invalid increment value passed to mixpanel.people.increment - must be a number');
5929
5478
  return;
5930
5479
  } else {
5931
5480
  $add[k] = v;
@@ -6049,7 +5598,7 @@
6049
5598
  if (!_.isNumber(amount)) {
6050
5599
  amount = parseFloat(amount);
6051
5600
  if (isNaN(amount)) {
6052
- console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
5601
+ console.error('Invalid value passed to mixpanel.people.track_charge - must be a number');
6053
5602
  return;
6054
5603
  }
6055
5604
  }
@@ -6085,7 +5634,7 @@
6085
5634
  */
6086
5635
  MixpanelPeople.prototype.delete_user = function() {
6087
5636
  if (!this._identify_called()) {
6088
- console$1.error('mixpanel.people.delete_user() requires you to call identify() first');
5637
+ console.error('mixpanel.people.delete_user() requires you to call identify() first');
6089
5638
  return;
6090
5639
  }
6091
5640
  var data = {'$delete': this._mixpanel.get_distinct_id()};
@@ -6113,7 +5662,6 @@
6113
5662
  }
6114
5663
 
6115
5664
  var date_encoded_data = _.encodeDates(data);
6116
- var truncated_data = _.truncate(date_encoded_data, 255);
6117
5665
 
6118
5666
  if (!this._identify_called()) {
6119
5667
  this._enqueue(data);
@@ -6124,11 +5672,12 @@
6124
5672
  callback(-1);
6125
5673
  }
6126
5674
  }
6127
- return truncated_data;
5675
+ return _.truncate(date_encoded_data, 255);
6128
5676
  }
6129
5677
 
6130
5678
  return this._mixpanel._track_or_batch({
6131
- truncated_data: truncated_data,
5679
+ type: 'people',
5680
+ data: date_encoded_data,
6132
5681
  endpoint: this._get_config('api_host') + '/engage/',
6133
5682
  batcher: this._mixpanel.request_batchers.people
6134
5683
  }, callback);
@@ -6159,7 +5708,7 @@
6159
5708
  } else if (UNION_ACTION in data) {
6160
5709
  this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data);
6161
5710
  } else {
6162
- console$1.error('Invalid call to _enqueue():', data);
5711
+ console.error('Invalid call to _enqueue():', data);
6163
5712
  }
6164
5713
  };
6165
5714
 
@@ -6292,6 +5841,9 @@
6292
5841
  var INIT_MODULE = 0;
6293
5842
  var INIT_SNIPPET = 1;
6294
5843
 
5844
+ var IDENTITY_FUNC = function(x) {return x;};
5845
+ var NOOP_FUNC = function() {};
5846
+
6295
5847
  /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
6296
5848
 
6297
5849
 
@@ -6324,7 +5876,6 @@
6324
5876
  'api_method': 'POST',
6325
5877
  'api_transport': 'XHR',
6326
5878
  'app_host': 'https://mixpanel.com',
6327
- 'autotrack': true,
6328
5879
  'cdn': 'https://cdn.mxpnl.com',
6329
5880
  'cross_site_cookie': false,
6330
5881
  'cross_subdomain_cookie': true,
@@ -6332,7 +5883,7 @@
6332
5883
  'persistence_name': '',
6333
5884
  'cookie_domain': '',
6334
5885
  'cookie_name': '',
6335
- 'loaded': function() {},
5886
+ 'loaded': NOOP_FUNC,
6336
5887
  'store_google': true,
6337
5888
  'save_referrer': true,
6338
5889
  'test': false,
@@ -6355,10 +5906,12 @@
6355
5906
  'inapp_protocol': '//',
6356
5907
  'inapp_link_new_window': false,
6357
5908
  'ignore_dnt': false,
6358
- 'batch_requests': false, // for now
5909
+ 'batch_requests': true,
6359
5910
  'batch_size': 50,
6360
5911
  'batch_flush_interval_ms': 5000,
6361
- 'batch_request_timeout_ms': 90000
5912
+ 'batch_request_timeout_ms': 90000,
5913
+ 'batch_autostart': true,
5914
+ 'hooks': {}
6362
5915
  };
6363
5916
 
6364
5917
  var DOM_LOADED = false;
@@ -6386,7 +5939,7 @@
6386
5939
  instance = target;
6387
5940
  } else {
6388
5941
  if (target && !_.isArray(target)) {
6389
- console$1.error('You have already initialized ' + name);
5942
+ console.error('You have already initialized ' + name);
6390
5943
  return;
6391
5944
  }
6392
5945
  instance = new MixpanelLib();
@@ -6405,21 +5958,6 @@
6405
5958
  // global debug to be true
6406
5959
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
6407
5960
 
6408
- instance['__autotrack_enabled'] = instance.get_config('autotrack');
6409
- if (instance.get_config('autotrack')) {
6410
- var num_buckets = 100;
6411
- var num_enabled_buckets = 100;
6412
- if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
6413
- instance['__autotrack_enabled'] = false;
6414
- console$1.log('Not in active bucket: disabling Automatic Event Collection.');
6415
- } else if (!autotrack.isBrowserSupported()) {
6416
- instance['__autotrack_enabled'] = false;
6417
- console$1.log('Disabling Automatic Event Collection because this browser is not supported');
6418
- } else {
6419
- autotrack.init(instance);
6420
- }
6421
- }
6422
-
6423
5961
  // if target is not defined, we called init after the lib already
6424
5962
  // loaded, so there won't be an array of things to execute
6425
5963
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -6458,11 +5996,11 @@
6458
5996
  */
6459
5997
  MixpanelLib.prototype.init = function (token, config, name) {
6460
5998
  if (_.isUndefined(name)) {
6461
- console$1.error('You must name your new library: init(token, config, name)');
5999
+ console.error('You must name your new library: init(token, config, name)');
6462
6000
  return;
6463
6001
  }
6464
6002
  if (name === PRIMARY_INSTANCE_NAME) {
6465
- console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6003
+ console.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet');
6466
6004
  return;
6467
6005
  }
6468
6006
 
@@ -6487,23 +6025,13 @@
6487
6025
  this['config'] = {};
6488
6026
  this['_triggered_notifs'] = [];
6489
6027
 
6490
- // rollout: enable batch_requests by default for 30% of projects
6491
- // (only if they have not specified a value in their init config
6492
- // and they aren't using a custom API host)
6493
- var variable_features = {};
6494
- var api_host = config['api_host'];
6495
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
6496
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 30)) {
6497
- variable_features['batch_requests'] = true;
6498
- }
6499
-
6500
- this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
6028
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
6501
6029
  'name': name,
6502
6030
  'token': token,
6503
6031
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
6504
6032
  }));
6505
6033
 
6506
- this['_jsc'] = function() {};
6034
+ this['_jsc'] = NOOP_FUNC;
6507
6035
 
6508
6036
  this.__dom_loaded_queue = [];
6509
6037
  this.__request_queue = [];
@@ -6519,22 +6047,42 @@
6519
6047
  if (this._batch_requests) {
6520
6048
  if (!_.localStorage.is_supported(true) || !USE_XHR) {
6521
6049
  this._batch_requests = false;
6522
- console$1.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6050
+ console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
6523
6051
  } else {
6524
- this.start_batch_requests();
6052
+ this.init_batchers();
6525
6053
  if (sendBeacon && window$1.addEventListener) {
6526
- window$1.addEventListener('unload', _.bind(function() {
6527
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
6528
- // Since sendBeacon doesn't report success/failure, events will not be removed from
6529
- // the persistent store; if the site is loaded again, the events will be flushed again
6530
- // on startup and deduplicated on the Mixpanel server side.
6531
- this.request_batchers.events.flush({sendBeacon: true});
6532
- }, this));
6054
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
6055
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
6056
+ // events will not be removed from the persistent store; if the site is loaded again,
6057
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
6058
+ // side.
6059
+ // There is no reliable way to capture only page close events, so we lean on the
6060
+ // visibilitychange and pagehide events as recommended at
6061
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
6062
+ // These events fire when the user clicks away from the current page/tab, so will occur
6063
+ // more frequently than page unload, but are the only mechanism currently for capturing
6064
+ // this scenario somewhat reliably.
6065
+ var flush_on_unload = _.bind(function() {
6066
+ if (!this.request_batchers.events.stopped) {
6067
+ this.request_batchers.events.flush({unloading: true});
6068
+ }
6069
+ }, this);
6070
+ window$1.addEventListener('pagehide', function(ev) {
6071
+ if (ev['persisted']) {
6072
+ flush_on_unload();
6073
+ }
6074
+ });
6075
+ window$1.addEventListener('visibilitychange', function() {
6076
+ if (document$1['visibilityState'] === 'hidden') {
6077
+ flush_on_unload();
6078
+ }
6079
+ });
6533
6080
  }
6534
6081
  }
6535
6082
  }
6536
6083
 
6537
6084
  this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']);
6085
+ this.unpersisted_superprops = {};
6538
6086
  this._gdpr_init();
6539
6087
 
6540
6088
  var uuid = _.UUID();
@@ -6584,7 +6132,7 @@
6584
6132
 
6585
6133
  MixpanelLib.prototype._track_dom = function(DomClass, args) {
6586
6134
  if (this.get_config('img')) {
6587
- console$1.error('You can\'t use DOM tracking functions with img = true.');
6135
+ console.error('You can\'t use DOM tracking functions with img = true.');
6588
6136
  return false;
6589
6137
  }
6590
6138
 
@@ -6694,9 +6242,16 @@
6694
6242
  try {
6695
6243
  succeeded = sendBeacon(url, body_data);
6696
6244
  } catch (e) {
6697
- console$1.error(e);
6245
+ console.error(e);
6698
6246
  succeeded = false;
6699
6247
  }
6248
+ try {
6249
+ if (callback) {
6250
+ callback(succeeded ? 1 : 0);
6251
+ }
6252
+ } catch (e) {
6253
+ console.error(e);
6254
+ }
6700
6255
  } else if (USE_XHR) {
6701
6256
  try {
6702
6257
  var req = new XMLHttpRequest();
@@ -6727,7 +6282,7 @@
6727
6282
  try {
6728
6283
  response = _.JSONDecode(req.responseText);
6729
6284
  } catch (e) {
6730
- console$1.error(e);
6285
+ console.error(e);
6731
6286
  if (options.ignore_json_errors) {
6732
6287
  response = req.responseText;
6733
6288
  } else {
@@ -6750,7 +6305,7 @@
6750
6305
  } else {
6751
6306
  error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText;
6752
6307
  }
6753
- console$1.error(error);
6308
+ console.error(error);
6754
6309
  if (callback) {
6755
6310
  if (verbose_mode) {
6756
6311
  callback({status: 0, error: error, xhr_req: req});
@@ -6763,7 +6318,7 @@
6763
6318
  };
6764
6319
  req.send(body_data);
6765
6320
  } catch (e) {
6766
- console$1.error(e);
6321
+ console.error(e);
6767
6322
  succeeded = false;
6768
6323
  }
6769
6324
  } else {
@@ -6831,32 +6386,53 @@
6831
6386
 
6832
6387
  // request queueing utils
6833
6388
 
6834
- MixpanelLib.prototype.start_batch_requests = function() {
6389
+ MixpanelLib.prototype.are_batchers_initialized = function() {
6390
+ return !!this.request_batchers.events;
6391
+ };
6392
+
6393
+ MixpanelLib.prototype.init_batchers = function() {
6835
6394
  var token = this.get_config('token');
6836
- if (!this.request_batchers.events) { // no batchers initialized yet
6837
- var batcher_config = {
6838
- libConfig: this['config'],
6839
- sendRequestFunc: _.bind(function(endpoint, data, options, cb) {
6840
- this._send_request(
6841
- this.get_config('api_host') + endpoint,
6842
- encode_data_for_request(data),
6843
- options,
6844
- this._prepare_callback(cb, data)
6845
- );
6846
- }, this)
6847
- };
6395
+ if (!this.are_batchers_initialized()) {
6396
+ var batcher_for = _.bind(function(attrs) {
6397
+ return new RequestBatcher(
6398
+ '__mpq_' + token + attrs.queue_suffix,
6399
+ {
6400
+ libConfig: this['config'],
6401
+ sendRequestFunc: _.bind(function(data, options, cb) {
6402
+ this._send_request(
6403
+ this.get_config('api_host') + attrs.endpoint,
6404
+ encode_data_for_request(data),
6405
+ options,
6406
+ this._prepare_callback(cb, data)
6407
+ );
6408
+ }, this),
6409
+ beforeSendHook: _.bind(function(item) {
6410
+ return this._run_hook('before_send_' + attrs.type, item);
6411
+ }, this)
6412
+ }
6413
+ );
6414
+ }, this);
6848
6415
  this.request_batchers = {
6849
- events: new RequestBatcher('__mpq_' + token + '_ev', '/track/', batcher_config),
6850
- people: new RequestBatcher('__mpq_' + token + '_pp', '/engage/', batcher_config),
6851
- groups: new RequestBatcher('__mpq_' + token + '_gr', '/groups/', batcher_config)
6416
+ events: batcher_for({type: 'events', endpoint: '/track/', queue_suffix: '_ev'}),
6417
+ people: batcher_for({type: 'people', endpoint: '/engage/', queue_suffix: '_pp'}),
6418
+ groups: batcher_for({type: 'groups', endpoint: '/groups/', queue_suffix: '_gr'})
6852
6419
  };
6853
6420
  }
6854
- _.each(this.request_batchers, function(batcher) {
6855
- batcher.start();
6856
- });
6421
+ if (this.get_config('batch_autostart')) {
6422
+ this.start_batch_senders();
6423
+ }
6857
6424
  };
6858
6425
 
6859
- MixpanelLib.prototype.stop_batch_requests = function() {
6426
+ MixpanelLib.prototype.start_batch_senders = function() {
6427
+ if (this.are_batchers_initialized()) {
6428
+ this._batch_requests = true;
6429
+ _.each(this.request_batchers, function(batcher) {
6430
+ batcher.start();
6431
+ });
6432
+ }
6433
+ };
6434
+
6435
+ MixpanelLib.prototype.stop_batch_senders = function() {
6860
6436
  this._batch_requests = false;
6861
6437
  _.each(this.request_batchers, function(batcher) {
6862
6438
  batcher.stop();
@@ -6901,23 +6477,30 @@
6901
6477
 
6902
6478
  // internal method for handling track vs batch-enqueue logic
6903
6479
  MixpanelLib.prototype._track_or_batch = function(options, callback) {
6904
- var truncated_data = options.truncated_data;
6480
+ var truncated_data = _.truncate(options.data, 255);
6905
6481
  var endpoint = options.endpoint;
6906
6482
  var batcher = options.batcher;
6907
6483
  var should_send_immediately = options.should_send_immediately;
6908
6484
  var send_request_options = options.send_request_options || {};
6909
- callback = callback || function() {};
6485
+ callback = callback || NOOP_FUNC;
6910
6486
 
6911
6487
  var request_enqueued_or_initiated = true;
6912
6488
  var send_request_immediately = _.bind(function() {
6913
- console$1.log('MIXPANEL REQUEST:');
6914
- console$1.log(truncated_data);
6915
- return this._send_request(
6916
- endpoint,
6917
- encode_data_for_request(truncated_data),
6918
- send_request_options,
6919
- this._prepare_callback(callback, truncated_data)
6920
- );
6489
+ if (!send_request_options.skip_hooks) {
6490
+ truncated_data = this._run_hook('before_send_' + options.type, truncated_data);
6491
+ }
6492
+ if (truncated_data) {
6493
+ console.log('MIXPANEL REQUEST:');
6494
+ console.log(truncated_data);
6495
+ return this._send_request(
6496
+ endpoint,
6497
+ encode_data_for_request(truncated_data),
6498
+ send_request_options,
6499
+ this._prepare_callback(callback, truncated_data)
6500
+ );
6501
+ } else {
6502
+ return null;
6503
+ }
6921
6504
  }, this);
6922
6505
 
6923
6506
  if (this._batch_requests && !should_send_immediately) {
@@ -6970,11 +6553,11 @@
6970
6553
  }
6971
6554
  var should_send_immediately = options['send_immediately'];
6972
6555
  if (typeof callback !== 'function') {
6973
- callback = function() {};
6556
+ callback = NOOP_FUNC;
6974
6557
  }
6975
6558
 
6976
6559
  if (_.isUndefined(event_name)) {
6977
- console$1.error('No event name provided to mixpanel.track');
6560
+ console.error('No event name provided to mixpanel.track');
6978
6561
  return;
6979
6562
  }
6980
6563
 
@@ -7005,6 +6588,7 @@
7005
6588
  {},
7006
6589
  _.info.properties(),
7007
6590
  this['persistence'].properties(),
6591
+ this.unpersisted_superprops,
7008
6592
  properties
7009
6593
  );
7010
6594
 
@@ -7014,7 +6598,7 @@
7014
6598
  delete properties[blacklisted_prop];
7015
6599
  });
7016
6600
  } else {
7017
- console$1.error('Invalid value for property_blacklist config: ' + property_blacklist);
6601
+ console.error('Invalid value for property_blacklist config: ' + property_blacklist);
7018
6602
  }
7019
6603
 
7020
6604
  var data = {
@@ -7022,7 +6606,8 @@
7022
6606
  'properties': properties
7023
6607
  };
7024
6608
  var ret = this._track_or_batch({
7025
- truncated_data: _.truncate(data, 255),
6609
+ type: 'events',
6610
+ data: data,
7026
6611
  endpoint: this.get_config('api_host') + '/track/',
7027
6612
  batcher: this.request_batchers.events,
7028
6613
  should_send_immediately: should_send_immediately,
@@ -7258,7 +6843,7 @@
7258
6843
  */
7259
6844
  MixpanelLib.prototype.time_event = function(event_name) {
7260
6845
  if (_.isUndefined(event_name)) {
7261
- console$1.error('No event name provided to mixpanel.time_event');
6846
+ console.error('No event name provided to mixpanel.time_event');
7262
6847
  return;
7263
6848
  }
7264
6849
 
@@ -7269,6 +6854,27 @@
7269
6854
  this['persistence'].set_event_timer(event_name, new Date().getTime());
7270
6855
  };
7271
6856
 
6857
+ var REGISTER_DEFAULTS = {
6858
+ 'persistent': true
6859
+ };
6860
+ /**
6861
+ * Helper to parse options param for register methods, maintaining
6862
+ * legacy support for plain "days" param instead of options object
6863
+ * @param {Number|Object} [days_or_options] 'days' option (Number), or Options object for register methods
6864
+ * @returns {Object} options object
6865
+ */
6866
+ var options_for_register = function(days_or_options) {
6867
+ var options;
6868
+ if (_.isObject(days_or_options)) {
6869
+ options = days_or_options;
6870
+ } else if (!_.isUndefined(days_or_options)) {
6871
+ options = {'days': days_or_options};
6872
+ } else {
6873
+ options = {};
6874
+ }
6875
+ return _.extend({}, REGISTER_DEFAULTS, options);
6876
+ };
6877
+
7272
6878
  /**
7273
6879
  * Register a set of super properties, which are included with all
7274
6880
  * events. This will overwrite previous super property values.
@@ -7284,11 +6890,21 @@
7284
6890
  * 'Account Type': 'Free'
7285
6891
  * });
7286
6892
  *
6893
+ * // register only for the current pageload
6894
+ * mixpanel.register({'Name': 'Pat'}, {persistent: false});
6895
+ *
7287
6896
  * @param {Object} properties An associative array of properties to store about the user
7288
- * @param {Number} [days] How many days since the user's last visit to store the super properties
6897
+ * @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)
6898
+ * @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)
6899
+ * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
7289
6900
  */
7290
- MixpanelLib.prototype.register = function(props, days) {
7291
- this['persistence'].register(props, days);
6901
+ MixpanelLib.prototype.register = function(props, days_or_options) {
6902
+ var options = options_for_register(days_or_options);
6903
+ if (options['persistent']) {
6904
+ this['persistence'].register(props, options['days']);
6905
+ } else {
6906
+ _.extend(this.unpersisted_superprops, props);
6907
+ }
7292
6908
  };
7293
6909
 
7294
6910
  /**
@@ -7302,6 +6918,11 @@
7302
6918
  * 'First Login Date': new Date().toISOString()
7303
6919
  * });
7304
6920
  *
6921
+ * // register once, only for the current pageload
6922
+ * mixpanel.register_once({
6923
+ * 'First interaction time': new Date().toISOString()
6924
+ * }, 'None', {persistent: false});
6925
+ *
7305
6926
  * ### Notes:
7306
6927
  *
7307
6928
  * If default_value is specified, current super properties
@@ -7309,19 +6930,40 @@
7309
6930
  *
7310
6931
  * @param {Object} properties An associative array of properties to store about the user
7311
6932
  * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None'
7312
- * @param {Number} [days] How many days since the users last visit to store the super properties
6933
+ * @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)
6934
+ * @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)
6935
+ * @param {boolean} [days_or_options.persistent=true] - whether to put in persistent storage (cookie/localStorage)
7313
6936
  */
7314
- MixpanelLib.prototype.register_once = function(props, default_value, days) {
7315
- this['persistence'].register_once(props, default_value, days);
6937
+ MixpanelLib.prototype.register_once = function(props, default_value, days_or_options) {
6938
+ var options = options_for_register(days_or_options);
6939
+ if (options['persistent']) {
6940
+ this['persistence'].register_once(props, default_value, options['days']);
6941
+ } else {
6942
+ if (typeof(default_value) === 'undefined') {
6943
+ default_value = 'None';
6944
+ }
6945
+ _.each(props, function(val, prop) {
6946
+ if (!this.unpersisted_superprops.hasOwnProperty(prop) || this.unpersisted_superprops[prop] === default_value) {
6947
+ this.unpersisted_superprops[prop] = val;
6948
+ }
6949
+ }, this);
6950
+ }
7316
6951
  };
7317
6952
 
7318
6953
  /**
7319
6954
  * Delete a super property stored with the current user.
7320
6955
  *
7321
6956
  * @param {String} property The name of the super property to remove
6957
+ * @param {Object} [options]
6958
+ * @param {boolean} [options.persistent=true] - whether to look in persistent storage (cookie/localStorage)
7322
6959
  */
7323
- MixpanelLib.prototype.unregister = function(property) {
7324
- this['persistence'].unregister(property);
6960
+ MixpanelLib.prototype.unregister = function(property, options) {
6961
+ options = options_for_register(options);
6962
+ if (options['persistent']) {
6963
+ this['persistence'].unregister(property);
6964
+ } else {
6965
+ delete this.unpersisted_superprops[property];
6966
+ }
7325
6967
  };
7326
6968
 
7327
6969
  MixpanelLib.prototype._register_single = function(prop, value) {
@@ -7392,7 +7034,10 @@
7392
7034
  // send an $identify event any time the distinct_id is changing - logic on the server
7393
7035
  // will determine whether or not to do anything with it.
7394
7036
  if (new_distinct_id !== previous_distinct_id) {
7395
- this.track('$identify', { 'distinct_id': new_distinct_id, '$anon_distinct_id': previous_distinct_id });
7037
+ this.track('$identify', {
7038
+ 'distinct_id': new_distinct_id,
7039
+ '$anon_distinct_id': previous_distinct_id
7040
+ }, {skip_hooks: true});
7396
7041
  }
7397
7042
  };
7398
7043
 
@@ -7471,7 +7116,7 @@
7471
7116
  // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with
7472
7117
  // this ID, as it will duplicate users.
7473
7118
  if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) {
7474
- console$1.critical('Attempting to create alias for existing People user - aborting.');
7119
+ console.critical('Attempting to create alias for existing People user - aborting.');
7475
7120
  return -2;
7476
7121
  }
7477
7122
 
@@ -7481,12 +7126,17 @@
7481
7126
  }
7482
7127
  if (alias !== original) {
7483
7128
  this._register_single(ALIAS_ID_KEY, alias);
7484
- return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() {
7129
+ return this.track('$create_alias', {
7130
+ 'alias': alias,
7131
+ 'distinct_id': original
7132
+ }, {
7133
+ skip_hooks: true
7134
+ }, function() {
7485
7135
  // Flush the people queue
7486
7136
  _this.identify(alias);
7487
7137
  });
7488
7138
  } else {
7489
- console$1.error('alias matches current distinct_id - skipping api call.');
7139
+ console.error('alias matches current distinct_id - skipping api call.');
7490
7140
  this.identify(alias);
7491
7141
  return -1;
7492
7142
  }
@@ -7660,6 +7310,21 @@
7660
7310
  return this['config'][prop_name];
7661
7311
  };
7662
7312
 
7313
+ /**
7314
+ * Fetch a hook function from config, with safe default, and run it
7315
+ * against the given arguments
7316
+ * @param {string} hook_name which hook to retrieve
7317
+ * @returns {any|null} return value of user-provided hook, or null if nothing was returned
7318
+ */
7319
+ MixpanelLib.prototype._run_hook = function(hook_name) {
7320
+ var ret = (this['config']['hooks'][hook_name] || IDENTITY_FUNC).apply(this, slice.call(arguments, 1));
7321
+ if (typeof ret === 'undefined') {
7322
+ console.error(hook_name + ' hook did not return a value');
7323
+ ret = null;
7324
+ }
7325
+ return ret;
7326
+ };
7327
+
7663
7328
  /**
7664
7329
  * Returns the value of the super property named property_name. If no such
7665
7330
  * property is set, get_property() will return the undefined value.
@@ -7720,7 +7385,7 @@
7720
7385
  return;
7721
7386
  }
7722
7387
 
7723
- console$1.log('MIXPANEL NOTIFICATION CHECK');
7388
+ console.log('MIXPANEL NOTIFICATION CHECK');
7724
7389
 
7725
7390
  var data = {
7726
7391
  'verbose': true,
@@ -8045,7 +7710,8 @@
8045
7710
  MixpanelLib.prototype['add_group'] = MixpanelLib.prototype.add_group;
8046
7711
  MixpanelLib.prototype['remove_group'] = MixpanelLib.prototype.remove_group;
8047
7712
  MixpanelLib.prototype['track_with_groups'] = MixpanelLib.prototype.track_with_groups;
8048
- MixpanelLib.prototype['stop_batch_requests'] = MixpanelLib.prototype.stop_batch_requests;
7713
+ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.prototype.start_batch_senders;
7714
+ MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
8049
7715
 
8050
7716
  // MixpanelPersistence Exports
8051
7717
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;