@mparticle/web-sdk 3.24.0 → 3.26.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 +372 -93
- 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 +7 -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 +11 -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 +129 -20
- package/src/buttonTracking/checkoutIntent.ts +85 -27
- 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 +59 -10
- 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,16 @@ 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}
|
|
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}, with adds on an add-to-cart click.
|
|
153
|
+
// Submits and wallet frame focus feed the kit only, and so do payment picks: a payment radio's option
|
|
154
|
+
// names, or the payment family of a focused frame.
|
|
142
155
|
export class ButtonTracker {
|
|
143
156
|
private readonly mpInstance: IMParticleWebSDKInstance;
|
|
144
157
|
private active = false;
|
|
145
158
|
private hasRoktKit = false;
|
|
159
|
+
private logButtons = false;
|
|
160
|
+
private logLinks = false;
|
|
146
161
|
// Bumped on reset so classification queued before it is dropped.
|
|
147
162
|
private generation = 0;
|
|
148
163
|
private lastCommitClick = -Infinity;
|
|
@@ -186,8 +201,13 @@ export class ButtonTracker {
|
|
|
186
201
|
this.teardown();
|
|
187
202
|
}
|
|
188
203
|
this.hasRoktKit = options.hasRoktKit;
|
|
204
|
+
this.logButtons = options.logButtons;
|
|
205
|
+
this.logLinks = options.logLinks;
|
|
189
206
|
|
|
190
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
(!(this.logButtons || this.logLinks) || !this.eventsEnabled()) &&
|
|
209
|
+
this.isKitSuppressed()
|
|
210
|
+
) {
|
|
191
211
|
this.log('opted out, not starting');
|
|
192
212
|
return;
|
|
193
213
|
}
|
|
@@ -358,7 +378,11 @@ export class ButtonTracker {
|
|
|
358
378
|
return;
|
|
359
379
|
}
|
|
360
380
|
const layer = event.type;
|
|
361
|
-
const logsEvents =
|
|
381
|
+
const logsEvents =
|
|
382
|
+
layer === 'click' &&
|
|
383
|
+
(this.logButtons || this.logLinks) &&
|
|
384
|
+
this.eventsEnabled() &&
|
|
385
|
+
!this.hasTargetingOptOut();
|
|
362
386
|
const signalsKit = !this.isKitSuppressed();
|
|
363
387
|
if (!logsEvents && !signalsKit) {
|
|
364
388
|
return;
|
|
@@ -372,9 +396,13 @@ export class ButtonTracker {
|
|
|
372
396
|
// button that disables or relabels itself is seen as it was clicked.
|
|
373
397
|
// Logging and the kit hand-off wait for the next paint.
|
|
374
398
|
let click: IButtonClick | undefined;
|
|
399
|
+
let asLink = false;
|
|
375
400
|
let box: HTMLInputElement | undefined;
|
|
376
401
|
let kind: CheckoutIntentKind | undefined;
|
|
377
402
|
let source: CheckoutIntentSource | undefined;
|
|
403
|
+
let adds: true | undefined;
|
|
404
|
+
let choice: IPaymentChoice | undefined;
|
|
405
|
+
let family: PaymentFrameFamily | undefined;
|
|
378
406
|
let unconfirmedCommit = false;
|
|
379
407
|
if (layer === 'click') {
|
|
380
408
|
// The composed path is only populated during dispatch.
|
|
@@ -384,9 +412,28 @@ export class ButtonTracker {
|
|
|
384
412
|
const excluded = isExcludedPath(path);
|
|
385
413
|
const control = excluded ? undefined : findControl(path, true);
|
|
386
414
|
if (control) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
415
|
+
// With links alone, only an anchor can be logged.
|
|
416
|
+
click =
|
|
417
|
+
logsEvents &&
|
|
418
|
+
(this.logButtons || control.matches('a[href]'))
|
|
419
|
+
? describeButtonClick(control, path)
|
|
420
|
+
: undefined;
|
|
421
|
+
if (click) {
|
|
422
|
+
asLink = this.logLinks && isLinkClick(click);
|
|
423
|
+
// Without link tracking, a link that leaves the page stays a
|
|
424
|
+
// button click, except a plain link to another page of the
|
|
425
|
+
// site, which button tracking never logged.
|
|
426
|
+
const asButton =
|
|
427
|
+
this.logButtons &&
|
|
428
|
+
!asLink &&
|
|
429
|
+
!(
|
|
430
|
+
click.linkType === 'internal' &&
|
|
431
|
+
control.matches('a[href]:not([role="button"])')
|
|
432
|
+
);
|
|
433
|
+
if (!asLink && !asButton) {
|
|
434
|
+
click = undefined;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
390
437
|
if (click && click.checked !== undefined) {
|
|
391
438
|
box = control as HTMLInputElement;
|
|
392
439
|
}
|
|
@@ -396,6 +443,10 @@ export class ButtonTracker {
|
|
|
396
443
|
: undefined;
|
|
397
444
|
kind = intent ? intent.kind : undefined;
|
|
398
445
|
source = intent ? intent.source : undefined;
|
|
446
|
+
adds = intent ? intent.adds : undefined;
|
|
447
|
+
choice = signalsKit
|
|
448
|
+
? describePaymentChoice(control, path)
|
|
449
|
+
: undefined;
|
|
399
450
|
}
|
|
400
451
|
this.lastClick = { t, excluded, kind, source };
|
|
401
452
|
} else if (layer === 'submit') {
|
|
@@ -411,6 +462,7 @@ export class ButtonTracker {
|
|
|
411
462
|
const intent = classifyIntent(control, pathname);
|
|
412
463
|
kind = intent ? intent.kind : undefined;
|
|
413
464
|
source = intent ? intent.source : undefined;
|
|
465
|
+
adds = intent ? intent.adds : undefined;
|
|
414
466
|
}
|
|
415
467
|
unconfirmedCommit = !submitter;
|
|
416
468
|
}
|
|
@@ -424,7 +476,12 @@ export class ButtonTracker {
|
|
|
424
476
|
if (layer === 'blur') {
|
|
425
477
|
// Read after the blur settles: some browsers have not moved
|
|
426
478
|
// focus into the frame yet when the window blurs.
|
|
427
|
-
|
|
479
|
+
const frame = classifyPaymentFrame(
|
|
480
|
+
document.activeElement,
|
|
481
|
+
window
|
|
482
|
+
);
|
|
483
|
+
kind = frame ? frame.kind : undefined;
|
|
484
|
+
family = frame ? frame.family : undefined;
|
|
428
485
|
source = 'frame';
|
|
429
486
|
} else if (
|
|
430
487
|
unconfirmedCommit &&
|
|
@@ -444,10 +501,17 @@ export class ButtonTracker {
|
|
|
444
501
|
? !click.checked
|
|
445
502
|
: box.checked;
|
|
446
503
|
}
|
|
447
|
-
this.logClick(click, pathname);
|
|
504
|
+
this.logClick(click, pathname, asLink);
|
|
505
|
+
}
|
|
506
|
+
// Ahead of the intent, so a pre-selection it fires goes out
|
|
507
|
+
// with this pick.
|
|
508
|
+
if (choice && isPicked(choice.radio)) {
|
|
509
|
+
this.pickPayment(t, pathname, choice.names);
|
|
510
|
+
} else if (family) {
|
|
511
|
+
this.pickPayment(t, pathname, undefined, family);
|
|
448
512
|
}
|
|
449
513
|
if (kind) {
|
|
450
|
-
this.emit(kind, source, layer, t, pathname);
|
|
514
|
+
this.emit(kind, source, layer, t, pathname, adds);
|
|
451
515
|
}
|
|
452
516
|
} catch (e) {
|
|
453
517
|
this.log('[error] delivery threw: ' + e);
|
|
@@ -456,8 +520,12 @@ export class ButtonTracker {
|
|
|
456
520
|
}
|
|
457
521
|
|
|
458
522
|
// Every real click is one event, repeats included, up to a ceiling per page.
|
|
459
|
-
private logClick(
|
|
460
|
-
|
|
523
|
+
private logClick(
|
|
524
|
+
click: IButtonClick,
|
|
525
|
+
pathname: string,
|
|
526
|
+
asLink: boolean
|
|
527
|
+
): void {
|
|
528
|
+
if (!this.eventsEnabled() || this.hasTargetingOptOut()) {
|
|
461
529
|
return;
|
|
462
530
|
}
|
|
463
531
|
if (pathname !== this.pagePath) {
|
|
@@ -472,7 +540,9 @@ export class ButtonTracker {
|
|
|
472
540
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
473
541
|
const queued = this.queuedForUpload();
|
|
474
542
|
this.mpInstance._Events.logEvent(
|
|
475
|
-
|
|
543
|
+
asLink
|
|
544
|
+
? buildLinkClickEvent(click, pathname)
|
|
545
|
+
: buildButtonClickEvent(click, pathname)
|
|
476
546
|
);
|
|
477
547
|
// Only a click waiting for the batch interval is sent early: a held
|
|
478
548
|
// one goes out when released, and with batching off it already has.
|
|
@@ -525,7 +595,8 @@ export class ButtonTracker {
|
|
|
525
595
|
source: CheckoutIntentSource | undefined,
|
|
526
596
|
layer: string,
|
|
527
597
|
t: number,
|
|
528
|
-
pathname: string
|
|
598
|
+
pathname: string,
|
|
599
|
+
adds?: true
|
|
529
600
|
): void {
|
|
530
601
|
if (this.isKitSuppressed()) {
|
|
531
602
|
return;
|
|
@@ -534,24 +605,62 @@ export class ButtonTracker {
|
|
|
534
605
|
this.lastCommitClick = t;
|
|
535
606
|
}
|
|
536
607
|
|
|
537
|
-
|
|
608
|
+
// An add-to-cart click throttles apart from a click that opens the
|
|
609
|
+
// cart, so opening the cart first never drops the add.
|
|
610
|
+
const key = adds ? kind + ':adds' : kind;
|
|
611
|
+
const last = this.lastSent[key];
|
|
538
612
|
if (last && last.pathname === pathname && t - last.t < THROTTLE_MS) {
|
|
539
613
|
return;
|
|
540
614
|
}
|
|
541
|
-
this.lastSent[
|
|
615
|
+
this.lastSent[key] = { t, pathname };
|
|
542
616
|
|
|
543
617
|
this.mpInstance._RoktManager.notifyCheckoutIntent(
|
|
544
|
-
Object.freeze(
|
|
618
|
+
Object.freeze(
|
|
619
|
+
adds
|
|
620
|
+
? { kind, t, source, pathname, adds }
|
|
621
|
+
: { kind, t, source, pathname }
|
|
622
|
+
),
|
|
545
623
|
pathname
|
|
546
624
|
);
|
|
547
625
|
}
|
|
548
626
|
|
|
627
|
+
private pickPayment(
|
|
628
|
+
t: number,
|
|
629
|
+
pathname: string,
|
|
630
|
+
names?: string[],
|
|
631
|
+
family?: PaymentFrameFamily
|
|
632
|
+
): void {
|
|
633
|
+
if (this.isKitSuppressed()) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
this.mpInstance._RoktManager.notifyPaymentPick(
|
|
637
|
+
Object.freeze({
|
|
638
|
+
t,
|
|
639
|
+
pathname,
|
|
640
|
+
names: names ? Object.freeze(names.slice()) : undefined,
|
|
641
|
+
family,
|
|
642
|
+
})
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
549
646
|
// Events follow the SDK's opt-out like any other event.
|
|
550
647
|
private eventsEnabled(): boolean {
|
|
551
648
|
const store = this.mpInstance._Store;
|
|
552
649
|
return !!store && store.isEnabled;
|
|
553
650
|
}
|
|
554
651
|
|
|
652
|
+
// Read per click, since the launcher options can arrive after the tracker
|
|
653
|
+
// starts. Only where this init configures a Rokt kit, so Global Privacy
|
|
654
|
+
// Control alone doesn't stop click logging elsewhere.
|
|
655
|
+
private hasTargetingOptOut(): boolean {
|
|
656
|
+
const roktManager = this.mpInstance._RoktManager;
|
|
657
|
+
return (
|
|
658
|
+
this.hasRoktKit &&
|
|
659
|
+
!!roktManager &&
|
|
660
|
+
roktManager.hasTargetingOptOut()
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
555
664
|
// The Rokt manager owns the kit's privacy gates: flag, opt-out, launcher
|
|
556
665
|
// options and Global Privacy Control.
|
|
557
666
|
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.
|
|
215
|
-
|
|
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.
|
|
233
|
+
function cartControlAction(control: Element, cores: string[]): 'add' | 'open' | undefined {
|
|
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 'add';
|
|
238
|
+
}
|
|
239
|
+
return (control.hasAttribute('aria-expanded') || control.hasAttribute('aria-haspopup')) &&
|
|
240
|
+
texts.some(text => openCartLabels.test(text))
|
|
241
|
+
? 'open'
|
|
242
|
+
: undefined;
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
// Which rule recognised the intent: a vendor's own markup, a phrase in the
|
|
@@ -233,6 +258,8 @@ export type CheckoutIntentSource =
|
|
|
233
258
|
export interface ICheckoutIntent {
|
|
234
259
|
kind: Exclude<CheckoutIntentKind, 'frame'>;
|
|
235
260
|
source: CheckoutIntentSource;
|
|
261
|
+
// Set on a cart click that puts something in the cart, not one that opens it.
|
|
262
|
+
adds?: true;
|
|
236
263
|
}
|
|
237
264
|
|
|
238
265
|
// "Buy now" and "Purchase" count only on a checkout route, and never on a plain
|
|
@@ -252,8 +279,8 @@ function isCommitLabel(text: string, isLink: boolean, pathname: string): boolean
|
|
|
252
279
|
// pickers, toggles and pay-later messages never count; a vendor's own wallet
|
|
253
280
|
// button counts before any label rule; a confirm label on any source counts
|
|
254
281
|
// before the look-alike exclusions veto; then a wallet-named control, wallet
|
|
255
|
-
// phrases and steps.
|
|
256
|
-
// those do.
|
|
282
|
+
// phrases and steps. An add-to-cart label, links included, or a control that
|
|
283
|
+
// opens the cart on the page counts only where none of those do.
|
|
257
284
|
export function classifyIntent(
|
|
258
285
|
control: Element,
|
|
259
286
|
pathname: string
|
|
@@ -269,9 +296,13 @@ export function classifyIntent(
|
|
|
269
296
|
}
|
|
270
297
|
const texts = labels(control);
|
|
271
298
|
const cores = texts.map(coreLabel);
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
299
|
+
const cartAction = cartControlAction(control, cores);
|
|
300
|
+
let cart: ICheckoutIntent | undefined;
|
|
301
|
+
if (cartAction === 'add') {
|
|
302
|
+
cart = { kind: 'cart', source: 'label', adds: true };
|
|
303
|
+
} else if (cartAction === 'open') {
|
|
304
|
+
cart = { kind: 'cart', source: 'label' };
|
|
305
|
+
}
|
|
275
306
|
if (isQuietIntentControl(control, false, !!cart)) {
|
|
276
307
|
return undefined;
|
|
277
308
|
}
|
|
@@ -325,12 +356,20 @@ function isOnDomain(host: string, domain: string): boolean {
|
|
|
325
356
|
);
|
|
326
357
|
}
|
|
327
358
|
|
|
359
|
+
export interface IPaymentFrame {
|
|
360
|
+
kind: 'wallet' | 'frame';
|
|
361
|
+
family?: PaymentFrameFamily;
|
|
362
|
+
}
|
|
363
|
+
|
|
328
364
|
// 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
|
-
|
|
365
|
+
// focus into hosted card fields or messages is only a frame. A family comes
|
|
366
|
+
// only from a frame that can draw no other method: PayPal's button frame also
|
|
367
|
+
// draws Pay Later, Venmo and its card form, Stripe's quick-pay frame draws
|
|
368
|
+
// Apple Pay or Google Pay by browser, and its payment form holds several.
|
|
369
|
+
export function classifyPaymentFrame(
|
|
331
370
|
target: EventTarget | null,
|
|
332
371
|
win: Window
|
|
333
|
-
):
|
|
372
|
+
): IPaymentFrame | undefined {
|
|
334
373
|
if (
|
|
335
374
|
win.document.visibilityState !== 'visible' ||
|
|
336
375
|
!(target instanceof HTMLIFrameElement)
|
|
@@ -343,20 +382,39 @@ export function classifyIntentFrame(
|
|
|
343
382
|
) {
|
|
344
383
|
return undefined;
|
|
345
384
|
}
|
|
385
|
+
// A frame that has its own src is judged by that address, never by its name.
|
|
386
|
+
if (
|
|
387
|
+
(!target.src || target.src === 'about:blank') &&
|
|
388
|
+
PAYPAL_SDK_FRAME.test(target.name)
|
|
389
|
+
) {
|
|
390
|
+
return PAYPAL_SDK_BUTTON_FRAME.test(target.name)
|
|
391
|
+
? { kind: 'wallet' }
|
|
392
|
+
: { kind: 'frame' };
|
|
393
|
+
}
|
|
346
394
|
try {
|
|
347
395
|
const url = new URL(target.src);
|
|
348
396
|
const host = url.protocol === 'https:' ? url.hostname : '';
|
|
349
397
|
if (paypalFrameHosts.has(host)) {
|
|
350
|
-
|
|
398
|
+
if (PAYPAL_BUTTON_FRAME.test(url.pathname)) {
|
|
399
|
+
return { kind: 'wallet' };
|
|
400
|
+
}
|
|
401
|
+
return PAYPAL_CARD_FRAME.test(url.pathname)
|
|
402
|
+
? { kind: 'frame', family: 'card' }
|
|
403
|
+
: { kind: 'frame' };
|
|
351
404
|
}
|
|
352
|
-
if (
|
|
353
|
-
return 'wallet';
|
|
405
|
+
if (Object.prototype.hasOwnProperty.call(walletFrameFamilies, host)) {
|
|
406
|
+
return { kind: 'wallet', family: walletFrameFamilies[host] };
|
|
354
407
|
}
|
|
355
408
|
if (host === 'js.stripe.com') {
|
|
356
|
-
|
|
409
|
+
if (STRIPE_WALLET_FRAME.test(url.pathname)) {
|
|
410
|
+
return { kind: 'wallet' };
|
|
411
|
+
}
|
|
412
|
+
return STRIPE_CARD_FRAME.test(url.pathname)
|
|
413
|
+
? { kind: 'frame', family: 'card' }
|
|
414
|
+
: { kind: 'frame' };
|
|
357
415
|
}
|
|
358
416
|
if (CARD_FRAME_DOMAINS.some(domain => isOnDomain(host, domain))) {
|
|
359
|
-
return 'frame';
|
|
417
|
+
return { kind: 'frame' };
|
|
360
418
|
}
|
|
361
419
|
} catch (e) {
|
|
362
420
|
// 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
|