@mparticle/web-sdk 3.26.1 → 3.28.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 +622 -110
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +3 -1
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +0 -6
- 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 +9 -0
- package/package.json +1 -1
- package/src/batchUploader.ts +25 -9
- package/src/buttonTracking/buttonClickEvent.ts +9 -3
- package/src/buttonTracking/buttonDescription.ts +325 -17
- package/src/buttonTracking/buttonTracker.ts +21 -68
- package/src/buttonTracking/checkoutIntent.ts +63 -9
- package/src/buttonTracking/controls.ts +54 -2
- package/src/buttonTracking/labelMasking.ts +6 -0
- package/src/pageUrl.ts +58 -9
- package/src/pageViewTracker.ts +9 -3
- package/src/pathMasking.ts +306 -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.28.0";
|
|
208
208
|
|
|
209
209
|
var Constants = {
|
|
210
210
|
sdkVersion: version,
|
|
@@ -2732,6 +2732,248 @@ 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
|
+
// Pages under a personal route that name no one: the sign-in, sign-up and
|
|
2760
|
+
// account steps sites keep there, as in `/u/login` or `/u/mfa-sms-challenge`.
|
|
2761
|
+
var ACCOUNT_PAGE = /^(?:log-?in|log-?out|sign-?in|sign-?out|sign-?up|register|registration|create-account|account|settings|forgot-password|reset-password|password|challenge|consent|callback|authorize|continue|mfa(?:-[a-z]+)*)$/i;
|
|
2762
|
+
// Whether a segment after a route goes as `*`: always after an order route, and
|
|
2763
|
+
// after a personal route unless it is one of the account pages kept there.
|
|
2764
|
+
function hidesSegmentAfter(route, segment) {
|
|
2765
|
+
return hidesNextSegment(route) && !(PERSONAL_ROUTE.test(route) && ACCOUNT_PAGE.test(segment));
|
|
2766
|
+
}
|
|
2767
|
+
function hidesNextSegment(route) {
|
|
2768
|
+
return PERSONAL_ROUTE.test(route) || ORDER_ROUTE.test(route) && !PRODUCT_ORDER_WORD.test(route);
|
|
2769
|
+
}
|
|
2770
|
+
// How many times a value is decoded at most, so one encoded twice or more reads
|
|
2771
|
+
// as its plain text.
|
|
2772
|
+
var MAX_DECODES = 5;
|
|
2773
|
+
// Decodes until the text stops changing. Undefined when the first decode fails;
|
|
2774
|
+
// a later failure keeps what decoded so far, as a literal `%` would.
|
|
2775
|
+
function decodeFully(text) {
|
|
2776
|
+
var current = text;
|
|
2777
|
+
for (var i = 0; i < MAX_DECODES; i++) {
|
|
2778
|
+
var next = void 0;
|
|
2779
|
+
try {
|
|
2780
|
+
next = decodeURIComponent(current);
|
|
2781
|
+
} catch (e) {
|
|
2782
|
+
return i === 0 ? undefined : current;
|
|
2783
|
+
}
|
|
2784
|
+
if (next === current) {
|
|
2785
|
+
return current;
|
|
2786
|
+
}
|
|
2787
|
+
current = next;
|
|
2788
|
+
}
|
|
2789
|
+
return current;
|
|
2790
|
+
}
|
|
2791
|
+
// The route word in a raw path segment: its last part behind an encoded slash,
|
|
2792
|
+
// read before any matrix parameter such as `;jsessionid=`, its `;` encoded once,
|
|
2793
|
+
// twice or not at all, so a parameter's own value never hides it. Undefined when
|
|
2794
|
+
// it can't be decoded, or is still encoded after MAX_DECODES.
|
|
2795
|
+
function routeWord(segment) {
|
|
2796
|
+
var text = segment;
|
|
2797
|
+
for (var i = 0; i < MAX_DECODES; i++) {
|
|
2798
|
+
var route = text.split(MATRIX_SEPARATOR)[0];
|
|
2799
|
+
var next = void 0;
|
|
2800
|
+
try {
|
|
2801
|
+
next = decodeURIComponent(route);
|
|
2802
|
+
} catch (e) {
|
|
2803
|
+
return i === 0 ? undefined : route.split('/').pop();
|
|
2804
|
+
}
|
|
2805
|
+
if (next === route) {
|
|
2806
|
+
return route.split('/').pop();
|
|
2807
|
+
}
|
|
2808
|
+
text = next;
|
|
2809
|
+
}
|
|
2810
|
+
return undefined;
|
|
2811
|
+
}
|
|
2812
|
+
// The last segment before index i that isn't empty, so `/orders//12345678`
|
|
2813
|
+
// still reads the order route.
|
|
2814
|
+
function routeSegment(segments, i) {
|
|
2815
|
+
var routeIndex = i - 1;
|
|
2816
|
+
while (routeIndex > 0 && segments[routeIndex] === '') {
|
|
2817
|
+
routeIndex--;
|
|
2818
|
+
}
|
|
2819
|
+
return segments[routeIndex];
|
|
2820
|
+
}
|
|
2821
|
+
// A piece with a lowercase letter right before an uppercase one, as a random
|
|
2822
|
+
// code has and a Title-Case word doesn't.
|
|
2823
|
+
var INNER_CASE_SWITCH = /[a-z][A-Z]/;
|
|
2824
|
+
var isToken = function isToken(text) {
|
|
2825
|
+
return MIXED_TOKEN.test(text) || CASE_SWITCHING_TOKEN.test(text);
|
|
2826
|
+
};
|
|
2827
|
+
var mixesLettersAndDigits = function mixesLettersAndDigits(piece) {
|
|
2828
|
+
return /[0-9]/.test(piece) && /[A-Za-z]/.test(piece);
|
|
2829
|
+
};
|
|
2830
|
+
// `+` sits inside base64 tokens, so it never splits one. A token that `-` or `_`
|
|
2831
|
+
// cuts into short pieces is still one when the pieces read as one joined and
|
|
2832
|
+
// either a piece switches case inside it or two or more pieces mix letters and
|
|
2833
|
+
// digits, as `a1b2-c3d4-e5f6` does. Title-Case-Words and plain slugs such as
|
|
2834
|
+
// `iphone-15-pro` do neither.
|
|
2835
|
+
function isTokenSegment(segment) {
|
|
2836
|
+
var text = segment.replace(/\+/g, '');
|
|
2837
|
+
var pieces = text.split(/[-_.~]/);
|
|
2838
|
+
var joined = pieces.join('');
|
|
2839
|
+
return pieces.some(isToken) || pieces.some(function (piece) {
|
|
2840
|
+
return INNER_CASE_SWITCH.test(piece);
|
|
2841
|
+
}) && isToken(joined) || pieces.filter(mixesLettersAndDigits).length >= 2 && MIXED_TOKEN.test(joined);
|
|
2842
|
+
}
|
|
2843
|
+
// A word a product name is made of: three or more letters, lowercase or
|
|
2844
|
+
// Capitalised. A code's mixed case or capital-letter groups aren't words.
|
|
2845
|
+
var isWord = function isWord(piece) {
|
|
2846
|
+
return /^(?:[a-z]{3,}|[A-Z][a-z]{2,})$/.test(piece);
|
|
2847
|
+
};
|
|
2848
|
+
var UUID_IN_TEXT = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
2849
|
+
// Route words whose next segment names a product, as in `/p/<name>` or
|
|
2850
|
+
// `/product/<name>`. Only there can words beside a token be a product's name
|
|
2851
|
+
// rather than a person's or part of a code.
|
|
2852
|
+
var PRODUCT_ROUTE = /^(?:p|product|products|item|items|dp|shop|catalog)$/i;
|
|
2853
|
+
// A segment that holds a token goes as `*`. After a product route, only the
|
|
2854
|
+
// token goes, so `/p/trail-runner-gtx-zq7Kd93Lm41` keeps its name as
|
|
2855
|
+
// `/p/trail-runner-gtx-*`.
|
|
2856
|
+
// Every piece that isn't a word in an unbroken run with the token goes with it
|
|
2857
|
+
// as one `*`, so the rest of a UUID or of a code cut into short pieces never
|
|
2858
|
+
// stays. A piece that can't be decoded or decodes to a separator counts as a
|
|
2859
|
+
// token, and so does any piece that mixes letters and digits or switches case
|
|
2860
|
+
// inside, as a code's short pieces do. The segment goes whole as `*` when no
|
|
2861
|
+
// piece is a word or it holds a UUID. Undefined when it holds no token. `segment` is as the page
|
|
2862
|
+
// has it, `text` decoded, `route` the route word before it.
|
|
2863
|
+
function maskTokenSegment(segment, text, route) {
|
|
2864
|
+
if (!isTokenSegment(text)) {
|
|
2865
|
+
return undefined;
|
|
2866
|
+
}
|
|
2867
|
+
// A UUID anywhere in the decoded text, however its hyphens are escaped,
|
|
2868
|
+
// takes the whole segment.
|
|
2869
|
+
if (!route || !PRODUCT_ROUTE.test(route) || UUID_IN_TEXT.test(text)) {
|
|
2870
|
+
return '*';
|
|
2871
|
+
}
|
|
2872
|
+
var parts = segment.split(/([-_.~])/);
|
|
2873
|
+
var raws = parts.filter(function (_, i) {
|
|
2874
|
+
return i % 2 === 0;
|
|
2875
|
+
});
|
|
2876
|
+
var separators = parts.filter(function (_, i) {
|
|
2877
|
+
return i % 2 === 1;
|
|
2878
|
+
});
|
|
2879
|
+
var decoded = raws.map(function (raw) {
|
|
2880
|
+
return decodeFully(raw);
|
|
2881
|
+
});
|
|
2882
|
+
var kinds = decoded.map(function (piece) {
|
|
2883
|
+
if (piece === undefined || /[-_.~]/.test(piece)) {
|
|
2884
|
+
return 'token';
|
|
2885
|
+
}
|
|
2886
|
+
var plain = piece.replace(/\+/g, '');
|
|
2887
|
+
return isToken(plain) || mixesLettersAndDigits(plain) || INNER_CASE_SWITCH.test(plain) ? 'token' : isWord(plain) ? 'word' : 'other';
|
|
2888
|
+
});
|
|
2889
|
+
// Each unbroken run of pieces that aren't words is hidden when it holds a
|
|
2890
|
+
// token.
|
|
2891
|
+
var masked = kinds.map(function () {
|
|
2892
|
+
return false;
|
|
2893
|
+
});
|
|
2894
|
+
for (var start = 0; start < kinds.length;) {
|
|
2895
|
+
if (kinds[start] === 'word') {
|
|
2896
|
+
start++;
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
var end = start;
|
|
2900
|
+
while (end < kinds.length && kinds[end] !== 'word') {
|
|
2901
|
+
end++;
|
|
2902
|
+
}
|
|
2903
|
+
// A run reads as a token also when only its pieces joined do.
|
|
2904
|
+
if (kinds.slice(start, end).indexOf('token') !== -1 || isToken(raws.slice(start, end).join(''))) {
|
|
2905
|
+
for (var k = start; k < end; k++) {
|
|
2906
|
+
masked[k] = true;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
start = end;
|
|
2910
|
+
}
|
|
2911
|
+
if (masked.indexOf(true) === -1 || kinds.indexOf('word') === -1) {
|
|
2912
|
+
return '*';
|
|
2913
|
+
}
|
|
2914
|
+
var out = '';
|
|
2915
|
+
for (var k = 0; k < raws.length; k++) {
|
|
2916
|
+
if (masked[k] && k > 0 && masked[k - 1]) {
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
out += (k > 0 ? separators[k - 1] : '') + (masked[k] ? '*' : raws[k]);
|
|
2920
|
+
}
|
|
2921
|
+
return out;
|
|
2922
|
+
}
|
|
2923
|
+
// The path a click logs. A segment that can single out one shopper
|
|
2924
|
+
// goes as `*`: one holding an @, `;` or `=`, an id, or the one after a route
|
|
2925
|
+
// that names a person, carries a code or opens an order. A token inside a name
|
|
2926
|
+
// goes as `*` alone. Kept segments stay exactly as the page has them, plain
|
|
2927
|
+
// numbers included, since on a product page they name the product.
|
|
2928
|
+
function pagePath(pathname) {
|
|
2929
|
+
var segments = pathname.split('/');
|
|
2930
|
+
return segments.map(function (segment, i) {
|
|
2931
|
+
var text = decodeFully(segment);
|
|
2932
|
+
var route = i > 0 ? routeWord(routeSegment(segments, i)) : '';
|
|
2933
|
+
// A route word that can't be decoded may be any route, so the
|
|
2934
|
+
// segment after it is hidden too.
|
|
2935
|
+
var hiddenByRoute = i > 0 && !!text && (route === undefined || hidesSegmentAfter(route, text));
|
|
2936
|
+
// An encoded slash hides several segments in one, so it goes whole.
|
|
2937
|
+
if (text === undefined || /[@;=/]/.test(text) || LEFTOVER_ESCAPE.test(text) || hiddenByRoute) {
|
|
2938
|
+
return '*';
|
|
2939
|
+
}
|
|
2940
|
+
var masked = maskTokenSegment(segment, text, route);
|
|
2941
|
+
return masked === undefined ? segment : masked;
|
|
2942
|
+
}).join('/');
|
|
2943
|
+
}
|
|
2944
|
+
// A token long enough that no page or product name looks like it: 16 or more
|
|
2945
|
+
// letters and digits mixed, as checkout tokens are, 16 or more letters
|
|
2946
|
+
// switching case like a random code, or a UUID.
|
|
2947
|
+
var LONG_MIXED_TOKEN = /^(?=[^0-9]*[0-9])(?=[^A-Za-z]*[A-Za-z])[A-Za-z0-9]{16,}$/;
|
|
2948
|
+
var LONG_CASE_SWITCHING_TOKEN = /^(?=[A-Za-z]{16,}$)(?:[^a-z]*[a-z]+[A-Z]){3}/;
|
|
2949
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2950
|
+
// A token that `-` or `_` cuts into shorter pieces counts when two or more
|
|
2951
|
+
// pieces mix letters and digits, as `a1b2c3d4-e5f6g7h8` does and a product slug
|
|
2952
|
+
// such as `acmephone-15-pro-max-256gb` doesn't.
|
|
2953
|
+
function isLongToken(segment) {
|
|
2954
|
+
var pieces = segment.replace(/\+/g, '').split(/[-_.~]/);
|
|
2955
|
+
return UUID.test(segment) || pieces.some(function (piece) {
|
|
2956
|
+
return LONG_MIXED_TOKEN.test(piece) || LONG_CASE_SWITCHING_TOKEN.test(piece);
|
|
2957
|
+
}) || pieces.filter(mixesLettersAndDigits).length >= 2 && LONG_MIXED_TOKEN.test(pieces.join(''));
|
|
2958
|
+
}
|
|
2959
|
+
// The path every event carries, in its page URL and page views, where a
|
|
2960
|
+
// wrong `*` costs every output the page it names. Only what reliably singles
|
|
2961
|
+
// out one shopper goes as `*`: a segment holding an @, `;` or `=`, one that
|
|
2962
|
+
// can't be decoded or still holds an escape, a long token, and the one after a
|
|
2963
|
+
// route that names a person or opens an order when it holds a digit or a token.
|
|
2964
|
+
// Product codes and pages such as `/profile/edit` or `/orders/history` stay.
|
|
2965
|
+
function pageUrlPath(pathname) {
|
|
2966
|
+
var segments = pathname.split('/');
|
|
2967
|
+
return segments.map(function (segment, i) {
|
|
2968
|
+
var text = decodeFully(segment);
|
|
2969
|
+
var route = i > 0 ? routeWord(routeSegment(segments, i)) : '';
|
|
2970
|
+
var holdsId = !!text && (/[0-9]/.test(text) || isTokenSegment(text));
|
|
2971
|
+
// A route word that can't be decoded may be any route.
|
|
2972
|
+
var hiddenByRoute = i > 0 && !!text && (route === undefined || hidesNextSegment(route) && holdsId);
|
|
2973
|
+
return text === undefined || /[@;=/]/.test(text) || LEFTOVER_ESCAPE.test(text) || isLongToken(text) || hiddenByRoute ? '*' : segment;
|
|
2974
|
+
}).join('/');
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2735
2977
|
var HISTORY_METHODS = ['pushState', 'replaceState'];
|
|
2736
2978
|
var WRAPPED_MARKER = '__mpApvWrapped__';
|
|
2737
2979
|
var supportsHistoryTracking = function supportsHistoryTracking(win) {
|
|
@@ -3073,7 +3315,8 @@ var mParticle = (function () {
|
|
|
3073
3315
|
data: __assign(__assign(__assign({}, paramsToAttributes(params)), {
|
|
3074
3316
|
hostname: hostname,
|
|
3075
3317
|
title: title,
|
|
3076
|
-
path
|
|
3318
|
+
// Masked here, not in the page snapshot, whose raw path tells one page from the next.
|
|
3319
|
+
path: pageUrlPath(path)
|
|
3077
3320
|
}), autoPageViewAttribute(isAutoPageView)),
|
|
3078
3321
|
eventType: EventType.Unknown
|
|
3079
3322
|
};
|
|
@@ -3169,7 +3412,9 @@ var mParticle = (function () {
|
|
|
3169
3412
|
var name = _a.name;
|
|
3170
3413
|
return name;
|
|
3171
3414
|
});
|
|
3172
|
-
|
|
3415
|
+
// A log line is read by no output, so it takes the click path's fuller rules.
|
|
3416
|
+
var path = pagePath(page.path);
|
|
3417
|
+
return names.length ? "".concat(path, " (params: ").concat(names.join(), ")") : path;
|
|
3173
3418
|
};
|
|
3174
3419
|
// ---------------------------------------------------------------------------
|
|
3175
3420
|
// History patching
|
|
@@ -3343,7 +3588,9 @@ var mParticle = (function () {
|
|
|
3343
3588
|
title: title,
|
|
3344
3589
|
isAutoPageView: this.isAutoPageView
|
|
3345
3590
|
}));
|
|
3346
|
-
|
|
3591
|
+
// A title can name the shopper or an order, so it stays out of the log
|
|
3592
|
+
// line, for the same reason describePage omits param values.
|
|
3593
|
+
this.log("[fire] deferred flush -> _Events.logEvent(PageView) (page: ".concat(describePage(page), ")"));
|
|
3347
3594
|
this.mpInstance._Events.logEvent(event);
|
|
3348
3595
|
};
|
|
3349
3596
|
PageViewTracker.prototype.log = function (message) {
|
|
@@ -3353,12 +3600,16 @@ var mParticle = (function () {
|
|
|
3353
3600
|
}();
|
|
3354
3601
|
|
|
3355
3602
|
// The one rule for a URL that leaves the SDK, shared with kits through
|
|
3356
|
-
// `mParticle._cleanUrl`. `path` keeps scheme, host and path
|
|
3603
|
+
// `mParticle._cleanUrl`. `path` keeps scheme, host and path, with any path
|
|
3604
|
+
// segment that can single out one shopper as `*` (see pageUrlPath). `full` adds the
|
|
3357
3605
|
// query params an auto page view captures by default and the campaign
|
|
3358
|
-
// attribution params, then the fragment.
|
|
3606
|
+
// attribution params, then the fragment, a hash route's path masked the same way.
|
|
3359
3607
|
var UrlMode = {
|
|
3360
3608
|
Path: 'path',
|
|
3361
|
-
Full: 'full'
|
|
3609
|
+
Full: 'full',
|
|
3610
|
+
// A path for a log or an error report, masked with the click path's fuller
|
|
3611
|
+
// rules, since no output reads it as a page.
|
|
3612
|
+
Report: 'report'
|
|
3362
3613
|
};
|
|
3363
3614
|
// Campaign attribution and ad-network click ids. URLs keep them; an auto page view
|
|
3364
3615
|
// carries one as an attribute only when an input configures it.
|
|
@@ -3377,6 +3628,31 @@ var mParticle = (function () {
|
|
|
3377
3628
|
// Allowlisted params whose value is itself a URL, so it can carry a query.
|
|
3378
3629
|
var URL_VALUED_QUERY_PARAMS = ['ref', 'referrer'];
|
|
3379
3630
|
var USERINFO = /^((?:[a-z][a-z0-9+.-]*:)?\/\/)[^/]*@/i;
|
|
3631
|
+
var ORIGIN = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^/]*/i;
|
|
3632
|
+
// Only a URL or a path is masked. A bare value, such as `ref=spring2024`, is a
|
|
3633
|
+
// code rather than a page, and stays as written.
|
|
3634
|
+
var maskPath = function maskPath(url, mask) {
|
|
3635
|
+
if (mask === void 0) {
|
|
3636
|
+
mask = pageUrlPath;
|
|
3637
|
+
}
|
|
3638
|
+
var origin = ORIGIN.exec(url);
|
|
3639
|
+
if (origin) {
|
|
3640
|
+
return origin[0] + mask(url.slice(origin[0].length));
|
|
3641
|
+
}
|
|
3642
|
+
return url.charAt(0) === '/' ? mask(url) : url;
|
|
3643
|
+
};
|
|
3644
|
+
// A hash route keeps its `#` or `#!` and has its path masked as the URL's own.
|
|
3645
|
+
// The `&` pairs or flags after the path lose the OAuth/OIDC ones, as a bare
|
|
3646
|
+
// fragment's pairs do.
|
|
3647
|
+
var maskRoute = function maskRoute(route) {
|
|
3648
|
+
var prefix = /^#!?/.exec(route)[0];
|
|
3649
|
+
var pairsStart = route.indexOf('&');
|
|
3650
|
+
var pathEnd = pairsStart === -1 ? route.length : pairsStart;
|
|
3651
|
+
var pairs = route.slice(pathEnd).split('&').slice(1).filter(keepsFragmentPair).map(function (pair) {
|
|
3652
|
+
return "&".concat(pair);
|
|
3653
|
+
}).join('');
|
|
3654
|
+
return prefix + pageUrlPath(route.slice(prefix.length, pathEnd)) + pairs;
|
|
3655
|
+
};
|
|
3380
3656
|
var decodeQueryComponent = function decodeQueryComponent(component) {
|
|
3381
3657
|
return decodeURIComponent(component.replace(/\+/g, ' '));
|
|
3382
3658
|
};
|
|
@@ -3424,15 +3700,24 @@ var mParticle = (function () {
|
|
|
3424
3700
|
var cleanFragment = function cleanFragment(hash) {
|
|
3425
3701
|
var queryStart = hash.indexOf('?');
|
|
3426
3702
|
var route = queryStart === -1 ? hash : hash.slice(0, queryStart);
|
|
3427
|
-
|
|
3703
|
+
// `#/\u2026`, `#!\u2026`, or a fragment with a `/` before any `&` or `?`, as
|
|
3704
|
+
// `#orders/12345678` is in routers that drop the leading slash.
|
|
3705
|
+
// A `/` counts only before any `=`, so a bare pair such as
|
|
3706
|
+
// `#access_token=abc/def` is never read as a route.
|
|
3707
|
+
var isHashRoute = /^#!|^#[^?&=]*\//.test(hash);
|
|
3708
|
+
var isRoute = isHashRoute || queryStart !== -1 && route.indexOf('=') === -1;
|
|
3428
3709
|
if (isRoute && queryStart !== -1) {
|
|
3429
3710
|
var query = cleanQuery(hash.slice(queryStart + 1));
|
|
3430
|
-
return route + (query ? "?".concat(query) : '');
|
|
3711
|
+
return maskRoute(route) + (query ? "?".concat(query) : '');
|
|
3431
3712
|
}
|
|
3713
|
+
// A plain anchor such as `#reviews` names a part of the page, not a route.
|
|
3432
3714
|
if (hash.indexOf('=') === -1) {
|
|
3433
|
-
return hash;
|
|
3715
|
+
return isHashRoute ? maskRoute(hash) : hash;
|
|
3434
3716
|
}
|
|
3435
|
-
|
|
3717
|
+
// A hash route's path leads its pairs, as in `#/cb&tab=2`.
|
|
3718
|
+
var pairs = hash.slice(1).split('&').map(function (pair, i) {
|
|
3719
|
+
return i === 0 && isHashRoute ? maskRoute("#".concat(pair)).slice(1) : pair;
|
|
3720
|
+
}).filter(keepsFragmentPair).join('&');
|
|
3436
3721
|
return pairs ? "#".concat(pairs) : '';
|
|
3437
3722
|
};
|
|
3438
3723
|
// String-based rather than `new URL`, so kept pairs and the fragment go out
|
|
@@ -3446,7 +3731,7 @@ var mParticle = (function () {
|
|
|
3446
3731
|
var hash = hashStart === -1 ? '' : href.slice(hashStart);
|
|
3447
3732
|
var beforeHash = hashStart === -1 ? href : href.slice(0, hashStart);
|
|
3448
3733
|
var queryStart = beforeHash.indexOf('?');
|
|
3449
|
-
var path = (queryStart === -1 ? beforeHash : beforeHash.slice(0, queryStart)).replace(USERINFO, '$1');
|
|
3734
|
+
var path = maskPath((queryStart === -1 ? beforeHash : beforeHash.slice(0, queryStart)).replace(USERINFO, '$1'), mode === UrlMode.Report ? pagePath : pageUrlPath);
|
|
3450
3735
|
if (mode !== UrlMode.Full) {
|
|
3451
3736
|
return path;
|
|
3452
3737
|
}
|
|
@@ -4079,13 +4364,12 @@ var mParticle = (function () {
|
|
|
4079
4364
|
// TODO: https://go.mparticle.com/work/SQDSDKS-5165
|
|
4080
4365
|
(_d = this.batchesQueuedForProcessing).unshift.apply(_d, batchesThatDidNotUpload);
|
|
4081
4366
|
}
|
|
4082
|
-
// Update Offline Storage with current state of batch queue
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
// Local Storage once we can confirm they are successfully uploaded.
|
|
4367
|
+
// Update Offline Storage with current state of batch queue. Storage was
|
|
4368
|
+
// purged before sending. A beacon the browser accepted counts as sent, so
|
|
4369
|
+
// the beacon path writes nothing unless the browser refused a batch: then
|
|
4370
|
+
// `batchesThatDidNotUpload` holds it, and it is stored since the page may
|
|
4371
|
+
// be going away.
|
|
4372
|
+
if (this.offlineStorageEnabled && this.batchVault && (!useBeacon || !isEmpty(batchesThatDidNotUpload))) {
|
|
4089
4373
|
this.storeBatchesQueuedForProcessing();
|
|
4090
4374
|
}
|
|
4091
4375
|
if (triggerFuture && !this.destroyed) {
|
|
@@ -4142,7 +4426,7 @@ var mParticle = (function () {
|
|
|
4142
4426
|
};
|
|
4143
4427
|
BatchUploader.prototype.uploadBatches = function (batches, useBeacon) {
|
|
4144
4428
|
return __awaiter(this, void 0, void 0, function () {
|
|
4145
|
-
var uploads, uploadsToLog, i, fetchPayload, blob, response, e_1;
|
|
4429
|
+
var uploads, uploadsToLog, refused, i, fetchPayload, blob, response, e_1;
|
|
4146
4430
|
return __generator(this, function (_a) {
|
|
4147
4431
|
switch (_a.label) {
|
|
4148
4432
|
case 0:
|
|
@@ -4157,6 +4441,7 @@ var mParticle = (function () {
|
|
|
4157
4441
|
this.mpInstance.Logger.verbose("Uploading batches: ".concat(JSON.stringify(uploadsToLog)));
|
|
4158
4442
|
this.mpInstance.Logger.verbose("Batch count: ".concat(uploads.length));
|
|
4159
4443
|
}
|
|
4444
|
+
refused = [];
|
|
4160
4445
|
i = 0;
|
|
4161
4446
|
_a.label = 1;
|
|
4162
4447
|
case 1:
|
|
@@ -4173,7 +4458,13 @@ var mParticle = (function () {
|
|
|
4173
4458
|
blob = new Blob([fetchPayload.body], {
|
|
4174
4459
|
type: 'text/plain;charset=UTF-8'
|
|
4175
4460
|
});
|
|
4176
|
-
|
|
4461
|
+
// The browser refuses a beacon it won't send, such as one over its
|
|
4462
|
+
// size limit, which says nothing about the next batch. Keep it and
|
|
4463
|
+
// try the rest. Only false is a refusal: a wrapper that returns
|
|
4464
|
+
// nothing has still sent it.
|
|
4465
|
+
if (navigator.sendBeacon(this.uploadUrl, blob) === false) {
|
|
4466
|
+
refused.push(uploads[i]);
|
|
4467
|
+
}
|
|
4177
4468
|
return [3 /*break*/, 5];
|
|
4178
4469
|
case 2:
|
|
4179
4470
|
_a.trys.push([2, 4,, 5]);
|
|
@@ -4204,6 +4495,10 @@ var mParticle = (function () {
|
|
|
4204
4495
|
i++;
|
|
4205
4496
|
return [3 /*break*/, 1];
|
|
4206
4497
|
case 6:
|
|
4498
|
+
if (!isEmpty(refused)) {
|
|
4499
|
+
this.mpInstance.Logger.warning("The browser refused the exit upload of ".concat(refused.length, " batch(es). Keeping them for the next upload."));
|
|
4500
|
+
return [2 /*return*/, refused];
|
|
4501
|
+
}
|
|
4207
4502
|
return [2 /*return*/, null];
|
|
4208
4503
|
}
|
|
4209
4504
|
});
|
|
@@ -11784,7 +12079,7 @@ var mParticle = (function () {
|
|
|
11784
12079
|
label_source: click.labelSource,
|
|
11785
12080
|
element: click.element,
|
|
11786
12081
|
type: click.type,
|
|
11787
|
-
path: pathname,
|
|
12082
|
+
path: pagePath(pathname),
|
|
11788
12083
|
hostname: window.location.hostname
|
|
11789
12084
|
};
|
|
11790
12085
|
// Optional fields are left out when the control has no safe value for them.
|
|
@@ -11797,8 +12092,10 @@ var mParticle = (function () {
|
|
|
11797
12092
|
test_id: click.testId,
|
|
11798
12093
|
controls: click.controls,
|
|
11799
12094
|
form: click.form,
|
|
12095
|
+
group: click.group,
|
|
11800
12096
|
link_type: click.linkType,
|
|
11801
12097
|
link_domain: click.linkDomain,
|
|
12098
|
+
link_path: click.linkPath,
|
|
11802
12099
|
file_extension: click.fileExtension
|
|
11803
12100
|
};
|
|
11804
12101
|
for (var _i = 0, _a = Object.keys(optional); _i < _a.length; _i++) {
|
|
@@ -11825,8 +12122,9 @@ var mParticle = (function () {
|
|
|
11825
12122
|
};
|
|
11826
12123
|
// One custom event per click on a link that leaves the page. Typed Other, not
|
|
11827
12124
|
// 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
|
|
12125
|
+
// of the site goes without its text, which can name the shopper ("Hi, Jane"), and
|
|
12126
|
+
// with its destination path instead; a button or wallet acting as a link keeps
|
|
12127
|
+
// its name.
|
|
11830
12128
|
var buildLinkClickEvent = function buildLinkClickEvent(click, pathname) {
|
|
11831
12129
|
var data = clickAttributes(click.linkType === 'internal' && click.element === 'link' ? __assign(__assign({}, click), {
|
|
11832
12130
|
label: undefined,
|
|
@@ -11844,7 +12142,7 @@ var mParticle = (function () {
|
|
|
11844
12142
|
// Every attribute buildButtonClickEvent can set apart from the marker and the
|
|
11845
12143
|
// hostname, which is the same on every click on this page. A spec checks the
|
|
11846
12144
|
// 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'];
|
|
12145
|
+
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
12146
|
// Which check keeps some or all auto button clicks, or auto link clicks with
|
|
11849
12147
|
// `link`, from a kit, or undefined when the kit receives every one. A forwarding
|
|
11850
12148
|
// rule on an attribute whose value differs between clicks lets some through and
|
|
@@ -12058,17 +12356,20 @@ var mParticle = (function () {
|
|
|
12058
12356
|
// or around it never becomes its tap. Excluded clicks give nothing; disabled controls are skipped, a
|
|
12059
12357
|
// wallet-named one also by its `disabled` class. A caller that has already
|
|
12060
12358
|
// found the path not excluded passes `included` so the walk runs once.
|
|
12061
|
-
function findControl(path, included) {
|
|
12359
|
+
function findControl(path, included, limit) {
|
|
12062
12360
|
if (included === void 0) {
|
|
12063
12361
|
included = false;
|
|
12064
12362
|
}
|
|
12363
|
+
if (limit === void 0) {
|
|
12364
|
+
limit = 12;
|
|
12365
|
+
}
|
|
12065
12366
|
if (!included && isExcludedPath(path)) {
|
|
12066
12367
|
return undefined;
|
|
12067
12368
|
}
|
|
12068
12369
|
var control;
|
|
12069
12370
|
var option;
|
|
12070
12371
|
var picked = false;
|
|
12071
|
-
for (var _i = 0, _a = path.slice(0,
|
|
12372
|
+
for (var _i = 0, _a = path.slice(0, limit); _i < _a.length; _i++) {
|
|
12072
12373
|
var target = _a[_i];
|
|
12073
12374
|
if (control) {
|
|
12074
12375
|
break;
|
|
@@ -12091,6 +12392,39 @@ var mParticle = (function () {
|
|
|
12091
12392
|
}
|
|
12092
12393
|
return control;
|
|
12093
12394
|
}
|
|
12395
|
+
// Roles that act as controls without being buttons or links: pickers, tabs,
|
|
12396
|
+
// options and links run by script. A role can list fallbacks, so any of its
|
|
12397
|
+
// tokens counts.
|
|
12398
|
+
var ROLE_CONTROL = '[role~="radio"], [role~="menuitemradio"], [role~="checkbox"], [role~="menuitemcheckbox"], ' + '[role~="switch"], [role~="option"], [role~="tab"], [role~="link"]';
|
|
12399
|
+
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;
|
|
12400
|
+
// The nearest role control on the click path, for logging the click alone:
|
|
12401
|
+
// checkout intent and payment choices only ever read what findControl finds.
|
|
12402
|
+
// None when anything disabled sits on the path, so a role control around or
|
|
12403
|
+
// inside a disabled control never logs the click it suppressed.
|
|
12404
|
+
function findRoleControl(path, limit) {
|
|
12405
|
+
var entries = path.slice(0, limit).filter(function (target) {
|
|
12406
|
+
return target instanceof Element;
|
|
12407
|
+
});
|
|
12408
|
+
if (entries.some(function (target) {
|
|
12409
|
+
return target.matches(':disabled, [aria-disabled="true"]') || target.classList.contains('disabled') && isWalletNamed(target);
|
|
12410
|
+
})) {
|
|
12411
|
+
return undefined;
|
|
12412
|
+
}
|
|
12413
|
+
for (var _i = 0, entries_3 = entries; _i < entries_3.length; _i++) {
|
|
12414
|
+
var target = entries_3[_i];
|
|
12415
|
+
if (target.matches(ROLE_CONTROL)) {
|
|
12416
|
+
// A typing field, or anything in an editable region, takes typing
|
|
12417
|
+
// whatever role it carries.
|
|
12418
|
+
return target.matches(TYPING_FIELD) || hasComposedAncestor(target, EDITABLE) ? undefined : target;
|
|
12419
|
+
}
|
|
12420
|
+
// A click in a typing field belongs to the field, not to the role
|
|
12421
|
+
// control around it.
|
|
12422
|
+
if (target.matches(TYPING_FIELD)) {
|
|
12423
|
+
return undefined;
|
|
12424
|
+
}
|
|
12425
|
+
}
|
|
12426
|
+
return undefined;
|
|
12427
|
+
}
|
|
12094
12428
|
// Text that may hold what a shopper typed or what the page opted out: form
|
|
12095
12429
|
// fields, editable regions, and anything under data-mp-no-track. `own` is a
|
|
12096
12430
|
// box the element labels, which holds no typing of its own.
|
|
@@ -12131,6 +12465,11 @@ var mParticle = (function () {
|
|
|
12131
12465
|
var masked = text.replace(digitPatterns.number, '#').slice(0, 64).trim();
|
|
12132
12466
|
return masked || undefined;
|
|
12133
12467
|
}
|
|
12468
|
+
// Masks numbers and amounts in any script as '#', or undefined where the browser
|
|
12469
|
+
// can't see every script's digits.
|
|
12470
|
+
function maskNumbers(text) {
|
|
12471
|
+
return digitPatterns ? text.replace(digitPatterns.number, '#') : undefined;
|
|
12472
|
+
}
|
|
12134
12473
|
|
|
12135
12474
|
var DOWNLOAD_EXTENSIONS = /\.(pdf|zip|csv|txt|ics|docx?|xlsx?|pptx?|dmg|exe|apk|mp3|mp4)$/i;
|
|
12136
12475
|
function elementKind(control) {
|
|
@@ -12141,7 +12480,9 @@ var mParticle = (function () {
|
|
|
12141
12480
|
if (tag === 'button' || tag === 'input') {
|
|
12142
12481
|
return tag;
|
|
12143
12482
|
}
|
|
12144
|
-
|
|
12483
|
+
// A link run by script reads as a link, as an anchor does, so whoever reads
|
|
12484
|
+
// the click treats it the same.
|
|
12485
|
+
return control.matches('a[href]:not([role="button"]), [role~="link"]:not([role~="button"])') ? 'link' : 'role';
|
|
12145
12486
|
}
|
|
12146
12487
|
var NOT_RENDERED = 'script, style, template, [hidden], [aria-hidden="true"], [inert]';
|
|
12147
12488
|
// The most nodes read for one label source, and images looked at for one
|
|
@@ -12323,7 +12664,7 @@ var mParticle = (function () {
|
|
|
12323
12664
|
} else if (readable && isShown(control)) {
|
|
12324
12665
|
var text = renderedText(control);
|
|
12325
12666
|
if (text === undefined) {
|
|
12326
|
-
return {
|
|
12667
|
+
return headingName(control) || {
|
|
12327
12668
|
source: 'none'
|
|
12328
12669
|
};
|
|
12329
12670
|
}
|
|
@@ -12341,6 +12682,12 @@ var mParticle = (function () {
|
|
|
12341
12682
|
};
|
|
12342
12683
|
}
|
|
12343
12684
|
}
|
|
12685
|
+
if (!(control instanceof HTMLInputElement) && isShown(control)) {
|
|
12686
|
+
var heading = headingName(control);
|
|
12687
|
+
if (heading) {
|
|
12688
|
+
return heading;
|
|
12689
|
+
}
|
|
12690
|
+
}
|
|
12344
12691
|
var title = nonBlank(control.getAttribute('title'));
|
|
12345
12692
|
if (title) {
|
|
12346
12693
|
return {
|
|
@@ -12352,6 +12699,25 @@ var mParticle = (function () {
|
|
|
12352
12699
|
source: 'none'
|
|
12353
12700
|
};
|
|
12354
12701
|
}
|
|
12702
|
+
// A card whose text runs too long to keep, or has too many parts to read, is
|
|
12703
|
+
// named by its first heading, as a product or menu card usually is. Not a card
|
|
12704
|
+
// holding a form field, nor one that takes in slotted content or holds a
|
|
12705
|
+
// shadow root, whose fields the form field check can't see.
|
|
12706
|
+
function headingName(control) {
|
|
12707
|
+
if (isUnsafeTextSource(control) || control.shadowRoot || control.querySelector('slot') ||
|
|
12708
|
+
// A custom element can hide fields in a closed shadow root, which
|
|
12709
|
+
// nothing outside it can see.
|
|
12710
|
+
control.tagName.indexOf('-') !== -1 || Array.from(control.querySelectorAll('*')).some(function (element) {
|
|
12711
|
+
return !!element.shadowRoot || element.tagName.indexOf('-') !== -1;
|
|
12712
|
+
})) {
|
|
12713
|
+
return undefined;
|
|
12714
|
+
}
|
|
12715
|
+
var text = textOf(control.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]'));
|
|
12716
|
+
return text ? {
|
|
12717
|
+
raw: text,
|
|
12718
|
+
source: 'heading'
|
|
12719
|
+
} : undefined;
|
|
12720
|
+
}
|
|
12355
12721
|
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
12722
|
// Size, style and animation classes that share the icon prefixes.
|
|
12357
12723
|
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 +12771,12 @@ var mParticle = (function () {
|
|
|
12405
12771
|
var digits = id.replace(/[^0-9]/g, '').length;
|
|
12406
12772
|
return id.length <= 64 && digits <= 3 && HAND_WRITTEN_ID.test(id) ? id : undefined;
|
|
12407
12773
|
}
|
|
12774
|
+
// A picker's input name, which names its question. Sites often end it with the
|
|
12775
|
+
// id of the product it belongs to (`color-swatches-48213907`); without that
|
|
12776
|
+
// number it still names the question.
|
|
12777
|
+
function questionName(value) {
|
|
12778
|
+
return stableIdentifier(value && value.trim().replace(/[-_][0-9]{4,}$/, ''));
|
|
12779
|
+
}
|
|
12408
12780
|
var TEST_ID_ATTRIBUTES = ['data-testid', 'data-test-id', 'data-test', 'data-qa', 'data-cy'];
|
|
12409
12781
|
// The first of these attributes that holds a developer-written identifier.
|
|
12410
12782
|
function firstIdentifier(control, attributes) {
|
|
@@ -12432,15 +12804,60 @@ var mParticle = (function () {
|
|
|
12432
12804
|
}
|
|
12433
12805
|
// The browser's normalised type: a button defaults to 'submit' and an unknown
|
|
12434
12806
|
// input type reads as 'text'. A native button or input reports its own type, a
|
|
12435
|
-
// wallet-named one included
|
|
12436
|
-
//
|
|
12807
|
+
// wallet-named one included. A role control that isn't a button reads as its
|
|
12808
|
+
// role: 'radio', 'option', 'tab', 'link' and so on. Any other control, such as
|
|
12809
|
+
// a role button or a vendor's wallet element, reads as 'button'.
|
|
12810
|
+
var CONTROL_ROLES = ['radio', 'menuitemradio', 'checkbox', 'menuitemcheckbox', 'switch', 'option', 'tab', 'link'];
|
|
12437
12811
|
function controlType(control) {
|
|
12438
12812
|
var tag = control.tagName.toLowerCase();
|
|
12439
|
-
|
|
12813
|
+
if (tag === 'button' || tag === 'input') {
|
|
12814
|
+
return control.type;
|
|
12815
|
+
}
|
|
12816
|
+
if (control.matches('[role~="button"]')) {
|
|
12817
|
+
return 'button';
|
|
12818
|
+
}
|
|
12819
|
+
var roles = (control.getAttribute('role') || '').trim().split(/\s+/);
|
|
12820
|
+
for (var _i = 0, roles_1 = roles; _i < roles_1.length; _i++) {
|
|
12821
|
+
var role = roles_1[_i];
|
|
12822
|
+
if (CONTROL_ROLES.indexOf(role) !== -1) {
|
|
12823
|
+
return role;
|
|
12824
|
+
}
|
|
12825
|
+
}
|
|
12826
|
+
return 'button';
|
|
12827
|
+
}
|
|
12828
|
+
// The page a link to another page of the site opens: its decoded path alone,
|
|
12829
|
+
// since an id in a path can point at one shopper. A segment holding a token,
|
|
12830
|
+
// or following a route word that names a person or carries a code, goes as
|
|
12831
|
+
// `*`, and numbers in any script are masked as in labels. Dropped when it
|
|
12832
|
+
// holds an @, `;` or `=` (session ids and matrix parameters), is too long to
|
|
12833
|
+
// be a route, or the browser can't mask every script's digits.
|
|
12834
|
+
function linkPath(link) {
|
|
12835
|
+
// Each segment decodes on its own, so a literal `%` in one never stops the
|
|
12836
|
+
// next from decoding.
|
|
12837
|
+
var decoded = link.pathname.split('/').map(decodeFully);
|
|
12838
|
+
if (decoded.some(function (segment) {
|
|
12839
|
+
return segment === undefined;
|
|
12840
|
+
})) {
|
|
12841
|
+
return undefined;
|
|
12842
|
+
}
|
|
12843
|
+
var path = decoded.join('/');
|
|
12844
|
+
if (path.length > MAX_RAW_LABEL_LENGTH || /[@;=]/.test(path) || LEFTOVER_ESCAPE.test(path)) {
|
|
12845
|
+
return undefined;
|
|
12846
|
+
}
|
|
12847
|
+
var segments = path.split('/');
|
|
12848
|
+
var masked = segments.map(function (segment, i) {
|
|
12849
|
+
if (i > 0 && segment && hidesSegmentAfter(routeSegment(segments, i), segment)) {
|
|
12850
|
+
return '*';
|
|
12851
|
+
}
|
|
12852
|
+
var tokens = maskTokenSegment(segment, segment, i > 0 ? routeSegment(segments, i) : undefined);
|
|
12853
|
+
return tokens === undefined ? segment : tokens;
|
|
12854
|
+
});
|
|
12855
|
+
return maskNumbers(masked.join('/'));
|
|
12440
12856
|
}
|
|
12441
12857
|
// 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
|
|
12858
|
+
// a link that leaves the site, the path of one to another page of the site and the
|
|
12859
|
+
// extension of a file it downloads travel. mailto:, tel: and other schemes give
|
|
12860
|
+
// undefined.
|
|
12444
12861
|
function describeLink(link) {
|
|
12445
12862
|
var href = (link.getAttribute('href') || '').trim();
|
|
12446
12863
|
if (!href || href.charAt(0) === '#') {
|
|
@@ -12478,7 +12895,8 @@ var mParticle = (function () {
|
|
|
12478
12895
|
return link.pathname === window.location.pathname && link.search === window.location.search ? {
|
|
12479
12896
|
type: 'same_page'
|
|
12480
12897
|
} : {
|
|
12481
|
-
type: 'internal'
|
|
12898
|
+
type: 'internal',
|
|
12899
|
+
path: linkPath(link)
|
|
12482
12900
|
};
|
|
12483
12901
|
}
|
|
12484
12902
|
// Whether a described click is on a link that leaves the page, logged as a link
|
|
@@ -12488,7 +12906,7 @@ var mParticle = (function () {
|
|
|
12488
12906
|
}
|
|
12489
12907
|
// Checkboxes and radios, and the roles that act as them. A role can list
|
|
12490
12908
|
// 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"]';
|
|
12909
|
+
var CHOICE = 'input[type="checkbox"], input[type="radio"], [role~="checkbox"], [role~="radio"], ' + '[role~="switch"], [role~="menuitemcheckbox"], [role~="menuitemradio"], [role~="option"]';
|
|
12492
12910
|
var RADIO_ROLE = '[role~="radio"], [role~="menuitemradio"]';
|
|
12493
12911
|
function boxOf(element) {
|
|
12494
12912
|
var box = element instanceof HTMLLabelElement ? element.control : element;
|
|
@@ -12546,32 +12964,143 @@ var mParticle = (function () {
|
|
|
12546
12964
|
// radio's id, test id and the panel it opens name the option picked, so
|
|
12547
12965
|
// only the name of its input, the question, goes with it. A wrapper's own
|
|
12548
12966
|
// name, or a role radio's, can name the option instead.
|
|
12549
|
-
|
|
12550
|
-
var
|
|
12551
|
-
var
|
|
12967
|
+
// An option of a list box, like a radio, names the answer picked.
|
|
12968
|
+
var radio = isRadio(choice) || !!choice && choice.matches('[role~="option"]');
|
|
12969
|
+
var named = !radio ? control : choice instanceof HTMLInputElement && !choice.matches('[role~="option"]') ? choice : null;
|
|
12970
|
+
// A link run by script has no address to say where it goes, so it goes
|
|
12971
|
+
// without its text, as a plain link to another page of the site does: the
|
|
12972
|
+
// text can name the shopper ("Hi, Jane").
|
|
12973
|
+
// A native button keeps its name whatever its role, as it always has.
|
|
12974
|
+
var textless = !!choice || !link && element !== 'wallet' && control.matches('[role~="link"]') && !control.matches(CONTROL_SELECTOR);
|
|
12975
|
+
var name = textless ? undefined : accessibleName(control);
|
|
12552
12976
|
var label = name ? maskLabel(name.raw) : undefined;
|
|
12553
12977
|
return {
|
|
12554
12978
|
label: label,
|
|
12555
12979
|
labelSource: label ? name.source : 'none',
|
|
12556
12980
|
// Kept on its own as well: it often says more than the visible text,
|
|
12557
12981
|
// such as which item a remove button removes.
|
|
12558
|
-
ariaLabel:
|
|
12982
|
+
ariaLabel: textless ? undefined : maskLabel(control.getAttribute('aria-label')),
|
|
12559
12983
|
icon: choice ? undefined : iconHint(control),
|
|
12560
12984
|
elementId: radio ? undefined : stableIdentifier(control.getAttribute('id')),
|
|
12561
|
-
elementName: stableIdentifier(named && named.getAttribute('name')),
|
|
12985
|
+
elementName: (choice ? questionName : stableIdentifier)(named && named.getAttribute('name')),
|
|
12562
12986
|
testId: radio ? undefined : firstIdentifier(control, TEST_ID_ATTRIBUTES),
|
|
12563
12987
|
controls: radio ? undefined : controlsIdentifier(control),
|
|
12564
12988
|
form: formIdentifier(control),
|
|
12989
|
+
group: choice ? groupName(choice) : undefined,
|
|
12565
12990
|
element: element,
|
|
12566
12991
|
type: controlType(control),
|
|
12567
12992
|
linkType: link ? link.type : undefined,
|
|
12568
12993
|
linkDomain: link ? link.domain : undefined,
|
|
12994
|
+
linkPath: link ? link.path : undefined,
|
|
12569
12995
|
fileExtension: link ? link.fileExtension : undefined,
|
|
12570
12996
|
// The box as the click left it: the browser toggles it before the click
|
|
12571
12997
|
// reaches listeners, and puts it back if the click is cancelled.
|
|
12572
12998
|
checked: box ? box.checked : undefined
|
|
12573
12999
|
};
|
|
12574
13000
|
}
|
|
13001
|
+
// The question a picker answers, from the group around it: the group's
|
|
13002
|
+
// aria-label, aria-labelledby or legend, masked like a label and cut at its
|
|
13003
|
+
// first colon, since a legend can repeat the pick ("Color: Navy"). Never the
|
|
13004
|
+
// option's own text, which is the shopper's answer.
|
|
13005
|
+
var NAMED_GROUP = '[role="radiogroup"], [role="listbox"], [role="group"], fieldset';
|
|
13006
|
+
// A control such as the button that opens a list box shows the option picked.
|
|
13007
|
+
var NOT_A_QUESTION = CHOICE + ', ' + CONTROL_SELECTOR + ', a[href], input, select, textarea, slot, [aria-haspopup], [role~="button"], ' + '[role~="link"], [role~="tab"], [role~="combobox"]';
|
|
13008
|
+
var OPTION_KINDS = ['radio', 'menuitemradio', 'checkbox', 'menuitemcheckbox', 'switch', 'option'];
|
|
13009
|
+
function optionKind(option) {
|
|
13010
|
+
if (option instanceof HTMLInputElement) {
|
|
13011
|
+
return option.type;
|
|
13012
|
+
}
|
|
13013
|
+
var roles = (option.getAttribute('role') || '').trim().split(/\s+/);
|
|
13014
|
+
return roles.filter(function (role) {
|
|
13015
|
+
return OPTION_KINDS.indexOf(role) !== -1;
|
|
13016
|
+
})[0];
|
|
13017
|
+
}
|
|
13018
|
+
// Whether two options answer the same question. A native box shares its
|
|
13019
|
+
// question only with a box of the same name in the same form, as the browser
|
|
13020
|
+
// groups radios, so separate shipping and billing radios ask two.
|
|
13021
|
+
function sameQuestion(option, choice) {
|
|
13022
|
+
if (!(choice instanceof HTMLInputElement)) {
|
|
13023
|
+
// A role option answers only within its own radio group or list box.
|
|
13024
|
+
var container = '[role="radiogroup"], [role="listbox"]';
|
|
13025
|
+
return !(option instanceof HTMLInputElement) && optionKind(option) === optionKind(choice) && option.closest(container) === choice.closest(container);
|
|
13026
|
+
}
|
|
13027
|
+
var name = choice.getAttribute('name');
|
|
13028
|
+
return option instanceof HTMLInputElement && option.type === choice.type && !!name && option.getAttribute('name') === name && option.form === choice.form;
|
|
13029
|
+
}
|
|
13030
|
+
// Whether a group offers another answer to the question picked: a shown option
|
|
13031
|
+
// to the same question that isn't part of the one picked. A card around one
|
|
13032
|
+
// address radio and a "make default" checkbox, or around a role radio and the
|
|
13033
|
+
// hidden input behind it, offers none.
|
|
13034
|
+
function holdsOptions(group, choice) {
|
|
13035
|
+
var options = group.querySelectorAll(CHOICE);
|
|
13036
|
+
for (var i = 0; i < options.length; i++) {
|
|
13037
|
+
var option = options[i];
|
|
13038
|
+
if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) &&
|
|
13039
|
+
// A fully transparent option, as a native input behind a styled
|
|
13040
|
+
// one often is, shows the shopper no second answer.
|
|
13041
|
+
!someComposedAncestor(option, function (node) {
|
|
13042
|
+
return window.getComputedStyle(node).opacity === '0';
|
|
13043
|
+
}, group)) {
|
|
13044
|
+
return true;
|
|
13045
|
+
}
|
|
13046
|
+
}
|
|
13047
|
+
return false;
|
|
13048
|
+
}
|
|
13049
|
+
// The elements an option is named by: those its aria-labelledby points at and
|
|
13050
|
+
// a native box's labels.
|
|
13051
|
+
function optionLabels(option, root) {
|
|
13052
|
+
var labels = option instanceof HTMLInputElement && option.labels ? Array.from(option.labels) : [];
|
|
13053
|
+
(option.getAttribute('aria-labelledby') || '').trim().split(/\s+/).forEach(function (ref) {
|
|
13054
|
+
var target = ref && root.getElementById ? root.getElementById(ref) : null;
|
|
13055
|
+
if (target) {
|
|
13056
|
+
labels.push(target);
|
|
13057
|
+
}
|
|
13058
|
+
});
|
|
13059
|
+
return labels;
|
|
13060
|
+
}
|
|
13061
|
+
// The group asking the question a pick answers. A role radio's is its
|
|
13062
|
+
// radiogroup and an option's its list box; a role toggle, such as a switch,
|
|
13063
|
+
// stands alone. A native box's is the nearest group offering other answers,
|
|
13064
|
+
// since a group around one option, such as a saved address card, can be
|
|
13065
|
+
// named by that option.
|
|
13066
|
+
function questionGroup(choice) {
|
|
13067
|
+
if (!(choice instanceof HTMLInputElement)) {
|
|
13068
|
+
var container = choice.matches('[role~="option"]') ? choice.closest('[role="listbox"]') : choice.matches('[role~="radio"]') ? choice.closest('[role="radiogroup"]') : null;
|
|
13069
|
+
return container && holdsOptions(container, choice) ? container : null;
|
|
13070
|
+
}
|
|
13071
|
+
var group = choice.closest(NAMED_GROUP);
|
|
13072
|
+
while (group && !holdsOptions(group, choice)) {
|
|
13073
|
+
group = group.parentElement ? group.parentElement.closest(NAMED_GROUP) : null;
|
|
13074
|
+
}
|
|
13075
|
+
return group;
|
|
13076
|
+
}
|
|
13077
|
+
function groupName(choice) {
|
|
13078
|
+
var group = questionGroup(choice);
|
|
13079
|
+
if (!group) {
|
|
13080
|
+
return undefined;
|
|
13081
|
+
}
|
|
13082
|
+
var raw = nonBlank(group.getAttribute('aria-label'));
|
|
13083
|
+
if (!raw) {
|
|
13084
|
+
var ids = group.getAttribute('aria-labelledby');
|
|
13085
|
+
var legend = group.tagName === 'FIELDSET' ? Array.from(group.children).filter(function (child) {
|
|
13086
|
+
return child.tagName === 'LEGEND';
|
|
13087
|
+
})[0] : undefined;
|
|
13088
|
+
var root_2 = isFunction(group.getRootNode) ? group.getRootNode() : document;
|
|
13089
|
+
var source_1 = ids ? root_2.getElementById ? root_2.getElementById(ids.trim().split(/\s+/)[0]) : null : legend || null;
|
|
13090
|
+
// A label that is, holds or sits in an option or a control, or in an
|
|
13091
|
+
// option's label, would name the answer.
|
|
13092
|
+
var holder = source_1 ? source_1.closest('label') : null;
|
|
13093
|
+
// Nor one that holds, or sits in, what an option of the group names
|
|
13094
|
+
// itself by.
|
|
13095
|
+
var labelsOption = !!source_1 && Array.from(group.querySelectorAll(CHOICE)).some(function (option) {
|
|
13096
|
+
return optionLabels(option, root_2).some(function (label) {
|
|
13097
|
+
return source_1.contains(label) || label.contains(source_1);
|
|
13098
|
+
});
|
|
13099
|
+
});
|
|
13100
|
+
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;
|
|
13101
|
+
}
|
|
13102
|
+
return raw ? maskLabel(raw.split(':')[0]) : undefined;
|
|
13103
|
+
}
|
|
12575
13104
|
// A question about payment, read from the names its author gave it, so
|
|
12576
13105
|
// "checkout[payment_gateway]", "paymentMethod" and "tender_type" all count and
|
|
12577
13106
|
// "payee" does not. A radio's input name is its question; its group or
|
|
@@ -12621,7 +13150,20 @@ var mParticle = (function () {
|
|
|
12621
13150
|
return radio instanceof HTMLInputElement ? radio.checked : radio.getAttribute('aria-checked') === 'true';
|
|
12622
13151
|
}
|
|
12623
13152
|
|
|
12624
|
-
|
|
13153
|
+
// A checkout segment may name its kind or its version, as /express-checkout, /checkout-v2 and /checkout-external do.
|
|
13154
|
+
// Other hyphenated words make it a content slug, such as /help/checkout-faq or /products/self-checkout-kiosk.
|
|
13155
|
+
var CHECKOUT_SEGMENT = '(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?' + '(?:-(?:v\\d+|\\d+|flow|external|process|step(?:-\\d+)?))?';
|
|
13156
|
+
var checkoutPath = new RegExp('(?:^|\\/)(?:' + CHECKOUT_SEGMENT + '|payment|order|kasse|paiement|commander?|pago)(?:\\/|$)', 'i');
|
|
13157
|
+
// A route with a checkout segment itself, not just a payment or order one, which account pages also use.
|
|
13158
|
+
var checkoutSegmentPath = new RegExp('(?:^|\\/)' + CHECKOUT_SEGMENT + '(?:\\/|$)', 'i');
|
|
13159
|
+
// A confirmation page under a checkout route, such as /checkout/thankyou/<order> or /checkouts/<token>/thank_you, comes
|
|
13160
|
+
// after the confirm click. "Complete" and "success" mark one only as the last word of a segment, so /complete-checkout
|
|
13161
|
+
// and /checkout/complete-registration stay checkout routes.
|
|
13162
|
+
var confirmationPath = /(?:^|[\/_-])(?:thank[-_]?you|thanks|confirmation|confirmed|receipt|received)(?:[\/_-]|$)|(?:^|[\/_-])(?:complete|completed|success)(?:\/|$)/i;
|
|
13163
|
+
// A bare "Register" creates an account, rather than placing the order, on a checkout's sign-in or account step or in a
|
|
13164
|
+
// form that asks for a password.
|
|
13165
|
+
var bareRegister = /^register(?: now)?$/;
|
|
13166
|
+
var accountStepPath = /(?:^|\/)(?:register|registration|login|log-in|signin|sign-in|signup|sign-up|account|create-account)(?:\/|$)/i;
|
|
12625
13167
|
// English look-alikes are whole words; the other languages stay substring
|
|
12626
13168
|
// matches, since several of them do not separate words with spaces.
|
|
12627
13169
|
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,8 +13174,11 @@ var mParticle = (function () {
|
|
|
12632
13174
|
// A plain link is navigation, so in English only an order or purchase it
|
|
12633
13175
|
// places, submits or completes counts as the confirm click.
|
|
12634
13176
|
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)$');
|
|
13177
|
+
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
13178
|
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('|') + ')$');
|
|
13179
|
+
// "Save and continue" also saves profile, address and card forms, so it is a step only on a checkout segment's route
|
|
13180
|
+
// that isn't an account or confirmation page.
|
|
13181
|
+
var routeGatedSteps = /^save and continue$/;
|
|
12637
13182
|
var CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
|
|
12638
13183
|
// "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
|
|
12639
13184
|
var addToCartLabels = new RegExp('^add(?: [^ ]+){0,5} to ' + CART_NOUN + '$');
|
|
@@ -12679,7 +13224,7 @@ var mParticle = (function () {
|
|
|
12679
13224
|
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
13225
|
var EDGE_SYMBOLS = new RegExp('^' + EDGE + '|' + EDGE + '$', 'g');
|
|
12681
13226
|
function getIntentRouteClass(pathname) {
|
|
12682
|
-
return checkoutPath.test(pathname) ? 'checkout_path' : 'other';
|
|
13227
|
+
return checkoutPath.test(pathname) && !confirmationPath.test(pathname) ? 'checkout_path' : 'other';
|
|
12683
13228
|
}
|
|
12684
13229
|
var SEPARATORS = /[-_|\u00B7\u2022]/g;
|
|
12685
13230
|
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 +13289,26 @@ var mParticle = (function () {
|
|
|
12744
13289
|
return openCartLabels.test(text);
|
|
12745
13290
|
}) ? 'open' : undefined;
|
|
12746
13291
|
}
|
|
12747
|
-
//
|
|
12748
|
-
//
|
|
12749
|
-
function
|
|
13292
|
+
// Every field the browser counts as the form's own, form-attribute fields included. Read through the native getter
|
|
13293
|
+
// where the browser has one, since a field named "elements" shadows the form's own property.
|
|
13294
|
+
function formFields(form) {
|
|
13295
|
+
var descriptor = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, 'elements');
|
|
13296
|
+
return descriptor && descriptor.get ? descriptor.get.call(form) : form.elements;
|
|
13297
|
+
}
|
|
13298
|
+
// The form a button or input submits, which its form attribute can name from outside it.
|
|
13299
|
+
function createsAccount(control, pathname) {
|
|
13300
|
+
var form = control instanceof HTMLButtonElement || control instanceof HTMLInputElement ? control.form : control.closest('form');
|
|
13301
|
+
return accountStepPath.test(pathname) || !!form && Array.prototype.some.call(formFields(form), function (field) {
|
|
13302
|
+
return field instanceof HTMLInputElement && field.type === 'password';
|
|
13303
|
+
});
|
|
13304
|
+
}
|
|
13305
|
+
// "Buy now", "Purchase" and "Register" count only on a checkout route that isn't a
|
|
13306
|
+
// confirmation page, and never on a plain link, which is navigation.
|
|
13307
|
+
function isCommitLabel(text, isLink, pathname, control) {
|
|
12750
13308
|
if (isLink) {
|
|
12751
13309
|
return linkCommits.test(text);
|
|
12752
13310
|
}
|
|
12753
|
-
return commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path';
|
|
13311
|
+
return commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path' && !(bareRegister.test(text) && createsAccount(control, pathname));
|
|
12754
13312
|
}
|
|
12755
13313
|
// Classifies a clicked or submitting control found by findControl, in order:
|
|
12756
13314
|
// pickers, toggles and pay-later messages never count; a vendor's own wallet
|
|
@@ -12794,7 +13352,7 @@ var mParticle = (function () {
|
|
|
12794
13352
|
}
|
|
12795
13353
|
var isLink = control.matches('a[href]:not([role="button"])');
|
|
12796
13354
|
if (cores.some(function (text) {
|
|
12797
|
-
return isCommitLabel(text, isLink, pathname);
|
|
13355
|
+
return isCommitLabel(text, isLink, pathname, control);
|
|
12798
13356
|
})) {
|
|
12799
13357
|
return {
|
|
12800
13358
|
kind: 'commit',
|
|
@@ -12827,7 +13385,7 @@ var mParticle = (function () {
|
|
|
12827
13385
|
source: 'brand'
|
|
12828
13386
|
};
|
|
12829
13387
|
}
|
|
12830
|
-
if (steps.test(text)) {
|
|
13388
|
+
if (steps.test(text) || isButton && routeGatedSteps.test(text) && checkoutSegmentPath.test(pathname) && !confirmationPath.test(pathname) && !accountStepPath.test(pathname)) {
|
|
12831
13389
|
return {
|
|
12832
13390
|
kind: 'checkout_step',
|
|
12833
13391
|
source: 'label'
|
|
@@ -12915,13 +13473,12 @@ var mParticle = (function () {
|
|
|
12915
13473
|
var SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
|
|
12916
13474
|
// Apple Pay must begin inside the tap's own handler, so the tap is this recent.
|
|
12917
13475
|
var APPLE_PAY_AFTER_CLICK_MS = 1000;
|
|
12918
|
-
|
|
13476
|
+
// How far up from the clicked element a control is looked for. Checkout intent
|
|
13477
|
+
// keeps findControl's own 12; logging looks further, past deep product cards.
|
|
13478
|
+
var PATH_LIMIT = 20;
|
|
12919
13479
|
// A ceiling, not a dedupe: a held Enter key or browser automation fires a
|
|
12920
13480
|
// trusted click per repeat, and each one is forwarded to every kit.
|
|
12921
13481
|
var PAGE_EVENT_LIMIT = 100;
|
|
12922
|
-
// A click uploads at once. Taps that follow it this quickly are held and share
|
|
12923
|
-
// one upload once they stop.
|
|
12924
|
-
var UPLOAD_DEBOUNCE_MS = 100;
|
|
12925
13482
|
var btnWindow = function btnWindow() {
|
|
12926
13483
|
return typeof window === 'undefined' ? null : window;
|
|
12927
13484
|
};
|
|
@@ -13039,10 +13596,6 @@ var mParticle = (function () {
|
|
|
13039
13596
|
this.pageEvents = 0;
|
|
13040
13597
|
// Clicks read at dispatch and waiting for the next paint.
|
|
13041
13598
|
this.pending = [];
|
|
13042
|
-
this.uploadTimerId = null;
|
|
13043
|
-
this.uploadHeld = false;
|
|
13044
|
-
// Set while the page is left, when the SDK's exit upload sends the queue.
|
|
13045
|
-
this.leaving = false;
|
|
13046
13599
|
this.drain = function () {
|
|
13047
13600
|
var queue = _this.pending;
|
|
13048
13601
|
_this.pending = [];
|
|
@@ -13056,14 +13609,8 @@ var mParticle = (function () {
|
|
|
13056
13609
|
// beforeunload, the first of them.
|
|
13057
13610
|
this.onHide = function (event) {
|
|
13058
13611
|
if (event.type === 'beforeunload' || event.type === 'pagehide' || document.visibilityState === 'hidden') {
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
_this.drain();
|
|
13062
|
-
} finally {
|
|
13063
|
-
_this.leaving = false;
|
|
13064
|
-
}
|
|
13065
|
-
// The SDK's exit upload sends the queue, held taps included.
|
|
13066
|
-
_this.cancelUpload();
|
|
13612
|
+
// The SDK's exit upload then sends these clicks with the queue.
|
|
13613
|
+
_this.drain();
|
|
13067
13614
|
}
|
|
13068
13615
|
};
|
|
13069
13616
|
this.onPageShow = function (event) {
|
|
@@ -13190,7 +13737,6 @@ var mParticle = (function () {
|
|
|
13190
13737
|
this.lastSent = {};
|
|
13191
13738
|
this.pagePath = null;
|
|
13192
13739
|
this.pageEvents = 0;
|
|
13193
|
-
this.cancelUpload();
|
|
13194
13740
|
};
|
|
13195
13741
|
ButtonTracker.prototype.enqueue = function (delivery) {
|
|
13196
13742
|
this.pending.push(delivery);
|
|
@@ -13229,22 +13775,25 @@ var mParticle = (function () {
|
|
|
13229
13775
|
var path = isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : [];
|
|
13230
13776
|
var excluded = isExcludedPath(path);
|
|
13231
13777
|
var control = excluded ? undefined : findControl(path, true);
|
|
13232
|
-
|
|
13778
|
+
var logged = control || (excluded ? undefined : findControl(path, true, PATH_LIMIT) || findRoleControl(path, PATH_LIMIT));
|
|
13779
|
+
if (logged) {
|
|
13233
13780
|
// With links alone, only an anchor can be logged.
|
|
13234
|
-
click = logsEvents && (this.logButtons ||
|
|
13781
|
+
click = logsEvents && (this.logButtons || logged.matches('a[href]')) ? describeButtonClick(logged, path) : undefined;
|
|
13235
13782
|
if (click) {
|
|
13236
13783
|
asLink = this.logLinks && isLinkClick(click);
|
|
13237
13784
|
// Without link tracking, a link that leaves the page stays a
|
|
13238
13785
|
// button click, except a plain link to another page of the
|
|
13239
13786
|
// site, which button tracking never logged.
|
|
13240
|
-
var asButton = this.logButtons && !asLink && !(click.linkType === 'internal' &&
|
|
13787
|
+
var asButton = this.logButtons && !asLink && !(click.linkType === 'internal' && logged.matches('a[href]:not([role="button"])'));
|
|
13241
13788
|
if (!asLink && !asButton) {
|
|
13242
13789
|
click = undefined;
|
|
13243
13790
|
}
|
|
13244
13791
|
}
|
|
13245
13792
|
if (click && click.checked !== undefined) {
|
|
13246
|
-
box =
|
|
13793
|
+
box = logged;
|
|
13247
13794
|
}
|
|
13795
|
+
}
|
|
13796
|
+
if (control) {
|
|
13248
13797
|
// Checkout intent is only worked out when the kit can use it.
|
|
13249
13798
|
var intent = signalsKit ? classifyIntent(control, pathname) : undefined;
|
|
13250
13799
|
kind = intent ? intent.kind : undefined;
|
|
@@ -13331,46 +13880,9 @@ var mParticle = (function () {
|
|
|
13331
13880
|
this.pageEvents++;
|
|
13332
13881
|
// Restarts an expired session, as public logEvent() and page views do.
|
|
13333
13882
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
13334
|
-
|
|
13883
|
+
// No upload of its own, which a click that opens another page would
|
|
13884
|
+
// cancel: with batching on, the next batch or the exit upload sends it.
|
|
13335
13885
|
this.mpInstance._Events.logEvent(asLink ? buildLinkClickEvent(click, pathname) : buildButtonClickEvent(click, pathname));
|
|
13336
|
-
// Only a click waiting for the batch interval is sent early: a held
|
|
13337
|
-
// 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) {
|
|
13341
|
-
this.uploadNow();
|
|
13342
|
-
}
|
|
13343
|
-
};
|
|
13344
|
-
// Events waiting in the upload queue for the next batch interval.
|
|
13345
|
-
ButtonTracker.prototype.queuedForUpload = function () {
|
|
13346
|
-
var uploader = this.mpInstance._APIClient.uploader;
|
|
13347
|
-
return uploader ? uploader.eventsQueuedForProcessing.length : 0;
|
|
13348
|
-
};
|
|
13349
|
-
// Uploads a click without waiting for the batch interval. A tap inside the
|
|
13350
|
-
// wait after the last one is held, and each restarts the wait, so a burst
|
|
13351
|
-
// after the first click goes out in one request once the taps stop.
|
|
13352
|
-
ButtonTracker.prototype.uploadNow = function () {
|
|
13353
|
-
var _this = this;
|
|
13354
|
-
if (this.uploadTimerId === null) {
|
|
13355
|
-
this.mpInstance.upload();
|
|
13356
|
-
} else {
|
|
13357
|
-
clearTimeout(this.uploadTimerId);
|
|
13358
|
-
this.uploadHeld = true;
|
|
13359
|
-
}
|
|
13360
|
-
this.uploadTimerId = setTimeout(function () {
|
|
13361
|
-
_this.uploadTimerId = null;
|
|
13362
|
-
if (_this.uploadHeld) {
|
|
13363
|
-
_this.uploadHeld = false;
|
|
13364
|
-
_this.mpInstance.upload();
|
|
13365
|
-
}
|
|
13366
|
-
}, UPLOAD_DEBOUNCE_MS);
|
|
13367
|
-
};
|
|
13368
|
-
ButtonTracker.prototype.cancelUpload = function () {
|
|
13369
|
-
if (this.uploadTimerId !== null) {
|
|
13370
|
-
clearTimeout(this.uploadTimerId);
|
|
13371
|
-
this.uploadTimerId = null;
|
|
13372
|
-
}
|
|
13373
|
-
this.uploadHeld = false;
|
|
13374
13886
|
};
|
|
13375
13887
|
ButtonTracker.prototype.emit = function (kind, source, layer, t, pathname, adds) {
|
|
13376
13888
|
if (this.isKitSuppressed()) {
|