diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1d32aa..8e765cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: - name: Typecheck SDK run: pnpm --filter @lucent/sdk typecheck + - name: Analytics privacy and integration tests + run: pnpm test:analytics + - name: Typecheck app run: pnpm --filter @lucent/app typecheck diff --git a/ANALYTICS.md b/ANALYTICS.md new file mode 100644 index 0000000..238f3d2 --- /dev/null +++ b/ANALYTICS.md @@ -0,0 +1,137 @@ +# Tester analytics and launch guide + +Status: integration prepared on `feat/tester-analytics`; collection starts only after configuration, deployment and live verification. No historical visits are backfilled. This work does not activate an analytics account, deploy the application, or establish user traction. + +Public application: **https://www.lucent-pay.xyz/**. Its public response reported Vercel hosting when checked November 9, 2026. The repository also has Cloudflare deployment tooling; do not assume that deploying that Worker updates this public Vercel site. + +## What this measures + +Use Umami for aggregate traffic and explicit product events. The public tracker website identifier is not an API key. No paid subscription, API credential, server database, wallet identifier or new npm dependency is needed by this integration. + +| Question | Dashboard signal | Interpretation | +| --- | --- | --- | +| How much interest is there? | Visitors, visits, pageviews and known routes | Estimated browsers/sessions, not verified people | +| Where do recruited testers come from? | `source` and `campaign` event properties | Only supported, explicitly tagged invitation links | +| Can people connect? | `wallet_connect_started` → `wallet_connected` → `wallet_ready`, filter `mode=manual` | Connection and initial balance refresh are separate stages | +| Are people testing payments? | `registration_confirmed`, `deposit_confirmed`, `transfer_confirmed`, `withdrawal_confirmed` | Browser-reported chain success, not independently audited transactions | +| Is payroll being tried? | `payroll_confirmed` | A confirmed run, not employees/customers acquired | +| Is verification useful? | `disclosure_created`, `disclosure_verified` | Generated/verified disclosure, no amount or proof collected | +| Where is onboarding failing? | `action_failed` filtered by `action`, and `wallet_connect_failed` | Counts failures without raw messages or private context | + +Events can be blocked, lost, repeated, forged or suppressed by privacy controls. Umami visitor identification can group people on a shared network or split one person across devices/identifier rotation. Event totals are not unique testers, unique wallets, durable returning-user counts, or customer counts. New registration events also exclude accounts registered before instrumentation. + +## Account setup + +1. Create an account at [Umami Cloud](https://cloud.umami.is/). The [pricing page](https://umami.is/pricing), checked November 9, 2026, lists a Hobby plan with one website, 100,000 monthly events and six-month retention. Recheck limits when signing up; a paid trial is not required for the initial integration. Pageviews/custom events/properties contribute to usage. +2. Add a website named **Lucent Testnet**, using the domain **www.lucent-pay.xyz**. Copy its Website ID and the script URL supplied in its tracking snippet. Do not paste an API key into any `NEXT_PUBLIC_` variable. +3. Put these **build-time** variables in the hosting project's Production environment: + + ```dotenv + NEXT_PUBLIC_ANALYTICS_ENABLED=true + NEXT_PUBLIC_ANALYTICS_HOSTNAME=www.lucent-pay.xyz + NEXT_PUBLIC_UMAMI_WEBSITE_ID=c8ecbbeb-dea0-41e0-adc3-2a95b80a0f1e + NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://cloud.umami.is/script.js + ``` + + Use your snippet's trusted script URL if different. The Website ID above was supplied from the Lucent Umami tracking snippet; it is public configuration. See [the environment example](packages/app/.env.example). Keep Preview and Development collection disabled. The hostname guard additionally rejects preview domains, localhost and non-HTTPS pages. +4. In the Vercel project serving the public domain, save the variables, deploy the reviewed code and trigger a **new production build**. Setting runtime variables without rebuilding does not update browser-bundled `NEXT_PUBLIC_` values. Use the existing project's deployment process; do not create a second production site just for analytics. +5. Confirm the apex domain either redirects to `www.lucent-pay.xyz` or choose one canonical hostname consistently. The current configuration accepts only the exact configured hostname. Unmatched aliases do not collect. + +The Lucent Website ID was supplied on November 9, 2026 and is now recorded in the environment example and local build configuration. Hosting Production environment configuration, deployment and live dashboard verification are still required. Do not install an extra default tracking snippet: automatic tracking is intentionally disabled in this integration to prevent duplicate pageviews and URL leakage. + +## Privacy and controls + +The code builds a new allowlisted payload for every request. Only known route names, event names, the `testnet` label and selected enum properties can leave this module. Unknown routes become `/other`. Query strings, fragments, referrer URLs, dynamic document titles and arbitrary event properties are dropped. + +**Never add** wallet addresses (including hashed addresses), balances, amounts, salaries, names/emails, transaction hashes, contract IDs, auditor secrets, proof bundles, form contents or raw exception messages to analytics. There is no `identify` call, session replay, automatic error capture, performance capture, click/form autocapture or wallet-to-visitor mapping. + +A third-party tracker still runs on the page when enabled; trust the configured provider and review any future changes. Umami receives normal network request information and processes IP/User-Agent for rotating visitor identification. This integration does not promise anonymity from the provider. See its [metric definitions](https://docs.umami.is/docs/metric-definitions) and [privacy policy](https://umami.is/privacy). Hosting, RPC and blockchain services have separate data visibility. + +The public `/privacy` page explains tracking and provides two per-browser controls: + +- **Disable analytics** for a visitor who opts out. +- **Exclude as team/internal testing** before the owner, collaborators or automation test the public deployment. + +Both stop collection, as do available DNT/GPC browser signals and Umami's `umami.disabled` flag. Storage failure disables collection. Preferences survive reloads without touching wallet state. Do not clear wallet storage to reset analytics. Tracking is also off in development, when configuration is missing/invalid, or on the wrong hostname. + +The app buffers at most 32 allowlisted events while the script loads and discards them on opt-out. Delivery is best effort. Transport errors never fail or delay a financial operation. A script request already made cannot be undone by later opting out. + +## Event dictionary + +| Event | Trigger | Allowed contextual properties | +| --- | --- | --- | +| Pageview (no event name) | Initial page and client-side route changes | `network`, optional `source`/`campaign` | +| `wallet_connect_started` | Explicit connect or automatic reconnect starts | `mode=manual` or `auto` | +| `wallet_connected` | Wallet connection/key setup returns | `mode` | +| `wallet_ready` | Initial wallet state refresh completes | `mode` | +| `wallet_connect_failed` | Connection or initial refresh fails | `mode` | +| `action_started`, `action_failed` | Recognized payment action begins or its UI flow rejects | `action=registration/deposit/merge/transfer/withdrawal/payroll` | +| `registration_confirmed` | A new registration chain invocation succeeds | No account data | +| `deposit_confirmed`, `merge_confirmed` | Respective chain invocation succeeds | No financial data | +| `transfer_confirmed`, `withdrawal_confirmed` | Respective chain invocation succeeds | No financial data | +| `payroll_confirmed` | `execute_run` chain invocation succeeds | No run ID, employee count or salaries | +| `disclosure_created` | Holder generates a disclosure bundle | `direction=sent/received` | +| `disclosure_verified` | Verification returns successfully | No amount, event reference or verifier key | + +Confirmed events are emitted after successful chain submission/polling, before local state persistence. A later local persistence failure can therefore generate both a confirmed event and an action-failure event. These measure different stages; do not subtract every failure from confirmed transactions. A timed-out client may miss a landed transaction. No success is inferred merely from a button click. Existing-account registration is not counted as a new registration. + +## Tagged invitations + +Only these sources are accepted: `stellar`, `scf`, `twitter`, `telegram`, `discord`, `github`, `newsletter`, `community`. Campaigns: `testnet-pilot`, `scf46`. All other values are discarded, including arbitrary referral identifiers. Add a new enum deliberately if needed. + +Example invitation: + +```text +https://www.lucent-pay.xyz/?utm_source=stellar&utm_campaign=testnet-pilot +``` + +Use these as aggregate campaign tags, not per-person links. The tags are recorded only on events whose current URL has those parameters; there is no cross-page campaign persistence. Analyze tagged landing visits separately from later aggregate actions. The properties are custom `source` and `campaign` fields, not automatically populated native UTM reports. Full referrers are deliberately omitted. + +## Production smoke check before recruitment + +1. Set your normal browser to internal exclusion via `/privacy`. Use a **separate non-excluded browser/profile** for a one-time smoke check and record its date so you can exclude that QA window from reporting. Privacy controls or ad blockers may legitimately prevent collection. +2. Open the tagged invitation. In browser Network tools, check that the configured script loads and the collector accepts a **single** pageview. Inspect the outgoing JSON: only allowlisted route/name/properties, no URL query, referrer or sensitive fields. Confirm it appears in Umami. +3. Navigate `/` → `/home` → `/shield` without reloads. Confirm exactly one pageview per navigation. Reload once and verify one additional view. Duplicate snippets must be removed. +4. With disposable Testnet accounts, connect, register if new, obtain the correct test USDC/trustline, deposit, merge, send and withdraw a tiny amount. Verify the relevant events appear **after** on-chain success. Cancel one wallet request and check the failure stage without a confirmed event. No need to exercise unsafe escrow for the initial pilot. +5. Toggle opt-out on `/privacy`, navigate and attempt an action. No subsequent collector events should be sent; the app must still work. Check DNT/GPC and internal exclusion similarly. A preview deployment should never collect into the production property. +6. Repeat one flow from another device/browser with Freighter support. Document failures rather than treating a successful build as browser acceptance. Confirm the deployed `/privacy` notice and control links are available. + +The public site already responds successfully and exposes the proving isolation headers. That is a read-only deployment observation, not a fresh payment acceptance test. Analytics cannot start before the real Website ID is configured and this smoke check passes. + +## Recruitment plan and SCF evidence + +Start with **5–10 invited Testnet testers after the smoke check**, then expand after fixing the first onboarding failures. This is a proposed cohort size, not existing traction. Ask testers to use disposable accounts and no real funds. Explain the public demo auditor and recovery limitations. Start with confidential payments/disclosure; escrow remains a demonstration with its known custody caveat. Preserve local wallet state; reliable full-history recovery is not supplied by analytics. + +For each reporting week, save a dated dashboard export/screenshot with timezone and date range, release commit, campaign label and team/QA exclusions. Confirm export/report features available on your plan before relying on them; take sanitized screenshots if necessary. Keep evidence outside provider retention windows. Do not make a shared dashboard public without checking the displayed properties. + +A useful weekly table: + +| Period (UTC) | Estimated visitors | Manual wallet-ready visitors | Transfer event count / distinct visitors | Payroll event count | Feedback participants | Caveats | +| --- | --- | --- | --- | --- | --- | --- | +| Fill from observed data | — | — | — | — | — | QA exclusions, blockers, Testnet only | + +Where a funnel report is available, use the same date range, visitor unit, filters and order. Otherwise show the counts separately; unrelated daily totals do not establish an ordered conversion funnel. Do not claim retention across identifier rotation or equate connections/reconnects with new people. Keep developer testing and collaborator traffic out of external traction counts. + +Suggested application wording, **only after real data exists**: “During [period], Lucent's Testnet site recorded [N] estimated visitors and [M] visitors with a browser-reported confirmed transfer, excluding [documented internal/QA activity]. [K] people separately provided tester feedback.” Distinguish observations from goals and describe the measurement limits. + +Pair aggregate charts with reproducible Testnet transaction evidence from consenting pilot participants, issue-resolution notes and anonymized usability findings. Record consented feedback separately from telemetry and do not silently join browser identities to public wallet addresses. Public transaction evidence is not proof of unique people, paid customers, revenue, Mainnet use or security review. + +## Developer checks + +```bash +pnpm build:sdk +pnpm test:analytics +pnpm --filter @lucent/app typecheck +pnpm --filter @lucent/app build +``` + +The automated analytics test uses a fake tracker and mocked chain; it sends no live telemetry or transactions. It covers payload sanitization, environment/hostname gates, DNT/GPC, opt-out/internal exclusion, route deduplication, bounded queue, failure isolation, and rejected-versus-confirmed submission handling. No tests authenticate to the analytics dashboard. Production collection remains unverified until the smoke check above. + +Implementation references: `packages/app/lib/analytics.ts`, `components/analytics/Analytics.tsx`, `lib/wallet.ts`, `lib/wallet-context.tsx`, and `/privacy`. Official setup references: [tracker configuration](https://docs.umami.is/docs/tracker-configuration), [custom payloads](https://docs.umami.is/docs/tracker-functions), [installation](https://docs.umami.is/docs/collect-data). + +## Local verification record — November 9, 2026 + +- Analytics privacy/confirmation tests passed with an in-memory fake tracker and rejected/pending/successful submission promises. +- App TypeScript check and production build passed with a dummy public Website ID. +- An isolated headless Chrome test of that production build passed initial and client-side pageviews, sensitive query exclusion, opt-out/re-enable, team exclusion across reload, DNT/GPC and preview-domain exclusion. The test routed the site to a local server and replaced the analytics script with a fake collector; RPC and other external services were blocked. No real analytics or payment transactions were sent. +- No live Umami dashboard or production payment smoke test was performed. The production activation checklist above remains required. diff --git a/README.md b/README.md index 82d0a83..11a911c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,10 @@ wallet integration. - [Acknowledgments](#acknowledgments) - [License](#license) +## Tester analytics + +Traffic and explicit Testnet action tracking are prepared through Umami, disabled until production configuration is supplied. See [ANALYTICS.md](ANALYTICS.md) for account setup, privacy controls, launch checks and honest SCF evidence reporting. + ## Quickstart Requires **Node ≥ 20**, **pnpm 10**. For contract work you'll also need Rust diff --git a/package.json b/package.json index 7766315..c787709 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "deploy:contracts": "pnpm --filter @lucent/sdk exec tsx ../../scripts/deploy.ts", "deploy:app": "pnpm --filter @lucent/app run deploy:cf", "e2e": "pnpm --filter @lucent/sdk exec tsx ../../scripts/e2e.ts", - "e2e:disclosure": "pnpm --filter @lucent/sdk exec tsx ../../scripts/e2e-disclosure.ts" + "e2e:disclosure": "pnpm --filter @lucent/sdk exec tsx ../../scripts/e2e-disclosure.ts", + "test:analytics": "pnpm --filter @lucent/sdk exec tsx --tsconfig ../../packages/app/tsconfig.json ../../packages/app/test/analytics.mjs" }, "license": "MIT", "pnpm": { diff --git a/packages/app/.env.example b/packages/app/.env.example new file mode 100644 index 0000000..3d6bca0 --- /dev/null +++ b/packages/app/.env.example @@ -0,0 +1,8 @@ +# Analytics is off unless explicitly enabled. These values are public, NOT secrets. +NEXT_PUBLIC_ANALYTICS_ENABLED=false +# Exact production hostname, no https://, port or path. Preview/local hosts are excluded. +NEXT_PUBLIC_ANALYTICS_HOSTNAME=www.lucent-pay.xyz +# Website ID from your Umami tracking snippet (not an API key). +NEXT_PUBLIC_UMAMI_WEBSITE_ID=c8ecbbeb-dea0-41e0-adc3-2a95b80a0f1e +# Copy the script URL supplied by Umami. Change only for a trusted instance. +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://cloud.umami.is/script.js diff --git a/packages/app/README.md b/packages/app/README.md index 1d807de..026159f 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -69,3 +69,7 @@ The fix is already in place: 1. `scripts/vendor-bb.mjs` (run by `predev`/`prebuild`) copies bb.js's `dest/browser/` into `public/vendor/bb/` (git-ignored, regenerated each build). 2. The webpack client config aliases the bare `@aztec/bb.js` specifier to `false`. 3. `lib/bb-loader.ts` overrides the SDK's `setUltraHonkBackendLoader` to import `/vendor/bb/index.js` as **native ESM** from that stable path at runtime. The loader's `new Function` must stay lazy — Cloudflare Workers forbid eval at the top level. + +## Tester analytics + +See [the analytics setup and recruitment guide](../../ANALYTICS.md). Collection is off by default, enabled only for the configured HTTPS production hostname. Public settings and the notice live at `/privacy`. Never add a second automatic tracker snippet or send wallet/payment data to analytics. diff --git a/packages/app/app/home/disclose-flow.tsx b/packages/app/app/home/disclose-flow.tsx index 0ba4f01..8546961 100644 --- a/packages/app/app/home/disclose-flow.tsx +++ b/packages/app/app/home/disclose-flow.tsx @@ -12,6 +12,7 @@ import { useCallback, useState } from "react"; import type { TransferEvent, DisclosureRequest } from "@lucent/sdk"; import type { ConfidentialWallet } from "@/lib/wallet"; +import { trackEvent } from "@/lib/analytics"; import { errMsg } from "@/lib/err"; import { Button } from "@/components/ui/Button"; import { Textarea } from "@/components/ui/Textarea"; @@ -52,6 +53,7 @@ export function DiscloseFlow({ const request = parseDisclosureRequest(requestJson); const bundle = direction === "received" ? await wallet.discloseReceived(ev, request) : await wallet.discloseSent(ev, request); + trackEvent("disclosure_created", { direction }); setBundleJson(JSON.stringify(bundle, null, 2)); } catch (e) { setError(errMsg(e)); diff --git a/packages/app/app/home/home-client.tsx b/packages/app/app/home/home-client.tsx index 33580bb..773e0f6 100644 --- a/packages/app/app/home/home-client.tsx +++ b/packages/app/app/home/home-client.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, type ComponentType } from "react"; import Link from "next/link"; +import { trackEvent } from "@/lib/analytics"; import { useRouter } from "next/navigation"; import { motion } from "framer-motion"; import { @@ -359,6 +360,7 @@ export function HomeClient() { } async function handleRegister() { + trackEvent("action_started", { action: "registration" }); setError(null); setRegisterSteps([{ id: "register", label: "Prove key ownership", status: "active", estSeconds: 4 }]); setRegistering(true); @@ -367,6 +369,7 @@ export function HomeClient() { setRegisterSteps((s) => s.map((x) => ({ ...x, status: "done" }))); await refresh(); } catch (e) { + trackEvent("action_failed", { action: "registration" }); setError(errMsg(e)); setRegisterSteps((s) => s.map((x) => ({ ...x, status: "error" }))); } finally { diff --git a/packages/app/app/layout.tsx b/packages/app/app/layout.tsx index aaf8a1c..51eefae 100644 --- a/packages/app/app/layout.tsx +++ b/packages/app/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from "next"; import "./globals.css"; +import { Analytics } from "@/components/analytics/Analytics"; import { WalletProvider } from "@/lib/wallet-context"; export const metadata: Metadata = { @@ -20,6 +21,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( + {children} diff --git a/packages/app/app/page.tsx b/packages/app/app/page.tsx index 40f3377..bc9bb29 100644 --- a/packages/app/app/page.tsx +++ b/packages/app/app/page.tsx @@ -429,7 +429,10 @@ export default function LandingPage() {

The amount is the only secret

-
+
+ + Privacy + Stats diff --git a/packages/app/app/privacy/page.tsx b/packages/app/app/privacy/page.tsx new file mode 100644 index 0000000..f486371 --- /dev/null +++ b/packages/app/app/privacy/page.tsx @@ -0,0 +1,42 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { analyticsConfig, analyticsPreferences, setAnalyticsPreference, ANALYTICS_CHANGE_EVENT } from "@/lib/analytics"; + +export default function PrivacyPage() { + const [prefs, setPrefs] = useState | null>(null); + const [error, setError] = useState(false); + useEffect(() => { + const update = () => setPrefs(analyticsPreferences()); + update(); + window.addEventListener(ANALYTICS_CHANGE_EVENT, update); + window.addEventListener("storage", update); + return () => { window.removeEventListener(ANALYTICS_CHANGE_EVENT, update); window.removeEventListener("storage", update); }; + }, []); + function toggle(kind: "disabled" | "internal", value: boolean) { + setError(!setAnalyticsPreference(kind, value)); + } + return
+ ← Back to Lucent +

Privacy & analytics

+

Lucent is an unaudited Stellar Testnet application. Our demo auditor key is public. Use disposable test accounts and test assets.

+

What we measure

+

When configured for the public deployment, Umami measures page visits and named actions such as connecting a wallet, completing a Testnet transfer, or verifying a disclosure. This helps us improve onboarding and report early testing results. Visitor estimates are not verified people or customer counts.

+

Our analytics payload contains a known page name, event name, Testnet label, and optional allowlisted campaign source. We do not send wallet addresses, amounts, balances, transaction hashes, auditor keys, proofs, form input, raw errors, query strings, URL fragments, or referrer URLs. We do not enable session recording, autocapture or wallet-based identification.

+

Umami receives the network request and processes IP/browser information to estimate visitors using rotating identifiers. We do not add a persistent user ID. This is aggregate product analytics, not anonymity from the analytics service. See Umami's privacy policy. Retention depends on the selected plan; the initial rollout is intended to use the six-month Hobby plan.

+

Your controls

+

Opting out affects this browser only. Do Not Track and Global Privacy Control are also respected when exposed by your browser. Clearing browser storage resets these preferences and can also destroy confidential wallet recovery state—do not clear it just to change analytics settings.

+
+

{analyticsConfig() ? "Analytics is configured for the production hostname." : "Analytics is not configured in this build."}

+ {prefs && <> + {prefs.browserOptOut &&

Your browser privacy signal currently disables analytics.

} + + + } + {error &&

This browser could not save the preference. Analytics stays disabled when storage is unavailable.

} +
+

Separate network services

+

Your wallet, Stellar RPC, hosting provider and public blockchain process their own requests. Public transactions still expose participants and metadata. Turning off product analytics does not hide blockchain activity or remove infrastructure logs.

+
; +} diff --git a/packages/app/app/profile/page.tsx b/packages/app/app/profile/page.tsx index bf89132..1b051f4 100644 --- a/packages/app/app/profile/page.tsx +++ b/packages/app/app/profile/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { Copy, Check, LogOut, Activity, BookOpen, Info, RefreshCw } from "lucide-react"; import { AppShell } from "@/components/layout/AppShell"; @@ -104,6 +105,8 @@ export default function ProfilePage() {
+ Privacy & analytics settings + Protocol
diff --git a/packages/app/app/verify/verify-panel.tsx b/packages/app/app/verify/verify-panel.tsx index 0762735..9ce02e0 100644 --- a/packages/app/app/verify/verify-panel.tsx +++ b/packages/app/app/verify/verify-panel.tsx @@ -7,6 +7,8 @@ * binding are all re-read from the chain, never trusted from the bundle. */ +import { trackEvent } from "@/lib/analytics"; + import { useCallback, useEffect, useState } from "react"; import { ChainClient, @@ -112,6 +114,7 @@ export function VerifyPanel() { pinnedVk: vkBytes(artifacts.vk.vkBase64), }), ); + trackEvent("disclosure_verified"); } finally { await prover.destroy(); } diff --git a/packages/app/components/analytics/Analytics.tsx b/packages/app/components/analytics/Analytics.tsx new file mode 100644 index 0000000..221d3bb --- /dev/null +++ b/packages/app/components/analytics/Analytics.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Script from "next/script"; +import { usePathname } from "next/navigation"; +import { ANALYTICS_CHANGE_EVENT, analyticsAllowed, analyticsConfig, flushAnalytics, trackPageview } from "@/lib/analytics"; + +export function Analytics() { + const pathname = usePathname(); + const [enabled, setEnabled] = useState(false); + useEffect(() => { + const update = () => setEnabled(analyticsAllowed()); + update(); + window.addEventListener(ANALYTICS_CHANGE_EVENT, update); + window.addEventListener("storage", update); + return () => { window.removeEventListener(ANALYTICS_CHANGE_EVENT, update); window.removeEventListener("storage", update); }; + }, []); + useEffect(() => { + if (enabled) trackPageview(pathname); + }, [pathname, enabled]); + const cfg = analyticsConfig(); + if (!enabled || !cfg) return null; + return