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/src/request-queue.js
CHANGED
|
@@ -95,6 +95,7 @@ RequestQueue.prototype.fillBatch = function(batchSize) {
|
|
|
95
95
|
for (var i = 0; i < storedQueue.length; i++) {
|
|
96
96
|
var item = storedQueue[i];
|
|
97
97
|
if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
|
|
98
|
+
item.orphaned = true;
|
|
98
99
|
batch.push(item);
|
|
99
100
|
if (batch.length >= batchSize) {
|
|
100
101
|
break;
|
|
@@ -151,6 +152,52 @@ RequestQueue.prototype.removeItemsByID = function(ids, cb) {
|
|
|
151
152
|
}, this.pid);
|
|
152
153
|
};
|
|
153
154
|
|
|
155
|
+
// internal helper for RequestQueue.updatePayloads
|
|
156
|
+
var updatePayloads = function(existingItems, itemsToUpdate) {
|
|
157
|
+
var newItems = [];
|
|
158
|
+
_.each(existingItems, function(item) {
|
|
159
|
+
var id = item['id'];
|
|
160
|
+
if (id in itemsToUpdate) {
|
|
161
|
+
var newPayload = itemsToUpdate[id];
|
|
162
|
+
if (newPayload !== null) {
|
|
163
|
+
item['payload'] = newPayload;
|
|
164
|
+
newItems.push(item);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// no update
|
|
168
|
+
newItems.push(item);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return newItems;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Update payloads of given items in both in-memory queue and
|
|
176
|
+
* persisted queue. Items set to null are removed from queues.
|
|
177
|
+
*/
|
|
178
|
+
RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
|
|
179
|
+
this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
|
|
180
|
+
this.lock.withLock(_.bind(function lockAcquired() {
|
|
181
|
+
var succeeded;
|
|
182
|
+
try {
|
|
183
|
+
var storedQueue = this.readFromStorage();
|
|
184
|
+
storedQueue = updatePayloads(storedQueue, itemsToUpdate);
|
|
185
|
+
succeeded = this.saveToStorage(storedQueue);
|
|
186
|
+
} catch(err) {
|
|
187
|
+
logger.error('Error updating items', itemsToUpdate);
|
|
188
|
+
succeeded = false;
|
|
189
|
+
}
|
|
190
|
+
if (cb) {
|
|
191
|
+
cb(succeeded);
|
|
192
|
+
}
|
|
193
|
+
}, this), function lockFailure(err) {
|
|
194
|
+
logger.error('Error acquiring storage lock', err);
|
|
195
|
+
if (cb) {
|
|
196
|
+
cb(false);
|
|
197
|
+
}
|
|
198
|
+
}, this.pid);
|
|
199
|
+
};
|
|
200
|
+
|
|
154
201
|
/**
|
|
155
202
|
* Read and parse items array from localStorage entry, handling
|
|
156
203
|
* malformed/missing data if necessary.
|
package/src/utils.js
CHANGED
|
@@ -67,6 +67,19 @@ var console = {
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
/** @type {function(...*)} */
|
|
70
|
+
warn: function() {
|
|
71
|
+
if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
|
|
72
|
+
var args = ['Mixpanel warning:'].concat(_.toArray(arguments));
|
|
73
|
+
try {
|
|
74
|
+
windowConsole.warn.apply(windowConsole, args);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
_.each(args, function(arg) {
|
|
77
|
+
windowConsole.warn(arg);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
/** @type {function(...*)} */
|
|
70
83
|
error: function() {
|
|
71
84
|
if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
|
|
72
85
|
var args = ['Mixpanel error:'].concat(_.toArray(arguments));
|
|
@@ -232,13 +245,13 @@ _.toArray = function(iterable) {
|
|
|
232
245
|
return _.values(iterable);
|
|
233
246
|
};
|
|
234
247
|
|
|
235
|
-
_.map = function(arr, callback) {
|
|
248
|
+
_.map = function(arr, callback, context) {
|
|
236
249
|
if (nativeMap && arr.map === nativeMap) {
|
|
237
|
-
return arr.map(callback);
|
|
250
|
+
return arr.map(callback, context);
|
|
238
251
|
} else {
|
|
239
252
|
var results = [];
|
|
240
253
|
_.each(arr, function(item) {
|
|
241
|
-
results.push(callback(item));
|
|
254
|
+
results.push(callback.call(context, item));
|
|
242
255
|
});
|
|
243
256
|
return results;
|
|
244
257
|
}
|
|
@@ -266,10 +279,6 @@ _.values = function(obj) {
|
|
|
266
279
|
return results;
|
|
267
280
|
};
|
|
268
281
|
|
|
269
|
-
_.identity = function(value) {
|
|
270
|
-
return value;
|
|
271
|
-
};
|
|
272
|
-
|
|
273
282
|
_.include = function(obj, target) {
|
|
274
283
|
var found = false;
|
|
275
284
|
if (obj === null) {
|
|
@@ -932,9 +941,37 @@ _.UUID = (function() {
|
|
|
932
941
|
// _.isBlockedUA()
|
|
933
942
|
// This is to block various web spiders from executing our JS and
|
|
934
943
|
// sending false tracking data
|
|
944
|
+
var BLOCKED_UA_STRS = [
|
|
945
|
+
'baiduspider',
|
|
946
|
+
'bingbot',
|
|
947
|
+
'bingpreview',
|
|
948
|
+
'facebookexternal',
|
|
949
|
+
'pinterest',
|
|
950
|
+
'screaming frog',
|
|
951
|
+
'yahoo! slurp',
|
|
952
|
+
'yandexbot',
|
|
953
|
+
|
|
954
|
+
// a whole bunch of goog-specific crawlers
|
|
955
|
+
// https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
|
|
956
|
+
'adsbot-google',
|
|
957
|
+
'apis-google',
|
|
958
|
+
'duplexweb-google',
|
|
959
|
+
'feedfetcher-google',
|
|
960
|
+
'google favicon',
|
|
961
|
+
'google web preview',
|
|
962
|
+
'google-read-aloud',
|
|
963
|
+
'googlebot',
|
|
964
|
+
'googleweblight',
|
|
965
|
+
'mediapartners-google',
|
|
966
|
+
'storebot-google'
|
|
967
|
+
];
|
|
935
968
|
_.isBlockedUA = function(ua) {
|
|
936
|
-
|
|
937
|
-
|
|
969
|
+
var i;
|
|
970
|
+
ua = ua.toLowerCase();
|
|
971
|
+
for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
|
|
972
|
+
if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
938
975
|
}
|
|
939
976
|
return false;
|
|
940
977
|
};
|
|
@@ -979,10 +1016,6 @@ _.getQueryParam = function(url, param) {
|
|
|
979
1016
|
}
|
|
980
1017
|
};
|
|
981
1018
|
|
|
982
|
-
_.getHashParam = function(hash, param) {
|
|
983
|
-
var matches = hash.match(new RegExp(param + '=([^&]*)'));
|
|
984
|
-
return matches ? matches[1] : null;
|
|
985
|
-
};
|
|
986
1019
|
|
|
987
1020
|
// _.cookie
|
|
988
1021
|
// Methods partially borrowed from quirksmode.org/js/cookies.html
|
|
@@ -1649,28 +1682,6 @@ var cheap_guid = function(maxlen) {
|
|
|
1649
1682
|
return maxlen ? guid.substring(0, maxlen) : guid;
|
|
1650
1683
|
};
|
|
1651
1684
|
|
|
1652
|
-
/**
|
|
1653
|
-
* Check deterministically whether to include or exclude from a feature rollout/test based on the
|
|
1654
|
-
* given string and the desired percentage to include.
|
|
1655
|
-
* @param {String} str - string to run the check against (for instance a project's token)
|
|
1656
|
-
* @param {String} feature - name of feature (for inclusion in hash, to ensure different results
|
|
1657
|
-
* for different features)
|
|
1658
|
-
* @param {Number} percent_allowed - percentage chance that a given string will be included
|
|
1659
|
-
* @returns {Boolean} whether the given string should be included
|
|
1660
|
-
*/
|
|
1661
|
-
var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
|
|
1662
|
-
str = str + feature;
|
|
1663
|
-
|
|
1664
|
-
// Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
|
|
1665
|
-
var hash = 5381;
|
|
1666
|
-
for (var i = 0; i < str.length; i++) {
|
|
1667
|
-
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
1668
|
-
hash = hash & hash;
|
|
1669
|
-
}
|
|
1670
|
-
var dart = (hash >>> 0) % 100;
|
|
1671
|
-
return dart < percent_allowed;
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
1685
|
// naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
|
|
1675
1686
|
var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
|
|
1676
1687
|
// this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
|
|
@@ -1729,9 +1740,9 @@ export {
|
|
|
1729
1740
|
navigator,
|
|
1730
1741
|
cheap_guid,
|
|
1731
1742
|
console_with_prefix,
|
|
1732
|
-
determine_eligibility,
|
|
1733
1743
|
extract_domain,
|
|
1734
1744
|
localStorageSupported,
|
|
1735
1745
|
JSONStringify,
|
|
1736
|
-
JSONParse
|
|
1746
|
+
JSONParse,
|
|
1747
|
+
slice
|
|
1737
1748
|
};
|
package/tunnel.log
ADDED
|
File without changes
|
package/.travis.yml
DELETED
package/src/autotrack-utils.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { _ } from './utils';
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
* Get the className of an element, accounting for edge cases where element.className is an object
|
|
5
|
-
* @param {Element} el - element to get the className of
|
|
6
|
-
* @returns {string} the element's class
|
|
7
|
-
*/
|
|
8
|
-
export function getClassName(el) {
|
|
9
|
-
switch(typeof el.className) {
|
|
10
|
-
case 'string':
|
|
11
|
-
return el.className;
|
|
12
|
-
case 'object': // handle cases where className might be SVGAnimatedString or some other type
|
|
13
|
-
return el.className.baseVal || el.getAttribute('class') || '';
|
|
14
|
-
default: // future proof
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/*
|
|
20
|
-
* Get the direct text content of an element, protecting against sensitive data collection.
|
|
21
|
-
* Concats textContent of each of the element's text node children; this avoids potential
|
|
22
|
-
* collection of sensitive data that could happen if we used element.textContent and the
|
|
23
|
-
* element had sensitive child elements, since element.textContent includes child content.
|
|
24
|
-
* Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
|
|
25
|
-
* @param {Element} el - element to get the text of
|
|
26
|
-
* @returns {string} the element's direct text content
|
|
27
|
-
*/
|
|
28
|
-
export function getSafeText(el) {
|
|
29
|
-
var elText = '';
|
|
30
|
-
|
|
31
|
-
if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
|
|
32
|
-
_.each(el.childNodes, function(child) {
|
|
33
|
-
if (isTextNode(child) && child.textContent) {
|
|
34
|
-
elText += _.trim(child.textContent)
|
|
35
|
-
// scrub potentially sensitive values
|
|
36
|
-
.split(/(\s+)/).filter(shouldTrackValue).join('')
|
|
37
|
-
// normalize whitespace
|
|
38
|
-
.replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
|
|
39
|
-
// truncate
|
|
40
|
-
.substring(0, 255);
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return _.trim(elText);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/*
|
|
49
|
-
* Check whether an element has nodeType Node.ELEMENT_NODE
|
|
50
|
-
* @param {Element} el - element to check
|
|
51
|
-
* @returns {boolean} whether el is of the correct nodeType
|
|
52
|
-
*/
|
|
53
|
-
export function isElementNode(el) {
|
|
54
|
-
return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/*
|
|
58
|
-
* Check whether an element is of a given tag type.
|
|
59
|
-
* Due to potential reference discrepancies (such as the webcomponents.js polyfill),
|
|
60
|
-
* we want to match tagNames instead of specific references because something like
|
|
61
|
-
* element === document.body won't always work because element might not be a native
|
|
62
|
-
* element.
|
|
63
|
-
* @param {Element} el - element to check
|
|
64
|
-
* @param {string} tag - tag name (e.g., "div")
|
|
65
|
-
* @returns {boolean} whether el is of the given tag type
|
|
66
|
-
*/
|
|
67
|
-
export function isTag(el, tag) {
|
|
68
|
-
return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/*
|
|
72
|
-
* Check whether an element has nodeType Node.TEXT_NODE
|
|
73
|
-
* @param {Element} el - element to check
|
|
74
|
-
* @returns {boolean} whether el is of the correct nodeType
|
|
75
|
-
*/
|
|
76
|
-
export function isTextNode(el) {
|
|
77
|
-
return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/*
|
|
81
|
-
* Check whether a DOM event should be "tracked" or if it may contain sentitive data
|
|
82
|
-
* using a variety of heuristics.
|
|
83
|
-
* @param {Element} el - element to check
|
|
84
|
-
* @param {Event} event - event to check
|
|
85
|
-
* @returns {boolean} whether the event should be tracked
|
|
86
|
-
*/
|
|
87
|
-
export function shouldTrackDomEvent(el, event) {
|
|
88
|
-
if (!el || isTag(el, 'html') || !isElementNode(el)) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
var tag = el.tagName.toLowerCase();
|
|
92
|
-
switch (tag) {
|
|
93
|
-
case 'html':
|
|
94
|
-
return false;
|
|
95
|
-
case 'form':
|
|
96
|
-
return event.type === 'submit';
|
|
97
|
-
case 'input':
|
|
98
|
-
if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
|
|
99
|
-
return event.type === 'change';
|
|
100
|
-
} else {
|
|
101
|
-
return event.type === 'click';
|
|
102
|
-
}
|
|
103
|
-
case 'select':
|
|
104
|
-
case 'textarea':
|
|
105
|
-
return event.type === 'change';
|
|
106
|
-
default:
|
|
107
|
-
return event.type === 'click';
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/*
|
|
112
|
-
* Check whether a DOM element should be "tracked" or if it may contain sentitive data
|
|
113
|
-
* using a variety of heuristics.
|
|
114
|
-
* @param {Element} el - element to check
|
|
115
|
-
* @returns {boolean} whether the element should be tracked
|
|
116
|
-
*/
|
|
117
|
-
export function shouldTrackElement(el) {
|
|
118
|
-
for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
|
|
119
|
-
var classes = getClassName(curEl).split(' ');
|
|
120
|
-
if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (_.includes(getClassName(el).split(' '), 'mp-include')) {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// don't send data from inputs or similar elements since there will always be
|
|
130
|
-
// a risk of clientside javascript placing sensitive data in attributes
|
|
131
|
-
if (
|
|
132
|
-
isTag(el, 'input') ||
|
|
133
|
-
isTag(el, 'select') ||
|
|
134
|
-
isTag(el, 'textarea') ||
|
|
135
|
-
el.getAttribute('contenteditable') === 'true'
|
|
136
|
-
) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// don't include hidden or password fields
|
|
141
|
-
var type = el.type || '';
|
|
142
|
-
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"]
|
|
143
|
-
switch(type.toLowerCase()) {
|
|
144
|
-
case 'hidden':
|
|
145
|
-
return false;
|
|
146
|
-
case 'password':
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// filter out data from fields that look like sensitive fields
|
|
152
|
-
var name = el.name || el.id || '';
|
|
153
|
-
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"]
|
|
154
|
-
var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
155
|
-
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/*
|
|
164
|
-
* Check whether a string value should be "tracked" or if it may contain sentitive data
|
|
165
|
-
* using a variety of heuristics.
|
|
166
|
-
* @param {string} value - string value to check
|
|
167
|
-
* @returns {boolean} whether the element should be tracked
|
|
168
|
-
*/
|
|
169
|
-
export function shouldTrackValue(value) {
|
|
170
|
-
if (value === null || _.isUndefined(value)) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (typeof value === 'string') {
|
|
175
|
-
value = _.trim(value);
|
|
176
|
-
|
|
177
|
-
// check to see if input value looks like a credit card number
|
|
178
|
-
// see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
|
|
179
|
-
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}))$/;
|
|
180
|
-
if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// check to see if input value looks like a social security number
|
|
185
|
-
var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
|
|
186
|
-
if (ssnRegex.test(value)) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return true;
|
|
192
|
-
}
|