mixpanel-browser 2.58.0 → 2.60.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 +1 -1
- package/CHANGELOG.md +9 -1
- package/README.md +1 -1
- package/dist/mixpanel-core.cjs.js +888 -58
- package/dist/mixpanel-recorder.js +7 -3
- package/dist/mixpanel-recorder.min.js +2 -2
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +888 -58
- package/dist/mixpanel.amd.js +907 -77
- package/dist/mixpanel.cjs.js +907 -77
- package/dist/mixpanel.globals.js +888 -58
- package/dist/mixpanel.min.js +138 -122
- package/dist/mixpanel.module.js +907 -77
- package/dist/mixpanel.umd.js +907 -77
- package/package.json +1 -1
- package/src/autocapture/index.js +342 -0
- package/src/autocapture/utils.js +525 -0
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +8 -53
- package/src/mixpanel-persistence.js +6 -2
- package/src/utils.js +27 -1
- package/src/window.js +4 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// stateless utils
|
|
2
|
+
// mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
|
|
3
|
+
|
|
4
|
+
import { _, console_with_prefix, document } from '../utils'; // eslint-disable-line camelcase
|
|
5
|
+
import { window } from '../window';
|
|
6
|
+
|
|
7
|
+
var EV_CHANGE = 'change';
|
|
8
|
+
var EV_CLICK = 'click';
|
|
9
|
+
var EV_HASHCHANGE = 'hashchange';
|
|
10
|
+
var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
|
|
11
|
+
var EV_POPSTATE = 'popstate';
|
|
12
|
+
// TODO scrollend isn't available in Safari: document or polyfill?
|
|
13
|
+
var EV_SCROLLEND = 'scrollend';
|
|
14
|
+
var EV_SUBMIT = 'submit';
|
|
15
|
+
|
|
16
|
+
var CLICK_EVENT_PROPS = [
|
|
17
|
+
'clientX', 'clientY',
|
|
18
|
+
'offsetX', 'offsetY',
|
|
19
|
+
'pageX', 'pageY',
|
|
20
|
+
'screenX', 'screenY',
|
|
21
|
+
'x', 'y'
|
|
22
|
+
];
|
|
23
|
+
var OPT_IN_CLASSES = ['mp-include'];
|
|
24
|
+
var OPT_OUT_CLASSES = ['mp-no-track'];
|
|
25
|
+
var SENSITIVE_DATA_CLASSES = OPT_OUT_CLASSES.concat(['mp-sensitive']);
|
|
26
|
+
var TRACKED_ATTRS = [
|
|
27
|
+
'aria-label', 'aria-labelledby', 'aria-describedby',
|
|
28
|
+
'href', 'name', 'role', 'title', 'type'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
var logger = console_with_prefix('autocapture');
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function getClasses(el) {
|
|
35
|
+
var classes = {};
|
|
36
|
+
var classList = getClassName(el).split(' ');
|
|
37
|
+
for (var i = 0; i < classList.length; i++) {
|
|
38
|
+
var cls = classList[i];
|
|
39
|
+
if (cls) {
|
|
40
|
+
classes[cls] = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return classes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* Get the className of an element, accounting for edge cases where element.className is an object
|
|
48
|
+
* @param {Element} el - element to get the className of
|
|
49
|
+
* @returns {string} the element's class
|
|
50
|
+
*/
|
|
51
|
+
function getClassName(el) {
|
|
52
|
+
switch(typeof el.className) {
|
|
53
|
+
case 'string':
|
|
54
|
+
return el.className;
|
|
55
|
+
case 'object': // handle cases where className might be SVGAnimatedString or some other type
|
|
56
|
+
return el.className.baseVal || el.getAttribute('class') || '';
|
|
57
|
+
default: // future proof
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getPreviousElementSibling(el) {
|
|
63
|
+
if (el.previousElementSibling) {
|
|
64
|
+
return el.previousElementSibling;
|
|
65
|
+
} else {
|
|
66
|
+
do {
|
|
67
|
+
el = el.previousSibling;
|
|
68
|
+
} while (el && !isElementNode(el));
|
|
69
|
+
return el;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
|
|
74
|
+
var props = {
|
|
75
|
+
'$classes': getClassName(el).split(' '),
|
|
76
|
+
'$tag_name': el.tagName.toLowerCase()
|
|
77
|
+
};
|
|
78
|
+
var elId = el.id;
|
|
79
|
+
if (elId) {
|
|
80
|
+
props['$id'] = elId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
|
|
84
|
+
_.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
|
|
85
|
+
if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
|
|
86
|
+
var attrVal = el.getAttribute(attr);
|
|
87
|
+
if (shouldTrackValue(attrVal)) {
|
|
88
|
+
props['$attr-' + attr] = attrVal;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var nthChild = 1;
|
|
95
|
+
var nthOfType = 1;
|
|
96
|
+
var currentElem = el;
|
|
97
|
+
while (currentElem = getPreviousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
|
|
98
|
+
nthChild++;
|
|
99
|
+
if (currentElem.tagName === el.tagName) {
|
|
100
|
+
nthOfType++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
props['$nth_child'] = nthChild;
|
|
104
|
+
props['$nth_of_type'] = nthOfType;
|
|
105
|
+
|
|
106
|
+
return props;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getPropsForDOMEvent(ev, config) {
|
|
110
|
+
var allowElementCallback = config.allowElementCallback;
|
|
111
|
+
var allowSelectors = config.allowSelectors || [];
|
|
112
|
+
var blockAttrs = config.blockAttrs || [];
|
|
113
|
+
var blockElementCallback = config.blockElementCallback;
|
|
114
|
+
var blockSelectors = config.blockSelectors || [];
|
|
115
|
+
var captureTextContent = config.captureTextContent || false;
|
|
116
|
+
var captureExtraAttrs = config.captureExtraAttrs || [];
|
|
117
|
+
|
|
118
|
+
// convert array to set every time, as the config may have changed
|
|
119
|
+
var blockAttrsSet = {};
|
|
120
|
+
_.each(blockAttrs, function(attr) {
|
|
121
|
+
blockAttrsSet[attr] = true;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
var props = null;
|
|
125
|
+
|
|
126
|
+
var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
|
|
127
|
+
if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
|
|
128
|
+
target = target.parentNode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
shouldTrackDomEvent(target, ev) &&
|
|
133
|
+
isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
|
|
134
|
+
!isElementBlocked(target, ev, blockElementCallback, blockSelectors)
|
|
135
|
+
) {
|
|
136
|
+
var targetElementList = [target];
|
|
137
|
+
var curEl = target;
|
|
138
|
+
while (curEl.parentNode && !isTag(curEl, 'body')) {
|
|
139
|
+
targetElementList.push(curEl.parentNode);
|
|
140
|
+
curEl = curEl.parentNode;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var elementsJson = [];
|
|
144
|
+
var href, explicitNoTrack = false;
|
|
145
|
+
_.each(targetElementList, function(el) {
|
|
146
|
+
var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
|
|
147
|
+
|
|
148
|
+
// if the element or a parent element is an anchor tag
|
|
149
|
+
// include the href as a property
|
|
150
|
+
if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
|
|
151
|
+
href = el.getAttribute('href');
|
|
152
|
+
href = shouldTrackDetails && shouldTrackValue(href) && href;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
|
|
156
|
+
explicitNoTrack = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
|
|
160
|
+
}, this);
|
|
161
|
+
|
|
162
|
+
if (!explicitNoTrack) {
|
|
163
|
+
var docElement = document['documentElement'];
|
|
164
|
+
props = {
|
|
165
|
+
'$event_type': ev.type,
|
|
166
|
+
'$host': window.location.host,
|
|
167
|
+
'$pathname': window.location.pathname,
|
|
168
|
+
'$elements': elementsJson,
|
|
169
|
+
'$el_attr__href': href,
|
|
170
|
+
'$viewportHeight': Math.max(docElement['clientHeight'], window['innerHeight'] || 0),
|
|
171
|
+
'$viewportWidth': Math.max(docElement['clientWidth'], window['innerWidth'] || 0)
|
|
172
|
+
};
|
|
173
|
+
_.each(captureExtraAttrs, function(attr) {
|
|
174
|
+
if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
|
|
175
|
+
var attrVal = target.getAttribute(attr);
|
|
176
|
+
if (shouldTrackValue(attrVal)) {
|
|
177
|
+
props['$el_attr__' + attr] = attrVal;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (captureTextContent) {
|
|
183
|
+
elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
|
|
184
|
+
if (elementText && elementText.length) {
|
|
185
|
+
props['$el_text'] = elementText;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (ev.type === EV_CLICK) {
|
|
190
|
+
_.each(CLICK_EVENT_PROPS, function(prop) {
|
|
191
|
+
if (prop in ev) {
|
|
192
|
+
props['$' + prop] = ev[prop];
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
target = guessRealClickTarget(ev);
|
|
196
|
+
}
|
|
197
|
+
// prioritize text content from "real" click target if different from original target
|
|
198
|
+
if (captureTextContent) {
|
|
199
|
+
var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
|
|
200
|
+
if (elementText && elementText.length) {
|
|
201
|
+
props['$el_text'] = elementText;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (target) {
|
|
206
|
+
// target may have been recalculated; check allowlists and blocklists again
|
|
207
|
+
if (
|
|
208
|
+
!isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
|
|
209
|
+
isElementBlocked(target, ev, blockElementCallback, blockSelectors)
|
|
210
|
+
) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
|
|
215
|
+
props['$target'] = targetProps;
|
|
216
|
+
// pull up more props onto main event props
|
|
217
|
+
props['$el_classes'] = targetProps['$classes'];
|
|
218
|
+
_.extend(props, _.strip_empty_properties({
|
|
219
|
+
'$el_id': targetProps['$id'],
|
|
220
|
+
'$el_tag_name': targetProps['$tag_name']
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return props;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the direct text content of an element, protecting against sensitive data collection.
|
|
232
|
+
* Concats textContent of each of the element's text node children; this avoids potential
|
|
233
|
+
* collection of sensitive data that could happen if we used element.textContent and the
|
|
234
|
+
* element had sensitive child elements, since element.textContent includes child content.
|
|
235
|
+
* Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
|
|
236
|
+
* @param {Element} el - element to get the text of
|
|
237
|
+
* @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
|
|
238
|
+
* @returns {string} the element's direct text content
|
|
239
|
+
*/
|
|
240
|
+
function getSafeText(el, ev, allowElementCallback, allowSelectors) {
|
|
241
|
+
var elText = '';
|
|
242
|
+
|
|
243
|
+
if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
|
|
244
|
+
_.each(el.childNodes, function(child) {
|
|
245
|
+
if (isTextNode(child) && child.textContent) {
|
|
246
|
+
elText += _.trim(child.textContent)
|
|
247
|
+
// scrub potentially sensitive values
|
|
248
|
+
.split(/(\s+)/).filter(shouldTrackValue).join('')
|
|
249
|
+
// normalize whitespace
|
|
250
|
+
.replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
|
|
251
|
+
// truncate
|
|
252
|
+
.substring(0, 255);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return _.trim(elText);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function guessRealClickTarget(ev) {
|
|
261
|
+
var target = ev.target;
|
|
262
|
+
var composedPath = ev['composedPath']();
|
|
263
|
+
for (var i = 0; i < composedPath.length; i++) {
|
|
264
|
+
var node = composedPath[i];
|
|
265
|
+
if (
|
|
266
|
+
isTag(node, 'a') ||
|
|
267
|
+
isTag(node, 'button') ||
|
|
268
|
+
isTag(node, 'input') ||
|
|
269
|
+
isTag(node, 'select') ||
|
|
270
|
+
(node.getAttribute && node.getAttribute('role') === 'button')
|
|
271
|
+
) {
|
|
272
|
+
target = node;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (node === target) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return target;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
|
|
283
|
+
if (allowElementCallback) {
|
|
284
|
+
try {
|
|
285
|
+
if (!allowElementCallback(el, ev)) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
logger.critical('Error while checking element in allowElementCallback', err);
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!allowSelectors.length) {
|
|
295
|
+
// no allowlist; all elements are fair game
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (var i = 0; i < allowSelectors.length; i++) {
|
|
300
|
+
var sel = allowSelectors[i];
|
|
301
|
+
try {
|
|
302
|
+
if (el['matches'](sel)) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
logger.critical('Error while checking selector: ' + sel, err);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
|
|
313
|
+
var i;
|
|
314
|
+
|
|
315
|
+
if (blockElementCallback) {
|
|
316
|
+
try {
|
|
317
|
+
if (blockElementCallback(el, ev)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logger.critical('Error while checking element in blockElementCallback', err);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (blockSelectors && blockSelectors.length) {
|
|
327
|
+
// programmatically prevent tracking of elements that match CSS selectors
|
|
328
|
+
for (i = 0; i < blockSelectors.length; i++) {
|
|
329
|
+
var sel = blockSelectors[i];
|
|
330
|
+
try {
|
|
331
|
+
if (el['matches'](sel)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
logger.critical('Error while checking selector: ' + sel, err);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
|
|
341
|
+
var classes = getClasses(el);
|
|
342
|
+
for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
|
|
343
|
+
if (classes[OPT_OUT_CLASSES[i]]) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* Check whether a DOM node has nodeType Node.ELEMENT_NODE
|
|
353
|
+
* @param {Node} node - node to check
|
|
354
|
+
* @returns {boolean} whether node is of the correct nodeType
|
|
355
|
+
*/
|
|
356
|
+
function isElementNode(node) {
|
|
357
|
+
return node && node.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/*
|
|
361
|
+
* Check whether an element is of a given tag type.
|
|
362
|
+
* Due to potential reference discrepancies (such as the webcomponents.js polyfill),
|
|
363
|
+
* we want to match tagNames instead of specific references because something like
|
|
364
|
+
* element === document.body won't always work because element might not be a native
|
|
365
|
+
* element.
|
|
366
|
+
* @param {Element} el - element to check
|
|
367
|
+
* @param {string} tag - tag name (e.g., "div")
|
|
368
|
+
* @returns {boolean} whether el is of the given tag type
|
|
369
|
+
*/
|
|
370
|
+
function isTag(el, tag) {
|
|
371
|
+
return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/*
|
|
375
|
+
* Check whether a DOM node is a TEXT_NODE
|
|
376
|
+
* @param {Node} node - node to check
|
|
377
|
+
* @returns {boolean} whether node is of type Node.TEXT_NODE
|
|
378
|
+
*/
|
|
379
|
+
function isTextNode(node) {
|
|
380
|
+
return node && node.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function minDOMApisSupported() {
|
|
384
|
+
try {
|
|
385
|
+
var testEl = document.createElement('div');
|
|
386
|
+
return !!testEl['matches'];
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/*
|
|
393
|
+
* Check whether a DOM event should be "tracked" or if it may contain sensitive data
|
|
394
|
+
* using a variety of heuristics.
|
|
395
|
+
* @param {Element} el - element to check
|
|
396
|
+
* @param {Event} ev - event to check
|
|
397
|
+
* @returns {boolean} whether the event should be tracked
|
|
398
|
+
*/
|
|
399
|
+
function shouldTrackDomEvent(el, ev) {
|
|
400
|
+
if (!el || isTag(el, 'html') || !isElementNode(el)) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
var tag = el.tagName.toLowerCase();
|
|
404
|
+
switch (tag) {
|
|
405
|
+
case 'form':
|
|
406
|
+
return ev.type === EV_SUBMIT;
|
|
407
|
+
case 'input':
|
|
408
|
+
if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
|
|
409
|
+
return ev.type === EV_CHANGE;
|
|
410
|
+
} else {
|
|
411
|
+
return ev.type === EV_CLICK;
|
|
412
|
+
}
|
|
413
|
+
case 'select':
|
|
414
|
+
case 'textarea':
|
|
415
|
+
return ev.type === EV_CHANGE;
|
|
416
|
+
default:
|
|
417
|
+
return ev.type === EV_CLICK;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/*
|
|
422
|
+
* Check whether a DOM element should be "tracked" or if it may contain sensitive data
|
|
423
|
+
* using a variety of heuristics.
|
|
424
|
+
* @param {Element} el - element to check
|
|
425
|
+
* @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
|
|
426
|
+
* @returns {boolean} whether the element should be tracked
|
|
427
|
+
*/
|
|
428
|
+
function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
|
|
429
|
+
var i;
|
|
430
|
+
|
|
431
|
+
if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
|
|
436
|
+
var classes = getClasses(curEl);
|
|
437
|
+
for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
|
|
438
|
+
if (classes[SENSITIVE_DATA_CLASSES[i]]) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
var elClasses = getClasses(el);
|
|
445
|
+
for (i = 0; i < OPT_IN_CLASSES.length; i++) {
|
|
446
|
+
if (elClasses[OPT_IN_CLASSES[i]]) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// don't send data from inputs or similar elements since there will always be
|
|
452
|
+
// a risk of clientside javascript placing sensitive data in attributes
|
|
453
|
+
if (
|
|
454
|
+
isTag(el, 'input') ||
|
|
455
|
+
isTag(el, 'select') ||
|
|
456
|
+
isTag(el, 'textarea') ||
|
|
457
|
+
el.getAttribute('contenteditable') === 'true'
|
|
458
|
+
) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// don't include hidden or password fields
|
|
463
|
+
var type = el.type || '';
|
|
464
|
+
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"]
|
|
465
|
+
switch(type.toLowerCase()) {
|
|
466
|
+
case 'hidden':
|
|
467
|
+
return false;
|
|
468
|
+
case 'password':
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// filter out data from fields that look like sensitive fields
|
|
474
|
+
var name = el.name || el.id || '';
|
|
475
|
+
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"]
|
|
476
|
+
var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
|
|
477
|
+
if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
/*
|
|
487
|
+
* Check whether a string value should be "tracked" or if it may contain sensitive data
|
|
488
|
+
* using a variety of heuristics.
|
|
489
|
+
* @param {string} value - string value to check
|
|
490
|
+
* @returns {boolean} whether the element should be tracked
|
|
491
|
+
*/
|
|
492
|
+
function shouldTrackValue(value) {
|
|
493
|
+
if (value === null || _.isUndefined(value)) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (typeof value === 'string') {
|
|
498
|
+
value = _.trim(value);
|
|
499
|
+
|
|
500
|
+
// check to see if input value looks like a credit card number
|
|
501
|
+
// see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
|
|
502
|
+
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}))$/;
|
|
503
|
+
if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// check to see if input value looks like a social security number
|
|
508
|
+
var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
|
|
509
|
+
if (ssnRegex.test(value)) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export {
|
|
518
|
+
getPropsForDOMEvent,
|
|
519
|
+
getSafeText,
|
|
520
|
+
logger,
|
|
521
|
+
minDOMApisSupported,
|
|
522
|
+
shouldTrackDomEvent, shouldTrackElementDetails, shouldTrackValue,
|
|
523
|
+
EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_MP_LOCATION_CHANGE, EV_POPSTATE,
|
|
524
|
+
EV_SCROLLEND, EV_SUBMIT
|
|
525
|
+
};
|
package/src/config.js
CHANGED
package/src/mixpanel-core.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Config from './config';
|
|
3
3
|
import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice } from './utils';
|
|
4
4
|
import { window } from './window';
|
|
5
|
+
import { Autocapture } from './autocapture';
|
|
5
6
|
import { FormTracker, LinkTracker } from './dom-trackers';
|
|
6
7
|
import { RequestBatcher } from './request-batcher';
|
|
7
8
|
import { MixpanelGroup } from './mixpanel-group';
|
|
@@ -105,6 +106,7 @@ var DEFAULT_CONFIG = {
|
|
|
105
106
|
'api_transport': 'XHR',
|
|
106
107
|
'api_payload_format': PAYLOAD_TYPE_BASE64,
|
|
107
108
|
'app_host': 'https://mixpanel.com',
|
|
109
|
+
'autocapture': false,
|
|
108
110
|
'cdn': 'https://cdn.mxpnl.com',
|
|
109
111
|
'cross_site_cookie': false,
|
|
110
112
|
'cross_subdomain_cookie': true,
|
|
@@ -364,10 +366,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
364
366
|
}, '');
|
|
365
367
|
}
|
|
366
368
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this._init_url_change_tracking(track_pageview_option);
|
|
370
|
-
}
|
|
369
|
+
this.autocapture = new Autocapture(this);
|
|
370
|
+
this.autocapture.init();
|
|
371
371
|
|
|
372
372
|
if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
|
|
373
373
|
this.start_session_recording();
|
|
@@ -492,55 +492,6 @@ MixpanelLib.prototype._track_dom = function(DomClass, args) {
|
|
|
492
492
|
return dt.track.apply(dt, args);
|
|
493
493
|
};
|
|
494
494
|
|
|
495
|
-
MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) {
|
|
496
|
-
var previous_tracked_url = '';
|
|
497
|
-
var tracked = this.track_pageview();
|
|
498
|
-
if (tracked) {
|
|
499
|
-
previous_tracked_url = _.info.currentUrl();
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) {
|
|
503
|
-
window.addEventListener('popstate', function() {
|
|
504
|
-
window.dispatchEvent(new Event('mp_locationchange'));
|
|
505
|
-
});
|
|
506
|
-
window.addEventListener('hashchange', function() {
|
|
507
|
-
window.dispatchEvent(new Event('mp_locationchange'));
|
|
508
|
-
});
|
|
509
|
-
var nativePushState = window.history.pushState;
|
|
510
|
-
if (typeof nativePushState === 'function') {
|
|
511
|
-
window.history.pushState = function(state, unused, url) {
|
|
512
|
-
nativePushState.call(window.history, state, unused, url);
|
|
513
|
-
window.dispatchEvent(new Event('mp_locationchange'));
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
var nativeReplaceState = window.history.replaceState;
|
|
517
|
-
if (typeof nativeReplaceState === 'function') {
|
|
518
|
-
window.history.replaceState = function(state, unused, url) {
|
|
519
|
-
nativeReplaceState.call(window.history, state, unused, url);
|
|
520
|
-
window.dispatchEvent(new Event('mp_locationchange'));
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
window.addEventListener('mp_locationchange', function() {
|
|
524
|
-
var current_url = _.info.currentUrl();
|
|
525
|
-
var should_track = false;
|
|
526
|
-
if (track_pageview_option === 'full-url') {
|
|
527
|
-
should_track = current_url !== previous_tracked_url;
|
|
528
|
-
} else if (track_pageview_option === 'url-with-path-and-query-string') {
|
|
529
|
-
should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0];
|
|
530
|
-
} else if (track_pageview_option === 'url-with-path') {
|
|
531
|
-
should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (should_track) {
|
|
535
|
-
var tracked = this.track_pageview();
|
|
536
|
-
if (tracked) {
|
|
537
|
-
previous_tracked_url = current_url;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}.bind(this));
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
|
|
544
495
|
/**
|
|
545
496
|
* _prepare_callback() should be called by callers of _send_request for use
|
|
546
497
|
* as the callback argument.
|
|
@@ -1798,6 +1749,10 @@ MixpanelLib.prototype.set_config = function(config) {
|
|
|
1798
1749
|
this['persistence'].update_config(this['config']);
|
|
1799
1750
|
}
|
|
1800
1751
|
Config.DEBUG = Config.DEBUG || this.get_config('debug');
|
|
1752
|
+
|
|
1753
|
+
if ('autocapture' in config && this.autocapture) {
|
|
1754
|
+
this.autocapture.init();
|
|
1755
|
+
}
|
|
1801
1756
|
}
|
|
1802
1757
|
};
|
|
1803
1758
|
|
|
@@ -345,8 +345,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
|
|
|
345
345
|
if (!(k in union_q)) {
|
|
346
346
|
union_q[k] = [];
|
|
347
347
|
}
|
|
348
|
-
//
|
|
349
|
-
|
|
348
|
+
// Prevent duplicate values
|
|
349
|
+
_.each(v, function(item) {
|
|
350
|
+
if (!_.include(union_q[k], item)) {
|
|
351
|
+
union_q[k].push(item);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
350
354
|
}
|
|
351
355
|
});
|
|
352
356
|
this._pop_from_people_queue(UNSET_ACTION, q_data);
|
package/src/utils.js
CHANGED
|
@@ -110,6 +110,29 @@ var console_with_prefix = function(prefix) {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
var safewrap = function(f) {
|
|
114
|
+
return function() {
|
|
115
|
+
try {
|
|
116
|
+
return f.apply(this, arguments);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
|
|
119
|
+
if (Config.DEBUG){
|
|
120
|
+
console.critical(e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
var safewrapClass = function(klass) {
|
|
127
|
+
var proto = klass.prototype;
|
|
128
|
+
for (var func in proto) {
|
|
129
|
+
if (typeof(proto[func]) === 'function') {
|
|
130
|
+
proto[func] = safewrap(proto[func]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
|
|
113
136
|
// UNDERSCORE
|
|
114
137
|
// Embed part of the Underscore Library
|
|
115
138
|
_.bind = function(func, context) {
|
|
@@ -888,6 +911,7 @@ _.UUID = (function() {
|
|
|
888
911
|
var BLOCKED_UA_STRS = [
|
|
889
912
|
'ahrefsbot',
|
|
890
913
|
'ahrefssiteaudit',
|
|
914
|
+
'amazonbot',
|
|
891
915
|
'baiduspider',
|
|
892
916
|
'bingbot',
|
|
893
917
|
'bingpreview',
|
|
@@ -897,7 +921,7 @@ var BLOCKED_UA_STRS = [
|
|
|
897
921
|
'pinterest',
|
|
898
922
|
'screaming frog',
|
|
899
923
|
'yahoo! slurp',
|
|
900
|
-
'
|
|
924
|
+
'yandex',
|
|
901
925
|
|
|
902
926
|
// a whole bunch of goog-specific crawlers
|
|
903
927
|
// https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
|
|
@@ -1732,6 +1756,8 @@ export {
|
|
|
1732
1756
|
MAX_RECORDING_MS,
|
|
1733
1757
|
MAX_VALUE_FOR_MIN_RECORDING_MS,
|
|
1734
1758
|
navigator,
|
|
1759
|
+
safewrap,
|
|
1760
|
+
safewrapClass,
|
|
1735
1761
|
slice,
|
|
1736
1762
|
userAgent,
|
|
1737
1763
|
};
|