@mparticle/web-sdk 3.16.1 → 3.18.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.
@@ -0,0 +1,41 @@
1
+ // Masks a button label before it is logged. Button text can carry personal data,
2
+ // so this is the privacy boundary for every label and aria-label.
3
+
4
+ // Masking needs \p{Nd} to see every script's digits. Browsers without Unicode
5
+ // property escapes send no labels at all.
6
+ function createDigitPatterns(): { run: RegExp; number: RegExp } | undefined {
7
+ try {
8
+ return {
9
+ run: new RegExp('\\p{Nd}{6,}', 'u'),
10
+ number: new RegExp('\\p{Nd}+(?:[.,،٫٬]\\p{Nd}+)*', 'gu'),
11
+ };
12
+ } catch (e) {
13
+ return undefined;
14
+ }
15
+ }
16
+
17
+ const digitPatterns = createDigitPatterns();
18
+
19
+ // The label as logged: lower case, at most 64 characters, digits and amounts
20
+ // masked. Dropped when it looks like an email, phone or card number.
21
+ export function maskLabel(raw: string | null | undefined): string | undefined {
22
+ if (!raw || raw.length > 128 || !digitPatterns) {
23
+ return undefined;
24
+ }
25
+ const text = raw
26
+ .replace(/\s+/g, ' ')
27
+ .trim()
28
+ .toLowerCase();
29
+ if (
30
+ !text ||
31
+ text.indexOf('@') !== -1 ||
32
+ digitPatterns.run.test(text.replace(/[\s()\-‐-―]/g, ''))
33
+ ) {
34
+ return undefined;
35
+ }
36
+ const masked = text
37
+ .replace(digitPatterns.number, '#')
38
+ .slice(0, 64)
39
+ .trim();
40
+ return masked || undefined;
41
+ }
package/src/constants.ts CHANGED
@@ -190,6 +190,7 @@ const Constants = {
190
190
  CaptureIntegrationSpecificIdsV2: 'captureIntegrationSpecificIds.V2',
191
191
  AstBackgroundEvents: 'astBackgroundEvents',
192
192
  AutoLogPageView: 'autoLogPageView',
193
+ AutoButtonTracking: 'autoButtonTracking',
193
194
  },
194
195
  DefaultInstance: 'default_instance',
195
196
  CCPAPurpose: 'data_sale_opt_out',
@@ -34,6 +34,7 @@ export type {
34
34
  RoktKitFilterSettings,
35
35
  IRoktKitSettings,
36
36
  IRoktKit,
37
+ ICheckoutIntentSignal,
37
38
  IRoktOptions,
38
39
  IRoktLauncherOptions,
39
40
  } from './roktManager';
@@ -66,6 +66,7 @@ import {
66
66
  PageViewTracker,
67
67
  resetPageViewTracking,
68
68
  } from './pageViewTracker';
69
+ import { ButtonTracker, resetButtonTracking } from './buttonTracking/buttonTracker';
69
70
 
70
71
  export interface IErrorLogMessage {
71
72
  message?: string;
@@ -99,6 +100,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
99
100
  _IntegrationCapture: IntegrationCapture;
100
101
  _NativeSdkHelpers: INativeSdkHelpers;
101
102
  _PageViewTracker?: PageViewTracker;
103
+ _ButtonTracker?: ButtonTracker;
102
104
  _Persistence: IPersistence;
103
105
  _CookieConsentManager: ICookieConsentManager;
104
106
  _ErrorReportingDispatcher: ErrorReportingDispatcher;
@@ -286,6 +288,10 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
286
288
  this.reset = function(instance) {
287
289
  try {
288
290
  instance._Persistence.resetPersistence();
291
+ // The Rokt manager keeps its own store reference, so stop button
292
+ // tracking here rather than relying on the deleted store.
293
+ resetButtonTracking();
294
+ instance._ButtonTracker = undefined;
289
295
  if (instance._Store) {
290
296
  delete instance._Store;
291
297
  }
@@ -311,6 +317,8 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
311
317
 
312
318
  resetPageViewTracking();
313
319
  instance._PageViewTracker = undefined;
320
+ resetButtonTracking();
321
+ instance._ButtonTracker = undefined;
314
322
 
315
323
  if (!keepPersistence) {
316
324
  instance._Persistence.resetPersistence();
@@ -1133,6 +1141,13 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1133
1141
  self._SessionManager.resetSessionTimer();
1134
1142
  self._Store.isEnabled = !isOptingOut;
1135
1143
 
1144
+ if (isOptingOut) {
1145
+ if (self._ButtonTracker) {
1146
+ self._ButtonTracker.clearPending();
1147
+ }
1148
+ self._RoktManager.discardCheckoutIntents();
1149
+ }
1150
+
1136
1151
  self._Events.logOptOut();
1137
1152
  self._Persistence.update();
1138
1153
 
@@ -1347,7 +1362,7 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1347
1362
  function completeSDKInitialization(apiKey, config, mpInstance) {
1348
1363
  const kitBlocker = createKitBlocker(config, mpInstance);
1349
1364
  const { getFeatureFlag } = mpInstance._Helpers;
1350
- const { AutoLogPageView } = Constants.FeatureFlags;
1365
+ const { AutoLogPageView, AutoButtonTracking } = Constants.FeatureFlags;
1351
1366
 
1352
1367
  // Destroy previous batch uploader to prevent leaked timers and event listeners
1353
1368
  if (mpInstance._APIClient?.uploader) {
@@ -1367,6 +1382,8 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1367
1382
  // persistence or Identity directly.
1368
1383
  if (mpInstance._Store.webviewBridgeEnabled) {
1369
1384
  mpInstance._NativeSdkHelpers.initializeSessionAttributes(apiKey);
1385
+ // Button tracking never runs as a native passthrough.
1386
+ resetButtonTracking();
1370
1387
  } else {
1371
1388
  // Main SDK initialization flow
1372
1389
 
@@ -1477,6 +1494,19 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1477
1494
  mpInstance._PageViewTracker.teardown();
1478
1495
  }
1479
1496
 
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
+ if (getFeatureFlag(AutoButtonTracking)) {
1500
+ if (!mpInstance._ButtonTracker) {
1501
+ mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
1502
+ }
1503
+ mpInstance._ButtonTracker.init({ hasRoktKit: !!roktConfig });
1504
+ } else {
1505
+ // Also retires a tracker left by an earlier bundle load. Like APV,
1506
+ // this assumes a single active SDK instance.
1507
+ resetButtonTracking();
1508
+ }
1509
+
1480
1510
  processIdentityCallback(
1481
1511
  mpInstance,
1482
1512
  currentUser,
@@ -1596,6 +1626,8 @@ function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
1596
1626
  mpInstance._LoggingDispatcher.logger = mpInstance.Logger;
1597
1627
  mpInstance._Store = new Store(config, mpInstance, apiKey);
1598
1628
  window.mParticle.Store = mpInstance._Store;
1629
+ // Button tracking stops until this init's config decides whether it runs.
1630
+ resetButtonTracking();
1599
1631
  mpInstance.Logger.verbose(StartingInitialization);
1600
1632
 
1601
1633
  // Initialize CookieConsentManager with privacy flags from launcherOptions
@@ -23,8 +23,11 @@ import { PARTNER_MODULE_IDS } from "./cookieSyncManager";
23
23
  import IntegrationCapture from "./integrationCapture";
24
24
  import Constants from "./constants";
25
25
  import { subscribeToRouteChange } from "./routeChangeMonitor";
26
+ import type { CheckoutIntentKind } from "./buttonTracking/checkoutIntent";
26
27
 
27
28
  const PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
29
+ const CHECKOUT_INTENT_BUFFER_LIMIT = 4;
30
+ const CHECKOUT_INTENT_MAX_AGE_MS = 30000;
28
31
 
29
32
  type AttributeTargetUser = Pick<IMParticleUser, 'getUserIdentities' | 'setUserAttributes'>;
30
33
 
@@ -71,6 +74,11 @@ export interface RoktKitFilterSettings {
71
74
  filteredUser?: IMParticleUser | null;
72
75
  }
73
76
 
77
+ export interface ICheckoutIntentSignal {
78
+ kind: CheckoutIntentKind;
79
+ t: number;
80
+ }
81
+
74
82
  export interface IRoktKitSettings {
75
83
  accountId?: string;
76
84
  placementAttributesMapping?: string;
@@ -93,6 +101,8 @@ export interface IRoktKit {
93
101
  // Set by the kit only when it wants route changes; nothing subscribes otherwise. The
94
102
  // manager also calls it once on attach.
95
103
  onRouteChange?: () => void;
104
+ // Set by the kit only when it wants checkout intent signals.
105
+ onCheckoutIntent?: (signal: ICheckoutIntentSignal) => void;
96
106
  launcherOptions?: Dictionary<any>;
97
107
  settings?: IRoktKitSettings;
98
108
  integrationName?: string;
@@ -138,6 +148,8 @@ export default class RoktManager {
138
148
  private initialized: boolean = false;
139
149
  private isShoppableAdsLoaded: boolean = false;
140
150
  private stopRouteChangeWatch: (() => void) | null = null;
151
+ // Checkout intent raised before the kit attached: latest per kind.
152
+ private pendingCheckoutIntents: Array<{ signal: ICheckoutIntentSignal; pathname: string }> = [];
141
153
 
142
154
  // Keys this manager's route subscription, so a re-executed bundle's manager replaces
143
155
  // it while a second named instance keeps its own.
@@ -232,6 +244,7 @@ export default class RoktManager {
232
244
  this.kit = kit;
233
245
 
234
246
  this.watchRouteChanges();
247
+ this.flushCheckoutIntents();
235
248
 
236
249
  if (kit.settings?.accountId) {
237
250
  this.store.setRoktAccountId(kit.settings.accountId);
@@ -286,6 +299,69 @@ export default class RoktManager {
286
299
  }
287
300
  }
288
301
 
302
+ // Checkout intent is off unless the flag is on, and under opt-out, any Rokt
303
+ // privacy launcher option, or Global Privacy Control.
304
+ public isCheckoutIntentSuppressed(): boolean {
305
+ const flags = this.store?.SDKConfig?.flags;
306
+ if (!flags || flags[Constants.FeatureFlags.AutoButtonTracking] !== true || !this.store.isEnabled) {
307
+ return true;
308
+ }
309
+ const options = this.launcherOptions || {};
310
+ if (options.noFunctional || options.noTargeting || options.doNotShareOrSell) {
311
+ return true;
312
+ }
313
+ const nav = typeof navigator === 'undefined' ? undefined : (navigator as Navigator & { globalPrivacyControl?: boolean });
314
+ return !!nav && nav.globalPrivacyControl === true;
315
+ }
316
+
317
+ // Reads `this.kit` at call time; before a kit attaches the signal is buffered
318
+ // with the path it was raised on.
319
+ public notifyCheckoutIntent(signal: ICheckoutIntentSignal, pathname: string = window.location.pathname): void {
320
+ if (this.isCheckoutIntentSuppressed()) {
321
+ return;
322
+ }
323
+
324
+ const kit = this.kit;
325
+ if (!kit) {
326
+ this.pendingCheckoutIntents = this.pendingCheckoutIntents
327
+ .filter(pending => pending.signal.kind !== signal.kind)
328
+ .concat({ signal, pathname })
329
+ .slice(-CHECKOUT_INTENT_BUFFER_LIMIT);
330
+ return;
331
+ }
332
+
333
+ try {
334
+ if (isFunction(kit.onCheckoutIntent)) {
335
+ kit.onCheckoutIntent(signal);
336
+ }
337
+ } catch (e) {
338
+ this.logger?.error(
339
+ `RoktManager: Error in onCheckoutIntent: ${getErrorMessage(e)}`
340
+ );
341
+ }
342
+ }
343
+
344
+ public discardCheckoutIntents(): void {
345
+ this.pendingCheckoutIntents = [];
346
+ }
347
+
348
+ // Replays recent buffered signals raised on the current page, oldest first.
349
+ private flushCheckoutIntents(): void {
350
+ const pending = this.pendingCheckoutIntents;
351
+ this.pendingCheckoutIntents = [];
352
+
353
+ const now = Date.now();
354
+ const pathname = window.location.pathname;
355
+ pending
356
+ .filter(
357
+ ({ signal, pathname: signalPathname }) =>
358
+ now - signal.t < CHECKOUT_INTENT_MAX_AGE_MS &&
359
+ signalPathname === pathname
360
+ )
361
+ .sort((a, b) => a.signal.t - b.signal.t)
362
+ .forEach(({ signal }) => this.notifyCheckoutIntent(signal));
363
+ }
364
+
289
365
  private isAutoLogPageViewEnabled(): boolean {
290
366
  return (
291
367
  this.store?.SDKConfig?.flags?.[
package/src/store.ts CHANGED
@@ -153,6 +153,7 @@ export interface IFeatureFlags {
153
153
  'captureIntegrationSpecificIds.V2'?: string;
154
154
  astBackgroundEvents?: boolean;
155
155
  autoLogPageView?: boolean;
156
+ autoButtonTracking?: boolean;
156
157
  }
157
158
 
158
159
  // Temporary Interface until Store can be refactored as a class
@@ -776,7 +777,8 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
776
777
  CaptureIntegrationSpecificIds,
777
778
  CaptureIntegrationSpecificIdsV2,
778
779
  AstBackgroundEvents,
779
- AutoLogPageView
780
+ AutoLogPageView,
781
+ AutoButtonTracking,
780
782
  } = Constants.FeatureFlags;
781
783
 
782
784
  if (!config.flags) {
@@ -798,6 +800,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
798
800
  flags[CaptureIntegrationSpecificIdsV2] = (config.flags[CaptureIntegrationSpecificIdsV2] || 'none');
799
801
  flags[AstBackgroundEvents] = config.flags[AstBackgroundEvents] === 'True';
800
802
  flags[AutoLogPageView] = config.flags[AutoLogPageView] === 'True';
803
+ flags[AutoButtonTracking] = config.flags[AutoButtonTracking] === 'True';
801
804
  return flags;
802
805
  }
803
806