Skip to content

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 ​

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.

  1. Write a Zod schema for the event in packages/schemas/src/analytics/ (group it in a domain file like onboarding.ts). Name it object_action, snake_case, ≀40 chars (draft_created, record_viewed). Params are snake_case, ≀25 per event.

  2. Register it in the discriminatedUnion('name', […]) in packages/schemas/src/analytics/events.ts. The union is the catalog; AnalyticsEventName is derived from it, so there's no second list to keep in sync.

  3. 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_id is allowed (org identifier). See strategy Β§3.4.

  4. Emit via the typed wrapper, never logEvent directly:

    • 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.

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 backend src/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.

  1. Enable debug mode on the device/build:
    • Android: adb shell setprop debug.firebase.analytics.app <package> (dev variant com.tellia.mobile.dev). Unset with adb shell setprop debug.firebase.analytics.app .none..
    • iOS: launch with the -FIRDebugEnabled argument (Xcode scheme β†’ Run β†’ Arguments, or the dev build's launch args).
    • Web: use the GA Debugger extension, or confirm debug_mode on the measurement call.
  2. 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.
  3. Open GA4 β†’ Admin β†’ DebugView on the tell-ia-dev property, 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.
  4. screen_view on mobile is emitted manually per route (hooks/use-screen-tracking.ts); confirm screen_name is 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):

md
## 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 ​