A Product Analytics Tracking Plan Teams Will Actually Maintain

Build a lightweight tracking plan that connects decisions to events, types, privacy rules, ownership, testing, and release reviews.

JR4 min read

A tracking plan has a bad reputation because teams often meet one of two extremes: a spreadsheet with hundreds of undocumented events, or a governance project so ambitious that instrumentation never ships.

The useful version is neither. It is a compact contract that connects a product question to the event, properties, owner, privacy decision, and test that make the answer trustworthy.

Begin with a decision#

“Track onboarding” is not a requirement. “Decide which onboarding step to redesign by comparing completion and time-to-complete” is.

The second statement tells you what must be observable:

  • when the journey starts
  • which steps complete
  • the person and account taking the action
  • elapsed time or enough timestamps to derive it
  • the product variant or experiment

If no decision changes, the event probably does not need to exist. This simple gate is the best defense against an event catalog nobody understands.

Define events as completed facts#

Use past-tense names for actions that actually occurred:

account_created
data_source_connected
report_exported
subscription_started

A click and an outcome are different facts. connect_button_clicked can diagnose interface behavior; data_source_connected proves the integration became usable. Do not use the convenient client event as a substitute for a server-confirmed business result.

For every event, write one sentence beginning with “Emitted when…” Include exclusions. “Emitted when the first valid connection completes, excluding internal test accounts” is testable. “Tracks connections” is not.

Make properties a typed contract#

A property needs more than a name. Record:

  • JSON type
  • required or optional
  • example value
  • allowed values or format
  • source of truth
  • whether it can contain personal data

For money, choose minor units and an explicit currency. For durations, put the unit in the name: duration_ms. For IDs, use opaque application-generated values. For enum-like strings, define the allowed list and what should happen when the product adds a new value.

Do not attach an entire user, request, or billing object because it is available. Analytics schemas should be designed, not leaked out of application models.

Put identity in the plan#

Most product questions count an entity:

  • users
  • customer accounts
  • devices
  • projects
  • subscriptions

State which identifier each metric uses. In B2B software, include user_id and account_id when a person acts inside a company workspace. In anonymous flows, decide whether pre-sign-in activity stays separate, joins through an explicit linking event, or is intentionally excluded.

Identity is not a cleanup task for later. It determines whether a funnel, retention curve, or customer-facing dashboard is even possible.

Include privacy and lifecycle decisions#

Add columns for sensitivity and retention. This forces a useful conversation before data leaves the application.

Direct identifiers, free text, URL query strings, headers, and raw payloads deserve suspicion. Passwords, tokens, session cookies, authorization headers, and payment-card data never belong in analytics.

An opaque user ID is better than an email, but it is still linkable. Keep the mapping in your system of record and document the workflow for access or deletion requests.

Different data deserves different lifetimes. A product adoption event may support annual retention analysis; a delivery diagnostic might be useful for only a few weeks.

Make ownership concrete#

Every event should have:

  • a product owner for the business definition
  • an engineering owner for the emitting code
  • a destination collection
  • a schema version
  • a review date

The plan belongs in version control or another system reviewed with code. Update it in the same change that adds or modifies instrumentation. A spreadsheet updated after release becomes an archaeological record, not a contract.

Test semantics, not just calls#

An automated test that checks track() was called is too weak. Assert the payload:

expect(track).toHaveBeenCalledWith(
  expect.objectContaining({
    event: "subscription_started",
    user_id: "usr_test",
    account_id: "acct_test",
    plan: "pro",
    schema_version: 1
  })
);

Test missing required IDs, wrong numeric types, unknown names, sensitive fields, and environment routing. In staging, perform the real action, inspect the event, and run the actual chart or SQL query that will consume it.

After release, reconcile against the source: signups against user rows, payments against billing, API calls against gateway counters.

Deprecate without rewriting history#

When meaning changes materially, version the event or introduce a clearer one. Record:

  • last version that emitted the old shape
  • first version that emits the new shape
  • deployment date
  • dashboards and queries affected
  • planned removal date

Do not reuse an old event name for a different fact. Historical charts should remain interpretable even after the product evolves.

The minimum viable plan#

A practical first plan can have these columns:

event_name
decision_supported
emitted_when
owner
required_properties
optional_properties
identity_fields
sensitivity
schema_version
test_case
status

Start with activation, one recurring-value action, one failure event, and the essential revenue or subscription fact. Ten trusted events beat a hundred speculative ones.

Download the GraphJSON tracking-plan template and use the complete tracking plan and instrumentation QA guide for the review and rollout process.

JR

Written by JR

Founder and builder of GraphJSON.