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.
@@ -4509,7 +4509,7 @@ var IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {
4509
4509
 
4510
4510
  var Config = {
4511
4511
  DEBUG: false,
4512
- LIB_VERSION: '2.58.0'
4512
+ LIB_VERSION: '2.60.0'
4513
4513
  };
4514
4514
 
4515
4515
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
@@ -4521,11 +4521,14 @@ if (typeof(window) === 'undefined') {
4521
4521
  win = {
4522
4522
  navigator: { userAgent: '', onLine: true },
4523
4523
  document: {
4524
+ createElement: function() { return {}; },
4524
4525
  location: loc,
4525
4526
  referrer: ''
4526
4527
  },
4527
4528
  screen: { width: 0, height: 0 },
4528
- location: loc
4529
+ location: loc,
4530
+ addEventListener: function() {},
4531
+ removeEventListener: function() {}
4529
4532
  };
4530
4533
  } else {
4531
4534
  win = window;
@@ -5002,6 +5005,29 @@ var console_with_prefix = function(prefix) {
5002
5005
  };
5003
5006
 
5004
5007
 
5008
+ var safewrap = function(f) {
5009
+ return function() {
5010
+ try {
5011
+ return f.apply(this, arguments);
5012
+ } catch (e) {
5013
+ console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
5014
+ if (Config.DEBUG){
5015
+ console$1.critical(e);
5016
+ }
5017
+ }
5018
+ };
5019
+ };
5020
+
5021
+ var safewrapClass = function(klass) {
5022
+ var proto = klass.prototype;
5023
+ for (var func in proto) {
5024
+ if (typeof(proto[func]) === 'function') {
5025
+ proto[func] = safewrap(proto[func]);
5026
+ }
5027
+ }
5028
+ };
5029
+
5030
+
5005
5031
  // UNDERSCORE
5006
5032
  // Embed part of the Underscore Library
5007
5033
  _.bind = function(func, context) {
@@ -5780,6 +5806,7 @@ _.UUID = (function() {
5780
5806
  var BLOCKED_UA_STRS = [
5781
5807
  'ahrefsbot',
5782
5808
  'ahrefssiteaudit',
5809
+ 'amazonbot',
5783
5810
  'baiduspider',
5784
5811
  'bingbot',
5785
5812
  'bingpreview',
@@ -5789,7 +5816,7 @@ var BLOCKED_UA_STRS = [
5789
5816
  'pinterest',
5790
5817
  'screaming frog',
5791
5818
  'yahoo! slurp',
5792
- 'yandexbot',
5819
+ 'yandex',
5793
5820
 
5794
5821
  // a whole bunch of goog-specific crawlers
5795
5822
  // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
@@ -6909,7 +6936,7 @@ function _addOptOutCheck(method, getConfigValue) {
6909
6936
  };
6910
6937
  }
6911
6938
 
6912
- var logger$4 = console_with_prefix('lock');
6939
+ var logger$5 = console_with_prefix('lock');
6913
6940
 
6914
6941
  /**
6915
6942
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -6962,7 +6989,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
6962
6989
 
6963
6990
  var delay = function(cb) {
6964
6991
  if (new Date().getTime() - startTime > timeoutMS) {
6965
- logger$4.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
6992
+ logger$5.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
6966
6993
  storage.removeItem(keyZ);
6967
6994
  storage.removeItem(keyY);
6968
6995
  loop();
@@ -7109,7 +7136,7 @@ LocalStorageWrapper.prototype.removeItem = function (key) {
7109
7136
  }, this));
7110
7137
  };
7111
7138
 
7112
- var logger$3 = console_with_prefix('batch');
7139
+ var logger$4 = console_with_prefix('batch');
7113
7140
 
7114
7141
  /**
7115
7142
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -7136,7 +7163,7 @@ var RequestQueue = function (storageKey, options) {
7136
7163
  this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
7137
7164
  this.queueStorage.init();
7138
7165
  }
7139
- this.reportError = options.errorReporter || _.bind(logger$3.error, logger$3);
7166
+ this.reportError = options.errorReporter || _.bind(logger$4.error, logger$4);
7140
7167
 
7141
7168
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
7142
7169
 
@@ -7466,7 +7493,7 @@ RequestQueue.prototype.clear = function () {
7466
7493
  // maximum interval between request retries after exponential backoff
7467
7494
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
7468
7495
 
7469
- var logger$2 = console_with_prefix('batch');
7496
+ var logger$3 = console_with_prefix('batch');
7470
7497
 
7471
7498
  /**
7472
7499
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -7590,7 +7617,7 @@ RequestBatcher.prototype.sendRequestPromise = function(data, options) {
7590
7617
  */
7591
7618
  RequestBatcher.prototype.flush = function(options) {
7592
7619
  if (this.requestInProgress) {
7593
- logger$2.log('Flush: Request already in progress');
7620
+ logger$3.log('Flush: Request already in progress');
7594
7621
  return PromisePolyfill.resolve();
7595
7622
  }
7596
7623
 
@@ -7767,7 +7794,7 @@ RequestBatcher.prototype.flush = function(options) {
7767
7794
  if (options.unloading) {
7768
7795
  requestOptions.transport = 'sendBeacon';
7769
7796
  }
7770
- logger$2.log('MIXPANEL REQUEST:', dataForRequest);
7797
+ logger$3.log('MIXPANEL REQUEST:', dataForRequest);
7771
7798
  return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
7772
7799
  }, this))
7773
7800
  .catch(_.bind(function(err) {
@@ -7780,7 +7807,7 @@ RequestBatcher.prototype.flush = function(options) {
7780
7807
  * Log error to global logger and optional user-defined logger.
7781
7808
  */
7782
7809
  RequestBatcher.prototype.reportError = function(msg, err) {
7783
- logger$2.error.apply(logger$2.error, arguments);
7810
+ logger$3.error.apply(logger$3.error, arguments);
7784
7811
  if (this.errorReporter) {
7785
7812
  try {
7786
7813
  if (!(err instanceof Error)) {
@@ -7788,12 +7815,12 @@ RequestBatcher.prototype.reportError = function(msg, err) {
7788
7815
  }
7789
7816
  this.errorReporter(msg, err);
7790
7817
  } catch(err) {
7791
- logger$2.error(err);
7818
+ logger$3.error(err);
7792
7819
  }
7793
7820
  }
7794
7821
  };
7795
7822
 
7796
- var logger$1 = console_with_prefix('recorder');
7823
+ var logger$2 = console_with_prefix('recorder');
7797
7824
  var CompressionStream = win['CompressionStream'];
7798
7825
 
7799
7826
  var RECORDER_BATCHER_LIB_CONFIG = {
@@ -7874,20 +7901,20 @@ SessionRecording.prototype.get_config = function(configVar) {
7874
7901
 
7875
7902
  SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
7876
7903
  if (this._stopRecording !== null) {
7877
- logger$1.log('Recording already in progress, skipping startRecording.');
7904
+ logger$2.log('Recording already in progress, skipping startRecording.');
7878
7905
  return;
7879
7906
  }
7880
7907
 
7881
7908
  this.recordMaxMs = this.getConfig('record_max_ms');
7882
7909
  if (this.recordMaxMs > MAX_RECORDING_MS) {
7883
7910
  this.recordMaxMs = MAX_RECORDING_MS;
7884
- logger$1.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7911
+ logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
7885
7912
  }
7886
7913
 
7887
7914
  this.recordMinMs = this.getConfig('record_min_ms');
7888
7915
  if (this.recordMinMs > MAX_VALUE_FOR_MIN_RECORDING_MS) {
7889
7916
  this.recordMinMs = MAX_VALUE_FOR_MIN_RECORDING_MS;
7890
- logger$1.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7917
+ logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
7891
7918
  }
7892
7919
 
7893
7920
  this.replayStartTime = new Date().getTime();
@@ -8090,18 +8117,18 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
8090
8117
 
8091
8118
 
8092
8119
  SessionRecording.prototype.reportError = function(msg, err) {
8093
- logger$1.error.apply(logger$1.error, arguments);
8120
+ logger$2.error.apply(logger$2.error, arguments);
8094
8121
  try {
8095
8122
  if (!err && !(msg instanceof Error)) {
8096
8123
  msg = new Error(msg);
8097
8124
  }
8098
8125
  this.getConfig('error_reporter')(msg, err);
8099
8126
  } catch(err) {
8100
- logger$1.error(err);
8127
+ logger$2.error(err);
8101
8128
  }
8102
8129
  };
8103
8130
 
8104
- var logger = console_with_prefix('recorder');
8131
+ var logger$1 = console_with_prefix('recorder');
8105
8132
 
8106
8133
  /**
8107
8134
  * Recorder API: manages recordings and exposes methods public to the core Mixpanel library.
@@ -8114,17 +8141,17 @@ var MixpanelRecorder = function(mixpanelInstance) {
8114
8141
 
8115
8142
  MixpanelRecorder.prototype.startRecording = function(shouldStopBatcher) {
8116
8143
  if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
8117
- logger.log('Recording already in progress, skipping startRecording.');
8144
+ logger$1.log('Recording already in progress, skipping startRecording.');
8118
8145
  return;
8119
8146
  }
8120
8147
 
8121
8148
  var onIdleTimeout = _.bind(function () {
8122
- logger.log('Idle timeout reached, restarting recording.');
8149
+ logger$1.log('Idle timeout reached, restarting recording.');
8123
8150
  this.resetRecording();
8124
8151
  }, this);
8125
8152
 
8126
8153
  var onMaxLengthReached = _.bind(function () {
8127
- logger.log('Max recording length reached, stopping recording.');
8154
+ logger$1.log('Max recording length reached, stopping recording.');
8128
8155
  this.resetRecording();
8129
8156
  }, this);
8130
8157
 
@@ -8169,6 +8196,851 @@ Object.defineProperty(MixpanelRecorder.prototype, 'replayId', {
8169
8196
 
8170
8197
  win['__mp_recorder'] = MixpanelRecorder;
8171
8198
 
8199
+ // stateless utils
8200
+
8201
+ var EV_CHANGE = 'change';
8202
+ var EV_CLICK = 'click';
8203
+ var EV_HASHCHANGE = 'hashchange';
8204
+ var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
8205
+ var EV_POPSTATE = 'popstate';
8206
+ // TODO scrollend isn't available in Safari: document or polyfill?
8207
+ var EV_SCROLLEND = 'scrollend';
8208
+ var EV_SUBMIT = 'submit';
8209
+
8210
+ var CLICK_EVENT_PROPS = [
8211
+ 'clientX', 'clientY',
8212
+ 'offsetX', 'offsetY',
8213
+ 'pageX', 'pageY',
8214
+ 'screenX', 'screenY',
8215
+ 'x', 'y'
8216
+ ];
8217
+ var OPT_IN_CLASSES = ['mp-include'];
8218
+ var OPT_OUT_CLASSES = ['mp-no-track'];
8219
+ var SENSITIVE_DATA_CLASSES = OPT_OUT_CLASSES.concat(['mp-sensitive']);
8220
+ var TRACKED_ATTRS = [
8221
+ 'aria-label', 'aria-labelledby', 'aria-describedby',
8222
+ 'href', 'name', 'role', 'title', 'type'
8223
+ ];
8224
+
8225
+ var logger = console_with_prefix('autocapture');
8226
+
8227
+
8228
+ function getClasses(el) {
8229
+ var classes = {};
8230
+ var classList = getClassName(el).split(' ');
8231
+ for (var i = 0; i < classList.length; i++) {
8232
+ var cls = classList[i];
8233
+ if (cls) {
8234
+ classes[cls] = true;
8235
+ }
8236
+ }
8237
+ return classes;
8238
+ }
8239
+
8240
+ /*
8241
+ * Get the className of an element, accounting for edge cases where element.className is an object
8242
+ * @param {Element} el - element to get the className of
8243
+ * @returns {string} the element's class
8244
+ */
8245
+ function getClassName(el) {
8246
+ switch(typeof el.className) {
8247
+ case 'string':
8248
+ return el.className;
8249
+ case 'object': // handle cases where className might be SVGAnimatedString or some other type
8250
+ return el.className.baseVal || el.getAttribute('class') || '';
8251
+ default: // future proof
8252
+ return '';
8253
+ }
8254
+ }
8255
+
8256
+ function getPreviousElementSibling(el) {
8257
+ if (el.previousElementSibling) {
8258
+ return el.previousElementSibling;
8259
+ } else {
8260
+ do {
8261
+ el = el.previousSibling;
8262
+ } while (el && !isElementNode(el));
8263
+ return el;
8264
+ }
8265
+ }
8266
+
8267
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
8268
+ var props = {
8269
+ '$classes': getClassName(el).split(' '),
8270
+ '$tag_name': el.tagName.toLowerCase()
8271
+ };
8272
+ var elId = el.id;
8273
+ if (elId) {
8274
+ props['$id'] = elId;
8275
+ }
8276
+
8277
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
8278
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
8279
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
8280
+ var attrVal = el.getAttribute(attr);
8281
+ if (shouldTrackValue(attrVal)) {
8282
+ props['$attr-' + attr] = attrVal;
8283
+ }
8284
+ }
8285
+ });
8286
+ }
8287
+
8288
+ var nthChild = 1;
8289
+ var nthOfType = 1;
8290
+ var currentElem = el;
8291
+ while (currentElem = getPreviousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
8292
+ nthChild++;
8293
+ if (currentElem.tagName === el.tagName) {
8294
+ nthOfType++;
8295
+ }
8296
+ }
8297
+ props['$nth_child'] = nthChild;
8298
+ props['$nth_of_type'] = nthOfType;
8299
+
8300
+ return props;
8301
+ }
8302
+
8303
+ function getPropsForDOMEvent(ev, config) {
8304
+ var allowElementCallback = config.allowElementCallback;
8305
+ var allowSelectors = config.allowSelectors || [];
8306
+ var blockAttrs = config.blockAttrs || [];
8307
+ var blockElementCallback = config.blockElementCallback;
8308
+ var blockSelectors = config.blockSelectors || [];
8309
+ var captureTextContent = config.captureTextContent || false;
8310
+ var captureExtraAttrs = config.captureExtraAttrs || [];
8311
+
8312
+ // convert array to set every time, as the config may have changed
8313
+ var blockAttrsSet = {};
8314
+ _.each(blockAttrs, function(attr) {
8315
+ blockAttrsSet[attr] = true;
8316
+ });
8317
+
8318
+ var props = null;
8319
+
8320
+ var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
8321
+ if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
8322
+ target = target.parentNode;
8323
+ }
8324
+
8325
+ if (
8326
+ shouldTrackDomEvent(target, ev) &&
8327
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
8328
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
8329
+ ) {
8330
+ var targetElementList = [target];
8331
+ var curEl = target;
8332
+ while (curEl.parentNode && !isTag(curEl, 'body')) {
8333
+ targetElementList.push(curEl.parentNode);
8334
+ curEl = curEl.parentNode;
8335
+ }
8336
+
8337
+ var elementsJson = [];
8338
+ var href, explicitNoTrack = false;
8339
+ _.each(targetElementList, function(el) {
8340
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
8341
+
8342
+ // if the element or a parent element is an anchor tag
8343
+ // include the href as a property
8344
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
8345
+ href = el.getAttribute('href');
8346
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
8347
+ }
8348
+
8349
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
8350
+ explicitNoTrack = true;
8351
+ }
8352
+
8353
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
8354
+ }, this);
8355
+
8356
+ if (!explicitNoTrack) {
8357
+ var docElement = document$1['documentElement'];
8358
+ props = {
8359
+ '$event_type': ev.type,
8360
+ '$host': win.location.host,
8361
+ '$pathname': win.location.pathname,
8362
+ '$elements': elementsJson,
8363
+ '$el_attr__href': href,
8364
+ '$viewportHeight': Math.max(docElement['clientHeight'], win['innerHeight'] || 0),
8365
+ '$viewportWidth': Math.max(docElement['clientWidth'], win['innerWidth'] || 0)
8366
+ };
8367
+ _.each(captureExtraAttrs, function(attr) {
8368
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
8369
+ var attrVal = target.getAttribute(attr);
8370
+ if (shouldTrackValue(attrVal)) {
8371
+ props['$el_attr__' + attr] = attrVal;
8372
+ }
8373
+ }
8374
+ });
8375
+
8376
+ if (captureTextContent) {
8377
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
8378
+ if (elementText && elementText.length) {
8379
+ props['$el_text'] = elementText;
8380
+ }
8381
+ }
8382
+
8383
+ if (ev.type === EV_CLICK) {
8384
+ _.each(CLICK_EVENT_PROPS, function(prop) {
8385
+ if (prop in ev) {
8386
+ props['$' + prop] = ev[prop];
8387
+ }
8388
+ });
8389
+ target = guessRealClickTarget(ev);
8390
+ }
8391
+ // prioritize text content from "real" click target if different from original target
8392
+ if (captureTextContent) {
8393
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
8394
+ if (elementText && elementText.length) {
8395
+ props['$el_text'] = elementText;
8396
+ }
8397
+ }
8398
+
8399
+ if (target) {
8400
+ // target may have been recalculated; check allowlists and blocklists again
8401
+ if (
8402
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
8403
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
8404
+ ) {
8405
+ return null;
8406
+ }
8407
+
8408
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
8409
+ props['$target'] = targetProps;
8410
+ // pull up more props onto main event props
8411
+ props['$el_classes'] = targetProps['$classes'];
8412
+ _.extend(props, _.strip_empty_properties({
8413
+ '$el_id': targetProps['$id'],
8414
+ '$el_tag_name': targetProps['$tag_name']
8415
+ }));
8416
+ }
8417
+ }
8418
+ }
8419
+
8420
+ return props;
8421
+ }
8422
+
8423
+
8424
+ /**
8425
+ * Get the direct text content of an element, protecting against sensitive data collection.
8426
+ * Concats textContent of each of the element's text node children; this avoids potential
8427
+ * collection of sensitive data that could happen if we used element.textContent and the
8428
+ * element had sensitive child elements, since element.textContent includes child content.
8429
+ * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
8430
+ * @param {Element} el - element to get the text of
8431
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
8432
+ * @returns {string} the element's direct text content
8433
+ */
8434
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
8435
+ var elText = '';
8436
+
8437
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
8438
+ _.each(el.childNodes, function(child) {
8439
+ if (isTextNode(child) && child.textContent) {
8440
+ elText += _.trim(child.textContent)
8441
+ // scrub potentially sensitive values
8442
+ .split(/(\s+)/).filter(shouldTrackValue).join('')
8443
+ // normalize whitespace
8444
+ .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
8445
+ // truncate
8446
+ .substring(0, 255);
8447
+ }
8448
+ });
8449
+ }
8450
+
8451
+ return _.trim(elText);
8452
+ }
8453
+
8454
+ function guessRealClickTarget(ev) {
8455
+ var target = ev.target;
8456
+ var composedPath = ev['composedPath']();
8457
+ for (var i = 0; i < composedPath.length; i++) {
8458
+ var node = composedPath[i];
8459
+ if (
8460
+ isTag(node, 'a') ||
8461
+ isTag(node, 'button') ||
8462
+ isTag(node, 'input') ||
8463
+ isTag(node, 'select') ||
8464
+ (node.getAttribute && node.getAttribute('role') === 'button')
8465
+ ) {
8466
+ target = node;
8467
+ break;
8468
+ }
8469
+ if (node === target) {
8470
+ break;
8471
+ }
8472
+ }
8473
+ return target;
8474
+ }
8475
+
8476
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
8477
+ if (allowElementCallback) {
8478
+ try {
8479
+ if (!allowElementCallback(el, ev)) {
8480
+ return false;
8481
+ }
8482
+ } catch (err) {
8483
+ logger.critical('Error while checking element in allowElementCallback', err);
8484
+ return false;
8485
+ }
8486
+ }
8487
+
8488
+ if (!allowSelectors.length) {
8489
+ // no allowlist; all elements are fair game
8490
+ return true;
8491
+ }
8492
+
8493
+ for (var i = 0; i < allowSelectors.length; i++) {
8494
+ var sel = allowSelectors[i];
8495
+ try {
8496
+ if (el['matches'](sel)) {
8497
+ return true;
8498
+ }
8499
+ } catch (err) {
8500
+ logger.critical('Error while checking selector: ' + sel, err);
8501
+ }
8502
+ }
8503
+ return false;
8504
+ }
8505
+
8506
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
8507
+ var i;
8508
+
8509
+ if (blockElementCallback) {
8510
+ try {
8511
+ if (blockElementCallback(el, ev)) {
8512
+ return true;
8513
+ }
8514
+ } catch (err) {
8515
+ logger.critical('Error while checking element in blockElementCallback', err);
8516
+ return true;
8517
+ }
8518
+ }
8519
+
8520
+ if (blockSelectors && blockSelectors.length) {
8521
+ // programmatically prevent tracking of elements that match CSS selectors
8522
+ for (i = 0; i < blockSelectors.length; i++) {
8523
+ var sel = blockSelectors[i];
8524
+ try {
8525
+ if (el['matches'](sel)) {
8526
+ return true;
8527
+ }
8528
+ } catch (err) {
8529
+ logger.critical('Error while checking selector: ' + sel, err);
8530
+ }
8531
+ }
8532
+ }
8533
+
8534
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
8535
+ var classes = getClasses(el);
8536
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
8537
+ if (classes[OPT_OUT_CLASSES[i]]) {
8538
+ return true;
8539
+ }
8540
+ }
8541
+
8542
+ return false;
8543
+ }
8544
+
8545
+ /*
8546
+ * Check whether a DOM node has nodeType Node.ELEMENT_NODE
8547
+ * @param {Node} node - node to check
8548
+ * @returns {boolean} whether node is of the correct nodeType
8549
+ */
8550
+ function isElementNode(node) {
8551
+ return node && node.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
8552
+ }
8553
+
8554
+ /*
8555
+ * Check whether an element is of a given tag type.
8556
+ * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
8557
+ * we want to match tagNames instead of specific references because something like
8558
+ * element === document.body won't always work because element might not be a native
8559
+ * element.
8560
+ * @param {Element} el - element to check
8561
+ * @param {string} tag - tag name (e.g., "div")
8562
+ * @returns {boolean} whether el is of the given tag type
8563
+ */
8564
+ function isTag(el, tag) {
8565
+ return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
8566
+ }
8567
+
8568
+ /*
8569
+ * Check whether a DOM node is a TEXT_NODE
8570
+ * @param {Node} node - node to check
8571
+ * @returns {boolean} whether node is of type Node.TEXT_NODE
8572
+ */
8573
+ function isTextNode(node) {
8574
+ return node && node.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
8575
+ }
8576
+
8577
+ function minDOMApisSupported() {
8578
+ try {
8579
+ var testEl = document$1.createElement('div');
8580
+ return !!testEl['matches'];
8581
+ } catch (err) {
8582
+ return false;
8583
+ }
8584
+ }
8585
+
8586
+ /*
8587
+ * Check whether a DOM event should be "tracked" or if it may contain sensitive data
8588
+ * using a variety of heuristics.
8589
+ * @param {Element} el - element to check
8590
+ * @param {Event} ev - event to check
8591
+ * @returns {boolean} whether the event should be tracked
8592
+ */
8593
+ function shouldTrackDomEvent(el, ev) {
8594
+ if (!el || isTag(el, 'html') || !isElementNode(el)) {
8595
+ return false;
8596
+ }
8597
+ var tag = el.tagName.toLowerCase();
8598
+ switch (tag) {
8599
+ case 'form':
8600
+ return ev.type === EV_SUBMIT;
8601
+ case 'input':
8602
+ if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
8603
+ return ev.type === EV_CHANGE;
8604
+ } else {
8605
+ return ev.type === EV_CLICK;
8606
+ }
8607
+ case 'select':
8608
+ case 'textarea':
8609
+ return ev.type === EV_CHANGE;
8610
+ default:
8611
+ return ev.type === EV_CLICK;
8612
+ }
8613
+ }
8614
+
8615
+ /*
8616
+ * Check whether a DOM element should be "tracked" or if it may contain sensitive data
8617
+ * using a variety of heuristics.
8618
+ * @param {Element} el - element to check
8619
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
8620
+ * @returns {boolean} whether the element should be tracked
8621
+ */
8622
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
8623
+ var i;
8624
+
8625
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
8626
+ return false;
8627
+ }
8628
+
8629
+ for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
8630
+ var classes = getClasses(curEl);
8631
+ for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
8632
+ if (classes[SENSITIVE_DATA_CLASSES[i]]) {
8633
+ return false;
8634
+ }
8635
+ }
8636
+ }
8637
+
8638
+ var elClasses = getClasses(el);
8639
+ for (i = 0; i < OPT_IN_CLASSES.length; i++) {
8640
+ if (elClasses[OPT_IN_CLASSES[i]]) {
8641
+ return true;
8642
+ }
8643
+ }
8644
+
8645
+ // don't send data from inputs or similar elements since there will always be
8646
+ // a risk of clientside javascript placing sensitive data in attributes
8647
+ if (
8648
+ isTag(el, 'input') ||
8649
+ isTag(el, 'select') ||
8650
+ isTag(el, 'textarea') ||
8651
+ el.getAttribute('contenteditable') === 'true'
8652
+ ) {
8653
+ return false;
8654
+ }
8655
+
8656
+ // don't include hidden or password fields
8657
+ var type = el.type || '';
8658
+ 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"]
8659
+ switch(type.toLowerCase()) {
8660
+ case 'hidden':
8661
+ return false;
8662
+ case 'password':
8663
+ return false;
8664
+ }
8665
+ }
8666
+
8667
+ // filter out data from fields that look like sensitive fields
8668
+ var name = el.name || el.id || '';
8669
+ 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"]
8670
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
8671
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
8672
+ return false;
8673
+ }
8674
+ }
8675
+
8676
+ return true;
8677
+ }
8678
+
8679
+
8680
+ /*
8681
+ * Check whether a string value should be "tracked" or if it may contain sensitive data
8682
+ * using a variety of heuristics.
8683
+ * @param {string} value - string value to check
8684
+ * @returns {boolean} whether the element should be tracked
8685
+ */
8686
+ function shouldTrackValue(value) {
8687
+ if (value === null || _.isUndefined(value)) {
8688
+ return false;
8689
+ }
8690
+
8691
+ if (typeof value === 'string') {
8692
+ value = _.trim(value);
8693
+
8694
+ // check to see if input value looks like a credit card number
8695
+ // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
8696
+ 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}))$/;
8697
+ if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
8698
+ return false;
8699
+ }
8700
+
8701
+ // check to see if input value looks like a social security number
8702
+ var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
8703
+ if (ssnRegex.test(value)) {
8704
+ return false;
8705
+ }
8706
+ }
8707
+
8708
+ return true;
8709
+ }
8710
+
8711
+ var AUTOCAPTURE_CONFIG_KEY = 'autocapture';
8712
+ var LEGACY_PAGEVIEW_CONFIG_KEY = 'track_pageview';
8713
+
8714
+ var PAGEVIEW_OPTION_FULL_URL = 'full-url';
8715
+ var PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING = 'url-with-path-and-query-string';
8716
+ var PAGEVIEW_OPTION_URL_WITH_PATH = 'url-with-path';
8717
+
8718
+ var CONFIG_ALLOW_ELEMENT_CALLBACK = 'allow_element_callback';
8719
+ var CONFIG_ALLOW_SELECTORS = 'allow_selectors';
8720
+ var CONFIG_ALLOW_URL_REGEXES = 'allow_url_regexes';
8721
+ var CONFIG_BLOCK_ATTRS = 'block_attrs';
8722
+ var CONFIG_BLOCK_ELEMENT_CALLBACK = 'block_element_callback';
8723
+ var CONFIG_BLOCK_SELECTORS = 'block_selectors';
8724
+ var CONFIG_BLOCK_URL_REGEXES = 'block_url_regexes';
8725
+ var CONFIG_CAPTURE_EXTRA_ATTRS = 'capture_extra_attrs';
8726
+ var CONFIG_CAPTURE_TEXT_CONTENT = 'capture_text_content';
8727
+ var CONFIG_SCROLL_CAPTURE_ALL = 'scroll_capture_all';
8728
+ var CONFIG_SCROLL_CHECKPOINTS = 'scroll_depth_percent_checkpoints';
8729
+ var CONFIG_TRACK_CLICK = 'click';
8730
+ var CONFIG_TRACK_INPUT = 'input';
8731
+ var CONFIG_TRACK_PAGEVIEW = 'pageview';
8732
+ var CONFIG_TRACK_SCROLL = 'scroll';
8733
+ var CONFIG_TRACK_SUBMIT = 'submit';
8734
+
8735
+ var CONFIG_DEFAULTS = {};
8736
+ CONFIG_DEFAULTS[CONFIG_ALLOW_SELECTORS] = [];
8737
+ CONFIG_DEFAULTS[CONFIG_ALLOW_URL_REGEXES] = [];
8738
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ATTRS] = [];
8739
+ CONFIG_DEFAULTS[CONFIG_BLOCK_ELEMENT_CALLBACK] = null;
8740
+ CONFIG_DEFAULTS[CONFIG_BLOCK_SELECTORS] = [];
8741
+ CONFIG_DEFAULTS[CONFIG_BLOCK_URL_REGEXES] = [];
8742
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_EXTRA_ATTRS] = [];
8743
+ CONFIG_DEFAULTS[CONFIG_CAPTURE_TEXT_CONTENT] = false;
8744
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CAPTURE_ALL] = false;
8745
+ CONFIG_DEFAULTS[CONFIG_SCROLL_CHECKPOINTS] = [25, 50, 75, 100];
8746
+ CONFIG_DEFAULTS[CONFIG_TRACK_CLICK] = true;
8747
+ CONFIG_DEFAULTS[CONFIG_TRACK_INPUT] = true;
8748
+ CONFIG_DEFAULTS[CONFIG_TRACK_PAGEVIEW] = PAGEVIEW_OPTION_FULL_URL;
8749
+ CONFIG_DEFAULTS[CONFIG_TRACK_SCROLL] = true;
8750
+ CONFIG_DEFAULTS[CONFIG_TRACK_SUBMIT] = true;
8751
+
8752
+ var DEFAULT_PROPS = {
8753
+ '$mp_autocapture': true
8754
+ };
8755
+
8756
+ var MP_EV_CLICK = '$mp_click';
8757
+ var MP_EV_INPUT = '$mp_input_change';
8758
+ var MP_EV_SCROLL = '$mp_scroll';
8759
+ var MP_EV_SUBMIT = '$mp_submit';
8760
+
8761
+ /**
8762
+ * Autocapture: manages automatic event tracking
8763
+ * @constructor
8764
+ */
8765
+ var Autocapture = function(mp) {
8766
+ this.mp = mp;
8767
+ };
8768
+
8769
+ Autocapture.prototype.init = function() {
8770
+ if (!minDOMApisSupported()) {
8771
+ logger.critical('Autocapture unavailable: missing required DOM APIs');
8772
+ return;
8773
+ }
8774
+
8775
+ this.initPageviewTracking();
8776
+ this.initClickTracking();
8777
+ this.initInputTracking();
8778
+ this.initScrollTracking();
8779
+ this.initSubmitTracking();
8780
+ };
8781
+
8782
+ Autocapture.prototype.getFullConfig = function() {
8783
+ var autocaptureConfig = this.mp.get_config(AUTOCAPTURE_CONFIG_KEY);
8784
+ if (!autocaptureConfig) {
8785
+ // Autocapture is completely off
8786
+ return {};
8787
+ } else if (_.isObject(autocaptureConfig)) {
8788
+ return _.extend({}, CONFIG_DEFAULTS, autocaptureConfig);
8789
+ } else {
8790
+ // Autocapture config is non-object truthy value, return default
8791
+ return CONFIG_DEFAULTS;
8792
+ }
8793
+ };
8794
+
8795
+ Autocapture.prototype.getConfig = function(key) {
8796
+ return this.getFullConfig()[key];
8797
+ };
8798
+
8799
+ Autocapture.prototype.currentUrlBlocked = function() {
8800
+ var i;
8801
+ var currentUrl = _.info.currentUrl();
8802
+
8803
+ var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
8804
+ if (allowUrlRegexes.length) {
8805
+ // we're using an allowlist, only track if current URL matches
8806
+ var allowed = false;
8807
+ for (i = 0; i < allowUrlRegexes.length; i++) {
8808
+ var allowRegex = allowUrlRegexes[i];
8809
+ try {
8810
+ if (currentUrl.match(allowRegex)) {
8811
+ allowed = true;
8812
+ break;
8813
+ }
8814
+ } catch (err) {
8815
+ logger.critical('Error while checking block URL regex: ' + allowRegex, err);
8816
+ return true;
8817
+ }
8818
+ }
8819
+ if (!allowed) {
8820
+ // wasn't allowed by any regex
8821
+ return true;
8822
+ }
8823
+ }
8824
+
8825
+ var blockUrlRegexes = this.getConfig(CONFIG_BLOCK_URL_REGEXES) || [];
8826
+ if (!blockUrlRegexes || !blockUrlRegexes.length) {
8827
+ return false;
8828
+ }
8829
+
8830
+ for (i = 0; i < blockUrlRegexes.length; i++) {
8831
+ try {
8832
+ if (currentUrl.match(blockUrlRegexes[i])) {
8833
+ return true;
8834
+ }
8835
+ } catch (err) {
8836
+ logger.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
8837
+ return true;
8838
+ }
8839
+ }
8840
+ return false;
8841
+ };
8842
+
8843
+ Autocapture.prototype.pageviewTrackingConfig = function() {
8844
+ // supports both autocapture config and old track_pageview config
8845
+ if (this.mp.get_config(AUTOCAPTURE_CONFIG_KEY)) {
8846
+ return this.getConfig(CONFIG_TRACK_PAGEVIEW);
8847
+ } else {
8848
+ return this.mp.get_config(LEGACY_PAGEVIEW_CONFIG_KEY);
8849
+ }
8850
+ };
8851
+
8852
+ // helper for event handlers
8853
+ Autocapture.prototype.trackDomEvent = function(ev, mpEventName) {
8854
+ if (this.currentUrlBlocked()) {
8855
+ return;
8856
+ }
8857
+
8858
+ var props = getPropsForDOMEvent(ev, {
8859
+ allowElementCallback: this.getConfig(CONFIG_ALLOW_ELEMENT_CALLBACK),
8860
+ allowSelectors: this.getConfig(CONFIG_ALLOW_SELECTORS),
8861
+ blockAttrs: this.getConfig(CONFIG_BLOCK_ATTRS),
8862
+ blockElementCallback: this.getConfig(CONFIG_BLOCK_ELEMENT_CALLBACK),
8863
+ blockSelectors: this.getConfig(CONFIG_BLOCK_SELECTORS),
8864
+ captureExtraAttrs: this.getConfig(CONFIG_CAPTURE_EXTRA_ATTRS),
8865
+ captureTextContent: this.getConfig(CONFIG_CAPTURE_TEXT_CONTENT)
8866
+ });
8867
+ if (props) {
8868
+ _.extend(props, DEFAULT_PROPS);
8869
+ this.mp.track(mpEventName, props);
8870
+ }
8871
+ };
8872
+
8873
+ Autocapture.prototype.initClickTracking = function() {
8874
+ win.removeEventListener(EV_CLICK, this.listenerClick);
8875
+
8876
+ if (!this.getConfig(CONFIG_TRACK_CLICK)) {
8877
+ return;
8878
+ }
8879
+ logger.log('Initializing click tracking');
8880
+
8881
+ this.listenerClick = win.addEventListener(EV_CLICK, function(ev) {
8882
+ if (!this.getConfig(CONFIG_TRACK_CLICK)) {
8883
+ return;
8884
+ }
8885
+ this.trackDomEvent(ev, MP_EV_CLICK);
8886
+ }.bind(this));
8887
+ };
8888
+
8889
+ Autocapture.prototype.initInputTracking = function() {
8890
+ win.removeEventListener(EV_CHANGE, this.listenerChange);
8891
+
8892
+ if (!this.getConfig(CONFIG_TRACK_INPUT)) {
8893
+ return;
8894
+ }
8895
+ logger.log('Initializing input tracking');
8896
+
8897
+ this.listenerChange = win.addEventListener(EV_CHANGE, function(ev) {
8898
+ if (!this.getConfig(CONFIG_TRACK_INPUT)) {
8899
+ return;
8900
+ }
8901
+ this.trackDomEvent(ev, MP_EV_INPUT);
8902
+ }.bind(this));
8903
+ };
8904
+
8905
+ Autocapture.prototype.initPageviewTracking = function() {
8906
+ win.removeEventListener(EV_POPSTATE, this.listenerPopstate);
8907
+ win.removeEventListener(EV_HASHCHANGE, this.listenerHashchange);
8908
+ win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
8909
+
8910
+ if (!this.pageviewTrackingConfig()) {
8911
+ return;
8912
+ }
8913
+ logger.log('Initializing pageview tracking');
8914
+
8915
+ var previousTrackedUrl = '';
8916
+ var tracked = false;
8917
+ if (!this.currentUrlBlocked()) {
8918
+ tracked = this.mp.track_pageview(DEFAULT_PROPS);
8919
+ }
8920
+ if (tracked) {
8921
+ previousTrackedUrl = _.info.currentUrl();
8922
+ }
8923
+
8924
+ this.listenerPopstate = win.addEventListener(EV_POPSTATE, function() {
8925
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
8926
+ });
8927
+ this.listenerHashchange = win.addEventListener(EV_HASHCHANGE, function() {
8928
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
8929
+ });
8930
+ var nativePushState = win.history.pushState;
8931
+ if (typeof nativePushState === 'function') {
8932
+ win.history.pushState = function(state, unused, url) {
8933
+ nativePushState.call(win.history, state, unused, url);
8934
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
8935
+ };
8936
+ }
8937
+ var nativeReplaceState = win.history.replaceState;
8938
+ if (typeof nativeReplaceState === 'function') {
8939
+ win.history.replaceState = function(state, unused, url) {
8940
+ nativeReplaceState.call(win.history, state, unused, url);
8941
+ win.dispatchEvent(new Event(EV_MP_LOCATION_CHANGE));
8942
+ };
8943
+ }
8944
+ this.listenerLocationchange = win.addEventListener(EV_MP_LOCATION_CHANGE, safewrap(function() {
8945
+ if (this.currentUrlBlocked()) {
8946
+ return;
8947
+ }
8948
+
8949
+ var currentUrl = _.info.currentUrl();
8950
+ var shouldTrack = false;
8951
+ var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
8952
+ var trackPageviewOption = this.pageviewTrackingConfig();
8953
+ if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
8954
+ shouldTrack = currentUrl !== previousTrackedUrl;
8955
+ } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
8956
+ shouldTrack = currentUrl.split('#')[0] !== previousTrackedUrl.split('#')[0];
8957
+ } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH) {
8958
+ shouldTrack = didPathChange;
8959
+ }
8960
+
8961
+ if (shouldTrack) {
8962
+ var tracked = this.mp.track_pageview(DEFAULT_PROPS);
8963
+ if (tracked) {
8964
+ previousTrackedUrl = currentUrl;
8965
+ }
8966
+ if (didPathChange) {
8967
+ this.lastScrollCheckpoint = 0;
8968
+ logger.log('Path change: re-initializing scroll depth checkpoints');
8969
+ }
8970
+ }
8971
+ }.bind(this)));
8972
+ };
8973
+
8974
+ Autocapture.prototype.initScrollTracking = function() {
8975
+ win.removeEventListener(EV_SCROLLEND, this.listenerScroll);
8976
+
8977
+ if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
8978
+ return;
8979
+ }
8980
+ logger.log('Initializing scroll tracking');
8981
+ this.lastScrollCheckpoint = 0;
8982
+
8983
+ this.listenerScroll = win.addEventListener(EV_SCROLLEND, safewrap(function() {
8984
+ if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
8985
+ return;
8986
+ }
8987
+ if (this.currentUrlBlocked()) {
8988
+ return;
8989
+ }
8990
+
8991
+ var shouldTrack = this.getConfig(CONFIG_SCROLL_CAPTURE_ALL);
8992
+ var scrollCheckpoints = (this.getConfig(CONFIG_SCROLL_CHECKPOINTS) || [])
8993
+ .slice()
8994
+ .sort(function(a, b) { return a - b; });
8995
+
8996
+ var scrollTop = win.scrollY;
8997
+ var props = _.extend({'$scroll_top': scrollTop}, DEFAULT_PROPS);
8998
+ try {
8999
+ var scrollHeight = document$1.body.scrollHeight;
9000
+ var scrollPercentage = Math.round((scrollTop / (scrollHeight - win.innerHeight)) * 100);
9001
+ props['$scroll_height'] = scrollHeight;
9002
+ props['$scroll_percentage'] = scrollPercentage;
9003
+ if (scrollPercentage > this.lastScrollCheckpoint) {
9004
+ for (var i = 0; i < scrollCheckpoints.length; i++) {
9005
+ var checkpoint = scrollCheckpoints[i];
9006
+ if (
9007
+ scrollPercentage >= checkpoint &&
9008
+ this.lastScrollCheckpoint < checkpoint
9009
+ ) {
9010
+ props['$scroll_checkpoint'] = checkpoint;
9011
+ this.lastScrollCheckpoint = checkpoint;
9012
+ shouldTrack = true;
9013
+ }
9014
+ }
9015
+ }
9016
+ } catch (err) {
9017
+ logger.critical('Error while calculating scroll percentage', err);
9018
+ }
9019
+ if (shouldTrack) {
9020
+ this.mp.track(MP_EV_SCROLL, props);
9021
+ }
9022
+ }.bind(this)));
9023
+ };
9024
+
9025
+ Autocapture.prototype.initSubmitTracking = function() {
9026
+ win.removeEventListener(EV_SUBMIT, this.listenerSubmit);
9027
+
9028
+ if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
9029
+ return;
9030
+ }
9031
+ logger.log('Initializing submit tracking');
9032
+
9033
+ this.listenerSubmit = win.addEventListener(EV_SUBMIT, function(ev) {
9034
+ if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
9035
+ return;
9036
+ }
9037
+ this.trackDomEvent(ev, MP_EV_SUBMIT);
9038
+ }.bind(this));
9039
+ };
9040
+
9041
+ // TODO integrate error_reporter from mixpanel instance
9042
+ safewrapClass(Autocapture);
9043
+
8172
9044
  /* eslint camelcase: "off" */
8173
9045
 
8174
9046
  /**
@@ -9415,8 +10287,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
9415
10287
  if (!(k in union_q)) {
9416
10288
  union_q[k] = [];
9417
10289
  }
9418
- // We may send duplicates, the server will dedup them.
9419
- union_q[k] = union_q[k].concat(v);
10290
+ // Prevent duplicate values
10291
+ _.each(v, function(item) {
10292
+ if (!_.include(union_q[k], item)) {
10293
+ union_q[k].push(item);
10294
+ }
10295
+ });
9420
10296
  }
9421
10297
  });
9422
10298
  this._pop_from_people_queue(UNSET_ACTION, q_data);
@@ -9588,6 +10464,7 @@ var DEFAULT_CONFIG = {
9588
10464
  'api_transport': 'XHR',
9589
10465
  'api_payload_format': PAYLOAD_TYPE_BASE64,
9590
10466
  'app_host': 'https://mixpanel.com',
10467
+ 'autocapture': false,
9591
10468
  'cdn': 'https://cdn.mxpnl.com',
9592
10469
  'cross_site_cookie': false,
9593
10470
  'cross_subdomain_cookie': true,
@@ -9847,10 +10724,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
9847
10724
  }, '');
9848
10725
  }
9849
10726
 
9850
- var track_pageview_option = this.get_config('track_pageview');
9851
- if (track_pageview_option) {
9852
- this._init_url_change_tracking(track_pageview_option);
9853
- }
10727
+ this.autocapture = new Autocapture(this);
10728
+ this.autocapture.init();
9854
10729
 
9855
10730
  if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
9856
10731
  this.start_session_recording();
@@ -9975,55 +10850,6 @@ MixpanelLib.prototype._track_dom = function(DomClass, args) {
9975
10850
  return dt.track.apply(dt, args);
9976
10851
  };
9977
10852
 
9978
- MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) {
9979
- var previous_tracked_url = '';
9980
- var tracked = this.track_pageview();
9981
- if (tracked) {
9982
- previous_tracked_url = _.info.currentUrl();
9983
- }
9984
-
9985
- if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) {
9986
- win.addEventListener('popstate', function() {
9987
- win.dispatchEvent(new Event('mp_locationchange'));
9988
- });
9989
- win.addEventListener('hashchange', function() {
9990
- win.dispatchEvent(new Event('mp_locationchange'));
9991
- });
9992
- var nativePushState = win.history.pushState;
9993
- if (typeof nativePushState === 'function') {
9994
- win.history.pushState = function(state, unused, url) {
9995
- nativePushState.call(win.history, state, unused, url);
9996
- win.dispatchEvent(new Event('mp_locationchange'));
9997
- };
9998
- }
9999
- var nativeReplaceState = win.history.replaceState;
10000
- if (typeof nativeReplaceState === 'function') {
10001
- win.history.replaceState = function(state, unused, url) {
10002
- nativeReplaceState.call(win.history, state, unused, url);
10003
- win.dispatchEvent(new Event('mp_locationchange'));
10004
- };
10005
- }
10006
- win.addEventListener('mp_locationchange', function() {
10007
- var current_url = _.info.currentUrl();
10008
- var should_track = false;
10009
- if (track_pageview_option === 'full-url') {
10010
- should_track = current_url !== previous_tracked_url;
10011
- } else if (track_pageview_option === 'url-with-path-and-query-string') {
10012
- should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0];
10013
- } else if (track_pageview_option === 'url-with-path') {
10014
- should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0];
10015
- }
10016
-
10017
- if (should_track) {
10018
- var tracked = this.track_pageview();
10019
- if (tracked) {
10020
- previous_tracked_url = current_url;
10021
- }
10022
- }
10023
- }.bind(this));
10024
- }
10025
- };
10026
-
10027
10853
  /**
10028
10854
  * _prepare_callback() should be called by callers of _send_request for use
10029
10855
  * as the callback argument.
@@ -11281,6 +12107,10 @@ MixpanelLib.prototype.set_config = function(config) {
11281
12107
  this['persistence'].update_config(this['config']);
11282
12108
  }
11283
12109
  Config.DEBUG = Config.DEBUG || this.get_config('debug');
12110
+
12111
+ if ('autocapture' in config && this.autocapture) {
12112
+ this.autocapture.init();
12113
+ }
11284
12114
  }
11285
12115
  };
11286
12116