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 throughtrackEvent() 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
awaitthat succeeded. - When the next line is a full-page navigation (
location.assign), usetrackEventAfterNavigation(). The event is stored insessionStorageand 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.
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_idis the internal user UUID.setGaUserId()sets it fromAuthContextand 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 ownclient_idcounts visitors. purchaserevenue is sent from the browser, so anyone can spoof it. The billing ledger is the source of truth for revenue.transaction_idis 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_locationandpage_referrerreduced 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.
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:
- Turn off Enhanced measurement → Page views → Page changes based on browser history events. The app sends its own page views.
- 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,queryandkeywordURL parameters — the app uses?q=and?search=for search boxes. - Turn on Redact data → Query parameters for
token,returnUrl,session_id,q,s,search,queryandkeyword.
docs/architecture/ANALYTICS_TRACKING.md.
