@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
@@ -72,6 +72,7 @@ import { parseChainId } from "./utils/chain";
72
72
  import { WagmiEventHandler } from "./wagmi";
73
73
  import { isSolanaChainId } from "./solana";
74
74
  import { SolanaManager } from "./solana/SolanaManager";
75
+ import { identifyPrivyUser } from "./privy";
75
76
  /**
76
77
  * Constants for provider switching reasons
77
78
  */
@@ -87,7 +88,7 @@ var FormoAnalytics = /** @class */ (function () {
87
88
  var _a, _b, _c;
88
89
  this.writeKey = writeKey;
89
90
  this.options = options;
90
- // Per-chain namespace state isolates EVM and Solana connection state
91
+ // Per-chain namespace state - isolates EVM and Solana connection state
91
92
  this._chainState = {
92
93
  evm: {},
93
94
  solana: {},
@@ -203,7 +204,7 @@ var FormoAnalytics = /** @class */ (function () {
203
204
  this.trackPageHits();
204
205
  }
205
206
  Object.defineProperty(FormoAnalytics.prototype, "_provider", {
206
- // EVM state accessors EVM listener paths must use these instead of
207
+ // EVM state accessors - EVM listener paths must use these instead of
207
208
  // currentAddress/currentChainId to avoid cross-namespace reads.
208
209
  get: function () {
209
210
  return this._chainState.evm.provider;
@@ -393,7 +394,7 @@ var FormoAnalytics = /** @class */ (function () {
393
394
  return [2 /*return*/];
394
395
  }
395
396
  // connect() persists wallet/chain state (active-wallet cookie,
396
- // currentAddress/currentChainId) before trackEvent's consent check
397
+ // currentAddress/currentChainId) before trackEvent's consent check -
397
398
  // gate the whole method so a suppressed visitor or excluded environment
398
399
  // (opt-out / timezone / host / path) leaves no session state.
399
400
  if (this.isTrackingSuppressed()) {
@@ -550,79 +551,85 @@ var FormoAnalytics = /** @class */ (function () {
550
551
  });
551
552
  });
552
553
  };
553
- /**
554
- * Emits an identify event with current wallet address and provider info.
555
- *
556
- * @param {string} params.address - Wallet address
557
- * @param {string} params.userId - External user ID
558
- * @param {string} params.rdns - Provider reverse domain name
559
- * @param {string} params.providerName - Provider display name
560
- * @param {IFormoEventProperties} properties - Additional properties to include with the identify event
561
- * @param {IFormoEventContext} context
562
- * @param {(...args: unknown[]) => void} callback
563
- * @returns {Promise<void>}
564
- *
565
- * @example
566
- * ```ts
567
- * // Basic identify
568
- * formo.identify({ address: '0x...', userId: 'user123' });
569
- *
570
- * // With Privy user
571
- * import { parsePrivyProperties } from '@formo/analytics';
572
- * const { user } = usePrivy();
573
- * if (user) {
574
- * const { properties, wallets } = parsePrivyProperties(user);
575
- * for (const wallet of wallets) {
576
- * formo.identify({ address: wallet.address, userId: user.id }, properties);
577
- * }
578
- * }
579
- * ```
580
- */
581
- FormoAnalytics.prototype.identify = function (params, properties, context, callback) {
554
+ FormoAnalytics.prototype.identify = function (paramsOrUser, propertiesOrOptions, context, callback) {
582
555
  return __awaiter(this, void 0, void 0, function () {
583
- var _i, _a, providerDetail, provider, address_1, validAddress_1, err_1, address, providerName, userId, rdns, validAddress, domain, isAlreadyIdentified, e_1;
556
+ var maybeUser, opts, params, properties, _i, _a, providerDetail, provider, address_1, validAddress_1, err_1, address, providerName, userId, rdns, setActive, validAddress, domain, isAlreadyIdentified, e_1;
584
557
  var _b, _c;
585
558
  return __generator(this, function (_d) {
586
559
  switch (_d.label) {
587
560
  case 0:
588
- _d.trys.push([0, 12, , 13]);
561
+ _d.trys.push([0, 14, , 15]);
562
+ maybeUser = paramsOrUser;
563
+ if (!(maybeUser &&
564
+ typeof maybeUser.id === "string" &&
565
+ maybeUser.address === undefined)) return [3 /*break*/, 2];
566
+ opts = (propertiesOrOptions !== null && propertiesOrOptions !== void 0 ? propertiesOrOptions : {});
567
+ // identifyPrivyUser records every linked wallet for clustering WITHOUT
568
+ // touching active state (internal setActive:false), promotes only the
569
+ // resolved active wallet, and reconciles the chain id with that wallet's
570
+ // namespace before emitting. It reads this.currentAddress itself to
571
+ // preserve an already-connected wallet, so this dispatch is a thin
572
+ // pass-through and both entry points behave identically.
573
+ return [4 /*yield*/, identifyPrivyUser(this, maybeUser, {
574
+ activeAddress: opts.activeAddress,
575
+ properties: opts.properties,
576
+ })];
577
+ case 1:
578
+ // identifyPrivyUser records every linked wallet for clustering WITHOUT
579
+ // touching active state (internal setActive:false), promotes only the
580
+ // resolved active wallet, and reconciles the chain id with that wallet's
581
+ // namespace before emitting. It reads this.currentAddress itself to
582
+ // preserve an already-connected wallet, so this dispatch is a thin
583
+ // pass-through and both entry points behave identically.
584
+ _d.sent();
585
+ return [2 /*return*/];
586
+ case 2:
587
+ params = paramsOrUser;
588
+ properties = propertiesOrOptions;
589
589
  // identify() writes the user-id cookie and marks wallet
590
- // identification before trackEvent's consent check gate the whole
590
+ // identification before trackEvent's consent check - gate the whole
591
591
  // method so a suppressed visitor or excluded environment (opt-out /
592
592
  // timezone / host / path) gets no identity persistence.
593
- if (this.isTrackingSuppressed()) {
594
- logger.info("identify() skipped: tracking is suppressed for this visitor or environment");
593
+ //
594
+ // The chain check belongs here and NOT in isTrackingSuppressed(), which
595
+ // identifyPrivyUser calls before it reconciles the chain. Folding it in
596
+ // there would block a Privy sync that reconciliation was about to make
597
+ // valid (activating a Solana wallet while an excluded EVM chain id is
598
+ // still current). By the time the Privy path reaches this guard it has
599
+ // already reconciled, so each inner identify is judged on the right chain.
600
+ if (this.isTrackingSuppressed() || this.isCurrentChainExcluded()) {
601
+ logger.info("identify() skipped: tracking is suppressed for this visitor, environment, or chain");
595
602
  return [2 /*return*/];
596
603
  }
597
- if (!!params) return [3 /*break*/, 10];
604
+ if (!!params) return [3 /*break*/, 12];
598
605
  // If no params provided, auto-identify
599
606
  logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
600
607
  _i = 0, _a = this._providers;
601
- _d.label = 1;
602
- case 1:
603
- if (!(_i < _a.length)) return [3 /*break*/, 9];
608
+ _d.label = 3;
609
+ case 3:
610
+ if (!(_i < _a.length)) return [3 /*break*/, 11];
604
611
  providerDetail = _a[_i];
605
612
  provider = providerDetail.provider;
606
613
  if (!provider)
607
- return [3 /*break*/, 8];
608
- _d.label = 2;
609
- case 2:
610
- _d.trys.push([2, 7, , 8]);
614
+ return [3 /*break*/, 10];
615
+ _d.label = 4;
616
+ case 4:
617
+ _d.trys.push([4, 9, , 10]);
611
618
  return [4 /*yield*/, this.getAddress(provider)];
612
- case 3:
619
+ case 5:
613
620
  address_1 = _d.sent();
614
- if (!address_1) return [3 /*break*/, 6];
621
+ if (!address_1) return [3 /*break*/, 8];
615
622
  validAddress_1 = validateAndChecksumAddress(address_1);
616
623
  logger.info("Auto-identify: Checking deduplication", {
617
624
  validAddress: validAddress_1,
618
625
  rdns: providerDetail.info.rdns,
619
626
  providerName: providerDetail.info.name,
620
627
  isAlreadyIdentified: validAddress_1
621
- ? this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns)
628
+ ? this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns, undefined, properties)
622
629
  : false,
623
630
  });
624
631
  if (!(validAddress_1 &&
625
- !this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns))) return [3 /*break*/, 5];
632
+ !this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns, undefined, properties))) return [3 /*break*/, 7];
626
633
  logger.info("Auto-identifying", validAddress_1, providerDetail.info.name, providerDetail.info.rdns);
627
634
  // NOTE: do not set this.currentAddress without explicit connect or identify
628
635
  return [4 /*yield*/, this.identify({
@@ -630,26 +637,26 @@ var FormoAnalytics = /** @class */ (function () {
630
637
  providerName: providerDetail.info.name,
631
638
  rdns: providerDetail.info.rdns,
632
639
  }, properties, context, callback)];
633
- case 4:
640
+ case 6:
634
641
  // NOTE: do not set this.currentAddress without explicit connect or identify
635
642
  _d.sent();
636
- return [3 /*break*/, 6];
637
- case 5:
643
+ return [3 /*break*/, 8];
644
+ case 7:
638
645
  if (validAddress_1) {
639
646
  logger.info("Auto-identify: Skipping already identified wallet", validAddress_1, providerDetail.info.name, providerDetail.info.rdns);
640
647
  }
641
- _d.label = 6;
642
- case 6: return [3 /*break*/, 8];
643
- case 7:
648
+ _d.label = 8;
649
+ case 8: return [3 /*break*/, 10];
650
+ case 9:
644
651
  err_1 = _d.sent();
645
652
  logger.error("Failed to identify provider ".concat(providerDetail.info.name, ":"), err_1);
646
- return [3 /*break*/, 8];
647
- case 8:
648
- _i++;
649
- return [3 /*break*/, 1];
650
- case 9: return [2 /*return*/];
653
+ return [3 /*break*/, 10];
651
654
  case 10:
652
- address = params.address, providerName = params.providerName, userId = params.userId, rdns = params.rdns;
655
+ _i++;
656
+ return [3 /*break*/, 3];
657
+ case 11: return [2 /*return*/];
658
+ case 12:
659
+ address = params.address, providerName = params.providerName, userId = params.userId, rdns = params.rdns, setActive = params.setActive;
653
660
  // Runtime validation: address is required
654
661
  if (!address) {
655
662
  (_b = logger.warn) === null || _b === void 0 ? void 0 : _b.call(logger, "identify() called without address - address is required");
@@ -658,20 +665,26 @@ var FormoAnalytics = /** @class */ (function () {
658
665
  // Explicit identify
659
666
  logger.info("Identify", address, userId, providerName, rdns);
660
667
  validAddress = validateAddress(address);
661
- if (validAddress) {
662
- this.currentAddress = validAddress;
663
- this.persistActiveWallet();
664
- }
665
- else {
668
+ if (!validAddress) {
666
669
  (_c = logger.warn) === null || _c === void 0 ? void 0 : _c.call(logger, "Invalid address provided to identify:", address);
667
670
  return [2 /*return*/];
668
671
  }
669
- if (userId) {
670
- this.currentUserId = userId;
671
- domain = getIdentityCookieDomain(this.crossSubdomainCookies);
672
- cookie().set(SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
672
+ // Promote this wallet to the SDK's active identity - the (currentAddress,
673
+ // currentUserId) pair later events are attributed to - unless the caller
674
+ // opts out with setActive:false. A non-active identify still emits its
675
+ // event and marks dedup below (for clustering), it just doesn't repoint
676
+ // attribution. Gating address and userId together prevents leaving the
677
+ // active address paired with a different wallet's user id.
678
+ if (setActive !== false) {
679
+ this.currentAddress = validAddress;
680
+ this.persistActiveWallet();
681
+ if (userId) {
682
+ this.currentUserId = userId;
683
+ domain = getIdentityCookieDomain(this.crossSubdomainCookies);
684
+ cookie().set(SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
685
+ }
673
686
  }
674
- isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "");
687
+ isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "", userId, properties);
675
688
  logger.debug("Identify: Checking deduplication", {
676
689
  validAddress: validAddress,
677
690
  rdns: rdns,
@@ -684,25 +697,77 @@ var FormoAnalytics = /** @class */ (function () {
684
697
  return [2 /*return*/];
685
698
  }
686
699
  // Mark as identified before emitting the event
687
- this.session.markWalletIdentified(validAddress, rdns || "");
700
+ this.session.markWalletIdentified(validAddress, rdns || "", userId, properties);
688
701
  return [4 /*yield*/, this.trackEvent(EventType.IDENTIFY, {
689
702
  address: validAddress,
690
703
  providerName: providerName,
691
704
  userId: userId,
692
705
  rdns: rdns,
693
706
  }, properties, context, callback)];
694
- case 11:
707
+ case 13:
695
708
  _d.sent();
696
- return [3 /*break*/, 13];
697
- case 12:
709
+ return [3 /*break*/, 15];
710
+ case 14:
698
711
  e_1 = _d.sent();
699
712
  logger.log("identify error", e_1);
700
- return [3 /*break*/, 13];
701
- case 13: return [2 /*return*/];
713
+ return [3 /*break*/, 15];
714
+ case 15: return [2 /*return*/];
702
715
  }
703
716
  });
704
717
  });
705
718
  };
719
+ /**
720
+ * Reconcile currentChainId with a newly-activated Privy wallet's chain
721
+ * namespace. identify() sets currentAddress but never touches the chain id
722
+ * (that comes from connect()/chain()/wagmi), so activating e.g. a Solana
723
+ * wallet while an EVM chain id is current would leave the address paired with
724
+ * a mismatched chain in events, excludeChains, and the active-wallet cookie.
725
+ *
726
+ * We can't infer the wallet's specific chain id from Privy's chainType, so on
727
+ * a namespace mismatch we clear the chain id rather than assert a wrong one; a
728
+ * real wallet connect will set the correct chain. Same-namespace activations
729
+ * (and wallets whose namespace can't be determined) leave the chain id alone.
730
+ *
731
+ * Privy doesn't always supply `chainType`: a `smart_wallet` entry is
732
+ * `{ type, address, smartWalletType }`, and `cross_app` wallets are bare
733
+ * `{ address }`. A `0x`-prefixed 20-byte address is unambiguously EVM though,
734
+ * so fall back to the address shape - otherwise activating an EVM smart
735
+ * wallet while a Solana chain id is current would leave the address paired
736
+ * with the wrong chain, and an `excludeChains` gate could drop the identify
737
+ * after it was already dedup-marked.
738
+ *
739
+ * @internal Not part of the public IFormoAnalytics contract - invoked by
740
+ * `identifyPrivyUser` (via a structural cast) before it emits, so both the
741
+ * `identify(user,{privy:true})` and direct `identifyPrivyUser()` paths
742
+ * reconcile the chain.
743
+ */
744
+ FormoAnalytics.prototype.syncPrivyActiveChain = function (chainType, address) {
745
+ if (this.currentChainId === undefined || this.currentChainId === null)
746
+ return;
747
+ var walletIsSolana;
748
+ var namespace = chainType === null || chainType === void 0 ? void 0 : chainType.toLowerCase();
749
+ // Only recognized namespaces decide. An unknown or future chainType
750
+ // ("bitcoin", "cosmos", …) must stay undecided rather than defaulting to
751
+ // EVM, which would wrongly clear a valid Solana chain id.
752
+ if (namespace === "solana") {
753
+ walletIsSolana = true;
754
+ }
755
+ else if (namespace === "ethereum") {
756
+ walletIsSolana = false;
757
+ }
758
+ else if (!namespace && address && /^0x[0-9a-f]{40}$/i.test(address)) {
759
+ // Only the EVM shape is inferable: a non-0x address could be Solana,
760
+ // Bitcoin, Cosmos, … so absence of 0x proves nothing.
761
+ walletIsSolana = false;
762
+ }
763
+ if (walletIsSolana === undefined)
764
+ return;
765
+ var currentIsSolana = isSolanaChainId(this.currentChainId);
766
+ if (walletIsSolana !== currentIsSolana) {
767
+ this.currentChainId = undefined;
768
+ this.persistActiveWallet();
769
+ }
770
+ };
706
771
  /**
707
772
  * Emits a detect wallet event with current wallet provider info.
708
773
  * @param {string} params.providerName
@@ -719,7 +784,7 @@ var FormoAnalytics = /** @class */ (function () {
719
784
  switch (_c.label) {
720
785
  case 0:
721
786
  // detect() marks wallet detection (a cookie write) before
722
- // trackEvent's consent check gate it for a suppressed visitor or
787
+ // trackEvent's consent check - gate it for a suppressed visitor or
723
788
  // excluded environment (opt-out / timezone / host / path).
724
789
  if (this.isTrackingSuppressed()) {
725
790
  logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
@@ -1088,7 +1153,7 @@ var FormoAnalytics = /** @class */ (function () {
1088
1153
  wasDisconnected = !this._evmAddress;
1089
1154
  // Update state regardless of whether connect *event* tracking is enabled,
1090
1155
  // so disconnect events keep valid address/chainId values. (excludeChains is
1091
- // NOT suppression it still updates state so currentChainId can gate
1156
+ // NOT suppression - it still updates state so currentChainId can gate
1092
1157
  // events.)
1093
1158
  if (this.isTrackingSuppressed()) {
1094
1159
  this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
@@ -1269,7 +1334,7 @@ var FormoAnalytics = /** @class */ (function () {
1269
1334
  }
1270
1335
  isActiveProvider = this._provider === provider;
1271
1336
  // Update state from active provider so disconnect events keep valid
1272
- // address/chainId values except while suppressed, where we must not
1337
+ // address/chainId values - except while suppressed, where we must not
1273
1338
  // LEARN identity (only drop a stale EVM wallet on a switch).
1274
1339
  if (isActiveProvider) {
1275
1340
  if (this.isTrackingSuppressed()) {
@@ -1540,7 +1605,7 @@ var FormoAnalytics = /** @class */ (function () {
1540
1605
  var _this = this;
1541
1606
  // Install a single, instance-agnostic wrapper around history.pushState /
1542
1607
  // replaceState so concurrent SDK instances (React Strict Mode, HMR) don't
1543
- // each stack their own wrapper which would dispatch N synthetic events
1608
+ // each stack their own wrapper - which would dispatch N synthetic events
1544
1609
  // per navigation and produce O(N^2) onLocationChange calls. The wrapper
1545
1610
  // dispatches once; per-instance bookkeeping is done by per-instance
1546
1611
  // listeners that each register/unregister themselves.
@@ -1653,22 +1718,50 @@ var FormoAnalytics = /** @class */ (function () {
1653
1718
  * Visitor-level tracking suppression.
1654
1719
  *
1655
1720
  * Returns true when the SDK must not persist any identity/session/chain
1656
- * state or send any events for this visitor i.e. an explicit opt-out or a
1721
+ * state or send any events for this visitor - i.e. an explicit opt-out or a
1657
1722
  * jurisdiction/timezone exclusion. Public entry points that write state
1658
1723
  * before reaching the `shouldTrack()` event gate (identify/connect/detect)
1659
1724
  * check this first so suppressed visitors leave no cookies or session state.
1660
1725
  * @returns {boolean} True if all tracking and persistence must be suppressed
1726
+ * @internal Also read by `identifyPrivyUser` (via a structural cast) so the
1727
+ * Privy sync skips chain reconciliation and emission for suppressed visitors.
1661
1728
  */
1662
1729
  FormoAnalytics.prototype.isTrackingSuppressed = function () {
1663
1730
  return this.hasOptedOutTracking() || this.isCurrentEnvironmentExcluded();
1664
1731
  };
1665
1732
  /**
1666
- * Whether the current environment is excluded from tracking — the visitor's
1733
+ * Whether the current chain id is in `tracking.excludeChains`.
1734
+ *
1735
+ * Split out from `shouldTrack()` so `identify()` can check it *before*
1736
+ * mutating identity state. `trackEvent()` drops an excluded event silently
1737
+ * and returns void, but `identify()` marks the wallet as identified first, so
1738
+ * without this guard an identify on an excluded chain is dedup-marked and
1739
+ * then discarded, and the wallet never re-emits for the rest of the session
1740
+ * even after switching to an allowed chain. On the Privy path that loses the
1741
+ * user's whole cluster at once rather than a single wallet.
1742
+ *
1743
+ * Mirrors the chain rule in `shouldTrack()`: only applies when `tracking` is
1744
+ * an options object with `excludeChains` set, and only once a chain id is
1745
+ * known.
1746
+ */
1747
+ FormoAnalytics.prototype.isCurrentChainExcluded = function () {
1748
+ if (this.options.tracking === null ||
1749
+ typeof this.options.tracking !== "object" ||
1750
+ Array.isArray(this.options.tracking)) {
1751
+ return false;
1752
+ }
1753
+ var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1754
+ return (excludeChains.length > 0 &&
1755
+ !!this.currentChainId &&
1756
+ excludeChains.includes(this.currentChainId));
1757
+ };
1758
+ /**
1759
+ * Whether the current environment is excluded from tracking - the visitor's
1667
1760
  * timezone, the current hostname, or the current pathname matches a
1668
1761
  * configured exclusion.
1669
1762
  *
1670
1763
  * Timezone is visitor/session-level (stable for the session); host/path are
1671
- * current-page-level and transient if a SPA navigates to an allowed path,
1764
+ * current-page-level and transient - if a SPA navigates to an allowed path,
1672
1765
  * tracking resumes for future actions. Used as the "do not write identity or
1673
1766
  * send events" gate at every entry point that would persist state before the
1674
1767
  * `shouldTrack()` event gate.
@@ -1681,7 +1774,7 @@ var FormoAnalytics = /** @class */ (function () {
1681
1774
  };
1682
1775
  /**
1683
1776
  * Whether the current hostname matches a configured `tracking.excludeHosts`
1684
- * entry (exact match). Current-page-level see isCurrentEnvironmentExcluded.
1777
+ * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
1685
1778
  * @returns {boolean} True if the current hostname is excluded
1686
1779
  */
1687
1780
  FormoAnalytics.prototype.isHostExcluded = function () {
@@ -1699,7 +1792,7 @@ var FormoAnalytics = /** @class */ (function () {
1699
1792
  };
1700
1793
  /**
1701
1794
  * Whether the current pathname matches a configured `tracking.excludePaths`
1702
- * entry (exact match). Current-page-level see isCurrentEnvironmentExcluded.
1795
+ * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
1703
1796
  * @returns {boolean} True if the current pathname is excluded
1704
1797
  */
1705
1798
  FormoAnalytics.prototype.isPathExcluded = function () {
@@ -1716,8 +1809,8 @@ var FormoAnalytics = /** @class */ (function () {
1716
1809
  return excludePaths.includes(window.location.pathname);
1717
1810
  };
1718
1811
  /**
1719
- * Whether the current call is in a visitor-level suppression state opt-out
1720
- * or excluded timezone for which any persisted identity cookie should be
1812
+ * Whether the current call is in a visitor-level suppression state - opt-out
1813
+ * or excluded timezone - for which any persisted identity cookie should be
1721
1814
  * actively purged (not merely skipped). Host/path exclusions are
1722
1815
  * deliberately excluded here: they are transient current-page states, so a
1723
1816
  * cookie legitimately written on an allowed page must survive a visit to an
@@ -1730,7 +1823,7 @@ var FormoAnalytics = /** @class */ (function () {
1730
1823
  /**
1731
1824
  * Whether the visitor's browser-resolved timezone matches a configured
1732
1825
  * `tracking.excludeTimezones` entry (case-insensitive). Client-side and
1733
- * best-effort see TrackingOptions.excludeTimezones.
1826
+ * best-effort - see TrackingOptions.excludeTimezones.
1734
1827
  * @returns {boolean} True if the current timezone is excluded
1735
1828
  */
1736
1829
  FormoAnalytics.prototype.isTimezoneExcluded = function () {
@@ -1769,7 +1862,7 @@ var FormoAnalytics = /** @class */ (function () {
1769
1862
  typeof this.options.tracking === "object" &&
1770
1863
  !Array.isArray(this.options.tracking)) {
1771
1864
  var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1772
- // Environment exclusions (timezone / host / path) no identify / connect
1865
+ // Environment exclusions (timezone / host / path) - no identify / connect
1773
1866
  // / track events while excluded. Host/path are exact-match.
1774
1867
  if (this.isCurrentEnvironmentExcluded()) {
1775
1868
  return false;
@@ -2382,7 +2475,7 @@ var FormoAnalytics = /** @class */ (function () {
2382
2475
  * WITHOUT emitting an event.
2383
2476
  *
2384
2477
  * Integrations (e.g. the wagmi handler) must call this on every
2385
- * connect / chain-change / disconnect even when the corresponding
2478
+ * connect / chain-change / disconnect - even when the corresponding
2386
2479
  * autocapture event is disabled. Otherwise `currentChainId` stays
2387
2480
  * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2388
2481
  * check (which keys off `currentChainId`, not the event payload) can
@@ -2396,7 +2489,7 @@ var FormoAnalytics = /** @class */ (function () {
2396
2489
  var chainId = params.chainId, address = params.address;
2397
2490
  if (this.isTrackingSuppressed()) {
2398
2491
  // While suppressed (opt-out / timezone / excluded host or path) we must
2399
- // never LEARN a new wallet but we must still CLEAR stale identity.
2492
+ // never LEARN a new wallet - but we must still CLEAR stale identity.
2400
2493
  // Otherwise a disconnect or wallet switch observed on a suppressed route
2401
2494
  // would leave the previously-learned address in memory and in the
2402
2495
  // active-wallet cookie, attaching it to later allowed-page events.
@@ -2478,14 +2571,14 @@ var FormoAnalytics = /** @class */ (function () {
2478
2571
  /**
2479
2572
  * Persist (or clear) the current wallet snapshot in a cookie so that the
2480
2573
  * SDK can repopulate `currentAddress`/`currentChainId` at init on the next
2481
- * page load closing the gap between page-show and wagmi/EIP-1193
2574
+ * page load - closing the gap between page-show and wagmi/EIP-1193
2482
2575
  * reconnection during which track()/page() events would otherwise ship
2483
2576
  * with an empty address.
2484
2577
  */
2485
2578
  FormoAnalytics.prototype.persistActiveWallet = function () {
2486
2579
  try {
2487
2580
  // Visitor-level suppression (opt-out or excluded timezone): purge any
2488
- // prior snapshot these are stable for the session, so deletion is safe.
2581
+ // prior snapshot - these are stable for the session, so deletion is safe.
2489
2582
  if (this.isPersistedIdentityPurgeRequired()) {
2490
2583
  cookie().remove(ACTIVE_WALLET_KEY);
2491
2584
  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";
@@ -7,7 +7,7 @@
7
7
  export * from "./FormoAnalytics";
8
8
  export * from "./types";
9
9
  export { formofy } from "./initialization";
10
- export { parsePrivyProperties } from "./privy";
10
+ export { parsePrivyProperties, identifyPrivyUser } from "./privy";
11
11
  export { SolanaManager } from "./solana";
12
12
  export { SOLANA_CHAIN_IDS, DEFAULT_SOLANA_CHAIN_ID, isSolanaChainId, } from "./solana";
13
13
  //# sourceMappingURL=core.js.map
@@ -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
  */