@formo/analytics 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  */
@@ -69,6 +69,7 @@ var provider_1 = require("./provider");
69
69
  var session_1 = require("./session");
70
70
  var types_1 = require("./types");
71
71
  var address_1 = require("./utils/address");
72
+ var timezone_1 = require("./utils/timezone");
72
73
  var validators_1 = require("./validators");
73
74
  var chain_1 = require("./utils/chain");
74
75
  var wagmi_1 = require("./wagmi");
@@ -394,6 +395,14 @@ var FormoAnalytics = /** @class */ (function () {
394
395
  logger_1.logger.warn("Connect: Invalid address provided (\"".concat(address, "\"). Please provide a valid EVM or Solana address."));
395
396
  return [2 /*return*/];
396
397
  }
398
+ // connect() persists wallet/chain state (active-wallet cookie,
399
+ // currentAddress/currentChainId) before trackEvent's consent check —
400
+ // gate the whole method so a suppressed visitor or excluded environment
401
+ // (opt-out / timezone / host / path) leaves no session state.
402
+ if (this.isTrackingSuppressed()) {
403
+ logger_1.logger.info("connect() skipped: tracking is suppressed for this visitor or environment");
404
+ return [2 /*return*/];
405
+ }
397
406
  this.setChainState(chainId, { address: validAddress });
398
407
  return [4 /*yield*/, this.trackEvent(constants_1.EventType.CONNECT, {
399
408
  chainId: chainId,
@@ -581,10 +590,11 @@ var FormoAnalytics = /** @class */ (function () {
581
590
  case 0:
582
591
  _d.trys.push([0, 12, , 13]);
583
592
  // identify() writes the user-id cookie and marks wallet
584
- // identification before trackEvent's consent check — gate the
585
- // whole method so an opted-out user gets no identity persistence.
586
- if (this.hasOptedOutTracking()) {
587
- logger_1.logger.info("identify() skipped: user has opted out of tracking");
593
+ // identification before trackEvent's consent check — gate the whole
594
+ // method so a suppressed visitor or excluded environment (opt-out /
595
+ // timezone / host / path) gets no identity persistence.
596
+ if (this.isTrackingSuppressed()) {
597
+ logger_1.logger.info("identify() skipped: tracking is suppressed for this visitor or environment");
588
598
  return [2 /*return*/];
589
599
  }
590
600
  if (!!params) return [3 /*break*/, 10];
@@ -712,9 +722,10 @@ var FormoAnalytics = /** @class */ (function () {
712
722
  switch (_c.label) {
713
723
  case 0:
714
724
  // detect() marks wallet detection (a cookie write) before
715
- // trackEvent's consent check — gate it on opt-out.
716
- if (this.hasOptedOutTracking()) {
717
- logger_1.logger.info("detect() skipped: user has opted out of tracking");
725
+ // trackEvent's consent check — gate it for a suppressed visitor or
726
+ // excluded environment (opt-out / timezone / host / path).
727
+ if (this.isTrackingSuppressed()) {
728
+ logger_1.logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
718
729
  return [2 /*return*/];
719
730
  }
720
731
  if (this.session.isWalletDetected(rdns))
@@ -1078,9 +1089,16 @@ var FormoAnalytics = /** @class */ (function () {
1078
1089
  case 26:
1079
1090
  nextChainId = _a.sent();
1080
1091
  wasDisconnected = !this._evmAddress;
1081
- // CRITICAL: Always update state regardless of whether connect tracking is enabled
1082
- // This ensures disconnect events will have valid address/chainId values
1083
- this.setChainState('evm', { address: address, chainId: nextChainId });
1092
+ // Update state regardless of whether connect *event* tracking is enabled,
1093
+ // so disconnect events keep valid address/chainId values. (excludeChains is
1094
+ // NOT suppression it still updates state so currentChainId can gate
1095
+ // events.)
1096
+ if (this.isTrackingSuppressed()) {
1097
+ this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
1098
+ }
1099
+ else {
1100
+ this.setChainState('evm', { address: address, chainId: nextChainId });
1101
+ }
1084
1102
  providerInfo = this.getProviderInfo(provider);
1085
1103
  effectiveChainId = nextChainId || 0;
1086
1104
  if (this.isAutocaptureEnabled("connect")) {
@@ -1253,13 +1271,19 @@ var FormoAnalytics = /** @class */ (function () {
1253
1271
  this._provider = provider;
1254
1272
  }
1255
1273
  isActiveProvider = this._provider === provider;
1256
- // CRITICAL: Always update state from active provider regardless of tracking config
1257
- // This ensures disconnect events will have valid address/chainId values
1274
+ // Update state from active provider so disconnect events keep valid
1275
+ // address/chainId values except while suppressed, where we must not
1276
+ // LEARN identity (only drop a stale EVM wallet on a switch).
1258
1277
  if (isActiveProvider) {
1259
- this.setChainState('evm', {
1260
- chainId: chainId,
1261
- address: (0, address_1.validateAndChecksumAddress)(address) || undefined,
1262
- });
1278
+ if (this.isTrackingSuppressed()) {
1279
+ this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
1280
+ }
1281
+ else {
1282
+ this.setChainState('evm', {
1283
+ chainId: chainId,
1284
+ address: (0, address_1.validateAndChecksumAddress)(address) || undefined,
1285
+ });
1286
+ }
1263
1287
  }
1264
1288
  // Conditionally emit connect event based on tracking configuration
1265
1289
  if (isActiveProvider && this._evmAddress) {
@@ -1628,6 +1652,108 @@ var FormoAnalytics = /** @class */ (function () {
1628
1652
  });
1629
1653
  });
1630
1654
  };
1655
+ /**
1656
+ * Visitor-level tracking suppression.
1657
+ *
1658
+ * Returns true when the SDK must not persist any identity/session/chain
1659
+ * state or send any events for this visitor — i.e. an explicit opt-out or a
1660
+ * jurisdiction/timezone exclusion. Public entry points that write state
1661
+ * before reaching the `shouldTrack()` event gate (identify/connect/detect)
1662
+ * check this first so suppressed visitors leave no cookies or session state.
1663
+ * @returns {boolean} True if all tracking and persistence must be suppressed
1664
+ */
1665
+ FormoAnalytics.prototype.isTrackingSuppressed = function () {
1666
+ return this.hasOptedOutTracking() || this.isCurrentEnvironmentExcluded();
1667
+ };
1668
+ /**
1669
+ * Whether the current environment is excluded from tracking — the visitor's
1670
+ * timezone, the current hostname, or the current pathname matches a
1671
+ * configured exclusion.
1672
+ *
1673
+ * Timezone is visitor/session-level (stable for the session); host/path are
1674
+ * current-page-level and transient — if a SPA navigates to an allowed path,
1675
+ * tracking resumes for future actions. Used as the "do not write identity or
1676
+ * send events" gate at every entry point that would persist state before the
1677
+ * `shouldTrack()` event gate.
1678
+ * @returns {boolean} True if the current environment is excluded
1679
+ */
1680
+ FormoAnalytics.prototype.isCurrentEnvironmentExcluded = function () {
1681
+ return (this.isTimezoneExcluded() ||
1682
+ this.isHostExcluded() ||
1683
+ this.isPathExcluded());
1684
+ };
1685
+ /**
1686
+ * Whether the current hostname matches a configured `tracking.excludeHosts`
1687
+ * entry (exact match). Current-page-level — see isCurrentEnvironmentExcluded.
1688
+ * @returns {boolean} True if the current hostname is excluded
1689
+ */
1690
+ FormoAnalytics.prototype.isHostExcluded = function () {
1691
+ var tracking = this.options.tracking;
1692
+ if (tracking === null ||
1693
+ typeof tracking !== "object" ||
1694
+ Array.isArray(tracking)) {
1695
+ return false;
1696
+ }
1697
+ if (typeof window === "undefined") {
1698
+ return false;
1699
+ }
1700
+ var _a = tracking.excludeHosts, excludeHosts = _a === void 0 ? [] : _a;
1701
+ return excludeHosts.includes(window.location.hostname);
1702
+ };
1703
+ /**
1704
+ * Whether the current pathname matches a configured `tracking.excludePaths`
1705
+ * entry (exact match). Current-page-level — see isCurrentEnvironmentExcluded.
1706
+ * @returns {boolean} True if the current pathname is excluded
1707
+ */
1708
+ FormoAnalytics.prototype.isPathExcluded = function () {
1709
+ var tracking = this.options.tracking;
1710
+ if (tracking === null ||
1711
+ typeof tracking !== "object" ||
1712
+ Array.isArray(tracking)) {
1713
+ return false;
1714
+ }
1715
+ if (typeof window === "undefined") {
1716
+ return false;
1717
+ }
1718
+ var _a = tracking.excludePaths, excludePaths = _a === void 0 ? [] : _a;
1719
+ return excludePaths.includes(window.location.pathname);
1720
+ };
1721
+ /**
1722
+ * Whether the current call is in a visitor-level suppression state — opt-out
1723
+ * or excluded timezone — for which any persisted identity cookie should be
1724
+ * actively purged (not merely skipped). Host/path exclusions are
1725
+ * deliberately excluded here: they are transient current-page states, so a
1726
+ * cookie legitimately written on an allowed page must survive a visit to an
1727
+ * excluded route.
1728
+ * @returns {boolean} True if persisted identity must be purged
1729
+ */
1730
+ FormoAnalytics.prototype.isPersistedIdentityPurgeRequired = function () {
1731
+ return this.hasOptedOutTracking() || this.isTimezoneExcluded();
1732
+ };
1733
+ /**
1734
+ * Whether the visitor's browser-resolved timezone matches a configured
1735
+ * `tracking.excludeTimezones` entry (case-insensitive). Client-side and
1736
+ * best-effort — see TrackingOptions.excludeTimezones.
1737
+ * @returns {boolean} True if the current timezone is excluded
1738
+ */
1739
+ FormoAnalytics.prototype.isTimezoneExcluded = function () {
1740
+ var tracking = this.options.tracking;
1741
+ if (tracking === null ||
1742
+ typeof tracking !== "object" ||
1743
+ Array.isArray(tracking)) {
1744
+ return false;
1745
+ }
1746
+ var _a = tracking.excludeTimezones, excludeTimezones = _a === void 0 ? [] : _a;
1747
+ if (excludeTimezones.length === 0) {
1748
+ return false;
1749
+ }
1750
+ var timezone = (0, timezone_1.getTimezone)();
1751
+ if (!timezone) {
1752
+ return false;
1753
+ }
1754
+ var lowerTimezone = timezone.toLowerCase();
1755
+ return excludeTimezones.some(function (tz) { return typeof tz === "string" && tz.toLowerCase() === lowerTimezone; });
1756
+ };
1631
1757
  /**
1632
1758
  * Determines if tracking should be enabled based on configuration and consent
1633
1759
  * @returns {boolean} True if tracking should be enabled
@@ -1645,20 +1771,11 @@ var FormoAnalytics = /** @class */ (function () {
1645
1771
  if (this.options.tracking !== null &&
1646
1772
  typeof this.options.tracking === "object" &&
1647
1773
  !Array.isArray(this.options.tracking)) {
1648
- var _a = this.options.tracking, _b = _a.excludeHosts, excludeHosts = _b === void 0 ? [] : _b, _c = _a.excludePaths, excludePaths = _c === void 0 ? [] : _c, _d = _a.excludeChains, excludeChains = _d === void 0 ? [] : _d;
1649
- // Check hostname exclusions - use exact matching
1650
- if (excludeHosts.length > 0 && typeof window !== "undefined") {
1651
- var hostname = window.location.hostname;
1652
- if (excludeHosts.includes(hostname)) {
1653
- return false;
1654
- }
1655
- }
1656
- // Check path exclusions - use exact matching
1657
- if (excludePaths.length > 0 && typeof window !== "undefined") {
1658
- var pathname = window.location.pathname;
1659
- if (excludePaths.includes(pathname)) {
1660
- return false;
1661
- }
1774
+ var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1775
+ // Environment exclusions (timezone / host / path) — no identify / connect
1776
+ // / track events while excluded. Host/path are exact-match.
1777
+ if (this.isCurrentEnvironmentExcluded()) {
1778
+ return false;
1662
1779
  }
1663
1780
  // Check chainId exclusions
1664
1781
  if (excludeChains.length > 0 &&
@@ -2049,10 +2166,31 @@ var FormoAnalytics = /** @class */ (function () {
2049
2166
  * value in the normal way; existing connections are never clobbered.
2050
2167
  */
2051
2168
  FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId) {
2169
+ // Never learn identity while suppressed (opt-out / timezone / excluded host
2170
+ // or path). A signature/transaction observed on an excluded route must not
2171
+ // populate currentAddress for later allowed-page events. backfill only ever
2172
+ // *adds* an address (it no-ops when one is already known), so there is no
2173
+ // stale state to clear here.
2174
+ if (this.isTrackingSuppressed())
2175
+ return;
2052
2176
  if (this._evmAddress)
2053
2177
  return;
2054
2178
  this.setChainState('evm', { address: address, chainId: chainId });
2055
2179
  };
2180
+ /**
2181
+ * Apply an EVM autocapture connect/switch while tracking is suppressed
2182
+ * (opt-out / timezone / excluded host or path): never LEARN the wallet, but
2183
+ * if it is a switch away from an already-learned EVM wallet, drop the stale
2184
+ * one (which also clears the active-wallet cookie) so it can't attach to a
2185
+ * later allowed-page event.
2186
+ */
2187
+ FormoAnalytics.prototype.clearStaleEvmWalletOnSwitchWhileSuppressed = function (address) {
2188
+ var evmAddress = this._chainState.evm.address;
2189
+ var incoming = (0, address_1.validateAndChecksumAddress)(address);
2190
+ if (evmAddress && incoming && incoming !== evmAddress) {
2191
+ this.clearChainState('evm');
2192
+ }
2193
+ };
2056
2194
  /**
2057
2195
  * Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
2058
2196
  */
@@ -2259,6 +2397,39 @@ var FormoAnalytics = /** @class */ (function () {
2259
2397
  */
2260
2398
  FormoAnalytics.prototype.syncWalletState = function (params) {
2261
2399
  var chainId = params.chainId, address = params.address;
2400
+ if (this.isTrackingSuppressed()) {
2401
+ // While suppressed (opt-out / timezone / excluded host or path) we must
2402
+ // never LEARN a new wallet — but we must still CLEAR stale identity.
2403
+ // Otherwise a disconnect or wallet switch observed on a suppressed route
2404
+ // would leave the previously-learned address in memory and in the
2405
+ // active-wallet cookie, attaching it to later allowed-page events.
2406
+ if (!address) {
2407
+ // Disconnect: drop the affected namespace(s).
2408
+ if (chainId !== undefined && chainId !== null) {
2409
+ this.clearChainState(chainId);
2410
+ }
2411
+ else {
2412
+ this.clearChainState("evm");
2413
+ this.clearChainState("solana");
2414
+ }
2415
+ return;
2416
+ }
2417
+ // Address present: a switch away from the wallet already learned in this
2418
+ // namespace invalidates it. Drop the stale one without learning the new
2419
+ // address; a fresh connect (nothing learned yet) or a re-confirmation of
2420
+ // the same address is a no-op.
2421
+ if (chainId === null || chainId === undefined)
2422
+ return;
2423
+ var namespace = this.getNamespace(chainId);
2424
+ var namespaceAddress = this._chainState[namespace].address;
2425
+ var validIncoming = (0, address_1.validateAddress)(address, chainId);
2426
+ if (namespaceAddress &&
2427
+ validIncoming &&
2428
+ validIncoming !== namespaceAddress) {
2429
+ this.clearChainState(chainId);
2430
+ }
2431
+ return;
2432
+ }
2262
2433
  if (!address) {
2263
2434
  if (chainId !== undefined && chainId !== null) {
2264
2435
  this.clearChainState(chainId);
@@ -2316,18 +2487,28 @@ var FormoAnalytics = /** @class */ (function () {
2316
2487
  */
2317
2488
  FormoAnalytics.prototype.persistActiveWallet = function () {
2318
2489
  try {
2319
- // Never write an identity cookie for an opted-out user; ensure any
2320
- // prior snapshot is removed instead.
2321
- if (this.hasOptedOutTracking()) {
2490
+ // Visitor-level suppression (opt-out or excluded timezone): purge any
2491
+ // prior snapshot — these are stable for the session, so deletion is safe.
2492
+ if (this.isPersistedIdentityPurgeRequired()) {
2322
2493
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2323
2494
  return;
2324
2495
  }
2325
2496
  if (this.currentAddress) {
2497
+ // Current-page exclusion (host/path): do not write a new snapshot while
2498
+ // on an excluded route, but leave any existing cookie intact. A cookie
2499
+ // written on an allowed page must survive a transient visit to an
2500
+ // excluded one (passive navigation does not call this method).
2501
+ if (this.isHostExcluded() || this.isPathExcluded()) {
2502
+ return;
2503
+ }
2326
2504
  var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
2327
2505
  var domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
2328
2506
  (0, storage_1.cookie)().set(constants_1.ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + constants_1.ACTIVE_WALLET_TTL_MS).toUTCString() }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
2329
2507
  }
2330
2508
  else {
2509
+ // No active wallet → clear the snapshot. This runs even on an excluded
2510
+ // route, so a disconnect/switch observed while suppressed actively
2511
+ // removes stale identity instead of leaving it for later allowed events.
2331
2512
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2332
2513
  }
2333
2514
  }
@@ -2341,12 +2522,18 @@ var FormoAnalytics = /** @class */ (function () {
2341
2522
  */
2342
2523
  FormoAnalytics.prototype.loadActiveWallet = function () {
2343
2524
  try {
2344
- // Never restore wallet identity into memory for an opted-out user
2345
- // (mirrors persistActiveWallet's guard); drop any stale snapshot.
2346
- if (this.hasOptedOutTracking()) {
2525
+ // Visitor-level suppression (opt-out or excluded timezone): never restore
2526
+ // identity into memory; drop the stale snapshot.
2527
+ if (this.isPersistedIdentityPurgeRequired()) {
2347
2528
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2348
2529
  return;
2349
2530
  }
2531
+ // Current-page exclusion (host/path): don't restore into memory while on
2532
+ // an excluded route, but keep the cookie so a later allowed-page load can
2533
+ // restore it.
2534
+ if (this.isHostExcluded() || this.isPathExcluded()) {
2535
+ return;
2536
+ }
2350
2537
  var raw = (0, storage_1.cookie)().get(constants_1.ACTIVE_WALLET_KEY);
2351
2538
  if (!raw)
2352
2539
  return;
@@ -276,13 +276,7 @@ var EventFactory = /** @class */ (function () {
276
276
  return (0, address_1.validateAddress)(address, chainId) || null;
277
277
  };
278
278
  EventFactory.prototype.getTimezone = function () {
279
- try {
280
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
281
- }
282
- catch (error) {
283
- logger_1.logger.error("Error resolving timezone:", error);
284
- return "";
285
- }
279
+ return (0, utils_1.getTimezone)();
286
280
  };
287
281
  EventFactory.prototype.getLocation = function () {
288
282
  try {
@@ -72,6 +72,20 @@ export interface TrackingOptions {
72
72
  excludeHosts?: string[];
73
73
  excludePaths?: string[];
74
74
  excludeChains?: ChainID[];
75
+ /**
76
+ * IANA timezone names to opt out of tracking entirely. When the visitor's
77
+ * resolved timezone (via `Intl.DateTimeFormat().resolvedOptions().timeZone`)
78
+ * matches one of these, no events are enqueued or sent — including `identify`
79
+ * and `connect`. Matched case-insensitively against the full timezone string.
80
+ *
81
+ * Note: this is client-side, timezone-derived geolocation. It is best-effort
82
+ * and can be bypassed (a VPN does not change the browser timezone, and users
83
+ * can change their OS timezone). For authoritative jurisdiction blocking, use
84
+ * server-side IP geolocation at your ingest endpoint instead.
85
+ *
86
+ * @example ["Europe/London", "America/New_York"]
87
+ */
88
+ excludeTimezones?: string[];
75
89
  /**
76
90
  * Additional query parameter names to strip from forwarded and stored URLs,
77
91
  * on top of a built-in always-on denylist (currently `privy_oauth_code`,
@@ -3,4 +3,5 @@ export * from "./base";
3
3
  export * from "./converter";
4
4
  export * from "./generate";
5
5
  export * from "./hash";
6
+ export * from "./timezone";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -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
  */