@mparticle/web-sdk 3.23.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 -1688
- package/dist/mparticle.esm.js +1 -1688
- package/dist/mparticle.js +1015 -253
- package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +7 -0
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +10 -3
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +8 -0
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +8 -3
- package/dist/types/src/buttonTracking/controls.d.ts +8 -4
- package/dist/types/src/buttonTracking/labelMasking.d.ts +1 -0
- package/dist/types/src/configAPIClient.d.ts +1 -0
- package/dist/types/src/constants.d.ts +1 -0
- package/dist/types/src/cookieConsentManager.d.ts +17 -3
- package/dist/types/src/forwarder-utils.d.ts +3 -0
- package/dist/types/src/pageViewTracker.d.ts +19 -3
- package/dist/types/src/roktLauncherOptions.d.ts +1 -0
- package/dist/types/src/roktManager.d.ts +13 -2
- package/dist/types/src/sdkRuntimeModels.d.ts +1 -0
- package/dist/types/src/store.d.ts +3 -0
- package/package.json +21 -14
- package/src/buttonTracking/buttonClickEvent.ts +126 -8
- package/src/buttonTracking/buttonDescription.ts +336 -53
- package/src/buttonTracking/buttonTracker.ts +231 -19
- package/src/buttonTracking/checkoutIntent.ts +117 -23
- package/src/buttonTracking/controls.ts +169 -55
- package/src/buttonTracking/labelMasking.ts +4 -1
- package/src/configAPIClient.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/cookieConsentManager.ts +23 -3
- package/src/cookieSyncManager.ts +11 -3
- package/src/events.ts +9 -3
- package/src/forwarder-utils.ts +38 -0
- package/src/forwarders.ts +2 -28
- package/src/mp-instance.ts +41 -7
- package/src/pageUrl.ts +43 -2
- package/src/pageViewTracker.ts +193 -111
- package/src/roktLauncherOptions.ts +1 -0
- package/src/roktManager.ts +66 -11
- package/src/sdkRuntimeModels.ts +1 -0
- package/src/store.ts +17 -0
|
@@ -6,29 +6,46 @@ import { isFunction } from '../utils';
|
|
|
6
6
|
export const CONTROL_SELECTOR =
|
|
7
7
|
'button, input[type="submit"], input[type="button"], input[type="image"], [role="button"]';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const EDITABLE = '[contenteditable]:not([contenteditable="false"])';
|
|
10
|
+
|
|
11
|
+
// One step up the composed tree: the slot, the parent, then the shadow host.
|
|
12
|
+
function composedParent(node: Element): Element | null {
|
|
13
|
+
const root = isFunction(node.getRootNode)
|
|
14
|
+
? (node.getRootNode() as ShadowRoot)
|
|
15
|
+
: null;
|
|
16
|
+
return (
|
|
17
|
+
(node as Element & { assignedSlot?: Element | null }).assignedSlot ||
|
|
18
|
+
node.parentElement ||
|
|
19
|
+
(root && root.host) ||
|
|
20
|
+
null
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Tests the element and its ancestors over the composed tree: slots, parents,
|
|
25
|
+
// then shadow hosts. With `until`, stops before reaching that ancestor.
|
|
26
|
+
export function someComposedAncestor(
|
|
11
27
|
element: Element,
|
|
12
|
-
|
|
28
|
+
test: (node: Element) => boolean,
|
|
29
|
+
until?: Element
|
|
13
30
|
): boolean {
|
|
14
31
|
let node: Element | null = element;
|
|
15
|
-
while (node) {
|
|
16
|
-
if (node
|
|
32
|
+
while (node && node !== until) {
|
|
33
|
+
if (test(node)) {
|
|
17
34
|
return true;
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
? (node.getRootNode() as ShadowRoot)
|
|
21
|
-
: null;
|
|
22
|
-
node =
|
|
23
|
-
(node as Element & { assignedSlot?: Element | null })
|
|
24
|
-
.assignedSlot ||
|
|
25
|
-
node.parentElement ||
|
|
26
|
-
(root && root.host) ||
|
|
27
|
-
null;
|
|
36
|
+
node = composedParent(node);
|
|
28
37
|
}
|
|
29
38
|
return false;
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
// closest() over the composed tree, starting at the element itself.
|
|
42
|
+
export function hasComposedAncestor(
|
|
43
|
+
element: Element,
|
|
44
|
+
selector: string
|
|
45
|
+
): boolean {
|
|
46
|
+
return someComposedAncestor(element, node => node.matches(selector));
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
const ROKT_SELECTOR = '[data-core-type], #rokt-controller-frame';
|
|
33
50
|
|
|
34
51
|
// A Rokt frame or script is served from a /wsdk/ path.
|
|
@@ -110,8 +127,44 @@ export function vendorButtonKind(
|
|
|
110
127
|
return host.matches(CARD_FORM_BUTTON) ? 'checkout_step' : 'wallet';
|
|
111
128
|
}
|
|
112
129
|
|
|
130
|
+
// A merchant-built wallet button that names its wallet in an id or test id, such
|
|
131
|
+
// as `venmo-pay-btn` or `applePayButton`. The name must also say button and not
|
|
132
|
+
// name a part or a wrapper of one, so a wallet's logo, label or container never
|
|
133
|
+
// counts, nor a button that only chooses a wallet. Affirm is also an English
|
|
134
|
+
// verb, so it counts only beside a payment word. Only the element's own
|
|
135
|
+
// attributes are read.
|
|
136
|
+
const WALLET_IN_NAME = /(?:^| )(?:apple ?pay|google ?pay|g ?pay|amazon ?pay|cash ?app|venmo|pay ?pal|shop ?pay|klarna|after ?pay|clear ?pay|sezzle|samsung ?pay)(?= |$)/;
|
|
137
|
+
const AFFIRM_IN_NAME = /(?:^| )affirm(?= |$)/;
|
|
138
|
+
const PAYMENT_IN_NAME = /(?:^| )(?:pay|payment|checkout)(?= |$)/;
|
|
139
|
+
const BUTTON_IN_NAME = /(?:^| )(?:btn|button)(?= |$)/;
|
|
140
|
+
const PART_IN_NAME = /(?:^| )(?:logo|icon|image|img|label|text|caption|message|messaging|info|tooltip|help|learn|terms|promo|banner|container|wrapper|wrap|placeholder|form|section|panel|modal|list|group|select|option|tab|toggle|radio|choice|method)(?= |$)/;
|
|
141
|
+
|
|
142
|
+
export function nameWords(value: string | null): string {
|
|
143
|
+
return (value || '')
|
|
144
|
+
.slice(0, 64)
|
|
145
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
146
|
+
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
147
|
+
.trim()
|
|
148
|
+
.toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isWalletNamed(element: Element): boolean {
|
|
152
|
+
return [element.getAttribute('id'), element.getAttribute('data-testid')]
|
|
153
|
+
.map(nameWords)
|
|
154
|
+
.some(
|
|
155
|
+
words =>
|
|
156
|
+
(WALLET_IN_NAME.test(words) ||
|
|
157
|
+
(AFFIRM_IN_NAME.test(words) &&
|
|
158
|
+
PAYMENT_IN_NAME.test(words))) &&
|
|
159
|
+
BUTTON_IN_NAME.test(words) &&
|
|
160
|
+
!PART_IN_NAME.test(words)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
113
164
|
export function isWalletElement(element: Element): boolean {
|
|
114
|
-
return
|
|
165
|
+
return (
|
|
166
|
+
vendorButtonKind(element) === 'wallet' || isWalletNamedControl(element)
|
|
167
|
+
);
|
|
115
168
|
}
|
|
116
169
|
|
|
117
170
|
// Pickers and toggles choose a payment method or open a panel; they never pay.
|
|
@@ -125,9 +178,11 @@ const MESSAGING =
|
|
|
125
178
|
'.affirm-product-modal, [data-pp-message]';
|
|
126
179
|
|
|
127
180
|
// A control that is still logged as a click but never counts as checkout intent.
|
|
181
|
+
// A cart button can open a flyout, so aria-expanded alone never quiets one.
|
|
128
182
|
export function isQuietIntentControl(
|
|
129
183
|
control: Element,
|
|
130
|
-
isVendorWallet: boolean
|
|
184
|
+
isVendorWallet: boolean,
|
|
185
|
+
isCart = false
|
|
131
186
|
): boolean {
|
|
132
187
|
if (control.matches(PICKER)) {
|
|
133
188
|
return true;
|
|
@@ -137,7 +192,7 @@ export function isQuietIntentControl(
|
|
|
137
192
|
}
|
|
138
193
|
if (
|
|
139
194
|
!isVendorWallet &&
|
|
140
|
-
(control.hasAttribute('aria-expanded') ||
|
|
195
|
+
((!isCart && control.hasAttribute('aria-expanded')) ||
|
|
141
196
|
control.hasAttribute('aria-pressed'))
|
|
142
197
|
) {
|
|
143
198
|
return true;
|
|
@@ -146,15 +201,58 @@ export function isQuietIntentControl(
|
|
|
146
201
|
}
|
|
147
202
|
|
|
148
203
|
// A checkbox or radio is a control of its own only when no button or link
|
|
149
|
-
// around it takes the click, so a tappable row
|
|
204
|
+
// around it takes the click, so a tappable row stays the control.
|
|
150
205
|
const OPTION_INPUT = 'input[type="checkbox"], input[type="radio"]';
|
|
206
|
+
// Anything that chooses, toggles or takes typing. One between a click's target
|
|
207
|
+
// and a wallet-named element, or inside that element, keeps it from becoming
|
|
208
|
+
// the tap. Hidden inputs and button-like inputs are not fields.
|
|
209
|
+
const NAME_BARRIER =
|
|
210
|
+
PICKER +
|
|
211
|
+
', ' +
|
|
212
|
+
PICKER_GROUP +
|
|
213
|
+
', input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), ' +
|
|
214
|
+
'select, textarea, option, optgroup, datalist, [aria-expanded], [aria-pressed], ' +
|
|
215
|
+
EDITABLE;
|
|
216
|
+
// What owns a click on anything inside it: a picker, a real control or link,
|
|
217
|
+
// a toggle, or an editable region.
|
|
218
|
+
const NAME_HOLDER =
|
|
219
|
+
PICKER +
|
|
220
|
+
', ' +
|
|
221
|
+
CONTROL_SELECTOR +
|
|
222
|
+
', a[href], ' +
|
|
223
|
+
VENDOR_BUTTON +
|
|
224
|
+
', [aria-expanded], [aria-pressed], ' +
|
|
225
|
+
EDITABLE;
|
|
151
226
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
227
|
+
// A wallet-named element that may stand as a control by its name alone: not
|
|
228
|
+
// itself a picker, toggle or form field; not inside a picker, a real button or
|
|
229
|
+
// link, a toggle, an editable region or another wallet-named element, which own
|
|
230
|
+
// the click with their own state; and holding no picker or field, in its open
|
|
231
|
+
// shadow root either, so a payment-method row with its own radio or select
|
|
232
|
+
// never becomes a wallet tap.
|
|
233
|
+
export function isWalletNamedControl(element: Element): boolean {
|
|
234
|
+
if (!isWalletNamed(element) || element.matches(NAME_BARRIER)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
for (
|
|
238
|
+
let node = composedParent(element);
|
|
239
|
+
node;
|
|
240
|
+
node = composedParent(node)
|
|
241
|
+
) {
|
|
242
|
+
if (node.matches(NAME_HOLDER) || isWalletNamed(node)) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const shadow = element.shadowRoot;
|
|
247
|
+
return (
|
|
248
|
+
!element.querySelector(NAME_BARRIER) &&
|
|
249
|
+
!(shadow && shadow.querySelector(NAME_BARRIER))
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Whether a click is never tracked: it has no element, or lands on a Rokt
|
|
254
|
+
// element or under data-mp-no-track. Reads at most 12 entries of the path.
|
|
255
|
+
export function isExcludedPath(path: Array<EventTarget | null>): boolean {
|
|
158
256
|
const entries = path.slice(0, 12);
|
|
159
257
|
let first: Element | undefined;
|
|
160
258
|
for (const target of entries) {
|
|
@@ -167,63 +265,79 @@ export function findControl(
|
|
|
167
265
|
!first ||
|
|
168
266
|
hasComposedAncestor(first, ROKT_SELECTOR + ', [data-mp-no-track]')
|
|
169
267
|
) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
for (const target of entries) {
|
|
271
|
+
if (target instanceof Element && hasRoktSrc(target)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Walks at most 12 entries of an event's composed path for the nearest control.
|
|
279
|
+
// A wallet-named element counts as one unless a picker, toggle or form field
|
|
280
|
+
// lies between it and the target or a picker holds it, so choosing an option in
|
|
281
|
+
// or around it never becomes its tap. Excluded clicks give nothing; disabled controls are skipped, a
|
|
282
|
+
// wallet-named one also by its `disabled` class. A caller that has already
|
|
283
|
+
// found the path not excluded passes `included` so the walk runs once.
|
|
284
|
+
export function findControl(
|
|
285
|
+
path: Array<EventTarget | null>,
|
|
286
|
+
included = false
|
|
287
|
+
): Element | undefined {
|
|
288
|
+
if (!included && isExcludedPath(path)) {
|
|
170
289
|
return undefined;
|
|
171
290
|
}
|
|
172
291
|
let control: Element | undefined;
|
|
173
292
|
let option: Element | undefined;
|
|
174
|
-
|
|
293
|
+
let picked = false;
|
|
294
|
+
for (const target of path.slice(0, 12)) {
|
|
295
|
+
if (control) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
175
298
|
if (!(target instanceof Element)) {
|
|
176
299
|
continue;
|
|
177
300
|
}
|
|
178
|
-
if (hasRoktSrc(target)) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
301
|
if (
|
|
182
|
-
|
|
183
|
-
|
|
302
|
+
target.matches(CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON) ||
|
|
303
|
+
(!picked && isWalletNamedControl(target))
|
|
184
304
|
) {
|
|
185
305
|
control = target;
|
|
186
|
-
} else
|
|
187
|
-
option
|
|
306
|
+
} else {
|
|
307
|
+
if (!option && target.matches(OPTION_INPUT)) {
|
|
308
|
+
option = target;
|
|
309
|
+
}
|
|
310
|
+
picked = picked || target.matches(NAME_BARRIER);
|
|
188
311
|
}
|
|
189
312
|
}
|
|
190
313
|
control = control || option;
|
|
191
|
-
if (
|
|
314
|
+
if (
|
|
315
|
+
!control ||
|
|
316
|
+
control.matches(':disabled, [aria-disabled="true"]') ||
|
|
317
|
+
(control.classList.contains('disabled') && isWalletNamed(control))
|
|
318
|
+
) {
|
|
192
319
|
return undefined;
|
|
193
320
|
}
|
|
194
321
|
return control;
|
|
195
322
|
}
|
|
196
323
|
|
|
197
|
-
const EDITABLE = '[contenteditable]:not([contenteditable="false"])';
|
|
198
|
-
|
|
199
324
|
// Text that may hold what a shopper typed or what the page opted out: form
|
|
200
|
-
// fields, editable regions, and anything under data-mp-no-track.
|
|
201
|
-
|
|
325
|
+
// fields, editable regions, and anything under data-mp-no-track. `own` is a
|
|
326
|
+
// box the element labels, which holds no typing of its own.
|
|
327
|
+
export function isUnsafeTextSource(element: Element, own?: Element): boolean {
|
|
328
|
+
const inside = 'input, select, textarea, [data-mp-no-track], ' + EDITABLE;
|
|
202
329
|
return (
|
|
203
330
|
element.matches(
|
|
204
331
|
'input, select, textarea, option, optgroup, datalist, ' + EDITABLE
|
|
205
332
|
) ||
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
333
|
+
(own
|
|
334
|
+
? Array.from(element.querySelectorAll(inside)).some(
|
|
335
|
+
found => found !== own
|
|
336
|
+
)
|
|
337
|
+
: !!element.querySelector(inside)) ||
|
|
209
338
|
hasComposedAncestor(
|
|
210
339
|
element,
|
|
211
340
|
'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE
|
|
212
341
|
)
|
|
213
342
|
);
|
|
214
343
|
}
|
|
215
|
-
|
|
216
|
-
// A label around a checkbox or radio holds no typed text, so only the other
|
|
217
|
-
// fields rule its text out.
|
|
218
|
-
export function isUnsafeLabelSource(label: Element): boolean {
|
|
219
|
-
return (
|
|
220
|
-
!!label.querySelector(
|
|
221
|
-
'input:not([type="checkbox"]):not([type="radio"]), select, textarea, [data-mp-no-track], ' +
|
|
222
|
-
EDITABLE
|
|
223
|
-
) ||
|
|
224
|
-
hasComposedAncestor(
|
|
225
|
-
label,
|
|
226
|
-
'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE
|
|
227
|
-
)
|
|
228
|
-
);
|
|
229
|
-
}
|
|
@@ -16,10 +16,13 @@ function createDigitPatterns(): { run: RegExp; number: RegExp } | undefined {
|
|
|
16
16
|
|
|
17
17
|
const digitPatterns = createDigitPatterns();
|
|
18
18
|
|
|
19
|
+
// Longer raw labels are dropped, not cut short.
|
|
20
|
+
export const MAX_RAW_LABEL_LENGTH = 128;
|
|
21
|
+
|
|
19
22
|
// The label as logged: lower case, at most 64 characters, digits and amounts
|
|
20
23
|
// masked. Dropped when it looks like an email, phone or card number.
|
|
21
24
|
export function maskLabel(raw: string | null | undefined): string | undefined {
|
|
22
|
-
if (!raw || raw.length >
|
|
25
|
+
if (!raw || raw.length > MAX_RAW_LABEL_LENGTH || !digitPatterns) {
|
|
23
26
|
return undefined;
|
|
24
27
|
}
|
|
25
28
|
const text = raw
|
package/src/configAPIClient.ts
CHANGED
package/src/constants.ts
CHANGED
|
@@ -191,6 +191,7 @@ const Constants = {
|
|
|
191
191
|
AstBackgroundEvents: 'astBackgroundEvents',
|
|
192
192
|
AutoLogPageView: 'autoLogPageView',
|
|
193
193
|
AutoButtonTracking: 'autoButtonTracking',
|
|
194
|
+
AutoLinkTracking: 'autoLinkTracking',
|
|
194
195
|
},
|
|
195
196
|
DefaultInstance: 'default_instance',
|
|
196
197
|
CCPAPurpose: 'data_sale_opt_out',
|
|
@@ -19,6 +19,12 @@ export interface ICookieConsentFlags {
|
|
|
19
19
|
* Default: false (targeting is allowed)
|
|
20
20
|
*/
|
|
21
21
|
noTargeting: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* When true, indicates the user has opted out of the sale or sharing of their data.
|
|
25
|
+
* Default: false (sharing is allowed)
|
|
26
|
+
*/
|
|
27
|
+
doNotShareOrSell?: boolean;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export interface ICookieConsentManager {
|
|
@@ -32,6 +38,11 @@ export interface ICookieConsentManager {
|
|
|
32
38
|
*/
|
|
33
39
|
getNoFunctional: () => boolean;
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Sharing is allowed when doNotShareOrSell is false (default)
|
|
43
|
+
*/
|
|
44
|
+
getDoNotShareOrSell: () => boolean;
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* Syncs the $NoTargeting user attribute based on the current noTargeting flag.
|
|
37
48
|
* Sets the attribute when noTargeting is true, removes it when false.
|
|
@@ -40,12 +51,12 @@ export interface ICookieConsentManager {
|
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
/**
|
|
43
|
-
* CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting
|
|
44
|
-
* that are passed via launcherOptions during SDK initialization.
|
|
54
|
+
* CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting,
|
|
55
|
+
* doNotShareOrSell) that are passed via launcherOptions during SDK initialization.
|
|
45
56
|
*
|
|
46
57
|
* These flags allow Rokt integration to respect user privacy choices.
|
|
47
58
|
*
|
|
48
|
-
* Default behavior:
|
|
59
|
+
* Default behavior: All flags default to false, meaning all features are allowed
|
|
49
60
|
* unless explicitly opted out by the user.
|
|
50
61
|
*/
|
|
51
62
|
export default class CookieConsentManager implements ICookieConsentManager {
|
|
@@ -53,6 +64,7 @@ export default class CookieConsentManager implements ICookieConsentManager {
|
|
|
53
64
|
this.flags = {
|
|
54
65
|
noFunctional: flags.noFunctional === true,
|
|
55
66
|
noTargeting: flags.noTargeting === true,
|
|
67
|
+
doNotShareOrSell: flags.doNotShareOrSell === true,
|
|
56
68
|
};
|
|
57
69
|
}
|
|
58
70
|
|
|
@@ -72,6 +84,14 @@ export default class CookieConsentManager implements ICookieConsentManager {
|
|
|
72
84
|
return this.flags.noFunctional;
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Returns true if the user has opted out of the sale or sharing of their data.
|
|
89
|
+
* Sharing is allowed when doNotShareOrSell is false (default).
|
|
90
|
+
*/
|
|
91
|
+
getDoNotShareOrSell(): boolean {
|
|
92
|
+
return this.flags.doNotShareOrSell;
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
syncNoTargetingAttribute(user: IMParticleUser | null): void {
|
|
76
96
|
if (!user) return;
|
|
77
97
|
|
package/src/cookieSyncManager.ts
CHANGED
|
@@ -118,9 +118,17 @@ export default function CookieSyncManager(
|
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// For Rokt, block cookie sync
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
// For Rokt, block cookie sync under the noTargeting or doNotShareOrSell privacy flag,
|
|
122
|
+
// or when the browser sends Global Privacy Control
|
|
123
|
+
if (moduleId === PARTNER_MODULE_IDS.Rokt) {
|
|
124
|
+
const nav = typeof navigator === 'undefined' ? undefined : (navigator as Navigator & { globalPrivacyControl?: boolean });
|
|
125
|
+
if (
|
|
126
|
+
mpInstance._CookieConsentManager.getNoTargeting() ||
|
|
127
|
+
mpInstance._CookieConsentManager.getDoNotShareOrSell() ||
|
|
128
|
+
(!!nav && nav.globalPrivacyControl === true)
|
|
129
|
+
) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
const { isEnabledForUserConsent } = mpInstance._Consent;
|
package/src/events.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
allowedQueryParams,
|
|
26
26
|
autoPageViewAttribute,
|
|
27
27
|
IPageViewOptions,
|
|
28
|
+
paramsToAttributes,
|
|
28
29
|
} from './pageViewTracker';
|
|
29
30
|
|
|
30
31
|
interface DOMHandlerElement extends HTMLElement {
|
|
@@ -151,8 +152,7 @@ export default function Events(
|
|
|
151
152
|
|
|
152
153
|
// The auto page view for the landing page. The SPA navigations that follow it
|
|
153
154
|
// come from PageViewTracker instead, so both emitters attach the same
|
|
154
|
-
// allowlisted query params
|
|
155
|
-
// attribution, since utm_*/gclid live on the entry URL.
|
|
155
|
+
// allowlisted query params.
|
|
156
156
|
this.logPageView = function(options?: IPageViewOptions): void {
|
|
157
157
|
self.logEvent({
|
|
158
158
|
messageType: Types.MessageType.PageView,
|
|
@@ -160,7 +160,13 @@ export default function Events(
|
|
|
160
160
|
data: {
|
|
161
161
|
// Params first so the core fields always win. See
|
|
162
162
|
// buildPageViewEvent, which does the same for SPA views.
|
|
163
|
-
...
|
|
163
|
+
...paramsToAttributes(
|
|
164
|
+
allowedQueryParams(
|
|
165
|
+
getHref(),
|
|
166
|
+
mpInstance._Store.SDKConfig.autoLogPageViewQueryParams
|
|
167
|
+
?.allowed
|
|
168
|
+
)
|
|
169
|
+
),
|
|
164
170
|
hostname: window.location.hostname,
|
|
165
171
|
title: window.document.title,
|
|
166
172
|
...autoPageViewAttribute(options && options.isAutoPageView),
|
package/src/forwarder-utils.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { inArray, valueof } from './utils';
|
|
|
7
7
|
import KitFilterHelper from './kitFilterHelper';
|
|
8
8
|
import * as EventsApi from '@mparticle/event-models';
|
|
9
9
|
import { IKitFilterSettings } from './configAPIClient';
|
|
10
|
+
import type { SDKEvent } from './sdkRuntimeModels';
|
|
11
|
+
import type KitBlocker from './kitBlocking';
|
|
10
12
|
import {
|
|
11
13
|
getEventNameFromBatchEvent,
|
|
12
14
|
getEventCategoryFromBatchEvent,
|
|
@@ -117,6 +119,42 @@ export function isBlockedByEventFilter(
|
|
|
117
119
|
return false;
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
// sendEventToForwarders passes no kitBlocker: apiClient has already applied
|
|
123
|
+
// the data plan by then.
|
|
124
|
+
export function isEventForwarded(
|
|
125
|
+
event: SDKEvent,
|
|
126
|
+
forwarder: ForwarderFilterConfig,
|
|
127
|
+
kitBlocker?: KitBlocker
|
|
128
|
+
): boolean {
|
|
129
|
+
const plannedEvent =
|
|
130
|
+
kitBlocker && kitBlocker.kitBlockingEnabled
|
|
131
|
+
? kitBlocker.createBlockedEvent(event)
|
|
132
|
+
: event;
|
|
133
|
+
if (!plannedEvent) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
isBlockedByForwardingRule(
|
|
139
|
+
plannedEvent.EventDataType,
|
|
140
|
+
plannedEvent.EventAttributes,
|
|
141
|
+
forwarder
|
|
142
|
+
)
|
|
143
|
+
) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const eventCategory = plannedEvent.EventCategory as valueof<
|
|
148
|
+
typeof EventType
|
|
149
|
+
>;
|
|
150
|
+
return !isBlockedByEventFilter(
|
|
151
|
+
plannedEvent.EventDataType,
|
|
152
|
+
KitFilterHelper.hashEventName(plannedEvent.EventName, eventCategory),
|
|
153
|
+
KitFilterHelper.hashEventType(eventCategory),
|
|
154
|
+
forwarder
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
120
158
|
export function filterEventAttributes(
|
|
121
159
|
messageType: number,
|
|
122
160
|
eventCategory: number,
|
package/src/forwarders.ts
CHANGED
|
@@ -6,8 +6,7 @@ import KitFilterHelper from './kitFilterHelper';
|
|
|
6
6
|
import Constants from './constants';
|
|
7
7
|
import APIClient, { IForwardingStatsData } from './apiClient';
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
isBlockedByEventFilter,
|
|
9
|
+
isEventForwarded,
|
|
11
10
|
filterEventAttributes,
|
|
12
11
|
filterUserIdentities,
|
|
13
12
|
} from './forwarder-utils';
|
|
@@ -266,39 +265,14 @@ export default function Forwarders(
|
|
|
266
265
|
return;
|
|
267
266
|
}
|
|
268
267
|
|
|
269
|
-
const hashedEventName = KitFilterHelper.hashEventName(
|
|
270
|
-
event.EventName,
|
|
271
|
-
event.EventCategory as any
|
|
272
|
-
);
|
|
273
|
-
const hashedEventType = KitFilterHelper.hashEventType(
|
|
274
|
-
event.EventCategory as any
|
|
275
|
-
);
|
|
276
|
-
|
|
277
268
|
const forwardEvent = function(forwarder: ConfiguredKit): void {
|
|
278
|
-
if (
|
|
279
|
-
isBlockedByForwardingRule(
|
|
280
|
-
event.EventDataType,
|
|
281
|
-
event.EventAttributes,
|
|
282
|
-
forwarder
|
|
283
|
-
)
|
|
284
|
-
) {
|
|
269
|
+
if (!isEventForwarded(event, forwarder)) {
|
|
285
270
|
return;
|
|
286
271
|
}
|
|
287
272
|
|
|
288
273
|
// Clone the event object, as we could be sending different attributes to each forwarder
|
|
289
274
|
const clonedEvent = extend(true, {}, event);
|
|
290
275
|
|
|
291
|
-
if (
|
|
292
|
-
isBlockedByEventFilter(
|
|
293
|
-
event.EventDataType,
|
|
294
|
-
hashedEventName,
|
|
295
|
-
hashedEventType,
|
|
296
|
-
forwarder
|
|
297
|
-
)
|
|
298
|
-
) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
276
|
clonedEvent.EventAttributes = filterEventAttributes(
|
|
303
277
|
event.EventDataType,
|
|
304
278
|
event.EventCategory,
|
package/src/mp-instance.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
resetPageViewTracking,
|
|
68
68
|
} from './pageViewTracker';
|
|
69
69
|
import { ButtonTracker, resetButtonTracking } from './buttonTracking/buttonTracker';
|
|
70
|
+
import { getButtonClickBlocker } from './buttonTracking/buttonClickEvent';
|
|
70
71
|
|
|
71
72
|
export interface IErrorLogMessage {
|
|
72
73
|
message?: string;
|
|
@@ -1362,7 +1363,11 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
|
|
|
1362
1363
|
function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
1363
1364
|
const kitBlocker = createKitBlocker(config, mpInstance);
|
|
1364
1365
|
const { getFeatureFlag } = mpInstance._Helpers;
|
|
1365
|
-
const {
|
|
1366
|
+
const {
|
|
1367
|
+
AutoLogPageView,
|
|
1368
|
+
AutoButtonTracking,
|
|
1369
|
+
AutoLinkTracking,
|
|
1370
|
+
} = Constants.FeatureFlags;
|
|
1366
1371
|
|
|
1367
1372
|
// Destroy previous batch uploader to prevent leaked timers and event listeners
|
|
1368
1373
|
if (mpInstance._APIClient?.uploader) {
|
|
@@ -1384,6 +1389,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
|
1384
1389
|
mpInstance._NativeSdkHelpers.initializeSessionAttributes(apiKey);
|
|
1385
1390
|
// Button tracking never runs as a native passthrough.
|
|
1386
1391
|
resetButtonTracking();
|
|
1392
|
+
mpInstance._RoktManager.setAutoButtonClicksForwarded(false);
|
|
1387
1393
|
} else {
|
|
1388
1394
|
// Main SDK initialization flow
|
|
1389
1395
|
|
|
@@ -1447,6 +1453,32 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
|
1447
1453
|
);
|
|
1448
1454
|
}
|
|
1449
1455
|
|
|
1456
|
+
// Checkout intent comes from the shopper's clicks, so it goes to the
|
|
1457
|
+
// Rokt kit only when that kit receives them. Set before
|
|
1458
|
+
// processForwarders: the kit can read it during this init, from its own
|
|
1459
|
+
// init or from a returning shopper's landing page view.
|
|
1460
|
+
const logButtons = !!getFeatureFlag(AutoButtonTracking);
|
|
1461
|
+
const logLinks = !!getFeatureFlag(AutoLinkTracking);
|
|
1462
|
+
// Links feed intent too, so with link tracking on the kit must also
|
|
1463
|
+
// receive Auto Link Click events.
|
|
1464
|
+
const buttonClickBlocker = roktConfig
|
|
1465
|
+
? getButtonClickBlocker(roktConfig, kitBlocker)
|
|
1466
|
+
: undefined;
|
|
1467
|
+
const linkClickBlocker =
|
|
1468
|
+
roktConfig && logLinks
|
|
1469
|
+
? getButtonClickBlocker(roktConfig, kitBlocker, true)
|
|
1470
|
+
: undefined;
|
|
1471
|
+
if (logButtons && (buttonClickBlocker || linkClickBlocker)) {
|
|
1472
|
+
mpInstance.Logger.warning(
|
|
1473
|
+
buttonClickBlocker
|
|
1474
|
+
? `Checkout intent is off: the ${buttonClickBlocker} keeps some or all Auto Button Click events from the Rokt kit.`
|
|
1475
|
+
: `Checkout intent is off: the ${linkClickBlocker} keeps some or all Auto Link Click events from the Rokt kit.`
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
mpInstance._RoktManager.setAutoButtonClicksForwarded(
|
|
1479
|
+
!!roktConfig && !buttonClickBlocker && !linkClickBlocker
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1450
1482
|
mpInstance._Forwarders.processForwarders(
|
|
1451
1483
|
config,
|
|
1452
1484
|
mpInstance._APIClient.prepareForwardingStats
|
|
@@ -1491,13 +1523,15 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
|
1491
1523
|
mpInstance._PageViewTracker.teardown();
|
|
1492
1524
|
}
|
|
1493
1525
|
|
|
1494
|
-
|
|
1495
|
-
// intent also goes to the Rokt kit when this init configures one.
|
|
1496
|
-
if (getFeatureFlag(AutoButtonTracking)) {
|
|
1526
|
+
if (logButtons || logLinks) {
|
|
1497
1527
|
if (!mpInstance._ButtonTracker) {
|
|
1498
1528
|
mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
|
|
1499
1529
|
}
|
|
1500
|
-
mpInstance._ButtonTracker.init({
|
|
1530
|
+
mpInstance._ButtonTracker.init({
|
|
1531
|
+
hasRoktKit: !!roktConfig,
|
|
1532
|
+
logButtons,
|
|
1533
|
+
logLinks,
|
|
1534
|
+
});
|
|
1501
1535
|
} else {
|
|
1502
1536
|
// Also retires a tracker left by an earlier bundle load. Like APV,
|
|
1503
1537
|
// this assumes a single active SDK instance.
|
|
@@ -1628,10 +1662,10 @@ function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
|
|
|
1628
1662
|
mpInstance.Logger.verbose(StartingInitialization);
|
|
1629
1663
|
|
|
1630
1664
|
// Initialize CookieConsentManager with privacy flags from launcherOptions
|
|
1631
|
-
const { noFunctional, noTargeting } = normalizeRoktLauncherOptions(
|
|
1665
|
+
const { noFunctional, noTargeting, doNotShareOrSell } = normalizeRoktLauncherOptions(
|
|
1632
1666
|
config?.launcherOptions
|
|
1633
1667
|
);
|
|
1634
|
-
mpInstance._CookieConsentManager = new CookieConsentManager({ noFunctional, noTargeting });
|
|
1668
|
+
mpInstance._CookieConsentManager = new CookieConsentManager({ noFunctional, noTargeting, doNotShareOrSell });
|
|
1635
1669
|
|
|
1636
1670
|
// Check to see if localStorage is available before main configuration runs
|
|
1637
1671
|
// since we will need this for the current implementation of user persistence
|