Frontend Tracking
For user and UX analytics, the frontend sends events through a single GraphQL mutation, and the shared hookuseFrontendTracking handles payload shaping and deduping.
How it works
This is the shortest mental model that stays correct as you add new tracking:1) Call useFrontendTracking() in your component
Example:
2) The hook dedupes events locally
useFrontendTracking builds a dedupe key and keeps a bounded cache in a Map so repeated events within a short window are ignored.
That logic lives entirely in:
apps/portal/src/components/hooks/useFrontendTracking.ts
3) The mutation contract stays stable
Events are sent using the generated mutation hook:useTrackFrontendEventMutation
The mutation document is:
apps/portal/src/gql/mutations/trackFrontendEvent.gql.ts
4) Consent gate runs before emission
Tracking honors consent before any event is sent.useFrontendTracking checks gdpr_consent through getGdprConsent() and treats unknown consent as denied.
When consent is denied, the hook skips trackFrontendEvent entirely.
📖 See also: GDPR Consent Flow for frontend-to-backend enforcement boundaries.
Extending this system
Use this checklist when you add new UX tracking from components.- Import and initialize the tracking hook in your component:
-
Call
track(payload)with:eventDomain: one of"auth" | "billing" | "navigation"eventName: your specific action string- optional
properties: a string-to-string map
-
If you need a new
eventDomain, update:FrontendTrackingDomainTypeinapps/portal/src/components/hooks/useFrontendTracking.ts- the mapping to
FrontendEventDomainEnumin the same file
- Verify:
What not to do
These anti-patterns break dedupe or reintroduce fragmented analytics paths. ❌ Never send analytics SDK calls directly from the browser. ❌ Never send high-cardinality data as tag-like strings; prefer bucketing on the backend analytics pipeline. ❌ Never bypass the dedupe helper by reimplementing it in each component.File conventions
This is where tracking code should live so it stays easy to audit. Prefer:- small, named events (
eventName) instead of free-form long strings - short
propertiesonly when they help debug or understand funnels
