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.
- package/.github/workflows/tests.yml +25 -0
- package/CHANGELOG.md +21 -0
- package/README.md +4 -2
- package/dist/mixpanel-jslib-snippet.min.js +3 -4
- package/dist/mixpanel-jslib-snippet.min.test.js +3 -4
- package/dist/mixpanel.amd.js +370 -704
- package/dist/mixpanel.cjs.js +370 -704
- package/dist/mixpanel.globals.js +373 -707
- package/dist/mixpanel.min.js +150 -158
- package/dist/mixpanel.umd.js +370 -704
- package/doc/build-docs.js +16 -0
- package/doc/readme.io/javascript-full-api-reference.md +44 -2
- package/mixpanel-jslib-snippet.js +1 -20
- package/package.json +3 -3
- package/src/config.js +1 -1
- package/src/gdpr-utils.js +7 -2
- package/src/mixpanel-core.js +201 -85
- package/src/mixpanel-group.js +6 -1
- package/src/mixpanel-people.js +3 -3
- package/src/request-batcher.js +27 -13
- package/src/request-queue.js +47 -0
- package/src/utils.js +48 -37
- package/tunnel.log +0 -0
- package/.travis.yml +0 -8
- package/src/autotrack-utils.js +0 -192
- package/src/autotrack.js +0 -355
package/dist/mixpanel.cjs.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var Config = {
|
|
4
4
|
DEBUG: false,
|
|
5
|
-
LIB_VERSION: '2.
|
|
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
|
|
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
|
|
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
|
|
108
|
-
error: log_func_with_prefix(console
|
|
109
|
-
critical: log_func_with_prefix(console
|
|
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
|
|
384
|
+
console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
|
|
376
385
|
if (Config.DEBUG){
|
|
377
|
-
console
|
|
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
|
-
|
|
939
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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']
|
|
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.
|
|
2475
|
+
if (options.unloading) {
|
|
2937
2476
|
requestOptions.transport = 'sendBeacon';
|
|
2938
2477
|
}
|
|
2939
|
-
logger.log('MIXPANEL REQUEST:',
|
|
2940
|
-
this.sendRequest(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3913
|
-
console
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5671
|
+
return _.truncate(date_encoded_data, 255);
|
|
6124
5672
|
}
|
|
6125
5673
|
|
|
6126
5674
|
return this._mixpanel._track_or_batch({
|
|
6127
|
-
|
|
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
|
|
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':
|
|
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':
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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'] =
|
|
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
|
|
6046
|
+
console.log('Turning off Mixpanel request-queueing; needs XHR and localStorage support');
|
|
6519
6047
|
} else {
|
|
6520
|
-
this.
|
|
6048
|
+
this.init_batchers();
|
|
6521
6049
|
if (sendBeacon && window$1.addEventListener) {
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
6833
|
-
var
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
this
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
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:
|
|
6846
|
-
people:
|
|
6847
|
-
groups:
|
|
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
|
-
|
|
6851
|
-
|
|
6852
|
-
}
|
|
6417
|
+
if (this.get_config('batch_autostart')) {
|
|
6418
|
+
this.start_batch_senders();
|
|
6419
|
+
}
|
|
6853
6420
|
};
|
|
6854
6421
|
|
|
6855
|
-
MixpanelLib.prototype.
|
|
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.
|
|
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 ||
|
|
6481
|
+
callback = callback || NOOP_FUNC;
|
|
6906
6482
|
|
|
6907
6483
|
var request_enqueued_or_initiated = true;
|
|
6908
6484
|
var send_request_immediately = _.bind(function() {
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
this.
|
|
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 =
|
|
6552
|
+
callback = NOOP_FUNC;
|
|
6970
6553
|
}
|
|
6971
6554
|
|
|
6972
6555
|
if (_.isUndefined(event_name)) {
|
|
6973
|
-
console
|
|
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
|
|
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
|
-
|
|
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
|
|
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} [
|
|
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,
|
|
7287
|
-
|
|
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} [
|
|
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,
|
|
7311
|
-
|
|
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
|
-
|
|
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', {
|
|
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
|
|
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', {
|
|
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
|
|
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
|
|
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['
|
|
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;
|