@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.
Files changed (39) hide show
  1. package/dist/mparticle.common.js +1 -1688
  2. package/dist/mparticle.esm.js +1 -1688
  3. package/dist/mparticle.js +1015 -253
  4. package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +7 -0
  5. package/dist/types/src/buttonTracking/buttonDescription.d.ts +10 -3
  6. package/dist/types/src/buttonTracking/buttonTracker.d.ts +8 -0
  7. package/dist/types/src/buttonTracking/checkoutIntent.d.ts +8 -3
  8. package/dist/types/src/buttonTracking/controls.d.ts +8 -4
  9. package/dist/types/src/buttonTracking/labelMasking.d.ts +1 -0
  10. package/dist/types/src/configAPIClient.d.ts +1 -0
  11. package/dist/types/src/constants.d.ts +1 -0
  12. package/dist/types/src/cookieConsentManager.d.ts +17 -3
  13. package/dist/types/src/forwarder-utils.d.ts +3 -0
  14. package/dist/types/src/pageViewTracker.d.ts +19 -3
  15. package/dist/types/src/roktLauncherOptions.d.ts +1 -0
  16. package/dist/types/src/roktManager.d.ts +13 -2
  17. package/dist/types/src/sdkRuntimeModels.d.ts +1 -0
  18. package/dist/types/src/store.d.ts +3 -0
  19. package/package.json +21 -14
  20. package/src/buttonTracking/buttonClickEvent.ts +126 -8
  21. package/src/buttonTracking/buttonDescription.ts +336 -53
  22. package/src/buttonTracking/buttonTracker.ts +231 -19
  23. package/src/buttonTracking/checkoutIntent.ts +117 -23
  24. package/src/buttonTracking/controls.ts +169 -55
  25. package/src/buttonTracking/labelMasking.ts +4 -1
  26. package/src/configAPIClient.ts +1 -0
  27. package/src/constants.ts +1 -0
  28. package/src/cookieConsentManager.ts +23 -3
  29. package/src/cookieSyncManager.ts +11 -3
  30. package/src/events.ts +9 -3
  31. package/src/forwarder-utils.ts +38 -0
  32. package/src/forwarders.ts +2 -28
  33. package/src/mp-instance.ts +41 -7
  34. package/src/pageUrl.ts +43 -2
  35. package/src/pageViewTracker.ts +193 -111
  36. package/src/roktLauncherOptions.ts +1 -0
  37. package/src/roktManager.ts +66 -11
  38. package/src/sdkRuntimeModels.ts +1 -0
  39. 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
- // closest() over the composed tree: slots, parents, then shadow hosts.
10
- export function hasComposedAncestor(
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
- selector: string
28
+ test: (node: Element) => boolean,
29
+ until?: Element
13
30
  ): boolean {
14
31
  let node: Element | null = element;
15
- while (node) {
16
- if (node.matches(selector)) {
32
+ while (node && node !== until) {
33
+ if (test(node)) {
17
34
  return true;
18
35
  }
19
- const root = isFunction(node.getRootNode)
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 vendorButtonKind(element) === 'wallet';
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 keeps its own name.
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
- // Walks at most 12 entries of an event's composed path for the nearest control.
153
- // One composed walk from the target rules out Rokt elements and anything under
154
- // data-mp-no-track; disabled controls are skipped.
155
- export function findControl(
156
- path: Array<EventTarget | null>
157
- ): Element | undefined {
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
- for (const target of entries) {
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
- !control &&
183
- target.matches(CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON)
302
+ target.matches(CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON) ||
303
+ (!picked && isWalletNamedControl(target))
184
304
  ) {
185
305
  control = target;
186
- } else if (!control && !option && target.matches(OPTION_INPUT)) {
187
- option = target;
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 (!control || control.matches(':disabled, [aria-disabled="true"]')) {
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
- 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;
202
329
  return (
203
330
  element.matches(
204
331
  'input, select, textarea, option, optgroup, datalist, ' + EDITABLE
205
332
  ) ||
206
- !!element.querySelector(
207
- 'input, select, textarea, [data-mp-no-track], ' + EDITABLE
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 > 128 || !digitPatterns) {
25
+ if (!raw || raw.length > MAX_RAW_LABEL_LENGTH || !digitPatterns) {
23
26
  return undefined;
24
27
  }
25
28
  const text = raw
@@ -78,6 +78,7 @@ export interface IConfigResponse {
78
78
  workspaceToken: string;
79
79
  pixelConfigs: IPixelConfiguration[];
80
80
  flags: SDKEventCustomFlags;
81
+ autoLogPageViewQueryParams?: string;
81
82
  }
82
83
 
83
84
  export interface IConfigAPIClient {
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;
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 — and this is the one that matters for campaign
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
- ...allowedQueryParams(getHref()),
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),
@@ -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
- isBlockedByForwardingRule,
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,
@@ -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 { AutoLogPageView, AutoButtonTracking } = Constants.FeatureFlags;
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
- // Button clicks are logged for any workspace that opts in. Checkout
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({ hasRoktKit: !!roktConfig });
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