Event Design
Designing the event taxonomy —names, properties, triggers —so the data is queryable, consistent, and durable across product changes.
Why
Event data is the raw material of all analytics, and bad event design poisons everything downstream. If events are named inconsistently ('button_click' vs 'buttonClicked' vs 'cta_click'), every query must guess the naming convention, and half the data is missed. If properties are missing (the event fires without the page URL or the user plan), the event cannot be sliced by the dimensions that matter. If triggers are wrong (the event fires on render instead of on click, counting impressions as clicks), every metric derived from it is inflated. Event design is the discipline of defining a taxonomy once, enforcing it with a linter and a wrapper, and treating it like a schema: versioned, documented, and migrated deliberately. The cost of bad event design is not visible until 6 months later, when the analyst asks 'how many users on the Pro plan clicked the deploy button' and the answer is 'we do not know, because the click event does not include the plan'.
When
Before instrumenting any new feature. Re-design when query patterns hit 'we do not have that property' frequently, or when event volume is > 2x expected (likely mis-triggered).
How
1. Define a naming convention: snake_case, verb_noun (e.g., project_created, deploy_started, not "createProject" or "new project")
2. Define the event taxonomy as a schema: each event has a name, a description, a trigger condition, and a required-properties list
3. For each event, define the required properties: user_id, session_id, timestamp (auto), plus event-specific (project_id, plan, page_url, element_id)
4. Define the trigger precisely: "fires on click of the #deploy-button element, not on render, not on hover" —imprecise triggers are the #1 cause of inflated metrics
5. Implement a typed wrapper: track("project_created", { projectId, plan }) where the wrapper enforces the schema (TypeScript types, runtime validation in dev)
6. Add a linter that checks every track() call against the schema —unknown event names or missing properties fail the build
7. Document the taxonomy in a single registry (spreadsheet or code file) that is the source of truth —every event, its properties, its trigger, its owner
8. Version the taxonomy: when an event changes (new property, renamed), bump the version, migrate historical data if needed, and update the registry
9. Add an event in dev/staging that fires on a known action and verify the event appears with the correct properties before shipping to productionExample
# Event taxonomy (excerpt)
Version: 3.2 Owner: @analytics-platform
event: project_created
description: user creates a new project
trigger: on successful POST /api/projects (200 response)
required: user_id, session_id, project_id, plan, template_id
optional: source (modal|dashboard|api)
version: 1.2 (added template_id in v1.2)
event: deploy_started
description: user clicks the Deploy button
trigger: on click of [data-testid="deploy-button"]
required: user_id, session_id, project_id, deploy_target
optional: is_redeploy (bool)
version: 2.0 (renamed from "deploy" in v2.0)
// Typed wrapper (TypeScript)
track("project_created", {
projectId: "prj_abc", // required
plan: "pro", // required
templateId: "tpl_blank", // required (v1.2+)
source: "modal", // optional
});
// Linter: if projectId or plan missing -> build fails
// Linter: if event name not in schema -> build fails
// Anti-pattern (what the linter catches):
track("createProject", { id: "abc" });
// -> ERROR: event "createProject" not in schema
// -> did you mean "project_created"?
// -> missing required: user_id, session_id, plan, template_idCommon Problems
- ⚠Inconsistent naming across teams —team A fires 'button_click', team B fires 'buttonClicked', team C fires 'cta_click', and every query must check all three, missing events that use a fourth convention nobody documented.
- ⚠Firing on render instead of on interaction —the 'deploy_clicked' event fires when the Deploy button renders, not when the user clicks it, so impressions and clicks are conflated and the CTR is 100%.
- ⚠Missing the properties that matter for slicing —the event fires with user_id and timestamp but not plan or source, so 'conversion by plan' and 'conversion by source' are unanswerable, and the data is useless for the questions the business has.
- ⚠No linter, so the schema drifts —a developer adds a new property to one call but not another, or misspells an event name, and the schema is a suggestion rather than a rule; the drift is discovered months later when a query returns empty.
Best Practices
- ✓Define a typed wrapper and a linter that enforces the schema at build time —the linter is what makes the schema binding; without it, the schema is documentation that drifts, and the data quality erodes silently.
- ✓Use a single naming convention (snake_case, verb_noun) enforced across all teams —consistency is more valuable than the specific convention, and a linter that rejects non-conforming names is how you enforce it.
- ✓Define triggers precisely ('on click of [data-testid=X]', not 'on the deploy button') —imprecise triggers are the #1 cause of inflated metrics, and the precision is what prevents the render-vs-click conflation.
- ✓Version the taxonomy and migrate deliberately —when an event changes, the version bumps, historical data is migrated if needed, and the registry is updated; this is what makes the data durable across product changes.