@mparticle/web-sdk 3.22.0 → 3.24.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 (37) hide show
  1. package/dist/mparticle.common.js +1 -1669
  2. package/dist/mparticle.esm.js +1 -1669
  3. package/dist/mparticle.js +4827 -4184
  4. package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +4 -0
  5. package/dist/types/src/buttonTracking/buttonDescription.d.ts +2 -2
  6. package/dist/types/src/buttonTracking/buttonTracker.d.ts +2 -0
  7. package/dist/types/src/buttonTracking/checkoutIntent.d.ts +2 -2
  8. package/dist/types/src/buttonTracking/controls.d.ts +6 -3
  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/forwarder-utils.d.ts +3 -0
  12. package/dist/types/src/pageUrl.d.ts +10 -0
  13. package/dist/types/src/pageViewTracker.d.ts +21 -3
  14. package/dist/types/src/roktManager.d.ts +8 -2
  15. package/dist/types/src/sdkRuntimeModels.d.ts +3 -0
  16. package/dist/types/src/store.d.ts +2 -0
  17. package/package.json +21 -14
  18. package/src/batchUploader.ts +3 -2
  19. package/src/buttonTracking/buttonClickEvent.ts +76 -1
  20. package/src/buttonTracking/buttonDescription.ts +236 -46
  21. package/src/buttonTracking/buttonTracker.ts +122 -7
  22. package/src/buttonTracking/checkoutIntent.ts +51 -9
  23. package/src/buttonTracking/controls.ts +160 -50
  24. package/src/buttonTracking/labelMasking.ts +4 -1
  25. package/src/configAPIClient.ts +1 -0
  26. package/src/events.ts +9 -3
  27. package/src/forwarder-utils.ts +38 -0
  28. package/src/forwarders.ts +2 -28
  29. package/src/mp-instance.ts +23 -10
  30. package/src/mparticle-instance-manager.ts +3 -0
  31. package/src/pageUrl.ts +191 -0
  32. package/src/pageViewTracker.ts +206 -78
  33. package/src/roktManager.ts +53 -20
  34. package/src/sdkRuntimeModels.ts +3 -0
  35. package/src/serverModel.ts +3 -3
  36. package/src/store.ts +14 -0
  37. package/src/utils.ts +1 -1
@@ -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
+ 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,35 +265,62 @@ 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
325
  // fields, editable regions, and anything under data-mp-no-track.
201
326
  export function isUnsafeTextSource(element: Element): boolean {
@@ -212,18 +337,3 @@ export function isUnsafeTextSource(element: Element): boolean {
212
337
  )
213
338
  );
214
339
  }
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/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,
@@ -45,7 +45,7 @@ import { removeExpiredIdentityCacheDates, hasExplicitIdentifier } from './identi
45
45
  import IntegrationCapture, { isIntegrationCaptureEnabled } from './integrationCapture';
46
46
  import { IPreInit, processReadyQueue } from './pre-init-utils';
47
47
  import { BaseEvent, MParticleWebSDK, SDKHelpersApi } from './sdkRuntimeModels';
48
- import { Dictionary, SDKEventAttrs } from '@mparticle/web-sdk';
48
+ import { Dictionary, MPID, SDKEventAttrs } from '@mparticle/web-sdk';
49
49
  import { IIdentity } from './identity.interfaces';
50
50
  import { IEvents } from './events.interfaces';
51
51
  import { IECommerce } from './ecommerce.interfaces';
@@ -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;
@@ -1384,6 +1385,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1384
1385
  mpInstance._NativeSdkHelpers.initializeSessionAttributes(apiKey);
1385
1386
  // Button tracking never runs as a native passthrough.
1386
1387
  resetButtonTracking();
1388
+ mpInstance._RoktManager.setAutoButtonClicksForwarded(false);
1387
1389
  } else {
1388
1390
  // Main SDK initialization flow
1389
1391
 
@@ -1424,12 +1426,8 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1424
1426
  if (roktConfig) {
1425
1427
  const accountId = roktConfig.settings?.accountId;
1426
1428
  mpInstance._Store.setRoktAccountId(accountId);
1427
- const { userAttributeFilters } = roktConfig;
1428
- const roktFilteredUser = filteredMparticleUser(
1429
- currentUserMPID,
1430
- { userAttributeFilters },
1431
- mpInstance
1432
- );
1429
+ const createRoktFilteredUser = (mpid: MPID) =>
1430
+ filteredMparticleUser(mpid, roktConfig, mpInstance, kitBlocker);
1433
1431
  const roktOptions: IRoktOptions = {
1434
1432
  sandbox: config?.isDevelopmentMode,
1435
1433
  launcherOptions: config?.launcherOptions,
@@ -1438,7 +1436,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1438
1436
  // https://go.mparticle.com/work/SQDSDKS-7339
1439
1437
  mpInstance._RoktManager.init(
1440
1438
  roktConfig,
1441
- roktFilteredUser,
1439
+ createRoktFilteredUser(currentUserMPID),
1442
1440
  mpInstance.Identity,
1443
1441
  mpInstance._Store,
1444
1442
  mpInstance.Logger,
@@ -1447,9 +1445,26 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1447
1445
  mpInstance._ErrorReportingDispatcher,
1448
1446
  mpInstance._LoggingDispatcher,
1449
1447
  mpInstance._IntegrationCapture,
1448
+ createRoktFilteredUser,
1450
1449
  );
1451
1450
  }
1452
1451
 
1452
+ // Checkout intent comes from the shopper's clicks, so it goes to the
1453
+ // Rokt kit only when that kit receives them. Set before
1454
+ // processForwarders: the kit can read it during this init, from its own
1455
+ // init or from a returning shopper's landing page view.
1456
+ const buttonClickBlocker = roktConfig
1457
+ ? getButtonClickBlocker(roktConfig, kitBlocker)
1458
+ : undefined;
1459
+ if (buttonClickBlocker && getFeatureFlag(AutoButtonTracking)) {
1460
+ mpInstance.Logger.warning(
1461
+ `Checkout intent is off: the ${buttonClickBlocker} keeps some or all Auto Button Click events from the Rokt kit.`
1462
+ );
1463
+ }
1464
+ mpInstance._RoktManager.setAutoButtonClicksForwarded(
1465
+ !!roktConfig && !buttonClickBlocker
1466
+ );
1467
+
1453
1468
  mpInstance._Forwarders.processForwarders(
1454
1469
  config,
1455
1470
  mpInstance._APIClient.prepareForwardingStats
@@ -1494,8 +1509,6 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1494
1509
  mpInstance._PageViewTracker.teardown();
1495
1510
  }
1496
1511
 
1497
- // Button clicks are logged for any workspace that opts in. Checkout
1498
- // intent also goes to the Rokt kit when this init configures one.
1499
1512
  if (getFeatureFlag(AutoButtonTracking)) {
1500
1513
  if (!mpInstance._ButtonTracker) {
1501
1514
  mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
@@ -7,6 +7,7 @@ import MPSideloadedKit from './sideloadedKit';
7
7
  import { IMParticleInstanceManager } from './sdkRuntimeModels';
8
8
  import { IStore } from './store';
9
9
  import { Dictionary } from '@mparticle/web-sdk';
10
+ import { cleanUrl } from './pageUrl';
10
11
 
11
12
  if (!Array.prototype.forEach) {
12
13
  Array.prototype.forEach = Polyfill.forEach;
@@ -449,6 +450,8 @@ function mParticleInstanceManager(this: IMParticleInstanceManager) {
449
450
  this.generateHash = function(string) {
450
451
  return self.getInstance().generateHash(string);
451
452
  };
453
+ // Kits clean every URL they send with the same rule as core events.
454
+ this._cleanUrl = cleanUrl;
452
455
  this.addForwarder = function(forwarder) {
453
456
  self.getInstance().addForwarder(forwarder);
454
457
  };