Tags: analytics concept

Tracking Plans

Date: 2026-08-16


The contract between the person who wants the number and the person who writes the code. Unenforced, it’s a wish — and a stale plan is worse than no plan, because it answers confidently.


What it is

A tracking plan is the specification of every event a site fires — its name, when it fires, what properties it carries, and who owns it — written before the events exist.

One row per event. Minimum viable columns:

ColumnContentWhy it’s there
Event nameExact string, as firedThe join key to everything
DescriptionWhat it means in a sentenceDistinguishes near-identical events later
TriggerWhen exactly it firesThe column that prevents most disputes
PropertiesName, type, required/optional, example valueThe half people forget to specify
OwnerA person, not a teamUnowned events rot
StatusProposed / live / deprecatedLets history stay queryable — see Event Taxonomy Design
DestinationsWhich tools receive itCompliance answers live here too

Trigger is the column that earns its keep. “Fires on add to cart” is not a specification. “Fires on successful server response to the add-to-cart call, not on button click, not on optimistic UI update” is — and the difference between those two is a permanent discrepancy between your numbers and the merchant’s.

One entry, filled in properly:

event: checkout_started
description: Customer has entered the checkout flow.
trigger: >
  Fires once on successful render of checkout step 1.
  NOT on click of the basket's "Checkout" button.
  NOT re-fired when the customer navigates back to step 1.
owner: alex
status: live
destinations: [ga4, warehouse, klaviyo]
properties:
  cart_value_pence:  { type: integer, required: true,  example: 249900 }
  currency:          { type: string,  required: true,  example: "GBP", allowed: [GBP, EUR] }
  item_count:        { type: integer, required: true,  example: 3 }
  is_guest:          { type: boolean, required: true,  example: true }
  delivery_option:   { type: string,  required: false, example: "next_day" }

Everything that prevents an argument later is in the boring fields. cart_value_pence names its unit, so nobody ships pounds into it. allowed on currency turns a free-text field into a validatable one. The two NOT lines in the trigger are worth more than the description.

Why it exists

Three jobs, only one of which is documentation:

  • Specification — instrumentation designed before the feature ships, rather than retrofitted after someone asks a question the data can’t answer
  • Diff source — the plan is what “what fires” gets compared against. Without it, an audit has nothing to check reality against and degrades into browsing. See Guide - Auditing a Tracking Plan
  • Interpretation — in two years, the plan is the only record of what a property meant. This is the job that survives everything else

Where it lives

The choice is a straight tradeoff between staying true and getting read.

LocationStays accurateGets read by non-devsEnforceable
SpreadsheetPoorlyYesNo
Markdown / schema file in the repoWell — it’s in the diffRarelyVia CI
Typed schema generating client codePerfectly, by constructionNoAbsolutely
Dedicated tool (Avo, Segment Protocols)WellYesYes, at collection

The spreadsheet is where most plans start and where most of them die: nothing links it to the code, so it drifts silently from the first sprint onwards.

The version that actually works is the plan as the source, the code as the output — generate a typed tracking client from the schema, so calling an unspecified event is a compile error rather than a discovery six months later. It’s a real engineering investment and it’s the only approach where the plan cannot drift, because drifting isn’t representable.

Middle ground, if codegen isn’t affordable: plan in the repo, validation in CI comparing fired events against it, and a periodic audit for what the CI can’t see.

Enforcement

A plan with no enforcement mechanism describes intentions, not data. Enforcement points, cheapest first:

  1. Review — plan updated as part of the feature’s definition of done. Free, and fails the moment there’s deadline pressure
  2. CI validation — a test asserting that events fired in integration tests exist in the plan with the right property types. Catches new violations, not decay in existing events
  3. Codegen — unspecified event is a type error. Catches everything at authoring time, catches nothing at runtime
  4. Collection-side validation — events failing the schema get quarantined rather than silently landing malformed. See Schema Enforcement. Catches everything, including third-party and tag-manager-injected events the codebase never knew about

These stack rather than substitute: codegen can’t see events fired by a tag someone added in GTM last Tuesday, and collection-side validation can’t stop them being written in the first place.

Failure modes

  • Drift — the default state. The plan describes release 4, production is on release 31. Everything derived from the plan is now confidently wrong, which is strictly worse than having no plan and knowing you don’t
  • Written after the fact — a plan reverse-engineered from what happens to fire is an inventory, not a specification. Useful, but it can never tell you an event is missing, which is what you needed it for
  • Deletion on deprecation — the row removed when the event stops firing. Two years of history now has no definition attached
  • Property rows left blank — event names get specified, property types and required-ness get “we’ll see”. Properties are where the ambiguity always was
  • Multiple copies — one in Notion, one in a spreadsheet, one in the analytics tool. Now nothing is the source of record
  • Owned by analytics alone — if engineering doesn’t treat it as part of the definition of done, it’s a document about engineering written by people who can’t change engineering’s behaviour

The honest tradeoff

The plan is pure overhead at the moment you maintain it, and the entire value arrives later, to someone else. That asymmetry is why plans decay regardless of how obviously correct they are.

Which means the practical question isn’t “should we have a tracking plan” but how much of it can be made a by-product of shipping — generated, validated, or enforced at collection — because the portion depending on discipline is the portion that will be out of date.