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