@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.
@@ -1,12 +1,20 @@
1
1
  import { IMParticleWebSDKInstance } from '../mp-instance';
2
2
  import { isFunction } from '../utils';
3
- import { buildButtonClickEvent } from './buttonClickEvent';
4
- import { describeButtonClick, IButtonClick } from './buttonDescription';
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
- classifyIntentFrame,
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 every button click as an mParticle event, and hands checkout intent to the
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 (!this.eventsEnabled() && this.isKitSuppressed()) {
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 = layer === 'click' && this.eventsEnabled();
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
- click = logsEvents
388
- ? describeButtonClick(control, path)
389
- : undefined;
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
- kind = classifyIntentFrame(document.activeElement, window);
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(click: IButtonClick, pathname: string): void {
460
- if (!this.eventsEnabled()) {
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
- buildButtonClickEvent(click, pathname)
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", "View cart", "Shopping bag", "Bag", and a count badge such as
86
- // "1 item in bag", which reads "item in bag" once digits are stripped.
87
- const cartLabels = new RegExp(
88
- '^(?:add(?: [^ ]+){0,5} to ' + CART_NOUN +
89
- '|(?:(?:view|open|go to) )?' + CART_NOUN + '(?: items?)?' +
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 walletFrameHosts = new Set([
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
- return (title ? [coreLabel(title)] : cores).some(text => cartLabels.test(text));
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. A cart label, links included, counts only where none of
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
- export function classifyIntentFrame(
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
- ): 'wallet' | 'frame' | undefined {
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
- return PAYPAL_BUTTON_FRAME.test(url.pathname) ? 'wallet' : 'frame';
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 (walletFrameHosts.has(host)) {
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
- return STRIPE_WALLET_FRAME.test(url.pathname) ? 'wallet' : 'frame';
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
- export function isUnsafeTextSource(element: Element): boolean {
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
- !!element.querySelector(
332
- 'input, select, textarea, [data-mp-no-track], ' + EDITABLE
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: Both flags default to false, meaning all features are allowed
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
 
@@ -118,9 +118,17 @@ export default function CookieSyncManager(
118
118
  return;
119
119
  }
120
120
 
121
- // For Rokt, block cookie sync when noTargeting privacy flag is true
122
- if (moduleId === PARTNER_MODULE_IDS.Rokt && mpInstance._CookieConsentManager.getNoTargeting()) {
123
- return;
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;
@@ -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 { AutoLogPageView, AutoButtonTracking } = Constants.FeatureFlags;
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
- if (buttonClickBlocker && getFeatureFlag(AutoButtonTracking)) {
1467
+ const linkClickBlocker =
1468
+ roktConfig && logLinks
1469
+ ? getButtonClickBlocker(roktConfig, kitBlocker, true)
1470
+ : undefined;
1471
+ if (logButtons && (buttonClickBlocker || linkClickBlocker)) {
1460
1472
  mpInstance.Logger.warning(
1461
- `Checkout intent is off: the ${buttonClickBlocker} keeps some or all Auto Button Click events from the Rokt kit.`
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 (getFeatureFlag(AutoButtonTracking)) {
1526
+ if (logButtons || logLinks) {
1513
1527
  if (!mpInstance._ButtonTracker) {
1514
1528
  mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
1515
1529
  }
1516
- mpInstance._ButtonTracker.init({ hasRoktKit: !!roktConfig });
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
@@ -5,6 +5,7 @@ export interface IRoktLauncherOptions extends Dictionary<any> {
5
5
  noDeviceID?: boolean;
6
6
  noFunctional?: boolean;
7
7
  noTargeting?: boolean;
8
+ doNotShareOrSell?: boolean;
8
9
  sessionId?: string;
9
10
  sessionToken?: string;
10
11
  }