@mparticle/web-sdk 3.17.0 → 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,376 @@
1
+ import { IMParticleWebSDKInstance } from '../mp-instance';
2
+ import { isFunction } from '../utils';
3
+ import { buildButtonClickEvent } from './buttonClickEvent';
4
+ import { describeButtonClick, IButtonClick } from './buttonDescription';
5
+ import {
6
+ CheckoutIntentKind,
7
+ classifyIntentControl,
8
+ classifyIntentFrame,
9
+ } from './checkoutIntent';
10
+
11
+ // State lives on `window`, like APV, so a re-executed bundle never doubles the
12
+ // listeners. Inspect `window.__mpBtn__` in a console to see if one is live.
13
+ export const WIN_BTN_KEY = '__mpBtn__';
14
+
15
+ const THROTTLE_MS = 2000;
16
+ const SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
17
+ const PATH_LIMIT = 12;
18
+ const EVENT_DEDUP_MS = 1000;
19
+ const PAGE_EVENT_LIMIT = 50;
20
+
21
+ interface IBtnState {
22
+ tracker?: ButtonTracker;
23
+ }
24
+
25
+ type WindowWithBtn = Window & {
26
+ [WIN_BTN_KEY]?: IBtnState;
27
+ };
28
+
29
+ export interface IButtonTrackerOptions {
30
+ // The Rokt kit signal only runs when this init configures a Rokt kit.
31
+ hasRoktKit: boolean;
32
+ }
33
+
34
+ const btnWindow = (): WindowWithBtn | null =>
35
+ typeof window === 'undefined' ? null : (window as WindowWithBtn);
36
+
37
+ export const getActiveButtonTracker = (): ButtonTracker | undefined => {
38
+ const win = btnWindow();
39
+ const state = win ? win[WIN_BTN_KEY] : undefined;
40
+ return state ? state.tracker : undefined;
41
+ };
42
+
43
+ // Stops whichever tracker is active, including one from an earlier bundle load.
44
+ export const resetButtonTracking = (): void => {
45
+ const win = btnWindow();
46
+ if (!win) {
47
+ return;
48
+ }
49
+ const active = getActiveButtonTracker();
50
+ if (active) {
51
+ active.teardown();
52
+ }
53
+ win[WIN_BTN_KEY] = {};
54
+ };
55
+
56
+ // Runs once the browser has painted the page's response to the interaction, so
57
+ // describing and logging a click never delays that paint.
58
+ const afterNextPaint = (fn: () => void): void => {
59
+ if (
60
+ isFunction(window.requestAnimationFrame) &&
61
+ document.visibilityState !== 'hidden'
62
+ ) {
63
+ requestAnimationFrame(() => {
64
+ setTimeout(fn, 0);
65
+ });
66
+ } else {
67
+ setTimeout(fn, 0);
68
+ }
69
+ };
70
+
71
+ const CAPTURE_PASSIVE: AddEventListenerOptions = {
72
+ capture: true,
73
+ passive: true,
74
+ };
75
+ const PASSIVE: AddEventListenerOptions = { passive: true };
76
+
77
+ // Logs every button click as an mParticle event, and hands checkout intent to the
78
+ // Rokt kit as a frozen {kind, t}. Submits and wallet frame focus feed the kit only.
79
+ export class ButtonTracker {
80
+ private readonly mpInstance: IMParticleWebSDKInstance;
81
+ private active = false;
82
+ private hasRoktKit = false;
83
+ // Bumped on reset so classification queued before it is dropped.
84
+ private generation = 0;
85
+ private lastCommitClick = -Infinity;
86
+ private lastSent: { [kind: string]: { t: number; pathname: string } } = {};
87
+ private pagePath: string | null = null;
88
+ private pageEvents = 0;
89
+ private lastEvent: { [key: string]: number } = {};
90
+ // Clicks read at dispatch and waiting for the next paint.
91
+ private pending: Array<() => void> = [];
92
+
93
+ constructor(mpInstance: IMParticleWebSDKInstance) {
94
+ this.mpInstance = mpInstance;
95
+ }
96
+
97
+ public get isActive(): boolean {
98
+ return this.active;
99
+ }
100
+
101
+ public init(options: IButtonTrackerOptions): void {
102
+ const win = btnWindow();
103
+ if (!win) {
104
+ return;
105
+ }
106
+
107
+ const previous = getActiveButtonTracker();
108
+ if (previous) {
109
+ previous.teardown();
110
+ }
111
+ if (this.active) {
112
+ this.teardown();
113
+ }
114
+ this.hasRoktKit = options.hasRoktKit;
115
+
116
+ if (!this.eventsEnabled() && this.isKitSuppressed()) {
117
+ this.log('opted out, not starting');
118
+ return;
119
+ }
120
+
121
+ win.document.addEventListener('click', this.onEvent, CAPTURE_PASSIVE);
122
+ win.document.addEventListener('submit', this.onEvent, CAPTURE_PASSIVE);
123
+ win.addEventListener('blur', this.onEvent, PASSIVE);
124
+ win.addEventListener('pageshow', this.onPageShow, PASSIVE);
125
+ win.addEventListener('beforeunload', this.onHide, CAPTURE_PASSIVE);
126
+ win.addEventListener('pagehide', this.onHide, CAPTURE_PASSIVE);
127
+ win.document.addEventListener(
128
+ 'visibilitychange',
129
+ this.onHide,
130
+ CAPTURE_PASSIVE
131
+ );
132
+
133
+ this.active = true;
134
+ if (!win[WIN_BTN_KEY]) {
135
+ win[WIN_BTN_KEY] = {};
136
+ }
137
+ win[WIN_BTN_KEY].tracker = this;
138
+ this.log('listening');
139
+ }
140
+
141
+ public teardown(): void {
142
+ const win = btnWindow();
143
+ if (win) {
144
+ win.document.removeEventListener('click', this.onEvent, true);
145
+ win.document.removeEventListener('submit', this.onEvent, true);
146
+ win.removeEventListener('blur', this.onEvent);
147
+ win.removeEventListener('pageshow', this.onPageShow);
148
+ win.removeEventListener('beforeunload', this.onHide, true);
149
+ win.removeEventListener('pagehide', this.onHide, true);
150
+ win.document.removeEventListener(
151
+ 'visibilitychange',
152
+ this.onHide,
153
+ true
154
+ );
155
+
156
+ const state = win[WIN_BTN_KEY];
157
+ if (state && state.tracker === this) {
158
+ state.tracker = undefined;
159
+ }
160
+ }
161
+ this.active = false;
162
+ // Signals not yet handed to a kit go with the tracker.
163
+ this.clearPending();
164
+ }
165
+
166
+ // Drops queued classification, commit timing and buffered signals, so
167
+ // nothing captured before an opt-out is delivered after an opt-in.
168
+ public clearPending(): void {
169
+ this.reset();
170
+ const roktManager = this.mpInstance._RoktManager;
171
+ if (roktManager) {
172
+ roktManager.discardCheckoutIntents();
173
+ }
174
+ }
175
+
176
+ private reset(): void {
177
+ this.generation++;
178
+ this.pending = [];
179
+ this.lastCommitClick = -Infinity;
180
+ this.lastSent = {};
181
+ this.pagePath = null;
182
+ this.pageEvents = 0;
183
+ this.lastEvent = {};
184
+ }
185
+
186
+ private enqueue(delivery: () => void): void {
187
+ this.pending.push(delivery);
188
+ if (this.pending.length === 1) {
189
+ afterNextPaint(this.drain);
190
+ }
191
+ }
192
+
193
+ private readonly drain = (): void => {
194
+ const queue = this.pending;
195
+ this.pending = [];
196
+ queue.forEach(delivery => delivery());
197
+ };
198
+
199
+ // A click that navigates away can unload the page before the next paint, so
200
+ // anything waiting is delivered as the page is hidden or left. The capture
201
+ // listeners run before the SDK's own upload on the same events, including
202
+ // beforeunload, the first of them.
203
+ private readonly onHide = (event: Event): void => {
204
+ if (
205
+ event.type === 'beforeunload' ||
206
+ event.type === 'pagehide' ||
207
+ document.visibilityState === 'hidden'
208
+ ) {
209
+ this.drain();
210
+ }
211
+ };
212
+
213
+ private readonly onPageShow = (event: PageTransitionEvent): void => {
214
+ // A page restored from the back-forward cache starts fresh.
215
+ if (event.persisted) {
216
+ this.reset();
217
+ }
218
+ };
219
+
220
+ private readonly onEvent = (event: Event): void => {
221
+ try {
222
+ this.observe(event);
223
+ } catch (e) {
224
+ this.log('[error] event handler threw: ' + e);
225
+ }
226
+ };
227
+
228
+ private observe(event: Event): void {
229
+ if (!this.active || !event.isTrusted) {
230
+ return;
231
+ }
232
+ const layer = event.type;
233
+ const logsEvents = layer === 'click' && this.eventsEnabled();
234
+ if (!logsEvents && this.isKitSuppressed()) {
235
+ return;
236
+ }
237
+
238
+ const t = Date.now();
239
+ const pathname = window.location.pathname;
240
+
241
+ // The button is read now, while the click is still being dispatched:
242
+ // this capture listener runs before the page's own handlers, so a
243
+ // button that disables or relabels itself is seen as it was clicked.
244
+ // Logging and the kit hand-off wait for the next paint.
245
+ let click: IButtonClick | undefined;
246
+ let kind: CheckoutIntentKind | undefined;
247
+ let unconfirmedCommit = false;
248
+ if (layer === 'click') {
249
+ // The composed path is only populated during dispatch.
250
+ const path = isFunction(event.composedPath)
251
+ ? event.composedPath().slice(0, PATH_LIMIT)
252
+ : [];
253
+ click = logsEvents ? describeButtonClick(path) : undefined;
254
+ kind = classifyIntentControl(path, pathname);
255
+ } else if (layer === 'submit') {
256
+ const submitter = (event as Event & { submitter?: Element | null })
257
+ .submitter;
258
+ // A form submitted without a submitter only counts right after a
259
+ // commit click, which is known once that click is delivered.
260
+ kind = submitter
261
+ ? classifyIntentControl([submitter], pathname)
262
+ : 'commit';
263
+ unconfirmedCommit = !submitter;
264
+ }
265
+
266
+ const generation = this.generation;
267
+ this.enqueue(() => {
268
+ if (generation !== this.generation) {
269
+ return;
270
+ }
271
+ try {
272
+ if (layer === 'blur') {
273
+ // Read after the blur settles: some browsers have not moved
274
+ // focus into the frame yet when the window blurs.
275
+ kind = classifyIntentFrame(document.activeElement, window);
276
+ } else if (
277
+ unconfirmedCommit &&
278
+ t - this.lastCommitClick > SUBMIT_AFTER_COMMIT_CLICK_MS
279
+ ) {
280
+ kind = undefined;
281
+ }
282
+ if (click && logsEvents) {
283
+ this.logClick(click, t, pathname);
284
+ }
285
+ if (kind) {
286
+ this.emit(kind, layer, t, pathname);
287
+ }
288
+ } catch (e) {
289
+ this.log('[error] delivery threw: ' + e);
290
+ }
291
+ });
292
+ }
293
+
294
+ // One event per identical button every second, and at most 50 per page.
295
+ private logClick(click: IButtonClick, t: number, pathname: string): void {
296
+ if (!this.eventsEnabled()) {
297
+ return;
298
+ }
299
+ if (pathname !== this.pagePath) {
300
+ this.pagePath = pathname;
301
+ this.pageEvents = 0;
302
+ this.lastEvent = {};
303
+ }
304
+ if (this.pageEvents >= PAGE_EVENT_LIMIT) {
305
+ return;
306
+ }
307
+ const key = [
308
+ click.label,
309
+ click.ariaLabel,
310
+ click.icon,
311
+ click.elementId,
312
+ click.elementName,
313
+ click.testId,
314
+ click.element,
315
+ click.type,
316
+ ].join('|');
317
+ const last = this.lastEvent[key];
318
+ if (last !== undefined && t - last < EVENT_DEDUP_MS) {
319
+ return;
320
+ }
321
+ this.lastEvent[key] = t;
322
+ this.pageEvents++;
323
+
324
+ // Restarts an expired session, as public logEvent() and page views do.
325
+ this.mpInstance._SessionManager.resetSessionTimer();
326
+ this.mpInstance._Events.logEvent(
327
+ buildButtonClickEvent(click, pathname)
328
+ );
329
+ }
330
+
331
+ private emit(
332
+ kind: CheckoutIntentKind,
333
+ layer: string,
334
+ t: number,
335
+ pathname: string
336
+ ): void {
337
+ if (this.isKitSuppressed()) {
338
+ return;
339
+ }
340
+ if (kind === 'commit' && layer === 'click') {
341
+ this.lastCommitClick = t;
342
+ }
343
+
344
+ const last = this.lastSent[kind];
345
+ if (last && last.pathname === pathname && t - last.t < THROTTLE_MS) {
346
+ return;
347
+ }
348
+ this.lastSent[kind] = { t, pathname };
349
+
350
+ this.mpInstance._RoktManager.notifyCheckoutIntent(
351
+ Object.freeze({ kind, t }),
352
+ pathname
353
+ );
354
+ }
355
+
356
+ // Events follow the SDK's opt-out like any other event.
357
+ private eventsEnabled(): boolean {
358
+ const store = this.mpInstance._Store;
359
+ return !!store && store.isEnabled;
360
+ }
361
+
362
+ // The Rokt manager owns the kit's privacy gates: flag, opt-out, launcher
363
+ // options and Global Privacy Control.
364
+ private isKitSuppressed(): boolean {
365
+ const roktManager = this.mpInstance._RoktManager;
366
+ return (
367
+ !this.hasRoktKit ||
368
+ !roktManager ||
369
+ roktManager.isCheckoutIntentSuppressed()
370
+ );
371
+ }
372
+
373
+ private log(message: string): void {
374
+ this.mpInstance.Logger.verbose('mParticle button tracking: ' + message);
375
+ }
376
+ }
@@ -1,5 +1,12 @@
1
- // Classifies a click, submit or frame focus as checkout intent. Reads only short
2
- // control labels, never form field values, and stores nothing.
1
+ import {
2
+ CONTROL_SELECTOR,
3
+ findControl,
4
+ isRoktElement,
5
+ isWalletElement,
6
+ } from './controls';
7
+
8
+ // Classifies a click, submit or frame focus as checkout intent for the Rokt kit.
9
+ // Reads only short control labels, never form field values, and stores nothing.
3
10
  export type CheckoutIntentKind =
4
11
  | 'commit'
5
12
  | 'wallet'
@@ -63,88 +70,62 @@ const frameHosts = new Set([
63
70
  'payments.amazon.com',
64
71
  'pay.shopify.com',
65
72
  ]);
73
+ // Keep these classes ASCII-only in source bytes to avoid mojibake parse failures
74
+ // when a host page forces non-UTF-8 decoding before JavaScript parse.
75
+ const TYPOGRAPHIC_APOSTROPHES = /[\u2019\u2018]/g;
76
+ const CURRENCY_AND_NUMBER_CHARS =
77
+ /[0-9\uFF10-\uFF19$\u20AC\u00A3\u00A5\uFFE5\u20B9\u5186.,\uFF0C]/g;
66
78
 
67
- const CONTROL_SELECTOR =
68
- 'button, input[type="submit"], input[type="button"], input[type="image"], [role="button"]';
69
-
70
- export function getIntentRouteClass(
71
- pathname: string
72
- ): 'checkout_path' | 'other' {
79
+ function getIntentRouteClass(pathname: string): 'checkout_path' | 'other' {
73
80
  return checkoutPath.test(pathname) ? 'checkout_path' : 'other';
74
81
  }
75
82
 
76
- // Rokt's own placements and frames never count as partner checkout controls.
77
- function isRoktElement(element: Element): boolean {
78
- if (element.closest('[data-core-type], #rokt-controller-frame')) {
79
- return true;
80
- }
81
- const src = element.getAttribute('src');
82
- if (src) {
83
- try {
84
- if (
85
- new URL(src, 'https://example.test').pathname.indexOf(
86
- '/wsdk/'
87
- ) !== -1
88
- ) {
89
- return true;
90
- }
91
- } catch (e) {
92
- // An unparseable src is not a Rokt element.
93
- }
94
- }
95
- return false;
96
- }
97
-
98
- function isWalletElement(element: Element): boolean {
99
- return element.matches(
100
- 'apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]'
101
- );
102
- }
103
-
104
83
  function normalize(text: string | null | undefined): string | undefined {
105
84
  if (!text || text.length > 128) {
106
85
  return undefined;
107
86
  }
108
87
  return text
109
88
  .toLowerCase()
110
- .replace(/[’‘]/g, "'")
111
- .replace(/[0-90-9$€£¥¥₹円.,,]/g, '')
89
+ .replace(TYPOGRAPHIC_APOSTROPHES, "'")
90
+ .replace(CURRENCY_AND_NUMBER_CHARS, '')
112
91
  .replace(/\s+/g, ' ')
113
92
  .trim();
114
93
  }
115
94
 
116
95
  // Labels come from aria-label then visible text. Only submit and button inputs
117
96
  // have their value read, so a typed field value is never touched.
118
- function labels(element: Element): string[] {
97
+ function labelSources(element: Element): Array<string | null> {
119
98
  if (
120
99
  element.children.length > 4 ||
121
- element.querySelector('input, select, textarea')
100
+ element.querySelector('input, select, textarea, [data-mp-no-track]')
122
101
  ) {
123
102
  return [];
124
103
  }
125
104
  let text: string | null = null;
126
- if (element.tagName.toLowerCase() === 'input') {
127
- const typeAttr = element.getAttribute('type');
128
- const type = typeAttr ? typeAttr.toLowerCase() : typeAttr;
105
+ if (element instanceof HTMLInputElement) {
106
+ const type = element.type;
129
107
  if (type === 'submit' || type === 'button') {
130
- text = (element as HTMLInputElement).value;
108
+ text = element.value;
131
109
  } else if (type === 'image') {
132
110
  text = element.getAttribute('alt');
133
111
  }
134
112
  } else {
135
113
  text = element.textContent;
136
114
  }
137
- return [
138
- normalize(element.getAttribute('aria-label')),
139
- normalize(text),
140
- ].filter((label): label is string => !!label);
115
+ return [element.getAttribute('aria-label'), text];
116
+ }
117
+
118
+ function labels(element: Element): string[] {
119
+ return labelSources(element)
120
+ .map(normalize)
121
+ .filter((label): label is string => !!label);
141
122
  }
142
123
 
143
124
  function classifyLabel(
144
125
  text: string,
145
126
  button: boolean,
146
127
  pathname: string
147
- ): CheckoutIntentKind | undefined {
128
+ ): Exclude<CheckoutIntentKind, 'frame'> | undefined {
148
129
  if (
149
130
  commits.test(text) ||
150
131
  (routeGatedCommits.test(text) &&
@@ -161,30 +142,18 @@ function classifyLabel(
161
142
  return undefined;
162
143
  }
163
144
 
164
- // Walks at most 12 entries of an event's composed path for the nearest control.
165
145
  export function classifyIntentControl(
166
146
  path: Array<EventTarget | null>,
167
147
  pathname: string
168
148
  ): CheckoutIntentKind | undefined {
169
- let control: Element | undefined;
170
- for (const target of path.slice(0, 12)) {
171
- if (!(target instanceof Element)) {
172
- continue;
173
- }
174
- if (isRoktElement(target)) {
175
- return undefined;
176
- }
177
- if (
178
- !control &&
179
- (target.matches(CONTROL_SELECTOR + ', a[href]') ||
180
- isWalletElement(target))
181
- ) {
182
- control = target;
183
- }
184
- }
185
- if (!control || isRoktElement(control)) {
186
- return undefined;
187
- }
149
+ const control = findControl(path);
150
+ return control ? classifyControl(control, pathname) : undefined;
151
+ }
152
+
153
+ function classifyControl(
154
+ control: Element,
155
+ pathname: string
156
+ ): Exclude<CheckoutIntentKind, 'frame'> | undefined {
188
157
  const texts = labels(control);
189
158
  if (texts.some(text => exclusions.test(text))) {
190
159
  return undefined;
@@ -0,0 +1,92 @@
1
+ import { isFunction } from '../utils';
2
+
3
+ // Finds the control a click landed on, for both checkout intent and button
4
+ // descriptions. Rokt's own elements, disabled controls and anything under
5
+ // data-mp-no-track are never returned.
6
+ export const CONTROL_SELECTOR =
7
+ 'button, input[type="submit"], input[type="button"], input[type="image"], [role="button"]';
8
+
9
+ // closest() over the composed tree: slots, parents, then shadow hosts.
10
+ export function hasComposedAncestor(
11
+ element: Element,
12
+ selector: string
13
+ ): boolean {
14
+ let node: Element | null = element;
15
+ while (node) {
16
+ if (node.matches(selector)) {
17
+ return true;
18
+ }
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;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ // Rokt's own placements and frames never count as partner checkout controls.
33
+ export function isRoktElement(element: Element): boolean {
34
+ if (
35
+ hasComposedAncestor(element, '[data-core-type], #rokt-controller-frame')
36
+ ) {
37
+ return true;
38
+ }
39
+ const src = element.getAttribute('src');
40
+ if (src) {
41
+ try {
42
+ if (
43
+ new URL(src, 'https://example.test').pathname.indexOf(
44
+ '/wsdk/'
45
+ ) !== -1
46
+ ) {
47
+ return true;
48
+ }
49
+ } catch (e) {
50
+ // An unparseable src is not a Rokt element.
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+
56
+ export function isWalletElement(element: Element): boolean {
57
+ return element.matches(
58
+ 'apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]'
59
+ );
60
+ }
61
+
62
+ // Walks at most 12 entries of an event's composed path for the nearest control.
63
+ // Rokt elements, disabled controls and anything under data-mp-no-track are skipped.
64
+ export function findControl(
65
+ path: Array<EventTarget | null>
66
+ ): Element | undefined {
67
+ let control: Element | undefined;
68
+ for (const target of path.slice(0, 12)) {
69
+ if (!(target instanceof Element)) {
70
+ continue;
71
+ }
72
+ if (isRoktElement(target) || target.matches('[data-mp-no-track]')) {
73
+ return undefined;
74
+ }
75
+ if (
76
+ !control &&
77
+ (target.matches(CONTROL_SELECTOR + ', a[href]') ||
78
+ isWalletElement(target))
79
+ ) {
80
+ control = target;
81
+ }
82
+ }
83
+ if (
84
+ !control ||
85
+ isRoktElement(control) ||
86
+ hasComposedAncestor(control, '[data-mp-no-track]') ||
87
+ control.matches(':disabled, [aria-disabled="true"]')
88
+ ) {
89
+ return undefined;
90
+ }
91
+ return control;
92
+ }
@@ -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,7 +190,7 @@ const Constants = {
190
190
  CaptureIntegrationSpecificIdsV2: 'captureIntegrationSpecificIds.V2',
191
191
  AstBackgroundEvents: 'astBackgroundEvents',
192
192
  AutoLogPageView: 'autoLogPageView',
193
- CheckoutIntent: 'checkoutIntent',
193
+ AutoButtonTracking: 'autoButtonTracking',
194
194
  },
195
195
  DefaultInstance: 'default_instance',
196
196
  CCPAPurpose: 'data_sale_opt_out',