@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.js CHANGED
@@ -204,7 +204,7 @@ var mParticle = (function () {
204
204
  Base64: Base64$1
205
205
  };
206
206
 
207
- var version = "3.27.0";
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 `=`, a token, or the one after a route
2837
- // that names a person, carries a code or opens an order. Kept segments stay
2838
- // exactly as the page has them, plain numbers included, since on a product page
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 || hidesNextSegment(route));
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
- return text === undefined || /[@;=/]/.test(text) || LEFTOVER_ESCAPE.test(text) || isTokenSegment(text) || hiddenByRoute ? '*' : segment;
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
- this.log("[fire] deferred flush -> _Events.logEvent(PageView) (page: ".concat(describePage(page), ", title: ").concat(title, ")"));
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
- if (!useBeacon && this.offlineStorageEnabled && this.batchVault) {
4275
- // Note: since beacon is "Fire and forget" it will empty `batchesThatDidNotUplod`
4276
- // regardless of whether the batches were successfully uploaded or not. We should
4277
- // therefore NOT overwrite Offline Storage when beacon returns, so that we can retry
4278
- // uploading saved batches at a later time. Batches should only be removed from
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
- navigator.sendBeacon(this.uploadUrl, blob);
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
- return isTokenSegment(segment) || i > 0 && segment && hidesNextSegment(routeSegment(segments, i)) ? '*' : segment;
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 checkoutPath = /(?:^|\/)(?:(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?(?:-(?:v\d+|\d+|flow|external|process|step(?:-\d+)?))?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i;
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
- _this.leaving = true;
13507
- try {
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
- var queued = this.queuedForUpload();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "3.27.0",
3
+ "version": "3.28.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -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
- if (!useBeacon && this.offlineStorageEnabled && this.batchVault) {
484
- // Note: since beacon is "Fire and forget" it will empty `batchesThatDidNotUplod`
485
- // regardless of whether the batches were successfully uploaded or not. We should
486
- // therefore NOT overwrite Offline Storage when beacon returns, so that we can retry
487
- // uploading saved batches at a later time. Batches should only be removed from
488
- // Local Storage once we can confirm they are successfully uploaded.
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
- navigator.sendBeacon(this.uploadUrl, blob);
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
- hidesNextSegment,
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
- isTokenSegment(segment) ||
502
- (i > 0 && segment && hidesNextSegment(routeSegment(segments, i)))
503
- ? '*'
504
- : segment
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
- this.leaving = true;
353
- try {
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
- const queued = this.queuedForUpload();
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(