@mparticle/web-sdk 3.18.0 → 3.19.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.
@@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul
15
15
  filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;f<c;f++)if(f in b){var g=b[f];a.call(e,g,f,b)&&d.push(g);}return d},// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
16
16
  isArray:function isArray(a){return "[object Array]"===Object.prototype.toString.call(a)},Base64:Base64$1};
17
17
 
18
- var version = "3.18.0";
18
+ var version = "3.19.0";
19
19
 
20
20
  var Constants={sdkVersion:version,sdkVendor:"mparticle",platform:"web",Messages:{DeprecationMessages:{MethodHasBeenDeprecated:"has been deprecated.",MethodMarkedForDeprecationPostfix:"is a deprecated method and will be removed in future releases.",AlternativeMethodPrefix:"Please use the alternate method:"},ErrorMessages:{NoToken:"A token must be specified.",EventNameInvalidType:"Event name must be a valid string value.",EventDataInvalidType:"Event data must be a valid object hash.",LoggingDisabled:"Event logging is currently disabled.",CookieParseError:"Could not parse cookie",EventEmpty:"Event object is null or undefined, cancelling send",APIRequestEmpty:"APIRequest is null or undefined, cancelling send",NoEventType:"Event type must be specified.",TransactionIdRequired:"Transaction ID is required",TransactionRequired:"A transaction attributes object is required",PromotionIdRequired:"Promotion ID is required",BadAttribute:"Attribute value cannot be object or array",BadKey:"Key value cannot be object or array",BadLogPurchase:"Transaction attributes and a product are both required to log a purchase, https://docs.mparticle.com/?javascript#measuring-transactions",AudienceAPINotEnabled:"Your workspace is not enabled to retrieve user audiences."},InformationMessages:{CookieSearch:"Searching for cookie",CookieFound:"Cookie found, parsing values",CookieNotFound:"Cookies not found",CookieSet:"Setting cookie",CookieSync:"Performing cookie sync",SendBegin:"Starting to send event",SendIdentityBegin:"Starting to send event to identity server",SendWindowsPhone:"Sending event to Windows Phone container",SendIOS:"Calling iOS path: ",SendAndroid:"Calling Android JS interface method: ",SendHttp:"Sending event to mParticle HTTP service",SendAliasHttp:"Sending alias request to mParticle HTTP service",SendIdentityHttp:"Sending event to mParticle HTTP service",StartingNewSession:"Starting new Session",StartingLogEvent:"Starting to log event",StartingLogOptOut:"Starting to log user opt in/out",StartingEndSession:"Starting to end session",StartingInitialization:"Starting to initialize",StartingLogCommerceEvent:"Starting to log commerce event",StartingAliasRequest:"Starting to Alias MPIDs",LoadingConfig:"Loading configuration options",AbandonLogEvent:"Cannot log event, logging disabled or developer token not set",AbandonAliasUsers:"Cannot Alias Users, logging disabled or developer token not set",AbandonStartSession:"Cannot start session, logging disabled or developer token not set",AbandonEndSession:"Cannot end session, logging disabled or developer token not set",NoSessionToEnd:"Cannot end session, no active session found"},ValidationMessages:{ModifyIdentityRequestUserIdentitiesPresent:"identityRequests to modify require userIdentities to be present. Request not sent to server. Please fix and try again",IdentityRequesetInvalidKey:"There is an invalid key on your identityRequest object. It can only contain a `userIdentities` object and a `onUserAlias` function. Request not sent to server. Please fix and try again.",OnUserAliasType:"The onUserAlias value must be a function.",UserIdentities:"The userIdentities key must be an object with keys of identityTypes and values of strings. Request not sent to server. Please fix and try again.",UserIdentitiesInvalidKey:"There is an invalid identity key on your `userIdentities` object within the identityRequest. Request not sent to server. Please fix and try again.",UserIdentitiesInvalidValues:"All user identity values must be strings or null. Request not sent to server. Please fix and try again.",AliasMissingMpid:"Alias Request must contain both a destinationMpid and a sourceMpid",AliasNonUniqueMpid:"Alias Request's destinationMpid and sourceMpid must be unique",AliasMissingTime:"Alias Request must have both a startTime and an endTime",AliasStartBeforeEndTime:"Alias Request's endTime must be later than its startTime"}},NativeSdkPaths:{LogEvent:"logEvent",SetUserTag:"setUserTag",RemoveUserTag:"removeUserTag",SetUserAttribute:"setUserAttribute",RemoveUserAttribute:"removeUserAttribute",SetSessionAttribute:"setSessionAttribute",LogOut:"logOut",SetUserAttributeList:"setUserAttributeList",RemoveAllUserAttributes:"removeAllUserAttributes",GetUserAttributesLists:"getUserAttributesLists",GetAllUserAttributes:"getAllUserAttributes",Identify:"identify",Logout:"logout",Login:"login",Modify:"modify",Alias:"aliasUsers",Upload:"upload"},StorageNames:{localStorageName:"mprtcl-api",localStorageNameV3:"mprtcl-v3",cookieName:"mprtcl-api",cookieNameV2:"mprtcl-v2",cookieNameV3:"mprtcl-v3",localStorageNameV4:"mprtcl-v4",cookieNameV4:"mprtcl-v4",currentStorageName:"mprtcl-v4"},DefaultConfig:{cookieDomain:null,cookieExpiration:365,logLevel:null,timeout:300,sessionTimeout:30,forwarderStatsTimeout:5e3,integrationDelayTimeout:5e3,maxCookieSize:3e3,aliasMaxWindow:90,uploadInterval:0// Maximum milliseconds in between batch uploads, below 500 will mean immediate upload. The server returns this as a string, but we are using it as a number internally
21
21
  },DefaultBaseUrls:{v1SecureServiceUrl:"jssdks.mparticle.com/v1/JS/",v2SecureServiceUrl:"jssdks.mparticle.com/v2/JS/",v3SecureServiceUrl:"jssdks.mparticle.com/v3/JS/",configUrl:"jssdkcdns.mparticle.com/JS/v2/",identityUrl:"identity.mparticle.com/v1/",aliasUrl:"jssdks.mparticle.com/v1/identity/",userAudienceUrl:"nativesdks.mparticle.com/v1/"},// These are the paths that are used to construct the CNAME urls
@@ -1359,10 +1359,12 @@ var ErrorReportingDispatcher=/** @class */function(){function a(){this.services=
1359
1359
 
1360
1360
  var LoggingDispatcher=/** @class */function(){function a(){this.services=[];}return a.prototype.register=function(a){this.services.push(a);},a.prototype.log=function(a){var b=this;this.services.forEach(function(c){var d;try{c.log(a);}catch(a){null===(d=b.logger)||void 0===d?void 0:d.error("Error in LoggingService: "+a);}});},a}();
1361
1361
 
1362
- var AUTO_BUTTON_ATTRIBUTE="is_auto_button_click";var BUTTON_CLICK_EVENT_NAME="Button Click";// One custom event per click, logged like logLink() with the auto marker and the
1362
+ var AUTO_BUTTON_ATTRIBUTE="is_auto_button_click";// Specific enough not to merge with a customer's own 'Button Click' event.
1363
+ var BUTTON_CLICK_EVENT_NAME="Auto Button Click";// One custom event per click, logged like logLink() with the auto marker and the
1363
1364
  // page fields that auto page views carry. No intent: classifying clicks is left
1364
1365
  // to whoever reads the data.
1365
- var buildButtonClickEvent=function(a,b){var c,d=(c={label_source:a.labelSource,element:a.element,type:a.type,path:b,hostname:window.location.hostname},c[AUTO_BUTTON_ATTRIBUTE]=!0,c);return a.label&&(d.label=a.label),a.ariaLabel&&(d.aria_label=a.ariaLabel),a.icon&&(d.icon=a.icon),a.elementId&&(d.element_id=a.elementId),a.elementName&&(d.element_name=a.elementName),a.testId&&(d.test_id=a.testId),{messageType:MessageType$1.PageEvent,name:BUTTON_CLICK_EVENT_NAME,data:d,eventType:EventType.Other}};
1366
+ var buildButtonClickEvent=function(a,b){// Optional fields are left out when the button has no safe value for them.
1367
+ for(var c,d,e=(c={label_source:a.labelSource,element:a.element,type:a.type,path:b,hostname:window.location.hostname},c[AUTO_BUTTON_ATTRIBUTE]=!0,c),f={label:a.label,aria_label:a.ariaLabel,icon:a.icon,element_id:a.elementId,element_name:a.elementName,test_id:a.testId,controls:a.controls,form:a.form},g=0,h=Object.keys(f);g<h.length;g++)d=h[g],f[d]&&(e[d]=f[d]);return {messageType:MessageType$1.PageEvent,name:BUTTON_CLICK_EVENT_NAME,data:e,eventType:EventType.Other}};
1366
1368
 
1367
1369
  // descriptions. Rokt's own elements, disabled controls and anything under
1368
1370
  // data-mp-no-track are never returned.
@@ -1371,7 +1373,8 @@ function hasComposedAncestor(a,b){for(var c=a;c;){if(c.matches(b))return !0;var
1371
1373
  function isRoktElement(a){if(hasComposedAncestor(a,"[data-core-type], #rokt-controller-frame"))return !0;var b=a.getAttribute("src");if(b)try{if(-1!==new URL(b,"https://example.test").pathname.indexOf("/wsdk/"))return !0}catch(a){// An unparseable src is not a Rokt element.
1372
1374
  }return !1}function isWalletElement(a){return a.matches("apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]")}// Walks at most 12 entries of an event's composed path for the nearest control.
1373
1375
  // Rokt elements, disabled controls and anything under data-mp-no-track are skipped.
1374
- function findControl(a){for(var b,c,d=0,e=a.slice(0,12);d<e.length;d++)if(c=e[d],!!(c instanceof Element)){if(isRoktElement(c)||c.matches("[data-mp-no-track]"))return;!b&&(c.matches("button, input[type=\"submit\"], input[type=\"button\"], input[type=\"image\"], [role=\"button\"], a[href]")||isWalletElement(c))&&(b=c);}return !b||isRoktElement(b)||hasComposedAncestor(b,"[data-mp-no-track]")||b.matches(":disabled, [aria-disabled=\"true\"]")?void 0:b}
1376
+ function findControl(a){for(var b,c,d=0,e=a.slice(0,12);d<e.length;d++)if(c=e[d],!!(c instanceof Element)){if(isRoktElement(c)||c.matches("[data-mp-no-track]"))return;!b&&(c.matches("button, input[type=\"submit\"], input[type=\"button\"], input[type=\"image\"], [role=\"button\"], a[href]")||isWalletElement(c))&&(b=c);}return !b||isRoktElement(b)||hasComposedAncestor(b,"[data-mp-no-track]")||b.matches(":disabled, [aria-disabled=\"true\"]")?void 0:b}// fields, editable regions, and anything under data-mp-no-track.
1377
+ function isUnsafeTextSource(a){return a.matches("input, select, textarea, option, optgroup, datalist, [contenteditable]:not([contenteditable=\"false\"])")||!!a.querySelector("input, select, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")||hasComposedAncestor(a,"select, datalist, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")}
1375
1378
 
1376
1379
  // Masks a button label before it is logged. Button text can carry personal data,
1377
1380
  // so this is the privacy boundary for every label and aria-label.
@@ -1381,8 +1384,7 @@ function createDigitPatterns(){try{return {run:/\p{Nd}{6,}/u,number:/\p{Nd}+(?:[
1381
1384
  // masked. Dropped when it looks like an email, phone or card number.
1382
1385
  function maskLabel(a){if(a&&!(128<a.length)&&digitPatterns){var b=a.replace(/\s+/g," ").trim().toLowerCase();if(b&&-1===b.indexOf("@")&&!digitPatterns.run.test(b.replace(/[\s()\-\u2010-\u2015]/g,""))){var c=b.replace(digitPatterns.number,"#").slice(0,64).trim();return c||void 0}}}
1383
1386
 
1384
- function elementKind(a){if(isWalletElement(a))return "wallet";var b=a.tagName.toLowerCase();return "button"===b||"input"===b?b:"role"}// fields, editable regions, and anything under data-mp-no-track.
1385
- function isUnsafeTextSource(a){return a.matches("input, select, textarea, option, optgroup, datalist, [contenteditable]:not([contenteditable=\"false\"])")||!!a.querySelector("input, select, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")||hasComposedAncestor(a,"select, datalist, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")}function textOf(a){return a&&!isUnsafeTextSource(a)&&a.textContent?a.textContent.trim():""}function nonBlank(a){return a&&a.trim()?a:void 0}// The button's name as a screen reader would read it, simplified: the first of
1387
+ function elementKind(a){if(isWalletElement(a))return "wallet";var b=a.tagName.toLowerCase();return "button"===b||"input"===b?b:"role"}function textOf(a){return a&&!isUnsafeTextSource(a)&&a.textContent?a.textContent.trim():""}function nonBlank(a){return a&&a.trim()?a:void 0}// The button's name as a screen reader would read it, simplified: the first of
1386
1388
  // aria-labelledby, aria-label, own text (or an inner image's alt), title, and for
1387
1389
  // inputs their value. The text sources are skipped for controls that hold form
1388
1390
  // fields or more than four children, so a typed value is never read.
@@ -1396,36 +1398,44 @@ function iconHint(a){var b=function(a){return !hasComposedAncestor(a,"[data-mp-n
1396
1398
  // (`ember1234`, `a8f3e2c1`, `radix-:r1:`) and long numbers don't match.
1397
1399
  var HAND_WRITTEN_ID=/^[A-Za-z]+[0-9]{0,3}(?:[-_](?:[A-Za-z]+[0-9]{0,3}|[0-9]{1,3}))*$/;// A developer-written identifier, or nothing.
1398
1400
  function stableIdentifier(a){if(a){// At most three digits in all, so numbers split into groups don't pass.
1399
- var b=a.trim(),c=b.replace(/[^0-9]/g,"").length;return 64>=b.length&&3>=c&&HAND_WRITTEN_ID.test(b)?b:void 0}}var TEST_ID_ATTRIBUTES=["data-testid","data-test-id","data-test","data-qa","data-cy"];function testIdentifier(a){for(var b=0,c=TEST_ID_ATTRIBUTES;b<c.length;b++){var d=c[b],e=stableIdentifier(a.getAttribute(d));if(e)return e}}// The browser's normalised type: a button defaults to 'submit' and an unknown
1401
+ var b=a.trim(),c=b.replace(/[^0-9]/g,"").length;return 64>=b.length&&3>=c&&HAND_WRITTEN_ID.test(b)?b:void 0}}var TEST_ID_ATTRIBUTES=["data-testid","data-test-id","data-test","data-qa","data-cy"];// The first of these attributes that holds a developer-written identifier.
1402
+ function firstIdentifier(a,b){for(var c=0,d=b;c<d.length;c++){var e=d[c],f=stableIdentifier(a.getAttribute(e));if(f)return f}}// What the button opens or acts on: a menu, drawer or dialog. aria-controls can
1403
+ // list several ids, so only the first is used.
1404
+ function controlsIdentifier(a){var b=a.getAttribute("aria-controls");return stableIdentifier(b?b.trim().split(/\s+/)[0]:null)||firstIdentifier(a,["popovertarget","commandfor"])}// The id of the form a button or input actually submits, which the browser
1405
+ // resolves from its form attribute or its owning form. getAttribute, because a
1406
+ // field named "id" shadows form.id.
1407
+ function formIdentifier(a){var b=a instanceof HTMLButtonElement||a instanceof HTMLInputElement?a.form:null;return b?stableIdentifier(b.getAttribute("id")):void 0}// The browser's normalised type: a button defaults to 'submit' and an unknown
1400
1408
  // input type reads as 'text'. Wallet and role buttons are just 'button'.
1401
- function controlType(a,b){return "button"===b||"input"===b?a.type:"button"}// Describes a clicked button for auto button tracking. Links are left to page
1409
+ function controlType(a,b){return "button"===b||"input"===b?a.type:"button"}// Describes a clicked control for auto button tracking. Links are left to page
1402
1410
  // views, so a link click is not described.
1403
- function describeButtonClick(a){var b=findControl(a);if(b&&!b.matches("a[href]:not([role=\"button\"])")){var c=elementKind(b),d=accessibleName(b),e=maskLabel(d.raw);return {label:e,labelSource:e?d.source:"none",// Kept on its own as well: it often says more than the visible text,
1411
+ function describeButtonClick(a){if(!a.matches("a[href]:not([role=\"button\"])")){var b=elementKind(a),c=accessibleName(a),d=maskLabel(c.raw);return {label:d,labelSource:d?c.source:"none",// Kept on its own as well: it often says more than the visible text,
1404
1412
  // such as which item a remove button removes.
1405
- ariaLabel:maskLabel(b.getAttribute("aria-label")),icon:iconHint(b),elementId:stableIdentifier(b.getAttribute("id")),elementName:stableIdentifier(b.getAttribute("name")),testId:testIdentifier(b),element:c,type:controlType(b,c)}}}
1413
+ ariaLabel:maskLabel(a.getAttribute("aria-label")),icon:iconHint(a),elementId:stableIdentifier(a.getAttribute("id")),elementName:stableIdentifier(a.getAttribute("name")),testId:firstIdentifier(a,TEST_ID_ATTRIBUTES),controls:controlsIdentifier(a),form:formIdentifier(a),element:b,type:controlType(a,b)}}}
1406
1414
 
1407
1415
  var checkoutPath=/(?:^|\/)(?:checkouts?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i,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\xF3n|iniciar sesi\xF3n|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"),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\xDFen|bestellung abschlie\xDFen|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\xF3n 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"+")$"),routeGatedCommits=/^(?: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)$/,steps=new RegExp("^(?:"+"checkout|check out|check-out|continue to payment|proceed to checkout|zur kasse|zur kasse gehen|auschecken|weiter zur zahlung|passer \xE0 la caisse|continuer vers le paiement|proc\xE9der 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)?"+")$"),wallets=/^(?:apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|affirm)$/,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"]),TYPOGRAPHIC_APOSTROPHES=/[\u2019\u2018]/g,CURRENCY_AND_NUMBER_CHARS=/[0-9\uFF10-\uFF19$\u20AC\u00A3\u00A5\uFFE5\u20B9\u5186.,\uFF0C]/g;// Exact HTTPS hosts of hosted wallet and payment frames.
1408
1416
  // Keep these classes ASCII-only in source bytes to avoid mojibake parse failures
1409
1417
  // when a host page forces non-UTF-8 decoding before JavaScript parse.
1410
1418
  function getIntentRouteClass(a){return checkoutPath.test(a)?"checkout_path":"other"}function normalize(a){return !a||128<a.length?void 0:a.toLowerCase().replace(TYPOGRAPHIC_APOSTROPHES,"'").replace(CURRENCY_AND_NUMBER_CHARS,"").replace(/\s+/g," ").trim()}// Labels come from aria-label then visible text. Only submit and button inputs
1411
- // have their value read, so a typed field value is never touched.
1412
- function labelSources(a){if(4<a.children.length||a.querySelector("input, select, textarea, [data-mp-no-track]"))return [];var b=null;if(a instanceof HTMLInputElement){var c=a.type;"submit"===c||"button"===c?b=a.value:"image"===c&&(b=a.getAttribute("alt"));}else b=a.textContent;return [a.getAttribute("aria-label"),b]}function labels(a){return labelSources(a).map(normalize).filter(function(a){return !!a})}function classifyLabel(a,b,c){return commits.test(a)||routeGatedCommits.test(a)&&"checkout_path"===getIntentRouteClass(c)?"commit":b&&wallets.test(a)?"wallet":steps.test(a)?"checkout_step":void 0}function classifyIntentControl(a,b){var c=findControl(a);return c?classifyControl(c,b):void 0}function classifyControl(a,b){var c=labels(a);if(!c.some(function(a){return exclusions.test(a)})){var d=!1;if(a.matches("a[href]")||(d=a.matches(CONTROL_SELECTOR)||isWalletElement(a)),d&&isWalletElement(a))return "wallet";for(var e=0,f=c;e<f.length;e++){var g=f[e],h=classifyLabel(g,d,b);if(h)return h}}}// Focus moving into a visible hosted wallet or payment frame.
1419
+ // have their value read, so a typed field value is never touched. Other text
1420
+ // follows the same rules as button descriptions.
1421
+ function labelSources(a){if(a instanceof HTMLInputElement){var b=a.type,c=null;return "submit"===b||"button"===b?c=a.value:"image"===b&&(c=a.getAttribute("alt")),[a.getAttribute("aria-label"),c]}return 4<a.children.length||isUnsafeTextSource(a)?[]:[a.getAttribute("aria-label"),a.textContent]}function labels(a){return labelSources(a).map(normalize).filter(function(a){return !!a})}function classifyLabel(a,b,c){return commits.test(a)||routeGatedCommits.test(a)&&"checkout_path"===getIntentRouteClass(c)?"commit":b&&wallets.test(a)?"wallet":steps.test(a)?"checkout_step":void 0}// Classifies a clicked or submitting control found by findControl.
1422
+ function classifyIntentControl(a,b){var c=labels(a);if(!c.some(function(a){return exclusions.test(a)})){var d=!1;if(a.matches("a[href]")||(d=a.matches(CONTROL_SELECTOR)||isWalletElement(a)),d&&isWalletElement(a))return "wallet";for(var e=0,f=c;e<f.length;e++){var g=f[e],h=classifyLabel(g,d,b);if(h)return h}}}// Focus moving into a visible hosted wallet or payment frame.
1413
1423
  function classifyIntentFrame(a,b){if("visible"===b.document.visibilityState&&a instanceof HTMLIFrameElement&&!isRoktElement(a))try{var c=new URL(a.src);if("https:"===c.protocol&&frameHosts.has(c.hostname))return "frame"}catch(a){// An unparseable frame src is not a wallet frame.
1414
1424
  }}
1415
1425
 
1416
1426
  // listeners. Inspect `window.__mpBtn__` in a console to see if one is live.
1417
- var WIN_BTN_KEY="__mpBtn__";var THROTTLE_MS=2e3,SUBMIT_AFTER_COMMIT_CLICK_MS=1e3,PATH_LIMIT=12,EVENT_DEDUP_MS=1e3,PAGE_EVENT_LIMIT=50,btnWindow=function(){return "undefined"==typeof window?null:window};var getActiveButtonTracker=function(){var a=btnWindow(),b=a?a[WIN_BTN_KEY]:void 0;return b?b.tracker:void 0};// Stops whichever tracker is active, including one from an earlier bundle load.
1427
+ var WIN_BTN_KEY="__mpBtn__";var THROTTLE_MS=2e3,SUBMIT_AFTER_COMMIT_CLICK_MS=1e3,PATH_LIMIT=12,PAGE_EVENT_LIMIT=100,btnWindow=function(){return "undefined"==typeof window?null:window};// A ceiling, not a dedupe: a held Enter key or browser automation fires a
1428
+ // trusted click per repeat, and each one is forwarded to every kit.
1429
+ var getActiveButtonTracker=function(){var a=btnWindow(),b=a?a[WIN_BTN_KEY]:void 0;return b?b.tracker:void 0};// Stops whichever tracker is active, including one from an earlier bundle load.
1418
1430
  var resetButtonTracking=function(){var a=btnWindow();if(a){var b=getActiveButtonTracker();b&&b.teardown(),a[WIN_BTN_KEY]={};}};// Runs once the browser has painted the page's response to the interaction, so
1419
1431
  // describing and logging a click never delays that paint.
1420
- var afterNextPaint=function(a){isFunction(window.requestAnimationFrame)&&"hidden"!==document.visibilityState?requestAnimationFrame(function(){setTimeout(a,0);}):setTimeout(a,0);},CAPTURE_PASSIVE={capture:!0,passive:!0},PASSIVE={passive:!0},ButtonTracker=/** @class */function(){function a(a){var b=this;this.active=!1,this.hasRoktKit=!1,this.generation=0,this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0,this.lastEvent={},this.pending=[],this.drain=function(){var a=b.pending;b.pending=[],a.forEach(function(a){return a()});},this.onHide=function(a){("beforeunload"===a.type||"pagehide"===a.type||"hidden"===document.visibilityState)&&b.drain();},this.onPageShow=function(a){a.persisted&&b.reset();},this.onEvent=function(a){try{b.observe(a);}catch(a){b.log("[error] event handler threw: "+a);}},this.mpInstance=a;}return Object.defineProperty(a.prototype,"isActive",{get:function get(){return this.active},enumerable:!1,configurable:!0}),a.prototype.init=function(a){var b=btnWindow();if(b){var c=getActiveButtonTracker();return c&&c.teardown(),this.active&&this.teardown(),this.hasRoktKit=a.hasRoktKit,!this.eventsEnabled()&&this.isKitSuppressed()?void this.log("opted out, not starting"):void(b.document.addEventListener("click",this.onEvent,CAPTURE_PASSIVE),b.document.addEventListener("submit",this.onEvent,CAPTURE_PASSIVE),b.addEventListener("blur",this.onEvent,PASSIVE),b.addEventListener("pageshow",this.onPageShow,PASSIVE),b.addEventListener("beforeunload",this.onHide,CAPTURE_PASSIVE),b.addEventListener("pagehide",this.onHide,CAPTURE_PASSIVE),b.document.addEventListener("visibilitychange",this.onHide,CAPTURE_PASSIVE),this.active=!0,!b[WIN_BTN_KEY]&&(b[WIN_BTN_KEY]={}),b[WIN_BTN_KEY].tracker=this,this.log("listening"))}},a.prototype.teardown=function(){var a=btnWindow();if(a){a.document.removeEventListener("click",this.onEvent,!0),a.document.removeEventListener("submit",this.onEvent,!0),a.removeEventListener("blur",this.onEvent),a.removeEventListener("pageshow",this.onPageShow),a.removeEventListener("beforeunload",this.onHide,!0),a.removeEventListener("pagehide",this.onHide,!0),a.document.removeEventListener("visibilitychange",this.onHide,!0);var b=a[WIN_BTN_KEY];b&&b.tracker===this&&(b.tracker=void 0);}this.active=!1,this.clearPending();},a.prototype.clearPending=function(){this.reset();var a=this.mpInstance._RoktManager;a&&a.discardCheckoutIntents();},a.prototype.reset=function(){this.generation++,this.pending=[],this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0,this.lastEvent={};},a.prototype.enqueue=function(a){this.pending.push(a),1===this.pending.length&&afterNextPaint(this.drain);},a.prototype.observe=function(a){var b=this;if(this.active&&a.isTrusted){// The button is read now, while the click is still being dispatched:
1432
+ var afterNextPaint=function(a){isFunction(window.requestAnimationFrame)&&"hidden"!==document.visibilityState?requestAnimationFrame(function(){setTimeout(a,0);}):setTimeout(a,0);},CAPTURE_PASSIVE={capture:!0,passive:!0},PASSIVE={passive:!0},ButtonTracker=/** @class */function(){function a(a){var b=this;this.active=!1,this.hasRoktKit=!1,this.generation=0,this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0,this.pending=[],this.drain=function(){var a=b.pending;b.pending=[],a.forEach(function(a){return a()});},this.onHide=function(a){("beforeunload"===a.type||"pagehide"===a.type||"hidden"===document.visibilityState)&&b.drain();},this.onPageShow=function(a){a.persisted&&b.reset();},this.onEvent=function(a){try{b.observe(a);}catch(a){b.log("[error] event handler threw: "+a);}},this.mpInstance=a;}return Object.defineProperty(a.prototype,"isActive",{get:function get(){return this.active},enumerable:!1,configurable:!0}),a.prototype.init=function(a){var b=btnWindow();if(b){var c=getActiveButtonTracker();return c&&c.teardown(),this.active&&this.teardown(),this.hasRoktKit=a.hasRoktKit,!this.eventsEnabled()&&this.isKitSuppressed()?void this.log("opted out, not starting"):void(b.document.addEventListener("click",this.onEvent,CAPTURE_PASSIVE),b.document.addEventListener("submit",this.onEvent,CAPTURE_PASSIVE),b.addEventListener("blur",this.onEvent,PASSIVE),b.addEventListener("pageshow",this.onPageShow,PASSIVE),b.addEventListener("beforeunload",this.onHide,CAPTURE_PASSIVE),b.addEventListener("pagehide",this.onHide,CAPTURE_PASSIVE),b.document.addEventListener("visibilitychange",this.onHide,CAPTURE_PASSIVE),this.active=!0,!b[WIN_BTN_KEY]&&(b[WIN_BTN_KEY]={}),b[WIN_BTN_KEY].tracker=this,this.log("listening"))}},a.prototype.teardown=function(){var a=btnWindow();if(a){a.document.removeEventListener("click",this.onEvent,!0),a.document.removeEventListener("submit",this.onEvent,!0),a.removeEventListener("blur",this.onEvent),a.removeEventListener("pageshow",this.onPageShow),a.removeEventListener("beforeunload",this.onHide,!0),a.removeEventListener("pagehide",this.onHide,!0),a.document.removeEventListener("visibilitychange",this.onHide,!0);var b=a[WIN_BTN_KEY];b&&b.tracker===this&&(b.tracker=void 0);}this.active=!1,this.clearPending();},a.prototype.clearPending=function(){this.reset();var a=this.mpInstance._RoktManager;a&&a.discardCheckoutIntents();},a.prototype.reset=function(){this.generation++,this.pending=[],this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0;},a.prototype.enqueue=function(a){this.pending.push(a),1===this.pending.length&&afterNextPaint(this.drain);},a.prototype.observe=function(a){var b=this;if(this.active&&a.isTrusted){// The button is read now, while the click is still being dispatched:
1421
1433
  // this capture listener runs before the page's own handlers, so a
1422
1434
  // button that disables or relabels itself is seen as it was clicked.
1423
1435
  // Logging and the kit hand-off wait for the next paint.
1424
- var c=a.type,d="click"===c&&this.eventsEnabled();if(d||!this.isKitSuppressed()){var e,f,g=Date.now(),h=window.location.pathname,i=!1;if("click"===c){// The composed path is only populated during dispatch.
1425
- var j=isFunction(a.composedPath)?a.composedPath().slice(0,PATH_LIMIT):[];e=d?describeButtonClick(j):void 0,f=classifyIntentControl(j,h);}else if("submit"===c){var k=a.submitter;// A form submitted without a submitter only counts right after a
1426
- // commit click, which is known once that click is delivered.
1427
- f=k?classifyIntentControl([k],h):"commit",i=!k;}var l=this.generation;this.enqueue(function(){if(l===b.generation)try{"blur"===c?f=classifyIntentFrame(document.activeElement,window):i&&g-b.lastCommitClick>SUBMIT_AFTER_COMMIT_CLICK_MS&&(f=void 0),e&&d&&b.logClick(e,g,h),f&&b.emit(f,c,g,h);}catch(a){b.log("[error] delivery threw: "+a);}});}}},a.prototype.logClick=function(a,b,c){if(this.eventsEnabled()&&(c!==this.pagePath&&(this.pagePath=c,this.pageEvents=0,this.lastEvent={}),!(this.pageEvents>=PAGE_EVENT_LIMIT))){var d=[a.label,a.ariaLabel,a.icon,a.elementId,a.elementName,a.testId,a.element,a.type].join("|"),e=this.lastEvent[d];void 0!==e&&b-e<EVENT_DEDUP_MS||(// Restarts an expired session, as public logEvent() and page views do.
1428
- this.lastEvent[d]=b,this.pageEvents++,this.mpInstance._SessionManager.resetSessionTimer(),this.mpInstance._Events.logEvent(buildButtonClickEvent(a,c)));}},a.prototype.emit=function(a,b,c,d){if(!this.isKitSuppressed()){"commit"===a&&"click"===b&&(this.lastCommitClick=c);var e=this.lastSent[a];e&&e.pathname===d&&c-e.t<THROTTLE_MS||(this.lastSent[a]={t:c,pathname:d},this.mpInstance._RoktManager.notifyCheckoutIntent(Object.freeze({kind:a,t:c}),d));}},a.prototype.eventsEnabled=function(){var a=this.mpInstance._Store;return !!a&&a.isEnabled},a.prototype.isKitSuppressed=function(){var a=this.mpInstance._RoktManager;return !this.hasRoktKit||!a||a.isCheckoutIntentSuppressed()},a.prototype.log=function(a){this.mpInstance.Logger.verbose("mParticle button tracking: "+a);},a}();// Logs every button click as an mParticle event, and hands checkout intent to the
1436
+ var c=a.type,d="click"===c&&this.eventsEnabled(),e=!this.isKitSuppressed();if(d||e){var f,g,h=Date.now(),i=window.location.pathname,j=!1;if("click"===c){// The composed path is only populated during dispatch.
1437
+ var k=findControl(isFunction(a.composedPath)?a.composedPath().slice(0,PATH_LIMIT):[]);k&&(f=d?describeButtonClick(k):void 0,g=e?classifyIntentControl(k,i):void 0);}else if("submit"===c){var l=a.submitter,k=l?findControl([l]):void 0;l?k&&(g=classifyIntentControl(k,i)):g="commit",j=!l;}var m=this.generation;this.enqueue(function(){if(m===b.generation)try{"blur"===c?g=classifyIntentFrame(document.activeElement,window):j&&h-b.lastCommitClick>SUBMIT_AFTER_COMMIT_CLICK_MS&&(g=void 0),f&&d&&b.logClick(f,i),g&&b.emit(g,c,h,i);}catch(a){b.log("[error] delivery threw: "+a);}});}}},a.prototype.logClick=function(a,b){this.eventsEnabled()&&(b!==this.pagePath&&(this.pagePath=b,this.pageEvents=0),this.pageEvents>=PAGE_EVENT_LIMIT||(// Restarts an expired session, as public logEvent() and page views do.
1438
+ this.pageEvents++,this.mpInstance._SessionManager.resetSessionTimer(),this.mpInstance._Events.logEvent(buildButtonClickEvent(a,b))));},a.prototype.emit=function(a,b,c,d){if(!this.isKitSuppressed()){"commit"===a&&"click"===b&&(this.lastCommitClick=c);var e=this.lastSent[a];e&&e.pathname===d&&c-e.t<THROTTLE_MS||(this.lastSent[a]={t:c,pathname:d},this.mpInstance._RoktManager.notifyCheckoutIntent(Object.freeze({kind:a,t:c}),d));}},a.prototype.eventsEnabled=function(){var a=this.mpInstance._Store;return !!a&&a.isEnabled},a.prototype.isKitSuppressed=function(){var a=this.mpInstance._RoktManager;return !this.hasRoktKit||!a||a.isCheckoutIntentSuppressed()},a.prototype.log=function(a){this.mpInstance.Logger.verbose("mParticle button tracking: "+a);},a}();// Logs every button click as an mParticle event, and hands checkout intent to the
1429
1439
 
1430
1440
  var Messages=Constants.Messages,HTTPCodes=Constants.HTTPCodes,FeatureFlags=Constants.FeatureFlags,CaptureIntegrationSpecificIdsV2Modes=Constants.CaptureIntegrationSpecificIdsV2Modes,ReportBatching=FeatureFlags.ReportBatching,CaptureIntegrationSpecificIds=FeatureFlags.CaptureIntegrationSpecificIds,CaptureIntegrationSpecificIdsV2=FeatureFlags.CaptureIntegrationSpecificIdsV2,StartingInitialization=Messages.InformationMessages.StartingInitialization;/**
1431
1441
  * <p>All of the following methods can be called on the primary mParticle class. In version 2.10.0, we introduced <a href="https://docs.mparticle.com/developers/sdk/web/multiple-instances/">multiple instances</a>. If you are using multiple instances (self hosted environments only), you should call these methods on each instance.</p>
@@ -15,7 +15,7 @@ map:function map(a,b){var c,d,e;if(null===this)throw new TypeError(" this is nul
15
15
  filter:function filter(a/*, thisArg*/){if(void 0===this||null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if("function"!=typeof a)throw new TypeError;for(var d=[],e=2<=arguments.length?arguments[1]:void 0,f=0;f<c;f++)if(f in b){var g=b[f];a.call(e,g,f,b)&&d.push(g);}return d},// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
16
16
  isArray:function isArray(a){return "[object Array]"===Object.prototype.toString.call(a)},Base64:Base64$1};
17
17
 
18
- var version = "3.18.0";
18
+ var version = "3.19.0";
19
19
 
20
20
  var Constants={sdkVersion:version,sdkVendor:"mparticle",platform:"web",Messages:{DeprecationMessages:{MethodHasBeenDeprecated:"has been deprecated.",MethodMarkedForDeprecationPostfix:"is a deprecated method and will be removed in future releases.",AlternativeMethodPrefix:"Please use the alternate method:"},ErrorMessages:{NoToken:"A token must be specified.",EventNameInvalidType:"Event name must be a valid string value.",EventDataInvalidType:"Event data must be a valid object hash.",LoggingDisabled:"Event logging is currently disabled.",CookieParseError:"Could not parse cookie",EventEmpty:"Event object is null or undefined, cancelling send",APIRequestEmpty:"APIRequest is null or undefined, cancelling send",NoEventType:"Event type must be specified.",TransactionIdRequired:"Transaction ID is required",TransactionRequired:"A transaction attributes object is required",PromotionIdRequired:"Promotion ID is required",BadAttribute:"Attribute value cannot be object or array",BadKey:"Key value cannot be object or array",BadLogPurchase:"Transaction attributes and a product are both required to log a purchase, https://docs.mparticle.com/?javascript#measuring-transactions",AudienceAPINotEnabled:"Your workspace is not enabled to retrieve user audiences."},InformationMessages:{CookieSearch:"Searching for cookie",CookieFound:"Cookie found, parsing values",CookieNotFound:"Cookies not found",CookieSet:"Setting cookie",CookieSync:"Performing cookie sync",SendBegin:"Starting to send event",SendIdentityBegin:"Starting to send event to identity server",SendWindowsPhone:"Sending event to Windows Phone container",SendIOS:"Calling iOS path: ",SendAndroid:"Calling Android JS interface method: ",SendHttp:"Sending event to mParticle HTTP service",SendAliasHttp:"Sending alias request to mParticle HTTP service",SendIdentityHttp:"Sending event to mParticle HTTP service",StartingNewSession:"Starting new Session",StartingLogEvent:"Starting to log event",StartingLogOptOut:"Starting to log user opt in/out",StartingEndSession:"Starting to end session",StartingInitialization:"Starting to initialize",StartingLogCommerceEvent:"Starting to log commerce event",StartingAliasRequest:"Starting to Alias MPIDs",LoadingConfig:"Loading configuration options",AbandonLogEvent:"Cannot log event, logging disabled or developer token not set",AbandonAliasUsers:"Cannot Alias Users, logging disabled or developer token not set",AbandonStartSession:"Cannot start session, logging disabled or developer token not set",AbandonEndSession:"Cannot end session, logging disabled or developer token not set",NoSessionToEnd:"Cannot end session, no active session found"},ValidationMessages:{ModifyIdentityRequestUserIdentitiesPresent:"identityRequests to modify require userIdentities to be present. Request not sent to server. Please fix and try again",IdentityRequesetInvalidKey:"There is an invalid key on your identityRequest object. It can only contain a `userIdentities` object and a `onUserAlias` function. Request not sent to server. Please fix and try again.",OnUserAliasType:"The onUserAlias value must be a function.",UserIdentities:"The userIdentities key must be an object with keys of identityTypes and values of strings. Request not sent to server. Please fix and try again.",UserIdentitiesInvalidKey:"There is an invalid identity key on your `userIdentities` object within the identityRequest. Request not sent to server. Please fix and try again.",UserIdentitiesInvalidValues:"All user identity values must be strings or null. Request not sent to server. Please fix and try again.",AliasMissingMpid:"Alias Request must contain both a destinationMpid and a sourceMpid",AliasNonUniqueMpid:"Alias Request's destinationMpid and sourceMpid must be unique",AliasMissingTime:"Alias Request must have both a startTime and an endTime",AliasStartBeforeEndTime:"Alias Request's endTime must be later than its startTime"}},NativeSdkPaths:{LogEvent:"logEvent",SetUserTag:"setUserTag",RemoveUserTag:"removeUserTag",SetUserAttribute:"setUserAttribute",RemoveUserAttribute:"removeUserAttribute",SetSessionAttribute:"setSessionAttribute",LogOut:"logOut",SetUserAttributeList:"setUserAttributeList",RemoveAllUserAttributes:"removeAllUserAttributes",GetUserAttributesLists:"getUserAttributesLists",GetAllUserAttributes:"getAllUserAttributes",Identify:"identify",Logout:"logout",Login:"login",Modify:"modify",Alias:"aliasUsers",Upload:"upload"},StorageNames:{localStorageName:"mprtcl-api",localStorageNameV3:"mprtcl-v3",cookieName:"mprtcl-api",cookieNameV2:"mprtcl-v2",cookieNameV3:"mprtcl-v3",localStorageNameV4:"mprtcl-v4",cookieNameV4:"mprtcl-v4",currentStorageName:"mprtcl-v4"},DefaultConfig:{cookieDomain:null,cookieExpiration:365,logLevel:null,timeout:300,sessionTimeout:30,forwarderStatsTimeout:5e3,integrationDelayTimeout:5e3,maxCookieSize:3e3,aliasMaxWindow:90,uploadInterval:0// Maximum milliseconds in between batch uploads, below 500 will mean immediate upload. The server returns this as a string, but we are using it as a number internally
21
21
  },DefaultBaseUrls:{v1SecureServiceUrl:"jssdks.mparticle.com/v1/JS/",v2SecureServiceUrl:"jssdks.mparticle.com/v2/JS/",v3SecureServiceUrl:"jssdks.mparticle.com/v3/JS/",configUrl:"jssdkcdns.mparticle.com/JS/v2/",identityUrl:"identity.mparticle.com/v1/",aliasUrl:"jssdks.mparticle.com/v1/identity/",userAudienceUrl:"nativesdks.mparticle.com/v1/"},// These are the paths that are used to construct the CNAME urls
@@ -1359,10 +1359,12 @@ var ErrorReportingDispatcher=/** @class */function(){function a(){this.services=
1359
1359
 
1360
1360
  var LoggingDispatcher=/** @class */function(){function a(){this.services=[];}return a.prototype.register=function(a){this.services.push(a);},a.prototype.log=function(a){var b=this;this.services.forEach(function(c){var d;try{c.log(a);}catch(a){null===(d=b.logger)||void 0===d?void 0:d.error("Error in LoggingService: "+a);}});},a}();
1361
1361
 
1362
- var AUTO_BUTTON_ATTRIBUTE="is_auto_button_click";var BUTTON_CLICK_EVENT_NAME="Button Click";// One custom event per click, logged like logLink() with the auto marker and the
1362
+ var AUTO_BUTTON_ATTRIBUTE="is_auto_button_click";// Specific enough not to merge with a customer's own 'Button Click' event.
1363
+ var BUTTON_CLICK_EVENT_NAME="Auto Button Click";// One custom event per click, logged like logLink() with the auto marker and the
1363
1364
  // page fields that auto page views carry. No intent: classifying clicks is left
1364
1365
  // to whoever reads the data.
1365
- var buildButtonClickEvent=function(a,b){var c,d=(c={label_source:a.labelSource,element:a.element,type:a.type,path:b,hostname:window.location.hostname},c[AUTO_BUTTON_ATTRIBUTE]=!0,c);return a.label&&(d.label=a.label),a.ariaLabel&&(d.aria_label=a.ariaLabel),a.icon&&(d.icon=a.icon),a.elementId&&(d.element_id=a.elementId),a.elementName&&(d.element_name=a.elementName),a.testId&&(d.test_id=a.testId),{messageType:MessageType$1.PageEvent,name:BUTTON_CLICK_EVENT_NAME,data:d,eventType:EventType.Other}};
1366
+ var buildButtonClickEvent=function(a,b){// Optional fields are left out when the button has no safe value for them.
1367
+ for(var c,d,e=(c={label_source:a.labelSource,element:a.element,type:a.type,path:b,hostname:window.location.hostname},c[AUTO_BUTTON_ATTRIBUTE]=!0,c),f={label:a.label,aria_label:a.ariaLabel,icon:a.icon,element_id:a.elementId,element_name:a.elementName,test_id:a.testId,controls:a.controls,form:a.form},g=0,h=Object.keys(f);g<h.length;g++)d=h[g],f[d]&&(e[d]=f[d]);return {messageType:MessageType$1.PageEvent,name:BUTTON_CLICK_EVENT_NAME,data:e,eventType:EventType.Other}};
1366
1368
 
1367
1369
  // descriptions. Rokt's own elements, disabled controls and anything under
1368
1370
  // data-mp-no-track are never returned.
@@ -1371,7 +1373,8 @@ function hasComposedAncestor(a,b){for(var c=a;c;){if(c.matches(b))return !0;var
1371
1373
  function isRoktElement(a){if(hasComposedAncestor(a,"[data-core-type], #rokt-controller-frame"))return !0;var b=a.getAttribute("src");if(b)try{if(-1!==new URL(b,"https://example.test").pathname.indexOf("/wsdk/"))return !0}catch(a){// An unparseable src is not a Rokt element.
1372
1374
  }return !1}function isWalletElement(a){return a.matches("apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]")}// Walks at most 12 entries of an event's composed path for the nearest control.
1373
1375
  // Rokt elements, disabled controls and anything under data-mp-no-track are skipped.
1374
- function findControl(a){for(var b,c,d=0,e=a.slice(0,12);d<e.length;d++)if(c=e[d],!!(c instanceof Element)){if(isRoktElement(c)||c.matches("[data-mp-no-track]"))return;!b&&(c.matches("button, input[type=\"submit\"], input[type=\"button\"], input[type=\"image\"], [role=\"button\"], a[href]")||isWalletElement(c))&&(b=c);}return !b||isRoktElement(b)||hasComposedAncestor(b,"[data-mp-no-track]")||b.matches(":disabled, [aria-disabled=\"true\"]")?void 0:b}
1376
+ function findControl(a){for(var b,c,d=0,e=a.slice(0,12);d<e.length;d++)if(c=e[d],!!(c instanceof Element)){if(isRoktElement(c)||c.matches("[data-mp-no-track]"))return;!b&&(c.matches("button, input[type=\"submit\"], input[type=\"button\"], input[type=\"image\"], [role=\"button\"], a[href]")||isWalletElement(c))&&(b=c);}return !b||isRoktElement(b)||hasComposedAncestor(b,"[data-mp-no-track]")||b.matches(":disabled, [aria-disabled=\"true\"]")?void 0:b}// fields, editable regions, and anything under data-mp-no-track.
1377
+ function isUnsafeTextSource(a){return a.matches("input, select, textarea, option, optgroup, datalist, [contenteditable]:not([contenteditable=\"false\"])")||!!a.querySelector("input, select, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")||hasComposedAncestor(a,"select, datalist, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")}
1375
1378
 
1376
1379
  // Masks a button label before it is logged. Button text can carry personal data,
1377
1380
  // so this is the privacy boundary for every label and aria-label.
@@ -1381,8 +1384,7 @@ function createDigitPatterns(){try{return {run:/\p{Nd}{6,}/u,number:/\p{Nd}+(?:[
1381
1384
  // masked. Dropped when it looks like an email, phone or card number.
1382
1385
  function maskLabel(a){if(a&&!(128<a.length)&&digitPatterns){var b=a.replace(/\s+/g," ").trim().toLowerCase();if(b&&-1===b.indexOf("@")&&!digitPatterns.run.test(b.replace(/[\s()\-\u2010-\u2015]/g,""))){var c=b.replace(digitPatterns.number,"#").slice(0,64).trim();return c||void 0}}}
1383
1386
 
1384
- function elementKind(a){if(isWalletElement(a))return "wallet";var b=a.tagName.toLowerCase();return "button"===b||"input"===b?b:"role"}// fields, editable regions, and anything under data-mp-no-track.
1385
- function isUnsafeTextSource(a){return a.matches("input, select, textarea, option, optgroup, datalist, [contenteditable]:not([contenteditable=\"false\"])")||!!a.querySelector("input, select, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")||hasComposedAncestor(a,"select, datalist, textarea, [data-mp-no-track], [contenteditable]:not([contenteditable=\"false\"])")}function textOf(a){return a&&!isUnsafeTextSource(a)&&a.textContent?a.textContent.trim():""}function nonBlank(a){return a&&a.trim()?a:void 0}// The button's name as a screen reader would read it, simplified: the first of
1387
+ function elementKind(a){if(isWalletElement(a))return "wallet";var b=a.tagName.toLowerCase();return "button"===b||"input"===b?b:"role"}function textOf(a){return a&&!isUnsafeTextSource(a)&&a.textContent?a.textContent.trim():""}function nonBlank(a){return a&&a.trim()?a:void 0}// The button's name as a screen reader would read it, simplified: the first of
1386
1388
  // aria-labelledby, aria-label, own text (or an inner image's alt), title, and for
1387
1389
  // inputs their value. The text sources are skipped for controls that hold form
1388
1390
  // fields or more than four children, so a typed value is never read.
@@ -1396,36 +1398,44 @@ function iconHint(a){var b=function(a){return !hasComposedAncestor(a,"[data-mp-n
1396
1398
  // (`ember1234`, `a8f3e2c1`, `radix-:r1:`) and long numbers don't match.
1397
1399
  var HAND_WRITTEN_ID=/^[A-Za-z]+[0-9]{0,3}(?:[-_](?:[A-Za-z]+[0-9]{0,3}|[0-9]{1,3}))*$/;// A developer-written identifier, or nothing.
1398
1400
  function stableIdentifier(a){if(a){// At most three digits in all, so numbers split into groups don't pass.
1399
- var b=a.trim(),c=b.replace(/[^0-9]/g,"").length;return 64>=b.length&&3>=c&&HAND_WRITTEN_ID.test(b)?b:void 0}}var TEST_ID_ATTRIBUTES=["data-testid","data-test-id","data-test","data-qa","data-cy"];function testIdentifier(a){for(var b=0,c=TEST_ID_ATTRIBUTES;b<c.length;b++){var d=c[b],e=stableIdentifier(a.getAttribute(d));if(e)return e}}// The browser's normalised type: a button defaults to 'submit' and an unknown
1401
+ var b=a.trim(),c=b.replace(/[^0-9]/g,"").length;return 64>=b.length&&3>=c&&HAND_WRITTEN_ID.test(b)?b:void 0}}var TEST_ID_ATTRIBUTES=["data-testid","data-test-id","data-test","data-qa","data-cy"];// The first of these attributes that holds a developer-written identifier.
1402
+ function firstIdentifier(a,b){for(var c=0,d=b;c<d.length;c++){var e=d[c],f=stableIdentifier(a.getAttribute(e));if(f)return f}}// What the button opens or acts on: a menu, drawer or dialog. aria-controls can
1403
+ // list several ids, so only the first is used.
1404
+ function controlsIdentifier(a){var b=a.getAttribute("aria-controls");return stableIdentifier(b?b.trim().split(/\s+/)[0]:null)||firstIdentifier(a,["popovertarget","commandfor"])}// The id of the form a button or input actually submits, which the browser
1405
+ // resolves from its form attribute or its owning form. getAttribute, because a
1406
+ // field named "id" shadows form.id.
1407
+ function formIdentifier(a){var b=a instanceof HTMLButtonElement||a instanceof HTMLInputElement?a.form:null;return b?stableIdentifier(b.getAttribute("id")):void 0}// The browser's normalised type: a button defaults to 'submit' and an unknown
1400
1408
  // input type reads as 'text'. Wallet and role buttons are just 'button'.
1401
- function controlType(a,b){return "button"===b||"input"===b?a.type:"button"}// Describes a clicked button for auto button tracking. Links are left to page
1409
+ function controlType(a,b){return "button"===b||"input"===b?a.type:"button"}// Describes a clicked control for auto button tracking. Links are left to page
1402
1410
  // views, so a link click is not described.
1403
- function describeButtonClick(a){var b=findControl(a);if(b&&!b.matches("a[href]:not([role=\"button\"])")){var c=elementKind(b),d=accessibleName(b),e=maskLabel(d.raw);return {label:e,labelSource:e?d.source:"none",// Kept on its own as well: it often says more than the visible text,
1411
+ function describeButtonClick(a){if(!a.matches("a[href]:not([role=\"button\"])")){var b=elementKind(a),c=accessibleName(a),d=maskLabel(c.raw);return {label:d,labelSource:d?c.source:"none",// Kept on its own as well: it often says more than the visible text,
1404
1412
  // such as which item a remove button removes.
1405
- ariaLabel:maskLabel(b.getAttribute("aria-label")),icon:iconHint(b),elementId:stableIdentifier(b.getAttribute("id")),elementName:stableIdentifier(b.getAttribute("name")),testId:testIdentifier(b),element:c,type:controlType(b,c)}}}
1413
+ ariaLabel:maskLabel(a.getAttribute("aria-label")),icon:iconHint(a),elementId:stableIdentifier(a.getAttribute("id")),elementName:stableIdentifier(a.getAttribute("name")),testId:firstIdentifier(a,TEST_ID_ATTRIBUTES),controls:controlsIdentifier(a),form:formIdentifier(a),element:b,type:controlType(a,b)}}}
1406
1414
 
1407
1415
  var checkoutPath=/(?:^|\/)(?:checkouts?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i,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\xF3n|iniciar sesi\xF3n|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"),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\xDFen|bestellung abschlie\xDFen|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\xF3n 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"+")$"),routeGatedCommits=/^(?: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)$/,steps=new RegExp("^(?:"+"checkout|check out|check-out|continue to payment|proceed to checkout|zur kasse|zur kasse gehen|auschecken|weiter zur zahlung|passer \xE0 la caisse|continuer vers le paiement|proc\xE9der 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)?"+")$"),wallets=/^(?:apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|affirm)$/,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"]),TYPOGRAPHIC_APOSTROPHES=/[\u2019\u2018]/g,CURRENCY_AND_NUMBER_CHARS=/[0-9\uFF10-\uFF19$\u20AC\u00A3\u00A5\uFFE5\u20B9\u5186.,\uFF0C]/g;// Exact HTTPS hosts of hosted wallet and payment frames.
1408
1416
  // Keep these classes ASCII-only in source bytes to avoid mojibake parse failures
1409
1417
  // when a host page forces non-UTF-8 decoding before JavaScript parse.
1410
1418
  function getIntentRouteClass(a){return checkoutPath.test(a)?"checkout_path":"other"}function normalize(a){return !a||128<a.length?void 0:a.toLowerCase().replace(TYPOGRAPHIC_APOSTROPHES,"'").replace(CURRENCY_AND_NUMBER_CHARS,"").replace(/\s+/g," ").trim()}// Labels come from aria-label then visible text. Only submit and button inputs
1411
- // have their value read, so a typed field value is never touched.
1412
- function labelSources(a){if(4<a.children.length||a.querySelector("input, select, textarea, [data-mp-no-track]"))return [];var b=null;if(a instanceof HTMLInputElement){var c=a.type;"submit"===c||"button"===c?b=a.value:"image"===c&&(b=a.getAttribute("alt"));}else b=a.textContent;return [a.getAttribute("aria-label"),b]}function labels(a){return labelSources(a).map(normalize).filter(function(a){return !!a})}function classifyLabel(a,b,c){return commits.test(a)||routeGatedCommits.test(a)&&"checkout_path"===getIntentRouteClass(c)?"commit":b&&wallets.test(a)?"wallet":steps.test(a)?"checkout_step":void 0}function classifyIntentControl(a,b){var c=findControl(a);return c?classifyControl(c,b):void 0}function classifyControl(a,b){var c=labels(a);if(!c.some(function(a){return exclusions.test(a)})){var d=!1;if(a.matches("a[href]")||(d=a.matches(CONTROL_SELECTOR)||isWalletElement(a)),d&&isWalletElement(a))return "wallet";for(var e=0,f=c;e<f.length;e++){var g=f[e],h=classifyLabel(g,d,b);if(h)return h}}}// Focus moving into a visible hosted wallet or payment frame.
1419
+ // have their value read, so a typed field value is never touched. Other text
1420
+ // follows the same rules as button descriptions.
1421
+ function labelSources(a){if(a instanceof HTMLInputElement){var b=a.type,c=null;return "submit"===b||"button"===b?c=a.value:"image"===b&&(c=a.getAttribute("alt")),[a.getAttribute("aria-label"),c]}return 4<a.children.length||isUnsafeTextSource(a)?[]:[a.getAttribute("aria-label"),a.textContent]}function labels(a){return labelSources(a).map(normalize).filter(function(a){return !!a})}function classifyLabel(a,b,c){return commits.test(a)||routeGatedCommits.test(a)&&"checkout_path"===getIntentRouteClass(c)?"commit":b&&wallets.test(a)?"wallet":steps.test(a)?"checkout_step":void 0}// Classifies a clicked or submitting control found by findControl.
1422
+ function classifyIntentControl(a,b){var c=labels(a);if(!c.some(function(a){return exclusions.test(a)})){var d=!1;if(a.matches("a[href]")||(d=a.matches(CONTROL_SELECTOR)||isWalletElement(a)),d&&isWalletElement(a))return "wallet";for(var e=0,f=c;e<f.length;e++){var g=f[e],h=classifyLabel(g,d,b);if(h)return h}}}// Focus moving into a visible hosted wallet or payment frame.
1413
1423
  function classifyIntentFrame(a,b){if("visible"===b.document.visibilityState&&a instanceof HTMLIFrameElement&&!isRoktElement(a))try{var c=new URL(a.src);if("https:"===c.protocol&&frameHosts.has(c.hostname))return "frame"}catch(a){// An unparseable frame src is not a wallet frame.
1414
1424
  }}
1415
1425
 
1416
1426
  // listeners. Inspect `window.__mpBtn__` in a console to see if one is live.
1417
- var WIN_BTN_KEY="__mpBtn__";var THROTTLE_MS=2e3,SUBMIT_AFTER_COMMIT_CLICK_MS=1e3,PATH_LIMIT=12,EVENT_DEDUP_MS=1e3,PAGE_EVENT_LIMIT=50,btnWindow=function(){return "undefined"==typeof window?null:window};var getActiveButtonTracker=function(){var a=btnWindow(),b=a?a[WIN_BTN_KEY]:void 0;return b?b.tracker:void 0};// Stops whichever tracker is active, including one from an earlier bundle load.
1427
+ var WIN_BTN_KEY="__mpBtn__";var THROTTLE_MS=2e3,SUBMIT_AFTER_COMMIT_CLICK_MS=1e3,PATH_LIMIT=12,PAGE_EVENT_LIMIT=100,btnWindow=function(){return "undefined"==typeof window?null:window};// A ceiling, not a dedupe: a held Enter key or browser automation fires a
1428
+ // trusted click per repeat, and each one is forwarded to every kit.
1429
+ var getActiveButtonTracker=function(){var a=btnWindow(),b=a?a[WIN_BTN_KEY]:void 0;return b?b.tracker:void 0};// Stops whichever tracker is active, including one from an earlier bundle load.
1418
1430
  var resetButtonTracking=function(){var a=btnWindow();if(a){var b=getActiveButtonTracker();b&&b.teardown(),a[WIN_BTN_KEY]={};}};// Runs once the browser has painted the page's response to the interaction, so
1419
1431
  // describing and logging a click never delays that paint.
1420
- var afterNextPaint=function(a){isFunction(window.requestAnimationFrame)&&"hidden"!==document.visibilityState?requestAnimationFrame(function(){setTimeout(a,0);}):setTimeout(a,0);},CAPTURE_PASSIVE={capture:!0,passive:!0},PASSIVE={passive:!0},ButtonTracker=/** @class */function(){function a(a){var b=this;this.active=!1,this.hasRoktKit=!1,this.generation=0,this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0,this.lastEvent={},this.pending=[],this.drain=function(){var a=b.pending;b.pending=[],a.forEach(function(a){return a()});},this.onHide=function(a){("beforeunload"===a.type||"pagehide"===a.type||"hidden"===document.visibilityState)&&b.drain();},this.onPageShow=function(a){a.persisted&&b.reset();},this.onEvent=function(a){try{b.observe(a);}catch(a){b.log("[error] event handler threw: "+a);}},this.mpInstance=a;}return Object.defineProperty(a.prototype,"isActive",{get:function get(){return this.active},enumerable:!1,configurable:!0}),a.prototype.init=function(a){var b=btnWindow();if(b){var c=getActiveButtonTracker();return c&&c.teardown(),this.active&&this.teardown(),this.hasRoktKit=a.hasRoktKit,!this.eventsEnabled()&&this.isKitSuppressed()?void this.log("opted out, not starting"):void(b.document.addEventListener("click",this.onEvent,CAPTURE_PASSIVE),b.document.addEventListener("submit",this.onEvent,CAPTURE_PASSIVE),b.addEventListener("blur",this.onEvent,PASSIVE),b.addEventListener("pageshow",this.onPageShow,PASSIVE),b.addEventListener("beforeunload",this.onHide,CAPTURE_PASSIVE),b.addEventListener("pagehide",this.onHide,CAPTURE_PASSIVE),b.document.addEventListener("visibilitychange",this.onHide,CAPTURE_PASSIVE),this.active=!0,!b[WIN_BTN_KEY]&&(b[WIN_BTN_KEY]={}),b[WIN_BTN_KEY].tracker=this,this.log("listening"))}},a.prototype.teardown=function(){var a=btnWindow();if(a){a.document.removeEventListener("click",this.onEvent,!0),a.document.removeEventListener("submit",this.onEvent,!0),a.removeEventListener("blur",this.onEvent),a.removeEventListener("pageshow",this.onPageShow),a.removeEventListener("beforeunload",this.onHide,!0),a.removeEventListener("pagehide",this.onHide,!0),a.document.removeEventListener("visibilitychange",this.onHide,!0);var b=a[WIN_BTN_KEY];b&&b.tracker===this&&(b.tracker=void 0);}this.active=!1,this.clearPending();},a.prototype.clearPending=function(){this.reset();var a=this.mpInstance._RoktManager;a&&a.discardCheckoutIntents();},a.prototype.reset=function(){this.generation++,this.pending=[],this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0,this.lastEvent={};},a.prototype.enqueue=function(a){this.pending.push(a),1===this.pending.length&&afterNextPaint(this.drain);},a.prototype.observe=function(a){var b=this;if(this.active&&a.isTrusted){// The button is read now, while the click is still being dispatched:
1432
+ var afterNextPaint=function(a){isFunction(window.requestAnimationFrame)&&"hidden"!==document.visibilityState?requestAnimationFrame(function(){setTimeout(a,0);}):setTimeout(a,0);},CAPTURE_PASSIVE={capture:!0,passive:!0},PASSIVE={passive:!0},ButtonTracker=/** @class */function(){function a(a){var b=this;this.active=!1,this.hasRoktKit=!1,this.generation=0,this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0,this.pending=[],this.drain=function(){var a=b.pending;b.pending=[],a.forEach(function(a){return a()});},this.onHide=function(a){("beforeunload"===a.type||"pagehide"===a.type||"hidden"===document.visibilityState)&&b.drain();},this.onPageShow=function(a){a.persisted&&b.reset();},this.onEvent=function(a){try{b.observe(a);}catch(a){b.log("[error] event handler threw: "+a);}},this.mpInstance=a;}return Object.defineProperty(a.prototype,"isActive",{get:function get(){return this.active},enumerable:!1,configurable:!0}),a.prototype.init=function(a){var b=btnWindow();if(b){var c=getActiveButtonTracker();return c&&c.teardown(),this.active&&this.teardown(),this.hasRoktKit=a.hasRoktKit,!this.eventsEnabled()&&this.isKitSuppressed()?void this.log("opted out, not starting"):void(b.document.addEventListener("click",this.onEvent,CAPTURE_PASSIVE),b.document.addEventListener("submit",this.onEvent,CAPTURE_PASSIVE),b.addEventListener("blur",this.onEvent,PASSIVE),b.addEventListener("pageshow",this.onPageShow,PASSIVE),b.addEventListener("beforeunload",this.onHide,CAPTURE_PASSIVE),b.addEventListener("pagehide",this.onHide,CAPTURE_PASSIVE),b.document.addEventListener("visibilitychange",this.onHide,CAPTURE_PASSIVE),this.active=!0,!b[WIN_BTN_KEY]&&(b[WIN_BTN_KEY]={}),b[WIN_BTN_KEY].tracker=this,this.log("listening"))}},a.prototype.teardown=function(){var a=btnWindow();if(a){a.document.removeEventListener("click",this.onEvent,!0),a.document.removeEventListener("submit",this.onEvent,!0),a.removeEventListener("blur",this.onEvent),a.removeEventListener("pageshow",this.onPageShow),a.removeEventListener("beforeunload",this.onHide,!0),a.removeEventListener("pagehide",this.onHide,!0),a.document.removeEventListener("visibilitychange",this.onHide,!0);var b=a[WIN_BTN_KEY];b&&b.tracker===this&&(b.tracker=void 0);}this.active=!1,this.clearPending();},a.prototype.clearPending=function(){this.reset();var a=this.mpInstance._RoktManager;a&&a.discardCheckoutIntents();},a.prototype.reset=function(){this.generation++,this.pending=[],this.lastCommitClick=-Infinity,this.lastSent={},this.pagePath=null,this.pageEvents=0;},a.prototype.enqueue=function(a){this.pending.push(a),1===this.pending.length&&afterNextPaint(this.drain);},a.prototype.observe=function(a){var b=this;if(this.active&&a.isTrusted){// The button is read now, while the click is still being dispatched:
1421
1433
  // this capture listener runs before the page's own handlers, so a
1422
1434
  // button that disables or relabels itself is seen as it was clicked.
1423
1435
  // Logging and the kit hand-off wait for the next paint.
1424
- var c=a.type,d="click"===c&&this.eventsEnabled();if(d||!this.isKitSuppressed()){var e,f,g=Date.now(),h=window.location.pathname,i=!1;if("click"===c){// The composed path is only populated during dispatch.
1425
- var j=isFunction(a.composedPath)?a.composedPath().slice(0,PATH_LIMIT):[];e=d?describeButtonClick(j):void 0,f=classifyIntentControl(j,h);}else if("submit"===c){var k=a.submitter;// A form submitted without a submitter only counts right after a
1426
- // commit click, which is known once that click is delivered.
1427
- f=k?classifyIntentControl([k],h):"commit",i=!k;}var l=this.generation;this.enqueue(function(){if(l===b.generation)try{"blur"===c?f=classifyIntentFrame(document.activeElement,window):i&&g-b.lastCommitClick>SUBMIT_AFTER_COMMIT_CLICK_MS&&(f=void 0),e&&d&&b.logClick(e,g,h),f&&b.emit(f,c,g,h);}catch(a){b.log("[error] delivery threw: "+a);}});}}},a.prototype.logClick=function(a,b,c){if(this.eventsEnabled()&&(c!==this.pagePath&&(this.pagePath=c,this.pageEvents=0,this.lastEvent={}),!(this.pageEvents>=PAGE_EVENT_LIMIT))){var d=[a.label,a.ariaLabel,a.icon,a.elementId,a.elementName,a.testId,a.element,a.type].join("|"),e=this.lastEvent[d];void 0!==e&&b-e<EVENT_DEDUP_MS||(// Restarts an expired session, as public logEvent() and page views do.
1428
- this.lastEvent[d]=b,this.pageEvents++,this.mpInstance._SessionManager.resetSessionTimer(),this.mpInstance._Events.logEvent(buildButtonClickEvent(a,c)));}},a.prototype.emit=function(a,b,c,d){if(!this.isKitSuppressed()){"commit"===a&&"click"===b&&(this.lastCommitClick=c);var e=this.lastSent[a];e&&e.pathname===d&&c-e.t<THROTTLE_MS||(this.lastSent[a]={t:c,pathname:d},this.mpInstance._RoktManager.notifyCheckoutIntent(Object.freeze({kind:a,t:c}),d));}},a.prototype.eventsEnabled=function(){var a=this.mpInstance._Store;return !!a&&a.isEnabled},a.prototype.isKitSuppressed=function(){var a=this.mpInstance._RoktManager;return !this.hasRoktKit||!a||a.isCheckoutIntentSuppressed()},a.prototype.log=function(a){this.mpInstance.Logger.verbose("mParticle button tracking: "+a);},a}();// Logs every button click as an mParticle event, and hands checkout intent to the
1436
+ var c=a.type,d="click"===c&&this.eventsEnabled(),e=!this.isKitSuppressed();if(d||e){var f,g,h=Date.now(),i=window.location.pathname,j=!1;if("click"===c){// The composed path is only populated during dispatch.
1437
+ var k=findControl(isFunction(a.composedPath)?a.composedPath().slice(0,PATH_LIMIT):[]);k&&(f=d?describeButtonClick(k):void 0,g=e?classifyIntentControl(k,i):void 0);}else if("submit"===c){var l=a.submitter,k=l?findControl([l]):void 0;l?k&&(g=classifyIntentControl(k,i)):g="commit",j=!l;}var m=this.generation;this.enqueue(function(){if(m===b.generation)try{"blur"===c?g=classifyIntentFrame(document.activeElement,window):j&&h-b.lastCommitClick>SUBMIT_AFTER_COMMIT_CLICK_MS&&(g=void 0),f&&d&&b.logClick(f,i),g&&b.emit(g,c,h,i);}catch(a){b.log("[error] delivery threw: "+a);}});}}},a.prototype.logClick=function(a,b){this.eventsEnabled()&&(b!==this.pagePath&&(this.pagePath=b,this.pageEvents=0),this.pageEvents>=PAGE_EVENT_LIMIT||(// Restarts an expired session, as public logEvent() and page views do.
1438
+ this.pageEvents++,this.mpInstance._SessionManager.resetSessionTimer(),this.mpInstance._Events.logEvent(buildButtonClickEvent(a,b))));},a.prototype.emit=function(a,b,c,d){if(!this.isKitSuppressed()){"commit"===a&&"click"===b&&(this.lastCommitClick=c);var e=this.lastSent[a];e&&e.pathname===d&&c-e.t<THROTTLE_MS||(this.lastSent[a]={t:c,pathname:d},this.mpInstance._RoktManager.notifyCheckoutIntent(Object.freeze({kind:a,t:c}),d));}},a.prototype.eventsEnabled=function(){var a=this.mpInstance._Store;return !!a&&a.isEnabled},a.prototype.isKitSuppressed=function(){var a=this.mpInstance._RoktManager;return !this.hasRoktKit||!a||a.isCheckoutIntentSuppressed()},a.prototype.log=function(a){this.mpInstance.Logger.verbose("mParticle button tracking: "+a);},a}();// Logs every button click as an mParticle event, and hands checkout intent to the
1429
1439
 
1430
1440
  var Messages=Constants.Messages,HTTPCodes=Constants.HTTPCodes,FeatureFlags=Constants.FeatureFlags,CaptureIntegrationSpecificIdsV2Modes=Constants.CaptureIntegrationSpecificIdsV2Modes,ReportBatching=FeatureFlags.ReportBatching,CaptureIntegrationSpecificIds=FeatureFlags.CaptureIntegrationSpecificIds,CaptureIntegrationSpecificIdsV2=FeatureFlags.CaptureIntegrationSpecificIdsV2,StartingInitialization=Messages.InformationMessages.StartingInitialization;/**
1431
1441
  * <p>All of the following methods can be called on the primary mParticle class. In version 2.10.0, we introduced <a href="https://docs.mparticle.com/developers/sdk/web/multiple-instances/">multiple instances</a>. If you are using multiple instances (self hosted environments only), you should call these methods on each instance.</p>
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.18.0";
207
+ var version = "3.19.0";
208
208
 
209
209
  var Constants = {
210
210
  sdkVersion: version,
@@ -11461,7 +11461,8 @@ var mParticle = (function () {
11461
11461
 
11462
11462
  // Marks an automatically logged button click, like `is_auto_page_view`.
11463
11463
  var AUTO_BUTTON_ATTRIBUTE = 'is_auto_button_click';
11464
- var BUTTON_CLICK_EVENT_NAME = 'Button Click';
11464
+ // Specific enough not to merge with a customer's own 'Button Click' event.
11465
+ var BUTTON_CLICK_EVENT_NAME = 'Auto Button Click';
11465
11466
  // One custom event per click, logged like logLink() with the auto marker and the
11466
11467
  // page fields that auto page views carry. No intent: classifying clicks is left
11467
11468
  // to whoever reads the data.
@@ -11474,23 +11475,22 @@ var mParticle = (function () {
11474
11475
  path: pathname,
11475
11476
  hostname: window.location.hostname
11476
11477
  }, _a[AUTO_BUTTON_ATTRIBUTE] = true, _a);
11477
- if (click.label) {
11478
- data.label = click.label;
11479
- }
11480
- if (click.ariaLabel) {
11481
- data.aria_label = click.ariaLabel;
11482
- }
11483
- if (click.icon) {
11484
- data.icon = click.icon;
11485
- }
11486
- if (click.elementId) {
11487
- data.element_id = click.elementId;
11488
- }
11489
- if (click.elementName) {
11490
- data.element_name = click.elementName;
11491
- }
11492
- if (click.testId) {
11493
- data.test_id = click.testId;
11478
+ // Optional fields are left out when the button has no safe value for them.
11479
+ var optional = {
11480
+ label: click.label,
11481
+ aria_label: click.ariaLabel,
11482
+ icon: click.icon,
11483
+ element_id: click.elementId,
11484
+ element_name: click.elementName,
11485
+ test_id: click.testId,
11486
+ controls: click.controls,
11487
+ form: click.form
11488
+ };
11489
+ for (var _i = 0, _b = Object.keys(optional); _i < _b.length; _i++) {
11490
+ var key = _b[_i];
11491
+ if (optional[key]) {
11492
+ data[key] = optional[key];
11493
+ }
11494
11494
  }
11495
11495
  return {
11496
11496
  messageType: MessageType$1.PageEvent,
@@ -11557,6 +11557,12 @@ var mParticle = (function () {
11557
11557
  }
11558
11558
  return control;
11559
11559
  }
11560
+ var EDITABLE = '[contenteditable]:not([contenteditable="false"])';
11561
+ // Text that may hold what a shopper typed or what the page opted out: form
11562
+ // fields, editable regions, and anything under data-mp-no-track.
11563
+ function isUnsafeTextSource(element) {
11564
+ 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
+ }
11560
11566
 
11561
11567
  // Masks a button label before it is logged. Button text can carry personal data,
11562
11568
  // so this is the privacy boundary for every label and aria-label.
@@ -11594,12 +11600,6 @@ var mParticle = (function () {
11594
11600
  var tag = control.tagName.toLowerCase();
11595
11601
  return tag === 'button' || tag === 'input' ? tag : 'role';
11596
11602
  }
11597
- var EDITABLE = '[contenteditable]:not([contenteditable="false"])';
11598
- // Text that may hold what a shopper typed or what the page opted out: form
11599
- // fields, editable regions, and anything under data-mp-no-track.
11600
- function isUnsafeTextSource(element) {
11601
- 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);
11602
- }
11603
11603
  function textOf(element) {
11604
11604
  return element && !isUnsafeTextSource(element) && element.textContent ? element.textContent.trim() : '';
11605
11605
  }
@@ -11743,9 +11743,10 @@ var mParticle = (function () {
11743
11743
  return id.length <= 64 && digits <= 3 && HAND_WRITTEN_ID.test(id) ? id : undefined;
11744
11744
  }
11745
11745
  var TEST_ID_ATTRIBUTES = ['data-testid', 'data-test-id', 'data-test', 'data-qa', 'data-cy'];
11746
- function testIdentifier(control) {
11747
- for (var _i = 0, TEST_ID_ATTRIBUTES_1 = TEST_ID_ATTRIBUTES; _i < TEST_ID_ATTRIBUTES_1.length; _i++) {
11748
- var attribute = TEST_ID_ATTRIBUTES_1[_i];
11746
+ // The first of these attributes that holds a developer-written identifier.
11747
+ function firstIdentifier(control, attributes) {
11748
+ for (var _i = 0, attributes_1 = attributes; _i < attributes_1.length; _i++) {
11749
+ var attribute = attributes_1[_i];
11749
11750
  var id = stableIdentifier(control.getAttribute(attribute));
11750
11751
  if (id) {
11751
11752
  return id;
@@ -11753,16 +11754,28 @@ var mParticle = (function () {
11753
11754
  }
11754
11755
  return undefined;
11755
11756
  }
11757
+ // What the button opens or acts on: a menu, drawer or dialog. aria-controls can
11758
+ // list several ids, so only the first is used.
11759
+ function controlsIdentifier(control) {
11760
+ var ariaControls = control.getAttribute('aria-controls');
11761
+ return stableIdentifier(ariaControls ? ariaControls.trim().split(/\s+/)[0] : null) || firstIdentifier(control, ['popovertarget', 'commandfor']);
11762
+ }
11763
+ // The id of the form a button or input actually submits, which the browser
11764
+ // resolves from its form attribute or its owning form. getAttribute, because a
11765
+ // field named "id" shadows form.id.
11766
+ function formIdentifier(control) {
11767
+ var form = control instanceof HTMLButtonElement || control instanceof HTMLInputElement ? control.form : null;
11768
+ return form ? stableIdentifier(form.getAttribute('id')) : undefined;
11769
+ }
11756
11770
  // The browser's normalised type: a button defaults to 'submit' and an unknown
11757
11771
  // input type reads as 'text'. Wallet and role buttons are just 'button'.
11758
11772
  function controlType(control, element) {
11759
11773
  return element === 'button' || element === 'input' ? control.type : 'button';
11760
11774
  }
11761
- // Describes a clicked button for auto button tracking. Links are left to page
11775
+ // Describes a clicked control for auto button tracking. Links are left to page
11762
11776
  // views, so a link click is not described.
11763
- function describeButtonClick(path) {
11764
- var control = findControl(path);
11765
- if (!control || control.matches('a[href]:not([role="button"])')) {
11777
+ function describeButtonClick(control) {
11778
+ if (control.matches('a[href]:not([role="button"])')) {
11766
11779
  return undefined;
11767
11780
  }
11768
11781
  var element = elementKind(control);
@@ -11777,7 +11790,9 @@ var mParticle = (function () {
11777
11790
  icon: iconHint(control),
11778
11791
  elementId: stableIdentifier(control.getAttribute('id')),
11779
11792
  elementName: stableIdentifier(control.getAttribute('name')),
11780
- testId: testIdentifier(control),
11793
+ testId: firstIdentifier(control, TEST_ID_ATTRIBUTES),
11794
+ controls: controlsIdentifier(control),
11795
+ form: formIdentifier(control),
11781
11796
  element: element,
11782
11797
  type: controlType(control, element)
11783
11798
  };
@@ -11805,23 +11820,23 @@ var mParticle = (function () {
11805
11820
  return text.toLowerCase().replace(TYPOGRAPHIC_APOSTROPHES, "'").replace(CURRENCY_AND_NUMBER_CHARS, '').replace(/\s+/g, ' ').trim();
11806
11821
  }
11807
11822
  // Labels come from aria-label then visible text. Only submit and button inputs
11808
- // have their value read, so a typed field value is never touched.
11823
+ // have their value read, so a typed field value is never touched. Other text
11824
+ // follows the same rules as button descriptions.
11809
11825
  function labelSources(element) {
11810
- if (element.children.length > 4 || element.querySelector('input, select, textarea, [data-mp-no-track]')) {
11811
- return [];
11812
- }
11813
- var text = null;
11814
11826
  if (element instanceof HTMLInputElement) {
11815
11827
  var type = element.type;
11828
+ var text = null;
11816
11829
  if (type === 'submit' || type === 'button') {
11817
11830
  text = element.value;
11818
11831
  } else if (type === 'image') {
11819
11832
  text = element.getAttribute('alt');
11820
11833
  }
11821
- } else {
11822
- text = element.textContent;
11834
+ return [element.getAttribute('aria-label'), text];
11823
11835
  }
11824
- return [element.getAttribute('aria-label'), text];
11836
+ if (element.children.length > 4 || isUnsafeTextSource(element)) {
11837
+ return [];
11838
+ }
11839
+ return [element.getAttribute('aria-label'), element.textContent];
11825
11840
  }
11826
11841
  function labels(element) {
11827
11842
  return labelSources(element).map(normalize).filter(function (label) {
@@ -11840,11 +11855,8 @@ var mParticle = (function () {
11840
11855
  }
11841
11856
  return undefined;
11842
11857
  }
11843
- function classifyIntentControl(path, pathname) {
11844
- var control = findControl(path);
11845
- return control ? classifyControl(control, pathname) : undefined;
11846
- }
11847
- function classifyControl(control, pathname) {
11858
+ // Classifies a clicked or submitting control found by findControl.
11859
+ function classifyIntentControl(control, pathname) {
11848
11860
  var texts = labels(control);
11849
11861
  if (texts.some(function (text) {
11850
11862
  return exclusions.test(text);
@@ -11892,8 +11904,9 @@ var mParticle = (function () {
11892
11904
  var THROTTLE_MS = 2000;
11893
11905
  var SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
11894
11906
  var PATH_LIMIT = 12;
11895
- var EVENT_DEDUP_MS = 1000;
11896
- var PAGE_EVENT_LIMIT = 50;
11907
+ // A ceiling, not a dedupe: a held Enter key or browser automation fires a
11908
+ // trusted click per repeat, and each one is forwarded to every kit.
11909
+ var PAGE_EVENT_LIMIT = 100;
11897
11910
  var btnWindow = function btnWindow() {
11898
11911
  return typeof window === 'undefined' ? null : window;
11899
11912
  };
@@ -11945,7 +11958,6 @@ var mParticle = (function () {
11945
11958
  this.lastSent = {};
11946
11959
  this.pagePath = null;
11947
11960
  this.pageEvents = 0;
11948
- this.lastEvent = {};
11949
11961
  // Clicks read at dispatch and waiting for the next paint.
11950
11962
  this.pending = [];
11951
11963
  this.drain = function () {
@@ -12052,7 +12064,6 @@ var mParticle = (function () {
12052
12064
  this.lastSent = {};
12053
12065
  this.pagePath = null;
12054
12066
  this.pageEvents = 0;
12055
- this.lastEvent = {};
12056
12067
  };
12057
12068
  ButtonTracker.prototype.enqueue = function (delivery) {
12058
12069
  this.pending.push(delivery);
@@ -12067,7 +12078,8 @@ var mParticle = (function () {
12067
12078
  }
12068
12079
  var layer = event.type;
12069
12080
  var logsEvents = layer === 'click' && this.eventsEnabled();
12070
- if (!logsEvents && this.isKitSuppressed()) {
12081
+ var signalsKit = !this.isKitSuppressed();
12082
+ if (!logsEvents && !signalsKit) {
12071
12083
  return;
12072
12084
  }
12073
12085
  var t = Date.now();
@@ -12081,14 +12093,22 @@ var mParticle = (function () {
12081
12093
  var unconfirmedCommit = false;
12082
12094
  if (layer === 'click') {
12083
12095
  // The composed path is only populated during dispatch.
12084
- var path = isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : [];
12085
- click = logsEvents ? describeButtonClick(path) : undefined;
12086
- kind = classifyIntentControl(path, pathname);
12096
+ var control = findControl(isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : []);
12097
+ if (control) {
12098
+ click = logsEvents ? describeButtonClick(control) : undefined;
12099
+ // Checkout intent is only worked out when the kit can use it.
12100
+ kind = signalsKit ? classifyIntentControl(control, pathname) : undefined;
12101
+ }
12087
12102
  } else if (layer === 'submit') {
12088
12103
  var submitter = event.submitter;
12104
+ var control = submitter ? findControl([submitter]) : undefined;
12089
12105
  // A form submitted without a submitter only counts right after a
12090
12106
  // commit click, which is known once that click is delivered.
12091
- kind = submitter ? classifyIntentControl([submitter], pathname) : 'commit';
12107
+ if (!submitter) {
12108
+ kind = 'commit';
12109
+ } else if (control) {
12110
+ kind = classifyIntentControl(control, pathname);
12111
+ }
12092
12112
  unconfirmedCommit = !submitter;
12093
12113
  }
12094
12114
  var generation = this.generation;
@@ -12105,7 +12125,7 @@ var mParticle = (function () {
12105
12125
  kind = undefined;
12106
12126
  }
12107
12127
  if (click && logsEvents) {
12108
- _this.logClick(click, t, pathname);
12128
+ _this.logClick(click, pathname);
12109
12129
  }
12110
12130
  if (kind) {
12111
12131
  _this.emit(kind, layer, t, pathname);
@@ -12115,25 +12135,18 @@ var mParticle = (function () {
12115
12135
  }
12116
12136
  });
12117
12137
  };
12118
- // One event per identical button every second, and at most 50 per page.
12119
- ButtonTracker.prototype.logClick = function (click, t, pathname) {
12138
+ // Every real click is one event, repeats included, up to a ceiling per page.
12139
+ ButtonTracker.prototype.logClick = function (click, pathname) {
12120
12140
  if (!this.eventsEnabled()) {
12121
12141
  return;
12122
12142
  }
12123
12143
  if (pathname !== this.pagePath) {
12124
12144
  this.pagePath = pathname;
12125
12145
  this.pageEvents = 0;
12126
- this.lastEvent = {};
12127
12146
  }
12128
12147
  if (this.pageEvents >= PAGE_EVENT_LIMIT) {
12129
12148
  return;
12130
12149
  }
12131
- var key = [click.label, click.ariaLabel, click.icon, click.elementId, click.elementName, click.testId, click.element, click.type].join('|');
12132
- var last = this.lastEvent[key];
12133
- if (last !== undefined && t - last < EVENT_DEDUP_MS) {
12134
- return;
12135
- }
12136
- this.lastEvent[key] = t;
12137
12150
  this.pageEvents++;
12138
12151
  // Restarts an expired session, as public logEvent() and page views do.
12139
12152
  this.mpInstance._SessionManager.resetSessionTimer();
@@ -1,5 +1,5 @@
1
1
  import { BaseEvent } from '../sdkRuntimeModels';
2
2
  import { IButtonClick } from './buttonDescription';
3
3
  export declare const AUTO_BUTTON_ATTRIBUTE = "is_auto_button_click";
4
- export declare const BUTTON_CLICK_EVENT_NAME = "Button Click";
4
+ export declare const BUTTON_CLICK_EVENT_NAME = "Auto Button Click";
5
5
  export declare const buildButtonClickEvent: (click: IButtonClick, pathname: string) => BaseEvent;
@@ -8,7 +8,9 @@ export interface IButtonClick {
8
8
  elementId?: string;
9
9
  elementName?: string;
10
10
  testId?: string;
11
+ controls?: string;
12
+ form?: string;
11
13
  element: ButtonElementKind;
12
14
  type: string;
13
15
  }
14
- export declare function describeButtonClick(path: Array<EventTarget | null>): IButtonClick | undefined;
16
+ export declare function describeButtonClick(control: Element): IButtonClick | undefined;
@@ -14,7 +14,6 @@ export declare class ButtonTracker {
14
14
  private lastSent;
15
15
  private pagePath;
16
16
  private pageEvents;
17
- private lastEvent;
18
17
  private pending;
19
18
  constructor(mpInstance: IMParticleWebSDKInstance);
20
19
  get isActive(): boolean;
@@ -1,3 +1,3 @@
1
1
  export type CheckoutIntentKind = 'commit' | 'wallet' | 'checkout_step' | 'frame';
2
- export declare function classifyIntentControl(path: Array<EventTarget | null>, pathname: string): CheckoutIntentKind | undefined;
2
+ export declare function classifyIntentControl(control: Element, pathname: string): Exclude<CheckoutIntentKind, 'frame'> | undefined;
3
3
  export declare function classifyIntentFrame(target: EventTarget | null, win: Window): 'frame' | undefined;
@@ -3,3 +3,4 @@ export declare function hasComposedAncestor(element: Element, selector: string):
3
3
  export declare function isRoktElement(element: Element): boolean;
4
4
  export declare function isWalletElement(element: Element): boolean;
5
5
  export declare function findControl(path: Array<EventTarget | null>): Element | undefined;
6
+ export declare function isUnsafeTextSource(element: Element): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -5,7 +5,8 @@ import { IButtonClick } from './buttonDescription';
5
5
  // Marks an automatically logged button click, like `is_auto_page_view`.
6
6
  export const AUTO_BUTTON_ATTRIBUTE = 'is_auto_button_click';
7
7
 
8
- export const BUTTON_CLICK_EVENT_NAME = 'Button Click';
8
+ // Specific enough not to merge with a customer's own 'Button Click' event.
9
+ export const BUTTON_CLICK_EVENT_NAME = 'Auto Button Click';
9
10
 
10
11
  // One custom event per click, logged like logLink() with the auto marker and the
11
12
  // page fields that auto page views carry. No intent: classifying clicks is left
@@ -22,23 +23,21 @@ export const buildButtonClickEvent = (
22
23
  hostname: window.location.hostname,
23
24
  [AUTO_BUTTON_ATTRIBUTE]: true,
24
25
  };
25
- if (click.label) {
26
- data.label = click.label;
27
- }
28
- if (click.ariaLabel) {
29
- data.aria_label = click.ariaLabel;
30
- }
31
- if (click.icon) {
32
- data.icon = click.icon;
33
- }
34
- if (click.elementId) {
35
- data.element_id = click.elementId;
36
- }
37
- if (click.elementName) {
38
- data.element_name = click.elementName;
39
- }
40
- if (click.testId) {
41
- data.test_id = click.testId;
26
+ // Optional fields are left out when the button has no safe value for them.
27
+ const optional: { [key: string]: string | undefined } = {
28
+ label: click.label,
29
+ aria_label: click.ariaLabel,
30
+ icon: click.icon,
31
+ element_id: click.elementId,
32
+ element_name: click.elementName,
33
+ test_id: click.testId,
34
+ controls: click.controls,
35
+ form: click.form,
36
+ };
37
+ for (const key of Object.keys(optional)) {
38
+ if (optional[key]) {
39
+ data[key] = optional[key];
40
+ }
42
41
  }
43
42
  return {
44
43
  messageType: MessageType.PageEvent,
@@ -1,5 +1,9 @@
1
1
  import { isFunction } from '../utils';
2
- import { findControl, hasComposedAncestor, isWalletElement } from './controls';
2
+ import {
3
+ hasComposedAncestor,
4
+ isUnsafeTextSource,
5
+ isWalletElement,
6
+ } from './controls';
3
7
  import { maskLabel } from './labelMasking';
4
8
 
5
9
  // Describes a clicked button for auto button tracking: its masked name, icon,
@@ -23,6 +27,8 @@ export interface IButtonClick {
23
27
  elementId?: string;
24
28
  elementName?: string;
25
29
  testId?: string;
30
+ controls?: string;
31
+ form?: string;
26
32
  element: ButtonElementKind;
27
33
  type: string;
28
34
  }
@@ -35,25 +41,6 @@ function elementKind(control: Element): ButtonElementKind {
35
41
  return tag === 'button' || tag === 'input' ? tag : 'role';
36
42
  }
37
43
 
38
- const EDITABLE = '[contenteditable]:not([contenteditable="false"])';
39
-
40
- // Text that may hold what a shopper typed or what the page opted out: form
41
- // fields, editable regions, and anything under data-mp-no-track.
42
- function isUnsafeTextSource(element: Element): boolean {
43
- return (
44
- element.matches(
45
- 'input, select, textarea, option, optgroup, datalist, ' + EDITABLE
46
- ) ||
47
- !!element.querySelector(
48
- 'input, select, textarea, [data-mp-no-track], ' + EDITABLE
49
- ) ||
50
- hasComposedAncestor(
51
- element,
52
- 'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE
53
- )
54
- );
55
- }
56
-
57
44
  function textOf(element: Element | null): string {
58
45
  return element && !isUnsafeTextSource(element) && element.textContent
59
46
  ? element.textContent.trim()
@@ -198,8 +185,12 @@ const TEST_ID_ATTRIBUTES = [
198
185
  'data-cy',
199
186
  ];
200
187
 
201
- function testIdentifier(control: Element): string | undefined {
202
- for (const attribute of TEST_ID_ATTRIBUTES) {
188
+ // The first of these attributes that holds a developer-written identifier.
189
+ function firstIdentifier(
190
+ control: Element,
191
+ attributes: string[]
192
+ ): string | undefined {
193
+ for (const attribute of attributes) {
203
194
  const id = stableIdentifier(control.getAttribute(attribute));
204
195
  if (id) {
205
196
  return id;
@@ -208,6 +199,29 @@ function testIdentifier(control: Element): string | undefined {
208
199
  return undefined;
209
200
  }
210
201
 
202
+ // What the button opens or acts on: a menu, drawer or dialog. aria-controls can
203
+ // list several ids, so only the first is used.
204
+ function controlsIdentifier(control: Element): string | undefined {
205
+ const ariaControls = control.getAttribute('aria-controls');
206
+ return (
207
+ stableIdentifier(
208
+ ariaControls ? ariaControls.trim().split(/\s+/)[0] : null
209
+ ) || firstIdentifier(control, ['popovertarget', 'commandfor'])
210
+ );
211
+ }
212
+
213
+ // The id of the form a button or input actually submits, which the browser
214
+ // resolves from its form attribute or its owning form. getAttribute, because a
215
+ // field named "id" shadows form.id.
216
+ function formIdentifier(control: Element): string | undefined {
217
+ const form =
218
+ control instanceof HTMLButtonElement ||
219
+ control instanceof HTMLInputElement
220
+ ? control.form
221
+ : null;
222
+ return form ? stableIdentifier(form.getAttribute('id')) : undefined;
223
+ }
224
+
211
225
  // The browser's normalised type: a button defaults to 'submit' and an unknown
212
226
  // input type reads as 'text'. Wallet and role buttons are just 'button'.
213
227
  function controlType(control: Element, element: ButtonElementKind): string {
@@ -216,13 +230,12 @@ function controlType(control: Element, element: ButtonElementKind): string {
216
230
  : 'button';
217
231
  }
218
232
 
219
- // Describes a clicked button for auto button tracking. Links are left to page
233
+ // Describes a clicked control for auto button tracking. Links are left to page
220
234
  // views, so a link click is not described.
221
235
  export function describeButtonClick(
222
- path: Array<EventTarget | null>
236
+ control: Element
223
237
  ): IButtonClick | undefined {
224
- const control = findControl(path);
225
- if (!control || control.matches('a[href]:not([role="button"])')) {
238
+ if (control.matches('a[href]:not([role="button"])')) {
226
239
  return undefined;
227
240
  }
228
241
  const element = elementKind(control);
@@ -237,7 +250,9 @@ export function describeButtonClick(
237
250
  icon: iconHint(control),
238
251
  elementId: stableIdentifier(control.getAttribute('id')),
239
252
  elementName: stableIdentifier(control.getAttribute('name')),
240
- testId: testIdentifier(control),
253
+ testId: firstIdentifier(control, TEST_ID_ATTRIBUTES),
254
+ controls: controlsIdentifier(control),
255
+ form: formIdentifier(control),
241
256
  element,
242
257
  type: controlType(control, element),
243
258
  };
@@ -7,6 +7,7 @@ import {
7
7
  classifyIntentControl,
8
8
  classifyIntentFrame,
9
9
  } from './checkoutIntent';
10
+ import { findControl } from './controls';
10
11
 
11
12
  // State lives on `window`, like APV, so a re-executed bundle never doubles the
12
13
  // listeners. Inspect `window.__mpBtn__` in a console to see if one is live.
@@ -15,8 +16,9 @@ export const WIN_BTN_KEY = '__mpBtn__';
15
16
  const THROTTLE_MS = 2000;
16
17
  const SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
17
18
  const PATH_LIMIT = 12;
18
- const EVENT_DEDUP_MS = 1000;
19
- const PAGE_EVENT_LIMIT = 50;
19
+ // A ceiling, not a dedupe: a held Enter key or browser automation fires a
20
+ // trusted click per repeat, and each one is forwarded to every kit.
21
+ const PAGE_EVENT_LIMIT = 100;
20
22
 
21
23
  interface IBtnState {
22
24
  tracker?: ButtonTracker;
@@ -86,7 +88,6 @@ export class ButtonTracker {
86
88
  private lastSent: { [kind: string]: { t: number; pathname: string } } = {};
87
89
  private pagePath: string | null = null;
88
90
  private pageEvents = 0;
89
- private lastEvent: { [key: string]: number } = {};
90
91
  // Clicks read at dispatch and waiting for the next paint.
91
92
  private pending: Array<() => void> = [];
92
93
 
@@ -180,7 +181,6 @@ export class ButtonTracker {
180
181
  this.lastSent = {};
181
182
  this.pagePath = null;
182
183
  this.pageEvents = 0;
183
- this.lastEvent = {};
184
184
  }
185
185
 
186
186
  private enqueue(delivery: () => void): void {
@@ -231,7 +231,8 @@ export class ButtonTracker {
231
231
  }
232
232
  const layer = event.type;
233
233
  const logsEvents = layer === 'click' && this.eventsEnabled();
234
- if (!logsEvents && this.isKitSuppressed()) {
234
+ const signalsKit = !this.isKitSuppressed();
235
+ if (!logsEvents && !signalsKit) {
235
236
  return;
236
237
  }
237
238
 
@@ -247,19 +248,29 @@ export class ButtonTracker {
247
248
  let unconfirmedCommit = false;
248
249
  if (layer === 'click') {
249
250
  // The composed path is only populated during dispatch.
250
- const path = isFunction(event.composedPath)
251
- ? event.composedPath().slice(0, PATH_LIMIT)
252
- : [];
253
- click = logsEvents ? describeButtonClick(path) : undefined;
254
- kind = classifyIntentControl(path, pathname);
251
+ const control = findControl(
252
+ isFunction(event.composedPath)
253
+ ? event.composedPath().slice(0, PATH_LIMIT)
254
+ : []
255
+ );
256
+ if (control) {
257
+ click = logsEvents ? describeButtonClick(control) : undefined;
258
+ // Checkout intent is only worked out when the kit can use it.
259
+ kind = signalsKit
260
+ ? classifyIntentControl(control, pathname)
261
+ : undefined;
262
+ }
255
263
  } else if (layer === 'submit') {
256
264
  const submitter = (event as Event & { submitter?: Element | null })
257
265
  .submitter;
266
+ const control = submitter ? findControl([submitter]) : undefined;
258
267
  // A form submitted without a submitter only counts right after a
259
268
  // commit click, which is known once that click is delivered.
260
- kind = submitter
261
- ? classifyIntentControl([submitter], pathname)
262
- : 'commit';
269
+ if (!submitter) {
270
+ kind = 'commit';
271
+ } else if (control) {
272
+ kind = classifyIntentControl(control, pathname);
273
+ }
263
274
  unconfirmedCommit = !submitter;
264
275
  }
265
276
 
@@ -280,7 +291,7 @@ export class ButtonTracker {
280
291
  kind = undefined;
281
292
  }
282
293
  if (click && logsEvents) {
283
- this.logClick(click, t, pathname);
294
+ this.logClick(click, pathname);
284
295
  }
285
296
  if (kind) {
286
297
  this.emit(kind, layer, t, pathname);
@@ -291,36 +302,19 @@ export class ButtonTracker {
291
302
  });
292
303
  }
293
304
 
294
- // One event per identical button every second, and at most 50 per page.
295
- private logClick(click: IButtonClick, t: number, pathname: string): void {
305
+ // Every real click is one event, repeats included, up to a ceiling per page.
306
+ private logClick(click: IButtonClick, pathname: string): void {
296
307
  if (!this.eventsEnabled()) {
297
308
  return;
298
309
  }
299
310
  if (pathname !== this.pagePath) {
300
311
  this.pagePath = pathname;
301
312
  this.pageEvents = 0;
302
- this.lastEvent = {};
303
313
  }
304
314
  if (this.pageEvents >= PAGE_EVENT_LIMIT) {
305
315
  return;
306
316
  }
307
- const key = [
308
- click.label,
309
- click.ariaLabel,
310
- click.icon,
311
- click.elementId,
312
- click.elementName,
313
- click.testId,
314
- click.element,
315
- click.type,
316
- ].join('|');
317
- const last = this.lastEvent[key];
318
- if (last !== undefined && t - last < EVENT_DEDUP_MS) {
319
- return;
320
- }
321
- this.lastEvent[key] = t;
322
317
  this.pageEvents++;
323
-
324
318
  // Restarts an expired session, as public logEvent() and page views do.
325
319
  this.mpInstance._SessionManager.resetSessionTimer();
326
320
  this.mpInstance._Events.logEvent(
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CONTROL_SELECTOR,
3
- findControl,
4
3
  isRoktElement,
4
+ isUnsafeTextSource,
5
5
  isWalletElement,
6
6
  } from './controls';
7
7
 
@@ -93,26 +93,23 @@ function normalize(text: string | null | undefined): string | undefined {
93
93
  }
94
94
 
95
95
  // Labels come from aria-label then visible text. Only submit and button inputs
96
- // have their value read, so a typed field value is never touched.
96
+ // have their value read, so a typed field value is never touched. Other text
97
+ // follows the same rules as button descriptions.
97
98
  function labelSources(element: Element): Array<string | null> {
98
- if (
99
- element.children.length > 4 ||
100
- element.querySelector('input, select, textarea, [data-mp-no-track]')
101
- ) {
102
- return [];
103
- }
104
- let text: string | null = null;
105
99
  if (element instanceof HTMLInputElement) {
106
100
  const type = element.type;
101
+ let text: string | null = null;
107
102
  if (type === 'submit' || type === 'button') {
108
103
  text = element.value;
109
104
  } else if (type === 'image') {
110
105
  text = element.getAttribute('alt');
111
106
  }
112
- } else {
113
- text = element.textContent;
107
+ return [element.getAttribute('aria-label'), text];
114
108
  }
115
- return [element.getAttribute('aria-label'), text];
109
+ if (element.children.length > 4 || isUnsafeTextSource(element)) {
110
+ return [];
111
+ }
112
+ return [element.getAttribute('aria-label'), element.textContent];
116
113
  }
117
114
 
118
115
  function labels(element: Element): string[] {
@@ -142,15 +139,8 @@ function classifyLabel(
142
139
  return undefined;
143
140
  }
144
141
 
142
+ // Classifies a clicked or submitting control found by findControl.
145
143
  export function classifyIntentControl(
146
- path: Array<EventTarget | null>,
147
- pathname: string
148
- ): CheckoutIntentKind | undefined {
149
- const control = findControl(path);
150
- return control ? classifyControl(control, pathname) : undefined;
151
- }
152
-
153
- function classifyControl(
154
144
  control: Element,
155
145
  pathname: string
156
146
  ): Exclude<CheckoutIntentKind, 'frame'> | undefined {
@@ -90,3 +90,22 @@ export function findControl(
90
90
  }
91
91
  return control;
92
92
  }
93
+
94
+ const EDITABLE = '[contenteditable]:not([contenteditable="false"])';
95
+
96
+ // Text that may hold what a shopper typed or what the page opted out: form
97
+ // fields, editable regions, and anything under data-mp-no-track.
98
+ export function isUnsafeTextSource(element: Element): boolean {
99
+ return (
100
+ element.matches(
101
+ 'input, select, textarea, option, optgroup, datalist, ' + EDITABLE
102
+ ) ||
103
+ !!element.querySelector(
104
+ 'input, select, textarea, [data-mp-no-track], ' + EDITABLE
105
+ ) ||
106
+ hasComposedAncestor(
107
+ element,
108
+ 'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE
109
+ )
110
+ );
111
+ }