@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.common.js +1 -1
- package/dist/mparticle.esm.js +1 -1
- package/dist/mparticle.js +500 -49
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +4 -1
- package/dist/types/src/buttonTracking/controls.d.ts +2 -1
- package/dist/types/src/buttonTracking/labelMasking.d.ts +1 -0
- package/dist/types/src/pageUrl.d.ts +1 -0
- package/dist/types/src/pathMasking.d.ts +7 -0
- package/package.json +1 -1
- package/src/buttonTracking/buttonClickEvent.ts +9 -3
- package/src/buttonTracking/buttonDescription.ts +319 -17
- package/src/buttonTracking/buttonTracker.ts +21 -10
- package/src/buttonTracking/checkoutIntent.ts +44 -8
- package/src/buttonTracking/controls.ts +54 -2
- package/src/buttonTracking/labelMasking.ts +6 -0
- package/src/pageUrl.ts +58 -9
- package/src/pageViewTracker.ts +6 -2
- package/src/pathMasking.ts +199 -0
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.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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
12436
|
-
//
|
|
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
|
-
|
|
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
|
|
12443
|
-
// mailto:, tel: and other schemes give
|
|
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
|
-
|
|
12550
|
-
var
|
|
12551
|
-
var
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
12748
|
-
//
|
|
12749
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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' &&
|
|
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 =
|
|
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
|
|
13339
|
-
// doesn't leave the page the way a confirm
|
|
13340
|
-
|
|
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
|
};
|