88 lines · 8.8 KB

Analytics

gitfor can send product analytics to PostHog: web analytics (pageviews), autocapture, heatmaps, session replay, feature flags and a few server-side events. Like billing, it's optional. With no POSTHOG_KEY, nothing loads: posthog-js isn't downloaded, no server events are sent, and the settings opt-out panel is hidden.

Configuration

Variable Purpose
POSTHOG_KEY Project token (phc_...). Enables analytics. VITE_POSTHOG_PROJECT_TOKEN is accepted as an alias.
POSTHOG_HOST Ingestion host, https://us.i.posthog.com by default (https://eu.i.posthog.com for EU). VITE_POSTHOG_HOST alias.
POSTHOG_PERSON_EMAIL true attaches the signed-in user's email to their PostHog person. Off by default.

These are read at runtime, so self-hosters don't need to rebuild to turn analytics on or off.

Analytics runs only in production. It is off under vite dev (import.meta.env.DEV in the browser, GITFOR_LOCAL=true on the server), whenever PUBLIC_URL is a loopback address, and whenever GITFOR_ENV is set to anything other than production (set GITFOR_ENV=staging on staging deployments).

Ingest proxy

The browser never talks to PostHog directly. posthog-js uses api_host: <PUBLIC_URL>/ingest, and the Worker (src/server/analytics-proxy.ts) forwards /ingest/* to POSTHOG_HOST (/ingest/static/* and /ingest/array/* go to the matching *-assets.i.posthog.com host). Only content type, encoding, accept and user agent headers are forwarded: no cookies, credentials or client IP. Upstream Set-Cookie is dropped. /ingest answers 404 while analytics is off, and ingest is a reserved account name.

The page CSP currently restricts only img-src, where the PostHog host is added when configured. src/server/security-headers.test.ts fails if a future connect-src, script-src or default-src directive leaves the PostHog host out.

Privacy

  • Consent signals. Do Not Track and Global Privacy Control turn analytics off. Each browser can also opt out under Settings -> Account -> Product analytics.
  • Origins. It runs only on the forge's own origin (PUBLIC_URL), never on the editor, user-content, sites or pages origins.
  • No PII by default. People are identified by their opaque account id. Emails are attached only with POSTHOG_PERSON_EMAIL=true. gitfor.dev turns this on. Once on, the email is also added to people who were identified earlier, on their next page load. The project also has IP anonymization on.
  • URLs.
    • Query strings and fragments are always dropped.
    • Repository paths are reduced to /[private]/[private]/<section> unless the page has confirmed the repository is public, so private repository names and file paths never leave the browser.
    • Links to other sites keep only their origin.
  • Autocapture records no element text or attributes.
  • Session replay.
    • All inputs are masked, and all text is masked except on public repository pages (dashboards, profiles, search and notifications list private repository names). data-ph-mask masks an element even there.
    • Replay pauses before navigating to a repository not known to be public, and to settings, admin and sign-in pages.
    • Console logs and network bodies and headers are not captured.
  • Server events carry only enums and counts, never names, emails, subjects or private repository names.

Events

Browser: $pageview, $pageleave, autocapture, heatmaps, web vitals and replay. Pageviews are sent when a navigation resolves, after route loaders have reported whether a repository is public, so a public repository's first pageview is not recorded as [private]. $exception is sent from DefaultCatchBoundary for errors that are not HttpErrors, with only the error type and a scrubbed message (src/lib/scrub-error.ts removes emails, URLs, tokens, paths, owner/repo names and quoted values; no stack).

Signing out, and Sign out everywhere, call posthog.reset(), so the next account in that browser starts with a new anonymous id.

Server (src/server/analytics.ts, sent via waitUntil with a 3 second timeout; failures are swallowed so they can never block or fail a request):

Event Properties
user_signed_up method (password or github)
user_signed_in
org_created org_id
repo_created visibility, owner_kind, init_readme
email_sent outcome (sent, disabled, dev_sink), thread_reply; no person
subscription_changed status, plan
credit_purchased pool, quantity, pack, amount_cents
mergefix_started trigger, mode, private_repo
pull_description_suggested byok
push refs, deletes, job_token, private_repo
pull_opened private_repo, cross_repo
pull_merged method, private_repo, cross_repo
ci_run_started runner_size, event, private_repo, fork (one per job)
ci_run_completed as started, plus status (conclusion) and minutes
issue_created source (web or api), private_repo
abuse_report_created source, category, duplicate, site, paused; no person
ai_usage feature, provider_kind, model, input_tokens, output_tokens, calls
domain_addon_changed status
account_disabled reason
$exception $exception_type, scrubbed $exception_message, $exception_source (worker.fetch, worker.queue); no person

runner_size is the billed size class (standard, large, xlarge, self_hosted), never the raw runs-on labels. For bring-your-own-key AI calls model is byok, because those model ids are free text.

Groups

Events about something an organization owns carry $groups: { org: <org account id> } for PostHog group analytics: pushes, pulls, CI, issues, repositories, Merge Fix, AI usage, billing and org_created. Callers pass { ownerAccountId } to captureServerEvent; the account's kind is looked up in the background and only org accounts become groups. User-owned subjects get no group.

Feature flags

useFeatureFlag("flag-key", fallback) (src/hooks/use-feature-flag.ts) returns the PostHog flag, or fallback while flags load and on instances without analytics. Code must always work with the fallback.

Project settings

The hosted project allows https://gitfor.dev for the toolbar and replay, and has these settings:

  • On: session replay, autocapture, heatmaps, web vitals, performance capture and IP anonymization.
  • Off: console log capture, exception autocapture (exceptions are captured explicitly and scrubbed, see above), and network headers and bodies in replays.

Self-hosters should set their own domain in PostHog's project settings.