@formo/analytics 1.32.0 → 1.33.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/dist/cjs/src/FormoAnalytics.d.ts +61 -0
- package/dist/cjs/src/FormoAnalytics.js +223 -36
- package/dist/cjs/src/event/EventFactory.d.ts +9 -3
- package/dist/cjs/src/event/EventFactory.js +19 -14
- package/dist/cjs/src/event/constants.d.ts +10 -1
- package/dist/cjs/src/event/constants.js +20 -8
- package/dist/cjs/src/types/base.d.ts +16 -2
- package/dist/cjs/src/utils/index.d.ts +1 -0
- package/dist/cjs/src/utils/index.js +1 -0
- package/dist/cjs/src/utils/timezone.d.ts +7 -0
- package/dist/cjs/src/utils/timezone.js +19 -0
- package/dist/esm/src/FormoAnalytics.d.ts +61 -0
- package/dist/esm/src/FormoAnalytics.js +223 -36
- package/dist/esm/src/event/EventFactory.d.ts +9 -3
- package/dist/esm/src/event/EventFactory.js +21 -16
- package/dist/esm/src/event/constants.d.ts +10 -1
- package/dist/esm/src/event/constants.js +19 -8
- package/dist/esm/src/types/base.d.ts +16 -2
- package/dist/esm/src/utils/index.d.ts +1 -0
- package/dist/esm/src/utils/index.js +1 -0
- package/dist/esm/src/utils/timezone.d.ts +7 -0
- package/dist/esm/src/utils/timezone.js +16 -0
- package/dist/index.umd.min.js +1 -1
- package/package.json +10 -10
|
@@ -19,4 +19,5 @@ __exportStar(require("./base"), exports);
|
|
|
19
19
|
__exportStar(require("./converter"), exports);
|
|
20
20
|
__exportStar(require("./generate"), exports);
|
|
21
21
|
__exportStar(require("./hash"), exports);
|
|
22
|
+
__exportStar(require("./timezone"), exports);
|
|
22
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the current IANA timezone (e.g. "Europe/London") via the Intl API.
|
|
3
|
+
* Returns "" when the timezone cannot be resolved (e.g. Intl unavailable).
|
|
4
|
+
*/
|
|
5
|
+
declare const getTimezone: () => string;
|
|
6
|
+
export { getTimezone };
|
|
7
|
+
//# sourceMappingURL=timezone.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTimezone = void 0;
|
|
4
|
+
var logger_1 = require("../logger");
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the current IANA timezone (e.g. "Europe/London") via the Intl API.
|
|
7
|
+
* Returns "" when the timezone cannot be resolved (e.g. Intl unavailable).
|
|
8
|
+
*/
|
|
9
|
+
var getTimezone = function () {
|
|
10
|
+
try {
|
|
11
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
logger_1.logger.error("Error resolving timezone:", error);
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
exports.getTimezone = getTimezone;
|
|
19
|
+
//# sourceMappingURL=timezone.js.map
|
|
@@ -282,6 +282,59 @@ export declare class FormoAnalytics implements IFormoAnalytics {
|
|
|
282
282
|
private static installHistoryHooksOnce;
|
|
283
283
|
private trackPageHit;
|
|
284
284
|
private trackEvent;
|
|
285
|
+
/**
|
|
286
|
+
* Visitor-level tracking suppression.
|
|
287
|
+
*
|
|
288
|
+
* Returns true when the SDK must not persist any identity/session/chain
|
|
289
|
+
* state or send any events for this visitor — i.e. an explicit opt-out or a
|
|
290
|
+
* jurisdiction/timezone exclusion. Public entry points that write state
|
|
291
|
+
* before reaching the `shouldTrack()` event gate (identify/connect/detect)
|
|
292
|
+
* check this first so suppressed visitors leave no cookies or session state.
|
|
293
|
+
* @returns {boolean} True if all tracking and persistence must be suppressed
|
|
294
|
+
*/
|
|
295
|
+
private isTrackingSuppressed;
|
|
296
|
+
/**
|
|
297
|
+
* Whether the current environment is excluded from tracking — the visitor's
|
|
298
|
+
* timezone, the current hostname, or the current pathname matches a
|
|
299
|
+
* configured exclusion.
|
|
300
|
+
*
|
|
301
|
+
* Timezone is visitor/session-level (stable for the session); host/path are
|
|
302
|
+
* current-page-level and transient — if a SPA navigates to an allowed path,
|
|
303
|
+
* tracking resumes for future actions. Used as the "do not write identity or
|
|
304
|
+
* send events" gate at every entry point that would persist state before the
|
|
305
|
+
* `shouldTrack()` event gate.
|
|
306
|
+
* @returns {boolean} True if the current environment is excluded
|
|
307
|
+
*/
|
|
308
|
+
private isCurrentEnvironmentExcluded;
|
|
309
|
+
/**
|
|
310
|
+
* Whether the current hostname matches a configured `tracking.excludeHosts`
|
|
311
|
+
* entry (exact match). Current-page-level — see isCurrentEnvironmentExcluded.
|
|
312
|
+
* @returns {boolean} True if the current hostname is excluded
|
|
313
|
+
*/
|
|
314
|
+
private isHostExcluded;
|
|
315
|
+
/**
|
|
316
|
+
* Whether the current pathname matches a configured `tracking.excludePaths`
|
|
317
|
+
* entry (exact match). Current-page-level — see isCurrentEnvironmentExcluded.
|
|
318
|
+
* @returns {boolean} True if the current pathname is excluded
|
|
319
|
+
*/
|
|
320
|
+
private isPathExcluded;
|
|
321
|
+
/**
|
|
322
|
+
* Whether the current call is in a visitor-level suppression state — opt-out
|
|
323
|
+
* or excluded timezone — for which any persisted identity cookie should be
|
|
324
|
+
* actively purged (not merely skipped). Host/path exclusions are
|
|
325
|
+
* deliberately excluded here: they are transient current-page states, so a
|
|
326
|
+
* cookie legitimately written on an allowed page must survive a visit to an
|
|
327
|
+
* excluded route.
|
|
328
|
+
* @returns {boolean} True if persisted identity must be purged
|
|
329
|
+
*/
|
|
330
|
+
private isPersistedIdentityPurgeRequired;
|
|
331
|
+
/**
|
|
332
|
+
* Whether the visitor's browser-resolved timezone matches a configured
|
|
333
|
+
* `tracking.excludeTimezones` entry (case-insensitive). Client-side and
|
|
334
|
+
* best-effort — see TrackingOptions.excludeTimezones.
|
|
335
|
+
* @returns {boolean} True if the current timezone is excluded
|
|
336
|
+
*/
|
|
337
|
+
private isTimezoneExcluded;
|
|
285
338
|
/**
|
|
286
339
|
* Determines if tracking should be enabled based on configuration and consent
|
|
287
340
|
* @returns {boolean} True if tracking should be enabled
|
|
@@ -329,6 +382,14 @@ export declare class FormoAnalytics implements IFormoAnalytics {
|
|
|
329
382
|
* value in the normal way; existing connections are never clobbered.
|
|
330
383
|
*/
|
|
331
384
|
private backfillActiveWallet;
|
|
385
|
+
/**
|
|
386
|
+
* Apply an EVM autocapture connect/switch while tracking is suppressed
|
|
387
|
+
* (opt-out / timezone / excluded host or path): never LEARN the wallet, but
|
|
388
|
+
* if it is a switch away from an already-learned EVM wallet, drop the stale
|
|
389
|
+
* one (which also clears the active-wallet cookie) so it can't attach to a
|
|
390
|
+
* later allowed-page event.
|
|
391
|
+
*/
|
|
392
|
+
private clearStaleEvmWalletOnSwitchWhileSuppressed;
|
|
332
393
|
/**
|
|
333
394
|
* Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
|
|
334
395
|
*/
|
|
@@ -66,6 +66,7 @@ import { detectInjectedProviderInfo, isValidProvider } from "./provider";
|
|
|
66
66
|
import { FormoAnalyticsSession, SESSION_WALLET_DETECTED_KEY, SESSION_WALLET_IDENTIFIED_KEY, } from "./session";
|
|
67
67
|
import { SignatureStatus, TransactionStatus, WRAPPED_REQUEST_SYMBOL, WRAPPED_REQUEST_REF_SYMBOL, } from "./types";
|
|
68
68
|
import { validateAddress, validateAndChecksumAddress } from "./utils/address";
|
|
69
|
+
import { getTimezone } from "./utils/timezone";
|
|
69
70
|
import { isLocalhost } from "./validators";
|
|
70
71
|
import { parseChainId } from "./utils/chain";
|
|
71
72
|
import { WagmiEventHandler } from "./wagmi";
|
|
@@ -391,6 +392,14 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
391
392
|
logger.warn("Connect: Invalid address provided (\"".concat(address, "\"). Please provide a valid EVM or Solana address."));
|
|
392
393
|
return [2 /*return*/];
|
|
393
394
|
}
|
|
395
|
+
// connect() persists wallet/chain state (active-wallet cookie,
|
|
396
|
+
// currentAddress/currentChainId) before trackEvent's consent check —
|
|
397
|
+
// gate the whole method so a suppressed visitor or excluded environment
|
|
398
|
+
// (opt-out / timezone / host / path) leaves no session state.
|
|
399
|
+
if (this.isTrackingSuppressed()) {
|
|
400
|
+
logger.info("connect() skipped: tracking is suppressed for this visitor or environment");
|
|
401
|
+
return [2 /*return*/];
|
|
402
|
+
}
|
|
394
403
|
this.setChainState(chainId, { address: validAddress });
|
|
395
404
|
return [4 /*yield*/, this.trackEvent(EventType.CONNECT, {
|
|
396
405
|
chainId: chainId,
|
|
@@ -578,10 +587,11 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
578
587
|
case 0:
|
|
579
588
|
_d.trys.push([0, 12, , 13]);
|
|
580
589
|
// identify() writes the user-id cookie and marks wallet
|
|
581
|
-
// identification before trackEvent's consent check — gate the
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
590
|
+
// identification before trackEvent's consent check — gate the whole
|
|
591
|
+
// method so a suppressed visitor or excluded environment (opt-out /
|
|
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");
|
|
585
595
|
return [2 /*return*/];
|
|
586
596
|
}
|
|
587
597
|
if (!!params) return [3 /*break*/, 10];
|
|
@@ -709,9 +719,10 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
709
719
|
switch (_c.label) {
|
|
710
720
|
case 0:
|
|
711
721
|
// detect() marks wallet detection (a cookie write) before
|
|
712
|
-
// trackEvent's consent check — gate it
|
|
713
|
-
|
|
714
|
-
|
|
722
|
+
// trackEvent's consent check — gate it for a suppressed visitor or
|
|
723
|
+
// excluded environment (opt-out / timezone / host / path).
|
|
724
|
+
if (this.isTrackingSuppressed()) {
|
|
725
|
+
logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
|
|
715
726
|
return [2 /*return*/];
|
|
716
727
|
}
|
|
717
728
|
if (this.session.isWalletDetected(rdns))
|
|
@@ -1075,9 +1086,16 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
1075
1086
|
case 26:
|
|
1076
1087
|
nextChainId = _a.sent();
|
|
1077
1088
|
wasDisconnected = !this._evmAddress;
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1089
|
+
// Update state regardless of whether connect *event* tracking is enabled,
|
|
1090
|
+
// so disconnect events keep valid address/chainId values. (excludeChains is
|
|
1091
|
+
// NOT suppression — it still updates state so currentChainId can gate
|
|
1092
|
+
// events.)
|
|
1093
|
+
if (this.isTrackingSuppressed()) {
|
|
1094
|
+
this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
this.setChainState('evm', { address: address, chainId: nextChainId });
|
|
1098
|
+
}
|
|
1081
1099
|
providerInfo = this.getProviderInfo(provider);
|
|
1082
1100
|
effectiveChainId = nextChainId || 0;
|
|
1083
1101
|
if (this.isAutocaptureEnabled("connect")) {
|
|
@@ -1250,13 +1268,19 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
1250
1268
|
this._provider = provider;
|
|
1251
1269
|
}
|
|
1252
1270
|
isActiveProvider = this._provider === provider;
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1271
|
+
// Update state from active provider so disconnect events keep valid
|
|
1272
|
+
// address/chainId values — except while suppressed, where we must not
|
|
1273
|
+
// LEARN identity (only drop a stale EVM wallet on a switch).
|
|
1255
1274
|
if (isActiveProvider) {
|
|
1256
|
-
this.
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1275
|
+
if (this.isTrackingSuppressed()) {
|
|
1276
|
+
this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
this.setChainState('evm', {
|
|
1280
|
+
chainId: chainId,
|
|
1281
|
+
address: validateAndChecksumAddress(address) || undefined,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1260
1284
|
}
|
|
1261
1285
|
// Conditionally emit connect event based on tracking configuration
|
|
1262
1286
|
if (isActiveProvider && this._evmAddress) {
|
|
@@ -1625,6 +1649,108 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
1625
1649
|
});
|
|
1626
1650
|
});
|
|
1627
1651
|
};
|
|
1652
|
+
/**
|
|
1653
|
+
* Visitor-level tracking suppression.
|
|
1654
|
+
*
|
|
1655
|
+
* 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
|
|
1657
|
+
* jurisdiction/timezone exclusion. Public entry points that write state
|
|
1658
|
+
* before reaching the `shouldTrack()` event gate (identify/connect/detect)
|
|
1659
|
+
* check this first so suppressed visitors leave no cookies or session state.
|
|
1660
|
+
* @returns {boolean} True if all tracking and persistence must be suppressed
|
|
1661
|
+
*/
|
|
1662
|
+
FormoAnalytics.prototype.isTrackingSuppressed = function () {
|
|
1663
|
+
return this.hasOptedOutTracking() || this.isCurrentEnvironmentExcluded();
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Whether the current environment is excluded from tracking — the visitor's
|
|
1667
|
+
* timezone, the current hostname, or the current pathname matches a
|
|
1668
|
+
* configured exclusion.
|
|
1669
|
+
*
|
|
1670
|
+
* 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,
|
|
1672
|
+
* tracking resumes for future actions. Used as the "do not write identity or
|
|
1673
|
+
* send events" gate at every entry point that would persist state before the
|
|
1674
|
+
* `shouldTrack()` event gate.
|
|
1675
|
+
* @returns {boolean} True if the current environment is excluded
|
|
1676
|
+
*/
|
|
1677
|
+
FormoAnalytics.prototype.isCurrentEnvironmentExcluded = function () {
|
|
1678
|
+
return (this.isTimezoneExcluded() ||
|
|
1679
|
+
this.isHostExcluded() ||
|
|
1680
|
+
this.isPathExcluded());
|
|
1681
|
+
};
|
|
1682
|
+
/**
|
|
1683
|
+
* Whether the current hostname matches a configured `tracking.excludeHosts`
|
|
1684
|
+
* entry (exact match). Current-page-level — see isCurrentEnvironmentExcluded.
|
|
1685
|
+
* @returns {boolean} True if the current hostname is excluded
|
|
1686
|
+
*/
|
|
1687
|
+
FormoAnalytics.prototype.isHostExcluded = function () {
|
|
1688
|
+
var tracking = this.options.tracking;
|
|
1689
|
+
if (tracking === null ||
|
|
1690
|
+
typeof tracking !== "object" ||
|
|
1691
|
+
Array.isArray(tracking)) {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
if (typeof window === "undefined") {
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
var _a = tracking.excludeHosts, excludeHosts = _a === void 0 ? [] : _a;
|
|
1698
|
+
return excludeHosts.includes(window.location.hostname);
|
|
1699
|
+
};
|
|
1700
|
+
/**
|
|
1701
|
+
* Whether the current pathname matches a configured `tracking.excludePaths`
|
|
1702
|
+
* entry (exact match). Current-page-level — see isCurrentEnvironmentExcluded.
|
|
1703
|
+
* @returns {boolean} True if the current pathname is excluded
|
|
1704
|
+
*/
|
|
1705
|
+
FormoAnalytics.prototype.isPathExcluded = function () {
|
|
1706
|
+
var tracking = this.options.tracking;
|
|
1707
|
+
if (tracking === null ||
|
|
1708
|
+
typeof tracking !== "object" ||
|
|
1709
|
+
Array.isArray(tracking)) {
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof window === "undefined") {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
var _a = tracking.excludePaths, excludePaths = _a === void 0 ? [] : _a;
|
|
1716
|
+
return excludePaths.includes(window.location.pathname);
|
|
1717
|
+
};
|
|
1718
|
+
/**
|
|
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
|
|
1721
|
+
* actively purged (not merely skipped). Host/path exclusions are
|
|
1722
|
+
* deliberately excluded here: they are transient current-page states, so a
|
|
1723
|
+
* cookie legitimately written on an allowed page must survive a visit to an
|
|
1724
|
+
* excluded route.
|
|
1725
|
+
* @returns {boolean} True if persisted identity must be purged
|
|
1726
|
+
*/
|
|
1727
|
+
FormoAnalytics.prototype.isPersistedIdentityPurgeRequired = function () {
|
|
1728
|
+
return this.hasOptedOutTracking() || this.isTimezoneExcluded();
|
|
1729
|
+
};
|
|
1730
|
+
/**
|
|
1731
|
+
* Whether the visitor's browser-resolved timezone matches a configured
|
|
1732
|
+
* `tracking.excludeTimezones` entry (case-insensitive). Client-side and
|
|
1733
|
+
* best-effort — see TrackingOptions.excludeTimezones.
|
|
1734
|
+
* @returns {boolean} True if the current timezone is excluded
|
|
1735
|
+
*/
|
|
1736
|
+
FormoAnalytics.prototype.isTimezoneExcluded = function () {
|
|
1737
|
+
var tracking = this.options.tracking;
|
|
1738
|
+
if (tracking === null ||
|
|
1739
|
+
typeof tracking !== "object" ||
|
|
1740
|
+
Array.isArray(tracking)) {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
var _a = tracking.excludeTimezones, excludeTimezones = _a === void 0 ? [] : _a;
|
|
1744
|
+
if (excludeTimezones.length === 0) {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
var timezone = getTimezone();
|
|
1748
|
+
if (!timezone) {
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
var lowerTimezone = timezone.toLowerCase();
|
|
1752
|
+
return excludeTimezones.some(function (tz) { return typeof tz === "string" && tz.toLowerCase() === lowerTimezone; });
|
|
1753
|
+
};
|
|
1628
1754
|
/**
|
|
1629
1755
|
* Determines if tracking should be enabled based on configuration and consent
|
|
1630
1756
|
* @returns {boolean} True if tracking should be enabled
|
|
@@ -1642,20 +1768,11 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
1642
1768
|
if (this.options.tracking !== null &&
|
|
1643
1769
|
typeof this.options.tracking === "object" &&
|
|
1644
1770
|
!Array.isArray(this.options.tracking)) {
|
|
1645
|
-
var _a = this.options.tracking
|
|
1646
|
-
//
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
return false;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
// Check path exclusions - use exact matching
|
|
1654
|
-
if (excludePaths.length > 0 && typeof window !== "undefined") {
|
|
1655
|
-
var pathname = window.location.pathname;
|
|
1656
|
-
if (excludePaths.includes(pathname)) {
|
|
1657
|
-
return false;
|
|
1658
|
-
}
|
|
1771
|
+
var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
|
|
1772
|
+
// Environment exclusions (timezone / host / path) — no identify / connect
|
|
1773
|
+
// / track events while excluded. Host/path are exact-match.
|
|
1774
|
+
if (this.isCurrentEnvironmentExcluded()) {
|
|
1775
|
+
return false;
|
|
1659
1776
|
}
|
|
1660
1777
|
// Check chainId exclusions
|
|
1661
1778
|
if (excludeChains.length > 0 &&
|
|
@@ -2046,10 +2163,31 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
2046
2163
|
* value in the normal way; existing connections are never clobbered.
|
|
2047
2164
|
*/
|
|
2048
2165
|
FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId) {
|
|
2166
|
+
// Never learn identity while suppressed (opt-out / timezone / excluded host
|
|
2167
|
+
// or path). A signature/transaction observed on an excluded route must not
|
|
2168
|
+
// populate currentAddress for later allowed-page events. backfill only ever
|
|
2169
|
+
// *adds* an address (it no-ops when one is already known), so there is no
|
|
2170
|
+
// stale state to clear here.
|
|
2171
|
+
if (this.isTrackingSuppressed())
|
|
2172
|
+
return;
|
|
2049
2173
|
if (this._evmAddress)
|
|
2050
2174
|
return;
|
|
2051
2175
|
this.setChainState('evm', { address: address, chainId: chainId });
|
|
2052
2176
|
};
|
|
2177
|
+
/**
|
|
2178
|
+
* Apply an EVM autocapture connect/switch while tracking is suppressed
|
|
2179
|
+
* (opt-out / timezone / excluded host or path): never LEARN the wallet, but
|
|
2180
|
+
* if it is a switch away from an already-learned EVM wallet, drop the stale
|
|
2181
|
+
* one (which also clears the active-wallet cookie) so it can't attach to a
|
|
2182
|
+
* later allowed-page event.
|
|
2183
|
+
*/
|
|
2184
|
+
FormoAnalytics.prototype.clearStaleEvmWalletOnSwitchWhileSuppressed = function (address) {
|
|
2185
|
+
var evmAddress = this._chainState.evm.address;
|
|
2186
|
+
var incoming = validateAndChecksumAddress(address);
|
|
2187
|
+
if (evmAddress && incoming && incoming !== evmAddress) {
|
|
2188
|
+
this.clearChainState('evm');
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2053
2191
|
/**
|
|
2054
2192
|
* Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
|
|
2055
2193
|
*/
|
|
@@ -2256,6 +2394,39 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
2256
2394
|
*/
|
|
2257
2395
|
FormoAnalytics.prototype.syncWalletState = function (params) {
|
|
2258
2396
|
var chainId = params.chainId, address = params.address;
|
|
2397
|
+
if (this.isTrackingSuppressed()) {
|
|
2398
|
+
// While suppressed (opt-out / timezone / excluded host or path) we must
|
|
2399
|
+
// never LEARN a new wallet — but we must still CLEAR stale identity.
|
|
2400
|
+
// Otherwise a disconnect or wallet switch observed on a suppressed route
|
|
2401
|
+
// would leave the previously-learned address in memory and in the
|
|
2402
|
+
// active-wallet cookie, attaching it to later allowed-page events.
|
|
2403
|
+
if (!address) {
|
|
2404
|
+
// Disconnect: drop the affected namespace(s).
|
|
2405
|
+
if (chainId !== undefined && chainId !== null) {
|
|
2406
|
+
this.clearChainState(chainId);
|
|
2407
|
+
}
|
|
2408
|
+
else {
|
|
2409
|
+
this.clearChainState("evm");
|
|
2410
|
+
this.clearChainState("solana");
|
|
2411
|
+
}
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
// Address present: a switch away from the wallet already learned in this
|
|
2415
|
+
// namespace invalidates it. Drop the stale one without learning the new
|
|
2416
|
+
// address; a fresh connect (nothing learned yet) or a re-confirmation of
|
|
2417
|
+
// the same address is a no-op.
|
|
2418
|
+
if (chainId === null || chainId === undefined)
|
|
2419
|
+
return;
|
|
2420
|
+
var namespace = this.getNamespace(chainId);
|
|
2421
|
+
var namespaceAddress = this._chainState[namespace].address;
|
|
2422
|
+
var validIncoming = validateAddress(address, chainId);
|
|
2423
|
+
if (namespaceAddress &&
|
|
2424
|
+
validIncoming &&
|
|
2425
|
+
validIncoming !== namespaceAddress) {
|
|
2426
|
+
this.clearChainState(chainId);
|
|
2427
|
+
}
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2259
2430
|
if (!address) {
|
|
2260
2431
|
if (chainId !== undefined && chainId !== null) {
|
|
2261
2432
|
this.clearChainState(chainId);
|
|
@@ -2313,18 +2484,28 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
2313
2484
|
*/
|
|
2314
2485
|
FormoAnalytics.prototype.persistActiveWallet = function () {
|
|
2315
2486
|
try {
|
|
2316
|
-
//
|
|
2317
|
-
// prior snapshot is
|
|
2318
|
-
if (this.
|
|
2487
|
+
// Visitor-level suppression (opt-out or excluded timezone): purge any
|
|
2488
|
+
// prior snapshot — these are stable for the session, so deletion is safe.
|
|
2489
|
+
if (this.isPersistedIdentityPurgeRequired()) {
|
|
2319
2490
|
cookie().remove(ACTIVE_WALLET_KEY);
|
|
2320
2491
|
return;
|
|
2321
2492
|
}
|
|
2322
2493
|
if (this.currentAddress) {
|
|
2494
|
+
// Current-page exclusion (host/path): do not write a new snapshot while
|
|
2495
|
+
// on an excluded route, but leave any existing cookie intact. A cookie
|
|
2496
|
+
// written on an allowed page must survive a transient visit to an
|
|
2497
|
+
// excluded one (passive navigation does not call this method).
|
|
2498
|
+
if (this.isHostExcluded() || this.isPathExcluded()) {
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2323
2501
|
var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
|
|
2324
2502
|
var domain = getIdentityCookieDomain(this.crossSubdomainCookies);
|
|
2325
2503
|
cookie().set(ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + ACTIVE_WALLET_TTL_MS).toUTCString() }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
|
|
2326
2504
|
}
|
|
2327
2505
|
else {
|
|
2506
|
+
// No active wallet → clear the snapshot. This runs even on an excluded
|
|
2507
|
+
// route, so a disconnect/switch observed while suppressed actively
|
|
2508
|
+
// removes stale identity instead of leaving it for later allowed events.
|
|
2328
2509
|
cookie().remove(ACTIVE_WALLET_KEY);
|
|
2329
2510
|
}
|
|
2330
2511
|
}
|
|
@@ -2338,12 +2519,18 @@ var FormoAnalytics = /** @class */ (function () {
|
|
|
2338
2519
|
*/
|
|
2339
2520
|
FormoAnalytics.prototype.loadActiveWallet = function () {
|
|
2340
2521
|
try {
|
|
2341
|
-
//
|
|
2342
|
-
//
|
|
2343
|
-
if (this.
|
|
2522
|
+
// Visitor-level suppression (opt-out or excluded timezone): never restore
|
|
2523
|
+
// identity into memory; drop the stale snapshot.
|
|
2524
|
+
if (this.isPersistedIdentityPurgeRequired()) {
|
|
2344
2525
|
cookie().remove(ACTIVE_WALLET_KEY);
|
|
2345
2526
|
return;
|
|
2346
2527
|
}
|
|
2528
|
+
// Current-page exclusion (host/path): don't restore into memory while on
|
|
2529
|
+
// an excluded route, but keep the cookie so a later allowed-page load can
|
|
2530
|
+
// restore it.
|
|
2531
|
+
if (this.isHostExcluded() || this.isPathExcluded()) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2347
2534
|
var raw = cookie().get(ACTIVE_WALLET_KEY);
|
|
2348
2535
|
if (!raw)
|
|
2349
2536
|
return;
|
|
@@ -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
|
|
28
|
-
*
|
|
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;
|
|
@@ -55,7 +55,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
55
55
|
return to.concat(ar || Array.prototype.slice.call(from));
|
|
56
56
|
};
|
|
57
57
|
import { COUNTRY_LIST, LOCAL_ANONYMOUS_ID_KEY, SESSION_TRAFFIC_SOURCE_KEY, } from "../constants";
|
|
58
|
-
import { toSnakeCase } from "../utils";
|
|
58
|
+
import { toSnakeCase, getTimezone } from "../utils";
|
|
59
59
|
import { validateAddress } from "../utils/address";
|
|
60
60
|
import { getCurrentTimeFormatted } from "../utils/timestamp";
|
|
61
61
|
import { isUndefined } from "../validators";
|
|
@@ -63,7 +63,7 @@ import { logger } from "../logger";
|
|
|
63
63
|
import mergeDeepRight from "../ramda/mergeDeepRight";
|
|
64
64
|
import { session } from "../storage";
|
|
65
65
|
import { version } from "../version";
|
|
66
|
-
import { CHANNEL, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, VERSION, } from "./constants";
|
|
66
|
+
import { CHANNEL, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, DEFAULT_REFERRAL_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, VERSION, } from "./constants";
|
|
67
67
|
import { generateAnonymousId } from "./utils";
|
|
68
68
|
import { detectBrowser } from "../browser/browsers";
|
|
69
69
|
var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
|
|
@@ -107,10 +107,9 @@ var EventFactory = /** @class */ (function () {
|
|
|
107
107
|
// - If no referral config exists → use defaults
|
|
108
108
|
// - If referral config exists but queryParams is undefined → use defaults
|
|
109
109
|
// - If referral config exists with queryParams → use those
|
|
110
|
-
var defaultParams = ["ref", "referral", "refcode"];
|
|
111
110
|
var referralParams = !((_a = _this.options) === null || _a === void 0 ? void 0 : _a.referral)
|
|
112
|
-
?
|
|
113
|
-
: ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b :
|
|
111
|
+
? DEFAULT_REFERRAL_PARAMS // No referral config at all → use defaults
|
|
112
|
+
: ((_b = _this.options.referral.queryParams) !== null && _b !== void 0 ? _b : DEFAULT_REFERRAL_PARAMS); // Has config → use queryParams or defaults
|
|
114
113
|
// Check query parameters (if any configured)
|
|
115
114
|
for (var _i = 0, referralParams_1 = referralParams; _i < referralParams_1.length; _i++) {
|
|
116
115
|
var param = referralParams_1[_i];
|
|
@@ -205,13 +204,14 @@ var EventFactory = /** @class */ (function () {
|
|
|
205
204
|
try {
|
|
206
205
|
urlObj = new URL(globalThis.location.href);
|
|
207
206
|
_this.redactQueryParams(urlObj);
|
|
207
|
+
_this.normalizeUrlPath(urlObj);
|
|
208
208
|
}
|
|
209
209
|
catch (_a) { }
|
|
210
210
|
if (isUndefined(pageProps.url)) {
|
|
211
211
|
pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
|
|
212
212
|
}
|
|
213
213
|
if (isUndefined(pageProps.path)) {
|
|
214
|
-
pageProps.path = globalThis.location.pathname;
|
|
214
|
+
pageProps.path = urlObj ? urlObj.pathname : globalThis.location.pathname;
|
|
215
215
|
}
|
|
216
216
|
if (isUndefined(pageProps.hash)) {
|
|
217
217
|
pageProps.hash = globalThis.location.hash;
|
|
@@ -270,13 +270,7 @@ var EventFactory = /** @class */ (function () {
|
|
|
270
270
|
return validateAddress(address, chainId) || null;
|
|
271
271
|
};
|
|
272
272
|
EventFactory.prototype.getTimezone = function () {
|
|
273
|
-
|
|
274
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
logger.error("Error resolving timezone:", error);
|
|
278
|
-
return "";
|
|
279
|
-
}
|
|
273
|
+
return getTimezone();
|
|
280
274
|
};
|
|
281
275
|
EventFactory.prototype.getLocation = function () {
|
|
282
276
|
try {
|
|
@@ -312,6 +306,16 @@ var EventFactory = /** @class */ (function () {
|
|
|
312
306
|
EventFactory.prototype.isExcludedQueryParam = function (key) {
|
|
313
307
|
return this.excludedQueryParams.has(key.toLowerCase());
|
|
314
308
|
};
|
|
309
|
+
/**
|
|
310
|
+
* Normalize URL paths for analytics aggregation by stripping trailing slashes
|
|
311
|
+
* from non-root paths. Query strings and hash fragments are preserved by
|
|
312
|
+
* mutating only the URL pathname.
|
|
313
|
+
*/
|
|
314
|
+
EventFactory.prototype.normalizeUrlPath = function (url) {
|
|
315
|
+
if (url.pathname !== "/") {
|
|
316
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
317
|
+
}
|
|
318
|
+
};
|
|
315
319
|
/**
|
|
316
320
|
* Strip excluded (sensitive) query parameters from a URL in place. Only the
|
|
317
321
|
* query string is touched; the path and hash/fragment are left as-is.
|
|
@@ -329,9 +333,9 @@ var EventFactory = /** @class */ (function () {
|
|
|
329
333
|
keysToDelete.forEach(function (key) { return url.searchParams.delete(key); });
|
|
330
334
|
};
|
|
331
335
|
/**
|
|
332
|
-
* Return the given absolute URL with excluded query parameters removed
|
|
333
|
-
*
|
|
334
|
-
* empty referrer).
|
|
336
|
+
* Return the given absolute URL with excluded query parameters removed and
|
|
337
|
+
* trailing slashes stripped from non-root paths. The input is returned
|
|
338
|
+
* unchanged when it is empty or cannot be parsed (e.g. an empty referrer).
|
|
335
339
|
*/
|
|
336
340
|
EventFactory.prototype.redactUrl = function (href) {
|
|
337
341
|
if (!href)
|
|
@@ -339,6 +343,7 @@ var EventFactory = /** @class */ (function () {
|
|
|
339
343
|
try {
|
|
340
344
|
var url = new URL(href);
|
|
341
345
|
this.redactQueryParams(url);
|
|
346
|
+
this.normalizeUrlPath(url);
|
|
342
347
|
return url.href;
|
|
343
348
|
}
|
|
344
349
|
catch (_a) {
|
|
@@ -18,6 +18,15 @@ declare const CLICK_ID_PARAMS: readonly ["gclid", "gad_source", "fbclid", "msclk
|
|
|
18
18
|
* cannot remove these built-ins. Matched case-insensitively.
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EXCLUDED_QUERY_PARAMS: readonly ["privy_oauth_code", "privy_oauth_state", "privy_oauth_provider"];
|
|
21
|
+
/**
|
|
22
|
+
* Default query parameter names checked (in order) for a referral code on the
|
|
23
|
+
* landing-page URL. The first parameter present supplies the `ref` traffic
|
|
24
|
+
* source. Consumers can override this list via `referral.queryParams`.
|
|
25
|
+
*
|
|
26
|
+
* Keep in sync with the ReferralOptions.queryParams @default in
|
|
27
|
+
* src/types/base.ts.
|
|
28
|
+
*/
|
|
29
|
+
declare const DEFAULT_REFERRAL_PARAMS: readonly ["ref", "referral", "refcode", "af", "referrer"];
|
|
21
30
|
/**
|
|
22
31
|
* Fields that should be excluded from page event properties parsing
|
|
23
32
|
* These are either:
|
|
@@ -25,5 +34,5 @@ declare const DEFAULT_EXCLUDED_QUERY_PARAMS: readonly ["privy_oauth_code", "priv
|
|
|
25
34
|
* - Semantic event properties that should not be overridden by URL params
|
|
26
35
|
*/
|
|
27
36
|
declare const PAGE_PROPERTIES_EXCLUDED_FIELDS: Set<string>;
|
|
28
|
-
export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
|
|
37
|
+
export { CHANNEL, VERSION, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, DEFAULT_REFERRAL_PARAMS, PAGE_PROPERTIES_EXCLUDED_FIELDS, };
|
|
29
38
|
//# sourceMappingURL=constants.d.ts.map
|