ANALYTICSTEMPLATE

Event Taxonomy

Six months from now you will open your analytics tool to answer one question: where do people drop off before paying? And you will find signup, sign_up, Signup Completed, and user_registered, all live, all slightly different, none documented. Which one goes in the funnel? Nobody remembers. The person who added the third one was you, and you don't remember either. The data was collected faithfully the entire time; it just can't be queried into meaning anymore.

This happens because event names get invented at the call site, one at a time, in the heat of shipping the feature. Each individual name looked fine. Taxonomy is the boring fix: one naming convention, one tracking plan file in the repo, and one rule that no event ships without a row in the plan. It costs about an hour up front and roughly nothing per event after that. The alternative costs you every funnel you'll ever want to build.

This matters double when agents write your code, because an agent asked to "add analytics to this feature" will cheerfully invent event names in whatever style the surrounding file suggests. Give it this plan and the naming rules become constraints it follows instead of decisions it improvises. The template below is the plan file itself: fill in the starter rows, commit it, and point every future analytics task at it.

markdown
# Tracking plan: [app name]

Single source of truth for analytics events. No event ships unless it has a
row here, added in the same PR that adds the capture call. Agents: when
asked to add analytics, add the row FIRST, then instrument to match it.

## Naming rules

1. `object_verb`, lowercase snake_case, verb in past tense for completed
   facts: `checkout_completed`, `report_exported`, `invite_sent`.
2. Present tense only for genuinely in-progress states (`trial_started` is
   past; a rare `export_running` heartbeat is present). When unsure, the
   thing finished: past tense.
3. Objects come from the product's own nouns ([project / order / report]),
   one noun per concept. If the UI says "workspace", no event says "team".
4. No screen names in event names. `settings_page_export_clicked` dies the
   day settings moves; `report_exported` with a `surface` property survives.
5. Never rename or repurpose a live event. Add the new name, capture both
   during a [30-day] overlap, note the migration in the changelog table.
6. Property keys: lowercase snake_case. Money is `amount_usd` in dollars as
   a number, not "$12.00" as a string. Durations are `_ms`. Booleans start
   with `is_` or `has_`. No free-text user input as property values, ever.

## The events (starter set — fill in, delete what doesn't apply)

| Event                 | Fires when (exact trigger)             | Properties                  | Since  |
|-----------------------|----------------------------------------|-----------------------------|--------|
| signup_completed      | server confirms account creation       | plan, referrer_source       | [date] |
| onboarding_completed  | last onboarding step submitted         | steps_skipped               | [date] |
| [object]_created      | [server persists a new object]         | [what varies about it]      | [date] |
| [object]_[verbed]     | [the one action this feature is for]   | [surface, what varies]      | [date] |
| checkout_started      | checkout session created server-side   | plan, amount_usd            | [date] |
| payment_succeeded     | payment webhook confirms (never the UI)| plan, amount_usd            | [date] |
| subscription_canceled | cancellation webhook confirms          | plan, months_subscribed     | [date] |

"Fires when" is a falsifiable statement about code, not a vibe.
"User checks out" is a vibe. "Stripe webhook `checkout.session.completed`
handled" is a trigger someone can verify against the codebase.

## Properties that ride on every event

- `distinct_id`: our database user id. Never the email (emails change and
  split the user's history when they do).
- [team_id / workspace_id, if the product has one]: on every event, so any
  chart can be cut by account, not just by person.

## What NOT to track

- No event per button. Track the completed action, not the click that
  attempted it; clicks without outcomes are noise wearing a name.
- Nothing you can compute from existing events (a `second_purchase` event
  is a query over `payment_succeeded`, not a new event).
- Nothing sensitive: no passwords, tokens, message contents, health or
  payment details in properties. Analytics tools are not access-controlled
  like your database, and exports live forever.

## Changelog

| Date   | Change                                  | Why                        |
|--------|-----------------------------------------|----------------------------|
| [date] | Plan created with [N] events            | initial funnel: [name it]  |
| [date] | [old_event] deprecated for [new_event]  | [reason]; overlap to [date]|

## Review gate (each PR that touches analytics)

- [ ] Every new capture call has a row here, spelled identically.
- [ ] Names follow rules 1-4; properties follow rule 6.
- [ ] `grep -rn "capture(" --include="*.ts*"` output matches this table:
      no orphan events, no phantom rows.

Adaptation notes:

  • Works unchanged for PostHog, Amplitude, Mixpanel, or Segment; the plan is tool-agnostic on purpose. The PostHog setup template handles the wiring side.
  • Start with the events for the one funnel you actually want to see (usually signup through payment). Ten well-defined events beat sixty speculative ones; you can always add a row, but you can't retroactively collect one.
  • On a team, the plan file's PR review is where naming arguments happen, which is exactly where you want them: before the event ships, not after two variants are live.
  • Keep the plan in the repo, next to the code, not in a wiki. Wikis drift; a file the agent must edit in the same PR physically can't.
  • The mistake: treating this as a one-time document instead of a living gate. The plan that stops sprawl is the one enforced at review time; the moment one undocumented event ships without consequence, you're four weeks from three spellings of signup again.