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