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