@formo/analytics 1.33.0 → 1.34.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.
Files changed (53) hide show
  1. package/README.md +3 -0
  2. package/dist/cjs/src/FormoAnalytics.d.ts +65 -19
  3. package/dist/cjs/src/FormoAnalytics.js +187 -94
  4. package/dist/cjs/src/core.d.ts +2 -2
  5. package/dist/cjs/src/core.js +2 -1
  6. package/dist/cjs/src/event/EventFactory.d.ts +10 -4
  7. package/dist/cjs/src/event/EventFactory.js +50 -23
  8. package/dist/cjs/src/event/constants.d.ts +10 -1
  9. package/dist/cjs/src/event/constants.js +20 -8
  10. package/dist/cjs/src/event/sanitize.d.ts +13 -0
  11. package/dist/cjs/src/event/sanitize.js +94 -0
  12. package/dist/cjs/src/privy/index.d.ts +8 -3
  13. package/dist/cjs/src/privy/index.js +8 -3
  14. package/dist/cjs/src/privy/types.d.ts +25 -2
  15. package/dist/cjs/src/privy/utils.d.ts +100 -0
  16. package/dist/cjs/src/privy/utils.js +375 -16
  17. package/dist/cjs/src/session/index.d.ts +73 -6
  18. package/dist/cjs/src/session/index.js +309 -12
  19. package/dist/cjs/src/solana/SolanaManager.d.ts +1 -1
  20. package/dist/cjs/src/solana/SolanaManager.js +1 -1
  21. package/dist/cjs/src/solana/storeTypes.d.ts +1 -1
  22. package/dist/cjs/src/solana/storeTypes.js +1 -1
  23. package/dist/cjs/src/solana/types.d.ts +2 -2
  24. package/dist/cjs/src/types/base.d.ts +19 -3
  25. package/dist/cjs/src/version.d.ts +1 -1
  26. package/dist/cjs/src/version.js +1 -1
  27. package/dist/esm/src/FormoAnalytics.d.ts +65 -19
  28. package/dist/esm/src/FormoAnalytics.js +187 -94
  29. package/dist/esm/src/core.d.ts +2 -2
  30. package/dist/esm/src/core.js +1 -1
  31. package/dist/esm/src/event/EventFactory.d.ts +10 -4
  32. package/dist/esm/src/event/EventFactory.js +51 -24
  33. package/dist/esm/src/event/constants.d.ts +10 -1
  34. package/dist/esm/src/event/constants.js +19 -8
  35. package/dist/esm/src/event/sanitize.d.ts +13 -0
  36. package/dist/esm/src/event/sanitize.js +88 -0
  37. package/dist/esm/src/privy/index.d.ts +8 -3
  38. package/dist/esm/src/privy/index.js +7 -3
  39. package/dist/esm/src/privy/types.d.ts +25 -2
  40. package/dist/esm/src/privy/utils.d.ts +100 -0
  41. package/dist/esm/src/privy/utils.js +374 -16
  42. package/dist/esm/src/session/index.d.ts +73 -6
  43. package/dist/esm/src/session/index.js +309 -12
  44. package/dist/esm/src/solana/SolanaManager.d.ts +1 -1
  45. package/dist/esm/src/solana/SolanaManager.js +1 -1
  46. package/dist/esm/src/solana/storeTypes.d.ts +1 -1
  47. package/dist/esm/src/solana/storeTypes.js +1 -1
  48. package/dist/esm/src/solana/types.d.ts +2 -2
  49. package/dist/esm/src/types/base.d.ts +19 -3
  50. package/dist/esm/src/version.d.ts +1 -1
  51. package/dist/esm/src/version.js +1 -1
  52. package/dist/index.umd.min.js +1 -1
  53. package/package.json +8 -8
@@ -75,6 +75,7 @@ var chain_1 = require("./utils/chain");
75
75
  var wagmi_1 = require("./wagmi");
76
76
  var solana_1 = require("./solana");
77
77
  var SolanaManager_1 = require("./solana/SolanaManager");
78
+ var privy_1 = require("./privy");
78
79
  /**
79
80
  * Constants for provider switching reasons
80
81
  */
@@ -90,7 +91,7 @@ var FormoAnalytics = /** @class */ (function () {
90
91
  var _a, _b, _c;
91
92
  this.writeKey = writeKey;
92
93
  this.options = options;
93
- // Per-chain namespace state isolates EVM and Solana connection state
94
+ // Per-chain namespace state - isolates EVM and Solana connection state
94
95
  this._chainState = {
95
96
  evm: {},
96
97
  solana: {},
@@ -206,7 +207,7 @@ var FormoAnalytics = /** @class */ (function () {
206
207
  this.trackPageHits();
207
208
  }
208
209
  Object.defineProperty(FormoAnalytics.prototype, "_provider", {
209
- // EVM state accessors EVM listener paths must use these instead of
210
+ // EVM state accessors - EVM listener paths must use these instead of
210
211
  // currentAddress/currentChainId to avoid cross-namespace reads.
211
212
  get: function () {
212
213
  return this._chainState.evm.provider;
@@ -396,7 +397,7 @@ var FormoAnalytics = /** @class */ (function () {
396
397
  return [2 /*return*/];
397
398
  }
398
399
  // connect() persists wallet/chain state (active-wallet cookie,
399
- // currentAddress/currentChainId) before trackEvent's consent check
400
+ // currentAddress/currentChainId) before trackEvent's consent check -
400
401
  // gate the whole method so a suppressed visitor or excluded environment
401
402
  // (opt-out / timezone / host / path) leaves no session state.
402
403
  if (this.isTrackingSuppressed()) {
@@ -553,79 +554,85 @@ var FormoAnalytics = /** @class */ (function () {
553
554
  });
554
555
  });
555
556
  };
556
- /**
557
- * Emits an identify event with current wallet address and provider info.
558
- *
559
- * @param {string} params.address - Wallet address
560
- * @param {string} params.userId - External user ID
561
- * @param {string} params.rdns - Provider reverse domain name
562
- * @param {string} params.providerName - Provider display name
563
- * @param {IFormoEventProperties} properties - Additional properties to include with the identify event
564
- * @param {IFormoEventContext} context
565
- * @param {(...args: unknown[]) => void} callback
566
- * @returns {Promise<void>}
567
- *
568
- * @example
569
- * ```ts
570
- * // Basic identify
571
- * formo.identify({ address: '0x...', userId: 'user123' });
572
- *
573
- * // With Privy user
574
- * import { parsePrivyProperties } from '@formo/analytics';
575
- * const { user } = usePrivy();
576
- * if (user) {
577
- * const { properties, wallets } = parsePrivyProperties(user);
578
- * for (const wallet of wallets) {
579
- * formo.identify({ address: wallet.address, userId: user.id }, properties);
580
- * }
581
- * }
582
- * ```
583
- */
584
- FormoAnalytics.prototype.identify = function (params, properties, context, callback) {
557
+ FormoAnalytics.prototype.identify = function (paramsOrUser, propertiesOrOptions, context, callback) {
585
558
  return __awaiter(this, void 0, void 0, function () {
586
- var _i, _a, providerDetail, provider, address_2, validAddress_1, err_1, address, providerName, userId, rdns, validAddress, domain, isAlreadyIdentified, e_1;
559
+ var maybeUser, opts, params, properties, _i, _a, providerDetail, provider, address_2, validAddress_1, err_1, address, providerName, userId, rdns, setActive, validAddress, domain, isAlreadyIdentified, e_1;
587
560
  var _b, _c;
588
561
  return __generator(this, function (_d) {
589
562
  switch (_d.label) {
590
563
  case 0:
591
- _d.trys.push([0, 12, , 13]);
564
+ _d.trys.push([0, 14, , 15]);
565
+ maybeUser = paramsOrUser;
566
+ if (!(maybeUser &&
567
+ typeof maybeUser.id === "string" &&
568
+ maybeUser.address === undefined)) return [3 /*break*/, 2];
569
+ opts = (propertiesOrOptions !== null && propertiesOrOptions !== void 0 ? propertiesOrOptions : {});
570
+ // identifyPrivyUser records every linked wallet for clustering WITHOUT
571
+ // touching active state (internal setActive:false), promotes only the
572
+ // resolved active wallet, and reconciles the chain id with that wallet's
573
+ // namespace before emitting. It reads this.currentAddress itself to
574
+ // preserve an already-connected wallet, so this dispatch is a thin
575
+ // pass-through and both entry points behave identically.
576
+ return [4 /*yield*/, (0, privy_1.identifyPrivyUser)(this, maybeUser, {
577
+ activeAddress: opts.activeAddress,
578
+ properties: opts.properties,
579
+ })];
580
+ case 1:
581
+ // identifyPrivyUser records every linked wallet for clustering WITHOUT
582
+ // touching active state (internal setActive:false), promotes only the
583
+ // resolved active wallet, and reconciles the chain id with that wallet's
584
+ // namespace before emitting. It reads this.currentAddress itself to
585
+ // preserve an already-connected wallet, so this dispatch is a thin
586
+ // pass-through and both entry points behave identically.
587
+ _d.sent();
588
+ return [2 /*return*/];
589
+ case 2:
590
+ params = paramsOrUser;
591
+ properties = propertiesOrOptions;
592
592
  // identify() writes the user-id cookie and marks wallet
593
- // identification before trackEvent's consent check gate the whole
593
+ // identification before trackEvent's consent check - gate the whole
594
594
  // method so a suppressed visitor or excluded environment (opt-out /
595
595
  // timezone / host / path) gets no identity persistence.
596
- if (this.isTrackingSuppressed()) {
597
- logger_1.logger.info("identify() skipped: tracking is suppressed for this visitor or environment");
596
+ //
597
+ // The chain check belongs here and NOT in isTrackingSuppressed(), which
598
+ // identifyPrivyUser calls before it reconciles the chain. Folding it in
599
+ // there would block a Privy sync that reconciliation was about to make
600
+ // valid (activating a Solana wallet while an excluded EVM chain id is
601
+ // still current). By the time the Privy path reaches this guard it has
602
+ // already reconciled, so each inner identify is judged on the right chain.
603
+ if (this.isTrackingSuppressed() || this.isCurrentChainExcluded()) {
604
+ logger_1.logger.info("identify() skipped: tracking is suppressed for this visitor, environment, or chain");
598
605
  return [2 /*return*/];
599
606
  }
600
- if (!!params) return [3 /*break*/, 10];
607
+ if (!!params) return [3 /*break*/, 12];
601
608
  // If no params provided, auto-identify
602
609
  logger_1.logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
603
610
  _i = 0, _a = this._providers;
604
- _d.label = 1;
605
- case 1:
606
- if (!(_i < _a.length)) return [3 /*break*/, 9];
611
+ _d.label = 3;
612
+ case 3:
613
+ if (!(_i < _a.length)) return [3 /*break*/, 11];
607
614
  providerDetail = _a[_i];
608
615
  provider = providerDetail.provider;
609
616
  if (!provider)
610
- return [3 /*break*/, 8];
611
- _d.label = 2;
612
- case 2:
613
- _d.trys.push([2, 7, , 8]);
617
+ return [3 /*break*/, 10];
618
+ _d.label = 4;
619
+ case 4:
620
+ _d.trys.push([4, 9, , 10]);
614
621
  return [4 /*yield*/, this.getAddress(provider)];
615
- case 3:
622
+ case 5:
616
623
  address_2 = _d.sent();
617
- if (!address_2) return [3 /*break*/, 6];
624
+ if (!address_2) return [3 /*break*/, 8];
618
625
  validAddress_1 = (0, address_1.validateAndChecksumAddress)(address_2);
619
626
  logger_1.logger.info("Auto-identify: Checking deduplication", {
620
627
  validAddress: validAddress_1,
621
628
  rdns: providerDetail.info.rdns,
622
629
  providerName: providerDetail.info.name,
623
630
  isAlreadyIdentified: validAddress_1
624
- ? this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns)
631
+ ? this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns, undefined, properties)
625
632
  : false,
626
633
  });
627
634
  if (!(validAddress_1 &&
628
- !this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns))) return [3 /*break*/, 5];
635
+ !this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns, undefined, properties))) return [3 /*break*/, 7];
629
636
  logger_1.logger.info("Auto-identifying", validAddress_1, providerDetail.info.name, providerDetail.info.rdns);
630
637
  // NOTE: do not set this.currentAddress without explicit connect or identify
631
638
  return [4 /*yield*/, this.identify({
@@ -633,26 +640,26 @@ var FormoAnalytics = /** @class */ (function () {
633
640
  providerName: providerDetail.info.name,
634
641
  rdns: providerDetail.info.rdns,
635
642
  }, properties, context, callback)];
636
- case 4:
643
+ case 6:
637
644
  // NOTE: do not set this.currentAddress without explicit connect or identify
638
645
  _d.sent();
639
- return [3 /*break*/, 6];
640
- case 5:
646
+ return [3 /*break*/, 8];
647
+ case 7:
641
648
  if (validAddress_1) {
642
649
  logger_1.logger.info("Auto-identify: Skipping already identified wallet", validAddress_1, providerDetail.info.name, providerDetail.info.rdns);
643
650
  }
644
- _d.label = 6;
645
- case 6: return [3 /*break*/, 8];
646
- case 7:
651
+ _d.label = 8;
652
+ case 8: return [3 /*break*/, 10];
653
+ case 9:
647
654
  err_1 = _d.sent();
648
655
  logger_1.logger.error("Failed to identify provider ".concat(providerDetail.info.name, ":"), err_1);
649
- return [3 /*break*/, 8];
650
- case 8:
651
- _i++;
652
- return [3 /*break*/, 1];
653
- case 9: return [2 /*return*/];
656
+ return [3 /*break*/, 10];
654
657
  case 10:
655
- address = params.address, providerName = params.providerName, userId = params.userId, rdns = params.rdns;
658
+ _i++;
659
+ return [3 /*break*/, 3];
660
+ case 11: return [2 /*return*/];
661
+ case 12:
662
+ address = params.address, providerName = params.providerName, userId = params.userId, rdns = params.rdns, setActive = params.setActive;
656
663
  // Runtime validation: address is required
657
664
  if (!address) {
658
665
  (_b = logger_1.logger.warn) === null || _b === void 0 ? void 0 : _b.call(logger_1.logger, "identify() called without address - address is required");
@@ -661,20 +668,26 @@ var FormoAnalytics = /** @class */ (function () {
661
668
  // Explicit identify
662
669
  logger_1.logger.info("Identify", address, userId, providerName, rdns);
663
670
  validAddress = (0, address_1.validateAddress)(address);
664
- if (validAddress) {
665
- this.currentAddress = validAddress;
666
- this.persistActiveWallet();
667
- }
668
- else {
671
+ if (!validAddress) {
669
672
  (_c = logger_1.logger.warn) === null || _c === void 0 ? void 0 : _c.call(logger_1.logger, "Invalid address provided to identify:", address);
670
673
  return [2 /*return*/];
671
674
  }
672
- if (userId) {
673
- this.currentUserId = userId;
674
- domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
675
- (0, storage_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
675
+ // Promote this wallet to the SDK's active identity - the (currentAddress,
676
+ // currentUserId) pair later events are attributed to - unless the caller
677
+ // opts out with setActive:false. A non-active identify still emits its
678
+ // event and marks dedup below (for clustering), it just doesn't repoint
679
+ // attribution. Gating address and userId together prevents leaving the
680
+ // active address paired with a different wallet's user id.
681
+ if (setActive !== false) {
682
+ this.currentAddress = validAddress;
683
+ this.persistActiveWallet();
684
+ if (userId) {
685
+ this.currentUserId = userId;
686
+ domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
687
+ (0, storage_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
688
+ }
676
689
  }
677
- isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "");
690
+ isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "", userId, properties);
678
691
  logger_1.logger.debug("Identify: Checking deduplication", {
679
692
  validAddress: validAddress,
680
693
  rdns: rdns,
@@ -687,25 +700,77 @@ var FormoAnalytics = /** @class */ (function () {
687
700
  return [2 /*return*/];
688
701
  }
689
702
  // Mark as identified before emitting the event
690
- this.session.markWalletIdentified(validAddress, rdns || "");
703
+ this.session.markWalletIdentified(validAddress, rdns || "", userId, properties);
691
704
  return [4 /*yield*/, this.trackEvent(constants_1.EventType.IDENTIFY, {
692
705
  address: validAddress,
693
706
  providerName: providerName,
694
707
  userId: userId,
695
708
  rdns: rdns,
696
709
  }, properties, context, callback)];
697
- case 11:
710
+ case 13:
698
711
  _d.sent();
699
- return [3 /*break*/, 13];
700
- case 12:
712
+ return [3 /*break*/, 15];
713
+ case 14:
701
714
  e_1 = _d.sent();
702
715
  logger_1.logger.log("identify error", e_1);
703
- return [3 /*break*/, 13];
704
- case 13: return [2 /*return*/];
716
+ return [3 /*break*/, 15];
717
+ case 15: return [2 /*return*/];
705
718
  }
706
719
  });
707
720
  });
708
721
  };
722
+ /**
723
+ * Reconcile currentChainId with a newly-activated Privy wallet's chain
724
+ * namespace. identify() sets currentAddress but never touches the chain id
725
+ * (that comes from connect()/chain()/wagmi), so activating e.g. a Solana
726
+ * wallet while an EVM chain id is current would leave the address paired with
727
+ * a mismatched chain in events, excludeChains, and the active-wallet cookie.
728
+ *
729
+ * We can't infer the wallet's specific chain id from Privy's chainType, so on
730
+ * a namespace mismatch we clear the chain id rather than assert a wrong one; a
731
+ * real wallet connect will set the correct chain. Same-namespace activations
732
+ * (and wallets whose namespace can't be determined) leave the chain id alone.
733
+ *
734
+ * Privy doesn't always supply `chainType`: a `smart_wallet` entry is
735
+ * `{ type, address, smartWalletType }`, and `cross_app` wallets are bare
736
+ * `{ address }`. A `0x`-prefixed 20-byte address is unambiguously EVM though,
737
+ * so fall back to the address shape - otherwise activating an EVM smart
738
+ * wallet while a Solana chain id is current would leave the address paired
739
+ * with the wrong chain, and an `excludeChains` gate could drop the identify
740
+ * after it was already dedup-marked.
741
+ *
742
+ * @internal Not part of the public IFormoAnalytics contract - invoked by
743
+ * `identifyPrivyUser` (via a structural cast) before it emits, so both the
744
+ * `identify(user,{privy:true})` and direct `identifyPrivyUser()` paths
745
+ * reconcile the chain.
746
+ */
747
+ FormoAnalytics.prototype.syncPrivyActiveChain = function (chainType, address) {
748
+ if (this.currentChainId === undefined || this.currentChainId === null)
749
+ return;
750
+ var walletIsSolana;
751
+ var namespace = chainType === null || chainType === void 0 ? void 0 : chainType.toLowerCase();
752
+ // Only recognized namespaces decide. An unknown or future chainType
753
+ // ("bitcoin", "cosmos", …) must stay undecided rather than defaulting to
754
+ // EVM, which would wrongly clear a valid Solana chain id.
755
+ if (namespace === "solana") {
756
+ walletIsSolana = true;
757
+ }
758
+ else if (namespace === "ethereum") {
759
+ walletIsSolana = false;
760
+ }
761
+ else if (!namespace && address && /^0x[0-9a-f]{40}$/i.test(address)) {
762
+ // Only the EVM shape is inferable: a non-0x address could be Solana,
763
+ // Bitcoin, Cosmos, … so absence of 0x proves nothing.
764
+ walletIsSolana = false;
765
+ }
766
+ if (walletIsSolana === undefined)
767
+ return;
768
+ var currentIsSolana = (0, solana_1.isSolanaChainId)(this.currentChainId);
769
+ if (walletIsSolana !== currentIsSolana) {
770
+ this.currentChainId = undefined;
771
+ this.persistActiveWallet();
772
+ }
773
+ };
709
774
  /**
710
775
  * Emits a detect wallet event with current wallet provider info.
711
776
  * @param {string} params.providerName
@@ -722,7 +787,7 @@ var FormoAnalytics = /** @class */ (function () {
722
787
  switch (_c.label) {
723
788
  case 0:
724
789
  // detect() marks wallet detection (a cookie write) before
725
- // trackEvent's consent check gate it for a suppressed visitor or
790
+ // trackEvent's consent check - gate it for a suppressed visitor or
726
791
  // excluded environment (opt-out / timezone / host / path).
727
792
  if (this.isTrackingSuppressed()) {
728
793
  logger_1.logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
@@ -1091,7 +1156,7 @@ var FormoAnalytics = /** @class */ (function () {
1091
1156
  wasDisconnected = !this._evmAddress;
1092
1157
  // Update state regardless of whether connect *event* tracking is enabled,
1093
1158
  // so disconnect events keep valid address/chainId values. (excludeChains is
1094
- // NOT suppression it still updates state so currentChainId can gate
1159
+ // NOT suppression - it still updates state so currentChainId can gate
1095
1160
  // events.)
1096
1161
  if (this.isTrackingSuppressed()) {
1097
1162
  this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
@@ -1272,7 +1337,7 @@ var FormoAnalytics = /** @class */ (function () {
1272
1337
  }
1273
1338
  isActiveProvider = this._provider === provider;
1274
1339
  // Update state from active provider so disconnect events keep valid
1275
- // address/chainId values except while suppressed, where we must not
1340
+ // address/chainId values - except while suppressed, where we must not
1276
1341
  // LEARN identity (only drop a stale EVM wallet on a switch).
1277
1342
  if (isActiveProvider) {
1278
1343
  if (this.isTrackingSuppressed()) {
@@ -1543,7 +1608,7 @@ var FormoAnalytics = /** @class */ (function () {
1543
1608
  var _this = this;
1544
1609
  // Install a single, instance-agnostic wrapper around history.pushState /
1545
1610
  // replaceState so concurrent SDK instances (React Strict Mode, HMR) don't
1546
- // each stack their own wrapper which would dispatch N synthetic events
1611
+ // each stack their own wrapper - which would dispatch N synthetic events
1547
1612
  // per navigation and produce O(N^2) onLocationChange calls. The wrapper
1548
1613
  // dispatches once; per-instance bookkeeping is done by per-instance
1549
1614
  // listeners that each register/unregister themselves.
@@ -1656,22 +1721,50 @@ var FormoAnalytics = /** @class */ (function () {
1656
1721
  * Visitor-level tracking suppression.
1657
1722
  *
1658
1723
  * Returns true when the SDK must not persist any identity/session/chain
1659
- * state or send any events for this visitor i.e. an explicit opt-out or a
1724
+ * state or send any events for this visitor - i.e. an explicit opt-out or a
1660
1725
  * jurisdiction/timezone exclusion. Public entry points that write state
1661
1726
  * before reaching the `shouldTrack()` event gate (identify/connect/detect)
1662
1727
  * check this first so suppressed visitors leave no cookies or session state.
1663
1728
  * @returns {boolean} True if all tracking and persistence must be suppressed
1729
+ * @internal Also read by `identifyPrivyUser` (via a structural cast) so the
1730
+ * Privy sync skips chain reconciliation and emission for suppressed visitors.
1664
1731
  */
1665
1732
  FormoAnalytics.prototype.isTrackingSuppressed = function () {
1666
1733
  return this.hasOptedOutTracking() || this.isCurrentEnvironmentExcluded();
1667
1734
  };
1668
1735
  /**
1669
- * Whether the current environment is excluded from tracking — the visitor's
1736
+ * Whether the current chain id is in `tracking.excludeChains`.
1737
+ *
1738
+ * Split out from `shouldTrack()` so `identify()` can check it *before*
1739
+ * mutating identity state. `trackEvent()` drops an excluded event silently
1740
+ * and returns void, but `identify()` marks the wallet as identified first, so
1741
+ * without this guard an identify on an excluded chain is dedup-marked and
1742
+ * then discarded, and the wallet never re-emits for the rest of the session
1743
+ * even after switching to an allowed chain. On the Privy path that loses the
1744
+ * user's whole cluster at once rather than a single wallet.
1745
+ *
1746
+ * Mirrors the chain rule in `shouldTrack()`: only applies when `tracking` is
1747
+ * an options object with `excludeChains` set, and only once a chain id is
1748
+ * known.
1749
+ */
1750
+ FormoAnalytics.prototype.isCurrentChainExcluded = function () {
1751
+ if (this.options.tracking === null ||
1752
+ typeof this.options.tracking !== "object" ||
1753
+ Array.isArray(this.options.tracking)) {
1754
+ return false;
1755
+ }
1756
+ var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1757
+ return (excludeChains.length > 0 &&
1758
+ !!this.currentChainId &&
1759
+ excludeChains.includes(this.currentChainId));
1760
+ };
1761
+ /**
1762
+ * Whether the current environment is excluded from tracking - the visitor's
1670
1763
  * timezone, the current hostname, or the current pathname matches a
1671
1764
  * configured exclusion.
1672
1765
  *
1673
1766
  * Timezone is visitor/session-level (stable for the session); host/path are
1674
- * current-page-level and transient if a SPA navigates to an allowed path,
1767
+ * current-page-level and transient - if a SPA navigates to an allowed path,
1675
1768
  * tracking resumes for future actions. Used as the "do not write identity or
1676
1769
  * send events" gate at every entry point that would persist state before the
1677
1770
  * `shouldTrack()` event gate.
@@ -1684,7 +1777,7 @@ var FormoAnalytics = /** @class */ (function () {
1684
1777
  };
1685
1778
  /**
1686
1779
  * Whether the current hostname matches a configured `tracking.excludeHosts`
1687
- * entry (exact match). Current-page-level see isCurrentEnvironmentExcluded.
1780
+ * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
1688
1781
  * @returns {boolean} True if the current hostname is excluded
1689
1782
  */
1690
1783
  FormoAnalytics.prototype.isHostExcluded = function () {
@@ -1702,7 +1795,7 @@ var FormoAnalytics = /** @class */ (function () {
1702
1795
  };
1703
1796
  /**
1704
1797
  * Whether the current pathname matches a configured `tracking.excludePaths`
1705
- * entry (exact match). Current-page-level see isCurrentEnvironmentExcluded.
1798
+ * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
1706
1799
  * @returns {boolean} True if the current pathname is excluded
1707
1800
  */
1708
1801
  FormoAnalytics.prototype.isPathExcluded = function () {
@@ -1719,8 +1812,8 @@ var FormoAnalytics = /** @class */ (function () {
1719
1812
  return excludePaths.includes(window.location.pathname);
1720
1813
  };
1721
1814
  /**
1722
- * Whether the current call is in a visitor-level suppression state opt-out
1723
- * or excluded timezone for which any persisted identity cookie should be
1815
+ * Whether the current call is in a visitor-level suppression state - opt-out
1816
+ * or excluded timezone - for which any persisted identity cookie should be
1724
1817
  * actively purged (not merely skipped). Host/path exclusions are
1725
1818
  * deliberately excluded here: they are transient current-page states, so a
1726
1819
  * cookie legitimately written on an allowed page must survive a visit to an
@@ -1733,7 +1826,7 @@ var FormoAnalytics = /** @class */ (function () {
1733
1826
  /**
1734
1827
  * Whether the visitor's browser-resolved timezone matches a configured
1735
1828
  * `tracking.excludeTimezones` entry (case-insensitive). Client-side and
1736
- * best-effort see TrackingOptions.excludeTimezones.
1829
+ * best-effort - see TrackingOptions.excludeTimezones.
1737
1830
  * @returns {boolean} True if the current timezone is excluded
1738
1831
  */
1739
1832
  FormoAnalytics.prototype.isTimezoneExcluded = function () {
@@ -1772,7 +1865,7 @@ var FormoAnalytics = /** @class */ (function () {
1772
1865
  typeof this.options.tracking === "object" &&
1773
1866
  !Array.isArray(this.options.tracking)) {
1774
1867
  var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1775
- // Environment exclusions (timezone / host / path) no identify / connect
1868
+ // Environment exclusions (timezone / host / path) - no identify / connect
1776
1869
  // / track events while excluded. Host/path are exact-match.
1777
1870
  if (this.isCurrentEnvironmentExcluded()) {
1778
1871
  return false;
@@ -2385,7 +2478,7 @@ var FormoAnalytics = /** @class */ (function () {
2385
2478
  * WITHOUT emitting an event.
2386
2479
  *
2387
2480
  * Integrations (e.g. the wagmi handler) must call this on every
2388
- * connect / chain-change / disconnect even when the corresponding
2481
+ * connect / chain-change / disconnect - even when the corresponding
2389
2482
  * autocapture event is disabled. Otherwise `currentChainId` stays
2390
2483
  * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2391
2484
  * check (which keys off `currentChainId`, not the event payload) can
@@ -2399,7 +2492,7 @@ var FormoAnalytics = /** @class */ (function () {
2399
2492
  var chainId = params.chainId, address = params.address;
2400
2493
  if (this.isTrackingSuppressed()) {
2401
2494
  // While suppressed (opt-out / timezone / excluded host or path) we must
2402
- // never LEARN a new wallet but we must still CLEAR stale identity.
2495
+ // never LEARN a new wallet - but we must still CLEAR stale identity.
2403
2496
  // Otherwise a disconnect or wallet switch observed on a suppressed route
2404
2497
  // would leave the previously-learned address in memory and in the
2405
2498
  // active-wallet cookie, attaching it to later allowed-page events.
@@ -2481,14 +2574,14 @@ var FormoAnalytics = /** @class */ (function () {
2481
2574
  /**
2482
2575
  * Persist (or clear) the current wallet snapshot in a cookie so that the
2483
2576
  * SDK can repopulate `currentAddress`/`currentChainId` at init on the next
2484
- * page load closing the gap between page-show and wagmi/EIP-1193
2577
+ * page load - closing the gap between page-show and wagmi/EIP-1193
2485
2578
  * reconnection during which track()/page() events would otherwise ship
2486
2579
  * with an empty address.
2487
2580
  */
2488
2581
  FormoAnalytics.prototype.persistActiveWallet = function () {
2489
2582
  try {
2490
2583
  // Visitor-level suppression (opt-out or excluded timezone): purge any
2491
- // prior snapshot these are stable for the session, so deletion is safe.
2584
+ // prior snapshot - these are stable for the session, so deletion is safe.
2492
2585
  if (this.isPersistedIdentityPurgeRequired()) {
2493
2586
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2494
2587
  return;
@@ -1,8 +1,8 @@
1
1
  export * from "./FormoAnalytics";
2
2
  export * from "./types";
3
3
  export { formofy } from "./initialization";
4
- export { parsePrivyProperties } from "./privy";
5
- export type { PrivyUser, PrivyLinkedAccount, PrivyAccountType, PrivyProfileProperties, PrivyWalletInfo, } from "./privy";
4
+ export { parsePrivyProperties, identifyPrivyUser } from "./privy";
5
+ export type { IdentifyPrivyUserOptions, PrivyUser, PrivyLinkedAccount, PrivyAccountType, PrivyProfileProperties, PrivyWalletInfo, } from "./privy";
6
6
  export { SolanaManager } from "./solana";
7
7
  export { SOLANA_CHAIN_IDS, DEFAULT_SOLANA_CHAIN_ID, isSolanaChainId, } from "./solana";
8
8
  export type { SolanaOptions, SolanaCluster } from "./solana";
@@ -20,13 +20,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
20
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.isSolanaChainId = exports.DEFAULT_SOLANA_CHAIN_ID = exports.SOLANA_CHAIN_IDS = exports.SolanaManager = exports.parsePrivyProperties = exports.formofy = void 0;
23
+ exports.isSolanaChainId = exports.DEFAULT_SOLANA_CHAIN_ID = exports.SOLANA_CHAIN_IDS = exports.SolanaManager = exports.identifyPrivyUser = exports.parsePrivyProperties = exports.formofy = void 0;
24
24
  __exportStar(require("./FormoAnalytics"), exports);
25
25
  __exportStar(require("./types"), exports);
26
26
  var initialization_1 = require("./initialization");
27
27
  Object.defineProperty(exports, "formofy", { enumerable: true, get: function () { return initialization_1.formofy; } });
28
28
  var privy_1 = require("./privy");
29
29
  Object.defineProperty(exports, "parsePrivyProperties", { enumerable: true, get: function () { return privy_1.parsePrivyProperties; } });
30
+ Object.defineProperty(exports, "identifyPrivyUser", { enumerable: true, get: function () { return privy_1.identifyPrivyUser; } });
30
31
  var solana_1 = require("./solana");
31
32
  Object.defineProperty(exports, "SolanaManager", { enumerable: true, get: function () { return solana_1.SolanaManager; } });
32
33
  var solana_2 = require("./solana");
@@ -18,15 +18,21 @@ declare class EventFactory implements IEventFactory {
18
18
  private getLanguage;
19
19
  private getLibraryVersion;
20
20
  private isExcludedQueryParam;
21
+ /**
22
+ * Normalize URL paths for analytics aggregation by stripping trailing slashes
23
+ * from non-root paths. Query strings and hash fragments are preserved by
24
+ * mutating only the URL pathname.
25
+ */
26
+ private normalizeUrlPath;
21
27
  /**
22
28
  * Strip excluded (sensitive) query parameters from a URL in place. Only the
23
29
  * query string is touched; the path and hash/fragment are left as-is.
24
30
  */
25
31
  private redactQueryParams;
26
32
  /**
27
- * Return the given absolute URL with excluded query parameters removed. The
28
- * input is returned unchanged when it is empty or cannot be parsed (e.g. an
29
- * empty referrer).
33
+ * Return the given absolute URL with excluded query parameters removed and
34
+ * trailing slashes stripped from non-root paths. The input is returned
35
+ * unchanged when it is empty or cannot be parsed (e.g. an empty referrer).
30
36
  */
31
37
  private redactUrl;
32
38
  private extractUTMParameters;
@@ -35,7 +41,7 @@ declare class EventFactory implements IEventFactory {
35
41
  /**
36
42
  * Returns the document referrer with same-host referrers filtered out.
37
43
  * Internal navigation populates `document.referrer` with the previous page
38
- * on the same site, which is not an attribution signal treating it as
44
+ * on the same site, which is not an attribution signal - treating it as
39
45
  * "external" would otherwise let an internal URL become the session's
40
46
  * first-touch referrer after a direct landing.
41
47
  */