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