mixpanel-browser 2.59.0 → 2.61.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/CHANGELOG.md +5 -1
- package/README.md +3 -3
- package/dist/mixpanel-core.cjs.js +612 -176
- package/dist/mixpanel-recorder.js +670 -224
- package/dist/mixpanel-recorder.min.js +11 -11
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +612 -176
- package/dist/mixpanel.amd.js +1000 -290
- package/dist/mixpanel.cjs.js +1000 -290
- package/dist/mixpanel.globals.js +612 -176
- package/dist/mixpanel.min.js +143 -134
- package/dist/mixpanel.module.js +1000 -290
- package/dist/mixpanel.umd.js +1000 -290
- package/package.json +2 -1
- package/src/autocapture/index.js +80 -9
- package/src/autocapture/utils.js +129 -38
- package/src/config.js +1 -1
- package/src/mixpanel-core.js +119 -19
- package/src/mixpanel-persistence.js +6 -2
- package/src/recorder/index.js +1 -70
- package/src/recorder/recorder.js +137 -0
- package/src/recorder/recording-registry.js +98 -0
- package/src/recorder/session-recording.js +162 -43
- package/src/recorder/utils.js +12 -0
- package/src/request-batcher.js +6 -2
- package/src/request-queue.js +45 -39
- package/src/shared-lock.js +1 -1
- package/src/storage/indexed-db.js +127 -0
- package/src/storage/local-storage.js +4 -8
- package/src/storage/wrapper.js +3 -3
- package/src/utils.js +99 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixpanel-browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.61.0",
|
|
4
4
|
"description": "The official Mixpanel JavaScript browser client library",
|
|
5
5
|
"main": "dist/mixpanel.cjs.js",
|
|
6
6
|
"module": "dist/mixpanel.module.js",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"dox": "0.9.0",
|
|
46
46
|
"eslint": "4.18.2",
|
|
47
47
|
"express": "4.12.2",
|
|
48
|
+
"fake-indexeddb": "6.0.0",
|
|
48
49
|
"jsdom": "16.5.0",
|
|
49
50
|
"jsdom-global": "3.0.2",
|
|
50
51
|
"localStorage": "1.0.4",
|
package/src/autocapture/index.js
CHANGED
|
@@ -13,9 +13,17 @@ var PAGEVIEW_OPTION_FULL_URL = 'full-url';
|
|
|
13
13
|
var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
|
|
14
14
|
var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
|
|
15
15
|
|
|
16
|
+
var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
|
|
17
|
+
var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
|
|
18
|
+
var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
|
|
19
|
+
var CONFIG_BLOCK_ATTRS = 'block_attrs';
|
|
20
|
+
var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
|
|
16
21
|
var CONFIG_BLOCK_SELECTORS = 'block_selectors';
|
|
17
22
|
var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
|
|
23
|
+
var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
|
|
18
24
|
var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
|
|
25
|
+
var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
|
|
26
|
+
var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
|
|
19
27
|
var CONFIG_TRACK_CLICK = 'click';
|
|
20
28
|
var CONFIG_TRACK_INPUT = 'input';
|
|
21
29
|
var CONFIG_TRACK_PAGEVIEW = 'pageview';
|
|
@@ -23,7 +31,16 @@ var CONFIG_TRACK_SCROLL = 'scroll';
|
|
|
23
31
|
var CONFIG_TRACK_SUBMIT = 'submit';
|
|
24
32
|
|
|
25
33
|
var CONFIG_DEFAULTS = {};
|
|
34
|
+
CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
|
|
35
|
+
CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
|
|
36
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
|
|
37
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
|
|
38
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
|
|
39
|
+
CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
|
|
40
|
+
CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
|
|
26
41
|
CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
|
|
42
|
+
CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
|
|
43
|
+
CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
|
|
27
44
|
CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
|
|
28
45
|
CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
|
|
29
46
|
CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
|
|
@@ -78,13 +95,37 @@ Autocapture.prototype.getConfig = function(key) {
|
|
|
78
95
|
};
|
|
79
96
|
|
|
80
97
|
Autocapture.prototype.currentUrlBlocked = function() {
|
|
98
|
+
var i;
|
|
99
|
+
var currentUrl = _.info.currentUrl();
|
|
100
|
+
|
|
101
|
+
var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
|
|
102
|
+
if (allowUrlRegexes.length) {
|
|
103
|
+
// we're using an allowlist, only track if current URL matches
|
|
104
|
+
var allowed = false;
|
|
105
|
+
for (i = 0; i < allowUrlRegexes.length; i++) {
|
|
106
|
+
var allowRegex = allowUrlRegexes[i];
|
|
107
|
+
try {
|
|
108
|
+
if (currentUrl.match(allowRegex)) {
|
|
109
|
+
allowed = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger.critical('Error while checking block URL regex: ' + allowRegex, err);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!allowed) {
|
|
118
|
+
// wasn't allowed by any regex
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
81
123
|
var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
|
|
82
124
|
if (!blockUrlRegexes || !blockUrlRegexes.length) {
|
|
83
125
|
return false;
|
|
84
126
|
}
|
|
85
127
|
|
|
86
|
-
|
|
87
|
-
for (var i = 0; i < blockUrlRegexes.length; i++) {
|
|
128
|
+
for (i = 0; i < blockUrlRegexes.length; i++) {
|
|
88
129
|
try {
|
|
89
130
|
if (currentUrl.match(blockUrlRegexes[i])) {
|
|
90
131
|
return true;
|
|
@@ -112,11 +153,15 @@ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
|
|
|
112
153
|
return;
|
|
113
154
|
}
|
|
114
155
|
|
|
115
|
-
var props = getPropsForDOMEvent(
|
|
116
|
-
|
|
117
|
-
this.getConfig(
|
|
118
|
-
this.getConfig(
|
|
119
|
-
|
|
156
|
+
var props = getPropsForDOMEvent(ev, {
|
|
157
|
+
allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
|
|
158
|
+
allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
|
|
159
|
+
blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
|
|
160
|
+
blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
|
|
161
|
+
blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
|
|
162
|
+
captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
|
|
163
|
+
captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
|
|
164
|
+
});
|
|
120
165
|
if (props) {
|
|
121
166
|
_.extend(props, DEFAULT_PROPS);
|
|
122
167
|
this.mp.track(mpEventName, props);
|
|
@@ -201,13 +246,14 @@ Autocapture.prototype.initPageviewTracking = function() {
|
|
|
201
246
|
|
|
202
247
|
var currentUrl = _.info.currentUrl();
|
|
203
248
|
var shouldTrack = false;
|
|
249
|
+
var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
|
|
204
250
|
var trackPageviewOption = this.pageviewTrackingConfig();
|
|
205
251
|
if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
|
|
206
252
|
shouldTrack = currentUrl !== previousTrackedUrl;
|
|
207
253
|
} else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
|
|
208
254
|
shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
|
|
209
255
|
} else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
|
|
210
|
-
shouldTrack =
|
|
256
|
+
shouldTrack = didPathChange;
|
|
211
257
|
}
|
|
212
258
|
|
|
213
259
|
if (shouldTrack) {
|
|
@@ -215,6 +261,10 @@ Autocapture.prototype.initPageviewTracking = function() {
|
|
|
215
261
|
if (tracked) {
|
|
216
262
|
previousTrackedUrl = currentUrl;
|
|
217
263
|
}
|
|
264
|
+
if (didPathChange) {
|
|
265
|
+
this.lastScrollCheckpoint = 0;
|
|
266
|
+
logger.log('Path change: re-initializing scroll depth checkpoints');
|
|
267
|
+
}
|
|
218
268
|
}
|
|
219
269
|
}.bind(this)));
|
|
220
270
|
};
|
|
@@ -226,6 +276,7 @@ Autocapture.prototype.initScrollTracking = function() {
|
|
|
226
276
|
return;
|
|
227
277
|
}
|
|
228
278
|
logger.log('Initializing scroll tracking');
|
|
279
|
+
this.lastScrollCheckpoint = 0;
|
|
229
280
|
|
|
230
281
|
this.listenerScroll = window.addEventListener(EV_SCROLLEND, safewrap(function() {
|
|
231
282
|
if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
|
|
@@ -235,6 +286,11 @@ Autocapture.prototype.initScrollTracking = function() {
|
|
|
235
286
|
return;
|
|
236
287
|
}
|
|
237
288
|
|
|
289
|
+
var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
|
|
290
|
+
var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
|
|
291
|
+
.slice()
|
|
292
|
+
.sort(function(a, b) { return a - b; });
|
|
293
|
+
|
|
238
294
|
var scrollTop = window.scrollY;
|
|
239
295
|
var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
|
|
240
296
|
try {
|
|
@@ -242,10 +298,25 @@ Autocapture.prototype.initScrollTracking = function() {
|
|
|
242
298
|
var scrollPercentage = Math.round((scrollTop / (scrollHeight - window.innerHeight)) * 100);
|
|
243
299
|
props['$scroll_height'] = scrollHeight;
|
|
244
300
|
props['$scroll_percentage'] = scrollPercentage;
|
|
301
|
+
if (scrollPercentage > this.lastScrollCheckpoint) {
|
|
302
|
+
for (var i = 0; i < scrollCheckpoints.length; i++) {
|
|
303
|
+
var checkpoint = scrollCheckpoints[i];
|
|
304
|
+
if (
|
|
305
|
+
scrollPercentage >= checkpoint &&
|
|
306
|
+
this.lastScrollCheckpoint < checkpoint
|
|
307
|
+
) {
|
|
308
|
+
props['$scroll_checkpoint'] = checkpoint;
|
|
309
|
+
this.lastScrollCheckpoint = checkpoint;
|
|
310
|
+
shouldTrack = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
245
314
|
} catch (err) {
|
|
246
315
|
logger.critical('Error while calculating scroll percentage', err);
|
|
247
316
|
}
|
|
248
|
-
|
|
317
|
+
if (shouldTrack) {
|
|
318
|
+
this.mp.track(MP_EV_SCROLL, props);
|
|
319
|
+
}
|
|
249
320
|
}.bind(this)));
|
|
250
321
|
};
|
|
251
322
|
|
package/src/autocapture/utils.js
CHANGED
|
@@ -70,7 +70,7 @@ function getPreviousElementSibling(el) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function getPropertiesFromElement(el) {
|
|
73
|
+
function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
|
|
74
74
|
var props = {
|
|
75
75
|
'$classes': getClassName(el).split(' '),
|
|
76
76
|
'$tag_name': el.tagName.toLowerCase()
|
|
@@ -80,9 +80,9 @@ function getPropertiesFromElement(el) {
|
|
|
80
80
|
props['$id'] = elId;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
if (
|
|
84
|
-
_.each(TRACKED_ATTRS, function(attr) {
|
|
85
|
-
if (el.hasAttribute(attr)) {
|
|
83
|
+
if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
|
|
84
|
+
_.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
|
|
85
|
+
if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
|
|
86
86
|
var attrVal = el.getAttribute(attr);
|
|
87
87
|
if (shouldTrackValue(attrVal)) {
|
|
88
88
|
props['$attr-' + attr] = attrVal;
|
|
@@ -106,8 +106,21 @@ function getPropertiesFromElement(el) {
|
|
|
106
106
|
return props;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function getPropsForDOMEvent(ev,
|
|
110
|
-
|
|
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
|
+
|
|
111
124
|
var props = null;
|
|
112
125
|
|
|
113
126
|
var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
|
|
@@ -115,7 +128,11 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
115
128
|
target = target.parentNode;
|
|
116
129
|
}
|
|
117
130
|
|
|
118
|
-
if (
|
|
131
|
+
if (
|
|
132
|
+
shouldTrackDomEvent(target, ev) &&
|
|
133
|
+
isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
|
|
134
|
+
!isElementBlocked(target, ev, blockElementCallback, blockSelectors)
|
|
135
|
+
) {
|
|
119
136
|
var targetElementList = [target];
|
|
120
137
|
var curEl = target;
|
|
121
138
|
while (curEl.parentNode && !isTag(curEl, 'body')) {
|
|
@@ -126,37 +143,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
126
143
|
var elementsJson = [];
|
|
127
144
|
var href, explicitNoTrack = false;
|
|
128
145
|
_.each(targetElementList, function(el) {
|
|
129
|
-
var
|
|
146
|
+
var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
|
|
130
147
|
|
|
131
148
|
// if the element or a parent element is an anchor tag
|
|
132
149
|
// include the href as a property
|
|
133
|
-
if (el.tagName.toLowerCase() === 'a') {
|
|
150
|
+
if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
|
|
134
151
|
href = el.getAttribute('href');
|
|
135
|
-
href =
|
|
152
|
+
href = shouldTrackDetails && shouldTrackValue(href) && href;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
_.each(OPT_OUT_CLASSES, function(cls) {
|
|
141
|
-
if (classes[cls]) {
|
|
142
|
-
explicitNoTrack = true;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (!explicitNoTrack) {
|
|
147
|
-
// programmatically prevent tracking of elements that match CSS selectors
|
|
148
|
-
_.each(blockSelectors, function(sel) {
|
|
149
|
-
try {
|
|
150
|
-
if (el['matches'](sel)) {
|
|
151
|
-
explicitNoTrack = true;
|
|
152
|
-
}
|
|
153
|
-
} catch (err) {
|
|
154
|
-
logger.critical('Error while checking selector: ' + sel, err);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
155
|
+
if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
|
|
156
|
+
explicitNoTrack = true;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
elementsJson.push(getPropertiesFromElement(el));
|
|
159
|
+
elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
|
|
160
160
|
}, this);
|
|
161
161
|
|
|
162
162
|
if (!explicitNoTrack) {
|
|
@@ -170,9 +170,17 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
170
170
|
'$viewportHeight': Math.max(docElement['clientHeight'], window['innerHeight'] || 0),
|
|
171
171
|
'$viewportWidth': Math.max(docElement['clientWidth'], window['innerWidth'] || 0)
|
|
172
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
|
+
});
|
|
173
181
|
|
|
174
182
|
if (captureTextContent) {
|
|
175
|
-
elementText = getSafeText(target);
|
|
183
|
+
elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
|
|
176
184
|
if (elementText && elementText.length) {
|
|
177
185
|
props['$el_text'] = elementText;
|
|
178
186
|
}
|
|
@@ -188,14 +196,22 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
188
196
|
}
|
|
189
197
|
// prioritize text content from "real" click target if different from original target
|
|
190
198
|
if (captureTextContent) {
|
|
191
|
-
var elementText = getSafeText(target);
|
|
199
|
+
var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
|
|
192
200
|
if (elementText && elementText.length) {
|
|
193
201
|
props['$el_text'] = elementText;
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
if (target) {
|
|
198
|
-
|
|
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);
|
|
199
215
|
props['$target'] = targetProps;
|
|
200
216
|
// pull up more props onto main event props
|
|
201
217
|
props['$el_classes'] = targetProps['$classes'];
|
|
@@ -211,19 +227,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
|
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
|
|
214
|
-
|
|
230
|
+
/**
|
|
215
231
|
* Get the direct text content of an element, protecting against sensitive data collection.
|
|
216
232
|
* Concats textContent of each of the element's text node children; this avoids potential
|
|
217
233
|
* collection of sensitive data that could happen if we used element.textContent and the
|
|
218
234
|
* element had sensitive child elements, since element.textContent includes child content.
|
|
219
235
|
* Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
|
|
220
236
|
* @param {Element} el - element to get the text of
|
|
237
|
+
* @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
|
|
221
238
|
* @returns {string} the element's direct text content
|
|
222
239
|
*/
|
|
223
|
-
function getSafeText(el) {
|
|
240
|
+
function getSafeText(el, ev, allowElementCallback, allowSelectors) {
|
|
224
241
|
var elText = '';
|
|
225
242
|
|
|
226
|
-
if (
|
|
243
|
+
if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
|
|
227
244
|
_.each(el.childNodes, function(child) {
|
|
228
245
|
if (isTextNode(child) && child.textContent) {
|
|
229
246
|
elText += _.trim(child.textContent)
|
|
@@ -262,6 +279,75 @@ function guessRealClickTarget(ev) {
|
|
|
262
279
|
return target;
|
|
263
280
|
}
|
|
264
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
|
+
|
|
265
351
|
/*
|
|
266
352
|
* Check whether a DOM node has nodeType Node.ELEMENT_NODE
|
|
267
353
|
* @param {Node} node - node to check
|
|
@@ -336,11 +422,16 @@ function shouldTrackDomEvent(el, ev) {
|
|
|
336
422
|
* Check whether a DOM element should be "tracked" or if it may contain sensitive data
|
|
337
423
|
* using a variety of heuristics.
|
|
338
424
|
* @param {Element} el - element to check
|
|
425
|
+
* @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
|
|
339
426
|
* @returns {boolean} whether the element should be tracked
|
|
340
427
|
*/
|
|
341
|
-
function
|
|
428
|
+
function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
|
|
342
429
|
var i;
|
|
343
430
|
|
|
431
|
+
if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
344
435
|
for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
|
|
345
436
|
var classes = getClasses(curEl);
|
|
346
437
|
for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
|
|
@@ -428,7 +519,7 @@ export {
|
|
|
428
519
|
getSafeText,
|
|
429
520
|
logger,
|
|
430
521
|
minDOMApisSupported,
|
|
431
|
-
shouldTrackDomEvent,
|
|
522
|
+
shouldTrackDomEvent, shouldTrackElementDetails, shouldTrackValue,
|
|
432
523
|
EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_MP_LOCATION_CHANGE, EV_POPSTATE,
|
|
433
524
|
EV_SCROLLEND, EV_SUBMIT
|
|
434
525
|
};
|
package/src/config.js
CHANGED
package/src/mixpanel-core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint camelcase: "off" */
|
|
2
2
|
import Config from './config';
|
|
3
|
-
import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice } from './utils';
|
|
3
|
+
import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice, NOOP_FUNC } from './utils';
|
|
4
|
+
import { isRecordingExpired } from './recorder/utils';
|
|
4
5
|
import { window } from './window';
|
|
5
6
|
import { Autocapture } from './autocapture';
|
|
6
7
|
import { FormTracker, LinkTracker } from './dom-trackers';
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
clearOptInOut,
|
|
21
22
|
addOptOutCheckMixpanelLib
|
|
22
23
|
} from './gdpr-utils';
|
|
24
|
+
import { IDBStorageWrapper, RECORDING_REGISTRY_STORE_NAME } from './storage/indexed-db';
|
|
23
25
|
|
|
24
26
|
/*
|
|
25
27
|
* Mixpanel JS Library
|
|
@@ -33,11 +35,6 @@ import {
|
|
|
33
35
|
* Released under the MIT License.
|
|
34
36
|
*/
|
|
35
37
|
|
|
36
|
-
// ==ClosureCompiler==
|
|
37
|
-
// @compilation_level ADVANCED_OPTIMIZATIONS
|
|
38
|
-
// @output_file_name mixpanel-2.8.min.js
|
|
39
|
-
// ==/ClosureCompiler==
|
|
40
|
-
|
|
41
38
|
/*
|
|
42
39
|
SIMPLE STYLE GUIDE:
|
|
43
40
|
|
|
@@ -60,7 +57,6 @@ var INIT_MODULE = 0;
|
|
|
60
57
|
var INIT_SNIPPET = 1;
|
|
61
58
|
|
|
62
59
|
var IDENTITY_FUNC = function(x) {return x;};
|
|
63
|
-
var NOOP_FUNC = function() {};
|
|
64
60
|
|
|
65
61
|
/** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel';
|
|
66
62
|
/** @const */ var PAYLOAD_TYPE_BASE64 = 'base64';
|
|
@@ -369,34 +365,125 @@ MixpanelLib.prototype._init = function(token, config, name) {
|
|
|
369
365
|
this.autocapture = new Autocapture(this);
|
|
370
366
|
this.autocapture.init();
|
|
371
367
|
|
|
372
|
-
|
|
373
|
-
|
|
368
|
+
this._init_tab_id();
|
|
369
|
+
this._check_and_start_session_recording();
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Assigns a unique UUID to this tab / window by leveraging sessionStorage.
|
|
374
|
+
* This is primarily used for session recording, where data must be isolated to the current tab.
|
|
375
|
+
*/
|
|
376
|
+
MixpanelLib.prototype._init_tab_id = function() {
|
|
377
|
+
if (_.sessionStorage.is_supported()) {
|
|
378
|
+
try {
|
|
379
|
+
var key_suffix = this.get_config('name') + '_' + this.get_config('token');
|
|
380
|
+
var tab_id_key = 'mp_tab_id_' + key_suffix;
|
|
381
|
+
|
|
382
|
+
// A flag is used to determine if sessionStorage is copied over and we need to generate a new tab ID.
|
|
383
|
+
// This enforces a unique ID in the cases like duplicated tab, window.open(...)
|
|
384
|
+
var should_generate_new_tab_id_key = 'mp_gen_new_tab_id_' + key_suffix;
|
|
385
|
+
if (_.sessionStorage.get(should_generate_new_tab_id_key) || !_.sessionStorage.get(tab_id_key)) {
|
|
386
|
+
_.sessionStorage.set(tab_id_key, '$tab-' + _.UUID());
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_.sessionStorage.set(should_generate_new_tab_id_key, '1');
|
|
390
|
+
this.tab_id = _.sessionStorage.get(tab_id_key);
|
|
391
|
+
|
|
392
|
+
// Remove the flag when the tab is unloaded to indicate the stored tab ID can be reused. This event is not reliable to detect all page unloads,
|
|
393
|
+
// but reliable in cases where the user remains in the tab e.g. a refresh or href navigation.
|
|
394
|
+
// If the flag is absent, this indicates to the next SDK instance that we can reuse the stored tab_id.
|
|
395
|
+
window.addEventListener('beforeunload', function () {
|
|
396
|
+
_.sessionStorage.remove(should_generate_new_tab_id_key);
|
|
397
|
+
});
|
|
398
|
+
} catch(err) {
|
|
399
|
+
this.report_error('Error initializing tab id', err);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
this.report_error('Session storage is not supported, cannot keep track of unique tab ID.');
|
|
374
403
|
}
|
|
375
404
|
};
|
|
376
405
|
|
|
377
|
-
MixpanelLib.prototype.
|
|
406
|
+
MixpanelLib.prototype.get_tab_id = function () {
|
|
407
|
+
return this.tab_id || null;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
MixpanelLib.prototype._should_load_recorder = function () {
|
|
411
|
+
var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
|
|
412
|
+
var tab_id = this.get_tab_id();
|
|
413
|
+
return recording_registry_idb.init()
|
|
414
|
+
.then(function () {
|
|
415
|
+
return recording_registry_idb.getAll();
|
|
416
|
+
})
|
|
417
|
+
.then(function (recordings) {
|
|
418
|
+
for (var i = 0; i < recordings.length; i++) {
|
|
419
|
+
// if there are expired recordings in the registry, we should load the recorder to flush them
|
|
420
|
+
// if there's a recording for this tab id, we should load the recorder to continue the recording
|
|
421
|
+
if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
})
|
|
427
|
+
.catch(_.bind(function (err) {
|
|
428
|
+
this.report_error('Error checking recording registry', err);
|
|
429
|
+
}, this));
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
|
|
378
433
|
if (!window['MutationObserver']) {
|
|
379
434
|
console.critical('Browser does not support MutationObserver; skipping session recording');
|
|
380
435
|
return;
|
|
381
436
|
}
|
|
382
437
|
|
|
383
|
-
var
|
|
384
|
-
|
|
385
|
-
|
|
438
|
+
var loadRecorder = _.bind(function(startNewIfInactive) {
|
|
439
|
+
var handleLoadedRecorder = _.bind(function() {
|
|
440
|
+
this._recorder = this._recorder || new window['__mp_recorder'](this);
|
|
441
|
+
this._recorder['resumeRecording'](startNewIfInactive);
|
|
442
|
+
}, this);
|
|
443
|
+
|
|
444
|
+
if (_.isUndefined(window['__mp_recorder'])) {
|
|
445
|
+
load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
|
|
446
|
+
} else {
|
|
447
|
+
handleLoadedRecorder();
|
|
448
|
+
}
|
|
386
449
|
}, this);
|
|
387
450
|
|
|
388
|
-
|
|
389
|
-
|
|
451
|
+
/**
|
|
452
|
+
* If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
|
|
453
|
+
* Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
|
|
454
|
+
*/
|
|
455
|
+
var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
|
|
456
|
+
if (force_start || is_sampled) {
|
|
457
|
+
loadRecorder(true);
|
|
390
458
|
} else {
|
|
391
|
-
|
|
459
|
+
this._should_load_recorder()
|
|
460
|
+
.then(function (shouldLoad) {
|
|
461
|
+
if (shouldLoad) {
|
|
462
|
+
loadRecorder(false);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
392
465
|
}
|
|
393
466
|
});
|
|
394
467
|
|
|
468
|
+
MixpanelLib.prototype.start_session_recording = function () {
|
|
469
|
+
this._check_and_start_session_recording(true);
|
|
470
|
+
};
|
|
471
|
+
|
|
395
472
|
MixpanelLib.prototype.stop_session_recording = function () {
|
|
396
473
|
if (this._recorder) {
|
|
397
474
|
this._recorder['stopRecording']();
|
|
398
|
-
}
|
|
399
|
-
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
MixpanelLib.prototype.pause_session_recording = function () {
|
|
479
|
+
if (this._recorder) {
|
|
480
|
+
this._recorder['pauseRecording']();
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
MixpanelLib.prototype.resume_session_recording = function () {
|
|
485
|
+
if (this._recorder) {
|
|
486
|
+
this._recorder['resumeRecording']();
|
|
400
487
|
}
|
|
401
488
|
};
|
|
402
489
|
|
|
@@ -431,6 +518,11 @@ MixpanelLib.prototype._get_session_replay_id = function () {
|
|
|
431
518
|
return replay_id || null;
|
|
432
519
|
};
|
|
433
520
|
|
|
521
|
+
// "private" public method to reach into the recorder in test cases
|
|
522
|
+
MixpanelLib.prototype.__get_recorder = function () {
|
|
523
|
+
return this._recorder;
|
|
524
|
+
};
|
|
525
|
+
|
|
434
526
|
// Private methods
|
|
435
527
|
|
|
436
528
|
MixpanelLib.prototype._loaded = function() {
|
|
@@ -770,7 +862,8 @@ MixpanelLib.prototype.init_batchers = function() {
|
|
|
770
862
|
return this._run_hook('before_send_' + attrs.type, item);
|
|
771
863
|
}, this),
|
|
772
864
|
stopAllBatchingFunc: _.bind(this.stop_batch_senders, this),
|
|
773
|
-
usePersistence: true
|
|
865
|
+
usePersistence: true,
|
|
866
|
+
enqueueThrottleMs: 10,
|
|
774
867
|
}
|
|
775
868
|
);
|
|
776
869
|
}, this);
|
|
@@ -1871,6 +1964,7 @@ MixpanelLib.prototype._gdpr_update_persistence = function(options) {
|
|
|
1871
1964
|
|
|
1872
1965
|
if (disabled) {
|
|
1873
1966
|
this.stop_batch_senders();
|
|
1967
|
+
this.stop_session_recording();
|
|
1874
1968
|
} else {
|
|
1875
1969
|
// only start batchers after opt-in if they have previously been started
|
|
1876
1970
|
// in order to avoid unintentionally starting up batching for the first time
|
|
@@ -2111,10 +2205,16 @@ MixpanelLib.prototype['start_batch_senders'] = MixpanelLib.protot
|
|
|
2111
2205
|
MixpanelLib.prototype['stop_batch_senders'] = MixpanelLib.prototype.stop_batch_senders;
|
|
2112
2206
|
MixpanelLib.prototype['start_session_recording'] = MixpanelLib.prototype.start_session_recording;
|
|
2113
2207
|
MixpanelLib.prototype['stop_session_recording'] = MixpanelLib.prototype.stop_session_recording;
|
|
2208
|
+
MixpanelLib.prototype['pause_session_recording'] = MixpanelLib.prototype.pause_session_recording;
|
|
2209
|
+
MixpanelLib.prototype['resume_session_recording'] = MixpanelLib.prototype.resume_session_recording;
|
|
2114
2210
|
MixpanelLib.prototype['get_session_recording_properties'] = MixpanelLib.prototype.get_session_recording_properties;
|
|
2115
2211
|
MixpanelLib.prototype['get_session_replay_url'] = MixpanelLib.prototype.get_session_replay_url;
|
|
2212
|
+
MixpanelLib.prototype['get_tab_id'] = MixpanelLib.prototype.get_tab_id;
|
|
2116
2213
|
MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES;
|
|
2117
2214
|
|
|
2215
|
+
// Exports intended only for testing
|
|
2216
|
+
MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
|
|
2217
|
+
|
|
2118
2218
|
// MixpanelPersistence Exports
|
|
2119
2219
|
MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;
|
|
2120
2220
|
MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword;
|