@mparticle/web-sdk 3.16.1 → 3.18.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.16.1";
207
+ var version = "3.18.0";
208
208
 
209
209
  var Constants = {
210
210
  sdkVersion: version,
@@ -370,16 +370,17 @@ var mParticle = (function () {
370
370
  CacheIdentity: 'cacheIdentity',
371
371
  AudienceAPI: 'audienceAPI',
372
372
  // CaptureIntegrationSpecificIds (legacy): boolean flag from server/UI
373
- // - 'True' → capture all integration-specific IDs
374
- // - 'False' → capture none
373
+ // - 'True' \u2192 capture all integration-specific IDs
374
+ // - 'False' \u2192 capture none
375
375
  CaptureIntegrationSpecificIds: 'captureIntegrationSpecificIds',
376
376
  // CaptureIntegrationSpecificIdsV2 (new): string mode from server
377
- // - 'all' → capture all IDs
378
- // - 'none' → capture none
379
- // - 'roktonly' → capture only Rokt-related IDs
377
+ // - 'all' \u2192 capture all IDs
378
+ // - 'none' \u2192 capture none
379
+ // - 'roktonly' \u2192 capture only Rokt-related IDs
380
380
  CaptureIntegrationSpecificIdsV2: 'captureIntegrationSpecificIds.V2',
381
381
  AstBackgroundEvents: 'astBackgroundEvents',
382
- AutoLogPageView: 'autoLogPageView'
382
+ AutoLogPageView: 'autoLogPageView',
383
+ AutoButtonTracking: 'autoButtonTracking'
383
384
  },
384
385
  DefaultInstance: 'default_instance',
385
386
  CCPAPurpose: 'data_sale_opt_out',
@@ -696,7 +697,7 @@ var mParticle = (function () {
696
697
  return fullUrl;
697
698
  };
698
699
  // Google's cookie matching service requires `google_hm` values to be web-safe
699
- // base64 (RFC 4648 §5) without padding, otherwise the write to the hosted
700
+ // base64 (RFC 4648 \u00A75) without padding, otherwise the write to the hosted
700
701
  // match table fails: https://developers.google.com/authorized-buyers/rtb/cookie-guide
701
702
  // (String#replaceAll is not available in our ES5 browser targets.)
702
703
  var WEB_SAFE_BASE64_REPLACEMENTS = {
@@ -3974,7 +3975,7 @@ var mParticle = (function () {
3974
3975
  }
3975
3976
  // The Search endpoint is colocated with /v1/identify under
3976
3977
  // identityUrl, so we reuse the same service URL builder. We do
3977
- // NOT append the apiKey to the URL — auth is done via x-mp-key.
3978
+ // NOT append the apiKey to the URL \u2014 auth is done via x-mp-key.
3978
3979
  var serviceUrl = _Helpers.createServiceUrl(identityUrl);
3979
3980
  var searchUrl = serviceUrl + 'search?cb=1';
3980
3981
  var environment = isDevelopmentMode ? 'development' : 'production';
@@ -4616,7 +4617,7 @@ var mParticle = (function () {
4616
4617
  var domain = moduleId === PARTNER_MODULE_IDS.TradeDesk ? window.location.hostname : undefined;
4617
4618
  // Google Marketing Platform accepts a web-safe base64-encoded MPID via the
4618
4619
  // `google_hm` query parameter, but only for accounts provisioned for Google
4619
- // Hosted Matching — Google returns errors otherwise. It is therefore gated
4620
+ // Hosted Matching \u2014 Google returns errors otherwise. It is therefore gated
4620
4621
  // behind the opt-in `enableHmTag` setting (off by default), which the server
4621
4622
  // delivers as the string 'True' when the UI toggle is enabled.
4622
4623
  var enableHmTag = ((_c = settings === null || settings === void 0 ? void 0 : settings.enableHmTag) === null || _c === void 0 ? void 0 : _c.toLowerCase()) === 'true';
@@ -5788,7 +5789,8 @@ var mParticle = (function () {
5788
5789
  CaptureIntegrationSpecificIds = _a.CaptureIntegrationSpecificIds,
5789
5790
  CaptureIntegrationSpecificIdsV2 = _a.CaptureIntegrationSpecificIdsV2,
5790
5791
  AstBackgroundEvents = _a.AstBackgroundEvents,
5791
- AutoLogPageView = _a.AutoLogPageView;
5792
+ AutoLogPageView = _a.AutoLogPageView,
5793
+ AutoButtonTracking = _a.AutoButtonTracking;
5792
5794
  if (!config.flags) {
5793
5795
  return {};
5794
5796
  }
@@ -5805,6 +5807,7 @@ var mParticle = (function () {
5805
5807
  flags[CaptureIntegrationSpecificIdsV2] = config.flags[CaptureIntegrationSpecificIdsV2] || 'none';
5806
5808
  flags[AstBackgroundEvents] = config.flags[AstBackgroundEvents] === 'True';
5807
5809
  flags[AutoLogPageView] = config.flags[AutoLogPageView] === 'True';
5810
+ flags[AutoButtonTracking] = config.flags[AutoButtonTracking] === 'True';
5808
5811
  return flags;
5809
5812
  }
5810
5813
  function processBaseUrls(config, flags, apiKey) {
@@ -6700,14 +6703,14 @@ var mParticle = (function () {
6700
6703
  return !!win && win.history !== undefined && typeof win.history.pushState === 'function' && typeof win.addEventListener === 'function';
6701
6704
  };
6702
6705
  // Wraps pushState/replaceState so `onNavigate` runs after the real method, and
6703
- // returns the single function that undoes it — or null when nothing was
6706
+ // returns the single function that undoes it \u2014 or null when nothing was
6704
6707
  // installed (history is already wrapped, or a frozen/sealed History rejected the
6705
6708
  // assignment). Handing back one undo closure keeps the wrapper and original
6706
6709
  // references out of the tracker entirely: there is no half-patched state for a
6707
6710
  // caller to inspect, and no way to restore the wrong pair.
6708
6711
  var patchHistory = function patchHistory(onNavigate, log) {
6709
6712
  if (window.history.pushState[WRAPPED_MARKER]) {
6710
- log('[patch] history already wrapped, skipping to avoid double-wrap — STACKED WRAPPER DETECTED');
6713
+ log('[patch] history already wrapped, skipping to avoid double-wrap \u2014 STACKED WRAPPER DETECTED');
6711
6714
  return null;
6712
6715
  }
6713
6716
  var originals = {};
@@ -6854,14 +6857,14 @@ var mParticle = (function () {
6854
6857
  // SECURITY: `code`, `state` and `nonce` are the OAuth 2.0 / OIDC authorization
6855
6858
  // code and the CSRF/replay tokens. They are credentials until redeemed, and
6856
6859
  // attaching them here persists them in the event store and forwards them to
6857
- // every configured kit. They are on the list by explicit product decision —
6860
+ // every configured kit. They are on the list by explicit product decision \u2014
6858
6861
  // deleting that line is the whole of the fix if that decision is revisited.
6859
6862
  var ALLOWED_QUERY_PARAMS = [
6860
6863
  // Campaign attribution
6861
6864
  'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id',
6862
6865
  // Ad-network click ids
6863
6866
  'gclid', 'gbraid', 'wbraid', 'fbclid', 'msclkid', 'ttclid', 'twclid', 'li_fat_id', 'dclid',
6864
- // OAuth / OIDC — see the SECURITY note above
6867
+ // OAuth / OIDC \u2014 see the SECURITY note above
6865
6868
  'client_id', 'redirect_uri', 'response_type', 'scope', 'state', 'code', 'nonce',
6866
6869
  // Pagination
6867
6870
  'page', 'limit', 'offset', 'cursor', 'per_page',
@@ -6876,7 +6879,7 @@ var mParticle = (function () {
6876
6879
  // Referral
6877
6880
  'ref', 'referrer'];
6878
6881
  // ---------------------------------------------------------------------------
6879
- // Pure helpers. No `this`, no globals, no side effects — callable and assertable
6882
+ // Pure helpers. No `this`, no globals, no side effects \u2014 callable and assertable
6880
6883
  // on their own, which is where the interesting rules live.
6881
6884
  // ---------------------------------------------------------------------------
6882
6885
  // Pulls the allowlisted query params off a URL. Delegates to the SDK's own
@@ -6903,7 +6906,7 @@ var mParticle = (function () {
6903
6906
  // Membership is an own-property check, not `in`. `in` walks the prototype
6904
6907
  // chain, so a name matching an Object.prototype member reports as present on
6905
6908
  // any plain object. ALLOWED_QUERY_PARAMS contains no such name, which was the
6906
- // only thing making `in` safe here — and it stops being a safe assumption the
6909
+ // only thing making `in` safe here \u2014 and it stops being a safe assumption the
6907
6910
  // moment this list can be extended from configuration.
6908
6911
  var capturedNames = function capturedNames(params) {
6909
6912
  return ALLOWED_QUERY_PARAMS.filter(function (name) {
@@ -6912,12 +6915,12 @@ var mParticle = (function () {
6912
6915
  };
6913
6916
  // The dedup key: pathname plus the allowlisted params in a fixed order, so that
6914
6917
  // reordering the query string is not a new page. Params outside the allowlist
6915
- // never make it into `page.params` and so cannot key a view — nor can the hash.
6918
+ // never make it into `page.params` and so cannot key a view \u2014 nor can the hash.
6916
6919
  //
6917
6920
  // Values are re-encoded because queryStringParser hands them back DECODED. A
6918
6921
  // value holding the pair delimiters would otherwise serialize exactly like two
6919
- // separate params — `{q: 'a&search=b'}` and `{q: 'a', search: 'b'}` both becoming
6920
- // `q=a&search=b` — and dedup would treat a real navigation between them as the
6922
+ // separate params \u2014 `{q: 'a&search=b'}` and `{q: 'a', search: 'b'}` both becoming
6923
+ // `q=a&search=b` \u2014 and dedup would treat a real navigation between them as the
6921
6924
  // same page and drop the view. `q`, `search` and `redirect_uri` carry `&` and `=`
6922
6925
  // routinely, so this is reachable rather than theoretical.
6923
6926
  var pageKey = function pageKey(page) {
@@ -7033,7 +7036,7 @@ var mParticle = (function () {
7033
7036
  };
7034
7037
  };
7035
7038
  // Log-safe description of a page: the path, plus the NAMES of the captured
7036
- // params. Values are deliberately omitted — verbose logging goes to the console,
7039
+ // params. Values are deliberately omitted \u2014 verbose logging goes to the console,
7037
7040
  // and session-replay tooling ships console output off-domain, which is not a
7038
7041
  // place for an OAuth code or a consumer's search terms.
7039
7042
  // Only reachable before init() assigns lastPage, which is ahead of any navigation
@@ -7081,7 +7084,7 @@ var mParticle = (function () {
7081
7084
  // Take over from whichever tracker is currently active: one left behind by
7082
7085
  // a previous module load, or this same tracker on a repeat
7083
7086
  // mParticle.init(). Their pending navigations are transferred rather than
7084
- // dropped — a route change may have queued a deferred fire that has not
7087
+ // dropped \u2014 a route change may have queued a deferred fire that has not
7085
7088
  // flushed yet, and dropping it loses a page view entirely.
7086
7089
  var active = getActiveTracker();
7087
7090
  var inheritedPages = this.retire(active);
@@ -7121,7 +7124,7 @@ var mParticle = (function () {
7121
7124
  if (!outgoing) {
7122
7125
  return [];
7123
7126
  }
7124
- this.log(outgoing === this ? '[init] retiring this tracker for a repeat mParticle.init() — transferring pending navigations and tearing down' : '[init] retiring a tracker from a previous module load — transferring pending navigations and tearing down');
7127
+ this.log(outgoing === this ? '[init] retiring this tracker for a repeat mParticle.init() \u2014 transferring pending navigations and tearing down' : '[init] retiring a tracker from a previous module load \u2014 transferring pending navigations and tearing down');
7125
7128
  var pages = outgoing.takePendingNavigations();
7126
7129
  outgoing.teardown();
7127
7130
  return pages;
@@ -7284,7 +7287,7 @@ var mParticle = (function () {
7284
7287
  };
7285
7288
  // The auto page view for the landing page. The SPA navigations that follow it
7286
7289
  // come from PageViewTracker instead, so both emitters attach the same
7287
- // allowlisted query params — and this is the one that matters for campaign
7290
+ // allowlisted query params \u2014 and this is the one that matters for campaign
7288
7291
  // attribution, since utm_*/gclid live on the entry URL.
7289
7292
  this.logPageView = function (options) {
7290
7293
  self.logEvent({
@@ -8429,8 +8432,8 @@ var mParticle = (function () {
8429
8432
  if (isEmpty(readyQueue)) {
8430
8433
  return [];
8431
8434
  }
8432
- // Without draining first, sync re-entry (cache-hit identify →
8433
- // parseIdentityResponse → processReadyQueue) re-iterates the same array:
8435
+ // Without draining first, sync re-entry (cache-hit identify \u2192
8436
+ // parseIdentityResponse \u2192 processReadyQueue) re-iterates the same array:
8434
8437
  // callers only replace `_preInit.readyQueue` with [] after we return, so the
8435
8438
  // nested call still sees every entry. That duplicates Identity/event calls
8436
8439
  // and can nest "Unable to compute proper mParticle function" wraps.
@@ -8508,7 +8511,7 @@ var mParticle = (function () {
8508
8511
  mpInstance.Logger.verbose(Messages$2.InformationMessages.StartingLogEvent + ': ' + method);
8509
8512
  // First, remove any falsy identity values and warn about them
8510
8513
  var removedFalsyIdentityData = mpInstance._Helpers.Validators.removeFalsyIdentityValues(identityApiData, mpInstance.Logger);
8511
- // Normalize convenience aliases (email_sha256 → other, mobile_sha256 → other2)
8514
+ // Normalize convenience aliases (email_sha256 \u2192 other, mobile_sha256 \u2192 other2)
8512
8515
  var cleanedIdentityApiData = (removedFalsyIdentityData === null || removedFalsyIdentityData === void 0 ? void 0 : removedFalsyIdentityData.userIdentities) ? __assign(__assign({}, removedFalsyIdentityData), {
8513
8516
  userIdentities: normalizeUserIdentityKeys(removedFalsyIdentityData.userIdentities)
8514
8517
  }) : removedFalsyIdentityData;
@@ -9691,7 +9694,7 @@ var mParticle = (function () {
9691
9694
  * Add a GDPR Consent State to the consent state object
9692
9695
  *
9693
9696
  * @method addGDPRConsentState
9694
- * @param purpose [String] Data processing purpose that describes the type of processing done on the data subject’s data
9697
+ * @param purpose [String] Data processing purpose that describes the type of processing done on the data subject\u2019s data
9695
9698
  * @param gdprConsent [Object] A GDPR consent object created via mParticle.Consent.createGDPRConsent(...)
9696
9699
  */
9697
9700
  function addGDPRConsentState(purpose, gdprConsent) {
@@ -9727,7 +9730,7 @@ var mParticle = (function () {
9727
9730
  * Remove a GDPR Consent State to the consent state object
9728
9731
  *
9729
9732
  * @method removeGDPRConsentState
9730
- * @param purpose [String] Data processing purpose that describes the type of processing done on the data subject’s data
9733
+ * @param purpose [String] Data processing purpose that describes the type of processing done on the data subject\u2019s data
9731
9734
  */
9732
9735
  function removeGDPRConsentState(purpose) {
9733
9736
  var normalizedPurpose = canonicalizeForDeduplication(purpose);
@@ -10624,6 +10627,8 @@ var mParticle = (function () {
10624
10627
  }
10625
10628
 
10626
10629
  var PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
10630
+ var CHECKOUT_INTENT_BUFFER_LIMIT = 4;
10631
+ var CHECKOUT_INTENT_MAX_AGE_MS = 30000;
10627
10632
  var ON_SHOPPABLE_ADS_READY_METHOD = 'onShoppableAdsReady';
10628
10633
  // The purpose of this class is to create a link between the Core mParticle SDK and the
10629
10634
  // Rokt Web SDK via a Web Kit.
@@ -10649,6 +10654,8 @@ var mParticle = (function () {
10649
10654
  this.initialized = false;
10650
10655
  this.isShoppableAdsLoaded = false;
10651
10656
  this.stopRouteChangeWatch = null;
10657
+ // Checkout intent raised before the kit attached: latest per kind.
10658
+ this.pendingCheckoutIntents = [];
10652
10659
  }
10653
10660
  /**
10654
10661
  * Sets a callback to be invoked when RoktManager becomes ready
@@ -10727,6 +10734,7 @@ var mParticle = (function () {
10727
10734
  var _a, _b, _c, _d;
10728
10735
  this.kit = kit;
10729
10736
  this.watchRouteChanges();
10737
+ this.flushCheckoutIntents();
10730
10738
  if ((_a = kit.settings) === null || _a === void 0 ? void 0 : _a.accountId) {
10731
10739
  this.store.setRoktAccountId(kit.settings.accountId);
10732
10740
  }
@@ -10772,6 +10780,70 @@ var mParticle = (function () {
10772
10780
  (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error("RoktManager: Error in onRouteChange: ".concat(getErrorMessage(e)));
10773
10781
  }
10774
10782
  };
10783
+ // Checkout intent is off unless the flag is on, and under opt-out, any Rokt
10784
+ // privacy launcher option, or Global Privacy Control.
10785
+ RoktManager.prototype.isCheckoutIntentSuppressed = function () {
10786
+ var _a, _b;
10787
+ var flags = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.SDKConfig) === null || _b === void 0 ? void 0 : _b.flags;
10788
+ if (!flags || flags[Constants.FeatureFlags.AutoButtonTracking] !== true || !this.store.isEnabled) {
10789
+ return true;
10790
+ }
10791
+ var options = this.launcherOptions || {};
10792
+ if (options.noFunctional || options.noTargeting || options.doNotShareOrSell) {
10793
+ return true;
10794
+ }
10795
+ var nav = typeof navigator === 'undefined' ? undefined : navigator;
10796
+ return !!nav && nav.globalPrivacyControl === true;
10797
+ };
10798
+ // Reads `this.kit` at call time; before a kit attaches the signal is buffered
10799
+ // with the path it was raised on.
10800
+ RoktManager.prototype.notifyCheckoutIntent = function (signal, pathname) {
10801
+ var _a;
10802
+ if (pathname === void 0) {
10803
+ pathname = window.location.pathname;
10804
+ }
10805
+ if (this.isCheckoutIntentSuppressed()) {
10806
+ return;
10807
+ }
10808
+ var kit = this.kit;
10809
+ if (!kit) {
10810
+ this.pendingCheckoutIntents = this.pendingCheckoutIntents.filter(function (pending) {
10811
+ return pending.signal.kind !== signal.kind;
10812
+ }).concat({
10813
+ signal: signal,
10814
+ pathname: pathname
10815
+ }).slice(-CHECKOUT_INTENT_BUFFER_LIMIT);
10816
+ return;
10817
+ }
10818
+ try {
10819
+ if (isFunction(kit.onCheckoutIntent)) {
10820
+ kit.onCheckoutIntent(signal);
10821
+ }
10822
+ } catch (e) {
10823
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error("RoktManager: Error in onCheckoutIntent: ".concat(getErrorMessage(e)));
10824
+ }
10825
+ };
10826
+ RoktManager.prototype.discardCheckoutIntents = function () {
10827
+ this.pendingCheckoutIntents = [];
10828
+ };
10829
+ // Replays recent buffered signals raised on the current page, oldest first.
10830
+ RoktManager.prototype.flushCheckoutIntents = function () {
10831
+ var _this = this;
10832
+ var pending = this.pendingCheckoutIntents;
10833
+ this.pendingCheckoutIntents = [];
10834
+ var now = Date.now();
10835
+ var pathname = window.location.pathname;
10836
+ pending.filter(function (_a) {
10837
+ var signal = _a.signal,
10838
+ signalPathname = _a.pathname;
10839
+ return now - signal.t < CHECKOUT_INTENT_MAX_AGE_MS && signalPathname === pathname;
10840
+ }).sort(function (a, b) {
10841
+ return a.signal.t - b.signal.t;
10842
+ }).forEach(function (_a) {
10843
+ var signal = _a.signal;
10844
+ return _this.notifyCheckoutIntent(signal);
10845
+ });
10846
+ };
10775
10847
  RoktManager.prototype.isAutoLogPageViewEnabled = function () {
10776
10848
  var _a, _b, _c;
10777
10849
  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;
@@ -10850,7 +10922,7 @@ var mParticle = (function () {
10850
10922
  if (isEmpty(newIdentities_1)) {
10851
10923
  this.setUserAttributes(this.currentUser, userAttributes_1);
10852
10924
  } else {
10853
- // Fire-and-forget identify — best-effort, does not block selectPlacements
10925
+ // Fire-and-forget identify \u2014 best-effort, does not block selectPlacements
10854
10926
  try {
10855
10927
  this.identityService.identify({
10856
10928
  userIdentities: __assign(__assign({}, currentUserIdentities), newIdentities_1)
@@ -11002,7 +11074,7 @@ var mParticle = (function () {
11002
11074
  /**
11003
11075
  * Tears down the current Rokt launcher and removes the placements it rendered.
11004
11076
  *
11005
- * Intended for hosts that cannot simply destroy the container element —
11077
+ * Intended for hosts that cannot simply destroy the container element \u2014
11006
11078
  * SPA route changes and confirmation pages, for example.
11007
11079
  *
11008
11080
  * Unlike the other proxied methods, a call made before the kit is ready is
@@ -11387,6 +11459,723 @@ var mParticle = (function () {
11387
11459
  return LoggingDispatcher;
11388
11460
  }();
11389
11461
 
11462
+ // Marks an automatically logged button click, like `is_auto_page_view`.
11463
+ var AUTO_BUTTON_ATTRIBUTE = 'is_auto_button_click';
11464
+ var BUTTON_CLICK_EVENT_NAME = 'Button Click';
11465
+ // One custom event per click, logged like logLink() with the auto marker and the
11466
+ // page fields that auto page views carry. No intent: classifying clicks is left
11467
+ // to whoever reads the data.
11468
+ var buildButtonClickEvent = function buildButtonClickEvent(click, pathname) {
11469
+ var _a;
11470
+ var data = (_a = {
11471
+ label_source: click.labelSource,
11472
+ element: click.element,
11473
+ type: click.type,
11474
+ path: pathname,
11475
+ hostname: window.location.hostname
11476
+ }, _a[AUTO_BUTTON_ATTRIBUTE] = true, _a);
11477
+ if (click.label) {
11478
+ data.label = click.label;
11479
+ }
11480
+ if (click.ariaLabel) {
11481
+ data.aria_label = click.ariaLabel;
11482
+ }
11483
+ if (click.icon) {
11484
+ data.icon = click.icon;
11485
+ }
11486
+ if (click.elementId) {
11487
+ data.element_id = click.elementId;
11488
+ }
11489
+ if (click.elementName) {
11490
+ data.element_name = click.elementName;
11491
+ }
11492
+ if (click.testId) {
11493
+ data.test_id = click.testId;
11494
+ }
11495
+ return {
11496
+ messageType: MessageType$1.PageEvent,
11497
+ name: BUTTON_CLICK_EVENT_NAME,
11498
+ data: data,
11499
+ eventType: EventType.Other
11500
+ };
11501
+ };
11502
+
11503
+ // Finds the control a click landed on, for both checkout intent and button
11504
+ // descriptions. Rokt's own elements, disabled controls and anything under
11505
+ // data-mp-no-track are never returned.
11506
+ var CONTROL_SELECTOR = 'button, input[type="submit"], input[type="button"], input[type="image"], [role="button"]';
11507
+ // closest() over the composed tree: slots, parents, then shadow hosts.
11508
+ function hasComposedAncestor(element, selector) {
11509
+ var node = element;
11510
+ while (node) {
11511
+ if (node.matches(selector)) {
11512
+ return true;
11513
+ }
11514
+ var root = isFunction(node.getRootNode) ? node.getRootNode() : null;
11515
+ node = node.assignedSlot || node.parentElement || root && root.host || null;
11516
+ }
11517
+ return false;
11518
+ }
11519
+ // Rokt's own placements and frames never count as partner checkout controls.
11520
+ function isRoktElement(element) {
11521
+ if (hasComposedAncestor(element, '[data-core-type], #rokt-controller-frame')) {
11522
+ return true;
11523
+ }
11524
+ var src = element.getAttribute('src');
11525
+ if (src) {
11526
+ try {
11527
+ if (new URL(src, 'https://example.test').pathname.indexOf('/wsdk/') !== -1) {
11528
+ return true;
11529
+ }
11530
+ } catch (e) {
11531
+ // An unparseable src is not a Rokt element.
11532
+ }
11533
+ }
11534
+ return false;
11535
+ }
11536
+ function isWalletElement(element) {
11537
+ return element.matches('apple-pay-button, shop-pay-button, .gpay-button, #AmazonPayButton, [data-amazon-pay-button]');
11538
+ }
11539
+ // Walks at most 12 entries of an event's composed path for the nearest control.
11540
+ // Rokt elements, disabled controls and anything under data-mp-no-track are skipped.
11541
+ function findControl(path) {
11542
+ var control;
11543
+ for (var _i = 0, _a = path.slice(0, 12); _i < _a.length; _i++) {
11544
+ var target = _a[_i];
11545
+ if (!(target instanceof Element)) {
11546
+ continue;
11547
+ }
11548
+ if (isRoktElement(target) || target.matches('[data-mp-no-track]')) {
11549
+ return undefined;
11550
+ }
11551
+ if (!control && (target.matches(CONTROL_SELECTOR + ', a[href]') || isWalletElement(target))) {
11552
+ control = target;
11553
+ }
11554
+ }
11555
+ if (!control || isRoktElement(control) || hasComposedAncestor(control, '[data-mp-no-track]') || control.matches(':disabled, [aria-disabled="true"]')) {
11556
+ return undefined;
11557
+ }
11558
+ return control;
11559
+ }
11560
+
11561
+ // Masks a button label before it is logged. Button text can carry personal data,
11562
+ // so this is the privacy boundary for every label and aria-label.
11563
+ // Masking needs \p{Nd} to see every script's digits. Browsers without Unicode
11564
+ // property escapes send no labels at all.
11565
+ function createDigitPatterns() {
11566
+ try {
11567
+ return {
11568
+ run: new RegExp('\\p{Nd}{6,}', 'u'),
11569
+ number: new RegExp('\\p{Nd}+(?:[.,\u060C\u066B\u066C]\\p{Nd}+)*', 'gu')
11570
+ };
11571
+ } catch (e) {
11572
+ return undefined;
11573
+ }
11574
+ }
11575
+ var digitPatterns = createDigitPatterns();
11576
+ // The label as logged: lower case, at most 64 characters, digits and amounts
11577
+ // masked. Dropped when it looks like an email, phone or card number.
11578
+ function maskLabel(raw) {
11579
+ if (!raw || raw.length > 128 || !digitPatterns) {
11580
+ return undefined;
11581
+ }
11582
+ var text = raw.replace(/\s+/g, ' ').trim().toLowerCase();
11583
+ if (!text || text.indexOf('@') !== -1 || digitPatterns.run.test(text.replace(/[\s()\-\u2010-\u2015]/g, ''))) {
11584
+ return undefined;
11585
+ }
11586
+ var masked = text.replace(digitPatterns.number, '#').slice(0, 64).trim();
11587
+ return masked || undefined;
11588
+ }
11589
+
11590
+ function elementKind(control) {
11591
+ if (isWalletElement(control)) {
11592
+ return 'wallet';
11593
+ }
11594
+ var tag = control.tagName.toLowerCase();
11595
+ return tag === 'button' || tag === 'input' ? tag : 'role';
11596
+ }
11597
+ var EDITABLE = '[contenteditable]:not([contenteditable="false"])';
11598
+ // Text that may hold what a shopper typed or what the page opted out: form
11599
+ // fields, editable regions, and anything under data-mp-no-track.
11600
+ function isUnsafeTextSource(element) {
11601
+ 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);
11602
+ }
11603
+ function textOf(element) {
11604
+ return element && !isUnsafeTextSource(element) && element.textContent ? element.textContent.trim() : '';
11605
+ }
11606
+ function nonBlank(value) {
11607
+ return value && value.trim() ? value : undefined;
11608
+ }
11609
+ // The button's name as a screen reader would read it, simplified: the first of
11610
+ // aria-labelledby, aria-label, own text (or an inner image's alt), title, and for
11611
+ // inputs their value. The text sources are skipped for controls that hold form
11612
+ // fields or more than four children, so a typed value is never read.
11613
+ function accessibleName(control) {
11614
+ var labelledBy = control.getAttribute('aria-labelledby');
11615
+ if (labelledBy) {
11616
+ var root_1 = isFunction(control.getRootNode) ? control.getRootNode() : document;
11617
+ var text = labelledBy.split(/\s+/).map(function (id) {
11618
+ return textOf(root_1.getElementById ? root_1.getElementById(id) : null);
11619
+ }).filter(function (part) {
11620
+ return !!part;
11621
+ }).join(' ');
11622
+ if (text) {
11623
+ return {
11624
+ raw: text,
11625
+ source: 'labelledby'
11626
+ };
11627
+ }
11628
+ }
11629
+ var aria = nonBlank(control.getAttribute('aria-label'));
11630
+ if (aria) {
11631
+ return {
11632
+ raw: aria,
11633
+ source: 'aria'
11634
+ };
11635
+ }
11636
+ var readable = control.children.length <= 4 && !isUnsafeTextSource(control);
11637
+ if (control instanceof HTMLInputElement) {
11638
+ // The browser's normalised type: an unknown value reads as 'text'.
11639
+ var type = control.type;
11640
+ if (type === 'image') {
11641
+ var alt = nonBlank(control.getAttribute('alt'));
11642
+ if (alt) {
11643
+ return {
11644
+ raw: alt,
11645
+ source: 'alt'
11646
+ };
11647
+ }
11648
+ } else if (type === 'submit' || type === 'button' || type === 'reset') {
11649
+ var value = nonBlank(control.value);
11650
+ if (value) {
11651
+ return {
11652
+ raw: value,
11653
+ source: 'value'
11654
+ };
11655
+ }
11656
+ if (type !== 'button') {
11657
+ // What the browser shows on an input without a value.
11658
+ return {
11659
+ raw: type,
11660
+ source: 'value'
11661
+ };
11662
+ }
11663
+ }
11664
+ } else if (readable) {
11665
+ var text = textOf(control);
11666
+ if (text) {
11667
+ return {
11668
+ raw: text,
11669
+ source: 'text'
11670
+ };
11671
+ }
11672
+ var image = control.querySelector('img[alt]');
11673
+ var alt = image ? nonBlank(image.getAttribute('alt')) : undefined;
11674
+ if (alt) {
11675
+ return {
11676
+ raw: alt,
11677
+ source: 'alt'
11678
+ };
11679
+ }
11680
+ }
11681
+ var title = nonBlank(control.getAttribute('title'));
11682
+ if (title) {
11683
+ return {
11684
+ raw: title,
11685
+ source: 'title'
11686
+ };
11687
+ }
11688
+ return {
11689
+ source: 'none'
11690
+ };
11691
+ }
11692
+ var ICON_CLASS = /^(?:fa|fas|far|fal|icon|bi|glyphicon|ti|mdi|ri|la|lucide|ion|feather|ph)-([a-z][a-z-]{1,31})$/;
11693
+ // Size, style and animation classes that share the icon prefixes.
11694
+ var ICON_MODIFIER = /^(?:solid|regular|light|thin|duotone|brands|sharp|fw|lg|xs|sm|xl|spin|pulse|border|inverse|stack|flip(?:-[a-z]+)?|rotate(?:-[a-z]+)?|pull-[a-z]+)$/;
11695
+ function iconToken(candidate) {
11696
+ if (!candidate) {
11697
+ return undefined;
11698
+ }
11699
+ var token = candidate.toLowerCase().replace(/^icon-/, '');
11700
+ return /^[a-z][a-z-]{1,31}$/.test(token) ? token : undefined;
11701
+ }
11702
+ // An icon name from an SVG sprite reference or an icon-font class, for buttons
11703
+ // whose only content is an icon. Letters and hyphens only.
11704
+ function iconHint(control) {
11705
+ var allowed = function allowed(element) {
11706
+ return !hasComposedAncestor(element, '[data-mp-no-track]');
11707
+ };
11708
+ var use = Array.from(control.querySelectorAll('use')).filter(allowed)[0];
11709
+ if (use) {
11710
+ var href = use.getAttribute('href') || use.getAttribute('xlink:href') || '';
11711
+ var hash = href.indexOf('#');
11712
+ var sprite = iconToken(hash === -1 ? null : href.slice(hash + 1));
11713
+ if (sprite) {
11714
+ return sprite;
11715
+ }
11716
+ }
11717
+ var candidates = [control].concat(Array.from(control.querySelectorAll('i, span, svg'))).filter(allowed).slice(0, 5);
11718
+ for (var _i = 0, candidates_1 = candidates; _i < candidates_1.length; _i++) {
11719
+ var element = candidates_1[_i];
11720
+ var classes = (element.getAttribute('class') || '').split(/\s+/);
11721
+ for (var _a = 0, classes_1 = classes; _a < classes_1.length; _a++) {
11722
+ var name_1 = classes_1[_a];
11723
+ var match = ICON_CLASS.exec(name_1.toLowerCase());
11724
+ if (match && !ICON_MODIFIER.test(match[1])) {
11725
+ return match[1];
11726
+ }
11727
+ }
11728
+ }
11729
+ return undefined;
11730
+ }
11731
+ // Words of letters, each optionally ending in up to three digits, joined by
11732
+ // hyphens or underscores: `add-to-cart`, `btnCheckout`, `step_2`. Generated ids
11733
+ // (`ember1234`, `a8f3e2c1`, `radix-:r1:`) and long numbers don't match.
11734
+ var HAND_WRITTEN_ID = /^[A-Za-z]+[0-9]{0,3}(?:[-_](?:[A-Za-z]+[0-9]{0,3}|[0-9]{1,3}))*$/;
11735
+ // A developer-written identifier, or nothing.
11736
+ function stableIdentifier(value) {
11737
+ if (!value) {
11738
+ return undefined;
11739
+ }
11740
+ var id = value.trim();
11741
+ // At most three digits in all, so numbers split into groups don't pass.
11742
+ var digits = id.replace(/[^0-9]/g, '').length;
11743
+ return id.length <= 64 && digits <= 3 && HAND_WRITTEN_ID.test(id) ? id : undefined;
11744
+ }
11745
+ var TEST_ID_ATTRIBUTES = ['data-testid', 'data-test-id', 'data-test', 'data-qa', 'data-cy'];
11746
+ function testIdentifier(control) {
11747
+ for (var _i = 0, TEST_ID_ATTRIBUTES_1 = TEST_ID_ATTRIBUTES; _i < TEST_ID_ATTRIBUTES_1.length; _i++) {
11748
+ var attribute = TEST_ID_ATTRIBUTES_1[_i];
11749
+ var id = stableIdentifier(control.getAttribute(attribute));
11750
+ if (id) {
11751
+ return id;
11752
+ }
11753
+ }
11754
+ return undefined;
11755
+ }
11756
+ // The browser's normalised type: a button defaults to 'submit' and an unknown
11757
+ // input type reads as 'text'. Wallet and role buttons are just 'button'.
11758
+ function controlType(control, element) {
11759
+ return element === 'button' || element === 'input' ? control.type : 'button';
11760
+ }
11761
+ // Describes a clicked button for auto button tracking. Links are left to page
11762
+ // views, so a link click is not described.
11763
+ function describeButtonClick(path) {
11764
+ var control = findControl(path);
11765
+ if (!control || control.matches('a[href]:not([role="button"])')) {
11766
+ return undefined;
11767
+ }
11768
+ var element = elementKind(control);
11769
+ var name = accessibleName(control);
11770
+ var label = maskLabel(name.raw);
11771
+ return {
11772
+ label: label,
11773
+ labelSource: label ? name.source : 'none',
11774
+ // Kept on its own as well: it often says more than the visible text,
11775
+ // such as which item a remove button removes.
11776
+ ariaLabel: maskLabel(control.getAttribute('aria-label')),
11777
+ icon: iconHint(control),
11778
+ elementId: stableIdentifier(control.getAttribute('id')),
11779
+ elementName: stableIdentifier(control.getAttribute('name')),
11780
+ testId: testIdentifier(control),
11781
+ element: element,
11782
+ type: controlType(control, element)
11783
+ };
11784
+ }
11785
+
11786
+ var checkoutPath = /(?:^|\/)(?:checkouts?|payment|order|kasse|paiement|commander?|pago)(?:\/|$)/i;
11787
+ var exclusions = new RegExp(['coupon|promo|newsletter|sign in|add to cart|gift card|pay later|pay in|payment plan|learn more', 'gutschein|anmelden|warenkorb|bon cadeau|connexion|panier|cup\u00F3n|iniciar sesi\u00F3n|carrito', 'carrello|codice sconto|accedi|buono regalo|winkelwagen|kortingscode|inloggen|cadeaubon|achteraf betalen', '\u30AB\u30FC\u30C8|\u30AF\u30FC\u30DD\u30F3|\u30ED\u30B0\u30A4\u30F3|\u4F1A\u54E1\u767B\u9332|\u30AE\u30D5\u30C8|\u5F8C\u6255\u3044|\u30E1\u30EB\u30DE\u30AC'].join('|'));
11788
+ var commits = new RegExp('^(?:' + ['(?:place|submit|complete|confirm|finali[sz]e)(?!(?: [^ ]+){0,2} (?:another|new|next)(?: |$))(?: [^ ]+){0,2} (?:order|purchase|booking|reservation|payment|checkout)', 'pay(?: securely)?(?: now)?|confirm and pay|order and pay|order with obligation to pay', '(?:jetzt )?(?:zahlungs|kosten)pflichtig (?:bestellen|buchen)|(?:jetzt )?bezahlen|kauf abschlie\u00DFen|bestellung abschlie\u00DFen', 'commander et payer|payer(?: maintenant)?|(?:confirmer|finaliser) (?:ma |la )?commande', "valider et payer|confirmer et payer|commande avec obligation de paiement|finaliser l'achat", '(?:realizar|confirmar|completar) (?:el |mi |la )?(?:pedido|compra|reserva)|finalizar (?:el |mi )?pedido|pagar(?: ahora)?', 'pedido con obligaci\u00F3n de pago', "(?:effettua|conferma|concludi|completa) (?:l'|il )?(?:ordine|acquisto)|ordine con obbligo di pagamento", 'paga(?: ora| adesso)?|ordina e paga', '(?:nu )?betalen|bestellen en betalen|bestelling plaatsen|plaats bestelling|bestelling afronden', 'bestelling met betalingsverplichting', '(?:\u3053\u306E\u5185\u5BB9\u3067|\u4E0A\u8A18\u306E?\u5185\u5BB9\u3067)?\u3054?(?:\u6CE8\u6587|\u8CFC\u5165|\u4E88\u7D04|\u6C7A\u6E08|\u304A?\u652F\u6255\u3044?)\u3092?(?:\u78BA\u5B9A|\u5B8C\u4E86)(?:\u3059\u308B)?', '(?:\u3053\u306E\u5185\u5BB9\u3067|\u4E0A\u8A18\u306E?\u5185\u5BB9\u3067)\u3054?(?:\u6CE8\u6587|\u8CFC\u5165|\u4E88\u7D04)\u3059\u308B|\u3054?\u6CE8\u6587\u3059\u308B|\u6C7A\u6E08\u3059\u308B|(?:\u4ECA\u3059\u3050|\u3092)?\u652F\u6255\u3046'].join('|') + ')$');
11789
+ 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)$');
11790
+ var steps = new RegExp('^(?:' + ['checkout|check out|check-out|continue to payment|proceed to checkout', '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('|') + ')$');
11791
+ var wallets = /^(?:apple pay|google pay|paypal|venmo|amazon pay|shop pay|klarna|afterpay|affirm)$/;
11792
+ // Exact HTTPS hosts of hosted wallet and payment frames.
11793
+ var frameHosts = new Set(['www.paypal.com', 'www.sandbox.paypal.com', 'pay.google.com', 'applepay.cdn-apple.com', 'js.stripe.com', 'checkoutshopper-live.adyen.com', 'payments.amazon.com', 'pay.shopify.com']);
11794
+ // Keep these classes ASCII-only in source bytes to avoid mojibake parse failures
11795
+ // when a host page forces non-UTF-8 decoding before JavaScript parse.
11796
+ var TYPOGRAPHIC_APOSTROPHES = /[\u2019\u2018]/g;
11797
+ var CURRENCY_AND_NUMBER_CHARS = /[0-9\uFF10-\uFF19$\u20AC\u00A3\u00A5\uFFE5\u20B9\u5186.,\uFF0C]/g;
11798
+ function getIntentRouteClass(pathname) {
11799
+ return checkoutPath.test(pathname) ? 'checkout_path' : 'other';
11800
+ }
11801
+ function normalize(text) {
11802
+ if (!text || text.length > 128) {
11803
+ return undefined;
11804
+ }
11805
+ return text.toLowerCase().replace(TYPOGRAPHIC_APOSTROPHES, "'").replace(CURRENCY_AND_NUMBER_CHARS, '').replace(/\s+/g, ' ').trim();
11806
+ }
11807
+ // Labels come from aria-label then visible text. Only submit and button inputs
11808
+ // have their value read, so a typed field value is never touched.
11809
+ function labelSources(element) {
11810
+ if (element.children.length > 4 || element.querySelector('input, select, textarea, [data-mp-no-track]')) {
11811
+ return [];
11812
+ }
11813
+ var text = null;
11814
+ if (element instanceof HTMLInputElement) {
11815
+ var type = element.type;
11816
+ if (type === 'submit' || type === 'button') {
11817
+ text = element.value;
11818
+ } else if (type === 'image') {
11819
+ text = element.getAttribute('alt');
11820
+ }
11821
+ } else {
11822
+ text = element.textContent;
11823
+ }
11824
+ return [element.getAttribute('aria-label'), text];
11825
+ }
11826
+ function labels(element) {
11827
+ return labelSources(element).map(normalize).filter(function (label) {
11828
+ return !!label;
11829
+ });
11830
+ }
11831
+ function classifyLabel(text, button, pathname) {
11832
+ if (commits.test(text) || routeGatedCommits.test(text) && getIntentRouteClass(pathname) === 'checkout_path') {
11833
+ return 'commit';
11834
+ }
11835
+ if (button && wallets.test(text)) {
11836
+ return 'wallet';
11837
+ }
11838
+ if (steps.test(text)) {
11839
+ return 'checkout_step';
11840
+ }
11841
+ return undefined;
11842
+ }
11843
+ function classifyIntentControl(path, pathname) {
11844
+ var control = findControl(path);
11845
+ return control ? classifyControl(control, pathname) : undefined;
11846
+ }
11847
+ function classifyControl(control, pathname) {
11848
+ var texts = labels(control);
11849
+ if (texts.some(function (text) {
11850
+ return exclusions.test(text);
11851
+ })) {
11852
+ return undefined;
11853
+ }
11854
+ var button = false;
11855
+ if (!control.matches('a[href]')) {
11856
+ button = control.matches(CONTROL_SELECTOR) || isWalletElement(control);
11857
+ }
11858
+ if (button && isWalletElement(control)) {
11859
+ return 'wallet';
11860
+ }
11861
+ for (var _i = 0, texts_1 = texts; _i < texts_1.length; _i++) {
11862
+ var text = texts_1[_i];
11863
+ var kind = classifyLabel(text, button, pathname);
11864
+ if (kind) {
11865
+ return kind;
11866
+ }
11867
+ }
11868
+ return undefined;
11869
+ }
11870
+ // Focus moving into a visible hosted wallet or payment frame.
11871
+ function classifyIntentFrame(target, win) {
11872
+ if (win.document.visibilityState !== 'visible' || !(target instanceof HTMLIFrameElement)) {
11873
+ return undefined;
11874
+ }
11875
+ if (isRoktElement(target)) {
11876
+ return undefined;
11877
+ }
11878
+ try {
11879
+ var url = new URL(target.src);
11880
+ if (url.protocol === 'https:' && frameHosts.has(url.hostname)) {
11881
+ return 'frame';
11882
+ }
11883
+ } catch (e) {
11884
+ // An unparseable frame src is not a wallet frame.
11885
+ }
11886
+ return undefined;
11887
+ }
11888
+
11889
+ // State lives on `window`, like APV, so a re-executed bundle never doubles the
11890
+ // listeners. Inspect `window.__mpBtn__` in a console to see if one is live.
11891
+ var WIN_BTN_KEY = '__mpBtn__';
11892
+ var THROTTLE_MS = 2000;
11893
+ var SUBMIT_AFTER_COMMIT_CLICK_MS = 1000;
11894
+ var PATH_LIMIT = 12;
11895
+ var EVENT_DEDUP_MS = 1000;
11896
+ var PAGE_EVENT_LIMIT = 50;
11897
+ var btnWindow = function btnWindow() {
11898
+ return typeof window === 'undefined' ? null : window;
11899
+ };
11900
+ var getActiveButtonTracker = function getActiveButtonTracker() {
11901
+ var win = btnWindow();
11902
+ var state = win ? win[WIN_BTN_KEY] : undefined;
11903
+ return state ? state.tracker : undefined;
11904
+ };
11905
+ // Stops whichever tracker is active, including one from an earlier bundle load.
11906
+ var resetButtonTracking = function resetButtonTracking() {
11907
+ var win = btnWindow();
11908
+ if (!win) {
11909
+ return;
11910
+ }
11911
+ var active = getActiveButtonTracker();
11912
+ if (active) {
11913
+ active.teardown();
11914
+ }
11915
+ win[WIN_BTN_KEY] = {};
11916
+ };
11917
+ // Runs once the browser has painted the page's response to the interaction, so
11918
+ // describing and logging a click never delays that paint.
11919
+ var afterNextPaint = function afterNextPaint(fn) {
11920
+ if (isFunction(window.requestAnimationFrame) && document.visibilityState !== 'hidden') {
11921
+ requestAnimationFrame(function () {
11922
+ setTimeout(fn, 0);
11923
+ });
11924
+ } else {
11925
+ setTimeout(fn, 0);
11926
+ }
11927
+ };
11928
+ var CAPTURE_PASSIVE = {
11929
+ capture: true,
11930
+ passive: true
11931
+ };
11932
+ var PASSIVE = {
11933
+ passive: true
11934
+ };
11935
+ // Logs every button click as an mParticle event, and hands checkout intent to the
11936
+ // Rokt kit as a frozen {kind, t}. Submits and wallet frame focus feed the kit only.
11937
+ var ButtonTracker = /** @class */function () {
11938
+ function ButtonTracker(mpInstance) {
11939
+ var _this = this;
11940
+ this.active = false;
11941
+ this.hasRoktKit = false;
11942
+ // Bumped on reset so classification queued before it is dropped.
11943
+ this.generation = 0;
11944
+ this.lastCommitClick = -Infinity;
11945
+ this.lastSent = {};
11946
+ this.pagePath = null;
11947
+ this.pageEvents = 0;
11948
+ this.lastEvent = {};
11949
+ // Clicks read at dispatch and waiting for the next paint.
11950
+ this.pending = [];
11951
+ this.drain = function () {
11952
+ var queue = _this.pending;
11953
+ _this.pending = [];
11954
+ queue.forEach(function (delivery) {
11955
+ return delivery();
11956
+ });
11957
+ };
11958
+ // A click that navigates away can unload the page before the next paint, so
11959
+ // anything waiting is delivered as the page is hidden or left. The capture
11960
+ // listeners run before the SDK's own upload on the same events, including
11961
+ // beforeunload, the first of them.
11962
+ this.onHide = function (event) {
11963
+ if (event.type === 'beforeunload' || event.type === 'pagehide' || document.visibilityState === 'hidden') {
11964
+ _this.drain();
11965
+ }
11966
+ };
11967
+ this.onPageShow = function (event) {
11968
+ // A page restored from the back-forward cache starts fresh.
11969
+ if (event.persisted) {
11970
+ _this.reset();
11971
+ }
11972
+ };
11973
+ this.onEvent = function (event) {
11974
+ try {
11975
+ _this.observe(event);
11976
+ } catch (e) {
11977
+ _this.log('[error] event handler threw: ' + e);
11978
+ }
11979
+ };
11980
+ this.mpInstance = mpInstance;
11981
+ }
11982
+ Object.defineProperty(ButtonTracker.prototype, "isActive", {
11983
+ get: function get() {
11984
+ return this.active;
11985
+ },
11986
+ enumerable: false,
11987
+ configurable: true
11988
+ });
11989
+ ButtonTracker.prototype.init = function (options) {
11990
+ var win = btnWindow();
11991
+ if (!win) {
11992
+ return;
11993
+ }
11994
+ var previous = getActiveButtonTracker();
11995
+ if (previous) {
11996
+ previous.teardown();
11997
+ }
11998
+ if (this.active) {
11999
+ this.teardown();
12000
+ }
12001
+ this.hasRoktKit = options.hasRoktKit;
12002
+ if (!this.eventsEnabled() && this.isKitSuppressed()) {
12003
+ this.log('opted out, not starting');
12004
+ return;
12005
+ }
12006
+ win.document.addEventListener('click', this.onEvent, CAPTURE_PASSIVE);
12007
+ win.document.addEventListener('submit', this.onEvent, CAPTURE_PASSIVE);
12008
+ win.addEventListener('blur', this.onEvent, PASSIVE);
12009
+ win.addEventListener('pageshow', this.onPageShow, PASSIVE);
12010
+ win.addEventListener('beforeunload', this.onHide, CAPTURE_PASSIVE);
12011
+ win.addEventListener('pagehide', this.onHide, CAPTURE_PASSIVE);
12012
+ win.document.addEventListener('visibilitychange', this.onHide, CAPTURE_PASSIVE);
12013
+ this.active = true;
12014
+ if (!win[WIN_BTN_KEY]) {
12015
+ win[WIN_BTN_KEY] = {};
12016
+ }
12017
+ win[WIN_BTN_KEY].tracker = this;
12018
+ this.log('listening');
12019
+ };
12020
+ ButtonTracker.prototype.teardown = function () {
12021
+ var win = btnWindow();
12022
+ if (win) {
12023
+ win.document.removeEventListener('click', this.onEvent, true);
12024
+ win.document.removeEventListener('submit', this.onEvent, true);
12025
+ win.removeEventListener('blur', this.onEvent);
12026
+ win.removeEventListener('pageshow', this.onPageShow);
12027
+ win.removeEventListener('beforeunload', this.onHide, true);
12028
+ win.removeEventListener('pagehide', this.onHide, true);
12029
+ win.document.removeEventListener('visibilitychange', this.onHide, true);
12030
+ var state = win[WIN_BTN_KEY];
12031
+ if (state && state.tracker === this) {
12032
+ state.tracker = undefined;
12033
+ }
12034
+ }
12035
+ this.active = false;
12036
+ // Signals not yet handed to a kit go with the tracker.
12037
+ this.clearPending();
12038
+ };
12039
+ // Drops queued classification, commit timing and buffered signals, so
12040
+ // nothing captured before an opt-out is delivered after an opt-in.
12041
+ ButtonTracker.prototype.clearPending = function () {
12042
+ this.reset();
12043
+ var roktManager = this.mpInstance._RoktManager;
12044
+ if (roktManager) {
12045
+ roktManager.discardCheckoutIntents();
12046
+ }
12047
+ };
12048
+ ButtonTracker.prototype.reset = function () {
12049
+ this.generation++;
12050
+ this.pending = [];
12051
+ this.lastCommitClick = -Infinity;
12052
+ this.lastSent = {};
12053
+ this.pagePath = null;
12054
+ this.pageEvents = 0;
12055
+ this.lastEvent = {};
12056
+ };
12057
+ ButtonTracker.prototype.enqueue = function (delivery) {
12058
+ this.pending.push(delivery);
12059
+ if (this.pending.length === 1) {
12060
+ afterNextPaint(this.drain);
12061
+ }
12062
+ };
12063
+ ButtonTracker.prototype.observe = function (event) {
12064
+ var _this = this;
12065
+ if (!this.active || !event.isTrusted) {
12066
+ return;
12067
+ }
12068
+ var layer = event.type;
12069
+ var logsEvents = layer === 'click' && this.eventsEnabled();
12070
+ if (!logsEvents && this.isKitSuppressed()) {
12071
+ return;
12072
+ }
12073
+ var t = Date.now();
12074
+ var pathname = window.location.pathname;
12075
+ // The button is read now, while the click is still being dispatched:
12076
+ // this capture listener runs before the page's own handlers, so a
12077
+ // button that disables or relabels itself is seen as it was clicked.
12078
+ // Logging and the kit hand-off wait for the next paint.
12079
+ var click;
12080
+ var kind;
12081
+ var unconfirmedCommit = false;
12082
+ if (layer === 'click') {
12083
+ // The composed path is only populated during dispatch.
12084
+ var path = isFunction(event.composedPath) ? event.composedPath().slice(0, PATH_LIMIT) : [];
12085
+ click = logsEvents ? describeButtonClick(path) : undefined;
12086
+ kind = classifyIntentControl(path, pathname);
12087
+ } else if (layer === 'submit') {
12088
+ var submitter = event.submitter;
12089
+ // A form submitted without a submitter only counts right after a
12090
+ // commit click, which is known once that click is delivered.
12091
+ kind = submitter ? classifyIntentControl([submitter], pathname) : 'commit';
12092
+ unconfirmedCommit = !submitter;
12093
+ }
12094
+ var generation = this.generation;
12095
+ this.enqueue(function () {
12096
+ if (generation !== _this.generation) {
12097
+ return;
12098
+ }
12099
+ try {
12100
+ if (layer === 'blur') {
12101
+ // Read after the blur settles: some browsers have not moved
12102
+ // focus into the frame yet when the window blurs.
12103
+ kind = classifyIntentFrame(document.activeElement, window);
12104
+ } else if (unconfirmedCommit && t - _this.lastCommitClick > SUBMIT_AFTER_COMMIT_CLICK_MS) {
12105
+ kind = undefined;
12106
+ }
12107
+ if (click && logsEvents) {
12108
+ _this.logClick(click, t, pathname);
12109
+ }
12110
+ if (kind) {
12111
+ _this.emit(kind, layer, t, pathname);
12112
+ }
12113
+ } catch (e) {
12114
+ _this.log('[error] delivery threw: ' + e);
12115
+ }
12116
+ });
12117
+ };
12118
+ // One event per identical button every second, and at most 50 per page.
12119
+ ButtonTracker.prototype.logClick = function (click, t, pathname) {
12120
+ if (!this.eventsEnabled()) {
12121
+ return;
12122
+ }
12123
+ if (pathname !== this.pagePath) {
12124
+ this.pagePath = pathname;
12125
+ this.pageEvents = 0;
12126
+ this.lastEvent = {};
12127
+ }
12128
+ if (this.pageEvents >= PAGE_EVENT_LIMIT) {
12129
+ return;
12130
+ }
12131
+ var key = [click.label, click.ariaLabel, click.icon, click.elementId, click.elementName, click.testId, click.element, click.type].join('|');
12132
+ var last = this.lastEvent[key];
12133
+ if (last !== undefined && t - last < EVENT_DEDUP_MS) {
12134
+ return;
12135
+ }
12136
+ this.lastEvent[key] = t;
12137
+ this.pageEvents++;
12138
+ // Restarts an expired session, as public logEvent() and page views do.
12139
+ this.mpInstance._SessionManager.resetSessionTimer();
12140
+ this.mpInstance._Events.logEvent(buildButtonClickEvent(click, pathname));
12141
+ };
12142
+ ButtonTracker.prototype.emit = function (kind, layer, t, pathname) {
12143
+ if (this.isKitSuppressed()) {
12144
+ return;
12145
+ }
12146
+ if (kind === 'commit' && layer === 'click') {
12147
+ this.lastCommitClick = t;
12148
+ }
12149
+ var last = this.lastSent[kind];
12150
+ if (last && last.pathname === pathname && t - last.t < THROTTLE_MS) {
12151
+ return;
12152
+ }
12153
+ this.lastSent[kind] = {
12154
+ t: t,
12155
+ pathname: pathname
12156
+ };
12157
+ this.mpInstance._RoktManager.notifyCheckoutIntent(Object.freeze({
12158
+ kind: kind,
12159
+ t: t
12160
+ }), pathname);
12161
+ };
12162
+ // Events follow the SDK's opt-out like any other event.
12163
+ ButtonTracker.prototype.eventsEnabled = function () {
12164
+ var store = this.mpInstance._Store;
12165
+ return !!store && store.isEnabled;
12166
+ };
12167
+ // The Rokt manager owns the kit's privacy gates: flag, opt-out, launcher
12168
+ // options and Global Privacy Control.
12169
+ ButtonTracker.prototype.isKitSuppressed = function () {
12170
+ var roktManager = this.mpInstance._RoktManager;
12171
+ return !this.hasRoktKit || !roktManager || roktManager.isCheckoutIntentSuppressed();
12172
+ };
12173
+ ButtonTracker.prototype.log = function (message) {
12174
+ this.mpInstance.Logger.verbose('mParticle button tracking: ' + message);
12175
+ };
12176
+ return ButtonTracker;
12177
+ }();
12178
+
11390
12179
  var Messages = Constants.Messages,
11391
12180
  HTTPCodes = Constants.HTTPCodes,
11392
12181
  FeatureFlags = Constants.FeatureFlags,
@@ -11520,6 +12309,10 @@ var mParticle = (function () {
11520
12309
  this.reset = function (instance) {
11521
12310
  try {
11522
12311
  instance._Persistence.resetPersistence();
12312
+ // The Rokt manager keeps its own store reference, so stop button
12313
+ // tracking here rather than relying on the deleted store.
12314
+ resetButtonTracking();
12315
+ instance._ButtonTracker = undefined;
11523
12316
  if (instance._Store) {
11524
12317
  delete instance._Store;
11525
12318
  }
@@ -11541,6 +12334,8 @@ var mParticle = (function () {
11541
12334
  instance._Events.stopTracking();
11542
12335
  resetPageViewTracking();
11543
12336
  instance._PageViewTracker = undefined;
12337
+ resetButtonTracking();
12338
+ instance._ButtonTracker = undefined;
11544
12339
  if (!keepPersistence) {
11545
12340
  instance._Persistence.resetPersistence();
11546
12341
  }
@@ -12139,6 +12934,12 @@ var mParticle = (function () {
12139
12934
  if (queued) return;
12140
12935
  self._SessionManager.resetSessionTimer();
12141
12936
  self._Store.isEnabled = !isOptingOut;
12937
+ if (isOptingOut) {
12938
+ if (self._ButtonTracker) {
12939
+ self._ButtonTracker.clearPending();
12940
+ }
12941
+ self._RoktManager.discardCheckoutIntents();
12942
+ }
12142
12943
  self._Events.logOptOut();
12143
12944
  self._Persistence.update();
12144
12945
  if (self._Store.activeForwarders.length) {
@@ -12305,7 +13106,9 @@ var mParticle = (function () {
12305
13106
  var _a, _b, _c, _d;
12306
13107
  var kitBlocker = createKitBlocker(config, mpInstance);
12307
13108
  var getFeatureFlag = mpInstance._Helpers.getFeatureFlag;
12308
- var AutoLogPageView = Constants.FeatureFlags.AutoLogPageView;
13109
+ var _e = Constants.FeatureFlags,
13110
+ AutoLogPageView = _e.AutoLogPageView,
13111
+ AutoButtonTracking = _e.AutoButtonTracking;
12309
13112
  // Destroy previous batch uploader to prevent leaked timers and event listeners
12310
13113
  if ((_a = mpInstance._APIClient) === null || _a === void 0 ? void 0 : _a.uploader) {
12311
13114
  mpInstance._APIClient.uploader.destroy();
@@ -12321,6 +13124,8 @@ var mParticle = (function () {
12321
13124
  // persistence or Identity directly.
12322
13125
  if (mpInstance._Store.webviewBridgeEnabled) {
12323
13126
  mpInstance._NativeSdkHelpers.initializeSessionAttributes(apiKey);
13127
+ // Button tracking never runs as a native passthrough.
13128
+ resetButtonTracking();
12324
13129
  } else {
12325
13130
  // Main SDK initialization flow
12326
13131
  // Load any settings/identities/attributes from cookie or localStorage
@@ -12400,6 +13205,20 @@ var mParticle = (function () {
12400
13205
  // so it stops listening. Re-created if the flag is re-enabled later.
12401
13206
  mpInstance._PageViewTracker.teardown();
12402
13207
  }
13208
+ // Button clicks are logged for any workspace that opts in. Checkout
13209
+ // intent also goes to the Rokt kit when this init configures one.
13210
+ if (getFeatureFlag(AutoButtonTracking)) {
13211
+ if (!mpInstance._ButtonTracker) {
13212
+ mpInstance._ButtonTracker = new ButtonTracker(mpInstance);
13213
+ }
13214
+ mpInstance._ButtonTracker.init({
13215
+ hasRoktKit: !!roktConfig
13216
+ });
13217
+ } else {
13218
+ // Also retires a tracker left by an earlier bundle load. Like APV,
13219
+ // this assumes a single active SDK instance.
13220
+ resetButtonTracking();
13221
+ }
12403
13222
  processIdentityCallback(mpInstance, currentUser, currentUserMPID, currentUserIdentities);
12404
13223
  }
12405
13224
  // We will continue to clear out the ready queue as part of the initial init flow
@@ -12493,6 +13312,8 @@ var mParticle = (function () {
12493
13312
  mpInstance._LoggingDispatcher.logger = mpInstance.Logger;
12494
13313
  mpInstance._Store = new Store(config, mpInstance, apiKey);
12495
13314
  window.mParticle.Store = mpInstance._Store;
13315
+ // Button tracking stops until this init's config decides whether it runs.
13316
+ resetButtonTracking();
12496
13317
  mpInstance.Logger.verbose(StartingInitialization);
12497
13318
  // Initialize CookieConsentManager with privacy flags from launcherOptions
12498
13319
  var _a = normalizeRoktLauncherOptions(config === null || config === void 0 ? void 0 : config.launcherOptions),