Skip to main content

Overview

The web app (apps/web) sends Google Analytics 4 page views and a fixed set of product events. They measure the growth funnel: sign up → verify email → agent created → agent published → public chat → purchase GA4 is off unless a superadmin sets a measurement id in System Config. It does not replace KB analytics, the chat timing framework or the billing ledger. Use the ledger for revenue and credits: browser analytics drop hits. The full taxonomy — every event, its params and the file it fires from — is in the repository at docs/architecture/ANALYTICS_TRACKING.md.

Sending an event

Every event goes through trackEvent() in apps/web/src/lib/ga.ts. The GaEvent union types each event name to its params, so an unknown event or a wrong param is a compile error.
  • Call it once, at the existing success point, after the await that succeeded.
  • When the next line is a full-page navigation (location.assign), use trackEventAfterNavigation(). The event is stored in sessionStorage and the next page sends it.
  • Before the id arrives from /api/client-config, calls are queued (cap 50). Without an id, during SSR and on /embed/*, nothing is sent. A call never throws.
To add an event: add a member to GaEvent, add the call, and add a row to the taxonomy table in ANALYTICS_TRACKING.md.

Events

Mark sign_up, agent_created, public_chat_started and purchase as GA4 key events.

Privacy

  • Never send an email, a name, message content, prompt text, a document name or a URL with a query string.
  • user_id is the internal user UUID. setGaUserId() sets it from AuthContext and clears it on logout.
  • Never send the anonymous visitor id (anonymousId) in any param. It is a bearer credential: the public transcript endpoints accept it as their only access check, so a GA row carrying it would let anyone with GA access read that visitor’s transcripts. GA’s own client_id counts visitors.
  • purchase revenue is sent from the browser, so anyone can spoof it. The billing ledger is the source of truth for revenue. transaction_id is the Stripe Checkout session id; GA uses it to de-duplicate purchases. It is not a credential: no app endpoint accepts it.
  • Page views are sent by the app with page_location and page_referrer reduced to origin + path, because sign-in, invitation and password-reset tokens travel in query strings.
  • GA never runs on the embed widget (public/embed.js, /embed/*), which renders on customer sites.
There is no consent banner and no GA4 Consent Mode yet. That is a pending product decision, and it must be settled before GA runs for EU/UK visitors on the live host.

Setup

Each environment has its own GA4 web stream. A superadmin sets its measurement id in Admin → System Config → Analytics (GA_MEASUREMENT_ID). The value is stored in the platform config, not in .env, and needs no rebuild. The web app reads it from the public GET /api/client-config once per page load, so a change takes effect within two minutes (the server caches the value for 60 seconds, and browsers cache the response for 60 seconds). A value that is not G- followed by 4 to 20 capital letters or digits is rejected on save. Clear the value to turn GA off. In each GA4 web stream:
  1. Turn off Enhanced measurement → Page views → Page changes based on browser history events. The app sends its own page views.
  2. Turn off Enhanced measurement → Site search, Form interactions and Outbound clicks. gtag.js runs them outside the app’s URL sanitiser, and Site search reads the q, s, search, query and keyword URL parameters — the app uses ?q= and ?search= for search boxes.
  3. Turn on Redact data → Query parameters for token, returnUrl, session_id, q, s, search, query and keyword.
The per-environment measurement ids and the full checklist are in docs/architecture/ANALYTICS_TRACKING.md.