Skip to content

feat(analytics): optional, privacy-first PostHog integration (web + Capacitor) - #156

Draft
majicmaj wants to merge 1 commit into
donetick:developfrom
majicmaj:posthog-analytics
Draft

feat(analytics): optional, privacy-first PostHog integration (web + Capacitor)#156
majicmaj wants to merge 1 commit into
donetick:developfrom
majicmaj:posthog-analytics

Conversation

@majicmaj

Copy link
Copy Markdown

Optional, privacy-first PostHog product analytics

Adds an opt-in PostHog analytics layer for both the web app and the Capacitor mobile app, from a single codebase.

It is disabled by default: with no key set, initAnalytics() returns immediately and the bundler eliminates the posthog-js import entirely (the if (!POSTHOG_KEY) return guard is inlined at build time), so self-hosters ship zero analytics code and make zero network calls. Enabling is just a couple of VITE_ env vars + a rebuild.

What's included

  • src/analytics/analytics.js — a small, no-op-safe wrapper (initAnalytics / identifyUser / resetAnalytics / captureEvent). Nothing else in the app talks to PostHog directly.
  • Automatic SPA pageviews via defaults: '2025-05-24' (capture_pageview: 'history_change'). React Router v6 navigates through the History API, so every route change is captured — no per-route wiring, no double-counting.
  • Identify on login (src/hooks/useAnalyticsIdentity.js, mounted in App.jsx) — fires when the authenticated profile loads, covering password login, OAuth, and refreshed sessions uniformly.
  • Reset on logout (ApiClient.handleLogout) so a shared device doesn't merge two accounts.
  • Env config surfaced through src/Config.js; placeholders added to .env, .env.development, .env.selfhosted.
  • Docs: docs/analytics.md — full setup, reverse proxy, Capacitor notes, privacy, consent.

Privacy-first defaults

person_profiles: 'identified_only' · respect_dnt: true · session-recording inputs masked (maskAllInputs) · replay off on native · localStorage persistence on native (cookies are unreliable on the capacitor:// origin).

Environment variables

Variable Required Applies to Notes
VITE_POSTHOG_KEY to enable web + native Project API Key (phc_...). Absent ⇒ fully disabled.
VITE_POSTHOG_HOST recommended web /ingest (same-origin proxy), managed-proxy domain, or direct cloud host.
VITE_POSTHOG_HOST_NATIVE mobile native Must be absolute — see below.
VITE_POSTHOG_UI_HOST with a proxy both Toolbar/replay links; defaults to https://us.posthog.com.

Vite inlines env vars at build time — rebuild after changing them. The Project API Key is a client-side write-only token, not a secret.

Reverse proxy (avoid ad-blockers)

Ad-blockers drop requests to *.posthog.com. Two documented options:

  • Managed reverse proxy (recommended): free on PostHog Cloud, configured in Org settings → Proxy. Point a neutral subdomain (e.g. e.donetick.com, avoid analytics/ph/tracking) via CNAME. Produces an absolute URL that works for web and mobile → set both VITE_POSTHOG_HOST and VITE_POSTHOG_HOST_NATIVE to it.
  • Self-hosted: ready-to-paste Go net/http/httputil handler (mount /ingest/ on the backend that already serves the SPA) and a Caddy config in the docs. Rules: split /static + /array → assets host, everything else → main host; rewrite the Host header; allow ~64 MB bodies.

Capacitor / mobile edge case ⚠️

Native loads from capacitor://localhost / https://localhostno same-origin backend, so a relative /ingest won't resolve. The wrapper detects Capacitor.isNativePlatform() and uses VITE_POSTHOG_HOST_NATIVE (absolute), falling back safely to the direct cloud host if a relative web host is configured. Uses posthog-js (WebView), not the React Native SDK.

Verification

  • npm run build (keyless): posthog-js not emitted (dead-code-eliminated).
  • npm run build with a dummy key: posthog-js code-split into a lazy ~75 KB (gzip) chunk, loaded on demand.
  • ✅ ESLint clean on all changed files; prettier-formatted.

Follow-ups (out of scope)

  • Optional backend PR to add the /ingest Go reverse-proxy route to donetick/donetick.
  • Explicit opt-in consent banner if a deployment needs strict GDPR (isAnalyticsEnabled is exported to gate it).

🤖 Generated with Claude Code

Adds an opt-in PostHog product-analytics layer that is a complete no-op
unless VITE_POSTHOG_KEY is set — self-hosters ship zero analytics and the
posthog-js library is eliminated from the bundle entirely (dynamic import
behind a build-time-inlined key guard).

- src/analytics/analytics.js: no-op-safe wrapper (init/identify/reset/capture)
  with privacy-first defaults (identified_only, respect_dnt, masked inputs)
- Automatic SPA pageviews via defaults:'2025-05-24' (history_change), so
  react-router navigations are captured without per-route wiring
- Identify on profile load (App.jsx hook), reset on logout (ApiClient)
- Capacitor-aware host resolution: native requires an absolute ingestion URL
  since the WebView has no same-origin backend for a relative /ingest proxy
- Env vars surfaced via Config.js; documented in docs/analytics.md incl.
  managed + self-hosted reverse proxy (Go/Caddy) to dodge ad-blockers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant