mixpanel-browser 2.59.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.
@@ -3,7 +3,7 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.59.0'
6
+ LIB_VERSION: '2.60.0'
7
7
  };
8
8
 
9
9
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -2197,7 +2197,7 @@
2197
2197
  }
2198
2198
  }
2199
2199
 
2200
- function getPropertiesFromElement(el) {
2200
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
2201
2201
  var props = {
2202
2202
  '$classes': getClassName(el).split(' '),
2203
2203
  '$tag_name': el.tagName.toLowerCase()
@@ -2207,9 +2207,9 @@
2207
2207
  props['$id'] = elId;
2208
2208
  }
2209
2209
 
2210
- if (shouldTrackElement(el)) {
2211
- _.each(TRACKED_ATTRS, function(attr) {
2212
- if (el.hasAttribute(attr)) {
2210
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
2211
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
2212
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
2213
2213
  var attrVal = el.getAttribute(attr);
2214
2214
  if (shouldTrackValue(attrVal)) {
2215
2215
  props['$attr-' + attr] = attrVal;
@@ -2233,8 +2233,21 @@
2233
2233
  return props;
2234
2234
  }
2235
2235
 
2236
- function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
2237
- blockSelectors = blockSelectors || [];
2236
+ function getPropsForDOMEvent(ev, config) {
2237
+ var allowElementCallback = config.allowElementCallback;
2238
+ var allowSelectors = config.allowSelectors || [];
2239
+ var blockAttrs = config.blockAttrs || [];
2240
+ var blockElementCallback = config.blockElementCallback;
2241
+ var blockSelectors = config.blockSelectors || [];
2242
+ var captureTextContent = config.captureTextContent || false;
2243
+ var captureExtraAttrs = config.captureExtraAttrs || [];
2244
+
2245
+ // convert array to set every time, as the config may have changed
2246
+ var blockAttrsSet = {};
2247
+ _.each(blockAttrs, function(attr) {
2248
+ blockAttrsSet[attr] = true;
2249
+ });
2250
+
2238
2251
  var props = null;
2239
2252
 
2240
2253
  var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
@@ -2242,7 +2255,11 @@
2242
2255
  target = target.parentNode;
2243
2256
  }
2244
2257
 
2245
- if (shouldTrackDomEvent(target, ev)) {
2258
+ if (
2259
+ shouldTrackDomEvent(target, ev) &&
2260
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
2261
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2262
+ ) {
2246
2263
  var targetElementList = [target];
2247
2264
  var curEl = target;
2248
2265
  while (curEl.parentNode && !isTag(curEl, 'body')) {
@@ -2253,37 +2270,20 @@
2253
2270
  var elementsJson = [];
2254
2271
  var href, explicitNoTrack = false;
2255
2272
  _.each(targetElementList, function(el) {
2256
- var shouldTrackEl = shouldTrackElement(el);
2273
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
2257
2274
 
2258
2275
  // if the element or a parent element is an anchor tag
2259
2276
  // include the href as a property
2260
- if (el.tagName.toLowerCase() === 'a') {
2277
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
2261
2278
  href = el.getAttribute('href');
2262
- href = shouldTrackEl && shouldTrackValue(href) && href;
2279
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
2263
2280
  }
2264
2281
 
2265
- // allow users to programmatically prevent tracking of elements by adding classes such as 'mp-no-track'
2266
- var classes = getClasses(el);
2267
- _.each(OPT_OUT_CLASSES, function(cls) {
2268
- if (classes[cls]) {
2269
- explicitNoTrack = true;
2270
- }
2271
- });
2272
-
2273
- if (!explicitNoTrack) {
2274
- // programmatically prevent tracking of elements that match CSS selectors
2275
- _.each(blockSelectors, function(sel) {
2276
- try {
2277
- if (el['matches'](sel)) {
2278
- explicitNoTrack = true;
2279
- }
2280
- } catch (err) {
2281
- logger$3.critical('Error while checking selector: ' + sel, err);
2282
- }
2283
- });
2282
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
2283
+ explicitNoTrack = true;
2284
2284
  }
2285
2285
 
2286
- elementsJson.push(getPropertiesFromElement(el));
2286
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
2287
2287
  }, this);
2288
2288
 
2289
2289
  if (!explicitNoTrack) {
@@ -2297,9 +2297,17 @@
2297
2297
  '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
2298
2298
  '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
2299
2299
  };
2300
+ _.each(captureExtraAttrs, function(attr) {
2301
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
2302
+ var attrVal = target.getAttribute(attr);
2303
+ if (shouldTrackValue(attrVal)) {
2304
+ props['$el_attr__' + attr] = attrVal;
2305
+ }
2306
+ }
2307
+ });
2300
2308
 
2301
2309
  if (captureTextContent) {
2302
- elementText = getSafeText(target);
2310
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2303
2311
  if (elementText && elementText.length) {
2304
2312
  props['$el_text'] = elementText;
2305
2313
  }
@@ -2315,14 +2323,22 @@
2315
2323
  }
2316
2324
  // prioritize text content from "real" click target if different from original target
2317
2325
  if (captureTextContent) {
2318
- var elementText = getSafeText(target);
2326
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
2319
2327
  if (elementText && elementText.length) {
2320
2328
  props['$el_text'] = elementText;
2321
2329
  }
2322
2330
  }
2323
2331
 
2324
2332
  if (target) {
2325
- var targetProps = getPropertiesFromElement(target);
2333
+ // target may have been recalculated; check allowlists and blocklists again
2334
+ if (
2335
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
2336
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
2337
+ ) {
2338
+ return null;
2339
+ }
2340
+
2341
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
2326
2342
  props['$target'] = targetProps;
2327
2343
  // pull up more props onto main event props
2328
2344
  props['$el_classes'] = targetProps['$classes'];
@@ -2338,19 +2354,20 @@
2338
2354
  }
2339
2355
 
2340
2356
 
2341
- /*
2357
+ /**
2342
2358
  * Get the direct text content of an element, protecting against sensitive data collection.
2343
2359
  * Concats textContent of each of the element's text node children; this avoids potential
2344
2360
  * collection of sensitive data that could happen if we used element.textContent and the
2345
2361
  * element had sensitive child elements, since element.textContent includes child content.
2346
2362
  * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
2347
2363
  * @param {Element} el - element to get the text of
2364
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2348
2365
  * @returns {string} the element's direct text content
2349
2366
  */
2350
- function getSafeText(el) {
2367
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
2351
2368
  var elText = '';
2352
2369
 
2353
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
2370
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
2354
2371
  _.each(el.childNodes, function(child) {
2355
2372
  if (isTextNode(child) && child.textContent) {
2356
2373
  elText += _.trim(child.textContent)
@@ -2389,6 +2406,75 @@
2389
2406
  return target;
2390
2407
  }
2391
2408
 
2409
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
2410
+ if (allowElementCallback) {
2411
+ try {
2412
+ if (!allowElementCallback(el, ev)) {
2413
+ return false;
2414
+ }
2415
+ } catch (err) {
2416
+ logger$3.critical('Error while checking element in allowElementCallback', err);
2417
+ return false;
2418
+ }
2419
+ }
2420
+
2421
+ if (!allowSelectors.length) {
2422
+ // no allowlist; all elements are fair game
2423
+ return true;
2424
+ }
2425
+
2426
+ for (var i = 0; i < allowSelectors.length; i++) {
2427
+ var sel = allowSelectors[i];
2428
+ try {
2429
+ if (el['matches'](sel)) {
2430
+ return true;
2431
+ }
2432
+ } catch (err) {
2433
+ logger$3.critical('Error while checking selector: ' + sel, err);
2434
+ }
2435
+ }
2436
+ return false;
2437
+ }
2438
+
2439
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
2440
+ var i;
2441
+
2442
+ if (blockElementCallback) {
2443
+ try {
2444
+ if (blockElementCallback(el, ev)) {
2445
+ return true;
2446
+ }
2447
+ } catch (err) {
2448
+ logger$3.critical('Error while checking element in blockElementCallback', err);
2449
+ return true;
2450
+ }
2451
+ }
2452
+
2453
+ if (blockSelectors && blockSelectors.length) {
2454
+ // programmatically prevent tracking of elements that match CSS selectors
2455
+ for (i = 0; i < blockSelectors.length; i++) {
2456
+ var sel = blockSelectors[i];
2457
+ try {
2458
+ if (el['matches'](sel)) {
2459
+ return true;
2460
+ }
2461
+ } catch (err) {
2462
+ logger$3.critical('Error while checking selector: ' + sel, err);
2463
+ }
2464
+ }
2465
+ }
2466
+
2467
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
2468
+ var classes = getClasses(el);
2469
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
2470
+ if (classes[OPT_OUT_CLASSES[i]]) {
2471
+ return true;
2472
+ }
2473
+ }
2474
+
2475
+ return false;
2476
+ }
2477
+
2392
2478
  /*
2393
2479
  * Check whether a DOM node has nodeType Node.ELEMENT_NODE
2394
2480
  * @param {Node} node - node to check
@@ -2463,11 +2549,16 @@
2463
2549
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
2464
2550
  * using a variety of heuristics.
2465
2551
  * @param {Element} el - element to check
2552
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
2466
2553
  * @returns {boolean} whether the element should be tracked
2467
2554
  */
2468
- function shouldTrackElement(el) {
2555
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
2469
2556
  var i;
2470
2557
 
2558
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
2559
+ return false;
2560
+ }
2561
+
2471
2562
  for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
2472
2563
  var classes = getClasses(curEl);
2473
2564
  for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
@@ -2557,9 +2648,17 @@
2557
2648
  var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
2558
2649
  var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
2559
2650
 
2651
+ var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
2652
+ var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
2653
+ var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
2654
+ var CONFIG_BLOCK_ATTRS = 'block_attrs';
2655
+ var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
2560
2656
  var CONFIG_BLOCK_SELECTORS = 'block_selectors';
2561
2657
  var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
2658
+ var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
2562
2659
  var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
2660
+ var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
2661
+ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
2563
2662
  var CONFIG_TRACK_CLICK = 'click';
2564
2663
  var CONFIG_TRACK_INPUT = 'input';
2565
2664
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
@@ -2567,7 +2666,16 @@
2567
2666
  var CONFIG_TRACK_SUBMIT = 'submit';
2568
2667
 
2569
2668
  var CONFIG_DEFAULTS = {};
2669
+ CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
2670
+ CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
2671
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
2672
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
2673
+ CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
2674
+ CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
2675
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
2570
2676
  CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
2677
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
2678
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
2571
2679
  CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
2572
2680
  CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
2573
2681
  CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
@@ -2622,13 +2730,37 @@
2622
2730
  };
2623
2731
 
2624
2732
  Autocapture.prototype.currentUrlBlocked = function() {
2733
+ var i;
2734
+ var currentUrl = _.info.currentUrl();
2735
+
2736
+ var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
2737
+ if (allowUrlRegexes.length) {
2738
+ // we're using an allowlist, only track if current URL matches
2739
+ var allowed = false;
2740
+ for (i = 0; i < allowUrlRegexes.length; i++) {
2741
+ var allowRegex = allowUrlRegexes[i];
2742
+ try {
2743
+ if (currentUrl.match(allowRegex)) {
2744
+ allowed = true;
2745
+ break;
2746
+ }
2747
+ } catch (err) {
2748
+ logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
2749
+ return true;
2750
+ }
2751
+ }
2752
+ if (!allowed) {
2753
+ // wasn't allowed by any regex
2754
+ return true;
2755
+ }
2756
+ }
2757
+
2625
2758
  var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
2626
2759
  if (!blockUrlRegexes || !blockUrlRegexes.length) {
2627
2760
  return false;
2628
2761
  }
2629
2762
 
2630
- var currentUrl = _.info.currentUrl();
2631
- for (var i = 0; i < blockUrlRegexes.length; i++) {
2763
+ for (i = 0; i < blockUrlRegexes.length; i++) {
2632
2764
  try {
2633
2765
  if (currentUrl.match(blockUrlRegexes[i])) {
2634
2766
  return true;
@@ -2656,11 +2788,15 @@
2656
2788
  return;
2657
2789
  }
2658
2790
 
2659
- var props = getPropsForDOMEvent(
2660
- ev,
2661
- this.getConfig(CONFIG_BLOCK_SELECTORS),
2662
- this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
2663
- );
2791
+ var props = getPropsForDOMEvent(ev, {
2792
+ allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
2793
+ allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
2794
+ blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
2795
+ blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
2796
+ blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
2797
+ captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
2798
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
2799
+ });
2664
2800
  if (props) {
2665
2801
  _.extend(props, DEFAULT_PROPS);
2666
2802
  this.mp.track(mpEventName, props);
@@ -2745,13 +2881,14 @@
2745
2881
 
2746
2882
  var currentUrl = _.info.currentUrl();
2747
2883
  var shouldTrack = false;
2884
+ var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
2748
2885
  var trackPageviewOption = this.pageviewTrackingConfig();
2749
2886
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
2750
2887
  shouldTrack = currentUrl !== previousTrackedUrl;
2751
2888
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
2752
2889
  shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
2753
2890
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
2754
- shouldTrack = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
2891
+ shouldTrack = didPathChange;
2755
2892
  }
2756
2893
 
2757
2894
  if (shouldTrack) {
@@ -2759,6 +2896,10 @@
2759
2896
  if (tracked) {
2760
2897
  previousTrackedUrl = currentUrl;
2761
2898
  }
2899
+ if (didPathChange) {
2900
+ this.lastScrollCheckpoint = 0;
2901
+ logger$3.log('Path change: re-initializing scroll depth checkpoints');
2902
+ }
2762
2903
  }
2763
2904
  }.bind(this)));
2764
2905
  };
@@ -2770,6 +2911,7 @@
2770
2911
  return;
2771
2912
  }
2772
2913
  logger$3.log('Initializing scroll tracking');
2914
+ this.lastScrollCheckpoint = 0;
2773
2915
 
2774
2916
  this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
2775
2917
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
@@ -2779,6 +2921,11 @@
2779
2921
  return;
2780
2922
  }
2781
2923
 
2924
+ var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
2925
+ var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
2926
+ .slice()
2927
+ .sort(function(a, b) { return a - b; });
2928
+
2782
2929
  var scrollTop = win.scrollY;
2783
2930
  var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
2784
2931
  try {
@@ -2786,10 +2933,25 @@
2786
2933
  var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
2787
2934
  props['$scroll_height'] = scrollHeight;
2788
2935
  props['$scroll_percentage'] = scrollPercentage;
2936
+ if (scrollPercentage > this.lastScrollCheckpoint) {
2937
+ for (var i = 0; i < scrollCheckpoints.length; i++) {
2938
+ var checkpoint = scrollCheckpoints[i];
2939
+ if (
2940
+ scrollPercentage >= checkpoint &&
2941
+ this.lastScrollCheckpoint < checkpoint
2942
+ ) {
2943
+ props['$scroll_checkpoint'] = checkpoint;
2944
+ this.lastScrollCheckpoint = checkpoint;
2945
+ shouldTrack = true;
2946
+ }
2947
+ }
2948
+ }
2789
2949
  } catch (err) {
2790
2950
  logger$3.critical('Error while calculating scroll percentage', err);
2791
2951
  }
2792
- this.mp.track(MP_EV_SCROLL, props);
2952
+ if (shouldTrack) {
2953
+ this.mp.track(MP_EV_SCROLL, props);
2954
+ }
2793
2955
  }.bind(this)));
2794
2956
  };
2795
2957
 
@@ -5241,8 +5403,12 @@
5241
5403
  if (!(k in union_q)) {
5242
5404
  union_q[k] = [];
5243
5405
  }
5244
- // We may send duplicates, the server will dedup them.
5245
- union_q[k] = union_q[k].concat(v);
5406
+ // Prevent duplicate values
5407
+ _.each(v, function(item) {
5408
+ if (!_.include(union_q[k], item)) {
5409
+ union_q[k].push(item);
5410
+ }
5411
+ });
5246
5412
  }
5247
5413
  });
5248
5414
  this._pop_from_people_queue(UNSET_ACTION, q_data);