Product Analytics β
How we answer product questions with data β activation, feature adoption, capture-loop health β without collecting anything we can't defend under GDPR. This page is the working reference; the full strategy lives in the canonical doc below.
Where the strategy + template live β
- Strategy (canonical):
apps/mobile/docs/TRACKING_STRATEGY.mdβ tooling decision (Firebase/GA4), GDPR posture, event taxonomy, phased plan. - Process rule + tracking block:
PLANNING.mdβ Tracking & metrics β the block every user-facing feature spec copies during the Spec phase. - Event catalog (code):
packages/schemas/src/analytics/.
Read the strategy before touching instrumentation; read the PLANNING.md block before writing a feature spec.
Adding a new event β
One shared catalog, imported by both clients. No string literals at call sites β an event absent from the catalog does not ship.
Write a Zod schema for the event in
packages/schemas/src/analytics/(group it in a domain file likeonboarding.ts). Name itobject_action, snake_case, β€40 chars (draft_created,record_viewed). Params are snake_case, β€25 per event.Register it in the
discriminatedUnion('name', [β¦])inpackages/schemas/src/analytics/events.ts. The union is the catalog;AnalyticsEventNameis derived from it, so there's no second list to keep in sync.PII rule β enums, buckets, booleans only. Never email, names, phone, free text (transcripts, notes, chat), precise geo, or anything that identifies a person. Durations and counts go in as buckets (
duration_bucket), not raw values.company_idis allowed (org identifier). See strategy Β§3.4.Emit via the typed wrapper, never
logEventdirectly:- Mobile:
trackAnalyticsEvent({ name, params })from@/lib/analytics(apps/mobile/lib/analytics/track.ts). It validates against the catalog, then checks consent before dispatch. - Web: the matching wrapper over
firebase/analytics(same contract).
The wrapper's argument type comes from the catalog, so an unknown event name or a bad param is a compile error β the catalog is the single source of truth.
- Mobile:
Client intent vs backend outcome β
The client instruments intent (the user did X); the backend instruments outcome (X actually worked). See strategy Β§5.
- Business KPIs are counted server-side only. Never count "records created" or "drafts processed" from client events β they're immune to ad-blockers, offline loss, and consent-revoked-mid-funnel.
- Pipeline outcomes (
draft_processed,record_created) are logged into the backendsrc/user-events/collection, PII-minimal, not GA4. - Client events answer "did the user try / reach X"; server events answer "did X succeed". Keep the split β don't paper over a server truth with a client event.
Verifying an event fires (GA4 DebugView) β
Verify every new event against the tell-ia-dev GA4 property before shipping.
- Enable debug mode on the device/build:
- Android:
adb shell setprop debug.firebase.analytics.app <package>(dev variantcom.tellia.mobile.dev). Unset withadb shell setprop debug.firebase.analytics.app .none.. - iOS: launch with the
-FIRDebugEnabledargument (Xcode scheme β Run β Arguments, or the dev build's launch args). - Web: use the GA Debugger extension, or confirm
debug_modeon the measurement call.
- Android:
- Grant analytics consent in the app (first-run prompt / Settings). No consent β the wrapper drops the event before dispatch and nothing reaches GA4 β this is by design.
- Open GA4 β Admin β DebugView on the
tell-ia-devproperty, trigger the action, and confirm the event name + params appear within a few seconds. Check params are the expected enums/buckets β no ids or free text. screen_viewon mobile is emitted manually per route (hooks/use-screen-tracking.ts); confirmscreen_nameis the route template with ids stripped (/records/[recordId]), never a resolved id.
Worked example β the Β§7 tracking block β
Every user-facing feature spec copies the tracking block from PLANNING.md and fills it in during the Spec phase. A completed example for draft_created (the core capture-loop event):
## Tracking
- **Question this answers:** Are field workers actually capturing drafts, and via which source?
- **Success metric:** β₯60% of weekly-active users create β₯1 draft/week by +30d post-launch.
- **Events:**
| Event | Trigger | Params | Platform |
| --------------- | ------------------------------ | ------------------- | -------- |
| `draft_created` | draft submitted (any source) | `source`, `offline` | M, W |
- **User properties added/changed:** none.
- **PII check:** none β `source` is an enum (chat/map/manual/import/call), `offline` a bool.
- **Catalog updated:** `packages/schemas/src/analytics` (PR link).
- **Where it's read:** GA4 activation funnel (signup β first draft β first record).Note the client/outcome split: draft_created is client intent; whether the draft parsed into a Record is the server-side draft_processed / record_created outcome, counted in user-events, not GA4.
Review gate: a user-facing feature PR without catalog events for its success metric is incomplete β same severity as missing tests.
See also β
- Logging β backend structured logging
- Dashboarding & Alerting β where reports and alerts live