From 5eec594bc42d51b6d2b7fa7ac20b414a0d95c565 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Tue, 31 Mar 2026 13:33:26 -0700 Subject: [PATCH] chore: add Google Analytics to website Load Google Analytics only after website analytics consent, keep admin routes excluded, and update the related legal and analytics documentation. --- .../content/docs/developers/12.analytics.mdx | 22 +++++++--- .../how-we-work/8.analytics-funnel.mdx | 6 +-- apps/web/content/legal/cookies.mdx | 6 +-- apps/web/content/legal/dpa.mdx | 3 +- apps/web/content/legal/privacy.mdx | 4 +- apps/web/src/router.tsx | 44 +++++++++++++++++++ 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/apps/web/content/docs/developers/12.analytics.mdx b/apps/web/content/docs/developers/12.analytics.mdx index 1b9c386a32..d647ab65ca 100644 --- a/apps/web/content/docs/developers/12.analytics.mdx +++ b/apps/web/content/docs/developers/12.analytics.mdx @@ -6,9 +6,9 @@ description: "Learn about analytics in Char" ## Scope -This is a code-derived inventory of what Char sends to PostHog across: +This is a code-derived inventory of Char's analytics integrations. Most product and backend analytics go to PostHog. The website also loads Google Analytics (GA4) for consented browser traffic. -- `apps/web` (browser events) +- `apps/web` (browser events and consented website analytics) - `apps/desktop` + `plugins/analytics` (desktop events) - `apps/api` + proxy/subscription crates (server-side events) @@ -22,6 +22,13 @@ This is a code-derived inventory of what Char sends to PostHog across: - Enabled options: - `autocapture: true` - `capture_pageview: true` +- Google Analytics (GA4) is initialized in `apps/web/src/router.tsx` only when: + - website analytics consent is enabled + - build is not dev (`!import.meta.env.DEV`) + - pathname does not start with `/admin` +- Implementation: + - loads `https://www.googletagmanager.com/gtag/js?id=G-4CDGPKJ8JB` + - calls `gtag("config", "G-4CDGPKJ8JB")` ### Desktop (`apps/desktop` + `plugins/analytics`) @@ -41,6 +48,7 @@ This is a code-derived inventory of what Char sends to PostHog across: |--------|-------------|-------------------| | Desktop custom events | Machine fingerprint (`hypr_host::fingerprint()`) | `identify(userId, payload)` sends PostHog `$identify` with `$anon_distinct_id` = machine fingerprint. | | Web custom/autocapture events | PostHog browser distinct ID | Auth callback calls `posthog.identify(userId, { email })`. | +| Web Google Analytics | GA4 browser client ID | No explicit identify call; website traffic remains anonymous in GA4. | | API `$ai_generation` | `x-device-fingerprint` if present, else `generation_id` | Optional `user_id` also included as event property. | | API `$stt_request` | `x-device-fingerprint` if present, else random UUID | Optional `user_id` also included as event property. | | API trial events | `x-device-fingerprint` if present (desktop), else authenticated `user_id` | `user_id` is included as an event property when distinct ID is fingerprint. No separate `$identify` call here. | @@ -74,6 +82,7 @@ This enrichment applies to desktop frontend events and Rust plugin `event_fire_a Notes: - PostHog autocapture and pageview are also on (production only), so PostHog default browser events are collected in addition to the custom events above. +- Google Analytics is also loaded for consented non-admin web sessions, but there are no custom GA events emitted in app code today. - Web auth callback calls `identify(userId, { email })` in `apps/web/src/routes/_view/callback/auth.tsx`. ### Desktop product events @@ -126,7 +135,7 @@ Notes: ## User journey funnel -The user lifecycle is divided into 8 stages. Each stage lists the PostHog events that measure it, how identity linking works at that point, and known gaps. +The user lifecycle is divided into 8 stages. Each stage lists the analytics signals that measure it, how identity linking works at that point, and known gaps. ### Stage 1: Acquisition (website visits) @@ -134,13 +143,14 @@ Goal: measure traffic to the website. | Event | Properties | Notes | |-------|------------|-------| +| Google Analytics pageview/session tracking | automatic | Standard GA4 browser analytics on consented non-admin web sessions. | | PostHog autocapture | automatic | Page clicks, form interactions. Production only. | | PostHog pageview | automatic | Every page load. Production only. | | `hero_section_viewed` | `timestamp` | Explicit signal that a visitor saw the main landing section. | | `reminder_requested` | `platform`, `timestamp`, `email` | Mobile app waitlist signup. | | `os_waitlist_joined` | `platform`, `timestamp`, `email` | Desktop waitlist signup. | -Identity: anonymous PostHog browser distinct ID. No user identity yet. +Identity: anonymous browser identifiers in PostHog and GA4. No user identity yet. ### Stage 2: Converting visits to installs @@ -317,7 +327,8 @@ PostHog user properties are set via `$set`, `$set_once`, and `$identify` payload - Source: `plugins/analytics/src/lib.rs`, `crates/analytics/src/lib.rs`. - Web: - PostHog is not initialized in dev mode. - - Source: `apps/web/src/providers/posthog.tsx`. + - Google Analytics is initialized only after website analytics consent is granted, only outside `/admin`, and not in dev mode. + - Source: `apps/web/src/providers/posthog.tsx`, `apps/web/src/router.tsx`. - API: - PostHog client is active only in non-debug builds (production requires `POSTHOG_API_KEY`). - Source: `apps/api/src/main.rs`. @@ -341,5 +352,6 @@ If a feature uses `FlagStrategy::Posthog(key)`, the check resolves via `is_featu - `analyticsCommands.identify(` - `AnalyticsPayload::builder("...`)` - `posthog.capture(` / `posthog.identify(` + - `gtag(` / `googletagmanager.com/gtag/js` 2. Verify payload keys at each callsite (watch for nested objects like `properties: {...}`). 3. Re-run this inventory after any analytics refactor in `plugins/analytics`, `crates/analytics`, `crates/llm-proxy`, `crates/transcribe-proxy`, or `crates/api-subscription`. diff --git a/apps/web/content/handbook/how-we-work/8.analytics-funnel.mdx b/apps/web/content/handbook/how-we-work/8.analytics-funnel.mdx index d048eb1274..e70d929e8f 100644 --- a/apps/web/content/handbook/how-we-work/8.analytics-funnel.mdx +++ b/apps/web/content/handbook/how-we-work/8.analytics-funnel.mdx @@ -1,10 +1,10 @@ --- title: "Analytics Funnel" section: "How We Work" -summary: "The 8-stage user journey we track with PostHog, from first website visit to retained paying user" +summary: "The 8-stage user journey we track across consented website analytics and PostHog, from first website visit to retained paying user" --- -We track the user lifecycle as an 8-stage funnel. Each stage has key events that tell us whether users are progressing. For implementation details and the full event catalog, see the [developer analytics docs](/docs/developers/analytics). +We track the user lifecycle as an 8-stage funnel. Website acquisition uses consented website analytics on the web, and product funnel events live in PostHog. For implementation details and the full event catalog, see the [developer analytics docs](/docs/developers/analytics). ``` [ 1. Acquisition ] @@ -19,7 +19,7 @@ We track the user lifecycle as an 8-stage funnel. Each stage has key events that ## 1. Acquisition (website visits) -Standard PostHog pageview and session tracking. +Standard consented website analytics tracking, plus PostHog pageview and session tracking. ## 2. Converting visits to app installs diff --git a/apps/web/content/legal/cookies.mdx b/apps/web/content/legal/cookies.mdx index dfc8d657a3..5d6be3cac2 100644 --- a/apps/web/content/legal/cookies.mdx +++ b/apps/web/content/legal/cookies.mdx @@ -1,5 +1,5 @@ --- -date: "2026-03-17" +date: "2026-03-31" title: "Cookie Policy" summary: "How we use cookies and similar tracking technologies" --- @@ -47,7 +47,7 @@ We do not currently use third-party advertising or retargeting cookies on Char-o We may allow trusted third parties to place cookies on your device. These third parties include: -- **Analytics Providers:** PostHog +- **Analytics Providers:** PostHog, Google Analytics - **Authentication & Account Services:** Supabase - **Security & Infrastructure Providers:** Cloudflare, Netlify - **Payment Processors:** Stripe (when using checkout/billing flows) @@ -79,7 +79,7 @@ You can opt out of certain cookie-based tracking: - **Cookie preferences:** Use the `Cookie preferences` control in the website footer to accept or reject non-essential web tracking. - **Browser controls:** Block or clear cookies/local storage from your browser settings. -- **Analytics providers:** Block analytics domains in your browser or content blocker settings (for example, PostHog domains). +- **Analytics providers:** Block analytics domains in your browser or content blocker settings (for example, PostHog, `google-analytics.com`, or `googletagmanager.com`). - **Desktop app telemetry:** Use the `Share usage data` toggle in desktop settings (desktop telemetry is not cookie-based). ### 6.3 Impact of Disabling Cookies diff --git a/apps/web/content/legal/dpa.mdx b/apps/web/content/legal/dpa.mdx index ca899dc91c..8f8fc840b4 100644 --- a/apps/web/content/legal/dpa.mdx +++ b/apps/web/content/legal/dpa.mdx @@ -1,5 +1,5 @@ --- -date: "2026-03-03" +date: "2026-03-31" title: "Data Processing Agreement" summary: "Our commitment to protecting your data in compliance with GDPR, CCPA, and other data protection laws" --- @@ -133,6 +133,7 @@ We implement the following technical and organizational measures: | -------------------------------------- | -------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Sentry](https://sentry.io/) | For logging errors | USA | GDPR/CCPA aligned DPA, prohibits sensitive data, deletion on request, customer audit rights | | [PostHog](https://posthog.com/) | For product analytics, feature usage telemetry, and cloud request metadata analytics | USA | SOC 2 Type II, GDPR/CCPA-aligned DPA, strict use‑only data purpose clauses, hardware MFA, public audit transparency | +| [Google Analytics](https://marketingplatform.google.com/about/analytics/) | For consented website analytics on Char-owned web properties | USA | Google Analytics data processing terms, configurable retention controls, encrypted transport, and account access controls | | [AWS](https://aws.amazon.com) | For integrating with 3rd party apps like GCal or Outlook | USA | Encryption in transit & at rest, strict access controls, incident response plans, privacy‑focused vendor management, confidentiality contracts, staff training | | [Nango](https://www.nango.dev/) | For OAuth connection management, credential storage, and sync infrastructure for connected services such as Google Calendar | USA | SOC 2 Type II, GDPR compliant, HIPAA compliant, AES-256-GCM encryption at rest for credentials, TLS 1.2+ in transit, 31-day default retention after deletion | | [Keygen](https://keygen.sh) | For issuing licenses for Char Pro | USA | Strong MFA, cryptographically signed APIs/licenses, automated vulnerability scanning, penetration testing, GDPR DSR support | diff --git a/apps/web/content/legal/privacy.mdx b/apps/web/content/legal/privacy.mdx index aacfde966b..ec31503651 100644 --- a/apps/web/content/legal/privacy.mdx +++ b/apps/web/content/legal/privacy.mdx @@ -1,5 +1,5 @@ --- -date: "2026-03-24" +date: "2026-03-31" title: "Privacy Policy" summary: "How we collect, use, and protect your personal information" --- @@ -82,7 +82,7 @@ We may share your information with third-party service providers who perform ser - **Cloud hosting and infrastructure providers** (e.g., Fly.io, Netlify, Cloudflare, Supabase) for hosting our application and storing your data securely - **Integration providers** (e.g., Nango) for OAuth connection management, credential handling, and calendar sync infrastructure when you connect Google Calendar or other supported accounts - **Payment processors** (e.g., Stripe) for processing payments securely -- **Analytics services** (e.g., PostHog) for understanding how users interact with our Service +- **Analytics services** (e.g., PostHog, Google Analytics) for understanding how users interact with our Service - **Speech-to-text providers** (e.g., Deepgram, AssemblyAI, Soniox) for cloud-based transcription when you enable this feature - **AI model providers** (e.g., via OpenRouter) for cloud-based AI features when you enable them - **Error monitoring services** (e.g., Sentry) for identifying and fixing issues diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx index 29c1b7015c..dfaf292b66 100644 --- a/apps/web/src/router.tsx +++ b/apps/web/src/router.tsx @@ -15,6 +15,49 @@ import { routeTree } from "./routeTree.gen"; const ZENDESK_SNIPPET_ID = "ze-snippet"; const ZENDESK_SNIPPET_SRC = "https://static.zdassets.com/ekr/snippet.js?key=15949e47-ed5a-4e52-846e-200dd0b8f4b9"; +const GOOGLE_TAG_ID = "google-tag"; +const GOOGLE_ANALYTICS_ID = "G-4CDGPKJ8JB"; + +type AnalyticsWindow = Window & + typeof globalThis & { + dataLayer?: unknown[]; + gtag?: (...args: unknown[]) => void; + }; + +function MaybeGoogleAnalytics({ enabled }: { enabled: boolean }) { + useEffect(() => { + if ( + typeof document === "undefined" || + import.meta.env.DEV || + !enabled || + window.location.pathname.startsWith("/admin") + ) { + return; + } + + if (document.getElementById(GOOGLE_TAG_ID)) { + return; + } + + const analyticsWindow = window as AnalyticsWindow; + analyticsWindow.dataLayer = analyticsWindow.dataLayer ?? []; + analyticsWindow.gtag = + analyticsWindow.gtag ?? + function gtag(...args) { + analyticsWindow.dataLayer?.push(args); + }; + analyticsWindow.gtag("js", new Date()); + analyticsWindow.gtag("config", GOOGLE_ANALYTICS_ID); + + const script = document.createElement("script"); + script.id = GOOGLE_TAG_ID; + script.src = `https://www.googletagmanager.com/gtag/js?id=${GOOGLE_ANALYTICS_ID}`; + script.async = true; + document.head.appendChild(script); + }, [enabled]); + + return null; +} function MaybeZendeskWidget({ enabled }: { enabled: boolean }) { useEffect(() => { @@ -54,6 +97,7 @@ function ConsentAwareProviders({ {children} +