@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.
- package/dist/mparticle.common.js +29 -19
- package/dist/mparticle.esm.js +29 -19
- package/dist/mparticle.js +78 -65
- package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +1 -1
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +3 -1
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +0 -1
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +1 -1
- package/dist/types/src/buttonTracking/controls.d.ts +1 -0
- package/package.json +1 -1
- package/src/buttonTracking/buttonClickEvent.ts +17 -18
- package/src/buttonTracking/buttonDescription.ts +42 -27
- package/src/buttonTracking/buttonTracker.ts +27 -33
- package/src/buttonTracking/checkoutIntent.ts +10 -20
- package/src/buttonTracking/controls.ts +19 -0
package/dist/mparticle.common.js
CHANGED
|
@@ -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
|
+
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"
|
|
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){
|
|
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"}//
|
|
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"]
|
|
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
|
|
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){
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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()
|
|
1425
|
-
var
|
|
1426
|
-
|
|
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.esm.js
CHANGED
|
@@ -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
|
+
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"
|
|
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){
|
|
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"}//
|
|
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"]
|
|
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
|
|
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){
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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()
|
|
1425
|
-
var
|
|
1426
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
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
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
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
|
|
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(
|
|
11764
|
-
|
|
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:
|
|
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
|
-
|
|
11822
|
-
text = element.textContent;
|
|
11834
|
+
return [element.getAttribute('aria-label'), text];
|
|
11823
11835
|
}
|
|
11824
|
-
|
|
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
|
-
|
|
11844
|
-
|
|
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
|
-
|
|
11896
|
-
|
|
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
|
-
|
|
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
|
|
12085
|
-
|
|
12086
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
12119
|
-
ButtonTracker.prototype.logClick = function (click,
|
|
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(
|
|
16
|
+
export declare function describeButtonClick(control: Element): IButtonClick | undefined;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type CheckoutIntentKind = 'commit' | 'wallet' | 'checkout_step' | 'frame';
|
|
2
|
-
export declare function classifyIntentControl(
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 {
|
|
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
|
-
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
236
|
+
control: Element
|
|
223
237
|
): IButtonClick | undefined {
|
|
224
|
-
|
|
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:
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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,
|
|
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
|
-
//
|
|
295
|
-
private logClick(click: IButtonClick,
|
|
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
|
-
|
|
113
|
-
text = element.textContent;
|
|
107
|
+
return [element.getAttribute('aria-label'), text];
|
|
114
108
|
}
|
|
115
|
-
|
|
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
|
+
}
|