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.
@@ -4513,7 +4513,7 @@
4513
4513
 
4514
4514
  var Config = {
4515
4515
  DEBUG: false,
4516
- LIB_VERSION: '2.59.0'
4516
+ LIB_VERSION: '2.60.0'
4517
4517
  };
4518
4518
 
4519
4519
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -8268,7 +8268,7 @@
8268
8268
  }
8269
8269
  }
8270
8270
 
8271
- function getPropertiesFromElement(el) {
8271
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
8272
8272
  var props = {
8273
8273
  '$classes': getClassName(el).split(' '),
8274
8274
  '$tag_name': el.tagName.toLowerCase()
@@ -8278,9 +8278,9 @@
8278
8278
  props['$id'] = elId;
8279
8279
  }
8280
8280
 
8281
- if (shouldTrackElement(el)) {
8282
- _.each(TRACKED_ATTRS, function(attr) {
8283
- if (el.hasAttribute(attr)) {
8281
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
8282
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
8283
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
8284
8284
  var attrVal = el.getAttribute(attr);
8285
8285
  if (shouldTrackValue(attrVal)) {
8286
8286
  props['$attr-' + attr] = attrVal;
@@ -8304,8 +8304,21 @@
8304
8304
  return props;
8305
8305
  }
8306
8306
 
8307
- function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
8308
- blockSelectors = blockSelectors || [];
8307
+ function getPropsForDOMEvent(ev, config) {
8308
+ var allowElementCallback = config.allowElementCallback;
8309
+ var allowSelectors = config.allowSelectors || [];
8310
+ var blockAttrs = config.blockAttrs || [];
8311
+ var blockElementCallback = config.blockElementCallback;
8312
+ var blockSelectors = config.blockSelectors || [];
8313
+ var captureTextContent = config.captureTextContent || false;
8314
+ var captureExtraAttrs = config.captureExtraAttrs || [];
8315
+
8316
+ // convert array to set every time, as the config may have changed
8317
+ var blockAttrsSet = {};
8318
+ _.each(blockAttrs, function(attr) {
8319
+ blockAttrsSet[attr] = true;
8320
+ });
8321
+
8309
8322
  var props = null;
8310
8323
 
8311
8324
  var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
@@ -8313,7 +8326,11 @@
8313
8326
  target = target.parentNode;
8314
8327
  }
8315
8328
 
8316
- if (shouldTrackDomEvent(target, ev)) {
8329
+ if (
8330
+ shouldTrackDomEvent(target, ev) &&
8331
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
8332
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
8333
+ ) {
8317
8334
  var targetElementList = [target];
8318
8335
  var curEl = target;
8319
8336
  while (curEl.parentNode && !isTag(curEl, 'body')) {
@@ -8324,37 +8341,20 @@
8324
8341
  var elementsJson = [];
8325
8342
  var href, explicitNoTrack = false;
8326
8343
  _.each(targetElementList, function(el) {
8327
- var shouldTrackEl = shouldTrackElement(el);
8344
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
8328
8345
 
8329
8346
  // if the element or a parent element is an anchor tag
8330
8347
  // include the href as a property
8331
- if (el.tagName.toLowerCase() === 'a') {
8348
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
8332
8349
  href = el.getAttribute('href');
8333
- href = shouldTrackEl && shouldTrackValue(href) && href;
8350
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
8334
8351
  }
8335
8352
 
8336
- // allow users to programmatically prevent tracking of elements by adding classes such as 'mp-no-track'
8337
- var classes = getClasses(el);
8338
- _.each(OPT_OUT_CLASSES, function(cls) {
8339
- if (classes[cls]) {
8340
- explicitNoTrack = true;
8341
- }
8342
- });
8343
-
8344
- if (!explicitNoTrack) {
8345
- // programmatically prevent tracking of elements that match CSS selectors
8346
- _.each(blockSelectors, function(sel) {
8347
- try {
8348
- if (el['matches'](sel)) {
8349
- explicitNoTrack = true;
8350
- }
8351
- } catch (err) {
8352
- logger.critical('Error while checking selector: ' + sel, err);
8353
- }
8354
- });
8353
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
8354
+ explicitNoTrack = true;
8355
8355
  }
8356
8356
 
8357
- elementsJson.push(getPropertiesFromElement(el));
8357
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
8358
8358
  }, this);
8359
8359
 
8360
8360
  if (!explicitNoTrack) {
@@ -8368,9 +8368,17 @@
8368
8368
  '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
8369
8369
  '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
8370
8370
  };
8371
+ _.each(captureExtraAttrs, function(attr) {
8372
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
8373
+ var attrVal = target.getAttribute(attr);
8374
+ if (shouldTrackValue(attrVal)) {
8375
+ props['$el_attr__' + attr] = attrVal;
8376
+ }
8377
+ }
8378
+ });
8371
8379
 
8372
8380
  if (captureTextContent) {
8373
- elementText = getSafeText(target);
8381
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
8374
8382
  if (elementText && elementText.length) {
8375
8383
  props['$el_text'] = elementText;
8376
8384
  }
@@ -8386,14 +8394,22 @@
8386
8394
  }
8387
8395
  // prioritize text content from "real" click target if different from original target
8388
8396
  if (captureTextContent) {
8389
- var elementText = getSafeText(target);
8397
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
8390
8398
  if (elementText && elementText.length) {
8391
8399
  props['$el_text'] = elementText;
8392
8400
  }
8393
8401
  }
8394
8402
 
8395
8403
  if (target) {
8396
- var targetProps = getPropertiesFromElement(target);
8404
+ // target may have been recalculated; check allowlists and blocklists again
8405
+ if (
8406
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
8407
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
8408
+ ) {
8409
+ return null;
8410
+ }
8411
+
8412
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
8397
8413
  props['$target'] = targetProps;
8398
8414
  // pull up more props onto main event props
8399
8415
  props['$el_classes'] = targetProps['$classes'];
@@ -8409,19 +8425,20 @@
8409
8425
  }
8410
8426
 
8411
8427
 
8412
- /*
8428
+ /**
8413
8429
  * Get the direct text content of an element, protecting against sensitive data collection.
8414
8430
  * Concats textContent of each of the element's text node children; this avoids potential
8415
8431
  * collection of sensitive data that could happen if we used element.textContent and the
8416
8432
  * element had sensitive child elements, since element.textContent includes child content.
8417
8433
  * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
8418
8434
  * @param {Element} el - element to get the text of
8435
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
8419
8436
  * @returns {string} the element's direct text content
8420
8437
  */
8421
- function getSafeText(el) {
8438
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
8422
8439
  var elText = '';
8423
8440
 
8424
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
8441
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
8425
8442
  _.each(el.childNodes, function(child) {
8426
8443
  if (isTextNode(child) && child.textContent) {
8427
8444
  elText += _.trim(child.textContent)
@@ -8460,6 +8477,75 @@
8460
8477
  return target;
8461
8478
  }
8462
8479
 
8480
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
8481
+ if (allowElementCallback) {
8482
+ try {
8483
+ if (!allowElementCallback(el, ev)) {
8484
+ return false;
8485
+ }
8486
+ } catch (err) {
8487
+ logger.critical('Error while checking element in allowElementCallback', err);
8488
+ return false;
8489
+ }
8490
+ }
8491
+
8492
+ if (!allowSelectors.length) {
8493
+ // no allowlist; all elements are fair game
8494
+ return true;
8495
+ }
8496
+
8497
+ for (var i = 0; i < allowSelectors.length; i++) {
8498
+ var sel = allowSelectors[i];
8499
+ try {
8500
+ if (el['matches'](sel)) {
8501
+ return true;
8502
+ }
8503
+ } catch (err) {
8504
+ logger.critical('Error while checking selector: ' + sel, err);
8505
+ }
8506
+ }
8507
+ return false;
8508
+ }
8509
+
8510
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
8511
+ var i;
8512
+
8513
+ if (blockElementCallback) {
8514
+ try {
8515
+ if (blockElementCallback(el, ev)) {
8516
+ return true;
8517
+ }
8518
+ } catch (err) {
8519
+ logger.critical('Error while checking element in blockElementCallback', err);
8520
+ return true;
8521
+ }
8522
+ }
8523
+
8524
+ if (blockSelectors && blockSelectors.length) {
8525
+ // programmatically prevent tracking of elements that match CSS selectors
8526
+ for (i = 0; i < blockSelectors.length; i++) {
8527
+ var sel = blockSelectors[i];
8528
+ try {
8529
+ if (el['matches'](sel)) {
8530
+ return true;
8531
+ }
8532
+ } catch (err) {
8533
+ logger.critical('Error while checking selector: ' + sel, err);
8534
+ }
8535
+ }
8536
+ }
8537
+
8538
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
8539
+ var classes = getClasses(el);
8540
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
8541
+ if (classes[OPT_OUT_CLASSES[i]]) {
8542
+ return true;
8543
+ }
8544
+ }
8545
+
8546
+ return false;
8547
+ }
8548
+
8463
8549
  /*
8464
8550
  * Check whether a DOM node has nodeType Node.ELEMENT_NODE
8465
8551
  * @param {Node} node - node to check
@@ -8534,11 +8620,16 @@
8534
8620
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
8535
8621
  * using a variety of heuristics.
8536
8622
  * @param {Element} el - element to check
8623
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
8537
8624
  * @returns {boolean} whether the element should be tracked
8538
8625
  */
8539
- function shouldTrackElement(el) {
8626
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
8540
8627
  var i;
8541
8628
 
8629
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
8630
+ return false;
8631
+ }
8632
+
8542
8633
  for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
8543
8634
  var classes = getClasses(curEl);
8544
8635
  for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
@@ -8628,9 +8719,17 @@
8628
8719
  var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
8629
8720
  var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
8630
8721
 
8722
+ var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
8723
+ var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
8724
+ var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
8725
+ var CONFIG_BLOCK_ATTRS = 'block_attrs';
8726
+ var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
8631
8727
  var CONFIG_BLOCK_SELECTORS = 'block_selectors';
8632
8728
  var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
8729
+ var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
8633
8730
  var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
8731
+ var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
8732
+ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
8634
8733
  var CONFIG_TRACK_CLICK = 'click';
8635
8734
  var CONFIG_TRACK_INPUT = 'input';
8636
8735
  var CONFIG_TRACK_PAGEVIEW = 'pageview';
@@ -8638,7 +8737,16 @@
8638
8737
  var CONFIG_TRACK_SUBMIT = 'submit';
8639
8738
 
8640
8739
  var CONFIG_DEFAULTS = {};
8740
+ CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
8741
+ CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
8742
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
8743
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
8744
+ CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
8745
+ CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
8746
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
8641
8747
  CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
8748
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
8749
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
8642
8750
  CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
8643
8751
  CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
8644
8752
  CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
@@ -8693,13 +8801,37 @@
8693
8801
  };
8694
8802
 
8695
8803
  Autocapture.prototype.currentUrlBlocked = function() {
8804
+ var i;
8805
+ var currentUrl = _.info.currentUrl();
8806
+
8807
+ var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
8808
+ if (allowUrlRegexes.length) {
8809
+ // we're using an allowlist, only track if current URL matches
8810
+ var allowed = false;
8811
+ for (i = 0; i < allowUrlRegexes.length; i++) {
8812
+ var allowRegex = allowUrlRegexes[i];
8813
+ try {
8814
+ if (currentUrl.match(allowRegex)) {
8815
+ allowed = true;
8816
+ break;
8817
+ }
8818
+ } catch (err) {
8819
+ logger.critical('Error while checking block URL regex: ' + allowRegex, err);
8820
+ return true;
8821
+ }
8822
+ }
8823
+ if (!allowed) {
8824
+ // wasn't allowed by any regex
8825
+ return true;
8826
+ }
8827
+ }
8828
+
8696
8829
  var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
8697
8830
  if (!blockUrlRegexes || !blockUrlRegexes.length) {
8698
8831
  return false;
8699
8832
  }
8700
8833
 
8701
- var currentUrl = _.info.currentUrl();
8702
- for (var i = 0; i < blockUrlRegexes.length; i++) {
8834
+ for (i = 0; i < blockUrlRegexes.length; i++) {
8703
8835
  try {
8704
8836
  if (currentUrl.match(blockUrlRegexes[i])) {
8705
8837
  return true;
@@ -8727,11 +8859,15 @@
8727
8859
  return;
8728
8860
  }
8729
8861
 
8730
- var props = getPropsForDOMEvent(
8731
- ev,
8732
- this.getConfig(CONFIG_BLOCK_SELECTORS),
8733
- this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
8734
- );
8862
+ var props = getPropsForDOMEvent(ev, {
8863
+ allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
8864
+ allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
8865
+ blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
8866
+ blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
8867
+ blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
8868
+ captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
8869
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
8870
+ });
8735
8871
  if (props) {
8736
8872
  _.extend(props, DEFAULT_PROPS);
8737
8873
  this.mp.track(mpEventName, props);
@@ -8816,13 +8952,14 @@
8816
8952
 
8817
8953
  var currentUrl = _.info.currentUrl();
8818
8954
  var shouldTrack = false;
8955
+ var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
8819
8956
  var trackPageviewOption = this.pageviewTrackingConfig();
8820
8957
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
8821
8958
  shouldTrack = currentUrl !== previousTrackedUrl;
8822
8959
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
8823
8960
  shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
8824
8961
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
8825
- shouldTrack = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
8962
+ shouldTrack = didPathChange;
8826
8963
  }
8827
8964
 
8828
8965
  if (shouldTrack) {
@@ -8830,6 +8967,10 @@
8830
8967
  if (tracked) {
8831
8968
  previousTrackedUrl = currentUrl;
8832
8969
  }
8970
+ if (didPathChange) {
8971
+ this.lastScrollCheckpoint = 0;
8972
+ logger.log('Path change: re-initializing scroll depth checkpoints');
8973
+ }
8833
8974
  }
8834
8975
  }.bind(this)));
8835
8976
  };
@@ -8841,6 +8982,7 @@
8841
8982
  return;
8842
8983
  }
8843
8984
  logger.log('Initializing scroll tracking');
8985
+ this.lastScrollCheckpoint = 0;
8844
8986
 
8845
8987
  this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
8846
8988
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
@@ -8850,6 +8992,11 @@
8850
8992
  return;
8851
8993
  }
8852
8994
 
8995
+ var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
8996
+ var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
8997
+ .slice()
8998
+ .sort(function(a, b) { return a - b; });
8999
+
8853
9000
  var scrollTop = win.scrollY;
8854
9001
  var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
8855
9002
  try {
@@ -8857,10 +9004,25 @@
8857
9004
  var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
8858
9005
  props['$scroll_height'] = scrollHeight;
8859
9006
  props['$scroll_percentage'] = scrollPercentage;
9007
+ if (scrollPercentage > this.lastScrollCheckpoint) {
9008
+ for (var i = 0; i < scrollCheckpoints.length; i++) {
9009
+ var checkpoint = scrollCheckpoints[i];
9010
+ if (
9011
+ scrollPercentage >= checkpoint &&
9012
+ this.lastScrollCheckpoint < checkpoint
9013
+ ) {
9014
+ props['$scroll_checkpoint'] = checkpoint;
9015
+ this.lastScrollCheckpoint = checkpoint;
9016
+ shouldTrack = true;
9017
+ }
9018
+ }
9019
+ }
8860
9020
  } catch (err) {
8861
9021
  logger.critical('Error while calculating scroll percentage', err);
8862
9022
  }
8863
- this.mp.track(MP_EV_SCROLL, props);
9023
+ if (shouldTrack) {
9024
+ this.mp.track(MP_EV_SCROLL, props);
9025
+ }
8864
9026
  }.bind(this)));
8865
9027
  };
8866
9028
 
@@ -10129,8 +10291,12 @@
10129
10291
  if (!(k in union_q)) {
10130
10292
  union_q[k] = [];
10131
10293
  }
10132
- // We may send duplicates, the server will dedup them.
10133
- union_q[k] = union_q[k].concat(v);
10294
+ // Prevent duplicate values
10295
+ _.each(v, function(item) {
10296
+ if (!_.include(union_q[k], item)) {
10297
+ union_q[k].push(item);
10298
+ }
10299
+ });
10134
10300
  }
10135
10301
  });
10136
10302
  this._pop_from_people_queue(UNSET_ACTION, q_data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.59.0",
3
+ "version": "2.60.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",
@@ -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
- var currentUrl = _.info.currentUrl();
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
- ev,
117
- this.getConfig(CONFIG_BLOCK_SELECTORS),
118
- this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
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 = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
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
- this.mp.track(MP_EV_SCROLL, props);
317
+ if (shouldTrack) {
318
+ this.mp.track(MP_EV_SCROLL, props);
319
+ }
249
320
  }.bind(this)));
250
321
  };
251
322