@mparticle/web-sdk 3.23.0 → 3.24.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.23.0";
207
+ var version = "3.24.0";
208
208
 
209
209
  var Constants = {
210
210
  sdkVersion: version,
@@ -315,7 +315,6 @@ var mParticle = (function () {
315
315
  aliasMaxWindow: 90,
316
316
  uploadInterval: 0 // Maximum milliseconds in between batch uploads, below 500 will mean immediate upload. The server returns this as a string, but we are using it as a number internally
317
317
  },
318
-
319
318
  DefaultBaseUrls: {
320
319
  v1SecureServiceUrl: 'jssdks.mparticle.com/v1/JS/',
321
320
  v2SecureServiceUrl: 'jssdks.mparticle.com/v2/JS/',
@@ -601,14 +600,13 @@ var mParticle = (function () {
601
600
  // @ts-ignore
602
601
  randomValue = window.crypto.getRandomValues(new Uint8Array(1)); // eslint-disable-line no-undef
603
602
  }
604
-
605
603
  if (randomValue) {
606
604
  // @ts-ignore
607
605
  return (a ^ randomValue[0] % 16 >> a / 4).toString(16);
608
606
  }
609
607
  return (a ^ Math.random() * 16 >> a / 4).toString(16);
610
608
  };
611
- var generateUniqueId = function generateUniqueId(a) {
609
+ var _generateUniqueId = function generateUniqueId(a) {
612
610
  // https://gist.github.com/jed/982883
613
611
  // Added support for crypto for better random
614
612
  if (a === void 0) {
@@ -624,7 +622,7 @@ var mParticle = (function () {
624
622
  // -1e11 -> //-100000000000,
625
623
  "".concat(1e7, "-").concat(1e3, "-").concat(4e3, "-").concat(8e3, "-").concat(1e11).replace(/[018]/g,
626
624
  // zeroes, ones, and eights with
627
- generateUniqueId // random hex digits
625
+ _generateUniqueId // random hex digits
628
626
  );
629
627
  };
630
628
  /**
@@ -930,7 +928,7 @@ var mParticle = (function () {
930
928
  * obfuscateData({ tags: ['premium', 'verified'] })
931
929
  * // Returns: { tags: ['string', 'string'] }
932
930
  */
933
- var obfuscateData = function obfuscateData(value) {
931
+ var _obfuscateData = function obfuscateData(value) {
934
932
  // Preserve null and undefined
935
933
  if (value === null || value === undefined) {
936
934
  return value;
@@ -938,7 +936,7 @@ var mParticle = (function () {
938
936
  // Handle arrays - recursively obfuscate each element
939
937
  if (Array.isArray(value)) {
940
938
  return value.map(function (item) {
941
- return obfuscateData(item);
939
+ return _obfuscateData(item);
942
940
  });
943
941
  }
944
942
  // Handle objects - recursively obfuscate each property
@@ -946,7 +944,7 @@ var mParticle = (function () {
946
944
  var obfuscated = {};
947
945
  for (var key in value) {
948
946
  if (value.hasOwnProperty(key)) {
949
- obfuscated[key] = obfuscateData(value[key]);
947
+ obfuscated[key] = _obfuscateData(value[key]);
950
948
  }
951
949
  }
952
950
  return obfuscated;
@@ -970,7 +968,7 @@ var mParticle = (function () {
970
968
  * // Returns: { email: 'string' }
971
969
  */
972
970
  var obfuscateDevData = function obfuscateDevData(data, isDevelopmentMode) {
973
- return isDevelopmentMode ? data : obfuscateData(data);
971
+ return isDevelopmentMode ? data : _obfuscateData(data);
974
972
  };
975
973
  // Standalone version of jQuery.extend, from https://github.com/dansdom/extend
976
974
  // https://go.mparticle.com/work/SQDSDKS-6047
@@ -2183,7 +2181,7 @@ var mParticle = (function () {
2183
2181
  application_info: {
2184
2182
  application_version: lastEvent.AppVersion,
2185
2183
  application_name: lastEvent.AppName,
2186
- "package": lastEvent.Package,
2184
+ package: lastEvent.Package,
2187
2185
  sideloaded_kits_count: mpInstance._Store.sideloadedKitsCount
2188
2186
  },
2189
2187
  device_info: {
@@ -2523,7 +2521,6 @@ var mParticle = (function () {
2523
2521
  //note: External Events DTO does not support the session mpids array as of this time.
2524
2522
  //spanning_mpids: sdkEvent.SessionMpids
2525
2523
  };
2526
-
2527
2524
  sessionEndEventData = Object.assign(sessionEndEventData, commonEventData);
2528
2525
  return {
2529
2526
  event_type: dist.EventTypeEnum.sessionEnd,
@@ -2652,7 +2649,7 @@ var mParticle = (function () {
2652
2649
  var commonEventData = convertBaseEventData(sdkEvent);
2653
2650
  var userAttributeChangeEvent = {
2654
2651
  user_attribute_name: sdkEvent.UserAttributeChanges.UserAttributeName,
2655
- "new": sdkEvent.UserAttributeChanges.New,
2652
+ new: sdkEvent.UserAttributeChanges.New,
2656
2653
  old: sdkEvent.UserAttributeChanges.Old,
2657
2654
  deleted: sdkEvent.UserAttributeChanges.Deleted,
2658
2655
  is_new_attribute: sdkEvent.UserAttributeChanges.IsNewAttribute
@@ -2666,7 +2663,7 @@ var mParticle = (function () {
2666
2663
  function convertUserIdentityChangeEvent(sdkEvent) {
2667
2664
  var commonEventData = convertBaseEventData(sdkEvent);
2668
2665
  var userIdentityChangeEvent = {
2669
- "new": {
2666
+ new: {
2670
2667
  identity_type: convertUserIdentityTypeToServerIdentityType(sdkEvent.UserIdentityChanges.New.IdentityType),
2671
2668
  identity: sdkEvent.UserIdentityChanges.New.Identity || null,
2672
2669
  timestamp_unixtime_ms: sdkEvent.Timestamp,
@@ -2886,23 +2883,9 @@ var mParticle = (function () {
2886
2883
  // Nothing has to be deleted, and there is no ordering in which half the state
2887
2884
  // survives the reset.
2888
2885
  var WIN_APV_KEY = '__mpApv__';
2889
- // The query params an auto page view may carry. An allowlist, not a denylist:
2890
- // partner URLs routinely hold order ids, email addresses and session tokens, and
2891
- // none of those should reach the event stream because someone forgot to exclude
2892
- // them. Anything absent from this list is dropped.
2893
- //
2894
- // SECURITY: `code`, `state` and `nonce` are the OAuth 2.0 / OIDC authorization
2895
- // code and the CSRF/replay tokens. They are credentials until redeemed, and
2896
- // attaching them here persists them in the event store and forwards them to
2897
- // every configured kit. They are on the list by explicit product decision \u2014
2898
- // deleting that line is the whole of the fix if that decision is revisited.
2886
+ // Parameters that identify which page a view is of. Authorization-flow and campaign
2887
+ // attribution parameters are deliberately absent; an input can add them explicitly.
2899
2888
  var ALLOWED_QUERY_PARAMS = [
2900
- // Campaign attribution
2901
- 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic', 'gad_source', 'gad_campaignid', 'mc_cid',
2902
- // Ad-network click ids
2903
- 'gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'ttclid', 'twclid', 'li_fat_id', 'dclid', 'srsltid', 'epik', 'ScCid', 'irclickid', 'rtid', 'rclid',
2904
- // OAuth / OIDC \u2014 see the SECURITY note above
2905
- 'client_id', 'redirect_uri', 'response_type', 'scope', 'state', 'code', 'nonce',
2906
2889
  // Pagination
2907
2890
  'page', 'limit', 'offset', 'cursor', 'per_page',
2908
2891
  // Site search term, as the common platforms spell it. Matching is
@@ -2916,6 +2899,22 @@ var mParticle = (function () {
2916
2899
  'category',
2917
2900
  // Referral
2918
2901
  'ref', 'referrer'];
2902
+ var AUTO_PAGE_VIEW_ATTRIBUTE = 'is_auto_page_view';
2903
+ var CORE_EVENT_FIELDS = ['hostname', 'title', 'path', AUTO_PAGE_VIEW_ATTRIBUTE];
2904
+ var PROTOTYPE_MEMBER_NAMES = ['constructor', '__proto__', 'prototype'];
2905
+ // Server flag values are the strings 'True' and 'False', so these get typed here by habit.
2906
+ var FLAG_VALUE_NAMES = ['true', 'false'];
2907
+ var RESERVED_QUERY_PARAMS = CORE_EVENT_FIELDS.concat(PROTOTYPE_MEMBER_NAMES, FLAG_VALUE_NAMES);
2908
+ var BUILT_IN_NAMES = ALLOWED_QUERY_PARAMS.map(function (name) {
2909
+ return name.toLowerCase();
2910
+ });
2911
+ var isBuiltIn = function isBuiltIn(name) {
2912
+ return BUILT_IN_NAMES.indexOf(name.toLowerCase()) !== -1;
2913
+ };
2914
+ // Excludes `&`, `=`, `%` and whitespace, which would distort the dedup key.
2915
+ var QUERY_PARAM_NAME = /^[a-z0-9_][a-z0-9_.-]{0,63}$/;
2916
+ var MAX_CUSTOM_QUERY_PARAMS = 25;
2917
+ var MAX_CUSTOM_QUERY_PARAM_VALUE_LENGTH = 512;
2919
2918
  // Params matched by name prefix, for platforms that put a per-product id in the
2920
2919
  // name. Salesforce Commerce Cloud spells a variant choice
2921
2920
  // `dwvar_<productId>_<attribute>`, e.g. `dwvar_12345_color=navy`.
@@ -2925,34 +2924,113 @@ var mParticle = (function () {
2925
2924
  return name.indexOf(prefix) === 0;
2926
2925
  });
2927
2926
  };
2928
- var ALLOWED_QUERY_PARAMS_LOWER = ALLOWED_QUERY_PARAMS.map(function (name) {
2929
- return name.toLowerCase();
2930
- });
2931
- // Whether a query param name, in any case, is one an auto page view captures.
2927
+ // Whether a query param name, in any case, is one an auto page view captures by default.
2932
2928
  var isAllowedQueryParam = function isAllowedQueryParam(name) {
2933
- var lower = name.toLowerCase();
2934
- return ALLOWED_QUERY_PARAMS_LOWER.indexOf(lower) !== -1 || hasAllowedPrefix(lower);
2929
+ return isBuiltIn(name) || hasAllowedPrefix(name.toLowerCase());
2935
2930
  };
2936
2931
  // ---------------------------------------------------------------------------
2937
2932
  // Pure helpers. No `this`, no globals, no side effects \u2014 callable and assertable
2938
2933
  // on their own, which is where the interesting rules live.
2939
2934
  // ---------------------------------------------------------------------------
2940
- // Pulls the allowlisted query params off a URL. Delegates to the SDK's own
2941
- // parser, which lowercases keys (so `?UTM_Source=` and `?utm_source=` land on one
2942
- // attribute), drops empty values, and carries the fallback for browsers without
2943
- // URLSearchParams. Tolerates an empty href, so SSR yields no params rather than
2944
- // throwing.
2945
- var allowedQueryParams = function allowedQueryParams(href) {
2946
- var params = queryStringParser(href, ALLOWED_QUERY_PARAMS);
2935
+ // Validated here as well as in the dashboard, because remote config is untrusted input.
2936
+ var parseQueryParamAllowlist = function parseQueryParamAllowlist(configured) {
2937
+ var allowed = [];
2938
+ var rejectedPositions = [];
2939
+ var overLimit = 0;
2940
+ if (!configured) {
2941
+ return {
2942
+ allowed: allowed,
2943
+ rejectedPositions: rejectedPositions,
2944
+ overLimit: overLimit
2945
+ };
2946
+ }
2947
+ var entries = Array.isArray(configured) ? configured : String(configured).split(',');
2948
+ entries.forEach(function (entry, index) {
2949
+ var position = index + 1;
2950
+ var name = String(entry).trim().toLowerCase();
2951
+ if (!name) {
2952
+ return;
2953
+ }
2954
+ if (!QUERY_PARAM_NAME.test(name) || RESERVED_QUERY_PARAMS.indexOf(name) !== -1) {
2955
+ rejectedPositions.push(position);
2956
+ return;
2957
+ }
2958
+ if (isBuiltIn(name) || allowed.indexOf(name) !== -1) {
2959
+ return;
2960
+ }
2961
+ if (allowed.length >= MAX_CUSTOM_QUERY_PARAMS) {
2962
+ overLimit++;
2963
+ return;
2964
+ }
2965
+ allowed.push(name);
2966
+ });
2967
+ return {
2968
+ allowed: allowed,
2969
+ rejectedPositions: rejectedPositions,
2970
+ overLimit: overLimit
2971
+ };
2972
+ };
2973
+ // Not localeCompare: this order feeds the dedup key, so it must be identical in every
2974
+ // browser, and localeCompare is locale-dependent.
2975
+ var byName = function byName(a, b) {
2976
+ return a < b ? -1 : a > b ? 1 : 0;
2977
+ };
2978
+ // Built-ins keep their positions and additions sort after them, so a dedup key depends on
2979
+ // which params are configured, not the order they were typed.
2980
+ var effectiveAllowlist = function effectiveAllowlist(extras) {
2981
+ if (extras === void 0) {
2982
+ extras = [];
2983
+ }
2984
+ return ALLOWED_QUERY_PARAMS.concat(extras.filter(function (name) {
2985
+ return !isBuiltIn(name);
2986
+ }).sort(byName));
2987
+ };
2988
+ // Over-long custom values are dropped rather than truncated: two truncated values could
2989
+ // share a dedup key and swallow a real page view.
2990
+ var isWithinValueLimit = function isWithinValueLimit(name, value) {
2991
+ return isBuiltIn(name) || value.length <= MAX_CUSTOM_QUERY_PARAM_VALUE_LENGTH;
2992
+ };
2993
+ // A prefix match is a platform name, not a configured extra, so the custom
2994
+ // value limit does not apply. A name already on the allowlist keeps that
2995
+ // position instead of being appended again.
2996
+ var prefixMatchedParams = function prefixMatchedParams(href, allowlist) {
2947
2997
  var allParams = queryStringParser(href);
2948
- Object.keys(allParams).filter(function (name) {
2949
- return hasAllowedPrefix(name) && allParams[name];
2950
- }).forEach(function (name) {
2951
- params[name] = allParams[name];
2998
+ return Object.keys(allParams).filter(function (name) {
2999
+ return hasAllowedPrefix(name) && allParams[name] && allowlist.indexOf(name) === -1;
3000
+ }).sort(byName).map(function (name) {
3001
+ return {
3002
+ name: name,
3003
+ value: allParams[name]
3004
+ };
2952
3005
  });
2953
- return params;
2954
3006
  };
2955
- var AUTO_PAGE_VIEW_ATTRIBUTE = 'is_auto_page_view';
3007
+ var allowedQueryParams = function allowedQueryParams(href, extras) {
3008
+ if (extras === void 0) {
3009
+ extras = [];
3010
+ }
3011
+ var allowlist = effectiveAllowlist(extras);
3012
+ var found = queryStringParser(href, allowlist);
3013
+ var listed = allowlist.filter(function (name) {
3014
+ return hasOwnProp(found, name);
3015
+ }).filter(function (name) {
3016
+ return isWithinValueLimit(name, found[name]);
3017
+ }).map(function (name) {
3018
+ return {
3019
+ name: name,
3020
+ value: found[name]
3021
+ };
3022
+ });
3023
+ return listed.concat(prefixMatchedParams(href, allowlist));
3024
+ };
3025
+ var paramsToAttributes = function paramsToAttributes(params) {
3026
+ var attributes = {};
3027
+ params.forEach(function (_a) {
3028
+ var name = _a.name,
3029
+ value = _a.value;
3030
+ attributes[name] = value;
3031
+ });
3032
+ return attributes;
3033
+ };
2956
3034
  // Absent rather than false on a manual page view, so the attribute is only ever
2957
3035
  // added by the automatic emitters. Shared by both of them so the key and that
2958
3036
  // rule have one definition instead of two that can drift.
@@ -2960,33 +3038,13 @@ var mParticle = (function () {
2960
3038
  var _a;
2961
3039
  return isAutoPageView ? (_a = {}, _a[AUTO_PAGE_VIEW_ATTRIBUTE] = true, _a) : {};
2962
3040
  };
2963
- // The captured params, in allowlist order, then prefix-matched names sorted.
2964
- // Neither depends on the object's insertion order, so reordering the query
2965
- // string cannot produce a different key.
2966
- //
2967
- // Membership is an own-property check, not `in`. `in` walks the prototype
2968
- // chain, so a name matching an Object.prototype member reports as present on
2969
- // any plain object. ALLOWED_QUERY_PARAMS contains no such name, which was the
2970
- // only thing making `in` safe here \u2014 and it stops being a safe assumption the
2971
- // moment this list can be extended from configuration.
2972
- var capturedNames = function capturedNames(params) {
2973
- return ALLOWED_QUERY_PARAMS.filter(function (name) {
2974
- return hasOwnProp(params, name);
2975
- }).concat(Object.keys(params).filter(hasAllowedPrefix).sort());
2976
- };
2977
- // The dedup key: pathname plus the allowlisted params in a fixed order, so that
2978
- // reordering the query string is not a new page. Params outside the allowlist
2979
- // never make it into `page.params` and so cannot key a view \u2014 nor can the hash.
2980
- //
2981
- // Values are re-encoded because queryStringParser hands them back DECODED. A
2982
- // value holding the pair delimiters would otherwise serialize exactly like two
2983
- // separate params \u2014 `{q: 'a&search=b'}` and `{q: 'a', search: 'b'}` both becoming
2984
- // `q=a&search=b` \u2014 and dedup would treat a real navigation between them as the
2985
- // same page and drop the view. `q`, `search` and `redirect_uri` carry `&` and `=`
2986
- // routinely, so this is reachable rather than theoretical.
3041
+ // Values are re-encoded because they arrive decoded: `{q: 'a&search=b'}` would otherwise
3042
+ // key the same as `{q: 'a', search: 'b'}` and dedup a real navigation away.
2987
3043
  var pageKey = function pageKey(page) {
2988
- var query = capturedNames(page.params).map(function (name) {
2989
- return "".concat(name, "=").concat(encodeURIComponent(page.params[name]));
3044
+ var query = page.params.map(function (_a) {
3045
+ var name = _a.name,
3046
+ value = _a.value;
3047
+ return "".concat(name, "=").concat(encodeURIComponent(value));
2990
3048
  }).join('&');
2991
3049
  return query ? "".concat(page.path, "?").concat(query) : page.path;
2992
3050
  };
@@ -3011,7 +3069,7 @@ var mParticle = (function () {
3011
3069
  // Params spread first, then the core fields by name, so a core field always
3012
3070
  // wins. No allowlist entry collides with hostname/title/path today; naming
3013
3071
  // them here is what keeps a later addition from silently overwriting one.
3014
- data: __assign(__assign(__assign({}, params), {
3072
+ data: __assign(__assign(__assign({}, paramsToAttributes(params)), {
3015
3073
  hostname: hostname,
3016
3074
  title: title,
3017
3075
  path: path
@@ -3090,10 +3148,10 @@ var mParticle = (function () {
3090
3148
  state.tracker = undefined;
3091
3149
  }
3092
3150
  };
3093
- var currentPage = function currentPage() {
3151
+ var currentPage = function currentPage(extras) {
3094
3152
  return {
3095
3153
  path: window.location.pathname,
3096
- params: allowedQueryParams(getHref())
3154
+ params: allowedQueryParams(getHref(), extras)
3097
3155
  };
3098
3156
  };
3099
3157
  // Log-safe description of a page: the path, plus the NAMES of the captured
@@ -3106,7 +3164,10 @@ var mParticle = (function () {
3106
3164
  if (!page) {
3107
3165
  return '';
3108
3166
  }
3109
- var names = capturedNames(page.params);
3167
+ var names = page.params.map(function (_a) {
3168
+ var name = _a.name;
3169
+ return name;
3170
+ });
3110
3171
  return names.length ? "".concat(page.path, " (params: ").concat(names.join(), ")") : page.path;
3111
3172
  };
3112
3173
  // ---------------------------------------------------------------------------
@@ -3119,6 +3180,7 @@ var mParticle = (function () {
3119
3180
  function PageViewTracker(mpInstance, options) {
3120
3181
  this.lastPage = null;
3121
3182
  this.active = false;
3183
+ this.customQueryParams = [];
3122
3184
  this.pendingNavigations = [];
3123
3185
  this.undoHistoryPatch = null;
3124
3186
  this.mpInstance = mpInstance;
@@ -3153,7 +3215,8 @@ var mParticle = (function () {
3153
3215
  inheritedPages = inheritedPages.concat(this.retire(this));
3154
3216
  }
3155
3217
  this.active = true;
3156
- this.lastPage = currentPage();
3218
+ this.customQueryParams = this.readCustomQueryParams();
3219
+ this.lastPage = currentPage(this.customQueryParams);
3157
3220
  this.log("[init] seeded lastPage: ".concat(describePage(this.lastPage)));
3158
3221
  this.undoHistoryPatch = subscribeToRouteChange('pageView', function (source) {
3159
3222
  return _this.safeHandleNavigation(source);
@@ -3178,6 +3241,25 @@ var mParticle = (function () {
3178
3241
  this.active = false;
3179
3242
  clearActiveTracker(this);
3180
3243
  };
3244
+ PageViewTracker.prototype.readCustomQueryParams = function () {
3245
+ var _a = this.mpInstance._Store.SDKConfig.autoLogPageViewQueryParams || {},
3246
+ _b = _a.allowed,
3247
+ allowed = _b === void 0 ? [] : _b,
3248
+ _c = _a.rejectedPositions,
3249
+ rejectedPositions = _c === void 0 ? [] : _c,
3250
+ _d = _a.overLimit,
3251
+ overLimit = _d === void 0 ? 0 : _d;
3252
+ if (rejectedPositions.length) {
3253
+ this.mpInstance.Logger.warning('mParticle APV: ignoring invalid additional page view query ' + "parameters at positions ".concat(rejectedPositions.join(', ')));
3254
+ }
3255
+ if (overLimit) {
3256
+ this.mpInstance.Logger.warning("mParticle APV: ignoring ".concat(overLimit, " additional page view ") + "query parameters beyond the limit of " + "".concat(MAX_CUSTOM_QUERY_PARAMS));
3257
+ }
3258
+ if (allowed.length) {
3259
+ this.log("[init] additional query params: ".concat(allowed.join()));
3260
+ }
3261
+ return allowed;
3262
+ };
3181
3263
  // Stops the outgoing tracker and returns the pages it had queued so this
3182
3264
  // tracker can re-schedule them once it is active. Private, but reachable
3183
3265
  // across instances: the handoff protocol stays internal to the class.
@@ -3219,7 +3301,7 @@ var mParticle = (function () {
3219
3301
  }
3220
3302
  };
3221
3303
  PageViewTracker.prototype.handleNavigation = function (source) {
3222
- var candidate = currentPage();
3304
+ var candidate = currentPage(this.customQueryParams);
3223
3305
  var lastKey = this.lastPage ? pageKey(this.lastPage) : null;
3224
3306
  this.log("[detect] navigation signal (source: ".concat(source, ", candidate: ").concat(describePage(candidate), ", last: ").concat(describePage(this.lastPage), ")"));
3225
3307
  if (!isNewPage(lastKey, pageKey(candidate))) {
@@ -3271,11 +3353,21 @@ var mParticle = (function () {
3271
3353
 
3272
3354
  // The one rule for a URL that leaves the SDK, shared with kits through
3273
3355
  // `mParticle._cleanUrl`. `path` keeps scheme, host and path. `full` adds the
3274
- // query params an auto page view may capture, then the fragment.
3356
+ // query params an auto page view captures by default and the campaign
3357
+ // attribution params, then the fragment.
3275
3358
  var UrlMode = {
3276
3359
  Path: 'path',
3277
3360
  Full: 'full'
3278
3361
  };
3362
+ // Campaign attribution and ad-network click ids. URLs keep them; an auto page view
3363
+ // carries one as an attribute only when an input configures it.
3364
+ var URL_ATTRIBUTION_QUERY_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic', 'gad_source', 'gad_campaignid', 'mc_cid', 'gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'ttclid', 'twclid', 'li_fat_id', 'dclid', 'srsltid', 'epik', 'ScCid', 'irclickid', 'rtid', 'rclid'];
3365
+ var URL_ATTRIBUTION_QUERY_PARAMS_LOWER = URL_ATTRIBUTION_QUERY_PARAMS.map(function (name) {
3366
+ return name.toLowerCase();
3367
+ });
3368
+ var isUrlQueryParam = function isUrlQueryParam(name) {
3369
+ return isAllowedQueryParam(name) || URL_ATTRIBUTION_QUERY_PARAMS_LOWER.indexOf(name.toLowerCase()) !== -1;
3370
+ };
3279
3371
  // OAuth 2.0 / OIDC authorization-flow params. They stay off URLs even where the
3280
3372
  // page view allowlist admits them, because a URL travels on every event.
3281
3373
  var EXCLUDED_URL_QUERY_PARAMS = ['client_id', 'redirect_uri', 'response_type', 'scope', 'state', 'code', 'nonce'];
@@ -3297,7 +3389,7 @@ var mParticle = (function () {
3297
3389
  return null;
3298
3390
  }
3299
3391
  var lowerName = name.toLowerCase();
3300
- if (!name || !isAllowedQueryParam(name) || EXCLUDED_URL_QUERY_PARAMS.indexOf(lowerName) !== -1) {
3392
+ if (!name || !isUrlQueryParam(name) || EXCLUDED_URL_QUERY_PARAMS.indexOf(lowerName) !== -1) {
3301
3393
  return null;
3302
3394
  }
3303
3395
  if (separator === -1 || URL_VALUED_QUERY_PARAMS.indexOf(lowerName) === -1) {
@@ -3745,7 +3837,7 @@ var mParticle = (function () {
3745
3837
  var event = {
3746
3838
  AppName: SDKConfig.appName,
3747
3839
  AppVersion: SDKConfig.appVersion,
3748
- Package: SDKConfig["package"],
3840
+ Package: SDKConfig.package,
3749
3841
  EventDataType: MessageType$1.AppStateTransition,
3750
3842
  Timestamp: now,
3751
3843
  SessionId: sessionId,
@@ -3976,7 +4068,7 @@ var mParticle = (function () {
3976
4068
  });
3977
4069
  batchesToUpload.forEach(function (batch) {
3978
4070
  if (retriedIds.indexOf(batch.source_request_id) === -1) {
3979
- _this_1.batchOrder["delete"](batch.source_request_id);
4071
+ _this_1.batchOrder.delete(batch.source_request_id);
3980
4072
  }
3981
4073
  });
3982
4074
  // Batches that do not successfully upload are added back to the process queue
@@ -4003,7 +4095,6 @@ var mParticle = (function () {
4003
4095
  });
4004
4096
  });
4005
4097
  };
4006
-
4007
4098
  BatchUploader.prototype.storeBatchesQueuedForProcessing = function () {
4008
4099
  var _this_1 = this;
4009
4100
  var _a;
@@ -4271,7 +4362,6 @@ var mParticle = (function () {
4271
4362
  logger.error('search called without a callback function; skipping request.');
4272
4363
  return [2 /*return*/];
4273
4364
  }
4274
-
4275
4365
  safeInvoke = function safeInvoke(result) {
4276
4366
  try {
4277
4367
  callback(result);
@@ -4301,7 +4391,6 @@ var mParticle = (function () {
4301
4391
  });
4302
4392
  return [2 /*return*/];
4303
4393
  }
4304
-
4305
4394
  _a.label = 1;
4306
4395
  case 1:
4307
4396
  _a.trys.push([1, 9,, 10]);
@@ -4604,7 +4693,7 @@ var mParticle = (function () {
4604
4693
  sdk_version: Constants.sdkVersion
4605
4694
  },
4606
4695
  environment: environment,
4607
- request_id: generateUniqueId(),
4696
+ request_id: _generateUniqueId(),
4608
4697
  request_timestamp_ms: Date.now()
4609
4698
  };
4610
4699
  };
@@ -5058,7 +5147,7 @@ var mParticle = (function () {
5058
5147
  this.decoded = decoded;
5059
5148
  this.parseStringOrNumber = parseStringOrNumber;
5060
5149
  this.generateHash = generateHash;
5061
- this.generateUniqueId = generateUniqueId;
5150
+ this.generateUniqueId = _generateUniqueId;
5062
5151
  // Imported Validators
5063
5152
  this.Validators = Validators;
5064
5153
  }
@@ -6144,6 +6233,7 @@ var mParticle = (function () {
6144
6233
  this.SDKConfig.flags = {};
6145
6234
  }
6146
6235
  this.SDKConfig.flags = processFlags(config);
6236
+ this.SDKConfig.autoLogPageViewQueryParams = parseQueryParamAllowlist(config.autoLogPageViewQueryParams);
6147
6237
  if (config.deviceId) {
6148
6238
  this.deviceId = config.deviceId;
6149
6239
  }
@@ -6178,7 +6268,7 @@ var mParticle = (function () {
6178
6268
  this.SDKConfig.appName = config.appName;
6179
6269
  }
6180
6270
  if (config.hasOwnProperty('package')) {
6181
- this.SDKConfig["package"] = config["package"];
6271
+ this.SDKConfig.package = config.package;
6182
6272
  }
6183
6273
  if (config.hasOwnProperty('integrationDelayTimeout')) {
6184
6274
  this.SDKConfig.integrationDelayTimeout = config.integrationDelayTimeout;
@@ -6420,6 +6510,9 @@ var mParticle = (function () {
6420
6510
  if (config.flags) {
6421
6511
  _this.SDKConfig.flags = processFlags(config);
6422
6512
  }
6513
+ // Stored whole: the tracker reports the rejections, and cannot recover them by
6514
+ // re-parsing a list that has already had them removed.
6515
+ _this.SDKConfig.autoLogPageViewQueryParams = parseQueryParamAllowlist(config.autoLogPageViewQueryParams);
6423
6516
  var baseUrls = processBaseUrls(config, _this.SDKConfig.flags, apiKey);
6424
6517
  for (var baseUrlKeys in baseUrls) {
6425
6518
  _this.SDKConfig[baseUrlKeys] = baseUrls[baseUrlKeys];
@@ -7438,13 +7531,13 @@ var mParticle = (function () {
7438
7531
  };
7439
7532
  // The auto page view for the landing page. The SPA navigations that follow it
7440
7533
  // come from PageViewTracker instead, so both emitters attach the same
7441
- // allowlisted query params \u2014 and this is the one that matters for campaign
7442
- // attribution, since utm_*/gclid live on the entry URL.
7534
+ // allowlisted query params.
7443
7535
  this.logPageView = function (options) {
7536
+ var _a;
7444
7537
  self.logEvent({
7445
7538
  messageType: Types.MessageType.PageView,
7446
7539
  name: 'PageView',
7447
- data: __assign(__assign(__assign({}, allowedQueryParams(getHref())), {
7540
+ data: __assign(__assign(__assign({}, paramsToAttributes(allowedQueryParams(getHref(), (_a = mpInstance._Store.SDKConfig.autoLogPageViewQueryParams) === null || _a === void 0 ? void 0 : _a.allowed))), {
7448
7541
  hostname: window.location.hostname,
7449
7542
  title: window.document.title
7450
7543
  }), autoPageViewAttribute(options && options.isAutoPageView)),
@@ -7738,6 +7831,19 @@ var mParticle = (function () {
7738
7831
  }
7739
7832
  return false;
7740
7833
  }
7834
+ // sendEventToForwarders passes no kitBlocker: apiClient has already applied
7835
+ // the data plan by then.
7836
+ function isEventForwarded(event, forwarder, kitBlocker) {
7837
+ var plannedEvent = kitBlocker && kitBlocker.kitBlockingEnabled ? kitBlocker.createBlockedEvent(event) : event;
7838
+ if (!plannedEvent) {
7839
+ return false;
7840
+ }
7841
+ if (isBlockedByForwardingRule(plannedEvent.EventDataType, plannedEvent.EventAttributes, forwarder)) {
7842
+ return false;
7843
+ }
7844
+ var eventCategory = plannedEvent.EventCategory;
7845
+ return !isBlockedByEventFilter(plannedEvent.EventDataType, KitFilterHelper.hashEventName(plannedEvent.EventName, eventCategory), KitFilterHelper.hashEventType(eventCategory), forwarder);
7846
+ }
7741
7847
  function filterEventAttributes(messageType, eventCategory, eventName, attributes, forwarder) {
7742
7848
  if (!attributes) {
7743
7849
  return attributes;
@@ -7888,17 +7994,12 @@ var mParticle = (function () {
7888
7994
  if (mpInstance._Store.webviewBridgeEnabled || !mpInstance._Store.activeForwarders) {
7889
7995
  return;
7890
7996
  }
7891
- var hashedEventName = KitFilterHelper.hashEventName(event.EventName, event.EventCategory);
7892
- var hashedEventType = KitFilterHelper.hashEventType(event.EventCategory);
7893
7997
  var forwardEvent = function forwardEvent(forwarder) {
7894
- if (isBlockedByForwardingRule(event.EventDataType, event.EventAttributes, forwarder)) {
7998
+ if (!isEventForwarded(event, forwarder)) {
7895
7999
  return;
7896
8000
  }
7897
8001
  // Clone the event object, as we could be sending different attributes to each forwarder
7898
8002
  var clonedEvent = extend(true, {}, event);
7899
- if (isBlockedByEventFilter(event.EventDataType, hashedEventName, hashedEventType, forwarder)) {
7900
- return;
7901
- }
7902
8003
  clonedEvent.EventAttributes = filterEventAttributes(event.EventDataType, event.EventCategory, event.EventName, clonedEvent.EventAttributes, forwarder);
7903
8004
  // Check user identity filtering rules
7904
8005
  clonedEvent.UserIdentities = filterUserIdentities(clonedEvent.UserIdentities, forwarder.userIdentityFilters);
@@ -8346,7 +8447,7 @@ var mParticle = (function () {
8346
8447
  ExpandedEventCount: 0,
8347
8448
  AppVersion: mpInstance.getAppVersion(),
8348
8449
  AppName: mpInstance.getAppName(),
8349
- Package: mpInstance._Store.SDKConfig["package"],
8450
+ Package: mpInstance._Store.SDKConfig.package,
8350
8451
  ClientGeneratedId: mpInstance._Store.clientId,
8351
8452
  DeviceId: mpInstance._Store.deviceId,
8352
8453
  IntegrationAttributes: integrationAttributes,
@@ -8575,7 +8676,6 @@ var mParticle = (function () {
8575
8676
  });
8576
8677
  });
8577
8678
  };
8578
-
8579
8679
  return AudienceManager;
8580
8680
  }();
8581
8681
 
@@ -10614,7 +10714,6 @@ var mParticle = (function () {
10614
10714
  });
10615
10715
  });
10616
10716
  };
10617
-
10618
10717
  this.sendIdentityRequest = function (identityApiRequest, method, callback, originalIdentityApiData, parseIdentityResponse, mpid, knownIdentities) {
10619
10718
  var _a, _b, _c, _d;
10620
10719
  return __awaiter(this, void 0, void 0, function () {
@@ -10634,13 +10733,11 @@ var mParticle = (function () {
10634
10733
  Logger.error(Messages$1.ErrorMessages.APIRequestEmpty);
10635
10734
  return [2 /*return*/];
10636
10735
  }
10637
-
10638
10736
  Logger.verbose(Messages$1.InformationMessages.SendIdentityHttp);
10639
10737
  if (mpInstance._Store.identityCallInFlight) {
10640
10738
  invokeCallback(callback, HTTPCodes$1.activeIdentityRequest, 'There is currently an Identity request processing. Please wait for this to return before requesting again');
10641
10739
  return [2 /*return*/];
10642
10740
  }
10643
-
10644
10741
  previousMPID = mpid || null;
10645
10742
  uploadUrl = this.getUploadUrl(method, mpid);
10646
10743
  uploader = window.fetch ? new FetchUploader(uploadUrl) : new XHRUploader(uploadUrl);
@@ -10698,7 +10795,7 @@ var mParticle = (function () {
10698
10795
  responseToLog = responseText;
10699
10796
  if (!isDevelopmentMode && (responseText === null || responseText === void 0 ? void 0 : responseText.matched_identities)) {
10700
10797
  responseToLog = __assign(__assign({}, responseText), {
10701
- matched_identities: obfuscateData(responseText.matched_identities)
10798
+ matched_identities: _obfuscateData(responseText.matched_identities)
10702
10799
  });
10703
10800
  }
10704
10801
  message = 'Received Identity Response from server: ' + JSON.stringify(responseToLog);
@@ -10753,7 +10850,6 @@ var mParticle = (function () {
10753
10850
  });
10754
10851
  });
10755
10852
  };
10756
-
10757
10853
  this.getUploadUrl = function (method, mpid) {
10758
10854
  var uploadServiceUrl = mpInstance._Helpers.createServiceUrl(mpInstance._Store.SDKConfig.identityUrl);
10759
10855
  var uploadUrl = method === Modify ? uploadServiceUrl + mpid + '/' + method : uploadServiceUrl + method;
@@ -10778,7 +10874,8 @@ var mParticle = (function () {
10778
10874
  }
10779
10875
 
10780
10876
  var PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
10781
- var CHECKOUT_INTENT_BUFFER_LIMIT = 4;
10877
+ // One signal per kind (commit, wallet, checkout_step, frame and cart), so a new kind never evicts another.
10878
+ var CHECKOUT_INTENT_BUFFER_LIMIT = 5;
10782
10879
  var CHECKOUT_INTENT_MAX_AGE_MS = 30000;
10783
10880
  var ON_SHOPPABLE_ADS_READY_METHOD = 'onShoppableAdsReady';
10784
10881
  // The purpose of this class is to create a link between the Core mParticle SDK and the
@@ -10807,6 +10904,8 @@ var mParticle = (function () {
10807
10904
  this.stopRouteChangeWatch = null;
10808
10905
  // Checkout intent raised before the kit attached: latest per kind.
10809
10906
  this.pendingCheckoutIntents = [];
10907
+ // Off until SDK init finds the Rokt connection receives auto button clicks.
10908
+ this.autoButtonClicksForwarded = false;
10810
10909
  }
10811
10910
  /**
10812
10911
  * Sets a callback to be invoked when RoktManager becomes ready
@@ -10933,12 +11032,16 @@ var mParticle = (function () {
10933
11032
  (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error("RoktManager: Error in onRouteChange: ".concat(getErrorMessage(e)));
10934
11033
  }
10935
11034
  };
11035
+ RoktManager.prototype.setAutoButtonClicksForwarded = function (forwarded) {
11036
+ this.autoButtonClicksForwarded = forwarded;
11037
+ };
10936
11038
  // Checkout intent is off unless the flag is on, and under opt-out, any Rokt
10937
- // privacy launcher option, or Global Privacy Control.
11039
+ // privacy launcher option, Global Privacy Control, or a Rokt connection that
11040
+ // does not receive auto button clicks.
10938
11041
  RoktManager.prototype.isCheckoutIntentSuppressed = function () {
10939
11042
  var _a, _b;
10940
11043
  var flags = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.SDKConfig) === null || _b === void 0 ? void 0 : _b.flags;
10941
- if (!flags || flags[Constants.FeatureFlags.AutoButtonTracking] !== true || !this.store.isEnabled) {
11044
+ if (!flags || flags[Constants.FeatureFlags.AutoButtonTracking] !== true || !this.store.isEnabled || !this.autoButtonClicksForwarded) {
10942
11045
  return true;
10943
11046
  }
10944
11047
  var options = this.launcherOptions || {};
@@ -11006,6 +11109,8 @@ var mParticle = (function () {
11006
11109
  return _this.notifyCheckoutIntent(signal);
11007
11110
  });
11008
11111
  };
11112
+ // The Rokt kit reads this to tell core's page views, whose title and utm
11113
+ // attributes a filter may have removed, from ones logged by hand.
11009
11114
  RoktManager.prototype.isAutoLogPageViewEnabled = function () {
11010
11115
  var _a, _b, _c;
11011
11116
  return ((_c = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.SDKConfig) === null || _b === void 0 ? void 0 : _b.flags) === null || _c === void 0 ? void 0 : _c[Constants.FeatureFlags.AutoLogPageView]) === true;
@@ -11153,7 +11258,6 @@ var mParticle = (function () {
11153
11258
  });
11154
11259
  });
11155
11260
  };
11156
-
11157
11261
  RoktManager.prototype.logFilteredIdentity = function (identityType) {
11158
11262
  var _a;
11159
11263
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.verbose("mParticle.Rokt selectPlacements did not add the ".concat(identityType, " user identity to the attributes because the Rokt connection does not allow it"));
@@ -11223,7 +11327,6 @@ var mParticle = (function () {
11223
11327
  });
11224
11328
  });
11225
11329
  };
11226
-
11227
11330
  RoktManager.prototype.setExtensionData = function (extensionData) {
11228
11331
  if (!this.isReady()) {
11229
11332
  this.deferredCall('setExtensionData', extensionData);
@@ -11272,12 +11375,10 @@ var mParticle = (function () {
11272
11375
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.verbose('mParticle.Rokt.terminate called before the Rokt kit was ready. Nothing to terminate.');
11273
11376
  return [2 /*return*/];
11274
11377
  }
11275
-
11276
11378
  if (!isFunction(this.kit.terminate)) {
11277
11379
  (_b = this.logger) === null || _b === void 0 ? void 0 : _b.error('mParticle.Rokt.terminate is not supported by the attached Rokt Kit version.');
11278
11380
  return [2 /*return*/];
11279
11381
  }
11280
-
11281
11382
  _d.label = 1;
11282
11383
  case 1:
11283
11384
  _d.trys.push([1, 3,, 4]);
@@ -11295,7 +11396,6 @@ var mParticle = (function () {
11295
11396
  });
11296
11397
  });
11297
11398
  };
11298
-
11299
11399
  RoktManager.prototype.onShoppableAdsReady = function (callback) {
11300
11400
  if (!this.kit || !this.isShoppableAdsLoaded) {
11301
11401
  this.deferredCall('onShoppableAdsReady', callback);
@@ -11318,7 +11418,7 @@ var mParticle = (function () {
11318
11418
  } catch (e) {
11319
11419
  (_a = _this.logger) === null || _a === void 0 ? void 0 : _a.error('RoktManager: Error flushing onShoppableAdsReady: ' + e);
11320
11420
  }
11321
- _this.messageQueue["delete"](key);
11421
+ _this.messageQueue.delete(key);
11322
11422
  }
11323
11423
  });
11324
11424
  this.isShoppableAdsLoaded = true;
@@ -11357,7 +11457,6 @@ var mParticle = (function () {
11357
11457
  });
11358
11458
  });
11359
11459
  };
11360
-
11361
11460
  RoktManager.prototype.getLocalSessionAttributes = function () {
11362
11461
  return this.store.getLocalSessionAttributes();
11363
11462
  };
@@ -11463,7 +11562,7 @@ var mParticle = (function () {
11463
11562
  if (resolve) {
11464
11563
  resolve(resolvedResult);
11465
11564
  }
11466
- })["catch"](handleError);
11565
+ }).catch(handleError);
11467
11566
  } catch (error) {
11468
11567
  handleError(error);
11469
11568
  }
@@ -11475,7 +11574,7 @@ var mParticle = (function () {
11475
11574
  RoktManager.prototype.deferredCall = function (methodName, payload) {
11476
11575
  var _this = this;
11477
11576
  return new Promise(function (resolve, reject) {
11478
- var messageId = "".concat(methodName, "_").concat(generateUniqueId());
11577
+ var messageId = "".concat(methodName, "_").concat(_generateUniqueId());
11479
11578
  _this.queueMessage({
11480
11579
  messageId: messageId,
11481
11580
  methodName: methodName,
@@ -11538,14 +11637,11 @@ var mParticle = (function () {
11538
11637
  if (!currentValue) {
11539
11638
  return true; // New value exists but no current value
11540
11639
  }
11541
-
11542
11640
  if (currentValue !== newValue) {
11543
11641
  return true; // Values are different
11544
11642
  }
11545
-
11546
11643
  return false; // Values are the same
11547
11644
  };
11548
-
11549
11645
  return RoktManager;
11550
11646
  }();
11551
11647
 
@@ -11679,23 +11775,71 @@ var mParticle = (function () {
11679
11775
  eventType: EventType.Other
11680
11776
  };
11681
11777
  };
11778
+ // Every attribute buildButtonClickEvent can set apart from the marker and the
11779
+ // hostname, which is the same on every click on this page. A spec checks the
11780
+ // two stay in step.
11781
+ var CLICK_ATTRIBUTES = ['label_source', 'element', 'type', 'path', 'label', 'aria_label', 'icon', 'element_id', 'element_name', 'test_id', 'controls', 'form', 'link_type', 'link_domain', 'file_extension', 'checked'];
11782
+ // Which check keeps some or all auto button clicks from a kit, or undefined
11783
+ // when the kit receives every one. A forwarding rule on an attribute whose
11784
+ // value differs between clicks lets some through and not others. Each other
11785
+ // check goes through isEventForwarded on its own, so together they decide as
11786
+ // it does for each click.
11787
+ var getButtonClickBlocker = function getButtonClickBlocker(forwarder, kitBlocker) {
11788
+ var _a;
11789
+ var rule = forwarder.filteringEventAttributeValue;
11790
+ if (rule && rule.eventAttributeValue && CLICK_ATTRIBUTES.some(function (name) {
11791
+ return KitFilterHelper.hashAttributeConditionalForwarding(name) === rule.eventAttributeName;
11792
+ })) {
11793
+ return 'forwarding rule';
11794
+ }
11795
+ var click = {
11796
+ EventName: BUTTON_CLICK_EVENT_NAME,
11797
+ EventCategory: EventType.Other,
11798
+ EventDataType: MessageType$1.PageEvent,
11799
+ EventAttributes: (_a = {}, _a[AUTO_BUTTON_ATTRIBUTE] = true, _a.hostname = window.location.hostname, _a)
11800
+ };
11801
+ var checks = [['data plan', {}], ['forwarding rule', {
11802
+ filteringEventAttributeValue: rule
11803
+ }], ['event name filter', {
11804
+ eventNameFilters: forwarder.eventNameFilters
11805
+ }], ['event type filter', {
11806
+ eventTypeFilters: forwarder.eventTypeFilters
11807
+ }]];
11808
+ var failed = checks.find(function (_a) {
11809
+ var filters = _a[1];
11810
+ return !isEventForwarded(click, filters, kitBlocker);
11811
+ });
11812
+ return failed ? failed[0] : undefined;
11813
+ };
11682
11814
 
11683
11815
  // Finds the control a click landed on, for both checkout intent and button
11684
11816
  // descriptions. Rokt's own elements, disabled controls and anything under
11685
11817
  // data-mp-no-track are never returned.
11686
11818
  var CONTROL_SELECTOR = 'button, input[type="submit"], input[type="button"], input[type="image"], [role="button"]';
11687
- // closest() over the composed tree: slots, parents, then shadow hosts.
11688
- function hasComposedAncestor(element, selector) {
11819
+ var EDITABLE = '[contenteditable]:not([contenteditable="false"])';
11820
+ // One step up the composed tree: the slot, the parent, then the shadow host.
11821
+ function composedParent(node) {
11822
+ var root = isFunction(node.getRootNode) ? node.getRootNode() : null;
11823
+ return node.assignedSlot || node.parentElement || root && root.host || null;
11824
+ }
11825
+ // Tests the element and its ancestors over the composed tree: slots, parents,
11826
+ // then shadow hosts. With `until`, stops before reaching that ancestor.
11827
+ function someComposedAncestor(element, test, until) {
11689
11828
  var node = element;
11690
- while (node) {
11691
- if (node.matches(selector)) {
11829
+ while (node && node !== until) {
11830
+ if (test(node)) {
11692
11831
  return true;
11693
11832
  }
11694
- var root = isFunction(node.getRootNode) ? node.getRootNode() : null;
11695
- node = node.assignedSlot || node.parentElement || root && root.host || null;
11833
+ node = composedParent(node);
11696
11834
  }
11697
11835
  return false;
11698
11836
  }
11837
+ // closest() over the composed tree, starting at the element itself.
11838
+ function hasComposedAncestor(element, selector) {
11839
+ return someComposedAncestor(element, function (node) {
11840
+ return node.matches(selector);
11841
+ });
11842
+ }
11699
11843
  var ROKT_SELECTOR = '[data-core-type], #rokt-controller-frame';
11700
11844
  // A Rokt frame or script is served from a /wsdk/ path.
11701
11845
  function hasRoktSrc(element) {
@@ -11741,8 +11885,27 @@ var mParticle = (function () {
11741
11885
  }
11742
11886
  return host.matches(CARD_FORM_BUTTON) ? 'checkout_step' : 'wallet';
11743
11887
  }
11888
+ // A merchant-built wallet button that names its wallet in an id or test id, such
11889
+ // as `venmo-pay-btn` or `applePayButton`. The name must also say button and not
11890
+ // name a part or a wrapper of one, so a wallet's logo, label or container never
11891
+ // counts, nor a button that only chooses a wallet. Affirm is also an English
11892
+ // verb, so it counts only beside a payment word. Only the element's own
11893
+ // attributes are read.
11894
+ var WALLET_IN_NAME = /(?:^| )(?:apple ?pay|google ?pay|g ?pay|amazon ?pay|cash ?app|venmo|pay ?pal|shop ?pay|klarna|after ?pay|clear ?pay|sezzle|samsung ?pay)(?= |$)/;
11895
+ var AFFIRM_IN_NAME = /(?:^| )affirm(?= |$)/;
11896
+ var PAYMENT_IN_NAME = /(?:^| )(?:pay|payment|checkout)(?= |$)/;
11897
+ var BUTTON_IN_NAME = /(?:^| )(?:btn|button)(?= |$)/;
11898
+ var PART_IN_NAME = /(?:^| )(?:logo|icon|image|img|label|text|caption|message|messaging|info|tooltip|help|learn|terms|promo|banner|container|wrapper|wrap|placeholder|form|section|panel|modal|list|group|select|option|tab|toggle|radio|choice|method)(?= |$)/;
11899
+ function nameWords(value) {
11900
+ return (value || '').slice(0, 64).replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[^A-Za-z0-9]+/g, ' ').trim().toLowerCase();
11901
+ }
11902
+ function isWalletNamed(element) {
11903
+ return [element.getAttribute('id'), element.getAttribute('data-testid')].map(nameWords).some(function (words) {
11904
+ return (WALLET_IN_NAME.test(words) || AFFIRM_IN_NAME.test(words) && PAYMENT_IN_NAME.test(words)) && BUTTON_IN_NAME.test(words) && !PART_IN_NAME.test(words);
11905
+ });
11906
+ }
11744
11907
  function isWalletElement(element) {
11745
- return vendorButtonKind(element) === 'wallet';
11908
+ return vendorButtonKind(element) === 'wallet' || isWalletNamedControl(element);
11746
11909
  }
11747
11910
  // Pickers and toggles choose a payment method or open a panel; they never pay.
11748
11911
  var PICKER = 'input[type="radio"], input[type="checkbox"], label, [role="radio"], [role="tab"], [role="option"], ' + '[role="checkbox"], [role="switch"], [role="menuitemradio"], [role="menuitemcheckbox"], [aria-checked], [aria-selected]';
@@ -11750,25 +11913,53 @@ var mParticle = (function () {
11750
11913
  // Pay-later and financing messages link to details rather than pay.
11751
11914
  var MESSAGING = 'klarna-placement, afterpay-placement, square-placement, .affirm-as-low-as, .affirm-site-modal, ' + '.affirm-product-modal, [data-pp-message]';
11752
11915
  // A control that is still logged as a click but never counts as checkout intent.
11753
- function isQuietIntentControl(control, isVendorWallet) {
11916
+ // A cart button can open a flyout, so aria-expanded alone never quiets one.
11917
+ function isQuietIntentControl(control, isVendorWallet, isCart) {
11918
+ if (isCart === void 0) {
11919
+ isCart = false;
11920
+ }
11754
11921
  if (control.matches(PICKER)) {
11755
11922
  return true;
11756
11923
  }
11757
11924
  if (hasComposedAncestor(control, PICKER_GROUP)) {
11758
11925
  return true;
11759
11926
  }
11760
- if (!isVendorWallet && (control.hasAttribute('aria-expanded') || control.hasAttribute('aria-pressed'))) {
11927
+ if (!isVendorWallet && (!isCart && control.hasAttribute('aria-expanded') || control.hasAttribute('aria-pressed'))) {
11761
11928
  return true;
11762
11929
  }
11763
11930
  return hasComposedAncestor(control, MESSAGING);
11764
11931
  }
11765
11932
  // A checkbox or radio is a control of its own only when no button or link
11766
- // around it takes the click, so a tappable row keeps its own name.
11933
+ // around it takes the click, so a tappable row stays the control.
11767
11934
  var OPTION_INPUT = 'input[type="checkbox"], input[type="radio"]';
11768
- // Walks at most 12 entries of an event's composed path for the nearest control.
11769
- // One composed walk from the target rules out Rokt elements and anything under
11770
- // data-mp-no-track; disabled controls are skipped.
11771
- function findControl(path) {
11935
+ // Anything that chooses, toggles or takes typing. One between a click's target
11936
+ // and a wallet-named element, or inside that element, keeps it from becoming
11937
+ // the tap. Hidden inputs and button-like inputs are not fields.
11938
+ var NAME_BARRIER = PICKER + ', ' + PICKER_GROUP + ', input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), ' + 'select, textarea, option, optgroup, datalist, [aria-expanded], [aria-pressed], ' + EDITABLE;
11939
+ // What owns a click on anything inside it: a picker, a real control or link,
11940
+ // a toggle, or an editable region.
11941
+ var NAME_HOLDER = PICKER + ', ' + CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON + ', [aria-expanded], [aria-pressed], ' + EDITABLE;
11942
+ // A wallet-named element that may stand as a control by its name alone: not
11943
+ // itself a picker, toggle or form field; not inside a picker, a real button or
11944
+ // link, a toggle, an editable region or another wallet-named element, which own
11945
+ // the click with their own state; and holding no picker or field, in its open
11946
+ // shadow root either, so a payment-method row with its own radio or select
11947
+ // never becomes a wallet tap.
11948
+ function isWalletNamedControl(element) {
11949
+ if (!isWalletNamed(element) || element.matches(NAME_BARRIER)) {
11950
+ return false;
11951
+ }
11952
+ for (var node = composedParent(element); node; node = composedParent(node)) {
11953
+ if (node.matches(NAME_HOLDER) || isWalletNamed(node)) {
11954
+ return false;
11955
+ }
11956
+ }
11957
+ var shadow = element.shadowRoot;
11958
+ return !element.querySelector(NAME_BARRIER) && !(shadow && shadow.querySelector(NAME_BARRIER));
11959
+ }
11960
+ // Whether a click is never tracked: it has no element, or lands on a Rokt
11961
+ // element or under data-mp-no-track. Reads at most 12 entries of the path.
11962
+ function isExcludedPath(path) {
11772
11963
  var entries = path.slice(0, 12);
11773
11964
  var first;
11774
11965
  for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
@@ -11779,41 +11970,60 @@ var mParticle = (function () {
11779
11970
  }
11780
11971
  }
11781
11972
  if (!first || hasComposedAncestor(first, ROKT_SELECTOR + ', [data-mp-no-track]')) {
11973
+ return true;
11974
+ }
11975
+ for (var _a = 0, entries_2 = entries; _a < entries_2.length; _a++) {
11976
+ var target = entries_2[_a];
11977
+ if (target instanceof Element && hasRoktSrc(target)) {
11978
+ return true;
11979
+ }
11980
+ }
11981
+ return false;
11982
+ }
11983
+ // Walks at most 12 entries of an event's composed path for the nearest control.
11984
+ // A wallet-named element counts as one unless a picker, toggle or form field
11985
+ // lies between it and the target or a picker holds it, so choosing an option in
11986
+ // or around it never becomes its tap. Excluded clicks give nothing; disabled controls are skipped, a
11987
+ // wallet-named one also by its `disabled` class. A caller that has already
11988
+ // found the path not excluded passes `included` so the walk runs once.
11989
+ function findControl(path, included) {
11990
+ if (included === void 0) {
11991
+ included = false;
11992
+ }
11993
+ if (!included && isExcludedPath(path)) {
11782
11994
  return undefined;
11783
11995
  }
11784
11996
  var control;
11785
11997
  var option;
11786
- for (var _a = 0, entries_2 = entries; _a < entries_2.length; _a++) {
11787
- var target = entries_2[_a];
11998
+ var picked = false;
11999
+ for (var _i = 0, _a = path.slice(0, 12); _i < _a.length; _i++) {
12000
+ var target = _a[_i];
12001
+ if (control) {
12002
+ break;
12003
+ }
11788
12004
  if (!(target instanceof Element)) {
11789
12005
  continue;
11790
12006
  }
11791
- if (hasRoktSrc(target)) {
11792
- return undefined;
11793
- }
11794
- if (!control && target.matches(CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON)) {
12007
+ if (target.matches(CONTROL_SELECTOR + ', a[href], ' + VENDOR_BUTTON) || !picked && isWalletNamedControl(target)) {
11795
12008
  control = target;
11796
- } else if (!control && !option && target.matches(OPTION_INPUT)) {
11797
- option = target;
12009
+ } else {
12010
+ if (!option && target.matches(OPTION_INPUT)) {
12011
+ option = target;
12012
+ }
12013
+ picked = picked || target.matches(NAME_BARRIER);
11798
12014
  }
11799
12015
  }
11800
12016
  control = control || option;
11801
- if (!control || control.matches(':disabled, [aria-disabled="true"]')) {
12017
+ if (!control || control.matches(':disabled, [aria-disabled="true"]') || control.classList.contains('disabled') && isWalletNamed(control)) {
11802
12018
  return undefined;
11803
12019
  }
11804
12020
  return control;
11805
12021
  }
11806
- var EDITABLE = '[contenteditable]:not([contenteditable="false"])';
11807
12022
  // Text that may hold what a shopper typed or what the page opted out: form
11808
12023
  // fields, editable regions, and anything under data-mp-no-track.
11809
12024
  function isUnsafeTextSource(element) {
11810
12025
  return element.matches('input, select, textarea, option, optgroup, datalist, ' + EDITABLE) || !!element.querySelector('input, select, textarea, [data-mp-no-track], ' + EDITABLE) || hasComposedAncestor(element, 'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE);
11811
12026
  }
11812
- // A label around a checkbox or radio holds no typed text, so only the other
11813
- // fields rule its text out.
11814
- function isUnsafeLabelSource(label) {
11815
- return !!label.querySelector('input:not([type="checkbox"]):not([type="radio"]), select, textarea, [data-mp-no-track], ' + EDITABLE) || hasComposedAncestor(label, 'select, datalist, textarea, [data-mp-no-track], ' + EDITABLE);
11816
- }
11817
12027
 
11818
12028
  // Masks a button label before it is logged. Button text can carry personal data,
11819
12029
  // so this is the privacy boundary for every label and aria-label.
@@ -11830,10 +12040,12 @@ var mParticle = (function () {
11830
12040
  }
11831
12041
  }
11832
12042
  var digitPatterns = createDigitPatterns();
12043
+ // Longer raw labels are dropped, not cut short.
12044
+ var MAX_RAW_LABEL_LENGTH = 128;
11833
12045
  // The label as logged: lower case, at most 64 characters, digits and amounts
11834
12046
  // masked. Dropped when it looks like an email, phone or card number.
11835
12047
  function maskLabel(raw) {
11836
- if (!raw || raw.length > 128 || !digitPatterns) {
12048
+ if (!raw || raw.length > MAX_RAW_LABEL_LENGTH || !digitPatterns) {
11837
12049
  return undefined;
11838
12050
  }
11839
12051
  var text = raw.replace(/\s+/g, ' ').trim().toLowerCase();
@@ -11855,24 +12067,139 @@ var mParticle = (function () {
11855
12067
  }
11856
12068
  return control.matches('a[href]:not([role="button"])') ? 'link' : 'role';
11857
12069
  }
12070
+ var NOT_RENDERED = 'script, style, template, [hidden], [aria-hidden="true"], [inert]';
12071
+ // The most nodes read for one label source, and images looked at for one
12072
+ // button. Each costs a style lookup in the click handler.
12073
+ var MAX_READS = 64;
12074
+ function assignedNodes(element) {
12075
+ var slot = element;
12076
+ return isFunction(slot.assignedNodes) ? slot.assignedNodes() : [];
12077
+ }
12078
+ // Left out of what is drawn by its parent: a light child of an open shadow host
12079
+ // that no slot takes, or a slot's own child while nodes are assigned to the
12080
+ // slot. A closed shadow root can't be seen, so its host's children count.
12081
+ function isUnslotted(node) {
12082
+ var parent = node.parentElement;
12083
+ if (!parent) {
12084
+ return false;
12085
+ }
12086
+ return parent.shadowRoot ? !node.assignedSlot : assignedNodes(parent).length > 0;
12087
+ }
12088
+ // Hides the element and all it holds, whatever their own style says.
12089
+ // Screen-reader-only text is clipped rather than hidden, so it is not. An SVG
12090
+ // title is never drawn but names its icon, so it is not either.
12091
+ function hidesSubtree(element) {
12092
+ if (element.matches(NOT_RENDERED) || isUnslotted(element)) {
12093
+ return true;
12094
+ }
12095
+ return !(element instanceof SVGTitleElement) && window.getComputedStyle(element).display === 'none';
12096
+ }
12097
+ // Visibility is inherited and a child can set it back to visible, so only the
12098
+ // element's own counts.
12099
+ function isVisible(element) {
12100
+ var visibility = window.getComputedStyle(element).visibility;
12101
+ return visibility !== 'hidden' && visibility !== 'collapse';
12102
+ }
12103
+ function isShown(element, until) {
12104
+ return !someComposedAncestor(element, hidesSubtree, until);
12105
+ }
12106
+ // A shadow host shows a light child only through a slot, and only while
12107
+ // nothing from that slot up to the host hides it.
12108
+ function isShownThroughSlot(node, host) {
12109
+ var slot = node.assignedSlot;
12110
+ return !!slot && isShown(slot, host);
12111
+ }
12112
+ // Whitespace hides nothing, so a node holding only whitespace is taken as it
12113
+ // is, without a style lookup: an icon's paths cost nothing. A slot's text can
12114
+ // sit in the nodes assigned to it, so a slot is never blank.
12115
+ function isBlank(node) {
12116
+ return !/\S/.test(node.textContent || '') && !(node instanceof Element && (node.matches('slot') || !!node.querySelector('slot')));
12117
+ }
12118
+ // The text a sighted user or a screen reader gets from an element, whitespace
12119
+ // collapsed. Undefined when that runs past what maskLabel keeps, takes more
12120
+ // than MAX_READS nodes to read, or takes in a form field through a slot, so a
12121
+ // label is never cut short at a node boundary.
12122
+ function renderedText(root) {
12123
+ var text = '';
12124
+ var reads = 0;
12125
+ var append = function append(value) {
12126
+ text = (text + value).replace(/\s+/g, ' ');
12127
+ };
12128
+ var _read = function read(element) {
12129
+ if (hidesSubtree(element)) {
12130
+ return true;
12131
+ }
12132
+ var assigned = assignedNodes(element);
12133
+ var nodes = assigned.length ? assigned : element.childNodes;
12134
+ for (var i = 0; i < nodes.length; i++) {
12135
+ var node = nodes[i];
12136
+ var isText = node.nodeType === Node.TEXT_NODE;
12137
+ if (!isText && !(node instanceof Element)) {
12138
+ continue;
12139
+ }
12140
+ if (isBlank(node)) {
12141
+ append(node.textContent);
12142
+ continue;
12143
+ }
12144
+ if (++reads > MAX_READS) {
12145
+ return false;
12146
+ }
12147
+ if (element.shadowRoot && !isShownThroughSlot(node, element)) {
12148
+ continue;
12149
+ }
12150
+ if (isText) {
12151
+ if (isVisible(node.assignedSlot || element)) {
12152
+ append(node.nodeValue);
12153
+ }
12154
+ } else if (
12155
+ // Assigned nodes sit outside the label source, where its own
12156
+ // form field check never looked.
12157
+ assigned.length && isUnsafeTextSource(node) || !_read(node)) {
12158
+ return false;
12159
+ }
12160
+ if (text.trim().length > MAX_RAW_LABEL_LENGTH) {
12161
+ return false;
12162
+ }
12163
+ }
12164
+ return true;
12165
+ };
12166
+ return _read(root) ? text.trim() : undefined;
12167
+ }
12168
+ // '' when the element can't name the button, undefined when its text can't be
12169
+ // read in full.
11858
12170
  function textOf(element) {
11859
- return element && !isUnsafeTextSource(element) && element.textContent ? element.textContent.trim() : '';
12171
+ return element && !isUnsafeTextSource(element) && isShown(element) ? renderedText(element) : '';
12172
+ }
12173
+ // The control itself is already known to be shown.
12174
+ function shownImageAlt(control) {
12175
+ var images = control.querySelectorAll('img[alt]');
12176
+ for (var i = 0; i < images.length && i < MAX_READS; i++) {
12177
+ if (isShown(images[i], control) && isVisible(images[i])) {
12178
+ return nonBlank(images[i].getAttribute('alt'));
12179
+ }
12180
+ }
12181
+ return undefined;
11860
12182
  }
11861
12183
  function nonBlank(value) {
11862
12184
  return value && value.trim() ? value : undefined;
11863
12185
  }
11864
12186
  // The button's name as a screen reader would read it, simplified: the first of
11865
- // aria-labelledby, aria-label, own text (or an inner image's alt), title, for
11866
- // inputs their value, and for checkboxes and radios their label's text. The text
11867
- // sources are skipped for controls that hold form fields or more than four
11868
- // children, so a typed value is never read.
12187
+ // aria-labelledby, aria-label, own text (or an inner image's alt), title, and
12188
+ // for inputs their value. The text sources are skipped for controls that hold
12189
+ // form fields or more than four children, so a typed value is never read.
11869
12190
  function accessibleName(control) {
11870
12191
  var labelledBy = control.getAttribute('aria-labelledby');
11871
12192
  if (labelledBy) {
11872
12193
  var root_1 = isFunction(control.getRootNode) ? control.getRootNode() : document;
11873
- var text = labelledBy.split(/\s+/).map(function (id) {
12194
+ var parts = labelledBy.split(/\s+/).map(function (id) {
11874
12195
  return textOf(root_1.getElementById ? root_1.getElementById(id) : null);
11875
- }).filter(function (part) {
12196
+ });
12197
+ if (parts.indexOf(undefined) !== -1) {
12198
+ return {
12199
+ source: 'none'
12200
+ };
12201
+ }
12202
+ var text = parts.filter(function (part) {
11876
12203
  return !!part;
11877
12204
  }).join(' ');
11878
12205
  if (text) {
@@ -11916,27 +12243,21 @@ var mParticle = (function () {
11916
12243
  source: 'value'
11917
12244
  };
11918
12245
  }
11919
- } else if (type === 'checkbox' || type === 'radio') {
11920
- // The label the browser ties to the box, never the box's value.
11921
- var label = control.labels ? control.labels[0] : undefined;
11922
- var text = label && !isUnsafeLabelSource(label) && label.textContent ? label.textContent.trim() : '';
11923
- if (text) {
11924
- return {
11925
- raw: text,
11926
- source: 'label'
11927
- };
11928
- }
11929
12246
  }
11930
- } else if (readable) {
11931
- var text = textOf(control);
12247
+ } else if (readable && isShown(control)) {
12248
+ var text = renderedText(control);
12249
+ if (text === undefined) {
12250
+ return {
12251
+ source: 'none'
12252
+ };
12253
+ }
11932
12254
  if (text) {
11933
12255
  return {
11934
12256
  raw: text,
11935
12257
  source: 'text'
11936
12258
  };
11937
12259
  }
11938
- var image = control.querySelector('img[alt]');
11939
- var alt = image ? nonBlank(image.getAttribute('alt')) : undefined;
12260
+ var alt = shownImageAlt(control);
11940
12261
  if (alt) {
11941
12262
  return {
11942
12263
  raw: alt,
@@ -12034,9 +12355,12 @@ var mParticle = (function () {
12034
12355
  return form ? stableIdentifier(form.getAttribute('id')) : undefined;
12035
12356
  }
12036
12357
  // The browser's normalised type: a button defaults to 'submit' and an unknown
12037
- // input type reads as 'text'. Wallet and role buttons are just 'button'.
12038
- function controlType(control, element) {
12039
- return element === 'button' || element === 'input' ? control.type : 'button';
12358
+ // input type reads as 'text'. A native button or input reports its own type, a
12359
+ // wallet-named one included; any other control, such as a role button or a
12360
+ // vendor's wallet element, reads as 'button'.
12361
+ function controlType(control) {
12362
+ var tag = control.tagName.toLowerCase();
12363
+ return tag === 'button' || tag === 'input' ? control.type : 'button';
12040
12364
  }
12041
12365
  // Reads the link's own resolved URL parts, never its full href: only the hostname of
12042
12366
  // a link that leaves the site and the extension of a file it downloads travel. Navigation
@@ -12079,9 +12403,38 @@ var mParticle = (function () {
12079
12403
  type: 'same_page'
12080
12404
  } : undefined;
12081
12405
  }
12406
+ // Checkboxes and radios, and the roles that act as them. A role can list
12407
+ // fallbacks, so any of its tokens counts.
12408
+ var CHOICE = 'input[type="checkbox"], input[type="radio"], [role~="checkbox"], [role~="radio"], ' + '[role~="switch"], [role~="menuitemcheckbox"], [role~="menuitemradio"]';
12409
+ var RADIO_ROLE = '[role~="radio"], [role~="menuitemradio"]';
12410
+ function boxOf(element) {
12411
+ var box = element instanceof HTMLLabelElement ? element.control : element;
12412
+ return box && box.matches(CHOICE) ? box : null;
12413
+ }
12414
+ // The box a click on this control picks. One the click passed through below
12415
+ // the control comes first: querySelector can't reach a slotted box, a box in a
12416
+ // shadow root or the box of a label inside the control, and a row can hold
12417
+ // more than one box. Else the control itself, the box it labels, or the first
12418
+ // one it wraps, as on an option row whose name repeats the option's text.
12419
+ function choiceOf(control, path) {
12420
+ for (var _i = 0, path_1 = path; _i < path_1.length; _i++) {
12421
+ var target = path_1[_i];
12422
+ if (target === control) {
12423
+ break;
12424
+ }
12425
+ var box = target instanceof Element ? boxOf(target) : null;
12426
+ if (box) {
12427
+ return box;
12428
+ }
12429
+ }
12430
+ return boxOf(control) || control.querySelector(CHOICE);
12431
+ }
12082
12432
  // Describes a clicked control for auto button tracking. A link counts only when page
12083
12433
  // views can't see the click, and carries its link type.
12084
- function describeButtonClick(control) {
12434
+ function describeButtonClick(control, path) {
12435
+ if (path === void 0) {
12436
+ path = [];
12437
+ }
12085
12438
  var link;
12086
12439
  if (control.matches('a[href]:not([role="button"])')) {
12087
12440
  link = describeLink(control);
@@ -12090,28 +12443,41 @@ var mParticle = (function () {
12090
12443
  }
12091
12444
  }
12092
12445
  var element = elementKind(control);
12093
- var name = accessibleName(control);
12094
- var label = maskLabel(name.raw);
12446
+ var box = control instanceof HTMLInputElement && (control.type === 'checkbox' || control.type === 'radio') ? control : undefined;
12447
+ // A box's text sent with its state is the shopper's answer, and the text
12448
+ // can be a name, an address or a sensitive choice, so a box goes by its
12449
+ // hand-written identifiers alone. So does a control acting as one or
12450
+ // wrapping one, and a label, whose click the browser passes on to its box
12451
+ // as a second event.
12452
+ var choice = box || choiceOf(control, path);
12453
+ // A checkbox's identifiers name its question, which its state answers. A
12454
+ // radio's id, test id and the panel it opens name the option picked, so
12455
+ // only the name of its input, the question, goes with it. A wrapper's own
12456
+ // name, or a role radio's, can name the option instead.
12457
+ var radio = choice instanceof HTMLInputElement ? choice.type === 'radio' : !!choice && choice.matches(RADIO_ROLE);
12458
+ var named = !radio ? control : choice instanceof HTMLInputElement ? choice : null;
12459
+ var name = choice ? undefined : accessibleName(control);
12460
+ var label = name ? maskLabel(name.raw) : undefined;
12095
12461
  return {
12096
12462
  label: label,
12097
12463
  labelSource: label ? name.source : 'none',
12098
12464
  // Kept on its own as well: it often says more than the visible text,
12099
12465
  // such as which item a remove button removes.
12100
- ariaLabel: maskLabel(control.getAttribute('aria-label')),
12101
- icon: iconHint(control),
12102
- elementId: stableIdentifier(control.getAttribute('id')),
12103
- elementName: stableIdentifier(control.getAttribute('name')),
12104
- testId: firstIdentifier(control, TEST_ID_ATTRIBUTES),
12105
- controls: controlsIdentifier(control),
12466
+ ariaLabel: choice ? undefined : maskLabel(control.getAttribute('aria-label')),
12467
+ icon: choice ? undefined : iconHint(control),
12468
+ elementId: radio ? undefined : stableIdentifier(control.getAttribute('id')),
12469
+ elementName: stableIdentifier(named && named.getAttribute('name')),
12470
+ testId: radio ? undefined : firstIdentifier(control, TEST_ID_ATTRIBUTES),
12471
+ controls: radio ? undefined : controlsIdentifier(control),
12106
12472
  form: formIdentifier(control),
12107
12473
  element: element,
12108
- type: controlType(control, element),
12474
+ type: controlType(control),
12109
12475
  linkType: link ? link.type : undefined,
12110
12476
  linkDomain: link ? link.domain : undefined,
12111
12477
  fileExtension: link ? link.fileExtension : undefined,
12112
12478
  // The box as the click left it: the browser toggles it before the click
12113
12479
  // reaches listeners, and puts it back if the click is cancelled.
12114
- checked: control instanceof HTMLInputElement && (control.type === 'checkbox' || control.type === 'radio') ? control.checked : undefined
12480
+ checked: box ? box.checked : undefined
12115
12481
  };
12116
12482
  }
12117
12483
 
@@ -12128,6 +12494,10 @@ var mParticle = (function () {
12128
12494
  var linkCommits = new RegExp('^(?:(?:place|submit|complete)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase)|' + otherCommits + ')$');
12129
12495
  var routeGatedCommits = new RegExp('^(?:buy now|purchase|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)$');
12130
12496
  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('|') + ')$');
12497
+ var CART_NOUN = '(?:(?:my|your|the) )?(?:shopping )?(?:cart|bag|basket)';
12498
+ // "Add to bag", "View cart", "Shopping bag", "Bag", and a count badge such as
12499
+ // "1 item in bag", which reads "item in bag" once digits are stripped.
12500
+ var cartLabels = new RegExp('^(?:add(?: [^ ]+){0,5} to ' + CART_NOUN + '|(?:(?:view|open|go to) )?' + CART_NOUN + '(?: items?)?' + '|items? in ' + CART_NOUN + ')$');
12131
12501
  var WALLET_NAMES = 'apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|clearpay|cash app(?: pay)?|affirm|sezzle|samsung pay';
12132
12502
  var WALLET_VERBS = 'pay|buy|purchase|order|book|donate|subscribe|check ?out|checkout|express checkout|continue|proceed|' + 'complete purchase|place (?:my |your )?order';
12133
12503
  // "Pay with Klarna", "Buy with GPay", "Continue to PayPal".
@@ -12203,6 +12573,14 @@ var mParticle = (function () {
12203
12573
  return !!label;
12204
12574
  });
12205
12575
  }
12576
+ // A cart button is often a bare icon, so its title names it when nothing else
12577
+ // does. Only the cart rule reads the title.
12578
+ function isCartControl(control, cores) {
12579
+ var title = cores.length ? undefined : normalize(control.getAttribute('title'));
12580
+ return (title ? [coreLabel(title)] : cores).some(function (text) {
12581
+ return cartLabels.test(text);
12582
+ });
12583
+ }
12206
12584
  // "Buy now" and "Purchase" count only on a checkout route, and never on a plain
12207
12585
  // link, which is navigation.
12208
12586
  function isCommitLabel(text, isLink, pathname) {
@@ -12214,20 +12592,33 @@ var mParticle = (function () {
12214
12592
  // Classifies a clicked or submitting control found by findControl, in order:
12215
12593
  // pickers, toggles and pay-later messages never count; a vendor's own wallet
12216
12594
  // button counts before any label rule; a confirm label on any source counts
12217
- // before the look-alike exclusions veto; then wallet phrases and steps.
12595
+ // before the look-alike exclusions veto; then a wallet-named control, wallet
12596
+ // phrases and steps. A cart label, links included, counts only where none of
12597
+ // those do.
12218
12598
  function classifyIntent(control, pathname) {
12219
12599
  var vendor = vendorButtonKind(control);
12220
- if (isQuietIntentControl(control, vendor === 'wallet') || vendor === 'none') {
12600
+ if (vendor === 'none') {
12221
12601
  return undefined;
12222
12602
  }
12223
12603
  if (vendor) {
12224
- return {
12604
+ return isQuietIntentControl(control, vendor === 'wallet') ? undefined : {
12225
12605
  kind: vendor,
12226
12606
  source: 'vendor'
12227
12607
  };
12228
12608
  }
12229
12609
  var texts = labels(control);
12230
12610
  var cores = texts.map(coreLabel);
12611
+ var cart = isCartControl(control, cores) ? {
12612
+ kind: 'cart',
12613
+ source: 'label'
12614
+ } : undefined;
12615
+ if (isQuietIntentControl(control, false, !!cart)) {
12616
+ return undefined;
12617
+ }
12618
+ // A bag button that opens a flyout counts only as a cart click.
12619
+ if (cart && control.hasAttribute('aria-expanded')) {
12620
+ return cart;
12621
+ }
12231
12622
  var isLink = control.matches('a[href]:not([role="button"])');
12232
12623
  if (cores.some(function (text) {
12233
12624
  return isCommitLabel(text, isLink, pathname);
@@ -12240,7 +12631,13 @@ var mParticle = (function () {
12240
12631
  if (texts.concat(cores).some(function (text) {
12241
12632
  return englishExclusions.test(text) || otherExclusions.test(text);
12242
12633
  })) {
12243
- return undefined;
12634
+ return cart;
12635
+ }
12636
+ if (isWalletNamedControl(control)) {
12637
+ return {
12638
+ kind: 'wallet',
12639
+ source: 'name'
12640
+ };
12244
12641
  }
12245
12642
  var isButton = !isLink && control.matches(CONTROL_SELECTOR);
12246
12643
  for (var _i = 0, cores_1 = cores; _i < cores_1.length; _i++) {
@@ -12264,7 +12661,7 @@ var mParticle = (function () {
12264
12661
  };
12265
12662
  }
12266
12663
  }
12267
- return undefined;
12664
+ return cart;
12268
12665
  }
12269
12666
  // The host is the domain or one of its subdomains, never a look-alike suffix.
12270
12667
  function isOnDomain(host, domain) {
@@ -12307,6 +12704,8 @@ var mParticle = (function () {
12307
12704
  var WIN_BTN_KEY = '__mpBtn__';
12308
12705
  var THROTTLE_MS = 2000;
12309
12706
  var SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
12707
+ // Apple Pay must begin inside the tap's own handler, so the tap is this recent.
12708
+ var APPLE_PAY_AFTER_CLICK_MS = 1000;
12310
12709
  var PATH_LIMIT = 12;
12311
12710
  // A ceiling, not a dedupe: a held Enter key or browser automation fires a
12312
12711
  // trusted click per repeat, and each one is forwarded to every kit.
@@ -12345,6 +12744,63 @@ var mParticle = (function () {
12345
12744
  setTimeout(fn, 0);
12346
12745
  }
12347
12746
  };
12747
+ // Apple Pay's session opens only from a shopper's tap, whatever button the page
12748
+ // draws for it, so its opening is a wallet tap no label or name rule can miss.
12749
+ // The wrapper keeps the page's own begin under this key, to put it back.
12750
+ var APPLE_PAY_HOOK = '__mpApplePayHook__';
12751
+ var applePayPrototype = function applePayPrototype(win) {
12752
+ var session = win['ApplePaySession'];
12753
+ return session && session.prototype;
12754
+ };
12755
+ // Wraps ApplePaySession.prototype.begin once per window, so the active tracker
12756
+ // hears that a session began. The wrapper reads no arguments and no payment
12757
+ // data, and returns or throws exactly what the original does. Nothing here can
12758
+ // throw into SDK init: a throwing getter or a frozen prototype leaves Apple Pay
12759
+ // as it was.
12760
+ var hookApplePay = function hookApplePay(win) {
12761
+ try {
12762
+ var prototype = applePayPrototype(win);
12763
+ var begin_1 = prototype && prototype.begin;
12764
+ if (!isFunction(begin_1) || begin_1[APPLE_PAY_HOOK]) {
12765
+ return;
12766
+ }
12767
+ var opened = function opened() {
12768
+ var args = [];
12769
+ for (var _i = 0; _i < arguments.length; _i++) {
12770
+ args[_i] = arguments[_i];
12771
+ }
12772
+ var result = begin_1.apply(this, args);
12773
+ try {
12774
+ var tracker = getActiveButtonTracker();
12775
+ if (tracker) {
12776
+ tracker.onApplePayBegin();
12777
+ }
12778
+ } catch (e) {
12779
+ // A session the page began is never disturbed by tracking.
12780
+ }
12781
+ return result;
12782
+ };
12783
+ // Not enumerable, so the marker doesn't show among the method's own keys.
12784
+ Object.defineProperty(opened, APPLE_PAY_HOOK, {
12785
+ value: begin_1
12786
+ });
12787
+ prototype.begin = opened;
12788
+ } catch (e) {
12789
+ // Apple Pay stays as the page set it up.
12790
+ }
12791
+ };
12792
+ // Puts the page's own begin back, unless another script has wrapped it since.
12793
+ var unhookApplePay = function unhookApplePay(win) {
12794
+ try {
12795
+ var prototype = applePayPrototype(win);
12796
+ var begin = prototype && prototype.begin;
12797
+ if (isFunction(begin) && isFunction(begin[APPLE_PAY_HOOK])) {
12798
+ prototype.begin = begin[APPLE_PAY_HOOK];
12799
+ }
12800
+ } catch (e) {
12801
+ // Apple Pay stays as it is.
12802
+ }
12803
+ };
12348
12804
  var CAPTURE_PASSIVE = {
12349
12805
  capture: true,
12350
12806
  passive: true
@@ -12362,6 +12818,9 @@ var mParticle = (function () {
12362
12818
  // Bumped on reset so classification queued before it is dropped.
12363
12819
  this.generation = 0;
12364
12820
  this.lastCommitClick = -Infinity;
12821
+ // The latest trusted click: whether the page excluded it, and the intent it
12822
+ // already carries to the kit, with its source, if any.
12823
+ this.lastClick = null;
12365
12824
  this.lastSent = {};
12366
12825
  this.pagePath = null;
12367
12826
  this.pageEvents = 0;
@@ -12445,8 +12904,40 @@ var mParticle = (function () {
12445
12904
  win[WIN_BTN_KEY] = {};
12446
12905
  }
12447
12906
  win[WIN_BTN_KEY].tracker = this;
12907
+ // Only where checkout intent can reach the kit; a shopper who opts back in mid-page gets no session
12908
+ // signal until the next page load.
12909
+ if (!this.isKitSuppressed()) {
12910
+ hookApplePay(win);
12911
+ }
12448
12912
  this.log('listening');
12449
12913
  };
12914
+ // An Apple Pay session begun right after a tracked tap counts as a wallet
12915
+ // tap for the kit; one begun with no such tap, or from a tap the page
12916
+ // excluded, does not. A tap that reads as a confirm tap reaches the kit as
12917
+ // one. It is delivered on a timer, not after the next paint, since the
12918
+ // payment sheet may hold painting back: a tap that already reads as a
12919
+ // wallet goes now under its own rule, and the throttle drops the copy
12920
+ // queued for the paint.
12921
+ ButtonTracker.prototype.onApplePayBegin = function () {
12922
+ var _this = this;
12923
+ var t = Date.now();
12924
+ var click = this.lastClick;
12925
+ if (!this.active || !click || click.excluded || click.kind === 'commit' || t - click.t > APPLE_PAY_AFTER_CLICK_MS || this.isKitSuppressed()) {
12926
+ return;
12927
+ }
12928
+ var pathname = window.location.pathname;
12929
+ var generation = this.generation;
12930
+ setTimeout(function () {
12931
+ if (generation !== _this.generation) {
12932
+ return;
12933
+ }
12934
+ try {
12935
+ _this.emit('wallet', click.kind === 'wallet' ? click.source : 'api', 'apple_pay', t, pathname);
12936
+ } catch (e) {
12937
+ _this.log('[error] Apple Pay delivery threw: ' + e);
12938
+ }
12939
+ }, 0);
12940
+ };
12450
12941
  ButtonTracker.prototype.teardown = function () {
12451
12942
  var win = btnWindow();
12452
12943
  if (win) {
@@ -12460,6 +12951,7 @@ var mParticle = (function () {
12460
12951
  var state = win[WIN_BTN_KEY];
12461
12952
  if (state && state.tracker === this) {
12462
12953
  state.tracker = undefined;
12954
+ unhookApplePay(win);
12463
12955
  }
12464
12956
  }
12465
12957
  this.active = false;
@@ -12479,6 +12971,7 @@ var mParticle = (function () {
12479
12971
  this.generation++;
12480
12972
  this.pending = [];
12481
12973
  this.lastCommitClick = -Infinity;
12974
+ this.lastClick = null;
12482
12975
  this.lastSent = {};
12483
12976
  this.pagePath = null;
12484
12977
  this.pageEvents = 0;
@@ -12514,9 +13007,11 @@ var mParticle = (function () {
12514
13007
  var unconfirmedCommit = false;
12515
13008
  if (layer === 'click') {
12516
13009
  // The composed path is only populated during dispatch.
12517
- var control = findControl(isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : []);
13010
+ var path = isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : [];
13011
+ var excluded = isExcludedPath(path);
13012
+ var control = excluded ? undefined : findControl(path, true);
12518
13013
  if (control) {
12519
- click = logsEvents ? describeButtonClick(control) : undefined;
13014
+ click = logsEvents ? describeButtonClick(control, path) : undefined;
12520
13015
  if (click && click.checked !== undefined) {
12521
13016
  box = control;
12522
13017
  }
@@ -12525,6 +13020,12 @@ var mParticle = (function () {
12525
13020
  kind = intent ? intent.kind : undefined;
12526
13021
  source = intent ? intent.source : undefined;
12527
13022
  }
13023
+ this.lastClick = {
13024
+ t: t,
13025
+ excluded: excluded,
13026
+ kind: kind,
13027
+ source: source
13028
+ };
12528
13029
  } else if (layer === 'submit') {
12529
13030
  var submitter = event.submitter;
12530
13031
  var control = submitter ? findControl([submitter]) : undefined;
@@ -13618,6 +14119,7 @@ var mParticle = (function () {
13618
14119
  mpInstance._NativeSdkHelpers.initializeSessionAttributes(apiKey);
13619
14120
  // Button tracking never runs as a native passthrough.
13620
14121
  resetButtonTracking();
14122
+ mpInstance._RoktManager.setAutoButtonClicksForwarded(false);
13621
14123
  } else {
13622
14124
  // Main SDK initialization flow
13623
14125
  // Load any settings/identities/attributes from cookie or localStorage
@@ -13662,6 +14164,15 @@ var mParticle = (function () {
13662
14164
  // https://go.mparticle.com/work/SQDSDKS-7339
13663
14165
  mpInstance._RoktManager.init(roktConfig_1, createRoktFilteredUser(currentUserMPID), mpInstance.Identity, mpInstance._Store, mpInstance.Logger, roktOptions, mpInstance.captureTiming, mpInstance._ErrorReportingDispatcher, mpInstance._LoggingDispatcher, mpInstance._IntegrationCapture, createRoktFilteredUser);
13664
14166
  }
14167
+ // Checkout intent comes from the shopper's clicks, so it goes to the
14168
+ // Rokt kit only when that kit receives them. Set before
14169
+ // processForwarders: the kit can read it during this init, from its own
14170
+ // init or from a returning shopper's landing page view.
14171
+ var buttonClickBlocker = roktConfig_1 ? getButtonClickBlocker(roktConfig_1, kitBlocker) : undefined;
14172
+ if (buttonClickBlocker && getFeatureFlag(AutoButtonTracking)) {
14173
+ mpInstance.Logger.warning("Checkout intent is off: the ".concat(buttonClickBlocker, " keeps some or all Auto Button Click events from the Rokt kit."));
14174
+ }
14175
+ mpInstance._RoktManager.setAutoButtonClicksForwarded(!!roktConfig_1 && !buttonClickBlocker);
13665
14176
  mpInstance._Forwarders.processForwarders(config, mpInstance._APIClient.prepareForwardingStats);
13666
14177
  mpInstance._Forwarders.processPixelConfigs(config);
13667
14178
  // Checks if session is created, resumed, or needs to be ended
@@ -13696,8 +14207,6 @@ var mParticle = (function () {
13696
14207
  // so it stops listening. Re-created if the flag is re-enabled later.
13697
14208
  mpInstance._PageViewTracker.teardown();
13698
14209
  }
13699
- // Button clicks are logged for any workspace that opts in. Checkout
13700
- // intent also goes to the Rokt kit when this init configures one.
13701
14210
  if (getFeatureFlag(AutoButtonTracking)) {
13702
14211
  if (!mpInstance._ButtonTracker) {
13703
14212
  mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
@@ -14308,8 +14817,8 @@ var mParticle = (function () {
14308
14817
  this.configurePixel = function (settings) {
14309
14818
  self.getInstance().configurePixel(settings);
14310
14819
  };
14311
- this._setIntegrationDelay = function (moduleId, _boolean) {
14312
- self.getInstance()._setIntegrationDelay(moduleId, _boolean);
14820
+ this._setIntegrationDelay = function (moduleId, boolean) {
14821
+ self.getInstance()._setIntegrationDelay(moduleId, boolean);
14313
14822
  };
14314
14823
  this._getIntegrationDelays = function () {
14315
14824
  return self.getInstance()._getIntegrationDelays();