@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.
- package/README.md +12 -6
- package/dist/mparticle.common.js +109 -39
- package/dist/mparticle.esm.js +109 -39
- package/dist/mparticle.js +853 -32
- package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +5 -0
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +14 -0
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +36 -0
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +3 -0
- package/dist/types/src/buttonTracking/controls.d.ts +5 -0
- package/dist/types/src/buttonTracking/labelMasking.d.ts +1 -0
- package/dist/types/src/constants.d.ts +1 -0
- package/dist/types/src/internal-types.d.ts +1 -1
- package/dist/types/src/mp-instance.d.ts +2 -0
- package/dist/types/src/roktManager.d.ts +11 -0
- package/dist/types/src/store.d.ts +1 -0
- package/package.json +4 -3
- package/src/buttonTracking/buttonClickEvent.ts +49 -0
- package/src/buttonTracking/buttonDescription.ts +244 -0
- package/src/buttonTracking/buttonTracker.ts +376 -0
- package/src/buttonTracking/checkoutIntent.ts +200 -0
- package/src/buttonTracking/controls.ts +92 -0
- package/src/buttonTracking/labelMasking.ts +41 -0
- package/src/constants.ts +1 -0
- package/src/internal-types.ts +1 -0
- package/src/mp-instance.ts +33 -1
- package/src/roktManager.ts +76 -0
- package/src/store.ts +4 -1
|
@@ -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',
|
package/src/internal-types.ts
CHANGED
package/src/mp-instance.ts
CHANGED
|
@@ -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
|
package/src/roktManager.ts
CHANGED
|
@@ -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
|
|