@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/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.29.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
  });
@@ -12483,12 +12587,14 @@ var mParticle = (function () {
12483
12587
  function textOf(element) {
12484
12588
  return element && !isUnsafeTextSource(element) && isShown(element) ? renderedText(element) : '';
12485
12589
  }
12486
- // The control itself is already known to be shown.
12590
+ // The control itself is already known to be shown. An inline icon names
12591
+ // itself with aria-label rather than alt.
12487
12592
  function shownImageAlt(control) {
12488
- var images = control.querySelectorAll('img[alt]');
12593
+ var images = control.querySelectorAll('img[alt], svg[aria-label], [role="img"][aria-label]');
12489
12594
  for (var i = 0; i < images.length && i < MAX_READS; i++) {
12490
- if (isShown(images[i], control) && isVisible(images[i])) {
12491
- return nonBlank(images[i].getAttribute('alt'));
12595
+ var alt = nonBlank(images[i].getAttribute('alt') || images[i].getAttribute('aria-label'));
12596
+ if (alt && isShown(images[i], control) && isVisible(images[i])) {
12597
+ return alt;
12492
12598
  }
12493
12599
  }
12494
12600
  return undefined;
@@ -12742,7 +12848,11 @@ var mParticle = (function () {
12742
12848
  }
12743
12849
  var segments = path.split('/');
12744
12850
  var masked = segments.map(function (segment, i) {
12745
- return isTokenSegment(segment) || i > 0 && segment && hidesNextSegment(routeSegment(segments, i)) ? '*' : segment;
12851
+ if (i > 0 && segment && hidesSegmentAfter(routeSegment(segments, i), segment)) {
12852
+ return '*';
12853
+ }
12854
+ var tokens = maskTokenSegment(segment, segment, i > 0 ? routeSegment(segments, i) : undefined);
12855
+ return tokens === undefined ? segment : tokens;
12746
12856
  });
12747
12857
  return maskNumbers(masked.join('/'));
12748
12858
  }
@@ -12887,15 +12997,14 @@ var mParticle = (function () {
12887
12997
  fileExtension: link ? link.fileExtension : undefined,
12888
12998
  // The box as the click left it: the browser toggles it before the click
12889
12999
  // 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
13000
+ checked: box ? box.checked : undefined
12892
13001
  };
12893
13002
  }
12894
13003
  // The question a picker answers, from the group around it: the group's
12895
13004
  // aria-label, aria-labelledby or legend, masked like a label and cut at its
12896
13005
  // first colon, since a legend can repeat the pick ("Color: Navy"). Never the
12897
13006
  // option's own text, which is the shopper's answer.
12898
- var NAMED_GROUP = '[role="radiogroup"], [role="listbox"], [role="group"], fieldset';
13007
+ var NAMED_GROUP = '[role="radiogroup"], [role="listbox"], [role="group"], fieldset, ' + '[role="region"][aria-label], [role="region"][aria-labelledby]';
12899
13008
  // A control such as the button that opens a list box shows the option picked.
12900
13009
  var NOT_A_QUESTION = CHOICE + ', ' + CONTROL_SELECTOR + ', a[href], input, select, textarea, slot, [aria-haspopup], [role~="button"], ' + '[role~="link"], [role~="tab"], [role~="combobox"]';
12901
13010
  var OPTION_KINDS = ['radio', 'menuitemradio', 'checkbox', 'menuitemcheckbox', 'switch', 'option'];
@@ -12920,6 +13029,95 @@ var mParticle = (function () {
12920
13029
  var name = choice.getAttribute('name');
12921
13030
  return option instanceof HTMLInputElement && option.type === choice.type && !!name && option.getAttribute('name') === name && option.form === choice.form;
12922
13031
  }
13032
+ function isTransparent(element, until) {
13033
+ return someComposedAncestor(element, function (node) {
13034
+ return window.getComputedStyle(node).opacity === '0';
13035
+ }, until);
13036
+ }
13037
+ // Content that is never painted where it sits: SVG that is only referenced or
13038
+ // read, and a canvas's fallback, which some browsers still give boxes.
13039
+ var UNDRAWN = 'svg title, svg desc, svg metadata, svg defs, svg symbol, svg clipPath, ' + 'svg mask, svg pattern, svg marker, svg linearGradient, ' + 'svg radialGradient, svg filter, canvas';
13040
+ // Whether text sits in any closed details element outside its summary.
13041
+ function isInClosedDetails(text, parent) {
13042
+ var details = parent.closest('details:not([open])');
13043
+ while (details) {
13044
+ var summary = Array.from(details.children).filter(function (child) {
13045
+ return child.tagName === 'SUMMARY';
13046
+ })[0];
13047
+ if (!summary || !summary.contains(text)) {
13048
+ return true;
13049
+ }
13050
+ details = details.parentElement ? details.parentElement.closest('details:not([open])') : null;
13051
+ }
13052
+ return false;
13053
+ }
13054
+ // Whether the browser shows an element through every ancestor it renders
13055
+ // under, shadow trees included, which a walk of light ancestors can't see
13056
+ // into when a shadow root is closed. Where it can't say (checkVisibility
13057
+ // arrived in Chrome 105, Firefox 125 and Safari 17.4), the element counts as
13058
+ // not shown, so option rows name no group in older browsers.
13059
+ function isComposedVisible(element) {
13060
+ var check = element.checkVisibility;
13061
+ return isFunction(check) &&
13062
+ // Older Chrome versions read the first two names, newer browsers the rest.
13063
+ check.call(element, {
13064
+ checkOpacity: true,
13065
+ checkVisibilityCSS: true,
13066
+ opacityProperty: true,
13067
+ visibilityProperty: true
13068
+ });
13069
+ }
13070
+ // Whether the browser lays a text node out in a box of its own. Text it never
13071
+ // draws, such as an SVG definition, canvas fallback, closed details or an
13072
+ // unslotted child of a shadow host, has none. Where boxes can't be read, the
13073
+ // text counts as not drawn.
13074
+ function isDrawn(text) {
13075
+ var range = isFunction(document.createRange) ? document.createRange() : null;
13076
+ if (!range || !isFunction(range.getClientRects)) {
13077
+ return false;
13078
+ }
13079
+ range.selectNodeContents(text);
13080
+ return Array.from(range.getClientRects()).some(function (box) {
13081
+ return box.width > 0 && box.height > 0;
13082
+ });
13083
+ }
13084
+ // Whether a row shows text of its own: a drawn, shown, non-transparent text
13085
+ // node, outside any form field, editable area or opted-out part, beside
13086
+ // nothing but the row's own input. Not a row with a shadow root, slot or
13087
+ // custom element, whose rendered text a walk of its light children can't see.
13088
+ function showsText(row, own) {
13089
+ if (isUnsafeTextSource(row, own) || hasComposedAncestor(own, '[data-mp-no-track]') || row.shadowRoot || row.tagName.indexOf('-') !== -1 || row.querySelector('slot')) {
13090
+ return false;
13091
+ }
13092
+ // A row too large to check in full counts as showing nothing.
13093
+ var descendants = row.querySelectorAll('*');
13094
+ if (descendants.length > MAX_READS) {
13095
+ return false;
13096
+ }
13097
+ for (var i = 0; i < descendants.length; i++) {
13098
+ if (descendants[i].shadowRoot || descendants[i].tagName.indexOf('-') !== -1) {
13099
+ return false;
13100
+ }
13101
+ }
13102
+ var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT);
13103
+ for (var i = 0; i < MAX_READS && walker.nextNode(); i++) {
13104
+ var parent_1 = walker.currentNode.parentElement;
13105
+ if (parent_1 && nonBlank(walker.currentNode.nodeValue) &&
13106
+ // Undrawn content, and the body of a closed details element,
13107
+ // which browsers still give layout boxes.
13108
+ !parent_1.closest(UNDRAWN) && !isInClosedDetails(walker.currentNode, parent_1) && isShown(parent_1, row) && isVisible(parent_1) && !isTransparent(parent_1, row) && isComposedVisible(parent_1) && isDrawn(walker.currentNode)) {
13109
+ return true;
13110
+ }
13111
+ }
13112
+ return false;
13113
+ }
13114
+ // A native input inside a shown control of its own, such as a menu option row
13115
+ // with a styled radio, which shows text of its own and doesn't also hold the
13116
+ // option picked.
13117
+ function showsRow(option, choice, group) {
13118
+ var row = option instanceof HTMLInputElement && option.parentElement ? option.parentElement.closest(CONTROL_SELECTOR) : null;
13119
+ return !!row && group.contains(row) && !row.contains(choice) && isShown(row, group) && isVisible(row) && !isTransparent(row, group) && showsText(row, option);
13120
+ }
12923
13121
  // Whether a group offers another answer to the question picked: a shown option
12924
13122
  // to the same question that isn't part of the one picked. A card around one
12925
13123
  // address radio and a "make default" checkbox, or around a role radio and the
@@ -12928,12 +13126,11 @@ var mParticle = (function () {
12928
13126
  var options = group.querySelectorAll(CHOICE);
12929
13127
  for (var i = 0; i < options.length; i++) {
12930
13128
  var option = options[i];
12931
- if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) &&
13129
+ if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) && (
12932
13130
  // A fully transparent option, as a native input behind a styled
12933
- // one often is, shows the shopper no second answer.
12934
- !someComposedAncestor(option, function (node) {
12935
- return window.getComputedStyle(node).opacity === '0';
12936
- }, group)) {
13131
+ // one often is, shows the shopper no second answer, unless a
13132
+ // shown row around it does.
13133
+ !isTransparent(option, group) || showsRow(option, choice, group))) {
12937
13134
  return true;
12938
13135
  }
12939
13136
  }
@@ -12951,27 +13148,30 @@ var mParticle = (function () {
12951
13148
  });
12952
13149
  return labels;
12953
13150
  }
12954
- // The group asking the question a pick answers. A role radio's is its
12955
- // radiogroup and an option's its list box; a role toggle, such as a switch,
12956
- // stands alone. A native box's is the nearest group offering other answers,
12957
- // since a group around one option, such as a saved address card, can be
12958
- // named by that option.
12959
- function questionGroup(choice) {
13151
+ // The name of the group asking the question a pick answers. A role radio's
13152
+ // group is its radiogroup and an option's its list box; a role toggle, such as
13153
+ // a switch, stands alone. A native box's is the nearest group offering other
13154
+ // answers, since a group around one option, such as a saved address card, can
13155
+ // be named by that option. A region with no usable name is passed over, so a
13156
+ // group around it can still name the pick. Each group's name is read once.
13157
+ function groupName(choice) {
12960
13158
  if (!(choice instanceof HTMLInputElement)) {
12961
13159
  var container = choice.matches('[role~="option"]') ? choice.closest('[role="listbox"]') : choice.matches('[role~="radio"]') ? choice.closest('[role="radiogroup"]') : null;
12962
- return container && holdsOptions(container, choice) ? container : null;
13160
+ return container && holdsOptions(container, choice) ? nameOf(container) : undefined;
12963
13161
  }
12964
13162
  var group = choice.closest(NAMED_GROUP);
12965
- while (group && !holdsOptions(group, choice)) {
13163
+ while (group) {
13164
+ if (holdsOptions(group, choice)) {
13165
+ var name_2 = nameOf(group);
13166
+ if (name_2 || !group.matches('[role="region"]')) {
13167
+ return name_2;
13168
+ }
13169
+ }
12966
13170
  group = group.parentElement ? group.parentElement.closest(NAMED_GROUP) : null;
12967
13171
  }
12968
- return group;
13172
+ return undefined;
12969
13173
  }
12970
- function groupName(choice) {
12971
- var group = questionGroup(choice);
12972
- if (!group) {
12973
- return undefined;
12974
- }
13174
+ function nameOf(group) {
12975
13175
  var raw = nonBlank(group.getAttribute('aria-label'));
12976
13176
  if (!raw) {
12977
13177
  var ids = group.getAttribute('aria-labelledby');
@@ -12979,7 +13179,11 @@ var mParticle = (function () {
12979
13179
  return child.tagName === 'LEGEND';
12980
13180
  })[0] : undefined;
12981
13181
  var root_2 = isFunction(group.getRootNode) ? group.getRootNode() : document;
12982
- var source_1 = ids ? root_2.getElementById ? root_2.getElementById(ids.trim().split(/\s+/)[0]) : null : legend || null;
13182
+ var target = ids ? root_2.getElementById ? root_2.getElementById(ids.trim().split(/\s+/)[0]) : null : legend || null;
13183
+ // A region opened by an accordion header outside a tab list is named
13184
+ // by the header's first heading, never the rest of what it shows.
13185
+ var header_1 = target && group.matches('[role="region"]') && (target.getAttribute('role') || '').trim() === 'tab' && !hasComposedAncestor(target, '[role~="tablist"]') && !target.querySelector(NOT_A_QUESTION) ? target : null;
13186
+ var source_1 = header_1 ? header_1.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]') : target;
12983
13187
  // A label that is, holds or sits in an option or a control, or in an
12984
13188
  // option's label, would name the answer.
12985
13189
  var holder = source_1 ? source_1.closest('label') : null;
@@ -12990,7 +13194,9 @@ var mParticle = (function () {
12990
13194
  return source_1.contains(label) || label.contains(source_1);
12991
13195
  });
12992
13196
  });
12993
- raw = source_1 && !labelsOption && !hasComposedAncestor(source_1, NOT_A_QUESTION) && !source_1.querySelector(NOT_A_QUESTION) && !(holder && (holder.control || holder.querySelector(CHOICE))) ? textOf(source_1) : undefined;
13197
+ raw = source_1 && !labelsOption && !someComposedAncestor(source_1, function (node) {
13198
+ return node !== header_1 && node.matches(NOT_A_QUESTION);
13199
+ }) && !source_1.querySelector(NOT_A_QUESTION) && !(holder && (holder.control || holder.querySelector(CHOICE))) ? textOf(source_1) : undefined;
12994
13200
  }
12995
13201
  return raw ? maskLabel(raw.split(':')[0]) : undefined;
12996
13202
  }
@@ -13008,9 +13214,9 @@ var mParticle = (function () {
13008
13214
  return PAYMENT_QUESTION.test(nameWords(words));
13009
13215
  });
13010
13216
  }
13011
- // What names a payment option: the radio's accessible name, the text and
13012
- // image of each label, and its hand-written ids, text masked like a label.
13013
- function optionNames(radio) {
13217
+ // What names a payment option to shoppers: the radio's accessible name and the
13218
+ // text and image of each label, masked like a label.
13219
+ function shownOptionNames(radio) {
13014
13220
  var names = [maskLabel(accessibleName(radio).raw)];
13015
13221
  var labels = radio instanceof HTMLInputElement && radio.labels ? Array.from(radio.labels).slice(0, 2) : [];
13016
13222
  labels.forEach(function (label) {
@@ -13019,11 +13225,18 @@ var mParticle = (function () {
13019
13225
  names.push(maskLabel(shownImageAlt(label)));
13020
13226
  }
13021
13227
  });
13022
- names.push(stableIdentifier(radio.getAttribute('id')), firstIdentifier(radio, TEST_ID_ATTRIBUTES));
13228
+ return names;
13229
+ }
13230
+ function distinctNames(names) {
13023
13231
  return names.filter(function (name, index) {
13024
13232
  return !!name && names.indexOf(name) === index;
13025
13233
  });
13026
13234
  }
13235
+ // What names a payment option: the names shoppers read, then its hand-written
13236
+ // ids.
13237
+ function optionNames(radio) {
13238
+ return distinctNames(shownOptionNames(radio).concat(stableIdentifier(radio.getAttribute('id')), firstIdentifier(radio, TEST_ID_ATTRIBUTES)));
13239
+ }
13027
13240
  function describePaymentChoice(control, path) {
13028
13241
  if (path === void 0) {
13029
13242
  path = [];
@@ -13038,6 +13251,39 @@ var mParticle = (function () {
13038
13251
  names: names
13039
13252
  } : undefined;
13040
13253
  }
13254
+ var CHECKED_RADIO = 'input[type="radio"]:checked, [role~="radio"][aria-checked="true"], [role~="menuitemradio"][aria-checked="true"]';
13255
+ // The payment radio the page holds checked, such as a card option checked by
13256
+ // default that the shopper never clicked. A page holding more than one option
13257
+ // names no single method, so it describes nothing. One option drawn twice
13258
+ // counts once: a role radio wrapping its native input, a hidden input a
13259
+ // component library keeps beside its role radio, or one group rendered for two
13260
+ // layouts, whose copies share the names shoppers read but not their ids. An
13261
+ // option shoppers see no name for is told apart by its ids.
13262
+ function describeCheckedPaymentChoice() {
13263
+ var radios = Array.from(document.querySelectorAll(CHECKED_RADIO)).filter(function (radio) {
13264
+ return isPicked(radio) && radio.getAttribute('aria-hidden') !== 'true' && !hasComposedAncestor(radio, '[data-mp-no-track]') && isPaymentQuestion(radio);
13265
+ });
13266
+ var choices = radios.filter(function (radio) {
13267
+ return !radios.some(function (other) {
13268
+ return other !== radio && radio.contains(other);
13269
+ });
13270
+ }).map(function (radio) {
13271
+ var shown = distinctNames(shownOptionNames(radio));
13272
+ var names = optionNames(radio);
13273
+ return {
13274
+ radio: radio,
13275
+ names: names,
13276
+ option: (shown.length ? shown : names).join('\n')
13277
+ };
13278
+ });
13279
+ var options = new Set(choices.map(function (choice) {
13280
+ return choice.option;
13281
+ }));
13282
+ return options.size === 1 && choices[0].names.length ? {
13283
+ radio: choices[0].radio,
13284
+ names: choices[0].names
13285
+ } : undefined;
13286
+ }
13041
13287
  // Read once the page has handled the click, which may have cancelled it.
13042
13288
  function isPicked(radio) {
13043
13289
  return radio instanceof HTMLInputElement ? radio.checked : radio.getAttribute('aria-checked') === 'true';
@@ -13045,7 +13291,10 @@ var mParticle = (function () {
13045
13291
 
13046
13292
  // A checkout segment may name its kind or its version, as /express-checkout, /checkout-v2 and /checkout-external do.
13047
13293
  // 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;
13294
+ var CHECKOUT_SEGMENT = '(?:(?:express|guest|quick|secure|complete|one-?page|single-?page|mobile)-)?checkouts?' + '(?:-(?:v\\d+|\\d+|flow|external|process|step(?:-\\d+)?))?';
13295
+ var checkoutPath = new RegExp('(?:^|\\/)(?:' + CHECKOUT_SEGMENT + '|payment|order|kasse|paiement|commander?|pago)(?:\\/|$)', 'i');
13296
+ // A route with a checkout segment itself, not just a payment or order one, which account pages also use.
13297
+ var checkoutSegmentPath = new RegExp('(?:^|\\/)' + CHECKOUT_SEGMENT + '(?:\\/|$)', 'i');
13049
13298
  // A confirmation page under a checkout route, such as /checkout/thankyou/<order> or /checkouts/<token>/thank_you, comes
13050
13299
  // after the confirm click. "Complete" and "success" mark one only as the last word of a segment, so /complete-checkout
13051
13300
  // and /checkout/complete-registration stay checkout routes.
@@ -13066,6 +13315,9 @@ var mParticle = (function () {
13066
13315
  var linkCommits = new RegExp('^(?:(?:place|submit|complete)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase)|' + otherCommits + ')$');
13067
13316
  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
13317
  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('|') + ')$');
13318
+ // "Save and continue" also saves profile, address and card forms, so it is a step only on a checkout segment's route
13319
+ // that isn't an account or confirmation page.
13320
+ var routeGatedSteps = /^save and continue$/;
13069
13321
  var CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
13070
13322
  // "Add to bag" and "Add 2 to cart": a click that puts something in the cart.
13071
13323
  var addToCartLabels = new RegExp('^add(?: [^ ]+){0,5} to ' + CART_NOUN + '$');
@@ -13078,6 +13330,9 @@ var mParticle = (function () {
13078
13330
  var walletPhrases = new RegExp('^(?:(?:' + WALLET_VERBS + ')(?: now| securely)? (?:with|using|via|through|by) (?:' + WALLET_NAMES + ')(?: express(?: checkout)?| checkout)?|(?:continue|proceed|go) (?:to|with) (?:' + WALLET_NAMES + '))$');
13079
13331
  // A wallet's own name on a button: "PayPal", "Klarna checkout", "Apple Pay logo".
13080
13332
  var walletBrands = new RegExp('^(?:' + WALLET_NAMES + ')(?: (?:checkout|check out|express(?: checkout)?|express pay|buy now|pay now|pay|now)' + '| (?:logo|icon|mark|button)(?: (?:logo|icon|mark|button))*)?$');
13333
+ // The wallets the Rokt kit reads from a click's label or identifiers, as words:
13334
+ // "Place order with PayPal!" and "paypal-pay-button" both name PayPal.
13335
+ var 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)(?= |$)/;
13081
13336
  // Exact HTTPS hosts of hosted wallet button frames.
13082
13337
  var walletFrameFamilies = {
13083
13338
  'pay.google.com': 'google_pay',
@@ -13272,7 +13527,7 @@ var mParticle = (function () {
13272
13527
  source: 'brand'
13273
13528
  };
13274
13529
  }
13275
- if (steps.test(text)) {
13530
+ if (steps.test(text) || isButton && routeGatedSteps.test(text) && checkoutSegmentPath.test(pathname) && !confirmationPath.test(pathname) && !accountStepPath.test(pathname)) {
13276
13531
  return {
13277
13532
  kind: 'checkout_step',
13278
13533
  source: 'label'
@@ -13281,6 +13536,24 @@ var mParticle = (function () {
13281
13536
  }
13282
13537
  return cart;
13283
13538
  }
13539
+ // The Rokt kit's own word split, so "PayPal!" and "paypal2" both read as
13540
+ // "paypal" here as they do there.
13541
+ function kitWords(value) {
13542
+ return (value || '').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().replace(/[^a-z]+/g, ' ').trim();
13543
+ }
13544
+ // A confirm control may name its wallet, so "Place order with PayPal" and a
13545
+ // "Place order" button with id paypal-pay-button are confirm clicks that still
13546
+ // name their own method. A logged click is judged on the fields the kit reads
13547
+ // a method from; a click that isn't logged, on the control's own label and
13548
+ // identifiers.
13549
+ function namesWallet(control, click) {
13550
+ var values = click ? [click.label, click.ariaLabel, click.elementId, click.elementName, click.testId] : labelSources(control).concat(['id', 'name'].concat(TEST_ID_ATTRIBUTES).map(function (attribute) {
13551
+ return control.getAttribute(attribute);
13552
+ }));
13553
+ return values.some(function (value) {
13554
+ return KIT_WALLET_WORDS.test(kitWords(value));
13555
+ });
13556
+ }
13284
13557
  // The host is the domain or one of its subdomains, never a look-alike suffix.
13285
13558
  function isOnDomain(host, domain) {
13286
13559
  return host === domain || host.length > domain.length && host.slice(host.length - domain.length - 1) === '.' + domain;
@@ -13366,9 +13639,6 @@ var mParticle = (function () {
13366
13639
  // A ceiling, not a dedupe: a held Enter key or browser automation fires a
13367
13640
  // trusted click per repeat, and each one is forwarded to every kit.
13368
13641
  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
13642
  var btnWindow = function btnWindow() {
13373
13643
  return typeof window === 'undefined' ? null : window;
13374
13644
  };
@@ -13486,10 +13756,6 @@ var mParticle = (function () {
13486
13756
  this.pageEvents = 0;
13487
13757
  // Clicks read at dispatch and waiting for the next paint.
13488
13758
  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
13759
  this.drain = function () {
13494
13760
  var queue = _this.pending;
13495
13761
  _this.pending = [];
@@ -13503,14 +13769,8 @@ var mParticle = (function () {
13503
13769
  // beforeunload, the first of them.
13504
13770
  this.onHide = function (event) {
13505
13771
  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();
13772
+ // The SDK's exit upload then sends these clicks with the queue.
13773
+ _this.drain();
13514
13774
  }
13515
13775
  };
13516
13776
  this.onPageShow = function (event) {
@@ -13637,7 +13897,6 @@ var mParticle = (function () {
13637
13897
  this.lastSent = {};
13638
13898
  this.pagePath = null;
13639
13899
  this.pageEvents = 0;
13640
- this.cancelUpload();
13641
13900
  };
13642
13901
  ButtonTracker.prototype.enqueue = function (delivery) {
13643
13902
  this.pending.push(delivery);
@@ -13669,6 +13928,7 @@ var mParticle = (function () {
13669
13928
  var source;
13670
13929
  var adds;
13671
13930
  var choice;
13931
+ var checkedNames;
13672
13932
  var family;
13673
13933
  var unconfirmedCommit = false;
13674
13934
  if (layer === 'click') {
@@ -13701,6 +13961,15 @@ var mParticle = (function () {
13701
13961
  source = intent ? intent.source : undefined;
13702
13962
  adds = intent ? intent.adds : undefined;
13703
13963
  choice = signalsKit ? describePaymentChoice(control, path) : undefined;
13964
+ // A confirm or checkout-step click picks no radio itself, so it
13965
+ // carries the one the page holds checked as read here. It is not
13966
+ // read again at delivery: the page's own handlers may clear or
13967
+ // replace it by then. A wallet tap names its own method, and
13968
+ // so does a confirm that names a wallet.
13969
+ if (!choice && (kind === 'commit' || kind === 'checkout_step') && !namesWallet(control, click)) {
13970
+ var checked = describeCheckedPaymentChoice();
13971
+ checkedNames = checked ? checked.names : undefined;
13972
+ }
13704
13973
  }
13705
13974
  this.lastClick = {
13706
13975
  t: t,
@@ -13755,6 +14024,8 @@ var mParticle = (function () {
13755
14024
  // with this pick.
13756
14025
  if (choice && isPicked(choice.radio)) {
13757
14026
  _this.pickPayment(t, pathname, choice.names);
14027
+ } else if (checkedNames) {
14028
+ _this.pickPayment(t, pathname, checkedNames);
13758
14029
  } else if (family) {
13759
14030
  _this.pickPayment(t, pathname, undefined, family);
13760
14031
  }
@@ -13781,47 +14052,9 @@ var mParticle = (function () {
13781
14052
  this.pageEvents++;
13782
14053
  // Restarts an expired session, as public logEvent() and page views do.
13783
14054
  this.mpInstance._SessionManager.resetSessionTimer();
13784
- var queued = this.queuedForUpload();
14055
+ // No upload of its own, which a click that opens another page would
14056
+ // cancel: with batching on, the next batch or the exit upload sends it.
13785
14057
  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
14058
  };
13826
14059
  ButtonTracker.prototype.emit = function (kind, source, layer, t, pathname, adds) {
13827
14060
  if (this.isKitSuppressed()) {