// blog

Designing an event taxonomy that survives a year

A naming convention, a property contract and a five-minute review that keep your event list readable long after the people who wrote it move on.

eventsinstrumentation

Every event taxonomy is clean on day one. The interesting question is what it looks like in twelve months, after four people have added events under deadline, two of them have left, and someone in a planning meeting asks what the difference is between signup_complete, Signup Completed and user_registered.

The answer is usually that there is no difference, and that all three are still firing. Nobody deletes an event, because nobody is sure who is relying on it. That is how an event list becomes archaeology, and it is why the dashboard nobody trusts is nearly always a naming problem before it is a tooling problem.

A taxonomy that survives a year is not more detailed than one that does not. It is smaller, and it has rules that a new engineer can apply without asking anyone.

Name the behaviour, not the interface

Interfaces change. Behaviours do not. If an event is named after a screen, a button, a modal or a release, it dies with that screen and takes its history with it — and the replacement event starts its life with no comparison period.

AvoidPreferWhy
clicked_blue_ctatrial_startedSurvives the redesign that makes the button green.
onboarding_step_3data_source_connectedSurvives the day step three becomes step two.
new_checkout_purchaseorder_completed"New" is a date stamp in disguise. Put the flow in a property.
modal_shownupgrade_prompt_shownSays which prompt, so the event is answerable on its own.

Pick one casing and one tense and never negotiate them again. object_verb_past_tense, lower snake case, is a defensible default: report_saved, invite_accepted, subscription_cancelled. The specific convention matters far less than the fact that it is written down and mechanically checkable.

Put the variance in properties

The fastest way to end up with two hundred events is to encode every dimension in the name. Each new plan, platform or surface multiplies the list, and every analysis then has to know all the variants.

// Three events that can never be compared to each other
track("mobile_pro_report_exported")
track("web_pro_report_exported")
track("web_free_report_exported")

// One event that answers all three questions, and more
track("report_exported", {
  platform: "web",        // web | ios | android
  plan: "pro",            // free | pro | scale
  format: "csv",          // csv | pdf | png
  row_count: 1420,
  scheduled: false,
})

The rule of thumb: if you would ever want to see the two things in the same total, they are one event with a property. If you would never put them on the same chart, they are separate events.

Four rules worth enforcing

  1. One event per meaningful outcome. If a user cannot tell that something happened, it is probably not an event — it is a log line. Ship logs to your logging system.
  2. Properties are typed and stable. plan is always a string from a known set; row_count is always a number, never "1420". A property that changes type mid-quarter silently breaks every filter built on it.
  3. Never put an unbounded value in a name or a low-cardinality property. User ids, email addresses and search strings belong in properties designed for high cardinality, not in the event name and not in a field you plan to group by.
  4. Every event has an owner and a one-line definition. Not a wiki page — one line, stored next to the event, saying when it fires and when it deliberately does not.

The definition has to travel with the number

Most disagreements about a metric are really disagreements about an event. Does trial_started fire when the user clicks "start trial", or when the backend confirms the trial record? Those two numbers differ by every failed request, and the gap is invisible on a chart.

Write the answer where the query is, not in a document someone has to find. If your analytics tool lets you attach a description to an event, that field is the highest-leverage text in your instrumentation. If it does not, keep a small events.ts in the codebase that exports the names as constants with the definition in a comment above each one, and make the SDK call take those constants. Free renames, no typos, and the definition is one click from the call site.

A five-minute review before anything ships

Add these to the pull-request template for anything that adds an event. They take a couple of minutes and prevent most of the mess:

  • Does an existing event already cover this, with one more property?
  • Does the name follow the convention, and is it about behaviour?
  • Are all the property names ones we already use elsewhere?
  • What question does this event answer, and who will ask it? If nobody can name the question, do not ship the event.

Fixing the taxonomy you already have

Do not start with a rewrite. Start by sorting your events by volume over the last ninety days and looking at what is actually being sent. In most products, twenty events carry ninety-five per cent of the volume, and the long tail is dead code from features that shipped and were removed.

Then work in three passes, in this order:

  1. Mark the dead ones. Anything with zero volume in ninety days is not being sent. Delete the call sites and stop maintaining them.
  2. Merge the duplicates. For each cluster that means the same thing, choose the survivor, start sending the survivor everywhere, and keep the old ones alive just long enough to overlap.
  3. Write the definitions. One line each, for what is left. This is the step that actually stops the drift, and it is the one teams skip.

Do not rename in place unless your tool can carry the history across the rename. A rename that resets a series is worse than a slightly awkward name, because the awkward name is still comparable to last quarter.

The takeaway

Aim for the smallest set of behaviour-named events that answers the questions you actually ask, push every variation into typed properties, and store the one-line definition next to the event. A taxonomy is not documentation of your product — it is the vocabulary your team argues in, and it is worth being fussy about.

Run this against your own data

Alitycs applies these definitions by default and shows you the query behind every answer. Free while you are small.

Disagree with something here? Tell us — corrections get published.