@mparticle/web-sdk 3.24.0 → 3.25.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 +332 -79
- package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +4 -1
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +8 -1
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +6 -0
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +6 -1
- package/dist/types/src/buttonTracking/controls.d.ts +2 -1
- package/dist/types/src/constants.d.ts +1 -0
- package/dist/types/src/cookieConsentManager.d.ts +17 -3
- package/dist/types/src/roktLauncherOptions.d.ts +1 -0
- package/dist/types/src/roktManager.d.ts +10 -1
- package/dist/types/src/store.d.ts +1 -0
- package/package.json +1 -1
- package/src/buttonTracking/buttonClickEvent.ts +61 -18
- package/src/buttonTracking/buttonDescription.ts +104 -11
- package/src/buttonTracking/buttonTracker.ts +112 -15
- package/src/buttonTracking/checkoutIntent.ts +75 -23
- package/src/buttonTracking/controls.ts +10 -6
- package/src/constants.ts +1 -0
- package/src/cookieConsentManager.ts +23 -3
- package/src/cookieSyncManager.ts +11 -3
- package/src/mp-instance.ts +26 -8
- package/src/roktLauncherOptions.ts +1 -0
- package/src/roktManager.ts +47 -7
- package/src/store.ts +3 -0
|
@@ -4,6 +4,9 @@ import { BaseEvent } from '../sdkRuntimeModels';
|
|
|
4
4
|
import { IButtonClick } from './buttonDescription';
|
|
5
5
|
export declare const AUTO_BUTTON_ATTRIBUTE = "is_auto_button_click";
|
|
6
6
|
export declare const BUTTON_CLICK_EVENT_NAME = "Auto Button Click";
|
|
7
|
+
export declare const AUTO_LINK_ATTRIBUTE = "is_auto_link_click";
|
|
8
|
+
export declare const LINK_CLICK_EVENT_NAME = "Auto Link Click";
|
|
7
9
|
export declare const buildButtonClickEvent: (click: IButtonClick, pathname: string) => BaseEvent;
|
|
10
|
+
export declare const buildLinkClickEvent: (click: IButtonClick, pathname: string) => BaseEvent;
|
|
8
11
|
export type ButtonClickBlocker = 'data plan' | 'forwarding rule' | 'event name filter' | 'event type filter';
|
|
9
|
-
export declare const getButtonClickBlocker: (forwarder: IKitFilterSettings, kitBlocker?: KitBlocker) => ButtonClickBlocker | undefined;
|
|
12
|
+
export declare const getButtonClickBlocker: (forwarder: IKitFilterSettings, kitBlocker?: KitBlocker, link?: boolean) => ButtonClickBlocker | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type ButtonElementKind = 'button' | 'input' | 'role' | 'wallet' | 'link';
|
|
2
|
-
export type LinkType = 'same_page' | 'outbound' | 'download';
|
|
2
|
+
export type LinkType = 'same_page' | 'internal' | 'outbound' | 'download';
|
|
3
3
|
export type ButtonLabelSource = 'labelledby' | 'aria' | 'text' | 'alt' | 'title' | 'value' | 'none';
|
|
4
4
|
export interface IButtonClick {
|
|
5
5
|
label?: string;
|
|
@@ -18,4 +18,11 @@ export interface IButtonClick {
|
|
|
18
18
|
fileExtension?: string;
|
|
19
19
|
checked?: boolean;
|
|
20
20
|
}
|
|
21
|
+
export declare function isLinkClick(click: IButtonClick): boolean;
|
|
21
22
|
export declare function describeButtonClick(control: Element, path?: Array<EventTarget | null>): IButtonClick | undefined;
|
|
23
|
+
export interface IPaymentChoice {
|
|
24
|
+
radio: Element;
|
|
25
|
+
names: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function describePaymentChoice(control: Element, path?: Array<EventTarget | null>): IPaymentChoice | undefined;
|
|
28
|
+
export declare function isPicked(radio: Element): boolean;
|
|
@@ -2,6 +2,8 @@ import { IMParticleWebSDKInstance } from '../mp-instance';
|
|
|
2
2
|
export declare const WIN_BTN_KEY = "__mpBtn__";
|
|
3
3
|
export interface IButtonTrackerOptions {
|
|
4
4
|
hasRoktKit: boolean;
|
|
5
|
+
logButtons: boolean;
|
|
6
|
+
logLinks: boolean;
|
|
5
7
|
}
|
|
6
8
|
export declare const getActiveButtonTracker: () => ButtonTracker | undefined;
|
|
7
9
|
export declare const resetButtonTracking: () => void;
|
|
@@ -9,6 +11,8 @@ export declare class ButtonTracker {
|
|
|
9
11
|
private readonly mpInstance;
|
|
10
12
|
private active;
|
|
11
13
|
private hasRoktKit;
|
|
14
|
+
private logButtons;
|
|
15
|
+
private logLinks;
|
|
12
16
|
private generation;
|
|
13
17
|
private lastCommitClick;
|
|
14
18
|
private lastClick;
|
|
@@ -37,7 +41,9 @@ export declare class ButtonTracker {
|
|
|
37
41
|
private uploadNow;
|
|
38
42
|
private cancelUpload;
|
|
39
43
|
private emit;
|
|
44
|
+
private pickPayment;
|
|
40
45
|
private eventsEnabled;
|
|
46
|
+
private hasTargetingOptOut;
|
|
41
47
|
private isKitSuppressed;
|
|
42
48
|
private log;
|
|
43
49
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type CheckoutIntentKind = 'commit' | 'wallet' | 'checkout_step' | 'frame' | 'cart';
|
|
2
|
+
export type PaymentFrameFamily = 'apple_pay' | 'google_pay' | 'amazon_pay' | 'shop_pay' | 'card';
|
|
2
3
|
export type CheckoutIntentSource = 'vendor' | 'label' | 'brand' | 'frame' | 'submit' | 'name' | 'api';
|
|
3
4
|
export interface ICheckoutIntent {
|
|
4
5
|
kind: Exclude<CheckoutIntentKind, 'frame'>;
|
|
@@ -6,4 +7,8 @@ export interface ICheckoutIntent {
|
|
|
6
7
|
}
|
|
7
8
|
export declare function classifyIntent(control: Element, pathname: string): ICheckoutIntent | undefined;
|
|
8
9
|
export declare function classifyIntentControl(control: Element, pathname: string): Exclude<CheckoutIntentKind, 'frame'> | undefined;
|
|
9
|
-
export
|
|
10
|
+
export interface IPaymentFrame {
|
|
11
|
+
kind: 'wallet' | 'frame';
|
|
12
|
+
family?: PaymentFrameFamily;
|
|
13
|
+
}
|
|
14
|
+
export declare function classifyPaymentFrame(target: EventTarget | null, win: Window): IPaymentFrame | undefined;
|
|
@@ -3,10 +3,11 @@ export declare function someComposedAncestor(element: Element, test: (node: Elem
|
|
|
3
3
|
export declare function hasComposedAncestor(element: Element, selector: string): boolean;
|
|
4
4
|
export declare function isRoktElement(element: Element): boolean;
|
|
5
5
|
export declare function vendorButtonKind(control: Element): 'wallet' | 'checkout_step' | 'none' | undefined;
|
|
6
|
+
export declare function nameWords(value: string | null): string;
|
|
6
7
|
export declare function isWalletNamed(element: Element): boolean;
|
|
7
8
|
export declare function isWalletElement(element: Element): boolean;
|
|
8
9
|
export declare function isQuietIntentControl(control: Element, isVendorWallet: boolean, isCart?: boolean): boolean;
|
|
9
10
|
export declare function isWalletNamedControl(element: Element): boolean;
|
|
10
11
|
export declare function isExcludedPath(path: Array<EventTarget | null>): boolean;
|
|
11
12
|
export declare function findControl(path: Array<EventTarget | null>, included?: boolean): Element | undefined;
|
|
12
|
-
export declare function isUnsafeTextSource(element: Element): boolean;
|
|
13
|
+
export declare function isUnsafeTextSource(element: Element, own?: Element): boolean;
|
|
@@ -163,6 +163,7 @@ declare const Constants: {
|
|
|
163
163
|
readonly AstBackgroundEvents: "astBackgroundEvents";
|
|
164
164
|
readonly AutoLogPageView: "autoLogPageView";
|
|
165
165
|
readonly AutoButtonTracking: "autoButtonTracking";
|
|
166
|
+
readonly AutoLinkTracking: "autoLinkTracking";
|
|
166
167
|
};
|
|
167
168
|
readonly DefaultInstance: "default_instance";
|
|
168
169
|
readonly CCPAPurpose: "data_sale_opt_out";
|
|
@@ -15,6 +15,11 @@ export interface ICookieConsentFlags {
|
|
|
15
15
|
* Default: false (targeting is allowed)
|
|
16
16
|
*/
|
|
17
17
|
noTargeting: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* When true, indicates the user has opted out of the sale or sharing of their data.
|
|
20
|
+
* Default: false (sharing is allowed)
|
|
21
|
+
*/
|
|
22
|
+
doNotShareOrSell?: boolean;
|
|
18
23
|
}
|
|
19
24
|
export interface ICookieConsentManager {
|
|
20
25
|
/**
|
|
@@ -25,6 +30,10 @@ export interface ICookieConsentManager {
|
|
|
25
30
|
* Functional tracking is allowed when noFunctional is false (default)
|
|
26
31
|
*/
|
|
27
32
|
getNoFunctional: () => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Sharing is allowed when doNotShareOrSell is false (default)
|
|
35
|
+
*/
|
|
36
|
+
getDoNotShareOrSell: () => boolean;
|
|
28
37
|
/**
|
|
29
38
|
* Syncs the $NoTargeting user attribute based on the current noTargeting flag.
|
|
30
39
|
* Sets the attribute when noTargeting is true, removes it when false.
|
|
@@ -32,12 +41,12 @@ export interface ICookieConsentManager {
|
|
|
32
41
|
syncNoTargetingAttribute: (user: IMParticleUser | null) => void;
|
|
33
42
|
}
|
|
34
43
|
/**
|
|
35
|
-
* CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting
|
|
36
|
-
* that are passed via launcherOptions during SDK initialization.
|
|
44
|
+
* CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting,
|
|
45
|
+
* doNotShareOrSell) that are passed via launcherOptions during SDK initialization.
|
|
37
46
|
*
|
|
38
47
|
* These flags allow Rokt integration to respect user privacy choices.
|
|
39
48
|
*
|
|
40
|
-
* Default behavior:
|
|
49
|
+
* Default behavior: All flags default to false, meaning all features are allowed
|
|
41
50
|
* unless explicitly opted out by the user.
|
|
42
51
|
*/
|
|
43
52
|
export default class CookieConsentManager implements ICookieConsentManager {
|
|
@@ -53,5 +62,10 @@ export default class CookieConsentManager implements ICookieConsentManager {
|
|
|
53
62
|
* Functional tracking is allowed when noFunctional is false (default).
|
|
54
63
|
*/
|
|
55
64
|
getNoFunctional(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Returns true if the user has opted out of the sale or sharing of their data.
|
|
67
|
+
* Sharing is allowed when doNotShareOrSell is false (default).
|
|
68
|
+
*/
|
|
69
|
+
getDoNotShareOrSell(): boolean;
|
|
56
70
|
syncNoTargetingAttribute(user: IMParticleUser | null): void;
|
|
57
71
|
}
|
|
@@ -9,7 +9,7 @@ import { MPID } from "@mparticle/web-sdk";
|
|
|
9
9
|
import { IErrorReportingService, ILoggingService } from "./reporting/types";
|
|
10
10
|
import { IRoktLauncherOptions } from "./roktLauncherOptions";
|
|
11
11
|
import IntegrationCapture from "./integrationCapture";
|
|
12
|
-
import type { CheckoutIntentKind, CheckoutIntentSource } from "./buttonTracking/checkoutIntent";
|
|
12
|
+
import type { CheckoutIntentKind, CheckoutIntentSource, PaymentFrameFamily } from "./buttonTracking/checkoutIntent";
|
|
13
13
|
export type RoktAttributeValueArray = Array<string | number | boolean>;
|
|
14
14
|
export type RoktAttributeValueType = string | number | boolean | undefined | null;
|
|
15
15
|
export type RoktAttributeValue = RoktAttributeValueType | RoktAttributeValueArray;
|
|
@@ -50,6 +50,12 @@ export interface ICheckoutIntentSignal {
|
|
|
50
50
|
source?: CheckoutIntentSource;
|
|
51
51
|
pathname?: string;
|
|
52
52
|
}
|
|
53
|
+
export interface IPaymentPickSignal {
|
|
54
|
+
t: number;
|
|
55
|
+
pathname: string;
|
|
56
|
+
names?: readonly string[];
|
|
57
|
+
family?: PaymentFrameFamily;
|
|
58
|
+
}
|
|
53
59
|
export interface IRoktKitSettings {
|
|
54
60
|
accountId?: string;
|
|
55
61
|
placementAttributesMapping?: string;
|
|
@@ -68,6 +74,7 @@ export interface IRoktKit {
|
|
|
68
74
|
terminate?: () => Promise<void>;
|
|
69
75
|
onRouteChange?: () => void;
|
|
70
76
|
onCheckoutIntent?: (signal: ICheckoutIntentSignal) => void;
|
|
77
|
+
onPaymentPick?: (signal: IPaymentPickSignal) => void;
|
|
71
78
|
launcherOptions?: Dictionary<any>;
|
|
72
79
|
settings?: IRoktKitSettings;
|
|
73
80
|
integrationName?: string;
|
|
@@ -129,10 +136,12 @@ export default class RoktManager {
|
|
|
129
136
|
private watchRouteChanges;
|
|
130
137
|
private notifyRouteChange;
|
|
131
138
|
setAutoButtonClicksForwarded(forwarded: boolean): void;
|
|
139
|
+
hasTargetingOptOut(): boolean;
|
|
132
140
|
isCheckoutIntentSuppressed(): boolean;
|
|
133
141
|
isCheckoutIntentLive(): boolean;
|
|
134
142
|
notifyCheckoutIntent(signal: ICheckoutIntentSignal, pathname?: string): void;
|
|
135
143
|
discardCheckoutIntents(): void;
|
|
144
|
+
notifyPaymentPick(signal: IPaymentPickSignal): void;
|
|
136
145
|
private flushCheckoutIntents;
|
|
137
146
|
isAutoLogPageViewEnabled(): boolean;
|
|
138
147
|
/**
|
package/package.json
CHANGED
|
@@ -12,22 +12,25 @@ export const AUTO_BUTTON_ATTRIBUTE = 'is_auto_button_click';
|
|
|
12
12
|
// Specific enough not to merge with a customer's own 'Button Click' event.
|
|
13
13
|
export const BUTTON_CLICK_EVENT_NAME = 'Auto Button Click';
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const
|
|
15
|
+
// Marks an automatically logged click on a link that leaves the page.
|
|
16
|
+
export const AUTO_LINK_ATTRIBUTE = 'is_auto_link_click';
|
|
17
|
+
|
|
18
|
+
export const LINK_CLICK_EVENT_NAME = 'Auto Link Click';
|
|
19
|
+
|
|
20
|
+
// The attributes both click events carry, with the page fields that auto page
|
|
21
|
+
// views carry. No intent: classifying clicks is left to whoever reads the data.
|
|
22
|
+
const clickAttributes = (
|
|
19
23
|
click: IButtonClick,
|
|
20
24
|
pathname: string
|
|
21
|
-
):
|
|
25
|
+
): { [key: string]: string | boolean } => {
|
|
22
26
|
const data: { [key: string]: string | boolean } = {
|
|
23
27
|
label_source: click.labelSource,
|
|
24
28
|
element: click.element,
|
|
25
29
|
type: click.type,
|
|
26
30
|
path: pathname,
|
|
27
31
|
hostname: window.location.hostname,
|
|
28
|
-
[AUTO_BUTTON_ATTRIBUTE]: true,
|
|
29
32
|
};
|
|
30
|
-
// Optional fields are left out when the
|
|
33
|
+
// Optional fields are left out when the control has no safe value for them.
|
|
31
34
|
const optional: { [key: string]: string | undefined } = {
|
|
32
35
|
label: click.label,
|
|
33
36
|
aria_label: click.ariaLabel,
|
|
@@ -49,6 +52,16 @@ export const buildButtonClickEvent = (
|
|
|
49
52
|
if (click.checked !== undefined) {
|
|
50
53
|
data.checked = click.checked;
|
|
51
54
|
}
|
|
55
|
+
return data;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// One custom event per button click, logged like logLink() with the auto marker.
|
|
59
|
+
export const buildButtonClickEvent = (
|
|
60
|
+
click: IButtonClick,
|
|
61
|
+
pathname: string
|
|
62
|
+
): BaseEvent => {
|
|
63
|
+
const data = clickAttributes(click, pathname);
|
|
64
|
+
data[AUTO_BUTTON_ATTRIBUTE] = true;
|
|
52
65
|
return {
|
|
53
66
|
messageType: MessageType.PageEvent,
|
|
54
67
|
name: BUTTON_CLICK_EVENT_NAME,
|
|
@@ -57,6 +70,34 @@ export const buildButtonClickEvent = (
|
|
|
57
70
|
};
|
|
58
71
|
};
|
|
59
72
|
|
|
73
|
+
// One custom event per click on a link that leaves the page. Typed Other, not
|
|
74
|
+
// Navigation, so no kit reads it as a page change. A plain link to another page
|
|
75
|
+
// of the site goes without its text, which can name the shopper ("Hi, Jane");
|
|
76
|
+
// a button or wallet acting as a link keeps its name.
|
|
77
|
+
export const buildLinkClickEvent = (
|
|
78
|
+
click: IButtonClick,
|
|
79
|
+
pathname: string
|
|
80
|
+
): BaseEvent => {
|
|
81
|
+
const data = clickAttributes(
|
|
82
|
+
click.linkType === 'internal' && click.element === 'link'
|
|
83
|
+
? {
|
|
84
|
+
...click,
|
|
85
|
+
label: undefined,
|
|
86
|
+
labelSource: 'none',
|
|
87
|
+
ariaLabel: undefined,
|
|
88
|
+
}
|
|
89
|
+
: click,
|
|
90
|
+
pathname
|
|
91
|
+
);
|
|
92
|
+
data[AUTO_LINK_ATTRIBUTE] = true;
|
|
93
|
+
return {
|
|
94
|
+
messageType: MessageType.PageEvent,
|
|
95
|
+
name: LINK_CLICK_EVENT_NAME,
|
|
96
|
+
data,
|
|
97
|
+
eventType: EventType.Other,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
60
101
|
// Every attribute buildButtonClickEvent can set apart from the marker and the
|
|
61
102
|
// hostname, which is the same on every click on this page. A spec checks the
|
|
62
103
|
// two stay in step.
|
|
@@ -85,14 +126,15 @@ export type ButtonClickBlocker =
|
|
|
85
126
|
| 'event name filter'
|
|
86
127
|
| 'event type filter';
|
|
87
128
|
|
|
88
|
-
// Which check keeps some or all auto button clicks
|
|
89
|
-
// when the kit receives every one. A forwarding
|
|
90
|
-
// value differs between clicks lets some through and
|
|
91
|
-
// check goes through isEventForwarded on its own, so
|
|
92
|
-
// it does for each click.
|
|
129
|
+
// Which check keeps some or all auto button clicks, or auto link clicks with
|
|
130
|
+
// `link`, from a kit, or undefined when the kit receives every one. A forwarding
|
|
131
|
+
// rule on an attribute whose value differs between clicks lets some through and
|
|
132
|
+
// not others. Each other check goes through isEventForwarded on its own, so
|
|
133
|
+
// together they decide as it does for each click.
|
|
93
134
|
export const getButtonClickBlocker = (
|
|
94
135
|
forwarder: IKitFilterSettings,
|
|
95
|
-
kitBlocker?: KitBlocker
|
|
136
|
+
kitBlocker?: KitBlocker,
|
|
137
|
+
link = false
|
|
96
138
|
): ButtonClickBlocker | undefined => {
|
|
97
139
|
const rule = forwarder.filteringEventAttributeValue;
|
|
98
140
|
if (
|
|
@@ -106,14 +148,15 @@ export const getButtonClickBlocker = (
|
|
|
106
148
|
) {
|
|
107
149
|
return 'forwarding rule';
|
|
108
150
|
}
|
|
151
|
+
const attributes: { [key: string]: string | boolean } = {
|
|
152
|
+
hostname: window.location.hostname,
|
|
153
|
+
};
|
|
154
|
+
attributes[link ? AUTO_LINK_ATTRIBUTE : AUTO_BUTTON_ATTRIBUTE] = true;
|
|
109
155
|
const click = ({
|
|
110
|
-
EventName: BUTTON_CLICK_EVENT_NAME,
|
|
156
|
+
EventName: link ? LINK_CLICK_EVENT_NAME : BUTTON_CLICK_EVENT_NAME,
|
|
111
157
|
EventCategory: EventType.Other,
|
|
112
158
|
EventDataType: MessageType.PageEvent,
|
|
113
|
-
EventAttributes:
|
|
114
|
-
[AUTO_BUTTON_ATTRIBUTE]: true,
|
|
115
|
-
hostname: window.location.hostname,
|
|
116
|
-
},
|
|
159
|
+
EventAttributes: attributes,
|
|
117
160
|
} as unknown) as SDKEvent;
|
|
118
161
|
const checks: Array<[ButtonClickBlocker, Partial<IKitFilterSettings>]> = [
|
|
119
162
|
['data plan', {}],
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
hasComposedAncestor,
|
|
4
4
|
isUnsafeTextSource,
|
|
5
5
|
isWalletElement,
|
|
6
|
+
nameWords,
|
|
6
7
|
someComposedAncestor,
|
|
7
8
|
} from './controls';
|
|
8
9
|
import { maskLabel, MAX_RAW_LABEL_LENGTH } from './labelMasking';
|
|
@@ -11,8 +12,9 @@ import { maskLabel, MAX_RAW_LABEL_LENGTH } from './labelMasking';
|
|
|
11
12
|
// hand-written identifiers and kind. Never reads form field values.
|
|
12
13
|
export type ButtonElementKind = 'button' | 'input' | 'role' | 'wallet' | 'link';
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
|
|
15
|
+
// A same_page link acts as a button. The others leave the page: to another page
|
|
16
|
+
// of the site, another site or a file.
|
|
17
|
+
export type LinkType = 'same_page' | 'internal' | 'outbound' | 'download';
|
|
16
18
|
|
|
17
19
|
const DOWNLOAD_EXTENSIONS = /\.(pdf|zip|csv|txt|ics|docx?|xlsx?|pptx?|dmg|exe|apk|mp3|mp4)$/i;
|
|
18
20
|
|
|
@@ -392,8 +394,8 @@ interface ILinkDetails {
|
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
// Reads the link's own resolved URL parts, never its full href: only the hostname of
|
|
395
|
-
// a link that leaves the site and the extension of a file it downloads travel.
|
|
396
|
-
//
|
|
397
|
+
// a link that leaves the site and the extension of a file it downloads travel.
|
|
398
|
+
// mailto:, tel: and other schemes give undefined.
|
|
397
399
|
function describeLink(link: Element): ILinkDetails | undefined {
|
|
398
400
|
const href = (link.getAttribute('href') || '').trim();
|
|
399
401
|
if (!href || href.charAt(0) === '#') {
|
|
@@ -427,7 +429,13 @@ function describeLink(link: Element): ILinkDetails | undefined {
|
|
|
427
429
|
return link.pathname === window.location.pathname &&
|
|
428
430
|
link.search === window.location.search
|
|
429
431
|
? { type: 'same_page' }
|
|
430
|
-
:
|
|
432
|
+
: { type: 'internal' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Whether a described click is on a link that leaves the page, logged as a link
|
|
436
|
+
// click rather than a button click.
|
|
437
|
+
export function isLinkClick(click: IButtonClick): boolean {
|
|
438
|
+
return !!click.linkType && click.linkType !== 'same_page';
|
|
431
439
|
}
|
|
432
440
|
|
|
433
441
|
// Checkboxes and radios, and the roles that act as them. A role can list
|
|
@@ -442,6 +450,12 @@ function boxOf(element: Element): Element | null {
|
|
|
442
450
|
return box && box.matches(CHOICE) ? box : null;
|
|
443
451
|
}
|
|
444
452
|
|
|
453
|
+
function isRadio(choice: Element | null): boolean {
|
|
454
|
+
return choice instanceof HTMLInputElement
|
|
455
|
+
? choice.type === 'radio'
|
|
456
|
+
: !!choice && choice.matches(RADIO_ROLE);
|
|
457
|
+
}
|
|
458
|
+
|
|
445
459
|
// The box a click on this control picks. One the click passed through below
|
|
446
460
|
// the control comes first: querySelector can't reach a slotted box, a box in a
|
|
447
461
|
// shadow root or the box of a label inside the control, and a row can hold
|
|
@@ -463,8 +477,9 @@ function choiceOf(
|
|
|
463
477
|
return boxOf(control) || control.querySelector(CHOICE);
|
|
464
478
|
}
|
|
465
479
|
|
|
466
|
-
// Describes a clicked control for auto button tracking. A link
|
|
467
|
-
//
|
|
480
|
+
// Describes a clicked control for auto button and link tracking. A link carries its
|
|
481
|
+
// link type, and one to another scheme is skipped. A role="button" anchor carries one
|
|
482
|
+
// only when it leaves the page; otherwise it stays a plain button.
|
|
468
483
|
export function describeButtonClick(
|
|
469
484
|
control: Element,
|
|
470
485
|
path: Array<EventTarget | null> = []
|
|
@@ -475,6 +490,11 @@ export function describeButtonClick(
|
|
|
475
490
|
if (!link) {
|
|
476
491
|
return undefined;
|
|
477
492
|
}
|
|
493
|
+
} else if (control.matches('a[href]')) {
|
|
494
|
+
link = describeLink(control);
|
|
495
|
+
if (link && link.type === 'same_page') {
|
|
496
|
+
link = undefined;
|
|
497
|
+
}
|
|
478
498
|
}
|
|
479
499
|
const element = elementKind(control);
|
|
480
500
|
const box =
|
|
@@ -492,10 +512,7 @@ export function describeButtonClick(
|
|
|
492
512
|
// radio's id, test id and the panel it opens name the option picked, so
|
|
493
513
|
// only the name of its input, the question, goes with it. A wrapper's own
|
|
494
514
|
// name, or a role radio's, can name the option instead.
|
|
495
|
-
const radio =
|
|
496
|
-
choice instanceof HTMLInputElement
|
|
497
|
-
? choice.type === 'radio'
|
|
498
|
-
: !!choice && choice.matches(RADIO_ROLE);
|
|
515
|
+
const radio = isRadio(choice);
|
|
499
516
|
const named = !radio
|
|
500
517
|
? control
|
|
501
518
|
: choice instanceof HTMLInputElement
|
|
@@ -531,3 +548,79 @@ export function describeButtonClick(
|
|
|
531
548
|
checked: box ? box.checked : undefined,
|
|
532
549
|
};
|
|
533
550
|
}
|
|
551
|
+
|
|
552
|
+
// A question about payment, read from the names its author gave it, so
|
|
553
|
+
// "checkout[payment_gateway]", "paymentMethod" and "tender_type" all count and
|
|
554
|
+
// "payee" does not. A radio's input name is its question; its group or
|
|
555
|
+
// fieldset speaks only for a radio with no name, as a role radio usually is.
|
|
556
|
+
const PAYMENT_QUESTION = /(?:^| )(?:pay|payment\w*|tenders?|gateways?)(?: |$)/;
|
|
557
|
+
const QUESTION_GROUP = '[role="radiogroup"], fieldset';
|
|
558
|
+
|
|
559
|
+
function isPaymentQuestion(radio: Element): boolean {
|
|
560
|
+
const name = radio.getAttribute('name');
|
|
561
|
+
const group = name ? null : radio.closest(QUESTION_GROUP);
|
|
562
|
+
const question = group
|
|
563
|
+
? [
|
|
564
|
+
group.getAttribute('id'),
|
|
565
|
+
group.getAttribute('name'),
|
|
566
|
+
group.getAttribute('aria-label'),
|
|
567
|
+
]
|
|
568
|
+
: [name];
|
|
569
|
+
return question.some(words => PAYMENT_QUESTION.test(nameWords(words)));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// What names a payment option: the radio's accessible name, the text and
|
|
573
|
+
// image of each label, and its hand-written ids, text masked like a label.
|
|
574
|
+
function optionNames(radio: Element): string[] {
|
|
575
|
+
const names: Array<string | undefined> = [
|
|
576
|
+
maskLabel(accessibleName(radio).raw),
|
|
577
|
+
];
|
|
578
|
+
const labels =
|
|
579
|
+
radio instanceof HTMLInputElement && radio.labels
|
|
580
|
+
? Array.from(radio.labels).slice(0, 2)
|
|
581
|
+
: [];
|
|
582
|
+
labels.forEach(label => {
|
|
583
|
+
if (isShown(label) && !isUnsafeTextSource(label, radio)) {
|
|
584
|
+
names.push(maskLabel(renderedText(label)));
|
|
585
|
+
names.push(maskLabel(shownImageAlt(label)));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
names.push(
|
|
589
|
+
stableIdentifier(radio.getAttribute('id')),
|
|
590
|
+
firstIdentifier(radio, TEST_ID_ATTRIBUTES)
|
|
591
|
+
);
|
|
592
|
+
return names.filter(
|
|
593
|
+
(name, index) => !!name && names.indexOf(name) === index
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// The payment radio a click picks and the names of its option, for the Rokt
|
|
598
|
+
// kit alone to read a payment method from. A radio's option is the shopper's
|
|
599
|
+
// answer, so these names never go into the logged click.
|
|
600
|
+
export interface IPaymentChoice {
|
|
601
|
+
radio: Element;
|
|
602
|
+
names: string[];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function describePaymentChoice(
|
|
606
|
+
control: Element,
|
|
607
|
+
path: Array<EventTarget | null> = []
|
|
608
|
+
): IPaymentChoice | undefined {
|
|
609
|
+
const choice = choiceOf(control, path);
|
|
610
|
+
if (
|
|
611
|
+
!isRadio(choice) ||
|
|
612
|
+
hasComposedAncestor(choice, '[data-mp-no-track]') ||
|
|
613
|
+
!isPaymentQuestion(choice)
|
|
614
|
+
) {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
const names = optionNames(choice);
|
|
618
|
+
return names.length ? { radio: choice, names } : undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Read once the page has handled the click, which may have cancelled it.
|
|
622
|
+
export function isPicked(radio: Element): boolean {
|
|
623
|
+
return radio instanceof HTMLInputElement
|
|
624
|
+
? radio.checked
|
|
625
|
+
: radio.getAttribute('aria-checked') === 'true';
|
|
626
|
+
}
|