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