Skip to content
bizurk
← ALL WRITING

2026-07-13 / 16 MIN READ

Event schema versioning analytics: surviving a rebuild

The version field, the additive-only rule, and the migration playbook that lets event schemas survive a tracking rebuild without losing comparison data.

The brand had three years of GA4 data and a Looker report that the CFO read every Monday. They were two weeks from shipping a server-side tracking rebuild. The senior data lead asked the question that decides every schema rebuild: "If I rename subtotal_price to subtotal in the new schema, does the historical dashboard still work?"

The honest answer is no, and the rest of this article is why.

I have shipped this rebuild a few times now, most recently in late 2025 across roughly 48 hours of Meta CAPI server-side work for a mid-market DTC operator. What survives a rebuild is not a clever Liquid trick or a one-time backfill. It is two rules, written into every event from day one, held with discipline through the migration window.

// schema rebuild · pick the forkevent_version field · additive only
YoY comparability
preserved (flagged)
schema quality
clean
consumer breakage
scoped
// cost: 8-week dual-write window · ~90 lines of staging SQL
Option C is the path that survives. Two rules: every event carries an event_version field; changes within a major version are additive only.

The fork: rebuild the tracking, do not lose two years

The brand was looking at three options. Option A was a hard cut: throw away the existing GA4 events on cutover day, ship the new schema clean, accept that nothing before the cutover joins cleanly to anything after. Option B was the opposite mistake, configuring the new tracking to emit the legacy event shape so the existing dashboards kept working unchanged. Option C was the boring one. Version every event from the start, change the schema additively until you can't, and run a transformation layer in the warehouse that translates old versions into the new shape on read.

The stakes were specific. Operator dashboards breaking for a quarter would have annoyed the team. Year-over-year comparisons going dead for 12 months would have killed the CFO's monthly board deck. The brand could survive a hard cut on operator reporting; they could not survive one on the financial side.

Picking between these three was the decision the data lead and I worked through over a 90-minute call. The rest of this is the record of that call.

Close-up macro of two translucent glass slabs joined at a fine seam with cool blue light catching the edge of the join.
// the seam · blue rim at the join

Option A: hard cut at the rebuild date

A hard cut is what most teams do because it is the fastest path to a clean codebase. Cutover day arrives, the new tracking starts firing, the new schema lands in the warehouse, and the old event tables are frozen. Forward of the cutover, everything is consistent; behind it sits a quarantine schema that nobody touches except for compliance archives.

The appeal is real. No compatibility code or transformation layer to maintain, no edge cases where a stray old-format event lands in the new pipeline. The new tracking ships in a week instead of two months and the data team gets to write SQL against a clean shape.

The cost shows up about three weeks later. Every dashboard that joins orders against pre-cutover sessions silently returns null. The Looker view that aggregated repeat-purchase rate over a rolling 12-month window starts producing nonsense because half of the window is in the legacy schema and half is in the new one. The CFO's board export, set up two years ago and tested twice since, breaks in a way that takes the data team a full week to diagnose because the report still runs to completion. It just runs against the wrong half of the data.

YoY comparisons stay dead until you have rebuilt 12 months of history in the new schema. That is a 4-6 week project of its own, and it tends to get deprioritized after the first two months because the new dashboards are working and the old ones are someone else's problem.

The hard cut is the right answer for a small brand whose pre-rebuild schema was so broken nothing salvageable lived inside it. It is almost never the right answer for a brand with three years of clean order history.

Option B: drag the legacy schema forward unchanged

Option B is the inverse trap. The team builds a new server-side tracking infrastructure but configures it to emit the same event names, property keys, and shapes the old client-side stack emitted. Continuity is preserved at the cost of fixing nothing.

The whole point of a tracking rebuild is to fix the schema. The old schema had a subtotal_price field that was sometimes a string, sometimes a decimal, sometimes a string with a leading dollar sign because somebody copied the wrong template. It had add_to_cart events that fired with a different property shape on the PDP versus the cart drawer. It had a parameter named transaction_id that was sometimes the order number and sometimes the order id depending on which Shopify app added the snippet. Carrying that mess forward unchanged means the rebuild bought you nothing except a more expensive way to be wrong.

I have not picked Option B on a real engagement. I include it because clients ask, and the answer needs to be in writing.

Option C: versioned migration with the additive-only rule

Option C is the path that survives. Two rules, applied from the first event the new pipeline sends.

Rule 1: every event carries an event_version field in its envelope. Not in the properties blob, in the envelope. The version is a semver-shaped string ("1.0", "2.0", "2.1") that names the shape of the properties for that specific event name. The same event name can carry different versions over time. The warehouse staging layer reads the version and routes the event through the correct transformer.

Rule 2: changes within a major version are additive only. Adding a new optional property is fine and bumps the minor version. Anything that breaks the existing shape (renaming a property, removing one, changing a type from string to number, tightening a nullable field to required) bumps the major version. The strict test for "this is additive" is whether a consumer reading the old version and ignoring any new properties continues to work without changes.

These two rules make every shape change reversible without rewriting history. The warehouse always knows what shape it is reading. A downstream consumer (a Looker report, a Klaviyo flow, a board export) reads the version it was built against until it migrates to the new one. Nothing has to migrate atomically.

The rebuild ships at version 2.0 of the existing event names. The new server-side tracking emits the new shape; the old client-side events, in the historical tables, are tagged version 1.x. The staging layer in BigQuery has a transformation rule per event name that translates version 1.x into the version 2.0 shape on read. Dashboards reading from the staged view see one consistent schema; dashboards reading raw can opt into a specific version. History is preserved and the rebuild is real.

Why parameter renames break dashboards two months later

The scenario that nobody plans for, and that breaks roughly half of the rebuilds I have seen done badly, is the parameter rename that the team thinks they handled.

The setup looks like this. The new schema is better than the old one in 14 ways and 1 of them is renaming subtotal_price to subtotal. The team picks the rename, updates the canonical schema doc, finds every reference in the dbt project, runs a search across the analytics repo, updates the Looker semantic model, and ships. Three weeks later, the dashboards are working. Six weeks later, the dashboards are still working. The team moves on.

In week 8, the CFO mentions on a Monday board call that the revenue number on the deck looks low. By low, they mean about 12% low. The data team chases it for a day and a half before someone realizes the board export is being generated by a Looker scheduled report that points at a Looker view that was built three years ago by a contractor who left, and that view still references subtotal_price. When the parameter got renamed, the field became null, and Looker silently treated null as zero in the SUM. For seven weeks, the CFO had been showing a board deck that under-reported revenue, and nobody noticed because the discount logic absorbed the discrepancy until a particular promotion broke the disguise.

This is the consumer surface that nobody remembers. It is not the dashboard. It is the report-that-feeds-the-dashboard, or the export-that-feeds-the-deck, or the Slack-stitched-summary-that-the-VP-of-marketing-reads. The longer your brand has been operating, the more of these you have, and the less likely it is that any single person knows about all of them. A rename in the new schema breaks each of these surfaces silently and on its own schedule.

The additive-only rule is what protects you from this. If you rename a field, the old field has to keep being written for as long as any consumer is still reading it. The new schema gets the new name. The transformation in the warehouse staging layer makes both names available. Consumers migrate one at a time. The CFO's board export does not need to migrate this quarter; it needs to migrate before you finally decide to drop the old field.

Mirror reflection on a still water surface doubling a row of vertical accents under a deep electric-blue sky, hard symmetry through the centerline.
// the mirror · doubled verticals

The migration playbook when you DO need to rename

Some renames are worth doing. The path is longer than people expect, and the discipline is the difference between a clean migration and a forensic exercise four months later.

Step 1: bump the major version in the schema doc. The new shape lives in the doc as purchase@2.0 with the renamed field; the pipeline still emits purchase@1.x. No consumer is affected. This step exists so the doc is unambiguous about what is coming.

Step 2: write both fields in parallel at version 2.0. Update the ingestion service to emit both subtotal_price and subtotal on every purchase event with the same value. Old consumers reading subtotal_price keep working because the field is still present; new consumers can start reading subtotal. This is the dual-write window, and it exists so consumer migration can happen at the consumer's own pace.

Step 3: migrate consumers one at a time. Update each Looker view, scheduled report, Klaviyo flow, and Slack-stitched export from the old field to the new one. The migration order is the consumer with the most traffic first (if it breaks, you find out fast) and the highest-stakes consumer last (the CFO's board deck migrates after you have done it five other times). Each consumer's migration is its own one-line PR with its own QA pass.

Step 4: drop the old field at 3.0. After the longest-running consumer has confirmed migration, ship the next major version that removes subtotal_price. Schedule it at least 90 days after dual-write started so any quarterly report has had a cycle to surface. Send an internal heads-up two weeks before the drop, then ship it.

The 90-day deprecation rule. No version with active reads stays live for more than 90 days after dual-write begins. If 90 days have passed and a consumer has not migrated, its owner gets a direct ping; if 120 days, the consumer migrates or accepts breakage on the next version drop. Without this rule the dual-write code accumulates, and three rebuilds later you have seven versions of purchase in the staging layer and no one can audit which one a downstream consumer actually reads.

The never-delete-in-a-minor-version rule. If you find yourself wanting to remove a field "while you are in there" between version 2.1 and 2.2, stop. That removal is a major version bump, not because the field matters, but because the additive-only rule is only valuable if you actually hold it. The first time you violate it, every downstream consumer learns that minor versions might break them, and the rule stops protecting anything.

What we chose, what we'd revisit

We picked Option C. The dual-write window ran about eight weeks, which felt long until a stale Looker view surfaced at week six and we realized we were about to repeat the CFO board-deck story for someone else.

The dual-write cost was modest: roughly 90 lines of staging-layer SQL covering four events with breaking changes. Most events did not need transformation. The ones that did got their transformer reviewed by the data lead before shipping, and tested against a sample of historical events to confirm the version 1.x shape produced the version 2.0 shape we expected.

What I would revisit is the consumer inventory. We did the inventory at the start, missed two Looker reports and one Klaviyo flow, and each one broke quietly during the migration window. The fix in each case was a 10-minute change; the discovery was a half-day of triage. Doing it again, I would budget a full week up front for the inventory pass, with a checklist that explicitly includes scheduled warehouse queries, every Looker view, every Klaviyo flow that filters on a tracking event, dashboards owned outside the data team, and recurring Slack-stitched or email exports.

I would also build automation for the version 1.x freeze. We left the version 1.x writers running for the full eight weeks even though no consumer was reading them after week three. Next time, a "no consumer has read this version in 14 days" check in the staging layer would nudge the schedule forward when it is safe.

A single broken slab fragment lit by warm pink and cool blue on opposing faces, isolated against a deep void.
// the fragment · twin lights on one shape

The first time you violate the additive-only contract, every downstream consumer learns that minor versions might break them, and the contract stops protecting anything.

The decision held. Eighteen months on, the brand still has YoY comparisons that work across the cutover. The CFO's board deck still runs every Monday. The version 2.x schema has had three minor bumps and one major (a 3.0 that retired two fields and added one). No comparison data has been lost across either rebuild, and that is the only metric that matters.

FAQ

Where does the event_version field actually live: in the envelope or the properties?

In the envelope, alongside event_id, event_name, and occurred_at. The version describes the shape of the properties blob for that specific event name. Putting it in properties means you have to parse properties to know how to parse properties, which is the kind of recursion that breaks ingestion services in interesting ways. Envelope keeps the routing logic clean.

What counts as additive in 'additive-only'?

Adding a new optional property, a new event name, or a default value on a previously-required field. Anything that changes an existing field's meaning, type, required-ness, or name is not additive. The strict test is whether a consumer reading the old version and ignoring any new properties continues to work unchanged.

How long should the dual-write window be?

Long enough that every consumer has had at least one full reporting cycle to migrate. Eight weeks is a reasonable default for DTC, where the longest-running scheduled reports tend to be monthly or quarterly. Hard floor of 90 days if any consumer is a quarterly export. Soft ceiling around 12 weeks; longer than that and the dual-write code accumulates faster than consumers migrate.

Do I need this if I am using GA4 + Klaviyo and nothing else?

Yes, and the version field is even more important when GA4 is in the mix. GA4 stores roughly 14 months of event data accessible to most exports, and you cannot edit that history. If you rename a parameter in GA4 today, the rename does not back-propagate. The data before the rename has the old name; data after the rename has the new name. The same versioning principle applies; you just enforce it in the dbt staging layer that pulls from the GA4 BigQuery export rather than in your own ingestion service.

Who owns the schema doc?

One person, ideally the data engineer or the senior developer who owns the warehouse staging layer. The doc lives in the repo. Every version bump goes through PR review. The reviewer's job is to verify that minor bumps are actually additive and major bumps actually need the breaking change. If you do not have one owner, the schema rots; if you have three, every change becomes a debate.

Can I skip the major version bump if 'nothing important' uses the renamed field?

No. The premise of the rule is that you do not actually know what uses the renamed field. The Looker view from three years ago, the Slack export the marketing intern set up last summer, the Klaviyo flow that segments on the parameter you forgot existed: those are exactly the consumers that break silently and embarrass the team. Treat every rename as a major version. The cost is one extra integer in a string. The savings are the difference between a clean migration and a forensic exercise.

Sources and specifics

  • The pattern was field-tested across two server-side tracking rebuilds in Q4 2025 and Q1 2026, both at DTC brands in the $2-10M revenue range.
  • The 48-hour Meta CAPI server-side build that anchors the rebuild experience was part of a broader Q1 2026 analytics engine rebuild for a mid-market DTC operator.
  • The 90-day deprecation window is the operational floor I default to; longer windows are reasonable, shorter windows tend to miss quarterly reports.
  • The downstream warehouse layer that consumes these versioned events is described in BigQuery for Shopify data and the parent warehouse-first reporting layer.
  • For the canonical event catalog these versioning rules apply to, see the sister piece on event schema design for DTC.
  • A close cousin of this pattern, applied to Shopify metafields rather than analytics events, lives in the metafield schema versioning walkthrough; the two pieces share the dual-write playbook.
  • The reconciliation logic that consumes the staged events lives in the Shopify, GA4, Meta reconciliation flow.
  • If you are about to rebuild a DTC tracking stack and the schema-survival path looks heavy from the outside, a DTC Stack Audit maps the existing consumer surface and tells you which renames are worth the migration cost and which can be quietly avoided.

// related

DTC Stack Audit

If this resonated, the audit covers your tracking layer end-to-end. Server-side CAPI, dedup logic, and attribution gaps - all mapped to your stack.

>See what is covered

Tell me what you’re trying to ship.

Send a quick message and I read it within a day, or talk to AI Michael first if you want to feel out your project before you write to me.

By sending this, you agree to the Terms and acknowledge the Privacy Policy.