@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
package/dist/mparticle.js
CHANGED
|
@@ -204,7 +204,7 @@ var mParticle = (function () {
|
|
|
204
204
|
Base64: Base64$1
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
-
var version = "3.
|
|
207
|
+
var version = "3.28.0";
|
|
208
208
|
|
|
209
209
|
var Constants = {
|
|
210
210
|
sdkVersion: version,
|
|
@@ -2756,6 +2756,14 @@ var mParticle = (function () {
|
|
|
2756
2756
|
// A valid escape left after decoding stopped, as when a literal `%` before it
|
|
2757
2757
|
// can't be decoded again. What it hides is unknown, so it counts as sensitive.
|
|
2758
2758
|
var LEFTOVER_ESCAPE = /%[0-9A-F]{2}/i;
|
|
2759
|
+
// Pages under a personal route that name no one: the sign-in, sign-up and
|
|
2760
|
+
// account steps sites keep there, as in `/u/login` or `/u/mfa-sms-challenge`.
|
|
2761
|
+
var 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;
|
|
2762
|
+
// Whether a segment after a route goes as `*`: always after an order route, and
|
|
2763
|
+
// after a personal route unless it is one of the account pages kept there.
|
|
2764
|
+
function hidesSegmentAfter(route, segment) {
|
|
2765
|
+
return hidesNextSegment(route) && !(PERSONAL_ROUTE.test(route) && ACCOUNT_PAGE.test(segment));
|
|
2766
|
+
}
|
|
2759
2767
|
function hidesNextSegment(route) {
|
|
2760
2768
|
return PERSONAL_ROUTE.test(route) || ORDER_ROUTE.test(route) && !PRODUCT_ORDER_WORD.test(route);
|
|
2761
2769
|
}
|
|
@@ -2832,11 +2840,91 @@ var mParticle = (function () {
|
|
|
2832
2840
|
return INNER_CASE_SWITCH.test(piece);
|
|
2833
2841
|
}) && isToken(joined) || pieces.filter(mixesLettersAndDigits).length >= 2 && MIXED_TOKEN.test(joined);
|
|
2834
2842
|
}
|
|
2843
|
+
// A word a product name is made of: three or more letters, lowercase or
|
|
2844
|
+
// Capitalised. A code's mixed case or capital-letter groups aren't words.
|
|
2845
|
+
var isWord = function isWord(piece) {
|
|
2846
|
+
return /^(?:[a-z]{3,}|[A-Z][a-z]{2,})$/.test(piece);
|
|
2847
|
+
};
|
|
2848
|
+
var UUID_IN_TEXT = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
2849
|
+
// Route words whose next segment names a product, as in `/p/<name>` or
|
|
2850
|
+
// `/product/<name>`. Only there can words beside a token be a product's name
|
|
2851
|
+
// rather than a person's or part of a code.
|
|
2852
|
+
var PRODUCT_ROUTE = /^(?:p|product|products|item|items|dp|shop|catalog)$/i;
|
|
2853
|
+
// A segment that holds a token goes as `*`. After a product route, only the
|
|
2854
|
+
// token goes, so `/p/trail-runner-gtx-zq7Kd93Lm41` keeps its name as
|
|
2855
|
+
// `/p/trail-runner-gtx-*`.
|
|
2856
|
+
// Every piece that isn't a word in an unbroken run with the token goes with it
|
|
2857
|
+
// as one `*`, so the rest of a UUID or of a code cut into short pieces never
|
|
2858
|
+
// stays. A piece that can't be decoded or decodes to a separator counts as a
|
|
2859
|
+
// token, and so does any piece that mixes letters and digits or switches case
|
|
2860
|
+
// inside, as a code's short pieces do. The segment goes whole as `*` when no
|
|
2861
|
+
// piece is a word or it holds a UUID. Undefined when it holds no token. `segment` is as the page
|
|
2862
|
+
// has it, `text` decoded, `route` the route word before it.
|
|
2863
|
+
function maskTokenSegment(segment, text, route) {
|
|
2864
|
+
if (!isTokenSegment(text)) {
|
|
2865
|
+
return undefined;
|
|
2866
|
+
}
|
|
2867
|
+
// A UUID anywhere in the decoded text, however its hyphens are escaped,
|
|
2868
|
+
// takes the whole segment.
|
|
2869
|
+
if (!route || !PRODUCT_ROUTE.test(route) || UUID_IN_TEXT.test(text)) {
|
|
2870
|
+
return '*';
|
|
2871
|
+
}
|
|
2872
|
+
var parts = segment.split(/([-_.~])/);
|
|
2873
|
+
var raws = parts.filter(function (_, i) {
|
|
2874
|
+
return i % 2 === 0;
|
|
2875
|
+
});
|
|
2876
|
+
var separators = parts.filter(function (_, i) {
|
|
2877
|
+
return i % 2 === 1;
|
|
2878
|
+
});
|
|
2879
|
+
var decoded = raws.map(function (raw) {
|
|
2880
|
+
return decodeFully(raw);
|
|
2881
|
+
});
|
|
2882
|
+
var kinds = decoded.map(function (piece) {
|
|
2883
|
+
if (piece === undefined || /[-_.~]/.test(piece)) {
|
|
2884
|
+
return 'token';
|
|
2885
|
+
}
|
|
2886
|
+
var plain = piece.replace(/\+/g, '');
|
|
2887
|
+
return isToken(plain) || mixesLettersAndDigits(plain) || INNER_CASE_SWITCH.test(plain) ? 'token' : isWord(plain) ? 'word' : 'other';
|
|
2888
|
+
});
|
|
2889
|
+
// Each unbroken run of pieces that aren't words is hidden when it holds a
|
|
2890
|
+
// token.
|
|
2891
|
+
var masked = kinds.map(function () {
|
|
2892
|
+
return false;
|
|
2893
|
+
});
|
|
2894
|
+
for (var start = 0; start < kinds.length;) {
|
|
2895
|
+
if (kinds[start] === 'word') {
|
|
2896
|
+
start++;
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
var end = start;
|
|
2900
|
+
while (end < kinds.length && kinds[end] !== 'word') {
|
|
2901
|
+
end++;
|
|
2902
|
+
}
|
|
2903
|
+
// A run reads as a token also when only its pieces joined do.
|
|
2904
|
+
if (kinds.slice(start, end).indexOf('token') !== -1 || isToken(raws.slice(start, end).join(''))) {
|
|
2905
|
+
for (var k = start; k < end; k++) {
|
|
2906
|
+
masked[k] = true;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
start = end;
|
|
2910
|
+
}
|
|
2911
|
+
if (masked.indexOf(true) === -1 || kinds.indexOf('word') === -1) {
|
|
2912
|
+
return '*';
|
|
2913
|
+
}
|
|
2914
|
+
var out = '';
|
|
2915
|
+
for (var k = 0; k < raws.length; k++) {
|
|
2916
|
+
if (masked[k] && k > 0 && masked[k - 1]) {
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
out += (k > 0 ? separators[k - 1] : '') + (masked[k] ? '*' : raws[k]);
|
|
2920
|
+
}
|
|
2921
|
+
return out;
|
|
2922
|
+
}
|
|
2835
2923
|
// The path a click logs. A segment that can single out one shopper
|
|
2836
|
-
// goes as `*`: one holding an @, `;` or `=`,
|
|
2837
|
-
// that names a person, carries a code or opens an order.
|
|
2838
|
-
// exactly as the page has them, plain
|
|
2839
|
-
// they name the product.
|
|
2924
|
+
// goes as `*`: one holding an @, `;` or `=`, an id, or the one after a route
|
|
2925
|
+
// that names a person, carries a code or opens an order. A token inside a name
|
|
2926
|
+
// goes as `*` alone. Kept segments stay exactly as the page has them, plain
|
|
2927
|
+
// numbers included, since on a product page they name the product.
|
|
2840
2928
|
function pagePath(pathname) {
|
|
2841
2929
|
var segments = pathname.split('/');
|
|
2842
2930
|
return segments.map(function (segment, i) {
|
|
@@ -2844,9 +2932,13 @@ var mParticle = (function () {
|
|
|
2844
2932
|
var route = i > 0 ? routeWord(routeSegment(segments, i)) : '';
|
|
2845
2933
|
// A route word that can't be decoded may be any route, so the
|
|
2846
2934
|
// segment after it is hidden too.
|
|
2847
|
-
var hiddenByRoute = i > 0 && !!text && (route === undefined ||
|
|
2935
|
+
var hiddenByRoute = i > 0 && !!text && (route === undefined || hidesSegmentAfter(route, text));
|
|
2848
2936
|
// An encoded slash hides several segments in one, so it goes whole.
|
|
2849
|
-
|
|
2937
|
+
if (text === undefined || /[@;=/]/.test(text) || LEFTOVER_ESCAPE.test(text) || hiddenByRoute) {
|
|
2938
|
+
return '*';
|
|
2939
|
+
}
|
|
2940
|
+
var masked = maskTokenSegment(segment, text, route);
|
|
2941
|
+
return masked === undefined ? segment : masked;
|
|
2850
2942
|
}).join('/');
|
|
2851
2943
|
}
|
|
2852
2944
|
// A token long enough that no page or product name looks like it: 16 or more
|
|
@@ -3496,7 +3588,9 @@ var mParticle = (function () {
|
|
|
3496
3588
|
title: title,
|
|
3497
3589
|
isAutoPageView: this.isAutoPageView
|
|
3498
3590
|
}));
|
|
3499
|
-
|
|
3591
|
+
// A title can name the shopper or an order, so it stays out of the log
|
|
3592
|
+
// line, for the same reason describePage omits param values.
|
|
3593
|
+
this.log("[fire] deferred flush -> _Events.logEvent(PageView) (page: ".concat(describePage(page), ")"));
|
|
3500
3594
|
this.mpInstance._Events.logEvent(event);
|
|
3501
3595
|
};
|
|
3502
3596
|
PageViewTracker.prototype.log = function (message) {
|
|
@@ -4270,13 +4364,12 @@ var mParticle = (function () {
|
|
|
4270
4364
|
// TODO: https://go.mparticle.com/work/SQDSDKS-5165
|
|
4271
4365
|
(_d = this.batchesQueuedForProcessing).unshift.apply(_d, batchesThatDidNotUpload);
|
|
4272
4366
|
}
|
|
4273
|
-
// Update Offline Storage with current state of batch queue
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
// Local Storage once we can confirm they are successfully uploaded.
|
|
4367
|
+
// Update Offline Storage with current state of batch queue. Storage was
|
|
4368
|
+
// purged before sending. A beacon the browser accepted counts as sent, so
|
|
4369
|
+
// the beacon path writes nothing unless the browser refused a batch: then
|
|
4370
|
+
// `batchesThatDidNotUpload` holds it, and it is stored since the page may
|
|
4371
|
+
// be going away.
|
|
4372
|
+
if (this.offlineStorageEnabled && this.batchVault && (!useBeacon || !isEmpty(batchesThatDidNotUpload))) {
|
|
4280
4373
|
this.storeBatchesQueuedForProcessing();
|
|
4281
4374
|
}
|
|
4282
4375
|
if (triggerFuture && !this.destroyed) {
|
|
@@ -4333,7 +4426,7 @@ var mParticle = (function () {
|
|
|
4333
4426
|
};
|
|
4334
4427
|
BatchUploader.prototype.uploadBatches = function (batches, useBeacon) {
|
|
4335
4428
|
return __awaiter(this, void 0, void 0, function () {
|
|
4336
|
-
var uploads, uploadsToLog, i, fetchPayload, blob, response, e_1;
|
|
4429
|
+
var uploads, uploadsToLog, refused, i, fetchPayload, blob, response, e_1;
|
|
4337
4430
|
return __generator(this, function (_a) {
|
|
4338
4431
|
switch (_a.label) {
|
|
4339
4432
|
case 0:
|
|
@@ -4348,6 +4441,7 @@ var mParticle = (function () {
|
|
|
4348
4441
|
this.mpInstance.Logger.verbose("Uploading batches: ".concat(JSON.stringify(uploadsToLog)));
|
|
4349
4442
|
this.mpInstance.Logger.verbose("Batch count: ".concat(uploads.length));
|
|
4350
4443
|
}
|
|
4444
|
+
refused = [];
|
|
4351
4445
|
i = 0;
|
|
4352
4446
|
_a.label = 1;
|
|
4353
4447
|
case 1:
|
|
@@ -4364,7 +4458,13 @@ var mParticle = (function () {
|
|
|
4364
4458
|
blob = new Blob([fetchPayload.body], {
|
|
4365
4459
|
type: 'text/plain;charset=UTF-8'
|
|
4366
4460
|
});
|
|
4367
|
-
|
|
4461
|
+
// The browser refuses a beacon it won't send, such as one over its
|
|
4462
|
+
// size limit, which says nothing about the next batch. Keep it and
|
|
4463
|
+
// try the rest. Only false is a refusal: a wrapper that returns
|
|
4464
|
+
// nothing has still sent it.
|
|
4465
|
+
if (navigator.sendBeacon(this.uploadUrl, blob) === false) {
|
|
4466
|
+
refused.push(uploads[i]);
|
|
4467
|
+
}
|
|
4368
4468
|
return [3 /*break*/, 5];
|
|
4369
4469
|
case 2:
|
|
4370
4470
|
_a.trys.push([2, 4,, 5]);
|
|
@@ -4395,6 +4495,10 @@ var mParticle = (function () {
|
|
|
4395
4495
|
i++;
|
|
4396
4496
|
return [3 /*break*/, 1];
|
|
4397
4497
|
case 6:
|
|
4498
|
+
if (!isEmpty(refused)) {
|
|
4499
|
+
this.mpInstance.Logger.warning("The browser refused the exit upload of ".concat(refused.length, " batch(es). Keeping them for the next upload."));
|
|
4500
|
+
return [2 /*return*/, refused];
|
|
4501
|
+
}
|
|
4398
4502
|
return [2 /*return*/, null];
|
|
4399
4503
|
}
|
|
4400
4504
|
});
|
|
@@ -12742,7 +12846,11 @@ var mParticle = (function () {
|
|
|
12742
12846
|
}
|
|
12743
12847
|
var segments = path.split('/');
|
|
12744
12848
|
var masked = segments.map(function (segment, i) {
|
|
12745
|
-
|
|
12849
|
+
if (i > 0 && segment && hidesSegmentAfter(routeSegment(segments, i), segment)) {
|
|
12850
|
+
return '*';
|
|
12851
|
+
}
|
|
12852
|
+
var tokens = maskTokenSegment(segment, segment, i > 0 ? routeSegment(segments, i) : undefined);
|
|
12853
|
+
return tokens === undefined ? segment : tokens;
|
|
12746
12854
|
});
|
|
12747
12855
|
return maskNumbers(masked.join('/'));
|
|
12748
12856
|
}
|
|
@@ -12887,8 +12995,7 @@ var mParticle = (function () {
|
|
|
12887
12995
|
fileExtension: link ? link.fileExtension : undefined,
|
|
12888
12996
|
// The box as the click left it: the browser toggles it before the click
|
|
12889
12997
|
// reaches listeners, and puts it back if the click is cancelled.
|
|
12890
|
-
checked: box ? box.checked : undefined
|
|
12891
|
-
pick: choice || control.matches('[role~="tab"]') ? true : undefined
|
|
12998
|
+
checked: box ? box.checked : undefined
|
|
12892
12999
|
};
|
|
12893
13000
|
}
|
|
12894
13001
|
// The question a picker answers, from the group around it: the group's
|
|
@@ -13045,7 +13152,10 @@ var mParticle = (function () {
|
|
|
13045
13152
|
|
|
13046
13153
|
// A checkout segment may name its kind or its version, as /express-checkout, /checkout-v2 and /checkout-external do.
|
|
13047
13154
|
// Other hyphenated words make it a content slug, such as /help/checkout-faq or /products/self-checkout-kiosk.
|
|
13048
|
-
var
|
|
13155
|
+
var CHECKOUT_SEGMENT = '(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?' + '(?:-(?:v\\d+|\\d+|flow|external|process|step(?:-\\d+)?))?';
|
|
13156
|
+
var checkoutPath = new RegExp('(?:^|\\/)(?:' + CHECKOUT_SEGMENT + '|payment|order|kasse|paiement|commander?|pago)(?:\\/|$)', 'i');
|
|
13157
|
+
// A route with a checkout segment itself, not just a payment or order one, which account pages also use.
|
|
13158
|
+
var checkoutSegmentPath = new RegExp('(?:^|\\/)' + CHECKOUT_SEGMENT + '(?:\\/|$)', 'i');
|
|
13049
13159
|
// A confirmation page under a checkout route, such as /checkout/thankyou/<order> or /checkouts/<token>/thank_you, comes
|
|
13050
13160
|
// after the confirm click. "Complete" and "success" mark one only as the last word of a segment, so /complete-checkout
|
|
13051
13161
|
// and /checkout/complete-registration stay checkout routes.
|
|
@@ -13066,6 +13176,9 @@ var mParticle = (function () {
|
|
|
13066
13176
|
var linkCommits = new RegExp('^(?:(?:place|submit|complete)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase)|' + otherCommits + ')$');
|
|
13067
13177
|
var routeGatedCommits = new RegExp('^(?:buy now|purchase|register(?: now)?|complete (?:my |your )?registration|jetzt kaufen|kaufen|commander|(?:passer|valider) (?:la |ma )?commande|finalizar (?:la |mi )?compra|' + 'comprar ahora|acquista ora|nu kopen|\u4ECA\u3059\u3050\u8CFC\u5165|\u8CFC\u5165\u3059\u308B|\u3053\u306E\u5185\u5BB9\u3067\u7533\u3057\u8FBC\u3080)$');
|
|
13068
13178
|
var steps = new RegExp('^(?:' + ["(?:let's |lets |go to |proceed to |secure )?(?:check ?out|checkout)(?: now| securely| as guest)?", '(?:continue|proceed) to (?:payment|review|billing|shipping|delivery|order review)|review (?:my |your )?order', 'guest checkout|(?:pay|continue|check ?out) (?:with|by) (?:card|credit card|debit card|credit or debit card)', 'zur kasse|zur kasse gehen|auschecken|weiter zur zahlung', 'passer \u00E0 la caisse|continuer vers le paiement|proc\u00E9der au paiement', 'ir a caja|continuar al pago|tramitar pedido', "procedi al checkout|procedi all'acquisto|procedi al pagamento|vai alla cassa", 'afrekenen|verder naar betalen|doorgaan naar betalen', '\u30EC\u30B8\u306B\u9032\u3080|\u3054?(?:\u8CFC\u5165|\u6CE8\u6587)\u624B\u7D9A\u304D(?:\u3078|\u306B)(?:\u9032\u3080)?|\u304A?\u652F\u6255\u3044(?:\u3078|\u306B)\u9032\u3080|(?:\u3054\u6CE8\u6587\u5185\u5BB9\u306E)?\u78BA\u8A8D\u753B\u9762\u3078(?:\u9032\u3080)?'].join('|') + ')$');
|
|
13179
|
+
// "Save and continue" also saves profile, address and card forms, so it is a step only on a checkout segment's route
|
|
13180
|
+
// that isn't an account or confirmation page.
|
|
13181
|
+
var routeGatedSteps = /^save and continue$/;
|
|
13069
13182
|
var CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
|
|
13070
13183
|
// "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
|
|
13071
13184
|
var addToCartLabels = new RegExp('^add(?: [^ ]+){0,5} to ' + CART_NOUN + '$');
|
|
@@ -13272,7 +13385,7 @@ var mParticle = (function () {
|
|
|
13272
13385
|
source: 'brand'
|
|
13273
13386
|
};
|
|
13274
13387
|
}
|
|
13275
|
-
if (steps.test(text)) {
|
|
13388
|
+
if (steps.test(text) || isButton && routeGatedSteps.test(text) && checkoutSegmentPath.test(pathname) && !confirmationPath.test(pathname) && !accountStepPath.test(pathname)) {
|
|
13276
13389
|
return {
|
|
13277
13390
|
kind: 'checkout_step',
|
|
13278
13391
|
source: 'label'
|
|
@@ -13366,9 +13479,6 @@ var mParticle = (function () {
|
|
|
13366
13479
|
// A ceiling, not a dedupe: a held Enter key or browser automation fires a
|
|
13367
13480
|
// trusted click per repeat, and each one is forwarded to every kit.
|
|
13368
13481
|
var PAGE_EVENT_LIMIT = 100;
|
|
13369
|
-
// A click uploads at once. Taps that follow it this quickly are held and share
|
|
13370
|
-
// one upload once they stop.
|
|
13371
|
-
var UPLOAD_DEBOUNCE_MS = 100;
|
|
13372
13482
|
var btnWindow = function btnWindow() {
|
|
13373
13483
|
return typeof window === 'undefined' ? null : window;
|
|
13374
13484
|
};
|
|
@@ -13486,10 +13596,6 @@ var mParticle = (function () {
|
|
|
13486
13596
|
this.pageEvents = 0;
|
|
13487
13597
|
// Clicks read at dispatch and waiting for the next paint.
|
|
13488
13598
|
this.pending = [];
|
|
13489
|
-
this.uploadTimerId = null;
|
|
13490
|
-
this.uploadHeld = false;
|
|
13491
|
-
// Set while the page is left, when the SDK's exit upload sends the queue.
|
|
13492
|
-
this.leaving = false;
|
|
13493
13599
|
this.drain = function () {
|
|
13494
13600
|
var queue = _this.pending;
|
|
13495
13601
|
_this.pending = [];
|
|
@@ -13503,14 +13609,8 @@ var mParticle = (function () {
|
|
|
13503
13609
|
// beforeunload, the first of them.
|
|
13504
13610
|
this.onHide = function (event) {
|
|
13505
13611
|
if (event.type === 'beforeunload' || event.type === 'pagehide' || document.visibilityState === 'hidden') {
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
_this.drain();
|
|
13509
|
-
} finally {
|
|
13510
|
-
_this.leaving = false;
|
|
13511
|
-
}
|
|
13512
|
-
// The SDK's exit upload sends the queue, held taps included.
|
|
13513
|
-
_this.cancelUpload();
|
|
13612
|
+
// The SDK's exit upload then sends these clicks with the queue.
|
|
13613
|
+
_this.drain();
|
|
13514
13614
|
}
|
|
13515
13615
|
};
|
|
13516
13616
|
this.onPageShow = function (event) {
|
|
@@ -13637,7 +13737,6 @@ var mParticle = (function () {
|
|
|
13637
13737
|
this.lastSent = {};
|
|
13638
13738
|
this.pagePath = null;
|
|
13639
13739
|
this.pageEvents = 0;
|
|
13640
|
-
this.cancelUpload();
|
|
13641
13740
|
};
|
|
13642
13741
|
ButtonTracker.prototype.enqueue = function (delivery) {
|
|
13643
13742
|
this.pending.push(delivery);
|
|
@@ -13781,47 +13880,9 @@ var mParticle = (function () {
|
|
|
13781
13880
|
this.pageEvents++;
|
|
13782
13881
|
// Restarts an expired session, as public logEvent() and page views do.
|
|
13783
13882
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
13784
|
-
|
|
13883
|
+
// No upload of its own, which a click that opens another page would
|
|
13884
|
+
// cancel: with batching on, the next batch or the exit upload sends it.
|
|
13785
13885
|
this.mpInstance._Events.logEvent(asLink ? buildLinkClickEvent(click, pathname) : buildButtonClickEvent(click, pathname));
|
|
13786
|
-
// Only a click waiting for the batch interval is sent early: a held
|
|
13787
|
-
// one goes out when released, and with batching off it already has.
|
|
13788
|
-
// A pick, such as a checkbox, a radio or a tab, waits for the batch,
|
|
13789
|
-
// since choosing an option doesn't leave the page the way a confirm
|
|
13790
|
-
// click can.
|
|
13791
|
-
if (!this.leaving && !click.pick && this.queuedForUpload() > queued) {
|
|
13792
|
-
this.uploadNow();
|
|
13793
|
-
}
|
|
13794
|
-
};
|
|
13795
|
-
// Events waiting in the upload queue for the next batch interval.
|
|
13796
|
-
ButtonTracker.prototype.queuedForUpload = function () {
|
|
13797
|
-
var uploader = this.mpInstance._APIClient.uploader;
|
|
13798
|
-
return uploader ? uploader.eventsQueuedForProcessing.length : 0;
|
|
13799
|
-
};
|
|
13800
|
-
// Uploads a click without waiting for the batch interval. A tap inside the
|
|
13801
|
-
// wait after the last one is held, and each restarts the wait, so a burst
|
|
13802
|
-
// after the first click goes out in one request once the taps stop.
|
|
13803
|
-
ButtonTracker.prototype.uploadNow = function () {
|
|
13804
|
-
var _this = this;
|
|
13805
|
-
if (this.uploadTimerId === null) {
|
|
13806
|
-
this.mpInstance.upload();
|
|
13807
|
-
} else {
|
|
13808
|
-
clearTimeout(this.uploadTimerId);
|
|
13809
|
-
this.uploadHeld = true;
|
|
13810
|
-
}
|
|
13811
|
-
this.uploadTimerId = setTimeout(function () {
|
|
13812
|
-
_this.uploadTimerId = null;
|
|
13813
|
-
if (_this.uploadHeld) {
|
|
13814
|
-
_this.uploadHeld = false;
|
|
13815
|
-
_this.mpInstance.upload();
|
|
13816
|
-
}
|
|
13817
|
-
}, UPLOAD_DEBOUNCE_MS);
|
|
13818
|
-
};
|
|
13819
|
-
ButtonTracker.prototype.cancelUpload = function () {
|
|
13820
|
-
if (this.uploadTimerId !== null) {
|
|
13821
|
-
clearTimeout(this.uploadTimerId);
|
|
13822
|
-
this.uploadTimerId = null;
|
|
13823
|
-
}
|
|
13824
|
-
this.uploadHeld = false;
|
|
13825
13886
|
};
|
|
13826
13887
|
ButtonTracker.prototype.emit = function (kind, source, layer, t, pathname, adds) {
|
|
13827
13888
|
if (this.isKitSuppressed()) {
|
|
@@ -19,7 +19,6 @@ export interface IButtonClick {
|
|
|
19
19
|
linkPath?: string;
|
|
20
20
|
fileExtension?: string;
|
|
21
21
|
checked?: boolean;
|
|
22
|
-
pick?: true;
|
|
23
22
|
}
|
|
24
23
|
export declare function isLinkClick(click: IButtonClick): boolean;
|
|
25
24
|
export declare function describeButtonClick(control: Element, path?: Array<EventTarget | null>): IButtonClick | undefined;
|
|
@@ -20,9 +20,6 @@ export declare class ButtonTracker {
|
|
|
20
20
|
private pagePath;
|
|
21
21
|
private pageEvents;
|
|
22
22
|
private pending;
|
|
23
|
-
private uploadTimerId;
|
|
24
|
-
private uploadHeld;
|
|
25
|
-
private leaving;
|
|
26
23
|
constructor(mpInstance: IMParticleWebSDKInstance);
|
|
27
24
|
get isActive(): boolean;
|
|
28
25
|
init(options: IButtonTrackerOptions): void;
|
|
@@ -37,9 +34,6 @@ export declare class ButtonTracker {
|
|
|
37
34
|
private readonly onEvent;
|
|
38
35
|
private observe;
|
|
39
36
|
private logClick;
|
|
40
|
-
private queuedForUpload;
|
|
41
|
-
private uploadNow;
|
|
42
|
-
private cancelUpload;
|
|
43
37
|
private emit;
|
|
44
38
|
private pickPayment;
|
|
45
39
|
private eventsEnabled;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare const LEFTOVER_ESCAPE: RegExp;
|
|
2
|
+
export declare function hidesSegmentAfter(route: string, segment: string): boolean;
|
|
2
3
|
export declare function hidesNextSegment(route: string): boolean;
|
|
3
4
|
export declare function decodeFully(text: string): string | undefined;
|
|
4
5
|
export declare function routeSegment(segments: string[], i: number): string;
|
|
5
6
|
export declare function isTokenSegment(segment: string): boolean;
|
|
7
|
+
export declare function maskTokenSegment(segment: string, text: string, route: string | undefined): string | undefined;
|
|
6
8
|
export declare function pagePath(pathname: string): string;
|
|
7
9
|
export declare function pageUrlPath(pathname: string): string;
|
package/package.json
CHANGED
package/src/batchUploader.ts
CHANGED
|
@@ -479,14 +479,16 @@ export class BatchUploader {
|
|
|
479
479
|
this.batchesQueuedForProcessing.unshift(...batchesThatDidNotUpload);
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
-
// Update Offline Storage with current state of batch queue
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
482
|
+
// Update Offline Storage with current state of batch queue. Storage was
|
|
483
|
+
// purged before sending. A beacon the browser accepted counts as sent, so
|
|
484
|
+
// the beacon path writes nothing unless the browser refused a batch: then
|
|
485
|
+
// `batchesThatDidNotUpload` holds it, and it is stored since the page may
|
|
486
|
+
// be going away.
|
|
487
|
+
if (
|
|
488
|
+
this.offlineStorageEnabled &&
|
|
489
|
+
this.batchVault &&
|
|
490
|
+
(!useBeacon || !isEmpty(batchesThatDidNotUpload))
|
|
491
|
+
) {
|
|
490
492
|
this.storeBatchesQueuedForProcessing();
|
|
491
493
|
}
|
|
492
494
|
|
|
@@ -570,6 +572,7 @@ export class BatchUploader {
|
|
|
570
572
|
this.mpInstance.Logger.verbose(`Batch count: ${uploads.length}`);
|
|
571
573
|
}
|
|
572
574
|
|
|
575
|
+
const refused: Batch[] = [];
|
|
573
576
|
for (let i = 0; i < uploads.length; i++) {
|
|
574
577
|
const fetchPayload: IFetchPayload = {
|
|
575
578
|
method: 'POST',
|
|
@@ -586,7 +589,13 @@ export class BatchUploader {
|
|
|
586
589
|
type: 'text/plain;charset=UTF-8',
|
|
587
590
|
});
|
|
588
591
|
|
|
589
|
-
|
|
592
|
+
// The browser refuses a beacon it won't send, such as one over its
|
|
593
|
+
// size limit, which says nothing about the next batch. Keep it and
|
|
594
|
+
// try the rest. Only false is a refusal: a wrapper that returns
|
|
595
|
+
// nothing has still sent it.
|
|
596
|
+
if (navigator.sendBeacon(this.uploadUrl, blob) === false) {
|
|
597
|
+
refused.push(uploads[i]);
|
|
598
|
+
}
|
|
590
599
|
} else {
|
|
591
600
|
try {
|
|
592
601
|
const response = await this.uploader.upload(fetchPayload);
|
|
@@ -629,6 +638,13 @@ export class BatchUploader {
|
|
|
629
638
|
}
|
|
630
639
|
}
|
|
631
640
|
}
|
|
641
|
+
|
|
642
|
+
if (!isEmpty(refused)) {
|
|
643
|
+
this.mpInstance.Logger.warning(
|
|
644
|
+
`The browser refused the exit upload of ${refused.length} batch(es). Keeping them for the next upload.`
|
|
645
|
+
);
|
|
646
|
+
return refused;
|
|
647
|
+
}
|
|
632
648
|
return null;
|
|
633
649
|
}
|
|
634
650
|
}
|
|
@@ -10,9 +10,9 @@ import {
|
|
|
10
10
|
import { maskLabel, maskNumbers, MAX_RAW_LABEL_LENGTH } from './labelMasking';
|
|
11
11
|
import {
|
|
12
12
|
decodeFully,
|
|
13
|
-
|
|
14
|
-
isTokenSegment,
|
|
13
|
+
hidesSegmentAfter,
|
|
15
14
|
LEFTOVER_ESCAPE,
|
|
15
|
+
maskTokenSegment,
|
|
16
16
|
routeSegment,
|
|
17
17
|
} from '../pathMasking';
|
|
18
18
|
|
|
@@ -54,8 +54,6 @@ export interface IButtonClick {
|
|
|
54
54
|
linkPath?: string;
|
|
55
55
|
fileExtension?: string;
|
|
56
56
|
checked?: boolean;
|
|
57
|
-
// A pick among options or tabs, which waits for the batch interval.
|
|
58
|
-
pick?: true;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
function elementKind(control: Element): ButtonElementKind {
|
|
@@ -497,12 +495,21 @@ function linkPath(link: HTMLAnchorElement): string | undefined {
|
|
|
497
495
|
return undefined;
|
|
498
496
|
}
|
|
499
497
|
const segments = path.split('/');
|
|
500
|
-
const masked = segments.map((segment, i) =>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
498
|
+
const masked = segments.map((segment, i) => {
|
|
499
|
+
if (
|
|
500
|
+
i > 0 &&
|
|
501
|
+
segment &&
|
|
502
|
+
hidesSegmentAfter(routeSegment(segments, i), segment)
|
|
503
|
+
) {
|
|
504
|
+
return '*';
|
|
505
|
+
}
|
|
506
|
+
const tokens = maskTokenSegment(
|
|
507
|
+
segment,
|
|
508
|
+
segment,
|
|
509
|
+
i > 0 ? routeSegment(segments, i) : undefined
|
|
510
|
+
);
|
|
511
|
+
return tokens === undefined ? segment : tokens;
|
|
512
|
+
});
|
|
506
513
|
return maskNumbers(masked.join('/'));
|
|
507
514
|
}
|
|
508
515
|
|
|
@@ -677,7 +684,6 @@ export function describeButtonClick(
|
|
|
677
684
|
// The box as the click left it: the browser toggles it before the click
|
|
678
685
|
// reaches listeners, and puts it back if the click is cancelled.
|
|
679
686
|
checked: box ? box.checked : undefined,
|
|
680
|
-
pick: choice || control.matches('[role~="tab"]') ? true : undefined,
|
|
681
687
|
};
|
|
682
688
|
}
|
|
683
689
|
|
|
@@ -32,9 +32,6 @@ const PATH_LIMIT = 20;
|
|
|
32
32
|
// A ceiling, not a dedupe: a held Enter key or browser automation fires a
|
|
33
33
|
// trusted click per repeat, and each one is forwarded to every kit.
|
|
34
34
|
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
35
|
|
|
39
36
|
interface IBtnState {
|
|
40
37
|
tracker?: ButtonTracker;
|
|
@@ -176,10 +173,6 @@ export class ButtonTracker {
|
|
|
176
173
|
private pageEvents = 0;
|
|
177
174
|
// Clicks read at dispatch and waiting for the next paint.
|
|
178
175
|
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
176
|
|
|
184
177
|
constructor(mpInstance: IMParticleWebSDKInstance) {
|
|
185
178
|
this.mpInstance = mpInstance;
|
|
@@ -323,7 +316,6 @@ export class ButtonTracker {
|
|
|
323
316
|
this.lastSent = {};
|
|
324
317
|
this.pagePath = null;
|
|
325
318
|
this.pageEvents = 0;
|
|
326
|
-
this.cancelUpload();
|
|
327
319
|
}
|
|
328
320
|
|
|
329
321
|
private enqueue(delivery: () => void): void {
|
|
@@ -349,14 +341,8 @@ export class ButtonTracker {
|
|
|
349
341
|
event.type === 'pagehide' ||
|
|
350
342
|
document.visibilityState === 'hidden'
|
|
351
343
|
) {
|
|
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();
|
|
344
|
+
// The SDK's exit upload then sends these clicks with the queue.
|
|
345
|
+
this.drain();
|
|
360
346
|
}
|
|
361
347
|
};
|
|
362
348
|
|
|
@@ -548,57 +534,13 @@ export class ButtonTracker {
|
|
|
548
534
|
this.pageEvents++;
|
|
549
535
|
// Restarts an expired session, as public logEvent() and page views do.
|
|
550
536
|
this.mpInstance._SessionManager.resetSessionTimer();
|
|
551
|
-
|
|
537
|
+
// No upload of its own, which a click that opens another page would
|
|
538
|
+
// cancel: with batching on, the next batch or the exit upload sends it.
|
|
552
539
|
this.mpInstance._Events.logEvent(
|
|
553
540
|
asLink
|
|
554
541
|
? buildLinkClickEvent(click, pathname)
|
|
555
542
|
: buildButtonClickEvent(click, pathname)
|
|
556
543
|
);
|
|
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
544
|
}
|
|
603
545
|
|
|
604
546
|
private emit(
|