@mparticle/web-sdk 3.26.1 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mparticle.js CHANGED
@@ -204,7 +204,7 @@ var mParticle = (function () {
204
204
  Base64: Base64$1
205
205
  };
206
206
 
207
- var version = "3.26.1";
207
+ var version = "3.27.0";
208
208
 
209
209
  var Constants = {
210
210
  sdkVersion: version,
@@ -2732,6 +2732,156 @@ var mParticle = (function () {
2732
2732
  }
2733
2733
  }
2734
2734
 
2735
+ // Masks the parts of a page path that can single out one shopper: pagePath for
2736
+ // the path a click logs, pageUrlPath for the page URL and page views every event
2737
+ // carries.
2738
+ // A token of letters and digits mixed, six or more long, as session ids,
2739
+ // hex and UUID parts and invite codes are: `A1B2C3D4E5F6`, `550e8400`.
2740
+ var MIXED_TOKEN = /^(?=[^0-9]*[0-9])(?=[^A-Za-z]*[A-Za-z])[A-Za-z0-9]{6,}$/;
2741
+ // Letters alone, eight or more long, switching case three times or more, as a
2742
+ // random code does (`xKpQzRtV`) and a camel-case route word rarely does.
2743
+ var CASE_SWITCHING_TOKEN = /^(?=[A-Za-z]{8,}$)(?:[^a-z]*[a-z]+[A-Z]){3}/;
2744
+ // Route words whose next segment names one person or carries a one-time code,
2745
+ // as in `/profile/jane-smith` or `/reset/abcdefgh`.
2746
+ var PERSONAL_ROUTE = /^(?:u|user|users|profile|profiles|member|members|people|reset|reset-password|password|verify|verification|confirm|activate|invite|invitation|token|unsubscribe|magic-link)$/i;
2747
+ // Route words whose next segment is one shopper's order: any with `order` or
2748
+ // `orders` as a whole word part, as in `view-order`, `order-received` or `order_id`.
2749
+ var ORDER_ROUTE = /(?:^|[-_])orders?(?:[-_]|$)/i;
2750
+ // Product words that name a way to buy, not an order: `pre-order`, `on-order`,
2751
+ // `back-order`, `made-to-order`, as the whole route word only, so
2752
+ // `return-to-order` and `pre-order-status` stay routes.
2753
+ var PRODUCT_ORDER_WORD = /^(?:(?:pre|on|back)[-_]|made[-_]to[-_])orders?$/i;
2754
+ // A `;` that starts a matrix parameter, plain or encoded any number of times.
2755
+ var MATRIX_SEPARATOR = /;|%(?:25)*3B/i;
2756
+ // A valid escape left after decoding stopped, as when a literal `%` before it
2757
+ // can't be decoded again. What it hides is unknown, so it counts as sensitive.
2758
+ var LEFTOVER_ESCAPE = /%[0-9A-F]{2}/i;
2759
+ function hidesNextSegment(route) {
2760
+ return PERSONAL_ROUTE.test(route) || ORDER_ROUTE.test(route) && !PRODUCT_ORDER_WORD.test(route);
2761
+ }
2762
+ // How many times a value is decoded at most, so one encoded twice or more reads
2763
+ // as its plain text.
2764
+ var MAX_DECODES = 5;
2765
+ // Decodes until the text stops changing. Undefined when the first decode fails;
2766
+ // a later failure keeps what decoded so far, as a literal `%` would.
2767
+ function decodeFully(text) {
2768
+ var current = text;
2769
+ for (var i = 0; i < MAX_DECODES; i++) {
2770
+ var next = void 0;
2771
+ try {
2772
+ next = decodeURIComponent(current);
2773
+ } catch (e) {
2774
+ return i === 0 ? undefined : current;
2775
+ }
2776
+ if (next === current) {
2777
+ return current;
2778
+ }
2779
+ current = next;
2780
+ }
2781
+ return current;
2782
+ }
2783
+ // The route word in a raw path segment: its last part behind an encoded slash,
2784
+ // read before any matrix parameter such as `;jsessionid=`, its `;` encoded once,
2785
+ // twice or not at all, so a parameter's own value never hides it. Undefined when
2786
+ // it can't be decoded, or is still encoded after MAX_DECODES.
2787
+ function routeWord(segment) {
2788
+ var text = segment;
2789
+ for (var i = 0; i < MAX_DECODES; i++) {
2790
+ var route = text.split(MATRIX_SEPARATOR)[0];
2791
+ var next = void 0;
2792
+ try {
2793
+ next = decodeURIComponent(route);
2794
+ } catch (e) {
2795
+ return i === 0 ? undefined : route.split('/').pop();
2796
+ }
2797
+ if (next === route) {
2798
+ return route.split('/').pop();
2799
+ }
2800
+ text = next;
2801
+ }
2802
+ return undefined;
2803
+ }
2804
+ // The last segment before index i that isn't empty, so `/orders//12345678`
2805
+ // still reads the order route.
2806
+ function routeSegment(segments, i) {
2807
+ var routeIndex = i - 1;
2808
+ while (routeIndex > 0 && segments[routeIndex] === '') {
2809
+ routeIndex--;
2810
+ }
2811
+ return segments[routeIndex];
2812
+ }
2813
+ // A piece with a lowercase letter right before an uppercase one, as a random
2814
+ // code has and a Title-Case word doesn't.
2815
+ var INNER_CASE_SWITCH = /[a-z][A-Z]/;
2816
+ var isToken = function isToken(text) {
2817
+ return MIXED_TOKEN.test(text) || CASE_SWITCHING_TOKEN.test(text);
2818
+ };
2819
+ var mixesLettersAndDigits = function mixesLettersAndDigits(piece) {
2820
+ return /[0-9]/.test(piece) && /[A-Za-z]/.test(piece);
2821
+ };
2822
+ // `+` sits inside base64 tokens, so it never splits one. A token that `-` or `_`
2823
+ // cuts into short pieces is still one when the pieces read as one joined and
2824
+ // either a piece switches case inside it or two or more pieces mix letters and
2825
+ // digits, as `a1b2-c3d4-e5f6` does. Title-Case-Words and plain slugs such as
2826
+ // `iphone-15-pro` do neither.
2827
+ function isTokenSegment(segment) {
2828
+ var text = segment.replace(/\+/g, '');
2829
+ var pieces = text.split(/[-_.~]/);
2830
+ var joined = pieces.join('');
2831
+ return pieces.some(isToken) || pieces.some(function (piece) {
2832
+ return INNER_CASE_SWITCH.test(piece);
2833
+ }) && isToken(joined) || pieces.filter(mixesLettersAndDigits).length >= 2 && MIXED_TOKEN.test(joined);
2834
+ }
2835
+ // The path a click logs. A segment that can single out one shopper
2836
+ // goes as `*`: one holding an @, `;` or `=`, a token, or the one after a route
2837
+ // that names a person, carries a code or opens an order. Kept segments stay
2838
+ // exactly as the page has them, plain numbers included, since on a product page
2839
+ // they name the product.
2840
+ function pagePath(pathname) {
2841
+ var segments = pathname.split('/');
2842
+ return segments.map(function (segment, i) {
2843
+ var text = decodeFully(segment);
2844
+ var route = i > 0 ? routeWord(routeSegment(segments, i)) : '';
2845
+ // A route word that can't be decoded may be any route, so the
2846
+ // segment after it is hidden too.
2847
+ var hiddenByRoute = i > 0 && !!text && (route === undefined || hidesNextSegment(route));
2848
+ // An encoded slash hides several segments in one, so it goes whole.
2849
+ return text === undefined || /[@;=/]/.test(text) || LEFTOVER_ESCAPE.test(text) || isTokenSegment(text) || hiddenByRoute ? '*' : segment;
2850
+ }).join('/');
2851
+ }
2852
+ // A token long enough that no page or product name looks like it: 16 or more
2853
+ // letters and digits mixed, as checkout tokens are, 16 or more letters
2854
+ // switching case like a random code, or a UUID.
2855
+ var LONG_MIXED_TOKEN = /^(?=[^0-9]*[0-9])(?=[^A-Za-z]*[A-Za-z])[A-Za-z0-9]{16,}$/;
2856
+ var LONG_CASE_SWITCHING_TOKEN = /^(?=[A-Za-z]{16,}$)(?:[^a-z]*[a-z]+[A-Z]){3}/;
2857
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2858
+ // A token that `-` or `_` cuts into shorter pieces counts when two or more
2859
+ // pieces mix letters and digits, as `a1b2c3d4-e5f6g7h8` does and a product slug
2860
+ // such as `acmephone-15-pro-max-256gb` doesn't.
2861
+ function isLongToken(segment) {
2862
+ var pieces = segment.replace(/\+/g, '').split(/[-_.~]/);
2863
+ return UUID.test(segment) || pieces.some(function (piece) {
2864
+ return LONG_MIXED_TOKEN.test(piece) || LONG_CASE_SWITCHING_TOKEN.test(piece);
2865
+ }) || pieces.filter(mixesLettersAndDigits).length >= 2 && LONG_MIXED_TOKEN.test(pieces.join(''));
2866
+ }
2867
+ // The path every event carries, in its page URL and page views, where a
2868
+ // wrong `*` costs every output the page it names. Only what reliably singles
2869
+ // out one shopper goes as `*`: a segment holding an @, `;` or `=`, one that
2870
+ // can't be decoded or still holds an escape, a long token, and the one after a
2871
+ // route that names a person or opens an order when it holds a digit or a token.
2872
+ // Product codes and pages such as `/profile/edit` or `/orders/history` stay.
2873
+ function pageUrlPath(pathname) {
2874
+ var segments = pathname.split('/');
2875
+ return segments.map(function (segment, i) {
2876
+ var text = decodeFully(segment);
2877
+ var route = i > 0 ? routeWord(routeSegment(segments, i)) : '';
2878
+ var holdsId = !!text && (/[0-9]/.test(text) || isTokenSegment(text));
2879
+ // A route word that can't be decoded may be any route.
2880
+ var hiddenByRoute = i > 0 && !!text && (route === undefined || hidesNextSegment(route) && holdsId);
2881
+ return text === undefined || /[@;=/]/.test(text) || LEFTOVER_ESCAPE.test(text) || isLongToken(text) || hiddenByRoute ? '*' : segment;
2882
+ }).join('/');
2883
+ }
2884
+
2735
2885
  var HISTORY_METHODS = ['pushState', 'replaceState'];
2736
2886
  var WRAPPED_MARKER = '__mpApvWrapped__';
2737
2887
  var supportsHistoryTracking = function supportsHistoryTracking(win) {
@@ -3073,7 +3223,8 @@ var mParticle = (function () {
3073
3223
  data: __assign(__assign(__assign({}, paramsToAttributes(params)), {
3074
3224
  hostname: hostname,
3075
3225
  title: title,
3076
- path: path
3226
+ // Masked here, not in the page snapshot, whose raw path tells one page from the next.
3227
+ path: pageUrlPath(path)
3077
3228
  }), autoPageViewAttribute(isAutoPageView)),
3078
3229
  eventType: EventType.Unknown
3079
3230
  };
@@ -3169,7 +3320,9 @@ var mParticle = (function () {
3169
3320
  var name = _a.name;
3170
3321
  return name;
3171
3322
  });
3172
- return names.length ? "".concat(page.path, " (params: ").concat(names.join(), ")") : page.path;
3323
+ // A log line is read by no output, so it takes the click path's fuller rules.
3324
+ var path = pagePath(page.path);
3325
+ return names.length ? "".concat(path, " (params: ").concat(names.join(), ")") : path;
3173
3326
  };
3174
3327
  // ---------------------------------------------------------------------------
3175
3328
  // History patching
@@ -3353,12 +3506,16 @@ var mParticle = (function () {
3353
3506
  }();
3354
3507
 
3355
3508
  // The one rule for a URL that leaves the SDK, shared with kits through
3356
- // `mParticle._cleanUrl`. `path` keeps scheme, host and path. `full` adds the
3509
+ // `mParticle._cleanUrl`. `path` keeps scheme, host and path, with any path
3510
+ // segment that can single out one shopper as `*` (see pageUrlPath). `full` adds the
3357
3511
  // query params an auto page view captures by default and the campaign
3358
- // attribution params, then the fragment.
3512
+ // attribution params, then the fragment, a hash route's path masked the same way.
3359
3513
  var UrlMode = {
3360
3514
  Path: 'path',
3361
- Full: 'full'
3515
+ Full: 'full',
3516
+ // A path for a log or an error report, masked with the click path's fuller
3517
+ // rules, since no output reads it as a page.
3518
+ Report: 'report'
3362
3519
  };
3363
3520
  // Campaign attribution and ad-network click ids. URLs keep them; an auto page view
3364
3521
  // carries one as an attribute only when an input configures it.
@@ -3377,6 +3534,31 @@ var mParticle = (function () {
3377
3534
  // Allowlisted params whose value is itself a URL, so it can carry a query.
3378
3535
  var URL_VALUED_QUERY_PARAMS = ['ref', 'referrer'];
3379
3536
  var USERINFO = /^((?:[a-z][a-z0-9+.-]*:)?\/\/)[^/]*@/i;
3537
+ var ORIGIN = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^/]*/i;
3538
+ // Only a URL or a path is masked. A bare value, such as `ref=spring2024`, is a
3539
+ // code rather than a page, and stays as written.
3540
+ var maskPath = function maskPath(url, mask) {
3541
+ if (mask === void 0) {
3542
+ mask = pageUrlPath;
3543
+ }
3544
+ var origin = ORIGIN.exec(url);
3545
+ if (origin) {
3546
+ return origin[0] + mask(url.slice(origin[0].length));
3547
+ }
3548
+ return url.charAt(0) === '/' ? mask(url) : url;
3549
+ };
3550
+ // A hash route keeps its `#` or `#!` and has its path masked as the URL's own.
3551
+ // The `&` pairs or flags after the path lose the OAuth/OIDC ones, as a bare
3552
+ // fragment's pairs do.
3553
+ var maskRoute = function maskRoute(route) {
3554
+ var prefix = /^#!?/.exec(route)[0];
3555
+ var pairsStart = route.indexOf('&');
3556
+ var pathEnd = pairsStart === -1 ? route.length : pairsStart;
3557
+ var pairs = route.slice(pathEnd).split('&').slice(1).filter(keepsFragmentPair).map(function (pair) {
3558
+ return "&".concat(pair);
3559
+ }).join('');
3560
+ return prefix + pageUrlPath(route.slice(prefix.length, pathEnd)) + pairs;
3561
+ };
3380
3562
  var decodeQueryComponent = function decodeQueryComponent(component) {
3381
3563
  return decodeURIComponent(component.replace(/\+/g, ' '));
3382
3564
  };
@@ -3424,15 +3606,24 @@ var mParticle = (function () {
3424
3606
  var cleanFragment = function cleanFragment(hash) {
3425
3607
  var queryStart = hash.indexOf('?');
3426
3608
  var route = queryStart === -1 ? hash : hash.slice(0, queryStart);
3427
- var isRoute = /^#[/!]/.test(hash) || queryStart !== -1 && route.indexOf('=') === -1;
3609
+ // `#/\u2026`, `#!\u2026`, or a fragment with a `/` before any `&` or `?`, as
3610
+ // `#orders/12345678` is in routers that drop the leading slash.
3611
+ // A `/` counts only before any `=`, so a bare pair such as
3612
+ // `#access_token=abc/def` is never read as a route.
3613
+ var isHashRoute = /^#!|^#[^?&=]*\//.test(hash);
3614
+ var isRoute = isHashRoute || queryStart !== -1 && route.indexOf('=') === -1;
3428
3615
  if (isRoute && queryStart !== -1) {
3429
3616
  var query = cleanQuery(hash.slice(queryStart + 1));
3430
- return route + (query ? "?".concat(query) : '');
3617
+ return maskRoute(route) + (query ? "?".concat(query) : '');
3431
3618
  }
3619
+ // A plain anchor such as `#reviews` names a part of the page, not a route.
3432
3620
  if (hash.indexOf('=') === -1) {
3433
- return hash;
3621
+ return isHashRoute ? maskRoute(hash) : hash;
3434
3622
  }
3435
- var pairs = hash.slice(1).split('&').filter(keepsFragmentPair).join('&');
3623
+ // A hash route's path leads its pairs, as in `#/cb&tab=2`.
3624
+ var pairs = hash.slice(1).split('&').map(function (pair, i) {
3625
+ return i === 0 && isHashRoute ? maskRoute("#".concat(pair)).slice(1) : pair;
3626
+ }).filter(keepsFragmentPair).join('&');
3436
3627
  return pairs ? "#".concat(pairs) : '';
3437
3628
  };
3438
3629
  // String-based rather than `new URL`, so kept pairs and the fragment go out
@@ -3446,7 +3637,7 @@ var mParticle = (function () {
3446
3637
  var hash = hashStart === -1 ? '' : href.slice(hashStart);
3447
3638
  var beforeHash = hashStart === -1 ? href : href.slice(0, hashStart);
3448
3639
  var queryStart = beforeHash.indexOf('?');
3449
- var path = (queryStart === -1 ? beforeHash : beforeHash.slice(0, queryStart)).replace(USERINFO, '$1');
3640
+ var path = maskPath((queryStart === -1 ? beforeHash : beforeHash.slice(0, queryStart)).replace(USERINFO, '$1'), mode === UrlMode.Report ? pagePath : pageUrlPath);
3450
3641
  if (mode !== UrlMode.Full) {
3451
3642
  return path;
3452
3643
  }
@@ -11784,7 +11975,7 @@ var mParticle = (function () {
11784
11975
  label_source: click.labelSource,
11785
11976
  element: click.element,
11786
11977
  type: click.type,
11787
- path: pathname,
11978
+ path: pagePath(pathname),
11788
11979
  hostname: window.location.hostname
11789
11980
  };
11790
11981
  // Optional fields are left out when the control has no safe value for them.
@@ -11797,8 +11988,10 @@ var mParticle = (function () {
11797
11988
  test_id: click.testId,
11798
11989
  controls: click.controls,
11799
11990
  form: click.form,
11991
+ group: click.group,
11800
11992
  link_type: click.linkType,
11801
11993
  link_domain: click.linkDomain,
11994
+ link_path: click.linkPath,
11802
11995
  file_extension: click.fileExtension
11803
11996
  };
11804
11997
  for (var _i = 0, _a = Object.keys(optional); _i < _a.length; _i++) {
@@ -11825,8 +12018,9 @@ var mParticle = (function () {
11825
12018
  };
11826
12019
  // One custom event per click on a link that leaves the page. Typed Other, not
11827
12020
  // Navigation, so no kit reads it as a page change. A plain link to another page
11828
- // of the site goes without its text, which can name the shopper ("Hi, Jane");
11829
- // a button or wallet acting as a link keeps its name.
12021
+ // of the site goes without its text, which can name the shopper ("Hi, Jane"), and
12022
+ // with its destination path instead; a button or wallet acting as a link keeps
12023
+ // its name.
11830
12024
  var buildLinkClickEvent = function buildLinkClickEvent(click, pathname) {
11831
12025
  var data = clickAttributes(click.linkType === 'internal' && click.element === 'link' ? __assign(__assign({}, click), {
11832
12026
  label: undefined,
@@ -11844,7 +12038,7 @@ var mParticle = (function () {
11844
12038
  // Every attribute buildButtonClickEvent can set apart from the marker and the
11845
12039
  // hostname, which is the same on every click on this page. A spec checks the
11846
12040
  // two stay in step.
11847
- var CLICK_ATTRIBUTES = ['label_source', 'element', 'type', 'path', 'label', 'aria_label', 'icon', 'element_id', 'element_name', 'test_id', 'controls', 'form', 'link_type', 'link_domain', 'file_extension', 'checked'];
12041
+ var CLICK_ATTRIBUTES = ['label_source', 'element', 'type', 'path', 'label', 'aria_label', 'icon', 'element_id', 'element_name', 'test_id', 'controls', 'form', 'group', 'link_type', 'link_domain', 'link_path', 'file_extension', 'checked'];
11848
12042
  // Which check keeps some or all auto button clicks, or auto link clicks with
11849
12043
  // `link`, from a kit, or undefined when the kit receives every one. A forwarding
11850
12044
  // rule on an attribute whose value differs between clicks lets some through and
@@ -12058,17 +12252,20 @@ var mParticle = (function () {
12058
12252
  // or around it never becomes its tap. Excluded clicks give nothing; disabled controls are skipped, a
12059
12253
  // wallet-named one also by its `disabled` class. A caller that has already
12060
12254
  // found the path not excluded passes `included` so the walk runs once.
12061
- function findControl(path, included) {
12255
+ function findControl(path, included, limit) {
12062
12256
  if (included === void 0) {
12063
12257
  included = false;
12064
12258
  }
12259
+ if (limit === void 0) {
12260
+ limit = 12;
12261
+ }
12065
12262
  if (!included && isExcludedPath(path)) {
12066
12263
  return undefined;
12067
12264
  }
12068
12265
  var control;
12069
12266
  var option;
12070
12267
  var picked = false;
12071
- for (var _i = 0, _a = path.slice(0, 12); _i < _a.length; _i++) {
12268
+ for (var _i = 0, _a = path.slice(0, limit); _i < _a.length; _i++) {
12072
12269
  var target = _a[_i];
12073
12270
  if (control) {
12074
12271
  break;
@@ -12091,6 +12288,39 @@ var mParticle = (function () {
12091
12288
  }
12092
12289
  return control;
12093
12290
  }
12291
+ // Roles that act as controls without being buttons or links: pickers, tabs,
12292
+ // options and links run by script. A role can list fallbacks, so any of its
12293
+ // tokens counts.
12294
+ var ROLE_CONTROL = '[role~="radio"], [role~="menuitemradio"], [role~="checkbox"], [role~="menuitemcheckbox"], ' + '[role~="switch"], [role~="option"], [role~="tab"], [role~="link"]';
12295
+ var TYPING_FIELD = 'input:not([type="submit"]):not([type="button"]):not([type="image"]):not([type="reset"]):not([type="radio"]):not([type="checkbox"]), ' + 'textarea, select, ' + EDITABLE;
12296
+ // The nearest role control on the click path, for logging the click alone:
12297
+ // checkout intent and payment choices only ever read what findControl finds.
12298
+ // None when anything disabled sits on the path, so a role control around or
12299
+ // inside a disabled control never logs the click it suppressed.
12300
+ function findRoleControl(path, limit) {
12301
+ var entries = path.slice(0, limit).filter(function (target) {
12302
+ return target instanceof Element;
12303
+ });
12304
+ if (entries.some(function (target) {
12305
+ return target.matches(':disabled, [aria-disabled="true"]') || target.classList.contains('disabled') && isWalletNamed(target);
12306
+ })) {
12307
+ return undefined;
12308
+ }
12309
+ for (var _i = 0, entries_3 = entries; _i < entries_3.length; _i++) {
12310
+ var target = entries_3[_i];
12311
+ if (target.matches(ROLE_CONTROL)) {
12312
+ // A typing field, or anything in an editable region, takes typing
12313
+ // whatever role it carries.
12314
+ return target.matches(TYPING_FIELD) || hasComposedAncestor(target, EDITABLE) ? undefined : target;
12315
+ }
12316
+ // A click in a typing field belongs to the field, not to the role
12317
+ // control around it.
12318
+ if (target.matches(TYPING_FIELD)) {
12319
+ return undefined;
12320
+ }
12321
+ }
12322
+ return undefined;
12323
+ }
12094
12324
  // Text that may hold what a shopper typed or what the page opted out: form
12095
12325
  // fields, editable regions, and anything under data-mp-no-track. `own` is a
12096
12326
  // box the element labels, which holds no typing of its own.
@@ -12131,6 +12361,11 @@ var mParticle = (function () {
12131
12361
  var masked = text.replace(digitPatterns.number, '#').slice(0, 64).trim();
12132
12362
  return masked || undefined;
12133
12363
  }
12364
+ // Masks numbers and amounts in any script as '#', or undefined where the browser
12365
+ // can't see every script's digits.
12366
+ function maskNumbers(text) {
12367
+ return digitPatterns ? text.replace(digitPatterns.number, '#') : undefined;
12368
+ }
12134
12369
 
12135
12370
  var DOWNLOAD_EXTENSIONS = /\.(pdf|zip|csv|txt|ics|docx?|xlsx?|pptx?|dmg|exe|apk|mp3|mp4)$/i;
12136
12371
  function elementKind(control) {
@@ -12141,7 +12376,9 @@ var mParticle = (function () {
12141
12376
  if (tag === 'button' || tag === 'input') {
12142
12377
  return tag;
12143
12378
  }
12144
- return control.matches('a[href]:not([role="button"])') ? 'link' : 'role';
12379
+ // A link run by script reads as a link, as an anchor does, so whoever reads
12380
+ // the click treats it the same.
12381
+ return control.matches('a[href]:not([role="button"]), [role~="link"]:not([role~="button"])') ? 'link' : 'role';
12145
12382
  }
12146
12383
  var NOT_RENDERED = 'script, style, template, [hidden], [aria-hidden="true"], [inert]';
12147
12384
  // The most nodes read for one label source, and images looked at for one
@@ -12323,7 +12560,7 @@ var mParticle = (function () {
12323
12560
  } else if (readable && isShown(control)) {
12324
12561
  var text = renderedText(control);
12325
12562
  if (text === undefined) {
12326
- return {
12563
+ return headingName(control) || {
12327
12564
  source: 'none'
12328
12565
  };
12329
12566
  }
@@ -12341,6 +12578,12 @@ var mParticle = (function () {
12341
12578
  };
12342
12579
  }
12343
12580
  }
12581
+ if (!(control instanceof HTMLInputElement) && isShown(control)) {
12582
+ var heading = headingName(control);
12583
+ if (heading) {
12584
+ return heading;
12585
+ }
12586
+ }
12344
12587
  var title = nonBlank(control.getAttribute('title'));
12345
12588
  if (title) {
12346
12589
  return {
@@ -12352,6 +12595,25 @@ var mParticle = (function () {
12352
12595
  source: 'none'
12353
12596
  };
12354
12597
  }
12598
+ // A card whose text runs too long to keep, or has too many parts to read, is
12599
+ // named by its first heading, as a product or menu card usually is. Not a card
12600
+ // holding a form field, nor one that takes in slotted content or holds a
12601
+ // shadow root, whose fields the form field check can't see.
12602
+ function headingName(control) {
12603
+ if (isUnsafeTextSource(control) || control.shadowRoot || control.querySelector('slot') ||
12604
+ // A custom element can hide fields in a closed shadow root, which
12605
+ // nothing outside it can see.
12606
+ control.tagName.indexOf('-') !== -1 || Array.from(control.querySelectorAll('*')).some(function (element) {
12607
+ return !!element.shadowRoot || element.tagName.indexOf('-') !== -1;
12608
+ })) {
12609
+ return undefined;
12610
+ }
12611
+ var text = textOf(control.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]'));
12612
+ return text ? {
12613
+ raw: text,
12614
+ source: 'heading'
12615
+ } : undefined;
12616
+ }
12355
12617
  var ICON_CLASS = /^(?:fa|fas|far|fal|icon|bi|glyphicon|ti|mdi|ri|la|lucide|ion|feather|ph)-([a-z][a-z-]{1,31})$/;
12356
12618
  // Size, style and animation classes that share the icon prefixes.
12357
12619
  var ICON_MODIFIER = /^(?:solid|regular|light|thin|duotone|brands|sharp|fw|lg|xs|sm|xl|spin|pulse|border|inverse|stack|flip(?:-[a-z]+)?|rotate(?:-[a-z]+)?|pull-[a-z]+)$/;
@@ -12405,6 +12667,12 @@ var mParticle = (function () {
12405
12667
  var digits = id.replace(/[^0-9]/g, '').length;
12406
12668
  return id.length <= 64 && digits <= 3 && HAND_WRITTEN_ID.test(id) ? id : undefined;
12407
12669
  }
12670
+ // A picker's input name, which names its question. Sites often end it with the
12671
+ // id of the product it belongs to (`color-swatches-48213907`); without that
12672
+ // number it still names the question.
12673
+ function questionName(value) {
12674
+ return stableIdentifier(value && value.trim().replace(/[-_][0-9]{4,}$/, ''));
12675
+ }
12408
12676
  var TEST_ID_ATTRIBUTES = ['data-testid', 'data-test-id', 'data-test', 'data-qa', 'data-cy'];
12409
12677
  // The first of these attributes that holds a developer-written identifier.
12410
12678
  function firstIdentifier(control, attributes) {
@@ -12432,15 +12700,56 @@ var mParticle = (function () {
12432
12700
  }
12433
12701
  // The browser's normalised type: a button defaults to 'submit' and an unknown
12434
12702
  // input type reads as 'text'. A native button or input reports its own type, a
12435
- // wallet-named one included; any other control, such as a role button or a
12436
- // vendor's wallet element, reads as 'button'.
12703
+ // wallet-named one included. A role control that isn't a button reads as its
12704
+ // role: 'radio', 'option', 'tab', 'link' and so on. Any other control, such as
12705
+ // a role button or a vendor's wallet element, reads as 'button'.
12706
+ var CONTROL_ROLES = ['radio', 'menuitemradio', 'checkbox', 'menuitemcheckbox', 'switch', 'option', 'tab', 'link'];
12437
12707
  function controlType(control) {
12438
12708
  var tag = control.tagName.toLowerCase();
12439
- return tag === 'button' || tag === 'input' ? control.type : 'button';
12709
+ if (tag === 'button' || tag === 'input') {
12710
+ return control.type;
12711
+ }
12712
+ if (control.matches('[role~="button"]')) {
12713
+ return 'button';
12714
+ }
12715
+ var roles = (control.getAttribute('role') || '').trim().split(/\s+/);
12716
+ for (var _i = 0, roles_1 = roles; _i < roles_1.length; _i++) {
12717
+ var role = roles_1[_i];
12718
+ if (CONTROL_ROLES.indexOf(role) !== -1) {
12719
+ return role;
12720
+ }
12721
+ }
12722
+ return 'button';
12723
+ }
12724
+ // The page a link to another page of the site opens: its decoded path alone,
12725
+ // since an id in a path can point at one shopper. A segment holding a token,
12726
+ // or following a route word that names a person or carries a code, goes as
12727
+ // `*`, and numbers in any script are masked as in labels. Dropped when it
12728
+ // holds an @, `;` or `=` (session ids and matrix parameters), is too long to
12729
+ // be a route, or the browser can't mask every script's digits.
12730
+ function linkPath(link) {
12731
+ // Each segment decodes on its own, so a literal `%` in one never stops the
12732
+ // next from decoding.
12733
+ var decoded = link.pathname.split('/').map(decodeFully);
12734
+ if (decoded.some(function (segment) {
12735
+ return segment === undefined;
12736
+ })) {
12737
+ return undefined;
12738
+ }
12739
+ var path = decoded.join('/');
12740
+ if (path.length > MAX_RAW_LABEL_LENGTH || /[@;=]/.test(path) || LEFTOVER_ESCAPE.test(path)) {
12741
+ return undefined;
12742
+ }
12743
+ var segments = path.split('/');
12744
+ var masked = segments.map(function (segment, i) {
12745
+ return isTokenSegment(segment) || i > 0 && segment && hidesNextSegment(routeSegment(segments, i)) ? '*' : segment;
12746
+ });
12747
+ return maskNumbers(masked.join('/'));
12440
12748
  }
12441
12749
  // Reads the link's own resolved URL parts, never its full href: only the hostname of
12442
- // a link that leaves the site and the extension of a file it downloads travel.
12443
- // mailto:, tel: and other schemes give undefined.
12750
+ // a link that leaves the site, the path of one to another page of the site and the
12751
+ // extension of a file it downloads travel. mailto:, tel: and other schemes give
12752
+ // undefined.
12444
12753
  function describeLink(link) {
12445
12754
  var href = (link.getAttribute('href') || '').trim();
12446
12755
  if (!href || href.charAt(0) === '#') {
@@ -12478,7 +12787,8 @@ var mParticle = (function () {
12478
12787
  return link.pathname === window.location.pathname && link.search === window.location.search ? {
12479
12788
  type: 'same_page'
12480
12789
  } : {
12481
- type: 'internal'
12790
+ type: 'internal',
12791
+ path: linkPath(link)
12482
12792
  };
12483
12793
  }
12484
12794
  // Whether a described click is on a link that leaves the page, logged as a link
@@ -12488,7 +12798,7 @@ var mParticle = (function () {
12488
12798
  }
12489
12799
  // Checkboxes and radios, and the roles that act as them. A role can list
12490
12800
  // fallbacks, so any of its tokens counts.
12491
- var CHOICE = 'input[type="checkbox"], input[type="radio"], [role~="checkbox"], [role~="radio"], ' + '[role~="switch"], [role~="menuitemcheckbox"], [role~="menuitemradio"]';
12801
+ var CHOICE = 'input[type="checkbox"], input[type="radio"], [role~="checkbox"], [role~="radio"], ' + '[role~="switch"], [role~="menuitemcheckbox"], [role~="menuitemradio"], [role~="option"]';
12492
12802
  var RADIO_ROLE = '[role~="radio"], [role~="menuitemradio"]';
12493
12803
  function boxOf(element) {
12494
12804
  var box = element instanceof HTMLLabelElement ? element.control : element;
@@ -12546,31 +12856,143 @@ var mParticle = (function () {
12546
12856
  // radio's id, test id and the panel it opens name the option picked, so
12547
12857
  // only the name of its input, the question, goes with it. A wrapper's own
12548
12858
  // name, or a role radio's, can name the option instead.
12549
- var radio = isRadio(choice);
12550
- var named = !radio ? control : choice instanceof HTMLInputElement ? choice : null;
12551
- var name = choice ? undefined : accessibleName(control);
12859
+ // An option of a list box, like a radio, names the answer picked.
12860
+ var radio = isRadio(choice) || !!choice && choice.matches('[role~="option"]');
12861
+ var named = !radio ? control : choice instanceof HTMLInputElement && !choice.matches('[role~="option"]') ? choice : null;
12862
+ // A link run by script has no address to say where it goes, so it goes
12863
+ // without its text, as a plain link to another page of the site does: the
12864
+ // text can name the shopper ("Hi, Jane").
12865
+ // A native button keeps its name whatever its role, as it always has.
12866
+ var textless = !!choice || !link && element !== 'wallet' && control.matches('[role~="link"]') && !control.matches(CONTROL_SELECTOR);
12867
+ var name = textless ? undefined : accessibleName(control);
12552
12868
  var label = name ? maskLabel(name.raw) : undefined;
12553
12869
  return {
12554
12870
  label: label,
12555
12871
  labelSource: label ? name.source : 'none',
12556
12872
  // Kept on its own as well: it often says more than the visible text,
12557
12873
  // such as which item a remove button removes.
12558
- ariaLabel: choice ? undefined : maskLabel(control.getAttribute('aria-label')),
12874
+ ariaLabel: textless ? undefined : maskLabel(control.getAttribute('aria-label')),
12559
12875
  icon: choice ? undefined : iconHint(control),
12560
12876
  elementId: radio ? undefined : stableIdentifier(control.getAttribute('id')),
12561
- elementName: stableIdentifier(named && named.getAttribute('name')),
12877
+ elementName: (choice ? questionName : stableIdentifier)(named && named.getAttribute('name')),
12562
12878
  testId: radio ? undefined : firstIdentifier(control, TEST_ID_ATTRIBUTES),
12563
12879
  controls: radio ? undefined : controlsIdentifier(control),
12564
12880
  form: formIdentifier(control),
12881
+ group: choice ? groupName(choice) : undefined,
12565
12882
  element: element,
12566
12883
  type: controlType(control),
12567
12884
  linkType: link ? link.type : undefined,
12568
12885
  linkDomain: link ? link.domain : undefined,
12886
+ linkPath: link ? link.path : undefined,
12569
12887
  fileExtension: link ? link.fileExtension : undefined,
12570
12888
  // The box as the click left it: the browser toggles it before the click
12571
12889
  // reaches listeners, and puts it back if the click is cancelled.
12572
- checked: box ? box.checked : undefined
12573
- };
12890
+ checked: box ? box.checked : undefined,
12891
+ pick: choice || control.matches('[role~="tab"]') ? true : undefined
12892
+ };
12893
+ }
12894
+ // The question a picker answers, from the group around it: the group's
12895
+ // aria-label, aria-labelledby or legend, masked like a label and cut at its
12896
+ // first colon, since a legend can repeat the pick ("Color: Navy"). Never the
12897
+ // option's own text, which is the shopper's answer.
12898
+ var NAMED_GROUP = '[role="radiogroup"], [role="listbox"], [role="group"], fieldset';
12899
+ // A control such as the button that opens a list box shows the option picked.
12900
+ var NOT_A_QUESTION = CHOICE + ', ' + CONTROL_SELECTOR + ', a[href], input, select, textarea, slot, [aria-haspopup], [role~="button"], ' + '[role~="link"], [role~="tab"], [role~="combobox"]';
12901
+ var OPTION_KINDS = ['radio', 'menuitemradio', 'checkbox', 'menuitemcheckbox', 'switch', 'option'];
12902
+ function optionKind(option) {
12903
+ if (option instanceof HTMLInputElement) {
12904
+ return option.type;
12905
+ }
12906
+ var roles = (option.getAttribute('role') || '').trim().split(/\s+/);
12907
+ return roles.filter(function (role) {
12908
+ return OPTION_KINDS.indexOf(role) !== -1;
12909
+ })[0];
12910
+ }
12911
+ // Whether two options answer the same question. A native box shares its
12912
+ // question only with a box of the same name in the same form, as the browser
12913
+ // groups radios, so separate shipping and billing radios ask two.
12914
+ function sameQuestion(option, choice) {
12915
+ if (!(choice instanceof HTMLInputElement)) {
12916
+ // A role option answers only within its own radio group or list box.
12917
+ var container = '[role="radiogroup"], [role="listbox"]';
12918
+ return !(option instanceof HTMLInputElement) && optionKind(option) === optionKind(choice) && option.closest(container) === choice.closest(container);
12919
+ }
12920
+ var name = choice.getAttribute('name');
12921
+ return option instanceof HTMLInputElement && option.type === choice.type && !!name && option.getAttribute('name') === name && option.form === choice.form;
12922
+ }
12923
+ // Whether a group offers another answer to the question picked: a shown option
12924
+ // to the same question that isn't part of the one picked. A card around one
12925
+ // address radio and a "make default" checkbox, or around a role radio and the
12926
+ // hidden input behind it, offers none.
12927
+ function holdsOptions(group, choice) {
12928
+ var options = group.querySelectorAll(CHOICE);
12929
+ for (var i = 0; i < options.length; i++) {
12930
+ var option = options[i];
12931
+ if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) &&
12932
+ // A fully transparent option, as a native input behind a styled
12933
+ // one often is, shows the shopper no second answer.
12934
+ !someComposedAncestor(option, function (node) {
12935
+ return window.getComputedStyle(node).opacity === '0';
12936
+ }, group)) {
12937
+ return true;
12938
+ }
12939
+ }
12940
+ return false;
12941
+ }
12942
+ // The elements an option is named by: those its aria-labelledby points at and
12943
+ // a native box's labels.
12944
+ function optionLabels(option, root) {
12945
+ var labels = option instanceof HTMLInputElement && option.labels ? Array.from(option.labels) : [];
12946
+ (option.getAttribute('aria-labelledby') || '').trim().split(/\s+/).forEach(function (ref) {
12947
+ var target = ref && root.getElementById ? root.getElementById(ref) : null;
12948
+ if (target) {
12949
+ labels.push(target);
12950
+ }
12951
+ });
12952
+ return labels;
12953
+ }
12954
+ // The group asking the question a pick answers. A role radio's is its
12955
+ // radiogroup and an option's its list box; a role toggle, such as a switch,
12956
+ // stands alone. A native box's is the nearest group offering other answers,
12957
+ // since a group around one option, such as a saved address card, can be
12958
+ // named by that option.
12959
+ function questionGroup(choice) {
12960
+ if (!(choice instanceof HTMLInputElement)) {
12961
+ var container = choice.matches('[role~="option"]') ? choice.closest('[role="listbox"]') : choice.matches('[role~="radio"]') ? choice.closest('[role="radiogroup"]') : null;
12962
+ return container && holdsOptions(container, choice) ? container : null;
12963
+ }
12964
+ var group = choice.closest(NAMED_GROUP);
12965
+ while (group && !holdsOptions(group, choice)) {
12966
+ group = group.parentElement ? group.parentElement.closest(NAMED_GROUP) : null;
12967
+ }
12968
+ return group;
12969
+ }
12970
+ function groupName(choice) {
12971
+ var group = questionGroup(choice);
12972
+ if (!group) {
12973
+ return undefined;
12974
+ }
12975
+ var raw = nonBlank(group.getAttribute('aria-label'));
12976
+ if (!raw) {
12977
+ var ids = group.getAttribute('aria-labelledby');
12978
+ var legend = group.tagName === 'FIELDSET' ? Array.from(group.children).filter(function (child) {
12979
+ return child.tagName === 'LEGEND';
12980
+ })[0] : undefined;
12981
+ var root_2 = isFunction(group.getRootNode) ? group.getRootNode() : document;
12982
+ var source_1 = ids ? root_2.getElementById ? root_2.getElementById(ids.trim().split(/\s+/)[0]) : null : legend || null;
12983
+ // A label that is, holds or sits in an option or a control, or in an
12984
+ // option's label, would name the answer.
12985
+ var holder = source_1 ? source_1.closest('label') : null;
12986
+ // Nor one that holds, or sits in, what an option of the group names
12987
+ // itself by.
12988
+ var labelsOption = !!source_1 && Array.from(group.querySelectorAll(CHOICE)).some(function (option) {
12989
+ return optionLabels(option, root_2).some(function (label) {
12990
+ return source_1.contains(label) || label.contains(source_1);
12991
+ });
12992
+ });
12993
+ raw = source_1 && !labelsOption && !hasComposedAncestor(source_1, NOT_A_QUESTION) && !source_1.querySelector(NOT_A_QUESTION) && !(holder && (holder.control || holder.querySelector(CHOICE))) ? textOf(source_1) : undefined;
12994
+ }
12995
+ return raw ? maskLabel(raw.split(':')[0]) : undefined;
12574
12996
  }
12575
12997
  // A question about payment, read from the names its author gave it, so
12576
12998
  // "checkout[payment_gateway]", "paymentMethod" and "tender_type" all count and
@@ -12621,7 +13043,17 @@ var mParticle = (function () {
12621
13043
  return radio instanceof HTMLInputElement ? radio.checked : radio.getAttribute('aria-checked') === 'true';
12622
13044
  }
12623
13045
 
12624
- var checkoutPath = /(?:^|\/)(?:checkouts?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i;
13046
+ // A checkout segment may name its kind or its version, as /express-checkout, /checkout-v2 and /checkout-external do.
13047
+ // Other hyphenated words make it a content slug, such as /help/checkout-faq or /products/self-checkout-kiosk.
13048
+ var checkoutPath = /(?:^|\/)(?:(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?(?:-(?:v\d+|\d+|flow|external|process|step(?:-\d+)?))?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i;
13049
+ // A confirmation page under a checkout route, such as /checkout/thankyou/<order> or /checkouts/<token>/thank_you, comes
13050
+ // after the confirm click. "Complete" and "success" mark one only as the last word of a segment, so /complete-checkout
13051
+ // and /checkout/complete-registration stay checkout routes.
13052
+ var confirmationPath = /(?:^|[\/_-])(?:thank[-_]?you|thanks|confirmation|confirmed|receipt|received)(?:[\/_-]|$)|(?:^|[\/_-])(?:complete|completed|success)(?:\/|$)/i;
13053
+ // A bare "Register" creates an account, rather than placing the order, on a checkout's sign-in or account step or in a
13054
+ // form that asks for a password.
13055
+ var bareRegister = /^register(?: now)?$/;
13056
+ var accountStepPath = /(?:^|\/)(?:register|registration|login|log-in|signin|sign-in|signup|sign-up|account|create-account)(?:\/|$)/i;
12625
13057
  // English look-alikes are whole words; the other languages stay substring
12626
13058
  // matches, since several of them do not separate words with spaces.
12627
13059
  var englishExclusions = new RegExp('\\b(?:coupon|promo(?: code)?|newsletter|sign in|sign up|log in|login|add to (?:cart|bag|basket)|' + 'gift card balance|(?:add|apply|redeem|check|enter|use) (?:a |an |your )?gift card|pay later|' + 'interest free|as low as|learn more|prequalify|apply (?:now|for)|payment plan|available|accepted|' + 'we accept|set up|add money|reload|top up|add to (?:apple|google) wallet|connect|link your|unlink|' + 'remove|delete|edit|change|manage|use a different|switch)\\b|^pay in$|\\bpay in (?:installments|instalments)\\b');
@@ -12632,7 +13064,7 @@ var mParticle = (function () {
12632
13064
  // A plain link is navigation, so in English only an order or purchase it
12633
13065
  // places, submits or completes counts as the confirm click.
12634
13066
  var linkCommits = new RegExp('^(?:(?:place|submit|complete)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase)|' + otherCommits + ')$');
12635
- var routeGatedCommits = new RegExp('^(?:buy now|purchase|jetzt kaufen|kaufen|commander|(?:passer|valider) (?:la |ma )?commande|finalizar (?:la |mi )?compra|' + 'comprar ahora|acquista ora|nu kopen|\u4ECA\u3059\u3050\u8CFC\u5165|\u8CFC\u5165\u3059\u308B|\u3053\u306E\u5185\u5BB9\u3067\u7533\u3057\u8FBC\u3080)$');
13067
+ var routeGatedCommits = new RegExp('^(?:buy now|purchase|register(?: now)?|complete (?:my |your )?registration|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)$');
12636
13068
  var steps = new RegExp('^(?:' + ["(?:let's |lets |go to |proceed to |secure )?(?:check ?out|checkout)(?: now| securely| as guest)?", '(?:continue|proceed) to (?:payment|review|billing|shipping|delivery|order review)|review (?:my |your )?order', 'guest checkout|(?:pay|continue|check ?out) (?:with|by) (?:card|credit card|debit card|credit or debit card)', 'zur kasse|zur kasse gehen|auschecken|weiter zur zahlung', 'passer \u00E0 la caisse|continuer vers le paiement|proc\u00E9der au paiement', 'ir a caja|continuar al pago|tramitar pedido', "procedi al checkout|procedi all'acquisto|procedi al pagamento|vai alla cassa", 'afrekenen|verder naar betalen|doorgaan naar betalen', '\u30EC\u30B8\u306B\u9032\u3080|\u3054?(?:\u8CFC\u5165|\u6CE8\u6587)\u624B\u7D9A\u304D(?:\u3078|\u306B)(?:\u9032\u3080)?|\u304A?\u652F\u6255\u3044(?:\u3078|\u306B)\u9032\u3080|(?:\u3054\u6CE8\u6587\u5185\u5BB9\u306E)?\u78BA\u8A8D\u753B\u9762\u3078(?:\u9032\u3080)?'].join('|') + ')$');
12637
13069
  var CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
12638
13070
  // "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
@@ -12679,7 +13111,7 @@ var mParticle = (function () {
12679
13111
  var EDGE = "[\\s!-/:-@\\[-`{-~\\u00A1-\\u00BF\\u00D7\\u00F7\\u2000-\\u206F\\u20A0-\\u20CF\\u2190-\\u2BFF" + "\\u3000-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u30FB\\uD800-\\uDFFF\\uFE00-\\uFE0F" + "\\uFF01-\\uFF0F\\uFF1A-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65]+";
12680
13112
  var EDGE_SYMBOLS = new RegExp('^' + EDGE + '|' + EDGE + '$', 'g');
12681
13113
  function getIntentRouteClass(pathname) {
12682
- return checkoutPath.test(pathname) ? 'checkout_path' : 'other';
13114
+ return checkoutPath.test(pathname) && !confirmationPath.test(pathname) ? 'checkout_path' : 'other';
12683
13115
  }
12684
13116
  var SEPARATORS = /[-_|\u00B7\u2022]/g;
12685
13117
  var CURRENCY_CODES = /(?:^| )(?:usd|aud|cad|nzd|gbp|eur|chf|sgd|hkd|jpy|inr|mxn)(?= |$)|\b(?:a|au|ca|c|us|nz|hk|s)\$/g;
@@ -12744,13 +13176,26 @@ var mParticle = (function () {
12744
13176
  return openCartLabels.test(text);
12745
13177
  }) ? 'open' : undefined;
12746
13178
  }
12747
- // "Buy now" and "Purchase" count only on a checkout route, and never on a plain
12748
- // link, which is navigation.
12749
- function isCommitLabel(text, isLink, pathname) {
13179
+ // Every field the browser counts as the form's own, form-attribute fields included. Read through the native getter
13180
+ // where the browser has one, since a field named "elements" shadows the form's own property.
13181
+ function formFields(form) {
13182
+ var descriptor = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, 'elements');
13183
+ return descriptor && descriptor.get ? descriptor.get.call(form) : form.elements;
13184
+ }
13185
+ // The form a button or input submits, which its form attribute can name from outside it.
13186
+ function createsAccount(control, pathname) {
13187
+ var form = control instanceof HTMLButtonElement || control instanceof HTMLInputElement ? control.form : control.closest('form');
13188
+ return accountStepPath.test(pathname) || !!form && Array.prototype.some.call(formFields(form), function (field) {
13189
+ return field instanceof HTMLInputElement && field.type === 'password';
13190
+ });
13191
+ }
13192
+ // "Buy now", "Purchase" and "Register" count only on a checkout route that isn't a
13193
+ // confirmation page, and never on a plain link, which is navigation.
13194
+ function isCommitLabel(text, isLink, pathname, control) {
12750
13195
  if (isLink) {
12751
13196
  return linkCommits.test(text);
12752
13197
  }
12753
- return commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path';
13198
+ return commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path' && !(bareRegister.test(text) && createsAccount(control, pathname));
12754
13199
  }
12755
13200
  // Classifies a clicked or submitting control found by findControl, in order:
12756
13201
  // pickers, toggles and pay-later messages never count; a vendor's own wallet
@@ -12794,7 +13239,7 @@ var mParticle = (function () {
12794
13239
  }
12795
13240
  var isLink = control.matches('a[href]:not([role="button"])');
12796
13241
  if (cores.some(function (text) {
12797
- return isCommitLabel(text, isLink, pathname);
13242
+ return isCommitLabel(text, isLink, pathname, control);
12798
13243
  })) {
12799
13244
  return {
12800
13245
  kind: 'commit',
@@ -12915,7 +13360,9 @@ var mParticle = (function () {
12915
13360
  var SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
12916
13361
  // Apple Pay must begin inside the tap's own handler, so the tap is this recent.
12917
13362
  var APPLE_PAY_AFTER_CLICK_MS = 1000;
12918
- var PATH_LIMIT = 12;
13363
+ // How far up from the clicked element a control is looked for. Checkout intent
13364
+ // keeps findControl's own 12; logging looks further, past deep product cards.
13365
+ var PATH_LIMIT = 20;
12919
13366
  // A ceiling, not a dedupe: a held Enter key or browser automation fires a
12920
13367
  // trusted click per repeat, and each one is forwarded to every kit.
12921
13368
  var PAGE_EVENT_LIMIT = 100;
@@ -13229,22 +13676,25 @@ var mParticle = (function () {
13229
13676
  var path = isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : [];
13230
13677
  var excluded = isExcludedPath(path);
13231
13678
  var control = excluded ? undefined : findControl(path, true);
13232
- if (control) {
13679
+ var logged = control || (excluded ? undefined : findControl(path, true, PATH_LIMIT) || findRoleControl(path, PATH_LIMIT));
13680
+ if (logged) {
13233
13681
  // With links alone, only an anchor can be logged.
13234
- click = logsEvents && (this.logButtons || control.matches('a[href]')) ? describeButtonClick(control, path) : undefined;
13682
+ click = logsEvents && (this.logButtons || logged.matches('a[href]')) ? describeButtonClick(logged, path) : undefined;
13235
13683
  if (click) {
13236
13684
  asLink = this.logLinks && isLinkClick(click);
13237
13685
  // Without link tracking, a link that leaves the page stays a
13238
13686
  // button click, except a plain link to another page of the
13239
13687
  // site, which button tracking never logged.
13240
- var asButton = this.logButtons && !asLink && !(click.linkType === 'internal' && control.matches('a[href]:not([role="button"])'));
13688
+ var asButton = this.logButtons && !asLink && !(click.linkType === 'internal' && logged.matches('a[href]:not([role="button"])'));
13241
13689
  if (!asLink && !asButton) {
13242
13690
  click = undefined;
13243
13691
  }
13244
13692
  }
13245
13693
  if (click && click.checked !== undefined) {
13246
- box = control;
13694
+ box = logged;
13247
13695
  }
13696
+ }
13697
+ if (control) {
13248
13698
  // Checkout intent is only worked out when the kit can use it.
13249
13699
  var intent = signalsKit ? classifyIntent(control, pathname) : undefined;
13250
13700
  kind = intent ? intent.kind : undefined;
@@ -13335,9 +13785,10 @@ var mParticle = (function () {
13335
13785
  this.mpInstance._Events.logEvent(asLink ? buildLinkClickEvent(click, pathname) : buildButtonClickEvent(click, pathname));
13336
13786
  // Only a click waiting for the batch interval is sent early: a held
13337
13787
  // one goes out when released, and with batching off it already has.
13338
- // A checkbox or radio waits for the batch, since choosing an option
13339
- // doesn't leave the page the way a confirm click can.
13340
- if (!this.leaving && click.checked === undefined && this.queuedForUpload() > queued) {
13788
+ // A pick, such as a checkbox, a radio or a tab, waits for the batch,
13789
+ // since choosing an option doesn't leave the page the way a confirm
13790
+ // click can.
13791
+ if (!this.leaving && !click.pick && this.queuedForUpload() > queued) {
13341
13792
  this.uploadNow();
13342
13793
  }
13343
13794
  };