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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.58.0'
5
+ LIB_VERSION: '2.60.0'
6
6
  };
7
7
 
8
8
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -14,11 +14,14 @@ if (typeof(window) === 'undefined') {
14
14
  win = {
15
15
  navigator: { userAgent: '', onLine: true },
16
16
  document: {
17
+ createElement: function() { return {}; },
17
18
  location: loc,
18
19
  referrer: ''
19
20
  },
20
21
  screen: { width: 0, height: 0 },
21
- location: loc
22
+ location: loc,
23
+ addEventListener: function() {},
24
+ removeEventListener: function() {}
22
25
  };
23
26
  } else {
24
27
  win = window;
@@ -493,6 +496,29 @@ var console_with_prefix = function(prefix) {
493
496
  };
494
497
 
495
498
 
499
+ var safewrap = function(f) {
500
+ return function() {
501
+ try {
502
+ return f.apply(this, arguments);
503
+ } catch (e) {
504
+ console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
505
+ if (Config.DEBUG){
506
+ console.critical(e);
507
+ }
508
+ }
509
+ };
510
+ };
511
+
512
+ var safewrapClass = function(klass) {
513
+ var proto = klass.prototype;
514
+ for (var func in proto) {
515
+ if (typeof(proto[func]) === 'function') {
516
+ proto[func] = safewrap(proto[func]);
517
+ }
518
+ }
519
+ };
520
+
521
+
496
522
  // UNDERSCORE
497
523
  // Embed part of the Underscore Library
498
524
  _.bind = function(func, context) {
@@ -1271,6 +1297,7 @@ _.UUID = (function() {
1271
1297
  var BLOCKED_UA_STRS = [
1272
1298
  'ahrefsbot',
1273
1299
  'ahrefssiteaudit',
1300
+ 'amazonbot',
1274
1301
  'baiduspider',
1275
1302
  'bingbot',
1276
1303
  'bingpreview',
@@ -1280,7 +1307,7 @@ var BLOCKED_UA_STRS = [
1280
1307
  'pinterest',
1281
1308
  'screaming frog',
1282
1309
  'yahoo! slurp',
1283
- 'yandexbot',
1310
+ 'yandex',
1284
1311
 
1285
1312
  // a whole bunch of goog-specific crawlers
1286
1313
  // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
@@ -2101,6 +2128,851 @@ _['info']['browserVersion'] = _.info.browserVersion;
2101
2128
  _['info']['properties'] = _.info.properties;
2102
2129
  _['NPO'] = NpoPromise;
2103
2130
 
2131
+ // stateless utils
2132
+
2133
+ var EV_CHANGE = 'change';
2134
+ var EV_CLICK = 'click';
2135
+ var EV_HASHCHANGE = 'hashchange';
2136
+ var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
2137
+ var EV_POPSTATE = 'popstate';
2138
+ // TODO scrollend isn't available in Safari: document or polyfill?
2139
+ var EV_SCROLLEND = 'scrollend';
2140
+ var EV_SUBMIT = 'submit';
2141
+
2142
+ var CLICK_EVENT_PROPS = [
2143
+ 'clientX', 'clientY',
2144
+ 'offsetX', 'offsetY',
2145
+ 'pageX', 'pageY',
2146
+ 'screenX', 'screenY',
2147
+ 'x', 'y'
2148
+ ];
2149
+ var OPT_IN_CLASSES = ['mp-include'];
2150
+ var OPT_OUT_CLASSES = ['mp-no-track'];
2151
+ var SENSITIVE_DATA_CLASSES = OPT_OUT_CLASSES.concat(['mp-sensitive']);
2152
+ var TRACKED_ATTRS = [
2153
+ 'aria-label', 'aria-labelledby', 'aria-describedby',
2154
+ 'href', 'name', 'role', 'title', 'type'
2155
+ ];
2156
+
2157
+ var logger$3 = console_with_prefix('autocapture');
2158
+
2159
+
2160
+ function getClasses(el) {
2161
+ var classes = {};
2162
+ var classList = getClassName(el).split(' ');
2163
+ for (var i = 0; i < classList.length; i++) {
2164
+ var cls = classList[i];
2165
+ if (cls) {
2166
+ classes[cls] = true;
2167
+ }
2168
+ }
2169
+ return classes;
2170
+ }
2171
+
2172
+ /*
2173
+ * Get the className of an element, accounting for edge cases where element.className is an object
2174
+ * @param {Element} el - element to get the className of
2175
+ * @returns {string} the element's class
2176
+ */
2177
+ function getClassName(el) {
2178
+ switch(typeof el.className) {
2179
+ case 'string':
2180
+ return el.className;
2181
+ case 'object': // handle cases where className might be SVGAnimatedString or some other type
2182
+ return el.className.baseVal || el.getAttribute('class') || '';
2183
+ default: // future proof
2184
+ return '';
2185
+ }
2186
+ }
2187
+
2188
+ function getPreviousElementSibling(el) {
2189
+ if (el.previousElementSibling) {
2190
+ return el.previousElementSibling;
2191
+ } else {
2192
+ do {
2193
+ el = el.previousSibling;
2194
+ } while (el && !isElementNode(el));
2195
+ return el;
2196
+ }
2197
+ }
2198
+
2199
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
2200
+ var props = {
2201
+ '$classes': getClassName(el).split(' '),
2202
+ '$tag_name': el.tagName.toLowerCase()
2203
+ };
2204
+ var elId = el.id;
2205
+ if (elId) {
2206
+ props['$id'] = elId;
2207
+ }
2208
+
2209
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
2210
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
2211
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
2212
+ var attrVal = el.getAttribute(attr);
2213
+ if (shouldTrackValue(attrVal)) {
2214
+ props['$attr-' + attr] = attrVal;
2215
+ }
2216
+ }
2217
+ });
2218
+ }
2219
+
2220
+ var nthChild = 1;
2221
+ var nthOfType = 1;
2222
+ var currentElem = el;
2223
+ while (currentElem = getPreviousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
2224
+ nthChild++;
2225
+ if (currentElem.tagName === el.tagName) {
2226
+ nthOfType++;
2227
+ }
2228
+ }
2229
+ props['$nth_child'] = nthChild;
2230
+ props['$nth_of_type'] = nthOfType;
2231
+
2232
+ return props;
2233
+ }
2234
+
2235
+ function getPropsForDOMEvent(ev, config) {
2236
+ var allowElementCallback = config.allowElementCallback;
2237
+ var allowSelectors = config.allowSelectors || [];
2238
+ var blockAttrs = config.blockAttrs || [];
2239
+ var blockElementCallback = config.blockElementCallback;
2240
+ var blockSelectors = config.blockSelectors || [];
2241
+ var captureTextContent = config.captureTextContent || false;
2242
+ var captureExtraAttrs = config.captureExtraAttrs || [];
2243
+
2244
+ // convert array to set every time, as the config may have changed
2245
+ var blockAttrsSet = {};
2246
+ _.each(blockAttrs, function(attr) {
2247
+ blockAttrsSet[attr] = true;
2248
+ });
2249
+
2250
+ var props = null;
2251
+
2252
+ var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
2253
+ if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
2254
+ target = target.parentNode;
2255
+ }
2256
+
2257
+ if (
2258
+ shouldTrackDomEvent(target, ev) &&
2259
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
2260
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2261
+ ) {
2262
+ var targetElementList = [target];
2263
+ var curEl = target;
2264
+ while (curEl.parentNode && !isTag(curEl, 'body')) {
2265
+ targetElementList.push(curEl.parentNode);
2266
+ curEl = curEl.parentNode;
2267
+ }
2268
+
2269
+ var elementsJson = [];
2270
+ var href, explicitNoTrack = false;
2271
+ _.each(targetElementList, function(el) {
2272
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
2273
+
2274
+ // if the element or a parent element is an anchor tag
2275
+ // include the href as a property
2276
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
2277
+ href = el.getAttribute('href');
2278
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
2279
+ }
2280
+
2281
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
2282
+ explicitNoTrack = true;
2283
+ }
2284
+
2285
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
2286
+ }, this);
2287
+
2288
+ if (!explicitNoTrack) {
2289
+ var docElement = document$1['documentElement'];
2290
+ props = {
2291
+ '$event_type': ev.type,
2292
+ '$host': win.location.host,
2293
+ '$pathname': win.location.pathname,
2294
+ '$elements': elementsJson,
2295
+ '$el_attr__href': href,
2296
+ '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
2297
+ '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
2298
+ };
2299
+ _.each(captureExtraAttrs, function(attr) {
2300
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
2301
+ var attrVal = target.getAttribute(attr);
2302
+ if (shouldTrackValue(attrVal)) {
2303
+ props['$el_attr__' + attr] = attrVal;
2304
+ }
2305
+ }
2306
+ });
2307
+
2308
+ if (captureTextContent) {
2309
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2310
+ if (elementText && elementText.length) {
2311
+ props['$el_text'] = elementText;
2312
+ }
2313
+ }
2314
+
2315
+ if (ev.type === EV_CLICK) {
2316
+ _.each(CLICK_EVENT_PROPS, function(prop) {
2317
+ if (prop in ev) {
2318
+ props['$' + prop] = ev[prop];
2319
+ }
2320
+ });
2321
+ target = guessRealClickTarget(ev);
2322
+ }
2323
+ // prioritize text content from "real" click target if different from original target
2324
+ if (captureTextContent) {
2325
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2326
+ if (elementText && elementText.length) {
2327
+ props['$el_text'] = elementText;
2328
+ }
2329
+ }
2330
+
2331
+ if (target) {
2332
+ // target may have been recalculated; check allowlists and blocklists again
2333
+ if (
2334
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
2335
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2336
+ ) {
2337
+ return null;
2338
+ }
2339
+
2340
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
2341
+ props['$target'] = targetProps;
2342
+ // pull up more props onto main event props
2343
+ props['$el_classes'] = targetProps['$classes'];
2344
+ _.extend(props, _.strip_empty_properties({
2345
+ '$el_id': targetProps['$id'],
2346
+ '$el_tag_name': targetProps['$tag_name']
2347
+ }));
2348
+ }
2349
+ }
2350
+ }
2351
+
2352
+ return props;
2353
+ }
2354
+
2355
+
2356
+ /**
2357
+ * Get the direct text content of an element, protecting against sensitive data collection.
2358
+ * Concats textContent of each of the element's text node children; this avoids potential
2359
+ * collection of sensitive data that could happen if we used element.textContent and the
2360
+ * element had sensitive child elements, since element.textContent includes child content.
2361
+ * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
2362
+ * @param {Element} el - element to get the text of
2363
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2364
+ * @returns {string} the element's direct text content
2365
+ */
2366
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
2367
+ var elText = '';
2368
+
2369
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
2370
+ _.each(el.childNodes, function(child) {
2371
+ if (isTextNode(child) && child.textContent) {
2372
+ elText += _.trim(child.textContent)
2373
+ // scrub potentially sensitive values
2374
+ .split(/(\s+)/).filter(shouldTrackValue).join('')
2375
+ // normalize whitespace
2376
+ .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
2377
+ // truncate
2378
+ .substring(0, 255);
2379
+ }
2380
+ });
2381
+ }
2382
+
2383
+ return _.trim(elText);
2384
+ }
2385
+
2386
+ function guessRealClickTarget(ev) {
2387
+ var target = ev.target;
2388
+ var composedPath = ev['composedPath']();
2389
+ for (var i = 0; i < composedPath.length; i++) {
2390
+ var node = composedPath[i];
2391
+ if (
2392
+ isTag(node, 'a') ||
2393
+ isTag(node, 'button') ||
2394
+ isTag(node, 'input') ||
2395
+ isTag(node, 'select') ||
2396
+ (node.getAttribute && node.getAttribute('role') === 'button')
2397
+ ) {
2398
+ target = node;
2399
+ break;
2400
+ }
2401
+ if (node === target) {
2402
+ break;
2403
+ }
2404
+ }
2405
+ return target;
2406
+ }
2407
+
2408
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
2409
+ if (allowElementCallback) {
2410
+ try {
2411
+ if (!allowElementCallback(el, ev)) {
2412
+ return false;
2413
+ }
2414
+ } catch (err) {
2415
+ logger$3.critical('Error while checking element in allowElementCallback', err);
2416
+ return false;
2417
+ }
2418
+ }
2419
+
2420
+ if (!allowSelectors.length) {
2421
+ // no allowlist; all elements are fair game
2422
+ return true;
2423
+ }
2424
+
2425
+ for (var i = 0; i < allowSelectors.length; i++) {
2426
+ var sel = allowSelectors[i];
2427
+ try {
2428
+ if (el['matches'](sel)) {
2429
+ return true;
2430
+ }
2431
+ } catch (err) {
2432
+ logger$3.critical('Error while checking selector: ' + sel, err);
2433
+ }
2434
+ }
2435
+ return false;
2436
+ }
2437
+
2438
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
2439
+ var i;
2440
+
2441
+ if (blockElementCallback) {
2442
+ try {
2443
+ if (blockElementCallback(el, ev)) {
2444
+ return true;
2445
+ }
2446
+ } catch (err) {
2447
+ logger$3.critical('Error while checking element in blockElementCallback', err);
2448
+ return true;
2449
+ }
2450
+ }
2451
+
2452
+ if (blockSelectors && blockSelectors.length) {
2453
+ // programmatically prevent tracking of elements that match CSS selectors
2454
+ for (i = 0; i < blockSelectors.length; i++) {
2455
+ var sel = blockSelectors[i];
2456
+ try {
2457
+ if (el['matches'](sel)) {
2458
+ return true;
2459
+ }
2460
+ } catch (err) {
2461
+ logger$3.critical('Error while checking selector: ' + sel, err);
2462
+ }
2463
+ }
2464
+ }
2465
+
2466
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
2467
+ var classes = getClasses(el);
2468
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
2469
+ if (classes[OPT_OUT_CLASSES[i]]) {
2470
+ return true;
2471
+ }
2472
+ }
2473
+
2474
+ return false;
2475
+ }
2476
+
2477
+ /*
2478
+ * Check whether a DOM node has nodeType Node.ELEMENT_NODE
2479
+ * @param {Node} node - node to check
2480
+ * @returns {boolean} whether node is of the correct nodeType
2481
+ */
2482
+ function isElementNode(node) {
2483
+ return node && node.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
2484
+ }
2485
+
2486
+ /*
2487
+ * Check whether an element is of a given tag type.
2488
+ * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
2489
+ * we want to match tagNames instead of specific references because something like
2490
+ * element === document.body won't always work because element might not be a native
2491
+ * element.
2492
+ * @param {Element} el - element to check
2493
+ * @param {string} tag - tag name (e.g., "div")
2494
+ * @returns {boolean} whether el is of the given tag type
2495
+ */
2496
+ function isTag(el, tag) {
2497
+ return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
2498
+ }
2499
+
2500
+ /*
2501
+ * Check whether a DOM node is a TEXT_NODE
2502
+ * @param {Node} node - node to check
2503
+ * @returns {boolean} whether node is of type Node.TEXT_NODE
2504
+ */
2505
+ function isTextNode(node) {
2506
+ return node && node.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
2507
+ }
2508
+
2509
+ function minDOMApisSupported() {
2510
+ try {
2511
+ var testEl = document$1.createElement('div');
2512
+ return !!testEl['matches'];
2513
+ } catch (err) {
2514
+ return false;
2515
+ }
2516
+ }
2517
+
2518
+ /*
2519
+ * Check whether a DOM event should be "tracked" or if it may contain sensitive data
2520
+ * using a variety of heuristics.
2521
+ * @param {Element} el - element to check
2522
+ * @param {Event} ev - event to check
2523
+ * @returns {boolean} whether the event should be tracked
2524
+ */
2525
+ function shouldTrackDomEvent(el, ev) {
2526
+ if (!el || isTag(el, 'html') || !isElementNode(el)) {
2527
+ return false;
2528
+ }
2529
+ var tag = el.tagName.toLowerCase();
2530
+ switch (tag) {
2531
+ case 'form':
2532
+ return ev.type === EV_SUBMIT;
2533
+ case 'input':
2534
+ if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
2535
+ return ev.type === EV_CHANGE;
2536
+ } else {
2537
+ return ev.type === EV_CLICK;
2538
+ }
2539
+ case 'select':
2540
+ case 'textarea':
2541
+ return ev.type === EV_CHANGE;
2542
+ default:
2543
+ return ev.type === EV_CLICK;
2544
+ }
2545
+ }
2546
+
2547
+ /*
2548
+ * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2549
+ * using a variety of heuristics.
2550
+ * @param {Element} el - element to check
2551
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2552
+ * @returns {boolean} whether the element should be tracked
2553
+ */
2554
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
2555
+ var i;
2556
+
2557
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
2558
+ return false;
2559
+ }
2560
+
2561
+ for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
2562
+ var classes = getClasses(curEl);
2563
+ for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
2564
+ if (classes[SENSITIVE_DATA_CLASSES[i]]) {
2565
+ return false;
2566
+ }
2567
+ }
2568
+ }
2569
+
2570
+ var elClasses = getClasses(el);
2571
+ for (i = 0; i < OPT_IN_CLASSES.length; i++) {
2572
+ if (elClasses[OPT_IN_CLASSES[i]]) {
2573
+ return true;
2574
+ }
2575
+ }
2576
+
2577
+ // don't send data from inputs or similar elements since there will always be
2578
+ // a risk of clientside javascript placing sensitive data in attributes
2579
+ if (
2580
+ isTag(el, 'input') ||
2581
+ isTag(el, 'select') ||
2582
+ isTag(el, 'textarea') ||
2583
+ el.getAttribute('contenteditable') === 'true'
2584
+ ) {
2585
+ return false;
2586
+ }
2587
+
2588
+ // don't include hidden or password fields
2589
+ var type = el.type || '';
2590
+ 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"]
2591
+ switch(type.toLowerCase()) {
2592
+ case 'hidden':
2593
+ return false;
2594
+ case 'password':
2595
+ return false;
2596
+ }
2597
+ }
2598
+
2599
+ // filter out data from fields that look like sensitive fields
2600
+ var name = el.name || el.id || '';
2601
+ 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"]
2602
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
2603
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
2604
+ return false;
2605
+ }
2606
+ }
2607
+
2608
+ return true;
2609
+ }
2610
+
2611
+
2612
+ /*
2613
+ * Check whether a string value should be "tracked" or if it may contain sensitive data
2614
+ * using a variety of heuristics.
2615
+ * @param {string} value - string value to check
2616
+ * @returns {boolean} whether the element should be tracked
2617
+ */
2618
+ function shouldTrackValue(value) {
2619
+ if (value === null || _.isUndefined(value)) {
2620
+ return false;
2621
+ }
2622
+
2623
+ if (typeof value === 'string') {
2624
+ value = _.trim(value);
2625
+
2626
+ // check to see if input value looks like a credit card number
2627
+ // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
2628
+ 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}))$/;
2629
+ if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
2630
+ return false;
2631
+ }
2632
+
2633
+ // check to see if input value looks like a social security number
2634
+ var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
2635
+ if (ssnRegex.test(value)) {
2636
+ return false;
2637
+ }
2638
+ }
2639
+
2640
+ return true;
2641
+ }
2642
+
2643
+ var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
2644
+ var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
2645
+
2646
+ var PAGEVIEW_OPTION_FULL_URL = 'full-url';
2647
+ var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
2648
+ var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
2649
+
2650
+ var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
2651
+ var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
2652
+ var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
2653
+ var CONFIG_BLOCK_ATTRS = 'block_attrs';
2654
+ var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
2655
+ var CONFIG_BLOCK_SELECTORS = 'block_selectors';
2656
+ var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
2657
+ var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
2658
+ var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
2659
+ var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
2660
+ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
2661
+ var CONFIG_TRACK_CLICK = 'click';
2662
+ var CONFIG_TRACK_INPUT = 'input';
2663
+ var CONFIG_TRACK_PAGEVIEW = 'pageview';
2664
+ var CONFIG_TRACK_SCROLL = 'scroll';
2665
+ var CONFIG_TRACK_SUBMIT = 'submit';
2666
+
2667
+ var CONFIG_DEFAULTS = {};
2668
+ CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
2669
+ CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
2670
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
2671
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
2672
+ CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
2673
+ CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
2674
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
2675
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
2676
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
2677
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
2678
+ CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
2679
+ CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
2680
+ CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
2681
+ CONFIG_DEFAULTS[CONFIG_TRACK_SCROLL] = true;
2682
+ CONFIG_DEFAULTS[CONFIG_TRACK_SUBMIT] = true;
2683
+
2684
+ var DEFAULT_PROPS = {
2685
+ '$mp_autocapture': true
2686
+ };
2687
+
2688
+ var MP_EV_CLICK = '$mp_click';
2689
+ var MP_EV_INPUT = '$mp_input_change';
2690
+ var MP_EV_SCROLL = '$mp_scroll';
2691
+ var MP_EV_SUBMIT = '$mp_submit';
2692
+
2693
+ /**
2694
+ * Autocapture: manages automatic event tracking
2695
+ * @constructor
2696
+ */
2697
+ var Autocapture = function(mp) {
2698
+ this.mp = mp;
2699
+ };
2700
+
2701
+ Autocapture.prototype.init = function() {
2702
+ if (!minDOMApisSupported()) {
2703
+ logger$3.critical('Autocapture unavailable: missing required DOM APIs');
2704
+ return;
2705
+ }
2706
+
2707
+ this.initPageviewTracking();
2708
+ this.initClickTracking();
2709
+ this.initInputTracking();
2710
+ this.initScrollTracking();
2711
+ this.initSubmitTracking();
2712
+ };
2713
+
2714
+ Autocapture.prototype.getFullConfig = function() {
2715
+ var autocaptureConfig = this.mp.get_config(AUTOCAPTURE_CONFIG_KEY);
2716
+ if (!autocaptureConfig) {
2717
+ // Autocapture is completely off
2718
+ return {};
2719
+ } else if (_.isObject(autocaptureConfig)) {
2720
+ return _.extend({}, CONFIG_DEFAULTS, autocaptureConfig);
2721
+ } else {
2722
+ // Autocapture config is non-object truthy value, return default
2723
+ return CONFIG_DEFAULTS;
2724
+ }
2725
+ };
2726
+
2727
+ Autocapture.prototype.getConfig = function(key) {
2728
+ return this.getFullConfig()[key];
2729
+ };
2730
+
2731
+ Autocapture.prototype.currentUrlBlocked = function() {
2732
+ var i;
2733
+ var currentUrl = _.info.currentUrl();
2734
+
2735
+ var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
2736
+ if (allowUrlRegexes.length) {
2737
+ // we're using an allowlist, only track if current URL matches
2738
+ var allowed = false;
2739
+ for (i = 0; i < allowUrlRegexes.length; i++) {
2740
+ var allowRegex = allowUrlRegexes[i];
2741
+ try {
2742
+ if (currentUrl.match(allowRegex)) {
2743
+ allowed = true;
2744
+ break;
2745
+ }
2746
+ } catch (err) {
2747
+ logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
2748
+ return true;
2749
+ }
2750
+ }
2751
+ if (!allowed) {
2752
+ // wasn't allowed by any regex
2753
+ return true;
2754
+ }
2755
+ }
2756
+
2757
+ var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
2758
+ if (!blockUrlRegexes || !blockUrlRegexes.length) {
2759
+ return false;
2760
+ }
2761
+
2762
+ for (i = 0; i < blockUrlRegexes.length; i++) {
2763
+ try {
2764
+ if (currentUrl.match(blockUrlRegexes[i])) {
2765
+ return true;
2766
+ }
2767
+ } catch (err) {
2768
+ logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
2769
+ return true;
2770
+ }
2771
+ }
2772
+ return false;
2773
+ };
2774
+
2775
+ Autocapture.prototype.pageviewTrackingConfig = function() {
2776
+ // supports both autocapture config and old track_pageview config
2777
+ if (this.mp.get_config(AUTOCAPTURE_CONFIG_KEY)) {
2778
+ return this.getConfig(CONFIG_TRACK_PAGEVIEW);
2779
+ } else {
2780
+ return this.mp.get_config(LEGACY_PAGEVIEW_CONFIG_KEY);
2781
+ }
2782
+ };
2783
+
2784
+ // helper for event handlers
2785
+ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
2786
+ if (this.currentUrlBlocked()) {
2787
+ return;
2788
+ }
2789
+
2790
+ var props = getPropsForDOMEvent(ev, {
2791
+ allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
2792
+ allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
2793
+ blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
2794
+ blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
2795
+ blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
2796
+ captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
2797
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
2798
+ });
2799
+ if (props) {
2800
+ _.extend(props, DEFAULT_PROPS);
2801
+ this.mp.track(mpEventName, props);
2802
+ }
2803
+ };
2804
+
2805
+ Autocapture.prototype.initClickTracking = function() {
2806
+ win.removeEventListener(EV_CLICK, this.listenerClick);
2807
+
2808
+ if (!this.getConfig(CONFIG_TRACK_CLICK)) {
2809
+ return;
2810
+ }
2811
+ logger$3.log('Initializing click tracking');
2812
+
2813
+ this.listenerClick = win.addEventListener(EV_CLICK, function(ev) {
2814
+ if (!this.getConfig(CONFIG_TRACK_CLICK)) {
2815
+ return;
2816
+ }
2817
+ this.trackDomEvent(ev, MP_EV_CLICK);
2818
+ }.bind(this));
2819
+ };
2820
+
2821
+ Autocapture.prototype.initInputTracking = function() {
2822
+ win.removeEventListener(EV_CHANGE, this.listenerChange);
2823
+
2824
+ if (!this.getConfig(CONFIG_TRACK_INPUT)) {
2825
+ return;
2826
+ }
2827
+ logger$3.log('Initializing input tracking');
2828
+
2829
+ this.listenerChange = win.addEventListener(EV_CHANGE, function(ev) {
2830
+ if (!this.getConfig(CONFIG_TRACK_INPUT)) {
2831
+ return;
2832
+ }
2833
+ this.trackDomEvent(ev, MP_EV_INPUT);
2834
+ }.bind(this));
2835
+ };
2836
+
2837
+ Autocapture.prototype.initPageviewTracking = function() {
2838
+ win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
2839
+ win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
2840
+ win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
2841
+
2842
+ if (!this.pageviewTrackingConfig()) {
2843
+ return;
2844
+ }
2845
+ logger$3.log('Initializing pageview tracking');
2846
+
2847
+ var previousTrackedUrl = '';
2848
+ var tracked = false;
2849
+ if (!this.currentUrlBlocked()) {
2850
+ tracked = this.mp.track_pageview(DEFAULT_PROPS);
2851
+ }
2852
+ if (tracked) {
2853
+ previousTrackedUrl = _.info.currentUrl();
2854
+ }
2855
+
2856
+ this.listenerPopstate = win.addEventListener(EV_POPSTATE, function() {
2857
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2858
+ });
2859
+ this.listenerHashchange = win.addEventListener(EV_HASHCHANGE, function() {
2860
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2861
+ });
2862
+ var nativePushState = win.history.pushState;
2863
+ if (typeof nativePushState === 'function') {
2864
+ win.history.pushState = function(state, unused, url) {
2865
+ nativePushState.call(win.history, state, unused, url);
2866
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2867
+ };
2868
+ }
2869
+ var nativeReplaceState = win.history.replaceState;
2870
+ if (typeof nativeReplaceState === 'function') {
2871
+ win.history.replaceState = function(state, unused, url) {
2872
+ nativeReplaceState.call(win.history, state, unused, url);
2873
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
2874
+ };
2875
+ }
2876
+ this.listenerLocationchange = win.addEventListener(EV_MP_LOCATION_CHANGE, safewrap(function() {
2877
+ if (this.currentUrlBlocked()) {
2878
+ return;
2879
+ }
2880
+
2881
+ var currentUrl = _.info.currentUrl();
2882
+ var shouldTrack = false;
2883
+ var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
2884
+ var trackPageviewOption = this.pageviewTrackingConfig();
2885
+ if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
2886
+ shouldTrack = currentUrl !== previousTrackedUrl;
2887
+ } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
2888
+ shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
2889
+ } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
2890
+ shouldTrack = didPathChange;
2891
+ }
2892
+
2893
+ if (shouldTrack) {
2894
+ var tracked = this.mp.track_pageview(DEFAULT_PROPS);
2895
+ if (tracked) {
2896
+ previousTrackedUrl = currentUrl;
2897
+ }
2898
+ if (didPathChange) {
2899
+ this.lastScrollCheckpoint = 0;
2900
+ logger$3.log('Path change: re-initializing scroll depth checkpoints');
2901
+ }
2902
+ }
2903
+ }.bind(this)));
2904
+ };
2905
+
2906
+ Autocapture.prototype.initScrollTracking = function() {
2907
+ win.removeEventListener(EV_SCROLLEND, this.listenerScroll);
2908
+
2909
+ if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
2910
+ return;
2911
+ }
2912
+ logger$3.log('Initializing scroll tracking');
2913
+ this.lastScrollCheckpoint = 0;
2914
+
2915
+ this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
2916
+ if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
2917
+ return;
2918
+ }
2919
+ if (this.currentUrlBlocked()) {
2920
+ return;
2921
+ }
2922
+
2923
+ var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
2924
+ var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
2925
+ .slice()
2926
+ .sort(function(a, b) { return a - b; });
2927
+
2928
+ var scrollTop = win.scrollY;
2929
+ var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
2930
+ try {
2931
+ var scrollHeight = document$1.body.scrollHeight;
2932
+ var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
2933
+ props['$scroll_height'] = scrollHeight;
2934
+ props['$scroll_percentage'] = scrollPercentage;
2935
+ if (scrollPercentage > this.lastScrollCheckpoint) {
2936
+ for (var i = 0; i < scrollCheckpoints.length; i++) {
2937
+ var checkpoint = scrollCheckpoints[i];
2938
+ if (
2939
+ scrollPercentage >= checkpoint &&
2940
+ this.lastScrollCheckpoint < checkpoint
2941
+ ) {
2942
+ props['$scroll_checkpoint'] = checkpoint;
2943
+ this.lastScrollCheckpoint = checkpoint;
2944
+ shouldTrack = true;
2945
+ }
2946
+ }
2947
+ }
2948
+ } catch (err) {
2949
+ logger$3.critical('Error while calculating scroll percentage', err);
2950
+ }
2951
+ if (shouldTrack) {
2952
+ this.mp.track(MP_EV_SCROLL, props);
2953
+ }
2954
+ }.bind(this)));
2955
+ };
2956
+
2957
+ Autocapture.prototype.initSubmitTracking = function() {
2958
+ win.removeEventListener(EV_SUBMIT, this.listenerSubmit);
2959
+
2960
+ if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
2961
+ return;
2962
+ }
2963
+ logger$3.log('Initializing submit tracking');
2964
+
2965
+ this.listenerSubmit = win.addEventListener(EV_SUBMIT, function(ev) {
2966
+ if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
2967
+ return;
2968
+ }
2969
+ this.trackDomEvent(ev, MP_EV_SUBMIT);
2970
+ }.bind(this));
2971
+ };
2972
+
2973
+ // TODO integrate error_reporter from mixpanel instance
2974
+ safewrapClass(Autocapture);
2975
+
2104
2976
  /* eslint camelcase: "off" */
2105
2977
 
2106
2978
  /**
@@ -4530,8 +5402,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
4530
5402
  if (!(k in union_q)) {
4531
5403
  union_q[k] = [];
4532
5404
  }
4533
- // We may send duplicates, the server will dedup them.
4534
- union_q[k] = union_q[k].concat(v);
5405
+ // Prevent duplicate values
5406
+ _.each(v, function(item) {
5407
+ if (!_.include(union_q[k], item)) {
5408
+ union_q[k].push(item);
5409
+ }
5410
+ });
4535
5411
  }
4536
5412
  });
4537
5413
  this._pop_from_people_queue(UNSET_ACTION, q_data);
@@ -4703,6 +5579,7 @@ var DEFAULT_CONFIG = {
4703
5579
  'api_transport': 'XHR',
4704
5580
  'api_payload_format': PAYLOAD_TYPE_BASE64,
4705
5581
  'app_host': 'https://mixpanel.com',
5582
+ 'autocapture': false,
4706
5583
  'cdn': 'https://cdn.mxpnl.com',
4707
5584
  'cross_site_cookie': false,
4708
5585
  'cross_subdomain_cookie': true,
@@ -4962,10 +5839,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
4962
5839
  }, '');
4963
5840
  }
4964
5841
 
4965
- var track_pageview_option = this.get_config('track_pageview');
4966
- if (track_pageview_option) {
4967
- this._init_url_change_tracking(track_pageview_option);
4968
- }
5842
+ this.autocapture = new Autocapture(this);
5843
+ this.autocapture.init();
4969
5844
 
4970
5845
  if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
4971
5846
  this.start_session_recording();
@@ -5090,55 +5965,6 @@ MixpanelLib.prototype._track_dom = function(DomClass, args) {
5090
5965
  return dt.track.apply(dt, args);
5091
5966
  };
5092
5967
 
5093
- MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) {
5094
- var previous_tracked_url = '';
5095
- var tracked = this.track_pageview();
5096
- if (tracked) {
5097
- previous_tracked_url = _.info.currentUrl();
5098
- }
5099
-
5100
- if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) {
5101
- win.addEventListener('popstate', function() {
5102
- win.dispatchEvent(new Event('mp_locationchange'));
5103
- });
5104
- win.addEventListener('hashchange', function() {
5105
- win.dispatchEvent(new Event('mp_locationchange'));
5106
- });
5107
- var nativePushState = win.history.pushState;
5108
- if (typeof nativePushState === 'function') {
5109
- win.history.pushState = function(state, unused, url) {
5110
- nativePushState.call(win.history, state, unused, url);
5111
- win.dispatchEvent(new Event('mp_locationchange'));
5112
- };
5113
- }
5114
- var nativeReplaceState = win.history.replaceState;
5115
- if (typeof nativeReplaceState === 'function') {
5116
- win.history.replaceState = function(state, unused, url) {
5117
- nativeReplaceState.call(win.history, state, unused, url);
5118
- win.dispatchEvent(new Event('mp_locationchange'));
5119
- };
5120
- }
5121
- win.addEventListener('mp_locationchange', function() {
5122
- var current_url = _.info.currentUrl();
5123
- var should_track = false;
5124
- if (track_pageview_option === 'full-url') {
5125
- should_track = current_url !== previous_tracked_url;
5126
- } else if (track_pageview_option === 'url-with-path-and-query-string') {
5127
- should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0];
5128
- } else if (track_pageview_option === 'url-with-path') {
5129
- should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0];
5130
- }
5131
-
5132
- if (should_track) {
5133
- var tracked = this.track_pageview();
5134
- if (tracked) {
5135
- previous_tracked_url = current_url;
5136
- }
5137
- }
5138
- }.bind(this));
5139
- }
5140
- };
5141
-
5142
5968
  /**
5143
5969
  * _prepare_callback() should be called by callers of _send_request for use
5144
5970
  * as the callback argument.
@@ -6396,6 +7222,10 @@ MixpanelLib.prototype.set_config = function(config) {
6396
7222
  this['persistence'].update_config(this['config']);
6397
7223
  }
6398
7224
  Config.DEBUG = Config.DEBUG || this.get_config('debug');
7225
+
7226
+ if ('autocapture' in config && this.autocapture) {
7227
+ this.autocapture.init();
7228
+ }
6399
7229
  }
6400
7230
  };
6401
7231