How to Migrate Product Analytics Without Breaking Your Metrics

A safe analytics migration playbook for historical exports, identity mapping, resumable backfills, dual writing, and reconciliation.

JR4 min read

Analytics migrations fail in two opposite ways. Some teams switch the SDK, discover that every trend starts on launch day, and lose the context behind retention and seasonality. Others import years of unreviewed history and bring every duplicate name, sensitive property, and obsolete schema into the new system.

A better migration treats the new event contract and historical continuity as separate projects. Build the production path first. Import only the history that supports a real decision. Reconcile both before turning the old system off.

Begin with the reports that must survive#

Do not start by asking how many rows the source contains. Start with:

  • Which dashboards drive recurring decisions?
  • Which cohorts need historical continuity?
  • Which customer or account histories matter operationally?
  • Which financial or usage totals need comparison?
  • How far back must those answers go?

This produces an event and property allowlist plus a required date range. A curated 18-month import can be more useful than a lifetime export full of events nobody owns.

It also identifies history you should not carry forward: accidental personal data, full URLs, raw request objects, obsolete experiments, and inconsistent event names.

Design one target contract#

New events and imported events should speak the same language:

{
  "event": "subscription_started",
  "event_id": "amplitude:source-id",
  "user_id": "usr_42",
  "account_id": "acct_7",
  "plan": "pro",
  "schema_version": 2,
  "migration_source": "amplitude"
}

Map vendor fields explicitly. Amplitude’s event_type, Mixpanel’s properties.distinct_id, PostHog’s $ properties, and GA4’s nested event_params are source formats, not your permanent product vocabulary.

Preserve the original occurrence time. Import time is a pipeline fact, not when the customer acted.

Preserve a source event ID when available and put it in a namespaced event_id. Network uncertainty and vendor exports can both produce duplicates; without a stable identifier, distinguishing a retry from a repeated business action becomes guesswork.

Keep raw exports immutable#

Use a staged flow:

source export
    ↓
encrypted immutable files
    ↓
deterministic transform
    ↓
validation and quarantine
    ↓
destination import

Record a checksum, date range, row count, and export time for every file. Write transformed output separately.

This may seem slower than piping one API directly into another. It is much faster the first time a transformation bug appears halfway through the import. Durable staging lets you fix code and replay without repeatedly querying the source or losing track of what changed.

Give staged files a deletion date and restricted access. Historical analytics exports can be one of the largest concentrations of user-level data in a company.

Make the importer restartable#

The importer should:

  • stream input rather than load everything into memory
  • validate every target event
  • batch conservatively
  • checkpoint after acknowledged batches
  • retry transient failures with backoff and jitter
  • quarantine permanently invalid rows
  • preserve a stable event ID
  • expose progress and failure counts

If one batch is invalid, split it to isolate the bad rows. Do not discard the entire file and do not silently skip malformed events.

At-least-once delivery is a reasonable model. If a timeout leaves the worker unsure whether a batch arrived, retry it and deduplicate critical metrics by event_id.

Pilot the difficult day#

Do not pilot only the cleanest recent hour. Choose a day with:

  • anonymous and authenticated activity
  • users in multiple accounts
  • optional and sparse fields
  • the largest payloads
  • midnight and timezone boundaries
  • duplicate or late records

Inspect the destination events and run the actual target reports. A matching row count can hide broken numeric types, empty account IDs, or timestamps shifted into the wrong day.

Fix the transform while the test interval is small.

Dual-write before cutover#

Once the target contract works, send new production events to both systems for a defined period. Observe each destination independently.

Compare raw events first:

  • counts by name and occurrence day
  • unique event IDs
  • missing identity rate
  • property type failures
  • important numeric totals

Then compare derived metrics using the same definition, time zone, filters, and identity rules.

Vendor dashboards can apply hidden or product-specific behavior: identity merges, attribution, bot filtering, late-arrival rules, sessions, modeled data, or project time zones. A difference between interfaces is not automatically an import failure. Raw source export parity is the first checkpoint.

Reconcile against the business too#

The old analytics tool can be wrong. For key facts, compare both systems to the source that created them:

  • signups to committed user records
  • checkouts to orders
  • subscriptions to billing
  • API usage to gateway counters

Write the expected tolerance and explain it. Client-side events may be lower because of blockers. Server-confirmed billing events should normally match much more closely.

Retire the old path deliberately#

Cut over when:

  • direct destination ingestion has survived a representative period
  • required dashboards and alerts have replacements
  • historical exclusions are documented
  • reconciliation is approved by metric owners
  • rollback and support contacts are known

Then remove old SDKs and secrets, stop exports, preserve read-only access for the required period, and delete staged data on schedule.

GraphJSON has detailed runbooks for Amplitude, Mixpanel, PostHog, and Google Analytics 4, plus the provider-neutral historical migration workflow.

JR

Written by JR

Founder and builder of GraphJSON.