Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions apps/web/content/docs/developers/12.analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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`)

Expand All @@ -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. |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -126,21 +135,22 @@ 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)

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

Expand Down Expand Up @@ -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`.
Expand All @@ -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`.
6 changes: 3 additions & 3 deletions apps/web/content/handbook/how-we-work/8.analytics-funnel.mdx
Original file line number Diff line number Diff line change
@@ -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 ]
Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions apps/web/content/legal/cookies.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
date: "2026-03-17"
date: "2026-03-31"
title: "Cookie Policy"
summary: "How we use cookies and similar tracking technologies"
---
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/web/content/legal/dpa.mdx
Original file line number Diff line number Diff line change
@@ -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"
---
Expand Down Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions apps/web/content/legal/privacy.mdx
Original file line number Diff line number Diff line change
@@ -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"
---
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions apps/web/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -54,6 +97,7 @@ function ConsentAwareProviders({
<PostHogProvider enabled={analyticsEnabled}>
<QueryClientProvider client={queryClient}>
{children}
<MaybeGoogleAnalytics enabled={analyticsEnabled} />
<MaybeZendeskWidget enabled={analyticsEnabled} />
</QueryClientProvider>
</PostHogProvider>
Expand Down