← All articles

Analytics · 11 min read · 2026-09-13

Event Schema Versioning: Evolve a Tracking Plan Safely

Every tracking plan that survives contact with a growing product eventually needs to change. An event was named badly, a property should have been required, a definition drifted from what the business now means by it, or two events should be merged. The instinct is to just fix it — rename the event, tighten the property, ship it. That instinct quietly destroys your historical data, because analytics reports query events by name and definition, and changing either mid-stream splits one continuous time series into two disconnected halves. Renaming an event without a migration plan creates broken time series (WarpDriven), and the break is often discovered weeks later when a funnel inexplicably drops to zero.

The alternative is to treat events as versioned artifacts and evolve the schema the way you evolve a public API: additively, with deprecation timelines and an overlap window, never with a hard rename in place. This post covers why schema changes break reporting, how a schema_version stamp makes changes explicit, the dual-write pattern that preserves history, how to protect cohort comparability across a change, and the lightweight governance that stops a rename from silently breaking a board report. It is the migration companion to designing the plan in the first place — see the event taxonomy and tracking-plan schema guide for the initial design; this post is about changing it after it is live.

Why a schema change breaks your reports

A funnel, a retention curve, and a cohort chart are all queries against events by name and property. When you rename "signup" to "account_created," every report built on the old name ends its series at the rename date and every report on the new name begins at zero — not because behavior changed, but because the identifier the query keys on changed underneath it. The result is an artificial cliff in one chart and an artificial launch in another, and anyone reading a trend across the boundary sees a discontinuity that has nothing to do with the product. The data is intact; the naming break just made it unreadable across the seam.

Redefinitions are more insidious than renames because they are invisible. If you keep the name "activation" but change when it fires — from first login to first meaningful action — the series looks continuous while secretly comparing two different things on either side of the change. Every cross-boundary comparison is now wrong, and nothing in the chart signals it. This is a close cousin of metric-definition drift, except deliberate: you are changing the definition on purpose, which means you have the chance to do it safely if you plan for the seam instead of pretending it is not there.

Version events like an API

The foundational move is to stamp every event with a schema_version property that records which version of the tracking plan produced it. Treat events as versioned artifacts: introduce a schema_version property, publish new versions with clear deprecation timelines, and route traffic through migration plans that preserve backward compatibility (Path to Project). The version stamp converts an invisible schema change into an explicit, filterable dimension — you can query "activation events under v2" and never accidentally blend them with v1 events that meant something different.

Versioning imposes the same additive discipline that keeps public APIs from breaking their clients. You do not mutate a field in place; you add the new shape alongside the old, mark the old as deprecated, and give consumers a window to move. When renaming fields, add a new field with a new tag and mark the old field as deprecated, only removing it after at least one full release cycle with the field deprecated and reserved (OneUptime). The event is a contract with every dashboard and model downstream, and the whole point of versioning is that you can change the contract without breaking the parties who signed the old one.

Dual-write: the safe migration pattern

The mechanism that preserves history through a change is dual-writing: during the transition, fire both the old event and the new event for the same action, so both time series continue uninterrupted. The old reports keep working on the old event; the new reports come online on the new event; and for the overlap window both exist in parallel, letting you validate that the new event reproduces the old one's counts before you cut over. The disciplined sequence is to ship the new event alongside the old, run both until dashboards are migrated, then formally remove the deprecated event — never rename in a single step.

How long you dual-write is governed by your slowest downstream consumer, not your fastest. Best practice is to write both old and new names for a transition period and mark one deprecated, giving consumers time to migrate (Trackingplan). If a monthly board report or a quarterly cohort model reads the old event, you cannot remove it until that artifact has been rebuilt — so the removal date follows the last consumer to migrate, which is usually a quarter out, not a sprint. Maintain a deprecation log and, where you can, automatically surface any report still querying a deprecated event, so removal is a verified step rather than a hopeful one. This is the same care that a data-quality audit applies to the rest of your instrumentation.

Protecting cohort comparability across a change

Cohort analysis is where schema changes do their quietest damage, because cohorts compare groups across time and a mid-series definition change makes the early cohorts non-comparable to the later ones. An April signup cohort measured under the old activation definition cannot be honestly compared to a July cohort measured under a new one — the numbers move because the ruler changed, not because the product did. If you present that chart without noting the seam, you will draw conclusions about a trend that is really a measurement artifact, the same class of error as the activation-definition validity traps.

The protection is the overlap window plus an explicit changeover marker. Dual-write so both definitions exist in parallel long enough for the new event to accumulate comparable history, validate during the overlap that the new event tracks the old within tolerance, then switch reports over and annotate the exact changeover date on any long-range chart. Readers of a multi-quarter cohort curve then know precisely where the definition shifted and can reason across it instead of being misled by it. Where a redefinition is unavoidable and back-comparability matters, backfill the new event against historical raw data if you retain it, so the new series extends cleanly into the past rather than starting at the changeover.

Govern the change with a lightweight proposal

The cost of a schema change is almost never in the app code that fires the event — it is in the dashboards, warehouse models, and alerts that read it, and those are exactly the dependencies the engineer making the change cannot see. A repeatable pattern closes that gap: propose an RFC describing the schema diff, impact analysis, rollout and rollback, test plan, and timeline, posted centrally and signed off before shipping. It does not need to be heavy; a single page that names the affected events and, crucially, the reports and teams that consume them is enough to catch the board report no one remembered depended on the event.

The impact analysis is the part that pays for itself. Common causes of schema problems include upstream changes that rename or remove fields and manual edits that bypass migration history — in other words, changes made without tracing who reads the data. Listing consumers before the change converts a silent break into a coordinated migration: each owner knows a change is coming, has a window to move, and confirms when they have. Fold schema changes into the same governance that manages your collection architecture, and evolving the tracking plan becomes routine maintenance rather than a recurring source of broken charts and lost trust in the numbers.

FAQ

Why does renaming an analytics event break historical data?

Because a funnel or cohort report queries an event by name, and renaming the event ends the old name's time series and starts a new one, so any chart spanning the change shows an artificial cliff. If you rename 'signup' to 'account_created' on a Tuesday, every report built on 'signup' flatlines that day and every report on 'account_created' starts from zero — even though user behavior did not change at all. Renaming without a migration plan therefore creates broken time series, which is why the disciplined approach is to ship the new event alongside the old, run both until dashboards are migrated, then formally remove the deprecated one. The data was never wrong; the naming discontinuity just made it unreadable across the boundary.

What is a schema_version property and why add one?

A schema_version property is a field stamped on every event that records which version of your tracking plan produced it, so downstream queries can distinguish an event fired under the old schema from the same event fired under the new one. It turns a silent, invisible schema change into an explicit, filterable dimension. When you change what an event means — add a required property, alter a value's units, redefine when it fires — a version stamp lets you segment 'before' from 'after' instead of blending two different definitions into one misleading trend. Without it, a mid-quarter redefinition contaminates the whole series and no one can tell which rows follow which rules. Treating events as versioned artifacts is the core of evolving a schema without chaos.

How long should you dual-write old and new events during a migration?

Long enough for every downstream consumer — dashboards, alerts, warehouse models, and any team querying the data — to migrate to the new event, which in practice means at least one full reporting cycle and often a quarter. The rule of thumb is to write both the old and new event names for a transition period, mark the old one as deprecated, and only remove it after at least one full release cycle with the field deprecated and reserved. The time is governed by your slowest consumer, not your fastest: a monthly board report built on the old event means you cannot remove it until that report has been rebuilt on the new one. Rushing removal is what turns a clean migration into a broken dashboard someone discovers weeks later.

How do you migrate a tracking plan without losing cohort comparability?

Keep the old event flowing until the new event has accumulated enough history to compare like-for-like, and use the schema_version stamp to align the two definitions rather than splicing them. Cohort analysis compares groups across time, so a definition change mid-series makes an April cohort measured under the old rules not comparable to a July cohort measured under the new ones. The safe pattern is to dual-write so both definitions exist in parallel for an overlap window, validate that the new event reproduces the old event's counts during that window, and only then switch reports over — with a documented note of the changeover date so anyone reading a long-range cohort chart knows where the definition shifted.

What belongs in an event-schema change proposal?

A short RFC describing the schema diff (what changes), an impact analysis (which dashboards, models, and teams depend on the affected events), a rollout and rollback plan, a test plan, and a timeline with the deprecation and removal dates. Posting this in a central place and getting sign-off before shipping is what prevents a well-meaning rename from silently breaking a downstream report no one remembered depended on it. The proposal does not need to be heavy — a page is enough — but it needs to name the consumers, because the cost of a schema change is almost never in the app code that fires the event; it is in the reports and models that read it.