@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
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { IMParticleWebSDKInstance } from '../mp-instance';
|
|
2
2
|
import { isFunction } from '../utils';
|
|
3
|
-
import { buildButtonClickEvent } from './buttonClickEvent';
|
|
4
|
-
import {
|
|
3
|
+
import { buildButtonClickEvent, buildLinkClickEvent } from './buttonClickEvent';
|
|
4
|
+
import {
|
|
5
|
+
describeButtonClick,
|
|
6
|
+
describePaymentChoice,
|
|
7
|
+
IButtonClick,
|
|
8
|
+
IPaymentChoice,
|
|
9
|
+
isLinkClick,
|
|
10
|
+
isPicked,
|
|
11
|
+
} from './buttonDescription';
|
|
5
12
|
import {
|
|
6
13
|
CheckoutIntentKind,
|
|
7
14
|
CheckoutIntentSource,
|
|
8
15
|
classifyIntent,
|
|
9
|
-
|
|
16
|
+
classifyPaymentFrame,
|
|
17
|
+
PaymentFrameFamily,
|
|
10
18
|
} from './checkoutIntent';
|
|
11
19
|
import { findControl, isExcludedPath } from './controls';
|
|
12
20
|
|
|
@@ -37,6 +45,9 @@ type WindowWithBtn = Window & {
|
|
|
37
45
|
export interface IButtonTrackerOptions {
|
|
38
46
|
// The Rokt kit signal only runs when this init configures a Rokt kit.
|
|
39
47
|
hasRoktKit: boolean;
|
|
48
|
+
// Button clicks and clicks on links that leave the page are logged apart.
|
|
49
|
+
logButtons: boolean;
|
|
50
|
+
logLinks: boolean;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
const btnWindow = (): WindowWithBtn | null =>
|
|
@@ -137,12 +148,15 @@ const CAPTURE_PASSIVE: AddEventListenerOptions = {
|
|
|
137
148
|
};
|
|
138
149
|
const PASSIVE: AddEventListenerOptions = { passive: true };
|
|
139
150
|
|
|
140
|
-
// Logs
|
|
141
|
-
// Rokt kit as a frozen {kind, t, source, pathname}. Submits and wallet frame focus feed the kit only
|
|
151
|
+
// Logs button clicks and clicks on links that leave the page as mParticle events, and hands checkout
|
|
152
|
+
// intent to the Rokt kit as a frozen {kind, t, source, pathname}. Submits and wallet frame focus feed the kit only,
|
|
153
|
+
// and so do payment picks: a payment radio's option names, or the payment family of a focused frame.
|
|
142
154
|
export class ButtonTracker {
|
|
143
155
|
private readonly mpInstance: IMParticleWebSDKInstance;
|
|
144
156
|
private active = false;
|
|
145
157
|
private hasRoktKit = false;
|
|
158
|
+
private logButtons = false;
|
|
159
|
+
private logLinks = false;
|
|
146
160
|
// Bumped on reset so classification queued before it is dropped.
|
|
147
161
|
private generation = 0;
|
|
148
162
|
private lastCommitClick = -Infinity;
|
|
@@ -186,8 +200,13 @@ export class ButtonTracker {
|
|
|
186
200
|
this.teardown();
|
|
187
201
|
}
|
|
188
202
|
this.hasRoktKit = options.hasRoktKit;
|
|
203
|
+
this.logButtons = options.logButtons;
|
|
204
|
+
this.logLinks = options.logLinks;
|
|
189
205
|
|
|
190
|
-
if (
|
|
206
|
+
if (
|
|
207
|
+
(!(this.logButtons || this.logLinks) || !this.eventsEnabled()) &&
|
|
208
|
+
this.isKitSuppressed()
|
|
209
|
+
) {
|
|
191
210
|
this.log('opted out, not starting');
|
|
192
211
|
return;
|
|
193
212
|
}
|
|
@@ -358,7 +377,11 @@ export class ButtonTracker {
|
|
|
358
377
|
return;
|
|
359
378
|
}
|
|
360
379
|
const layer = event.type;
|
|
361
|
-
const logsEvents =
|
|
380
|
+
const logsEvents =
|
|
381
|
+
layer === 'click' &&
|
|
382
|
+
(this.logButtons || this.logLinks) &&
|
|
383
|
+
this.eventsEnabled() &&
|
|
384
|
+
!this.hasTargetingOptOut();
|
|
362
385
|
const signalsKit = !this.isKitSuppressed();
|
|
363
386
|
if (!logsEvents && !signalsKit) {
|
|
364
387
|
return;
|
|
@@ -372,9 +395,12 @@ export class ButtonTracker {
|
|
|
372
395
|
// button that disables or relabels itself is seen as it was clicked.
|
|
373
396
|
// Logging and the kit hand-off wait for the next paint.
|
|
374
397
|
let click: IButtonClick | undefined;
|
|
398
|
+
let asLink = false;
|
|
375
399
|
let box: HTMLInputElement | undefined;
|
|
376
400
|
let kind: CheckoutIntentKind | undefined;
|
|
377
401
|
let source: CheckoutIntentSource | undefined;
|
|
402
|
+
let choice: IPaymentChoice | undefined;
|
|
403
|
+
let family: PaymentFrameFamily | undefined;
|
|
378
404
|
let unconfirmedCommit = false;
|
|
379
405
|
if (layer === 'click') {
|
|
380
406
|
// The composed path is only populated during dispatch.
|
|
@@ -384,9 +410,28 @@ export class ButtonTracker {
|
|
|
384
410
|
const excluded = isExcludedPath(path);
|
|
385
411
|
const control = excluded ? undefined : findControl(path, true);
|
|
386
412
|
if (control) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
413
|
+
// With links alone, only an anchor can be logged.
|
|
414
|
+
click =
|
|
415
|
+
logsEvents &&
|
|
416
|
+
(this.logButtons || control.matches('a[href]'))
|
|
417
|
+
? describeButtonClick(control, path)
|
|
418
|
+
: undefined;
|
|
419
|
+
if (click) {
|
|
420
|
+
asLink = this.logLinks && isLinkClick(click);
|
|
421
|
+
// Without link tracking, a link that leaves the page stays a
|
|
422
|
+
// button click, except a plain link to another page of the
|
|
423
|
+
// site, which button tracking never logged.
|
|
424
|
+
const asButton =
|
|
425
|
+
this.logButtons &&
|
|
426
|
+
!asLink &&
|
|
427
|
+
!(
|
|
428
|
+
click.linkType === 'internal' &&
|
|
429
|
+
control.matches('a[href]:not([role="button"])')
|
|
430
|
+
);
|
|
431
|
+
if (!asLink && !asButton) {
|
|
432
|
+
click = undefined;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
390
435
|
if (click && click.checked !== undefined) {
|
|
391
436
|
box = control as HTMLInputElement;
|
|
392
437
|
}
|
|
@@ -396,6 +441,9 @@ export class ButtonTracker {
|
|
|
396
441
|
: undefined;
|
|
397
442
|
kind = intent ? intent.kind : undefined;
|
|
398
443
|
source = intent ? intent.source : undefined;
|
|
444
|
+
choice = signalsKit
|
|
445
|
+
? describePaymentChoice(control, path)
|
|
446
|
+
: undefined;
|
|
399
447
|
}
|
|
400
448
|
this.lastClick = { t, excluded, kind, source };
|
|
401
449
|
} else if (layer === 'submit') {
|
|
@@ -424,7 +472,12 @@ export class ButtonTracker {
|
|
|
424
472
|
if (layer === 'blur') {
|
|
425
473
|
// Read after the blur settles: some browsers have not moved
|
|
426
474
|
// focus into the frame yet when the window blurs.
|
|
427
|
-
|
|
475
|
+
const frame = classifyPaymentFrame(
|
|
476
|
+
document.activeElement,
|
|
477
|
+
window
|
|
478
|
+
);
|
|
479
|
+
kind = frame ? frame.kind : undefined;
|
|
480
|
+
family = frame ? frame.family : undefined;
|
|
428
481
|
source = 'frame';
|
|
429
482
|
} else if (
|
|
430
483
|
unconfirmedCommit &&
|
|
@@ -444,7 +497,14 @@ export class ButtonTracker {
|
|
|
444
497
|
? !click.checked
|
|
445
498
|
: box.checked;
|
|
446
499
|
}
|
|
447
|
-
this.logClick(click, pathname);
|
|
500
|
+
this.logClick(click, pathname, asLink);
|
|
501
|
+
}
|
|
502
|
+
// Ahead of the intent, so a pre-selection it fires goes out
|
|
503
|
+
// with this pick.
|
|
504
|
+
if (choice && isPicked(choice.radio)) {
|
|
505
|
+
this.pickPayment(t, pathname, choice.names);
|
|
506
|
+
} else if (family) {
|
|
507
|
+
this.pickPayment(t, pathname, undefined, family);
|
|
448
508
|
}
|
|
449
509
|
if (kind) {
|
|
450
510
|
this.emit(kind, source, layer, t, pathname);
|
|
@@ -456,8 +516,12 @@ export class ButtonTracker {
|
|
|
456
516
|
}
|
|
457
517
|
|
|
458
518
|
// Every real click is one event, repeats included, up to a ceiling per page.
|
|
459
|
-
private logClick(
|
|
460
|
-
|
|
519
|
+
private logClick(
|
|
520
|
+
click: IButtonClick,
|
|
521
|
+
pathname: string,
|
|
522
|
+
asLink: boolean
|
|
523
|
+
): void {
|
|
524
|
+
if (!this.eventsEnabled() || this.hasTargetingOptOut()) {
|
|
461
525
|
return;
|
|
462
526
|
}
|
|
463
527
|
if (pathname !== this.pagePath) {
|
|
@@ -472,7 +536,9 @@ export class ButtonTracker {
|
|
|
472
536
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
473
537
|
const queued = this.queuedForUpload();
|
|
474
538
|
this.mpInstance._Events.logEvent(
|
|
475
|
-
|
|
539
|
+
asLink
|
|
540
|
+
? buildLinkClickEvent(click, pathname)
|
|
541
|
+
: buildButtonClickEvent(click, pathname)
|
|
476
542
|
);
|
|
477
543
|
// Only a click waiting for the batch interval is sent early: a held
|
|
478
544
|
// one goes out when released, and with batching off it already has.
|
|
@@ -546,12 +612,43 @@ export class ButtonTracker {
|
|
|
546
612
|
);
|
|
547
613
|
}
|
|
548
614
|
|
|
615
|
+
private pickPayment(
|
|
616
|
+
t: number,
|
|
617
|
+
pathname: string,
|
|
618
|
+
names?: string[],
|
|
619
|
+
family?: PaymentFrameFamily
|
|
620
|
+
): void {
|
|
621
|
+
if (this.isKitSuppressed()) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
this.mpInstance._RoktManager.notifyPaymentPick(
|
|
625
|
+
Object.freeze({
|
|
626
|
+
t,
|
|
627
|
+
pathname,
|
|
628
|
+
names: names ? Object.freeze(names.slice()) : undefined,
|
|
629
|
+
family,
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
549
634
|
// Events follow the SDK's opt-out like any other event.
|
|
550
635
|
private eventsEnabled(): boolean {
|
|
551
636
|
const store = this.mpInstance._Store;
|
|
552
637
|
return !!store && store.isEnabled;
|
|
553
638
|
}
|
|
554
639
|
|
|
640
|
+
// Read per click, since the launcher options can arrive after the tracker
|
|
641
|
+
// starts. Only where this init configures a Rokt kit, so Global Privacy
|
|
642
|
+
// Control alone doesn't stop click logging elsewhere.
|
|
643
|
+
private hasTargetingOptOut(): boolean {
|
|
644
|
+
const roktManager = this.mpInstance._RoktManager;
|
|
645
|
+
return (
|
|
646
|
+
this.hasRoktKit &&
|
|
647
|
+
!!roktManager &&
|
|
648
|
+
roktManager.hasTargetingOptOut()
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
555
652
|
// The Rokt manager owns the kit's privacy gates: flag, opt-out, launcher
|
|
556
653
|
// options and Global Privacy Control.
|
|
557
654
|
private isKitSuppressed(): boolean {
|
|
@@ -82,11 +82,12 @@ const steps = new RegExp(
|
|
|
82
82
|
')$'
|
|
83
83
|
);
|
|
84
84
|
const CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
|
|
85
|
-
// "Add to bag"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
|
|
86
|
+
const addToCartLabels = new RegExp('^add(?: [^ ]+){0,5} to ' + CART_NOUN + '$');
|
|
87
|
+
// "View cart", "Shopping bag", "Bag", and a count badge such as "1 item in bag",
|
|
88
|
+
// which reads "item in bag" once digits are stripped: a click that opens the cart.
|
|
89
|
+
const openCartLabels = new RegExp(
|
|
90
|
+
'^(?:(?:(?:view|open|go to) )?' + CART_NOUN + '(?: items?)?' +
|
|
90
91
|
'|items? in ' + CART_NOUN + ')$'
|
|
91
92
|
);
|
|
92
93
|
const WALLET_NAMES =
|
|
@@ -104,20 +105,35 @@ const walletBrands = new RegExp(
|
|
|
104
105
|
'^(?:' + WALLET_NAMES + ')(?: (?:checkout|check out|express(?: checkout)?|express pay|buy now|pay now|pay|now)' +
|
|
105
106
|
'| (?:logo|icon|mark|button)(?: (?:logo|icon|mark|button))*)?$'
|
|
106
107
|
);
|
|
108
|
+
// The payment method a frame's focus picks, in the Rokt kit's family names.
|
|
109
|
+
export type PaymentFrameFamily =
|
|
110
|
+
| 'apple_pay'
|
|
111
|
+
| 'google_pay'
|
|
112
|
+
| 'amazon_pay'
|
|
113
|
+
| 'shop_pay'
|
|
114
|
+
| 'card';
|
|
107
115
|
// Exact HTTPS hosts of hosted wallet button frames.
|
|
108
|
-
const
|
|
109
|
-
'pay.google.com',
|
|
110
|
-
'applepay.cdn-apple.com',
|
|
111
|
-
'payments.amazon.com',
|
|
112
|
-
'pay.shopify.com',
|
|
113
|
-
|
|
116
|
+
const walletFrameFamilies: { [host: string]: PaymentFrameFamily } = {
|
|
117
|
+
'pay.google.com': 'google_pay',
|
|
118
|
+
'applepay.cdn-apple.com': 'apple_pay',
|
|
119
|
+
'payments.amazon.com': 'amazon_pay',
|
|
120
|
+
'pay.shopify.com': 'shop_pay',
|
|
121
|
+
};
|
|
114
122
|
// PayPal serves its buttons and its card fields and pay-later messages from
|
|
115
123
|
// the same hosts; only the button frames are wallet taps.
|
|
116
124
|
const paypalFrameHosts = new Set(['www.paypal.com', 'www.sandbox.paypal.com']);
|
|
117
125
|
const PAYPAL_BUTTON_FRAME = /^\/smart\/(?:buttons|menu)(?:\/|$)/;
|
|
126
|
+
const PAYPAL_CARD_FRAME = /^\/smart\/card-fields?(?:\/|$)/;
|
|
127
|
+
// PayPal's JS SDK, used directly or wrapped by a processor, leaves its frames
|
|
128
|
+
// without a src and names them __zoid__paypal_<component>__ instead.
|
|
129
|
+
const PAYPAL_SDK_FRAME = /^__zoid__paypal_/;
|
|
130
|
+
const PAYPAL_SDK_BUTTON_FRAME = /^__zoid__paypal_(?:buttons|menu)__/;
|
|
118
131
|
// Stripe's quick-pay button frames; its other frames are card and form fields.
|
|
119
132
|
const STRIPE_WALLET_FRAME = /\/elements-inner-(?:payment-request|express-checkout)-/;
|
|
133
|
+
const STRIPE_CARD_FRAME = /\/elements-inner-card\b/;
|
|
120
134
|
// Hosted card-entry and fingerprint frames: focus there is typing card details.
|
|
135
|
+
// Some of these hosts also serve gift card and bank account fields, so none
|
|
136
|
+
// names a family.
|
|
121
137
|
const CARD_FRAME_DOMAINS = [
|
|
122
138
|
'adyen.com',
|
|
123
139
|
'braintreegateway.com',
|
|
@@ -211,10 +227,19 @@ function labels(element: Element): string[] {
|
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
// A cart button is often a bare icon, so its title names it when nothing else
|
|
214
|
-
// does. Only the cart rule reads the title.
|
|
230
|
+
// does. Only the cart rule reads the title. Opening the cart counts only for a
|
|
231
|
+
// control that opens it on the page, such as a flyout: one that leads to the cart
|
|
232
|
+
// page unloads before a call made on the click can return.
|
|
215
233
|
function isCartControl(control: Element, cores: string[]): boolean {
|
|
216
234
|
const title = cores.length ? undefined : normalize(control.getAttribute('title'));
|
|
217
|
-
|
|
235
|
+
const texts = title ? [coreLabel(title)] : cores;
|
|
236
|
+
if (texts.some(text => addToCartLabels.test(text))) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return (
|
|
240
|
+
(control.hasAttribute('aria-expanded') || control.hasAttribute('aria-haspopup')) &&
|
|
241
|
+
texts.some(text => openCartLabels.test(text))
|
|
242
|
+
);
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
// Which rule recognised the intent: a vendor's own markup, a phrase in the
|
|
@@ -252,8 +277,8 @@ function isCommitLabel(text: string, isLink: boolean, pathname: string): boolean
|
|
|
252
277
|
// pickers, toggles and pay-later messages never count; a vendor's own wallet
|
|
253
278
|
// button counts before any label rule; a confirm label on any source counts
|
|
254
279
|
// before the look-alike exclusions veto; then a wallet-named control, wallet
|
|
255
|
-
// phrases and steps.
|
|
256
|
-
// those do.
|
|
280
|
+
// phrases and steps. An add-to-cart label, links included, or a control that
|
|
281
|
+
// opens the cart on the page counts only where none of those do.
|
|
257
282
|
export function classifyIntent(
|
|
258
283
|
control: Element,
|
|
259
284
|
pathname: string
|
|
@@ -325,12 +350,20 @@ function isOnDomain(host: string, domain: string): boolean {
|
|
|
325
350
|
);
|
|
326
351
|
}
|
|
327
352
|
|
|
353
|
+
export interface IPaymentFrame {
|
|
354
|
+
kind: 'wallet' | 'frame';
|
|
355
|
+
family?: PaymentFrameFamily;
|
|
356
|
+
}
|
|
357
|
+
|
|
328
358
|
// Focus moving into a visible hosted wallet button counts as a wallet click;
|
|
329
|
-
// focus into hosted card fields or messages is only a frame.
|
|
330
|
-
|
|
359
|
+
// focus into hosted card fields or messages is only a frame. A family comes
|
|
360
|
+
// only from a frame that can draw no other method: PayPal's button frame also
|
|
361
|
+
// draws Pay Later, Venmo and its card form, Stripe's quick-pay frame draws
|
|
362
|
+
// Apple Pay or Google Pay by browser, and its payment form holds several.
|
|
363
|
+
export function classifyPaymentFrame(
|
|
331
364
|
target: EventTarget | null,
|
|
332
365
|
win: Window
|
|
333
|
-
):
|
|
366
|
+
): IPaymentFrame | undefined {
|
|
334
367
|
if (
|
|
335
368
|
win.document.visibilityState !== 'visible' ||
|
|
336
369
|
!(target instanceof HTMLIFrameElement)
|
|
@@ -343,20 +376,39 @@ export function classifyIntentFrame(
|
|
|
343
376
|
) {
|
|
344
377
|
return undefined;
|
|
345
378
|
}
|
|
379
|
+
// A frame that has its own src is judged by that address, never by its name.
|
|
380
|
+
if (
|
|
381
|
+
(!target.src || target.src === 'about:blank') &&
|
|
382
|
+
PAYPAL_SDK_FRAME.test(target.name)
|
|
383
|
+
) {
|
|
384
|
+
return PAYPAL_SDK_BUTTON_FRAME.test(target.name)
|
|
385
|
+
? { kind: 'wallet' }
|
|
386
|
+
: { kind: 'frame' };
|
|
387
|
+
}
|
|
346
388
|
try {
|
|
347
389
|
const url = new URL(target.src);
|
|
348
390
|
const host = url.protocol === 'https:' ? url.hostname : '';
|
|
349
391
|
if (paypalFrameHosts.has(host)) {
|
|
350
|
-
|
|
392
|
+
if (PAYPAL_BUTTON_FRAME.test(url.pathname)) {
|
|
393
|
+
return { kind: 'wallet' };
|
|
394
|
+
}
|
|
395
|
+
return PAYPAL_CARD_FRAME.test(url.pathname)
|
|
396
|
+
? { kind: 'frame', family: 'card' }
|
|
397
|
+
: { kind: 'frame' };
|
|
351
398
|
}
|
|
352
|
-
if (
|
|
353
|
-
return 'wallet';
|
|
399
|
+
if (Object.prototype.hasOwnProperty.call(walletFrameFamilies, host)) {
|
|
400
|
+
return { kind: 'wallet', family: walletFrameFamilies[host] };
|
|
354
401
|
}
|
|
355
402
|
if (host === 'js.stripe.com') {
|
|
356
|
-
|
|
403
|
+
if (STRIPE_WALLET_FRAME.test(url.pathname)) {
|
|
404
|
+
return { kind: 'wallet' };
|
|
405
|
+
}
|
|
406
|
+
return STRIPE_CARD_FRAME.test(url.pathname)
|
|
407
|
+
? { kind: 'frame', family: 'card' }
|
|
408
|
+
: { kind: 'frame' };
|
|
357
409
|
}
|
|
358
410
|
if (CARD_FRAME_DOMAINS.some(domain => isOnDomain(host, domain))) {
|
|
359
|
-
return 'frame';
|
|
411
|
+
return { kind: 'frame' };
|
|
360
412
|
}
|
|
361
413
|
} catch (e) {
|
|
362
414
|
// An unparseable frame src is not a wallet frame.
|
|
@@ -139,7 +139,7 @@ const PAYMENT_IN_NAME = /(?:^| )(?:pay|payment|checkout)(?= |$)/;
|
|
|
139
139
|
const BUTTON_IN_NAME = /(?:^| )(?:btn|button)(?= |$)/;
|
|
140
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
141
|
|
|
142
|
-
function nameWords(value: string | null): string {
|
|
142
|
+
export function nameWords(value: string | null): string {
|
|
143
143
|
return (value || '')
|
|
144
144
|
.slice(0, 64)
|
|
145
145
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
@@ -322,15 +322,19 @@ export function findControl(
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
// Text that may hold what a shopper typed or what the page opted out: form
|
|
325
|
-
// fields, editable regions, and anything under data-mp-no-track.
|
|
326
|
-
|
|
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;
|
|
327
329
|
return (
|
|
328
330
|
element.matches(
|
|
329
331
|
'input, select, textarea, option, optgroup, datalist, ' + EDITABLE
|
|
330
332
|
) ||
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
333
|
+
(own
|
|
334
|
+
? Array.from(element.querySelectorAll(inside)).some(
|
|
335
|
+
found => found !== own
|
|
336
|
+
)
|
|
337
|
+
: !!element.querySelector(inside)) ||
|
|
334
338
|
hasComposedAncestor(
|
|
335
339
|
element,
|
|
336
340
|
'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE
|
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/mp-instance.ts
CHANGED
|
@@ -1363,7 +1363,11 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
|
|
|
1363
1363
|
function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
1364
1364
|
const kitBlocker = createKitBlocker(config, mpInstance);
|
|
1365
1365
|
const { getFeatureFlag } = mpInstance._Helpers;
|
|
1366
|
-
const {
|
|
1366
|
+
const {
|
|
1367
|
+
AutoLogPageView,
|
|
1368
|
+
AutoButtonTracking,
|
|
1369
|
+
AutoLinkTracking,
|
|
1370
|
+
} = Constants.FeatureFlags;
|
|
1367
1371
|
|
|
1368
1372
|
// Destroy previous batch uploader to prevent leaked timers and event listeners
|
|
1369
1373
|
if (mpInstance._APIClient?.uploader) {
|
|
@@ -1453,16 +1457,26 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
|
1453
1457
|
// Rokt kit only when that kit receives them. Set before
|
|
1454
1458
|
// processForwarders: the kit can read it during this init, from its own
|
|
1455
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.
|
|
1456
1464
|
const buttonClickBlocker = roktConfig
|
|
1457
1465
|
? getButtonClickBlocker(roktConfig, kitBlocker)
|
|
1458
1466
|
: undefined;
|
|
1459
|
-
|
|
1467
|
+
const linkClickBlocker =
|
|
1468
|
+
roktConfig && logLinks
|
|
1469
|
+
? getButtonClickBlocker(roktConfig, kitBlocker, true)
|
|
1470
|
+
: undefined;
|
|
1471
|
+
if (logButtons && (buttonClickBlocker || linkClickBlocker)) {
|
|
1460
1472
|
mpInstance.Logger.warning(
|
|
1461
|
-
|
|
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.`
|
|
1462
1476
|
);
|
|
1463
1477
|
}
|
|
1464
1478
|
mpInstance._RoktManager.setAutoButtonClicksForwarded(
|
|
1465
|
-
!!roktConfig && !buttonClickBlocker
|
|
1479
|
+
!!roktConfig && !buttonClickBlocker && !linkClickBlocker
|
|
1466
1480
|
);
|
|
1467
1481
|
|
|
1468
1482
|
mpInstance._Forwarders.processForwarders(
|
|
@@ -1509,11 +1523,15 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
|
|
|
1509
1523
|
mpInstance._PageViewTracker.teardown();
|
|
1510
1524
|
}
|
|
1511
1525
|
|
|
1512
|
-
if (
|
|
1526
|
+
if (logButtons || logLinks) {
|
|
1513
1527
|
if (!mpInstance._ButtonTracker) {
|
|
1514
1528
|
mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
|
|
1515
1529
|
}
|
|
1516
|
-
mpInstance._ButtonTracker.init({
|
|
1530
|
+
mpInstance._ButtonTracker.init({
|
|
1531
|
+
hasRoktKit: !!roktConfig,
|
|
1532
|
+
logButtons,
|
|
1533
|
+
logLinks,
|
|
1534
|
+
});
|
|
1517
1535
|
} else {
|
|
1518
1536
|
// Also retires a tracker left by an earlier bundle load. Like APV,
|
|
1519
1537
|
// this assumes a single active SDK instance.
|
|
@@ -1644,10 +1662,10 @@ function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
|
|
|
1644
1662
|
mpInstance.Logger.verbose(StartingInitialization);
|
|
1645
1663
|
|
|
1646
1664
|
// Initialize CookieConsentManager with privacy flags from launcherOptions
|
|
1647
|
-
const { noFunctional, noTargeting } = normalizeRoktLauncherOptions(
|
|
1665
|
+
const { noFunctional, noTargeting, doNotShareOrSell } = normalizeRoktLauncherOptions(
|
|
1648
1666
|
config?.launcherOptions
|
|
1649
1667
|
);
|
|
1650
|
-
mpInstance._CookieConsentManager = new CookieConsentManager({ noFunctional, noTargeting });
|
|
1668
|
+
mpInstance._CookieConsentManager = new CookieConsentManager({ noFunctional, noTargeting, doNotShareOrSell });
|
|
1651
1669
|
|
|
1652
1670
|
// Check to see if localStorage is available before main configuration runs
|
|
1653
1671
|
// since we will need this for the current implementation of user persistence
|