@mparticle/web-sdk 3.27.0 → 3.29.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 +2 -2
- package/dist/mparticle.common.js +1 -1
- package/dist/mparticle.esm.js +1 -1
- package/dist/mparticle.js +341 -108
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +2 -1
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +0 -6
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +2 -0
- package/dist/types/src/pathMasking.d.ts +2 -0
- package/package.json +1 -1
- package/src/batchUploader.ts +25 -9
- package/src/buttonTracking/buttonDescription.ts +266 -46
- package/src/buttonTracking/buttonTracker.ts +22 -62
- package/src/buttonTracking/checkoutIntent.ts +56 -2
- package/src/pageViewTracker.ts +3 -1
- package/src/pathMasking.ts +116 -9
|
@@ -3,6 +3,7 @@ import { isFunction } from '../utils';
|
|
|
3
3
|
import { buildButtonClickEvent, buildLinkClickEvent } from './buttonClickEvent';
|
|
4
4
|
import {
|
|
5
5
|
describeButtonClick,
|
|
6
|
+
describeCheckedPaymentChoice,
|
|
6
7
|
describePaymentChoice,
|
|
7
8
|
IButtonClick,
|
|
8
9
|
IPaymentChoice,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
CheckoutIntentSource,
|
|
15
16
|
classifyIntent,
|
|
16
17
|
classifyPaymentFrame,
|
|
18
|
+
namesWallet,
|
|
17
19
|
PaymentFrameFamily,
|
|
18
20
|
} from './checkoutIntent';
|
|
19
21
|
import { findControl, findRoleControl, isExcludedPath } from './controls';
|
|
@@ -32,9 +34,6 @@ const PATH_LIMIT = 20;
|
|
|
32
34
|
// A ceiling, not a dedupe: a held Enter key or browser automation fires a
|
|
33
35
|
// trusted click per repeat, and each one is forwarded to every kit.
|
|
34
36
|
const PAGE_EVENT_LIMIT = 100;
|
|
35
|
-
// A click uploads at once. Taps that follow it this quickly are held and share
|
|
36
|
-
// one upload once they stop.
|
|
37
|
-
const UPLOAD_DEBOUNCE_MS = 100;
|
|
38
37
|
|
|
39
38
|
interface IBtnState {
|
|
40
39
|
tracker?: ButtonTracker;
|
|
@@ -176,10 +175,6 @@ export class ButtonTracker {
|
|
|
176
175
|
private pageEvents = 0;
|
|
177
176
|
// Clicks read at dispatch and waiting for the next paint.
|
|
178
177
|
private pending: Array<() => void> = [];
|
|
179
|
-
private uploadTimerId: ReturnType<typeof setTimeout> | null = null;
|
|
180
|
-
private uploadHeld = false;
|
|
181
|
-
// Set while the page is left, when the SDK's exit upload sends the queue.
|
|
182
|
-
private leaving = false;
|
|
183
178
|
|
|
184
179
|
constructor(mpInstance: IMParticleWebSDKInstance) {
|
|
185
180
|
this.mpInstance = mpInstance;
|
|
@@ -323,7 +318,6 @@ export class ButtonTracker {
|
|
|
323
318
|
this.lastSent = {};
|
|
324
319
|
this.pagePath = null;
|
|
325
320
|
this.pageEvents = 0;
|
|
326
|
-
this.cancelUpload();
|
|
327
321
|
}
|
|
328
322
|
|
|
329
323
|
private enqueue(delivery: () => void): void {
|
|
@@ -349,14 +343,8 @@ export class ButtonTracker {
|
|
|
349
343
|
event.type === 'pagehide' ||
|
|
350
344
|
document.visibilityState === 'hidden'
|
|
351
345
|
) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
this.drain();
|
|
355
|
-
} finally {
|
|
356
|
-
this.leaving = false;
|
|
357
|
-
}
|
|
358
|
-
// The SDK's exit upload sends the queue, held taps included.
|
|
359
|
-
this.cancelUpload();
|
|
346
|
+
// The SDK's exit upload then sends these clicks with the queue.
|
|
347
|
+
this.drain();
|
|
360
348
|
}
|
|
361
349
|
};
|
|
362
350
|
|
|
@@ -404,6 +392,7 @@ export class ButtonTracker {
|
|
|
404
392
|
let source: CheckoutIntentSource | undefined;
|
|
405
393
|
let adds: true | undefined;
|
|
406
394
|
let choice: IPaymentChoice | undefined;
|
|
395
|
+
let checkedNames: string[] | undefined;
|
|
407
396
|
let family: PaymentFrameFamily | undefined;
|
|
408
397
|
let unconfirmedCommit = false;
|
|
409
398
|
if (layer === 'click') {
|
|
@@ -457,6 +446,19 @@ export class ButtonTracker {
|
|
|
457
446
|
choice = signalsKit
|
|
458
447
|
? describePaymentChoice(control, path)
|
|
459
448
|
: undefined;
|
|
449
|
+
// A confirm or checkout-step click picks no radio itself, so it
|
|
450
|
+
// carries the one the page holds checked as read here. It is not
|
|
451
|
+
// read again at delivery: the page's own handlers may clear or
|
|
452
|
+
// replace it by then. A wallet tap names its own method, and
|
|
453
|
+
// so does a confirm that names a wallet.
|
|
454
|
+
if (
|
|
455
|
+
!choice &&
|
|
456
|
+
(kind === 'commit' || kind === 'checkout_step') &&
|
|
457
|
+
!namesWallet(control, click)
|
|
458
|
+
) {
|
|
459
|
+
const checked = describeCheckedPaymentChoice();
|
|
460
|
+
checkedNames = checked ? checked.names : undefined;
|
|
461
|
+
}
|
|
460
462
|
}
|
|
461
463
|
this.lastClick = { t, excluded, kind, source };
|
|
462
464
|
} else if (layer === 'submit') {
|
|
@@ -517,6 +519,8 @@ export class ButtonTracker {
|
|
|
517
519
|
// with this pick.
|
|
518
520
|
if (choice && isPicked(choice.radio)) {
|
|
519
521
|
this.pickPayment(t, pathname, choice.names);
|
|
522
|
+
} else if (checkedNames) {
|
|
523
|
+
this.pickPayment(t, pathname, checkedNames);
|
|
520
524
|
} else if (family) {
|
|
521
525
|
this.pickPayment(t, pathname, undefined, family);
|
|
522
526
|
}
|
|
@@ -548,57 +552,13 @@ export class ButtonTracker {
|
|
|
548
552
|
this.pageEvents++;
|
|
549
553
|
// Restarts an expired session, as public logEvent() and page views do.
|
|
550
554
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
551
|
-
|
|
555
|
+
// No upload of its own, which a click that opens another page would
|
|
556
|
+
// cancel: with batching on, the next batch or the exit upload sends it.
|
|
552
557
|
this.mpInstance._Events.logEvent(
|
|
553
558
|
asLink
|
|
554
559
|
? buildLinkClickEvent(click, pathname)
|
|
555
560
|
: buildButtonClickEvent(click, pathname)
|
|
556
561
|
);
|
|
557
|
-
// Only a click waiting for the batch interval is sent early: a held
|
|
558
|
-
// one goes out when released, and with batching off it already has.
|
|
559
|
-
// A pick, such as a checkbox, a radio or a tab, waits for the batch,
|
|
560
|
-
// since choosing an option doesn't leave the page the way a confirm
|
|
561
|
-
// click can.
|
|
562
|
-
if (
|
|
563
|
-
!this.leaving &&
|
|
564
|
-
!click.pick &&
|
|
565
|
-
this.queuedForUpload() > queued
|
|
566
|
-
) {
|
|
567
|
-
this.uploadNow();
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Events waiting in the upload queue for the next batch interval.
|
|
572
|
-
private queuedForUpload(): number {
|
|
573
|
-
const uploader = this.mpInstance._APIClient.uploader;
|
|
574
|
-
return uploader ? uploader.eventsQueuedForProcessing.length : 0;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Uploads a click without waiting for the batch interval. A tap inside the
|
|
578
|
-
// wait after the last one is held, and each restarts the wait, so a burst
|
|
579
|
-
// after the first click goes out in one request once the taps stop.
|
|
580
|
-
private uploadNow(): void {
|
|
581
|
-
if (this.uploadTimerId === null) {
|
|
582
|
-
this.mpInstance.upload();
|
|
583
|
-
} else {
|
|
584
|
-
clearTimeout(this.uploadTimerId);
|
|
585
|
-
this.uploadHeld = true;
|
|
586
|
-
}
|
|
587
|
-
this.uploadTimerId = setTimeout(() => {
|
|
588
|
-
this.uploadTimerId = null;
|
|
589
|
-
if (this.uploadHeld) {
|
|
590
|
-
this.uploadHeld = false;
|
|
591
|
-
this.mpInstance.upload();
|
|
592
|
-
}
|
|
593
|
-
}, UPLOAD_DEBOUNCE_MS);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
private cancelUpload(): void {
|
|
597
|
-
if (this.uploadTimerId !== null) {
|
|
598
|
-
clearTimeout(this.uploadTimerId);
|
|
599
|
-
this.uploadTimerId = null;
|
|
600
|
-
}
|
|
601
|
-
this.uploadHeld = false;
|
|
602
562
|
}
|
|
603
563
|
|
|
604
564
|
private emit(
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isWalletNamedControl,
|
|
8
8
|
vendorButtonKind,
|
|
9
9
|
} from './controls';
|
|
10
|
+
import { IButtonClick, TEST_ID_ATTRIBUTES } from './buttonDescription';
|
|
10
11
|
|
|
11
12
|
// Classifies a click, submit or frame focus as checkout intent for the Rokt kit.
|
|
12
13
|
// Reads only short control labels, never form field values, and stores nothing.
|
|
@@ -19,7 +20,15 @@ export type CheckoutIntentKind =
|
|
|
19
20
|
|
|
20
21
|
// A checkout segment may name its kind or its version, as /express-checkout, /checkout-v2 and /checkout-external do.
|
|
21
22
|
// Other hyphenated words make it a content slug, such as /help/checkout-faq or /products/self-checkout-kiosk.
|
|
22
|
-
const
|
|
23
|
+
const CHECKOUT_SEGMENT =
|
|
24
|
+
'(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?' +
|
|
25
|
+
'(?:-(?:v\\d+|\\d+|flow|external|process|step(?:-\\d+)?))?';
|
|
26
|
+
const checkoutPath = new RegExp(
|
|
27
|
+
'(?:^|\\/)(?:' + CHECKOUT_SEGMENT + '|payment|order|kasse|paiement|commander?|pago)(?:\\/|$)',
|
|
28
|
+
'i'
|
|
29
|
+
);
|
|
30
|
+
// A route with a checkout segment itself, not just a payment or order one, which account pages also use.
|
|
31
|
+
const checkoutSegmentPath = new RegExp('(?:^|\\/)' + CHECKOUT_SEGMENT + '(?:\\/|$)', 'i');
|
|
23
32
|
// A confirmation page under a checkout route, such as /checkout/thankyou/<order> or /checkouts/<token>/thank_you, comes
|
|
24
33
|
// after the confirm click. "Complete" and "success" mark one only as the last word of a segment, so /complete-checkout
|
|
25
34
|
// and /checkout/complete-registration stay checkout routes.
|
|
@@ -91,6 +100,9 @@ const steps = new RegExp(
|
|
|
91
100
|
].join('|') +
|
|
92
101
|
')$'
|
|
93
102
|
);
|
|
103
|
+
// "Save and continue" also saves profile, address and card forms, so it is a step only on a checkout segment's route
|
|
104
|
+
// that isn't an account or confirmation page.
|
|
105
|
+
const routeGatedSteps = /^save and continue$/;
|
|
94
106
|
const CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
|
|
95
107
|
// "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
|
|
96
108
|
const addToCartLabels = new RegExp('^add(?: [^ ]+){0,5} to ' + CART_NOUN + '$');
|
|
@@ -115,6 +127,9 @@ const walletBrands = new RegExp(
|
|
|
115
127
|
'^(?:' + WALLET_NAMES + ')(?: (?:checkout|check out|express(?: checkout)?|express pay|buy now|pay now|pay|now)' +
|
|
116
128
|
'| (?:logo|icon|mark|button)(?: (?:logo|icon|mark|button))*)?$'
|
|
117
129
|
);
|
|
130
|
+
// The wallets the Rokt kit reads from a click's label or identifiers, as words:
|
|
131
|
+
// "Place order with PayPal!" and "paypal-pay-button" both name PayPal.
|
|
132
|
+
const KIT_WALLET_WORDS = /(?:^| )(?:apple ?pay|google ?pay|g ?pay|android ?pay|venmo|pay ?pal|amazon ?pay|shop ?pay|klarna|after ?pay|clear ?pay|affirm|cash ?app|sezzle|samsung ?pay)(?= |$)/;
|
|
118
133
|
// The payment method a frame's focus picks, in the Rokt kit's family names.
|
|
119
134
|
export type PaymentFrameFamily =
|
|
120
135
|
| 'apple_pay'
|
|
@@ -368,13 +383,52 @@ export function classifyIntent(
|
|
|
368
383
|
if (isButton && walletBrands.test(text)) {
|
|
369
384
|
return { kind: 'wallet', source: 'brand' };
|
|
370
385
|
}
|
|
371
|
-
if (
|
|
386
|
+
if (
|
|
387
|
+
steps.test(text) ||
|
|
388
|
+
(isButton &&
|
|
389
|
+
routeGatedSteps.test(text) &&
|
|
390
|
+
checkoutSegmentPath.test(pathname) &&
|
|
391
|
+
!confirmationPath.test(pathname) &&
|
|
392
|
+
!accountStepPath.test(pathname))
|
|
393
|
+
) {
|
|
372
394
|
return { kind: 'checkout_step', source: 'label' };
|
|
373
395
|
}
|
|
374
396
|
}
|
|
375
397
|
return cart;
|
|
376
398
|
}
|
|
377
399
|
|
|
400
|
+
// The Rokt kit's own word split, so "PayPal!" and "paypal2" both read as
|
|
401
|
+
// "paypal" here as they do there.
|
|
402
|
+
function kitWords(value: string | null | undefined): string {
|
|
403
|
+
return (value || '')
|
|
404
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
405
|
+
.toLowerCase()
|
|
406
|
+
.replace(/[^a-z]+/g, ' ')
|
|
407
|
+
.trim();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// A confirm control may name its wallet, so "Place order with PayPal" and a
|
|
411
|
+
// "Place order" button with id paypal-pay-button are confirm clicks that still
|
|
412
|
+
// name their own method. A logged click is judged on the fields the kit reads
|
|
413
|
+
// a method from; a click that isn't logged, on the control's own label and
|
|
414
|
+
// identifiers.
|
|
415
|
+
export function namesWallet(control: Element, click?: IButtonClick): boolean {
|
|
416
|
+
const values = click
|
|
417
|
+
? [
|
|
418
|
+
click.label,
|
|
419
|
+
click.ariaLabel,
|
|
420
|
+
click.elementId,
|
|
421
|
+
click.elementName,
|
|
422
|
+
click.testId,
|
|
423
|
+
]
|
|
424
|
+
: labelSources(control).concat(
|
|
425
|
+
['id', 'name']
|
|
426
|
+
.concat(TEST_ID_ATTRIBUTES)
|
|
427
|
+
.map(attribute => control.getAttribute(attribute))
|
|
428
|
+
);
|
|
429
|
+
return values.some(value => KIT_WALLET_WORDS.test(kitWords(value)));
|
|
430
|
+
}
|
|
431
|
+
|
|
378
432
|
export function classifyIntentControl(
|
|
379
433
|
control: Element,
|
|
380
434
|
pathname: string
|
package/src/pageViewTracker.ts
CHANGED
|
@@ -661,10 +661,12 @@ export class PageViewTracker {
|
|
|
661
661
|
isAutoPageView: this.isAutoPageView,
|
|
662
662
|
});
|
|
663
663
|
|
|
664
|
+
// A title can name the shopper or an order, so it stays out of the log
|
|
665
|
+
// line, for the same reason describePage omits param values.
|
|
664
666
|
this.log(
|
|
665
667
|
`[fire] deferred flush -> _Events.logEvent(PageView) (page: ${describePage(
|
|
666
668
|
page
|
|
667
|
-
)}
|
|
669
|
+
)})`
|
|
668
670
|
);
|
|
669
671
|
|
|
670
672
|
this.mpInstance._Events.logEvent(event);
|
package/src/pathMasking.ts
CHANGED
|
@@ -25,6 +25,19 @@ const MATRIX_SEPARATOR = /;|%(?:25)*3B/i;
|
|
|
25
25
|
// can't be decoded again. What it hides is unknown, so it counts as sensitive.
|
|
26
26
|
export const LEFTOVER_ESCAPE = /%[0-9A-F]{2}/i;
|
|
27
27
|
|
|
28
|
+
// Pages under a personal route that name no one: the sign-in, sign-up and
|
|
29
|
+
// account steps sites keep there, as in `/u/login` or `/u/mfa-sms-challenge`.
|
|
30
|
+
const ACCOUNT_PAGE = /^(?:log-?in|log-?out|sign-?in|sign-?out|sign-?up|register|registration|create-account|account|settings|forgot-password|reset-password|password|challenge|consent|callback|authorize|continue|mfa(?:-[a-z]+)*)$/i;
|
|
31
|
+
|
|
32
|
+
// Whether a segment after a route goes as `*`: always after an order route, and
|
|
33
|
+
// after a personal route unless it is one of the account pages kept there.
|
|
34
|
+
export function hidesSegmentAfter(route: string, segment: string): boolean {
|
|
35
|
+
return (
|
|
36
|
+
hidesNextSegment(route) &&
|
|
37
|
+
!(PERSONAL_ROUTE.test(route) && ACCOUNT_PAGE.test(segment))
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
export function hidesNextSegment(route: string): boolean {
|
|
29
42
|
return (
|
|
30
43
|
PERSONAL_ROUTE.test(route) ||
|
|
@@ -115,11 +128,102 @@ export function isTokenSegment(segment: string): boolean {
|
|
|
115
128
|
);
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
// A word a product name is made of: three or more letters, lowercase or
|
|
132
|
+
// Capitalised. A code's mixed case or capital-letter groups aren't words.
|
|
133
|
+
const isWord = (piece: string): boolean =>
|
|
134
|
+
/^(?:[a-z]{3,}|[A-Z][a-z]{2,})$/.test(piece);
|
|
135
|
+
|
|
136
|
+
const UUID_IN_TEXT = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
137
|
+
|
|
138
|
+
// Route words whose next segment names a product, as in `/p/<name>` or
|
|
139
|
+
// `/product/<name>`. Only there can words beside a token be a product's name
|
|
140
|
+
// rather than a person's or part of a code.
|
|
141
|
+
const PRODUCT_ROUTE = /^(?:p|product|products|item|items|dp|shop|catalog)$/i;
|
|
142
|
+
|
|
143
|
+
// A segment that holds a token goes as `*`. After a product route, only the
|
|
144
|
+
// token goes, so `/p/trail-runner-gtx-zq7Kd93Lm41` keeps its name as
|
|
145
|
+
// `/p/trail-runner-gtx-*`.
|
|
146
|
+
// Every piece that isn't a word in an unbroken run with the token goes with it
|
|
147
|
+
// as one `*`, so the rest of a UUID or of a code cut into short pieces never
|
|
148
|
+
// stays. A piece that can't be decoded or decodes to a separator counts as a
|
|
149
|
+
// token, and so does any piece that mixes letters and digits or switches case
|
|
150
|
+
// inside, as a code's short pieces do. The segment goes whole as `*` when no
|
|
151
|
+
// piece is a word or it holds a UUID. Undefined when it holds no token. `segment` is as the page
|
|
152
|
+
// has it, `text` decoded, `route` the route word before it.
|
|
153
|
+
export function maskTokenSegment(
|
|
154
|
+
segment: string,
|
|
155
|
+
text: string,
|
|
156
|
+
route: string | undefined
|
|
157
|
+
): string | undefined {
|
|
158
|
+
if (!isTokenSegment(text)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
// A UUID anywhere in the decoded text, however its hyphens are escaped,
|
|
162
|
+
// takes the whole segment.
|
|
163
|
+
if (!route || !PRODUCT_ROUTE.test(route) || UUID_IN_TEXT.test(text)) {
|
|
164
|
+
return '*';
|
|
165
|
+
}
|
|
166
|
+
const parts = segment.split(/([-_.~])/);
|
|
167
|
+
const raws = parts.filter((_, i) => i % 2 === 0);
|
|
168
|
+
const separators = parts.filter((_, i) => i % 2 === 1);
|
|
169
|
+
const decoded = raws.map(raw => decodeFully(raw));
|
|
170
|
+
const kinds = decoded.map(piece => {
|
|
171
|
+
if (piece === undefined || /[-_.~]/.test(piece)) {
|
|
172
|
+
return 'token';
|
|
173
|
+
}
|
|
174
|
+
const plain = piece.replace(/\+/g, '');
|
|
175
|
+
return isToken(plain) ||
|
|
176
|
+
mixesLettersAndDigits(plain) ||
|
|
177
|
+
INNER_CASE_SWITCH.test(plain)
|
|
178
|
+
? 'token'
|
|
179
|
+
: isWord(plain)
|
|
180
|
+
? 'word'
|
|
181
|
+
: 'other';
|
|
182
|
+
});
|
|
183
|
+
// Each unbroken run of pieces that aren't words is hidden when it holds a
|
|
184
|
+
// token.
|
|
185
|
+
const masked = kinds.map(() => false);
|
|
186
|
+
for (let start = 0; start < kinds.length; ) {
|
|
187
|
+
if (kinds[start] === 'word') {
|
|
188
|
+
start++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let end = start;
|
|
192
|
+
while (end < kinds.length && kinds[end] !== 'word') {
|
|
193
|
+
end++;
|
|
194
|
+
}
|
|
195
|
+
// A run reads as a token also when only its pieces joined do.
|
|
196
|
+
if (
|
|
197
|
+
kinds.slice(start, end).indexOf('token') !== -1 ||
|
|
198
|
+
isToken(raws.slice(start, end).join(''))
|
|
199
|
+
) {
|
|
200
|
+
for (let k = start; k < end; k++) {
|
|
201
|
+
masked[k] = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
start = end;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
masked.indexOf(true) === -1 ||
|
|
208
|
+
kinds.indexOf('word') === -1
|
|
209
|
+
) {
|
|
210
|
+
return '*';
|
|
211
|
+
}
|
|
212
|
+
let out = '';
|
|
213
|
+
for (let k = 0; k < raws.length; k++) {
|
|
214
|
+
if (masked[k] && k > 0 && masked[k - 1]) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
out += (k > 0 ? separators[k - 1] : '') + (masked[k] ? '*' : raws[k]);
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
118
222
|
// The path a click logs. A segment that can single out one shopper
|
|
119
|
-
// goes as `*`: one holding an @, `;` or `=`,
|
|
120
|
-
// that names a person, carries a code or opens an order.
|
|
121
|
-
// exactly as the page has them, plain
|
|
122
|
-
// they name the product.
|
|
223
|
+
// goes as `*`: one holding an @, `;` or `=`, an id, or the one after a route
|
|
224
|
+
// that names a person, carries a code or opens an order. A token inside a name
|
|
225
|
+
// goes as `*` alone. Kept segments stay exactly as the page has them, plain
|
|
226
|
+
// numbers included, since on a product page they name the product.
|
|
123
227
|
export function pagePath(pathname: string): string {
|
|
124
228
|
const segments = pathname.split('/');
|
|
125
229
|
return segments
|
|
@@ -131,15 +235,18 @@ export function pagePath(pathname: string): string {
|
|
|
131
235
|
const hiddenByRoute =
|
|
132
236
|
i > 0 &&
|
|
133
237
|
!!text &&
|
|
134
|
-
(route === undefined ||
|
|
238
|
+
(route === undefined || hidesSegmentAfter(route, text));
|
|
135
239
|
// An encoded slash hides several segments in one, so it goes whole.
|
|
136
|
-
|
|
240
|
+
if (
|
|
241
|
+
text === undefined ||
|
|
137
242
|
/[@;=/]/.test(text) ||
|
|
138
243
|
LEFTOVER_ESCAPE.test(text) ||
|
|
139
|
-
isTokenSegment(text) ||
|
|
140
244
|
hiddenByRoute
|
|
141
|
-
|
|
142
|
-
|
|
245
|
+
) {
|
|
246
|
+
return '*';
|
|
247
|
+
}
|
|
248
|
+
const masked = maskTokenSegment(segment, text, route);
|
|
249
|
+
return masked === undefined ? segment : masked;
|
|
143
250
|
})
|
|
144
251
|
.join('/');
|
|
145
252
|
}
|