Add timezone-based tracking opt-out via excludeTimezones#283
Merged
Conversation
Add `tracking.excludeTimezones` (IANA timezone names) so visitors in matching timezones produce no events at all — including identify and connect. Enforced in the single shouldTrack() gate that every event flows through, mirroring the existing excludeHosts/excludePaths/ excludeChains exclusions. Matching is case-insensitive. Extract the Intl-based timezone resolution into a shared utils/timezone.getTimezone() helper, reused by EventFactory and the new gate. Note: this is client-side, timezone-derived geolocation and is best-effort (a VPN does not change browser timezone, and users can change their OS timezone). For authoritative jurisdiction blocking, use server-side IP geolocation at the ingest endpoint.
There was a problem hiding this comment.
Code Review
This pull request introduces timezone-based tracking exclusion to allow opting out visitors in specific timezones entirely. Feedback focuses on optimizing the timezone matching loop by lowercasing the visitor's timezone once, adding a fallback to an empty string in the timezone utility for older environments, and restoring the original global.Intl object in tests to prevent global state pollution.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Previously excludeTimezones was only enforced in shouldTrack(), which runs inside trackEvent() — after identify()/connect()/detect() had already persisted identity/session/chain state (user-id, active-wallet, wallet-identification/detection cookies, currentAddress/currentChainId). A timezone-excluded visitor therefore left cookies and state behind even though no events were sent. Introduce isTrackingSuppressed() (opt-out OR excluded timezone) and gate the three public entry points on it before any state writes, mirroring the existing opt-out early-exit pattern. The autocapture provider-event path intentionally keeps persisting state so disconnect payloads stay valid. Extract the timezone match into isTimezoneExcluded(), reused by shouldTrack(). Add a test asserting no wallet/identity/session state is persisted for an excluded visitor.
- isTimezoneExcluded(): hoist timezone.toLowerCase() out of the some() loop and guard each entry with typeof tz === "string" - getTimezone(): fall back to "" when resolvedOptions().timeZone is undefined, matching the declared string return type - spec: restore the original global.Intl in afterEach instead of deleting it, to avoid polluting other suites
…sitors The public identify/connect/detect path was already gated, but the EIP-1193 / Wagmi autocapture and syncWalletState paths reach persistActiveWallet()/loadActiveWallet() directly, which only checked hasOptedOutTracking(). An excluded-timezone visitor therefore still got an active-wallet cookie (and a restored snapshot at init). Broaden both persistence guards to isTrackingSuppressed() (opt-out OR excluded timezone). This is the single cookie chokepoint, so it closes the leak across all paths at once. In-memory currentAddress/currentChainId are intentionally left intact for disconnect correctness; only the persisted cookie is suppressed. Add tests covering the syncWalletState (autocapture) path plus a non-excluded control.
Extend the persistence-suppression model to the current-page environment excludes. excludeHosts/excludePaths are now treated as current-page suppression (not permanent opt-out): while on an excluded host or path no events are sent and no wallet/session cookies are written, and tracking resumes once the SPA navigates to an allowed route. - Add isHostExcluded()/isPathExcluded()/isCurrentEnvironmentExcluded() helpers; isTrackingSuppressed() = opt-out || current-environment-excluded. - Refactor shouldTrack() to reuse the helpers (single source of truth for host/path matching). - Gate the identity-write entry points (identify/connect/detect/ syncWalletState and the EIP-1193 autocapture state sync) on isTrackingSuppressed(), so excluded-page activity is never learned and can't be backfilled into a later allowed-page event. - Two-tier cookie handling: opt-out/timezone are visitor-level and purge the active-wallet cookie; host/path are transient and only skip new writes — a cookie written on an allowed page survives a visit to an excluded route (isPersistedIdentityPurgeRequired()). excludeChains is intentionally left out of suppression: it must keep updating in-memory chain state so currentChainId can gate events and keep disconnect payloads valid. excludeQueryParams stays as event-construction redaction. Both are unchanged. Add tests for excluded host/path (no track, no cookie), the no-delete contract on navigation, and tracking resumption on an allowed path.
…nect/switch syncWalletState() previously returned immediately whenever tracking was suppressed, which skipped disconnects and wallet switches. A wallet learned on an allowed page therefore survived a disconnect/switch observed on an excluded route and could attach to later allowed-page events. Split the suppressed-route behaviour into "never learn new identity" vs "always clear stale identity": - syncWalletState(): on a disconnect (no address) clear the namespace(s); on a switch (different address in the namespace) drop the stale wallet; a fresh connect or same-address re-confirmation stays a no-op. - persistActiveWallet(): only the *write* path is skipped on an excluded host/path; when state has been cleared (no active wallet) the cookie is removed even on an excluded route, so stale identity is purged. Passive navigation onto an excluded route never calls this method, so a valid cookie still survives. - EIP-1193 autocapture switch: clear the stale EVM wallet instead of merely skipping the learn. Covers the Wagmi disconnect/switch path (WagmiEventHandler routes through syncWalletState). Add tests for disconnect and switch observed on an excluded path.
…y gating Add tests for gaps in the suppression coverage: - identify()/detect() write no session markers while on an excluded path - loadActiveWallet() does not restore a snapshot at init on an excluded path but preserves the cookie - loadActiveWallet() purges the snapshot at init for an excluded-timezone visitor
…while suppressed backfillActiveWallet() (used by the autocapture signature/transaction payload builders) and the EIP-1193 connect listener (onConnected) wrote EVM chain state directly, bypassing the suppression-aware path. A signature/transaction or connect observed on an excluded host/path/timezone could therefore populate currentAddress in memory and leak into later allowed-route events, even though the event and cookie write were skipped. - backfillActiveWallet(): no-op while suppressed (it only ever adds an address, so there is no stale state to clear). - onConnected() and onAccountsChanged(): route the suppressed branch through a shared clearStaleEvmWalletOnSwitchWhileSuppressed() helper — never learn, but drop a stale EVM wallet on a switch. onChainChanged is unaffected: it updates only chainId, not the address. Add tests for the backfill path (suppressed no-op + allowed control) and the shared switch-clear helper.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for excluding visitors from tracking based on their IANA timezone. When a visitor's resolved timezone matches one of the configured
excludeTimezones, no events (identify, connect, track, etc.) are enqueued or sent.Key Changes
getTimezone()insrc/utils/timezone.tsthat resolves the current IANA timezone via theIntl.DateTimeFormatAPI with error handlingTrackingOptionsinterface to include optionalexcludeTimezones: string[]field with comprehensive documentation noting this is client-side, best-effort geolocationFormoAnalytics.shouldTrack()that gates all event tracking at the single choke point, ensuring excluded timezones produce no events at allEventFactoryto use the new sharedgetTimezone()utility instead of duplicating the logictest/trackingTimezoneExclusion.spec.ts) covering:Implementation Details
https://claude.ai/code/session_01QoaE5LN9ksZ2eeaxZr1MZc
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.