@mparticle/web-sdk 3.20.0 → 3.22.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.
package/dist/mparticle.js CHANGED
@@ -204,7 +204,7 @@ var mParticle = (function () {
204
204
  Base64: Base64$1
205
205
  };
206
206
 
207
- var version = "3.20.0";
207
+ var version = "3.22.0";
208
208
 
209
209
  var Constants = {
210
210
  sdkVersion: version,
@@ -3014,6 +3014,11 @@ var mParticle = (function () {
3014
3014
  this.exitHandler = null;
3015
3015
  this.visibilityChangeHandler = null;
3016
3016
  this.destroyed = false;
3017
+ // The order this page's uploads first picked each batch up in, by request
3018
+ // id. Batches enter uploads in creation order, so this keeps that order for
3019
+ // batches that share a millisecond.
3020
+ this.batchOrder = new Map();
3021
+ this.nextBatchOrder = 0;
3017
3022
  this.mpInstance = mpInstance;
3018
3023
  this.uploadIntervalMillis = uploadInterval;
3019
3024
  this.batchingEnabled = uploadInterval >= BatchUploader.MINIMUM_INTERVAL_MILLIS;
@@ -3299,8 +3304,9 @@ var mParticle = (function () {
3299
3304
  useBeacon = false;
3300
3305
  }
3301
3306
  return __awaiter(this, void 0, void 0, function () {
3302
- var currentUser, currentEvents, newBatches, batchesToUpload, batchesThatDidNotUpload;
3307
+ var currentUser, currentEvents, newBatches, batchesToUpload, batchesThatDidNotUpload, retriedIds;
3303
3308
  var _b, _c, _d;
3309
+ var _this_1 = this;
3304
3310
  return __generator(this, function (_e) {
3305
3311
  switch (_e.label) {
3306
3312
  case 0:
@@ -3326,9 +3332,22 @@ var mParticle = (function () {
3326
3332
  }
3327
3333
  batchesToUpload = this.batchesQueuedForProcessing;
3328
3334
  this.batchesQueuedForProcessing = [];
3335
+ batchesToUpload.forEach(function (batch) {
3336
+ if (!_this_1.batchOrder.has(batch.source_request_id)) {
3337
+ _this_1.batchOrder.set(batch.source_request_id, _this_1.nextBatchOrder++);
3338
+ }
3339
+ });
3329
3340
  return [4 /*yield*/, this.uploadBatches(batchesToUpload, useBeacon)];
3330
3341
  case 1:
3331
3342
  batchesThatDidNotUpload = _e.sent();
3343
+ retriedIds = (batchesThatDidNotUpload || []).map(function (batch) {
3344
+ return batch.source_request_id;
3345
+ });
3346
+ batchesToUpload.forEach(function (batch) {
3347
+ if (retriedIds.indexOf(batch.source_request_id) === -1) {
3348
+ _this_1.batchOrder["delete"](batch.source_request_id);
3349
+ }
3350
+ });
3332
3351
  // Batches that do not successfully upload are added back to the process queue
3333
3352
  // in the order they were created so that we can attempt re-transmission in
3334
3353
  // the same sequence. This is to prevent any potential data corruption.
@@ -3355,7 +3374,19 @@ var mParticle = (function () {
3355
3374
  };
3356
3375
 
3357
3376
  BatchUploader.prototype.storeBatchesQueuedForProcessing = function () {
3358
- var batches = this.batchesQueuedForProcessing;
3377
+ var _this_1 = this;
3378
+ var _a;
3379
+ // Another upload may have stored its failed batches while this one was
3380
+ // in flight. Merge them in creation order, whichever upload finished
3381
+ // first, so they are not overwritten and quota trimming drops the oldest.
3382
+ var orderOf = function orderOf(batch) {
3383
+ var _a;
3384
+ return (_a = _this_1.batchOrder.get(batch.source_request_id)) !== null && _a !== void 0 ? _a : -1;
3385
+ };
3386
+ var batches = __spreadArray(__spreadArray([], this.batchesQueuedForProcessing, true), (_a = this.batchVault.retrieve()) !== null && _a !== void 0 ? _a : [], true).sort(function (a, b) {
3387
+ var _a, _b;
3388
+ return orderOf(a) - orderOf(b) || ((_a = a.timestamp_unixtime_ms) !== null && _a !== void 0 ? _a : 0) - ((_b = b.timestamp_unixtime_ms) !== null && _b !== void 0 ? _b : 0);
3389
+ });
3359
3390
  // Nothing to persist: reset offline storage to an empty state.
3360
3391
  if (isEmpty(batches)) {
3361
3392
  this.batchVault.store([]);
@@ -6869,11 +6900,12 @@ var mParticle = (function () {
6869
6900
  // Pagination
6870
6901
  'page', 'limit', 'offset', 'cursor', 'per_page',
6871
6902
  // Site search term, as the common platforms spell it. Matching is
6872
- // case-insensitive; single-letter names other than `q` are too generic.
6873
- 'q', 'search', 'query', 'keyword', 'keywords', 'term', 'text', 'searchTerm', 'searchText', 'search_query', 'search_text', 'Ntt',
6903
+ // case-insensitive. `s` is WordPress and WooCommerce search; other
6904
+ // single-letter names are too generic. `oq` is a storefront search term.
6905
+ 'q', 's', 'oq', 'search', 'query', 'keyword', 'keywords', 'term', 'text', 'searchTerm', 'searchText', 'search_query', 'search_text', 'Ntt',
6874
6906
  // Product and variant identifiers. `cid` is not a category: it is a
6875
6907
  // customer id on many sites.
6876
- 'pid', 'productId', 'sku', 'skuId', 'variant',
6908
+ 'pid', 'productId', 'sku', 'skuId', 'variant', 'color', 'colour', 'size',
6877
6909
  // Category
6878
6910
  'category',
6879
6911
  // Referral
@@ -10795,6 +10827,15 @@ var mParticle = (function () {
10795
10827
  var nav = typeof navigator === 'undefined' ? undefined : navigator;
10796
10828
  return !!nav && nav.globalPrivacyControl === true;
10797
10829
  };
10830
+ // True when the kit can wait for the shopper's confirm click: signals are not
10831
+ // suppressed and the browser exposes the composed path the detector reads.
10832
+ // Decided from config alone, so tracker setup and teardown never change it.
10833
+ RoktManager.prototype.isCheckoutIntentLive = function () {
10834
+ if (this.isCheckoutIntentSuppressed()) {
10835
+ return false;
10836
+ }
10837
+ return typeof Event !== 'undefined' && isFunction(Event.prototype.composedPath);
10838
+ };
10798
10839
  // Reads `this.kit` at call time; before a kit attaches the signal is buffered
10799
10840
  // with the path it was raised on.
10800
10841
  RoktManager.prototype.notifyCheckoutIntent = function (signal, pathname) {
@@ -11484,7 +11525,10 @@ var mParticle = (function () {
11484
11525
  element_name: click.elementName,
11485
11526
  test_id: click.testId,
11486
11527
  controls: click.controls,
11487
- form: click.form
11528
+ form: click.form,
11529
+ link_type: click.linkType,
11530
+ link_domain: click.linkDomain,
11531
+ file_extension: click.fileExtension
11488
11532
  };
11489
11533
  for (var _i = 0, _b = Object.keys(optional); _i < _b.length; _i++) {
11490
11534
  var key = _b[_i];
@@ -11492,6 +11536,9 @@ var mParticle = (function () {
11492
11536
  data[key] = optional[key];
11493
11537
  }
11494
11538
  }
11539
+ if (click.checked !== undefined) {
11540
+ data.checked = click.checked;
11541
+ }
11495
11542
  return {
11496
11543
  messageType: MessageType$1.PageEvent,
11497
11544
  name: BUTTON_CLICK_EVENT_NAME,
@@ -11516,43 +11563,109 @@ var mParticle = (function () {
11516
11563
  }
11517
11564
  return false;
11518
11565
  }
11566
+ var ROKT_SELECTOR = '[data-core-type], #rokt-controller-frame';
11567
+ // A Rokt frame or script is served from a /wsdk/ path.
11568
+ function hasRoktSrc(element) {
11569
+ var src = element.getAttribute('src');
11570
+ if (!src) {
11571
+ return false;
11572
+ }
11573
+ try {
11574
+ return new URL(src, 'https://example.test').pathname.indexOf('/wsdk/') !== -1;
11575
+ } catch (e) {
11576
+ // An unparseable src is not a Rokt element.
11577
+ return false;
11578
+ }
11579
+ }
11519
11580
  // Rokt's own placements and frames never count as partner checkout controls.
11520
11581
  function isRoktElement(element) {
11521
- if (hasComposedAncestor(element, '[data-core-type], #rokt-controller-frame')) {
11522
- return true;
11582
+ return hasComposedAncestor(element, ROKT_SELECTOR) || hasRoktSrc(element);
11583
+ }
11584
+ // Wallet and quick-pay buttons as their vendors render them: official custom
11585
+ // elements and documented classes, ids and attributes.
11586
+ var WALLET_SIGNATURE = ['apple-pay-button', '.apple-pay-button', '.gpay-button', '.gpay-card-info-container', '#gpay-button-online-api-id', 'paypal-button', 'venmo-button', 'paypal-pay-later-button', 'paypal-credit-button', 'shop-pay-button', 'shop-pay-wallet-button', 'shopify-accelerated-checkout', 'shopify-accelerated-checkout-cart', 'shopify-apple-pay-button', 'shopify-google-pay-button', 'shopify-paypal-button', 'shopify-amazon-pay-button', '.amazonpay-button-focusable', '#AmazonPayButton', '[data-amazon-pay-button]', '[data-afterpay-entry-point]'].join(', ');
11587
+ // PayPal's card button opens a card form, so it is a step rather than a wallet.
11588
+ var CARD_FORM_BUTTON = 'paypal-basic-card-button';
11589
+ var VENDOR_BUTTON = WALLET_SIGNATURE + ', ' + CARD_FORM_BUTTON;
11590
+ // Apple's set-up and top-up buttons manage a wallet rather than pay with it.
11591
+ var NOT_A_PAYMENT = ['set-up', 'add-money', 'reload', 'top-up'].map(function (type) {
11592
+ return 'apple-pay-button[type="' + type + '"]';
11593
+ }).join(', ');
11594
+ // What a vendor's own button markup says about a control. Only the control or
11595
+ // the shadow host that owns it counts, so a merchant wrapper reusing a vendor
11596
+ // class never turns its children into wallet buttons.
11597
+ function vendorButtonKind(control) {
11598
+ var host = control.matches(VENDOR_BUTTON) ? control : null;
11599
+ if (!host) {
11600
+ var root = isFunction(control.getRootNode) ? control.getRootNode() : null;
11601
+ host = root && root.host && root.host.matches(VENDOR_BUTTON) ? root.host : null;
11602
+ }
11603
+ if (!host) {
11604
+ return undefined;
11523
11605
  }
11524
- var src = element.getAttribute('src');
11525
- if (src) {
11526
- try {
11527
- if (new URL(src, 'https://example.test').pathname.indexOf('/wsdk/') !== -1) {
11528
- return true;
11529
- }
11530
- } catch (e) {
11531
- // An unparseable src is not a Rokt element.
11532
- }
11606
+ if (host.matches(NOT_A_PAYMENT)) {
11607
+ return 'none';
11533
11608
  }
11534
- return false;
11609
+ return host.matches(CARD_FORM_BUTTON) ? 'checkout_step' : 'wallet';
11535
11610
  }
11536
11611
  function isWalletElement(element) {
11537
- return element.matches('apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]');
11612
+ return vendorButtonKind(element) === 'wallet';
11613
+ }
11614
+ // Pickers and toggles choose a payment method or open a panel; they never pay.
11615
+ var PICKER = 'input[type="radio"], input[type="checkbox"], label, [role="radio"], [role="tab"], [role="option"], ' + '[role="checkbox"], [role="switch"], [role="menuitemradio"], [role="menuitemcheckbox"], [aria-checked], [aria-selected]';
11616
+ var PICKER_GROUP = '[role="radiogroup"], [role="tablist"], [role="listbox"]';
11617
+ // Pay-later and financing messages link to details rather than pay.
11618
+ var MESSAGING = 'klarna-placement, afterpay-placement, square-placement, .affirm-as-low-as, .affirm-site-modal, ' + '.affirm-product-modal, [data-pp-message]';
11619
+ // A control that is still logged as a click but never counts as checkout intent.
11620
+ function isQuietIntentControl(control, isVendorWallet) {
11621
+ if (control.matches(PICKER)) {
11622
+ return true;
11623
+ }
11624
+ if (hasComposedAncestor(control, PICKER_GROUP)) {
11625
+ return true;
11626
+ }
11627
+ if (!isVendorWallet && (control.hasAttribute('aria-expanded') || control.hasAttribute('aria-pressed'))) {
11628
+ return true;
11629
+ }
11630
+ return hasComposedAncestor(control, MESSAGING);
11538
11631
  }
11632
+ // A checkbox or radio is a control of its own only when no button or link
11633
+ // around it takes the click, so a tappable row keeps its own name.
11634
+ var OPTION_INPUT = 'input[type="checkbox"], input[type="radio"]';
11539
11635
  // Walks at most 12 entries of an event's composed path for the nearest control.
11540
- // Rokt elements, disabled controls and anything under data-mp-no-track are skipped.
11636
+ // One composed walk from the target rules out Rokt elements and anything under
11637
+ // data-mp-no-track; disabled controls are skipped.
11541
11638
  function findControl(path) {
11639
+ var entries = path.slice(0, 12);
11640
+ var first;
11641
+ for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
11642
+ var target = entries_1[_i];
11643
+ if (target instanceof Element) {
11644
+ first = target;
11645
+ break;
11646
+ }
11647
+ }
11648
+ if (!first || hasComposedAncestor(first, ROKT_SELECTOR + ', [data-mp-no-track]')) {
11649
+ return undefined;
11650
+ }
11542
11651
  var control;
11543
- for (var _i = 0, _a = path.slice(0, 12); _i < _a.length; _i++) {
11544
- var target = _a[_i];
11652
+ var option;
11653
+ for (var _a = 0, entries_2 = entries; _a < entries_2.length; _a++) {
11654
+ var target = entries_2[_a];
11545
11655
  if (!(target instanceof Element)) {
11546
11656
  continue;
11547
11657
  }
11548
- if (isRoktElement(target) || target.matches('[data-mp-no-track]')) {
11658
+ if (hasRoktSrc(target)) {
11549
11659
  return undefined;
11550
11660
  }
11551
- if (!control && (target.matches(CONTROL_SELECTOR + ', a[href]') || isWalletElement(target))) {
11661
+ if (!control && target.matches(CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON)) {
11552
11662
  control = target;
11663
+ } else if (!control && !option && target.matches(OPTION_INPUT)) {
11664
+ option = target;
11553
11665
  }
11554
11666
  }
11555
- if (!control || isRoktElement(control) || hasComposedAncestor(control, '[data-mp-no-track]') || control.matches(':disabled, [aria-disabled="true"]')) {
11667
+ control = control || option;
11668
+ if (!control || control.matches(':disabled, [aria-disabled="true"]')) {
11556
11669
  return undefined;
11557
11670
  }
11558
11671
  return control;
@@ -11563,6 +11676,11 @@ var mParticle = (function () {
11563
11676
  function isUnsafeTextSource(element) {
11564
11677
  return element.matches('input, select, textarea, option, optgroup, datalist, ' + EDITABLE) || !!element.querySelector('input, select, textarea, [data-mp-no-track], ' + EDITABLE) || hasComposedAncestor(element, 'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE);
11565
11678
  }
11679
+ // A label around a checkbox or radio holds no typed text, so only the other
11680
+ // fields rule its text out.
11681
+ function isUnsafeLabelSource(label) {
11682
+ return !!label.querySelector('input:not([type="checkbox"]):not([type="radio"]), select, textarea, [data-mp-no-track], ' + EDITABLE) || hasComposedAncestor(label, 'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE);
11683
+ }
11566
11684
 
11567
11685
  // Masks a button label before it is logged. Button text can carry personal data,
11568
11686
  // so this is the privacy boundary for every label and aria-label.
@@ -11593,12 +11711,16 @@ var mParticle = (function () {
11593
11711
  return masked || undefined;
11594
11712
  }
11595
11713
 
11714
+ var DOWNLOAD_EXTENSIONS = /\.(pdf|zip|csv|txt|ics|docx?|xlsx?|pptx?|dmg|exe|apk|mp3|mp4)$/i;
11596
11715
  function elementKind(control) {
11597
11716
  if (isWalletElement(control)) {
11598
11717
  return 'wallet';
11599
11718
  }
11600
11719
  var tag = control.tagName.toLowerCase();
11601
- return tag === 'button' || tag === 'input' ? tag : 'role';
11720
+ if (tag === 'button' || tag === 'input') {
11721
+ return tag;
11722
+ }
11723
+ return control.matches('a[href]:not([role="button"])') ? 'link' : 'role';
11602
11724
  }
11603
11725
  function textOf(element) {
11604
11726
  return element && !isUnsafeTextSource(element) && element.textContent ? element.textContent.trim() : '';
@@ -11607,9 +11729,10 @@ var mParticle = (function () {
11607
11729
  return value && value.trim() ? value : undefined;
11608
11730
  }
11609
11731
  // The button's name as a screen reader would read it, simplified: the first of
11610
- // aria-labelledby, aria-label, own text (or an inner image's alt), title, and for
11611
- // inputs their value. The text sources are skipped for controls that hold form
11612
- // fields or more than four children, so a typed value is never read.
11732
+ // aria-labelledby, aria-label, own text (or an inner image's alt), title, for
11733
+ // inputs their value, and for checkboxes and radios their label's text. The text
11734
+ // sources are skipped for controls that hold form fields or more than four
11735
+ // children, so a typed value is never read.
11613
11736
  function accessibleName(control) {
11614
11737
  var labelledBy = control.getAttribute('aria-labelledby');
11615
11738
  if (labelledBy) {
@@ -11660,6 +11783,16 @@ var mParticle = (function () {
11660
11783
  source: 'value'
11661
11784
  };
11662
11785
  }
11786
+ } else if (type === 'checkbox' || type === 'radio') {
11787
+ // The label the browser ties to the box, never the box's value.
11788
+ var label = control.labels ? control.labels[0] : undefined;
11789
+ var text = label && !isUnsafeLabelSource(label) && label.textContent ? label.textContent.trim() : '';
11790
+ if (text) {
11791
+ return {
11792
+ raw: text,
11793
+ source: 'label'
11794
+ };
11795
+ }
11663
11796
  }
11664
11797
  } else if (readable) {
11665
11798
  var text = textOf(control);
@@ -11772,11 +11905,56 @@ var mParticle = (function () {
11772
11905
  function controlType(control, element) {
11773
11906
  return element === 'button' || element === 'input' ? control.type : 'button';
11774
11907
  }
11775
- // Describes a clicked control for auto button tracking. Links are left to page
11776
- // views, so a link click is not described.
11908
+ // Reads the link's own resolved URL parts, never its full href: only the hostname of
11909
+ // a link that leaves the site and the extension of a file it downloads travel. Navigation
11910
+ // within the site is left to page views, and mailto:, tel: and other schemes are skipped.
11911
+ function describeLink(link) {
11912
+ var href = (link.getAttribute('href') || '').trim();
11913
+ if (!href || href.charAt(0) === '#') {
11914
+ return {
11915
+ type: 'same_page'
11916
+ };
11917
+ }
11918
+ if (!(link instanceof HTMLAnchorElement)) {
11919
+ return undefined;
11920
+ }
11921
+ var protocol = link.protocol.toLowerCase();
11922
+ if (protocol === 'javascript:') {
11923
+ return {
11924
+ type: 'same_page'
11925
+ };
11926
+ }
11927
+ if (protocol !== 'http:' && protocol !== 'https:') {
11928
+ return undefined;
11929
+ }
11930
+ var domain = link.hostname !== window.location.hostname ? link.hostname.toLowerCase() : undefined;
11931
+ var extension = DOWNLOAD_EXTENSIONS.exec(link.pathname);
11932
+ if (link.hasAttribute('download') || extension) {
11933
+ return {
11934
+ type: 'download',
11935
+ domain: domain,
11936
+ fileExtension: extension ? extension[1].toLowerCase() : undefined
11937
+ };
11938
+ }
11939
+ if (domain) {
11940
+ return {
11941
+ type: 'outbound',
11942
+ domain: domain
11943
+ };
11944
+ }
11945
+ return link.pathname === window.location.pathname && link.search === window.location.search ? {
11946
+ type: 'same_page'
11947
+ } : undefined;
11948
+ }
11949
+ // Describes a clicked control for auto button tracking. A link counts only when page
11950
+ // views can't see the click, and carries its link type.
11777
11951
  function describeButtonClick(control) {
11952
+ var link;
11778
11953
  if (control.matches('a[href]:not([role="button"])')) {
11779
- return undefined;
11954
+ link = describeLink(control);
11955
+ if (!link) {
11956
+ return undefined;
11957
+ }
11780
11958
  }
11781
11959
  var element = elementKind(control);
11782
11960
  var name = accessibleName(control);
@@ -11794,34 +11972,83 @@ var mParticle = (function () {
11794
11972
  controls: controlsIdentifier(control),
11795
11973
  form: formIdentifier(control),
11796
11974
  element: element,
11797
- type: controlType(control, element)
11975
+ type: controlType(control, element),
11976
+ linkType: link ? link.type : undefined,
11977
+ linkDomain: link ? link.domain : undefined,
11978
+ fileExtension: link ? link.fileExtension : undefined,
11979
+ // The box as the click left it: the browser toggles it before the click
11980
+ // reaches listeners, and puts it back if the click is cancelled.
11981
+ checked: control instanceof HTMLInputElement && (control.type === 'checkbox' || control.type === 'radio') ? control.checked : undefined
11798
11982
  };
11799
11983
  }
11800
11984
 
11801
11985
  var checkoutPath = /(?:^|\/)(?:checkouts?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i;
11802
- var exclusions = new RegExp(['coupon|promo|newsletter|sign in|add to cart|gift card|pay later|pay in|payment plan|learn more', 'gutschein|anmelden|warenkorb|bon cadeau|connexion|panier|cup\u00F3n|iniciar sesi\u00F3n|carrito', 'carrello|codice sconto|accedi|buono regalo|winkelwagen|kortingscode|inloggen|cadeaubon|achteraf betalen', '\u30AB\u30FC\u30C8|\u30AF\u30FC\u30DD\u30F3|\u30ED\u30B0\u30A4\u30F3|\u4F1A\u54E1\u767B\u9332|\u30AE\u30D5\u30C8|\u5F8C\u6255\u3044|\u30E1\u30EB\u30DE\u30AC'].join('|'));
11803
- var commits = new RegExp('^(?:' + ['(?:place|submit|complete|confirm|finali[sz]e)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase|booking|reservation|payment|checkout)', 'pay(?: securely)?(?: now)?|confirm and pay|order and pay|order with obligation to pay', '(?:jetzt )?(?:zahlungs|kosten)pflichtig (?:bestellen|buchen)|(?:jetzt )?bezahlen|kauf abschlie\u00DFen|bestellung abschlie\u00DFen', 'commander et payer|payer(?: maintenant)?|(?:confirmer|finaliser) (?:ma |la )?commande', "valider et payer|confirmer et payer|commande avec obligation de paiement|finaliser l'achat", '(?:realizar|confirmar|completar) (?:el |mi |la )?(?:pedido|compra|reserva)|finalizar (?:el |mi )?pedido|pagar(?: ahora)?', 'pedido con obligaci\u00F3n de pago', "(?:effettua|conferma|concludi|completa) (?:l'|il )?(?:ordine|acquisto)|ordine con obbligo di pagamento", 'paga(?: ora| adesso)?|ordina e paga', '(?:nu )?betalen|bestellen en betalen|bestelling plaatsen|plaats bestelling|bestelling afronden', 'bestelling met betalingsverplichting', '(?:\u3053\u306E\u5185\u5BB9\u3067|\u4E0A\u8A18\u306E?\u5185\u5BB9\u3067)?\u3054?(?:\u6CE8\u6587|\u8CFC\u5165|\u4E88\u7D04|\u6C7A\u6E08|\u304A?\u652F\u6255\u3044?)\u3092?(?:\u78BA\u5B9A|\u5B8C\u4E86)(?:\u3059\u308B)?', '(?:\u3053\u306E\u5185\u5BB9\u3067|\u4E0A\u8A18\u306E?\u5185\u5BB9\u3067)\u3054?(?:\u6CE8\u6587|\u8CFC\u5165|\u4E88\u7D04)\u3059\u308B|\u3054?\u6CE8\u6587\u3059\u308B|\u6C7A\u6E08\u3059\u308B|(?:\u4ECA\u3059\u3050|\u3092)?\u652F\u6255\u3046'].join('|') + ')$');
11986
+ // English look-alikes are whole words; the other languages stay substring
11987
+ // matches, since several of them do not separate words with spaces.
11988
+ var englishExclusions = new RegExp('\\b(?:coupon|promo(?: code)?|newsletter|sign in|sign up|log in|login|add to (?:cart|bag|basket)|' + 'gift card balance|(?:add|apply|redeem|check|enter|use) (?:a |an |your )?gift card|pay later|' + 'interest free|as low as|learn more|prequalify|apply (?:now|for)|payment plan|available|accepted|' + 'we accept|set up|add money|reload|top up|add to (?:apple|google) wallet|connect|link your|unlink|' + 'remove|delete|edit|change|manage|use a different|switch)\\b|^pay in$|\\bpay in (?:installments|instalments)\\b');
11989
+ var otherExclusions = new RegExp(['gutschein|anmelden|warenkorb|bon cadeau|connexion|panier|cup\u00F3n|iniciar sesi\u00F3n|carrito', 'carrello|codice sconto|accedi|buono regalo|winkelwagen|kortingscode|inloggen|cadeaubon|achteraf betalen', '\u30AB\u30FC\u30C8|\u30AF\u30FC\u30DD\u30F3|\u30ED\u30B0\u30A4\u30F3|\u4F1A\u54E1\u767B\u9332|\u30AE\u30D5\u30C8|\u5F8C\u6255\u3044|\u30E1\u30EB\u30DE\u30AC'].join('|'));
11990
+ var englishCommits = '(?:(?:agree|accept|review) and )?(?:place|submit|complete|confirm|finali[sz]e)' + '(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?! (?:[^ ]+ )?gift card)' + '(?: [^ ]+){0,2} (?:order|purchase|booking|reservation|payment|checkout|transaction)' + '(?: now| securely| and pay| (?:with|using) [^ ]+(?: [^ ]+)?)?(?: (?:total|subtotal|items?|tickets?))?' + '|pay(?: securely)?(?: now)?|(?:confirm|agree|accept|submit|order|book) and pay' + '|pay and place (?:my |your )?order|order with obligation to pay';
11991
+ var otherCommits = ['(?:jetzt )?(?:zahlungs|kosten)pflichtig (?:bestellen|buchen)|(?:jetzt )?bezahlen|kauf abschlie\u00DFen|bestellung abschlie\u00DFen', 'commander et payer|payer(?: maintenant)?|(?:confirmer|finaliser) (?:ma |la )?commande', "valider et payer|confirmer et payer|commande avec obligation de paiement|finaliser l'achat", '(?:realizar|confirmar|completar) (?:el |mi |la )?(?:pedido|compra|reserva)|finalizar (?:el |mi )?pedido|pagar(?: ahora)?', 'pedido con obligaci\u00F3n de pago', "(?:effettua|conferma|concludi|completa) (?:l'|il )?(?:ordine|acquisto)|ordine con obbligo di pagamento", 'paga(?: ora| adesso)?|ordina e paga', '(?:nu )?betalen|bestellen en betalen|bestelling plaatsen|plaats bestelling|bestelling afronden', 'bestelling met betalingsverplichting', '(?:\u3053\u306E\u5185\u5BB9\u3067|\u4E0A\u8A18\u306E?\u5185\u5BB9\u3067)?\u3054?(?:\u6CE8\u6587|\u8CFC\u5165|\u4E88\u7D04|\u6C7A\u6E08|\u304A?\u652F\u6255\u3044?)\u3092?(?:\u78BA\u5B9A|\u5B8C\u4E86)(?:\u3059\u308B)?', '(?:\u3053\u306E\u5185\u5BB9\u3067|\u4E0A\u8A18\u306E?\u5185\u5BB9\u3067)\u3054?(?:\u6CE8\u6587|\u8CFC\u5165|\u4E88\u7D04)\u3059\u308B|\u3054?\u6CE8\u6587\u3059\u308B|\u6C7A\u6E08\u3059\u308B|(?:\u4ECA\u3059\u3050|\u3092)?\u652F\u6255\u3046'].join('|');
11992
+ var commits = new RegExp('^(?:' + englishCommits + '|' + otherCommits + ')$');
11993
+ // A plain link is navigation, so in English only an order or purchase it
11994
+ // places, submits or completes counts as the confirm click.
11995
+ var linkCommits = new RegExp('^(?:(?:place|submit|complete)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase)|' + otherCommits + ')$');
11804
11996
  var routeGatedCommits = new RegExp('^(?:buy now|purchase|jetzt kaufen|kaufen|commander|(?:passer|valider) (?:la |ma )?commande|finalizar (?:la |mi )?compra|' + 'comprar ahora|acquista ora|nu kopen|\u4ECA\u3059\u3050\u8CFC\u5165|\u8CFC\u5165\u3059\u308B|\u3053\u306E\u5185\u5BB9\u3067\u7533\u3057\u8FBC\u3080)$');
11805
- var steps = new RegExp('^(?:' + ['checkout|check out|check-out|continue to payment|proceed to checkout', 'zur kasse|zur kasse gehen|auschecken|weiter zur zahlung', 'passer \u00E0 la caisse|continuer vers le paiement|proc\u00E9der au paiement', 'ir a caja|continuar al pago|tramitar pedido', "procedi al checkout|procedi all'acquisto|procedi al pagamento|vai alla cassa", 'afrekenen|verder naar betalen|doorgaan naar betalen', '\u30EC\u30B8\u306B\u9032\u3080|\u3054?(?:\u8CFC\u5165|\u6CE8\u6587)\u624B\u7D9A\u304D(?:\u3078|\u306B)(?:\u9032\u3080)?|\u304A?\u652F\u6255\u3044(?:\u3078|\u306B)\u9032\u3080|(?:\u3054\u6CE8\u6587\u5185\u5BB9\u306E)?\u78BA\u8A8D\u753B\u9762\u3078(?:\u9032\u3080)?'].join('|') + ')$');
11806
- var wallets = /^(?:apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|affirm)$/;
11807
- // Exact HTTPS hosts of hosted wallet and payment frames.
11808
- var frameHosts = new Set(['www.paypal.com', 'www.sandbox.paypal.com', 'pay.google.com', 'applepay.cdn-apple.com', 'js.stripe.com', 'checkoutshopper-live.adyen.com', 'payments.amazon.com', 'pay.shopify.com']);
11997
+ var steps = new RegExp('^(?:' + ["(?:let's |lets |go to |proceed to |secure )?(?:check ?out|checkout)(?: now| securely| as guest)?", '(?:continue|proceed) to (?:payment|review|billing|shipping|delivery|order review)|review (?:my |your )?order', 'guest checkout|(?:pay|continue|check ?out) (?:with|by) (?:card|credit card|debit card|credit or debit card)', 'zur kasse|zur kasse gehen|auschecken|weiter zur zahlung', 'passer \u00E0 la caisse|continuer vers le paiement|proc\u00E9der au paiement', 'ir a caja|continuar al pago|tramitar pedido', "procedi al checkout|procedi all'acquisto|procedi al pagamento|vai alla cassa", 'afrekenen|verder naar betalen|doorgaan naar betalen', '\u30EC\u30B8\u306B\u9032\u3080|\u3054?(?:\u8CFC\u5165|\u6CE8\u6587)\u624B\u7D9A\u304D(?:\u3078|\u306B)(?:\u9032\u3080)?|\u304A?\u652F\u6255\u3044(?:\u3078|\u306B)\u9032\u3080|(?:\u3054\u6CE8\u6587\u5185\u5BB9\u306E)?\u78BA\u8A8D\u753B\u9762\u3078(?:\u9032\u3080)?'].join('|') + ')$');
11998
+ var WALLET_NAMES = 'apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|clearpay|cash app(?: pay)?|affirm|sezzle|samsung pay';
11999
+ var WALLET_VERBS = 'pay|buy|purchase|order|book|donate|subscribe|check ?out|checkout|express checkout|continue|proceed|' + 'complete purchase|place (?:my |your )?order';
12000
+ // "Pay with Klarna", "Buy with GPay", "Continue to PayPal".
12001
+ var walletPhrases = new RegExp('^(?:(?:' + WALLET_VERBS + ')(?: now| securely)? (?:with|using|via|through|by) (?:' + WALLET_NAMES + ')(?: express(?: checkout)?| checkout)?|(?:continue|proceed|go) (?:to|with) (?:' + WALLET_NAMES + '))$');
12002
+ // A wallet's own name on a button: "PayPal", "Klarna checkout", "Apple Pay logo".
12003
+ var walletBrands = new RegExp('^(?:' + WALLET_NAMES + ')(?: (?:checkout|check out|express(?: checkout)?|express pay|buy now|pay now|pay|now)' + '| (?:logo|icon|mark|button)(?: (?:logo|icon|mark|button))*)?$');
12004
+ // Exact HTTPS hosts of hosted wallet button frames.
12005
+ var walletFrameHosts = new Set(['pay.google.com', 'applepay.cdn-apple.com', 'payments.amazon.com', 'pay.shopify.com']);
12006
+ // PayPal serves its buttons and its card fields and pay-later messages from
12007
+ // the same hosts; only the button frames are wallet taps.
12008
+ var paypalFrameHosts = new Set(['www.paypal.com', 'www.sandbox.paypal.com']);
12009
+ var PAYPAL_BUTTON_FRAME = /^\/smart\/(?:buttons|menu)(?:\/|$)/;
12010
+ // Stripe's quick-pay button frames; its other frames are card and form fields.
12011
+ var STRIPE_WALLET_FRAME = /\/elements-inner-(?:payment-request|express-checkout)-/;
12012
+ // Hosted card-entry and fingerprint frames: focus there is typing card details.
12013
+ var CARD_FRAME_DOMAINS = ['adyen.com', 'braintreegateway.com', 'cybersource.com', 'squarecdn.com', 'checkout.com'];
11809
12014
  // Keep these classes ASCII-only in source bytes to avoid mojibake parse failures
11810
12015
  // when a host page forces non-UTF-8 decoding before JavaScript parse.
11811
12016
  var TYPOGRAPHIC_APOSTROPHES = /[\u2019\u2018]/g;
11812
12017
  var CURRENCY_AND_NUMBER_CHARS = /[0-9\uFF10-\uFF19$\u20AC\u00A3\u00A5\uFFE5\u20B9\u5186.,\uFF0C]/g;
12018
+ var BRACKETED = /\([^()]*\)|\[[^[\]]*\]|\uFF08[^\uFF08\uFF09]*\uFF09|\uFF3B[^\uFF3B\uFF3D]*\uFF3D|\u3010[^\u3010\u3011]*\u3011/g;
12019
+ // Spaces, punctuation, separators, arrows and symbols, CJK and full-width forms
12020
+ // included, plus emoji halves and variation selectors.
12021
+ var EDGE = "[\\s!-/:-@\\[-`{-~\\u00A1-\\u00BF\\u00D7\\u00F7\\u2000-\\u206F\\u20A0-\\u20CF\\u2190-\\u2BFF" + "\\u3000-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u30FB\\uD800-\\uDFFF\\uFE00-\\uFE0F" + "\\uFF01-\\uFF0F\\uFF1A-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65]+";
12022
+ var EDGE_SYMBOLS = new RegExp('^' + EDGE + '|' + EDGE + '$', 'g');
11813
12023
  function getIntentRouteClass(pathname) {
11814
12024
  return checkoutPath.test(pathname) ? 'checkout_path' : 'other';
11815
12025
  }
12026
+ var SEPARATORS = /[-_|\u00B7\u2022]/g;
12027
+ var CURRENCY_CODES = /(?:^| )(?:usd|aud|cad|nzd|gbp|eur|chf|sgd|hkd|jpy|inr|mxn)(?= |$)|\b(?:a|au|ca|c|us|nz|hk|s)\$/g;
12028
+ var WALLET_ALIASES = [[/\bapplepay\b/g, 'apple pay'], [/\bg ?pay\b/g, 'google pay'], [/\bamazonpay\b/g, 'amazon pay'], [/\bshoppay\b/g, 'shop pay'], [/\bcashapp\b/g, 'cash app']];
12029
+ // Labels longer than this are sentences: they neither match nor veto intent.
12030
+ var MAX_LABEL_WORDS = 8;
11816
12031
  function normalize(text) {
11817
12032
  if (!text || text.length > 128) {
11818
12033
  return undefined;
11819
12034
  }
11820
- return text.toLowerCase().replace(TYPOGRAPHIC_APOSTROPHES, "'").replace(CURRENCY_AND_NUMBER_CHARS, '').replace(/\s+/g, ' ').trim();
12035
+ var label = text.toLowerCase().replace(TYPOGRAPHIC_APOSTROPHES, "'").replace(/[&+]/g, ' and ').replace(SEPARATORS, ' ').replace(CURRENCY_CODES, ' ').replace(CURRENCY_AND_NUMBER_CHARS, '').replace(/\s+/g, ' ').trim();
12036
+ WALLET_ALIASES.forEach(function (_a) {
12037
+ var alias = _a[0],
12038
+ name = _a[1];
12039
+ label = label.replace(alias, name);
12040
+ });
12041
+ return label.split(' ').length > MAX_LABEL_WORDS ? undefined : label;
12042
+ }
12043
+ // The label without bracketed asides or the separators and symbols left at its
12044
+ // edges, so "place order -" and "place order ( items)" both read "place order".
12045
+ function coreLabel(text) {
12046
+ var core = text.replace(BRACKETED, ' ').replace(/\s+/g, ' ').replace(EDGE_SYMBOLS, '');
12047
+ return core || text.replace(EDGE_SYMBOLS, '');
11821
12048
  }
11822
12049
  // Labels come from aria-label then visible text. Only submit and button inputs
11823
- // have their value read, so a typed field value is never touched. Other text
11824
- // follows the same rules as button descriptions.
12050
+ // have their value read, so a typed field value is never touched. A control
12051
+ // holding form fields or more than four children gives only its aria-label.
11825
12052
  function labelSources(element) {
11826
12053
  if (element instanceof HTMLInputElement) {
11827
12054
  var type = element.type;
@@ -11834,7 +12061,7 @@ var mParticle = (function () {
11834
12061
  return [element.getAttribute('aria-label'), text];
11835
12062
  }
11836
12063
  if (element.children.length > 4 || isUnsafeTextSource(element)) {
11837
- return [];
12064
+ return [element.getAttribute('aria-label')];
11838
12065
  }
11839
12066
  return [element.getAttribute('aria-label'), element.textContent];
11840
12067
  }
@@ -11843,53 +12070,97 @@ var mParticle = (function () {
11843
12070
  return !!label;
11844
12071
  });
11845
12072
  }
11846
- function classifyLabel(text, button, pathname) {
11847
- if (commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path') {
11848
- return 'commit';
12073
+ // "Buy now" and "Purchase" count only on a checkout route, and never on a plain
12074
+ // link, which is navigation.
12075
+ function isCommitLabel(text, isLink, pathname) {
12076
+ if (isLink) {
12077
+ return linkCommits.test(text);
11849
12078
  }
11850
- if (button && wallets.test(text)) {
11851
- return 'wallet';
12079
+ return commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path';
12080
+ }
12081
+ // Classifies a clicked or submitting control found by findControl, in order:
12082
+ // pickers, toggles and pay-later messages never count; a vendor's own wallet
12083
+ // button counts before any label rule; a confirm label on any source counts
12084
+ // before the look-alike exclusions veto; then wallet phrases and steps.
12085
+ function classifyIntent(control, pathname) {
12086
+ var vendor = vendorButtonKind(control);
12087
+ if (isQuietIntentControl(control, vendor === 'wallet') || vendor === 'none') {
12088
+ return undefined;
11852
12089
  }
11853
- if (steps.test(text)) {
11854
- return 'checkout_step';
12090
+ if (vendor) {
12091
+ return {
12092
+ kind: vendor,
12093
+ source: 'vendor'
12094
+ };
11855
12095
  }
11856
- return undefined;
11857
- }
11858
- // Classifies a clicked or submitting control found by findControl.
11859
- function classifyIntentControl(control, pathname) {
11860
12096
  var texts = labels(control);
11861
- if (texts.some(function (text) {
11862
- return exclusions.test(text);
12097
+ var cores = texts.map(coreLabel);
12098
+ var isLink = control.matches('a[href]:not([role="button"])');
12099
+ if (cores.some(function (text) {
12100
+ return isCommitLabel(text, isLink, pathname);
11863
12101
  })) {
11864
- return undefined;
11865
- }
11866
- var button = false;
11867
- if (!control.matches('a[href]')) {
11868
- button = control.matches(CONTROL_SELECTOR) || isWalletElement(control);
12102
+ return {
12103
+ kind: 'commit',
12104
+ source: 'label'
12105
+ };
11869
12106
  }
11870
- if (button && isWalletElement(control)) {
11871
- return 'wallet';
12107
+ if (texts.concat(cores).some(function (text) {
12108
+ return englishExclusions.test(text) || otherExclusions.test(text);
12109
+ })) {
12110
+ return undefined;
11872
12111
  }
11873
- for (var _i = 0, texts_1 = texts; _i < texts_1.length; _i++) {
11874
- var text = texts_1[_i];
11875
- var kind = classifyLabel(text, button, pathname);
11876
- if (kind) {
11877
- return kind;
12112
+ var isButton = !isLink && control.matches(CONTROL_SELECTOR);
12113
+ for (var _i = 0, cores_1 = cores; _i < cores_1.length; _i++) {
12114
+ var text = cores_1[_i];
12115
+ if (isButton && walletPhrases.test(text)) {
12116
+ return {
12117
+ kind: 'wallet',
12118
+ source: 'label'
12119
+ };
12120
+ }
12121
+ if (isButton && walletBrands.test(text)) {
12122
+ return {
12123
+ kind: 'wallet',
12124
+ source: 'brand'
12125
+ };
12126
+ }
12127
+ if (steps.test(text)) {
12128
+ return {
12129
+ kind: 'checkout_step',
12130
+ source: 'label'
12131
+ };
11878
12132
  }
11879
12133
  }
11880
12134
  return undefined;
11881
12135
  }
11882
- // Focus moving into a visible hosted wallet or payment frame.
12136
+ // The host is the domain or one of its subdomains, never a look-alike suffix.
12137
+ function isOnDomain(host, domain) {
12138
+ return host === domain || host.length > domain.length && host.slice(host.length - domain.length - 1) === '.' + domain;
12139
+ }
12140
+ // Focus moving into a visible hosted wallet button counts as a wallet click;
12141
+ // focus into hosted card fields or messages is only a frame.
11883
12142
  function classifyIntentFrame(target, win) {
11884
12143
  if (win.document.visibilityState !== 'visible' || !(target instanceof HTMLIFrameElement)) {
11885
12144
  return undefined;
11886
12145
  }
11887
- if (isRoktElement(target)) {
12146
+ if (isRoktElement(target) || hasComposedAncestor(target, '[data-mp-no-track]')) {
11888
12147
  return undefined;
11889
12148
  }
11890
12149
  try {
11891
12150
  var url = new URL(target.src);
11892
- if (url.protocol === 'https:' && frameHosts.has(url.hostname)) {
12151
+ var host_1 = url.protocol === 'https:' ? url.hostname : '';
12152
+ if (paypalFrameHosts.has(host_1)) {
12153
+ return PAYPAL_BUTTON_FRAME.test(url.pathname) ? 'wallet' : 'frame';
12154
+ }
12155
+ if (walletFrameHosts.has(host_1)) {
12156
+ return 'wallet';
12157
+ }
12158
+ if (host_1 === 'js.stripe.com') {
12159
+ return STRIPE_WALLET_FRAME.test(url.pathname) ? 'wallet' : 'frame';
12160
+ }
12161
+ if (CARD_FRAME_DOMAINS.some(function (domain) {
12162
+ return isOnDomain(host_1, domain);
12163
+ })) {
11893
12164
  return 'frame';
11894
12165
  }
11895
12166
  } catch (e) {
@@ -11907,6 +12178,9 @@ var mParticle = (function () {
11907
12178
  // A ceiling, not a dedupe: a held Enter key or browser automation fires a
11908
12179
  // trusted click per repeat, and each one is forwarded to every kit.
11909
12180
  var PAGE_EVENT_LIMIT = 100;
12181
+ // A click uploads at once. Taps that follow it this quickly are held and share
12182
+ // one upload once they stop.
12183
+ var UPLOAD_DEBOUNCE_MS = 100;
11910
12184
  var btnWindow = function btnWindow() {
11911
12185
  return typeof window === 'undefined' ? null : window;
11912
12186
  };
@@ -11946,7 +12220,7 @@ var mParticle = (function () {
11946
12220
  passive: true
11947
12221
  };
11948
12222
  // Logs every button click as an mParticle event, and hands checkout intent to the
11949
- // Rokt kit as a frozen {kind, t}. Submits and wallet frame focus feed the kit only.
12223
+ // Rokt kit as a frozen {kind, t, source, pathname}. Submits and wallet frame focus feed the kit only.
11950
12224
  var ButtonTracker = /** @class */function () {
11951
12225
  function ButtonTracker(mpInstance) {
11952
12226
  var _this = this;
@@ -11960,6 +12234,10 @@ var mParticle = (function () {
11960
12234
  this.pageEvents = 0;
11961
12235
  // Clicks read at dispatch and waiting for the next paint.
11962
12236
  this.pending = [];
12237
+ this.uploadTimerId = null;
12238
+ this.uploadHeld = false;
12239
+ // Set while the page is left, when the SDK's exit upload sends the queue.
12240
+ this.leaving = false;
11963
12241
  this.drain = function () {
11964
12242
  var queue = _this.pending;
11965
12243
  _this.pending = [];
@@ -11973,7 +12251,14 @@ var mParticle = (function () {
11973
12251
  // beforeunload, the first of them.
11974
12252
  this.onHide = function (event) {
11975
12253
  if (event.type === 'beforeunload' || event.type === 'pagehide' || document.visibilityState === 'hidden') {
11976
- _this.drain();
12254
+ _this.leaving = true;
12255
+ try {
12256
+ _this.drain();
12257
+ } finally {
12258
+ _this.leaving = false;
12259
+ }
12260
+ // The SDK's exit upload sends the queue, held taps included.
12261
+ _this.cancelUpload();
11977
12262
  }
11978
12263
  };
11979
12264
  this.onPageShow = function (event) {
@@ -12064,6 +12349,7 @@ var mParticle = (function () {
12064
12349
  this.lastSent = {};
12065
12350
  this.pagePath = null;
12066
12351
  this.pageEvents = 0;
12352
+ this.cancelUpload();
12067
12353
  };
12068
12354
  ButtonTracker.prototype.enqueue = function (delivery) {
12069
12355
  this.pending.push(delivery);
@@ -12089,15 +12375,22 @@ var mParticle = (function () {
12089
12375
  // button that disables or relabels itself is seen as it was clicked.
12090
12376
  // Logging and the kit hand-off wait for the next paint.
12091
12377
  var click;
12378
+ var box;
12092
12379
  var kind;
12380
+ var source;
12093
12381
  var unconfirmedCommit = false;
12094
12382
  if (layer === 'click') {
12095
12383
  // The composed path is only populated during dispatch.
12096
12384
  var control = findControl(isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : []);
12097
12385
  if (control) {
12098
12386
  click = logsEvents ? describeButtonClick(control) : undefined;
12387
+ if (click && click.checked !== undefined) {
12388
+ box = control;
12389
+ }
12099
12390
  // Checkout intent is only worked out when the kit can use it.
12100
- kind = signalsKit ? classifyIntentControl(control, pathname) : undefined;
12391
+ var intent = signalsKit ? classifyIntent(control, pathname) : undefined;
12392
+ kind = intent ? intent.kind : undefined;
12393
+ source = intent ? intent.source : undefined;
12101
12394
  }
12102
12395
  } else if (layer === 'submit') {
12103
12396
  var submitter = event.submitter;
@@ -12106,8 +12399,11 @@ var mParticle = (function () {
12106
12399
  // commit click, which is known once that click is delivered.
12107
12400
  if (!submitter) {
12108
12401
  kind = 'commit';
12402
+ source = 'submit';
12109
12403
  } else if (control) {
12110
- kind = classifyIntentControl(control, pathname);
12404
+ var intent = classifyIntent(control, pathname);
12405
+ kind = intent ? intent.kind : undefined;
12406
+ source = intent ? intent.source : undefined;
12111
12407
  }
12112
12408
  unconfirmedCommit = !submitter;
12113
12409
  }
@@ -12121,14 +12417,23 @@ var mParticle = (function () {
12121
12417
  // Read after the blur settles: some browsers have not moved
12122
12418
  // focus into the frame yet when the window blurs.
12123
12419
  kind = classifyIntentFrame(document.activeElement, window);
12420
+ source = 'frame';
12124
12421
  } else if (unconfirmedCommit && t - _this.lastCommitClick > SUBMIT_AFTER_COMMIT_CLICK_MS) {
12125
12422
  kind = undefined;
12126
12423
  }
12127
12424
  if (click && logsEvents) {
12425
+ // The box as this click left it. The browser toggles it
12426
+ // before dispatch and puts it back once a cancelled click
12427
+ // is over, so only a cancelled click changes the reading
12428
+ // taken at dispatch: a checkbox to the other state, a
12429
+ // radio to however it was restored.
12430
+ if (box && event.defaultPrevented) {
12431
+ click.checked = box.type === 'checkbox' ? !click.checked : box.checked;
12432
+ }
12128
12433
  _this.logClick(click, pathname);
12129
12434
  }
12130
12435
  if (kind) {
12131
- _this.emit(kind, layer, t, pathname);
12436
+ _this.emit(kind, source, layer, t, pathname);
12132
12437
  }
12133
12438
  } catch (e) {
12134
12439
  _this.log('[error] delivery threw: ' + e);
@@ -12150,9 +12455,48 @@ var mParticle = (function () {
12150
12455
  this.pageEvents++;
12151
12456
  // Restarts an expired session, as public logEvent() and page views do.
12152
12457
  this.mpInstance._SessionManager.resetSessionTimer();
12458
+ var queued = this.queuedForUpload();
12153
12459
  this.mpInstance._Events.logEvent(buildButtonClickEvent(click, pathname));
12460
+ // Only a click waiting for the batch interval is sent early: a held
12461
+ // one goes out when released, and with batching off it already has.
12462
+ // A checkbox or radio waits for the batch, since choosing an option
12463
+ // doesn't leave the page the way a confirm click can.
12464
+ if (!this.leaving && click.checked === undefined && this.queuedForUpload() > queued) {
12465
+ this.uploadNow();
12466
+ }
12467
+ };
12468
+ // Events waiting in the upload queue for the next batch interval.
12469
+ ButtonTracker.prototype.queuedForUpload = function () {
12470
+ var uploader = this.mpInstance._APIClient.uploader;
12471
+ return uploader ? uploader.eventsQueuedForProcessing.length : 0;
12472
+ };
12473
+ // Uploads a click without waiting for the batch interval. A tap inside the
12474
+ // wait after the last one is held, and each restarts the wait, so a burst
12475
+ // after the first click goes out in one request once the taps stop.
12476
+ ButtonTracker.prototype.uploadNow = function () {
12477
+ var _this = this;
12478
+ if (this.uploadTimerId === null) {
12479
+ this.mpInstance.upload();
12480
+ } else {
12481
+ clearTimeout(this.uploadTimerId);
12482
+ this.uploadHeld = true;
12483
+ }
12484
+ this.uploadTimerId = setTimeout(function () {
12485
+ _this.uploadTimerId = null;
12486
+ if (_this.uploadHeld) {
12487
+ _this.uploadHeld = false;
12488
+ _this.mpInstance.upload();
12489
+ }
12490
+ }, UPLOAD_DEBOUNCE_MS);
12154
12491
  };
12155
- ButtonTracker.prototype.emit = function (kind, layer, t, pathname) {
12492
+ ButtonTracker.prototype.cancelUpload = function () {
12493
+ if (this.uploadTimerId !== null) {
12494
+ clearTimeout(this.uploadTimerId);
12495
+ this.uploadTimerId = null;
12496
+ }
12497
+ this.uploadHeld = false;
12498
+ };
12499
+ ButtonTracker.prototype.emit = function (kind, source, layer, t, pathname) {
12156
12500
  if (this.isKitSuppressed()) {
12157
12501
  return;
12158
12502
  }
@@ -12169,7 +12513,9 @@ var mParticle = (function () {
12169
12513
  };
12170
12514
  this.mpInstance._RoktManager.notifyCheckoutIntent(Object.freeze({
12171
12515
  kind: kind,
12172
- t: t
12516
+ t: t,
12517
+ source: source,
12518
+ pathname: pathname
12173
12519
  }), pathname);
12174
12520
  };
12175
12521
  // Events follow the SDK's opt-out like any other event.