@mparticle/web-sdk 3.27.0 → 3.28.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/dist/mparticle.common.js +1 -1
- package/dist/mparticle.esm.js +1 -1
- package/dist/mparticle.js +139 -78
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +0 -1
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +0 -6
- 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 +17 -11
- package/src/buttonTracking/buttonTracker.ts +4 -62
- package/src/buttonTracking/checkoutIntent.ts +20 -2
- package/src/pageViewTracker.ts +3 -1
- package/src/pathMasking.ts +116 -9
|
@@ -19,7 +19,15 @@ export type CheckoutIntentKind =
|
|
|
19
19
|
|
|
20
20
|
// A checkout segment may name its kind or its version, as /express-checkout, /checkout-v2 and /checkout-external do.
|
|
21
21
|
// Other hyphenated words make it a content slug, such as /help/checkout-faq or /products/self-checkout-kiosk.
|
|
22
|
-
const
|
|
22
|
+
const CHECKOUT_SEGMENT =
|
|
23
|
+
'(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?' +
|
|
24
|
+
'(?:-(?:v\\d+|\\d+|flow|external|process|step(?:-\\d+)?))?';
|
|
25
|
+
const checkoutPath = new RegExp(
|
|
26
|
+
'(?:^|\\/)(?:' + CHECKOUT_SEGMENT + '|payment|order|kasse|paiement|commander?|pago)(?:\\/|$)',
|
|
27
|
+
'i'
|
|
28
|
+
);
|
|
29
|
+
// A route with a checkout segment itself, not just a payment or order one, which account pages also use.
|
|
30
|
+
const checkoutSegmentPath = new RegExp('(?:^|\\/)' + CHECKOUT_SEGMENT + '(?:\\/|$)', 'i');
|
|
23
31
|
// A confirmation page under a checkout route, such as /checkout/thankyou/<order> or /checkouts/<token>/thank_you, comes
|
|
24
32
|
// after the confirm click. "Complete" and "success" mark one only as the last word of a segment, so /complete-checkout
|
|
25
33
|
// and /checkout/complete-registration stay checkout routes.
|
|
@@ -91,6 +99,9 @@ const steps = new RegExp(
|
|
|
91
99
|
].join('|') +
|
|
92
100
|
')$'
|
|
93
101
|
);
|
|
102
|
+
// "Save and continue" also saves profile, address and card forms, so it is a step only on a checkout segment's route
|
|
103
|
+
// that isn't an account or confirmation page.
|
|
104
|
+
const routeGatedSteps = /^save and continue$/;
|
|
94
105
|
const CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
|
|
95
106
|
// "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
|
|
96
107
|
const addToCartLabels = new RegExp('^add(?: [^ ]+){0,5} to ' + CART_NOUN + '$');
|
|
@@ -368,7 +379,14 @@ export function classifyIntent(
|
|
|
368
379
|
if (isButton && walletBrands.test(text)) {
|
|
369
380
|
return { kind: 'wallet', source: 'brand' };
|
|
370
381
|
}
|
|
371
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
steps.test(text) ||
|
|
384
|
+
(isButton &&
|
|
385
|
+
routeGatedSteps.test(text) &&
|
|
386
|
+
checkoutSegmentPath.test(pathname) &&
|
|
387
|
+
!confirmationPath.test(pathname) &&
|
|
388
|
+
!accountStepPath.test(pathname))
|
|
389
|
+
) {
|
|
372
390
|
return { kind: 'checkout_step', source: 'label' };
|
|
373
391
|
}
|
|
374
392
|
}
|
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
|
}
|