Execute PostHog major re-architecture and migration strategies with strangler fig pattern. Use when migrating to or from PostHog, performing major version upgrades, or re-platforming existing...
Migrate from Google Analytics, Mixpanel, Amplitude, or Segment to PostHog using a dual-write strategy (send events to both old and new platforms) followed by gradual traffic shifting. PostHog's capture API accepts events in a format similar to Segment's track/identify calls, making migration straightforward.
personalApiKey option; do not reuse a broadly scoped personal API key.| Source | Primary discovery risk | Evidence required before import |
|---|---|---|
| Google Analytics (GA4) | Event and identity models differ | Approved taxonomy and identity mapping |
| Mixpanel | Similar names can hide property or identity drift | Sample export reconciliation |
| Amplitude | Cohort and user-property semantics can differ | Property and timestamp comparison |
| Segment | Destination behavior can differ from direct SDK capture | Dual-write sample and delivery logs |
| Custom analytics | Source semantics are implementation-specific | Source contract, resumable export, and representative sample |
Use Read to inspect the relevant configuration and implementation before proposing changes. Use Write only for a new, explicitly requested artifact inside the target project. Use Edit for minimal changes to existing project files after the evidence pass.
Create a reviewed mapping table with source event, target event, source property, target property, source identity, target distinct ID, timestamp conversion, consent class, and owner. Reject unknown identities, empty event names, invalid timestamps, and unreviewed PII instead of silently coercing them.
Wrap the existing analytics boundary once, apply the frozen mapping before either destination, and attach a migration-run identifier. Track delivery results separately for the legacy and PostHog paths. The application path must remain successful when either analytics destination fails, and the adapter must expose independent kill switches plus a bounded flush on shutdown.
import { PostHog } from 'posthog-node';
const importer = new PostHog(process.env.POSTHOG_PROJECT_TOKEN!, {
host: process.env.POSTHOG_PUBLIC_HOST!,
historicalMigration: true,
});
Historical imports require a paid product analytics plan even though the import itself is free. Use the Python or Node SDK, or the public batch endpoint, only with events dated at least 48 hours before import. Export to durable storage first, checkpoint every batch, preserve ISO 8601 timestamps and stable distinct IDs, and stop on the first reconciliation breach.
{
"api_key": "project-token-from-secret-source",
"historical_migration": true,
"batch": [
{
"event": "mapped_event_name",
"properties": {"distinct_id": "stable-user-id", "migration_run": "approved-run-id"},
"timestamp": "approved-iso-8601-timestamp"
}
]
}
POST this shape to the selected regional /batch/ endpoint. Keep each request below the documented body-size limit, record its checkpoint and response, and never paste a real project token into a generated file or transcript.
Use explicit legacy, dual-write, and posthog-only states with legacy as the fail-closed default. If PostHog evaluates the cutover flag server-side, pass the feature flags secure API key through the SDK's personalApiKey option. Advance only after the approved observation window passes identity, count, property, timestamp, and business-metric reconciliation; roll back on any breach.
set -euo pipefail
# Compare event counts between old platform and PostHog
echo "=== PostHog Event Counts (last 7 days) ==="
curl "https://us.posthog.com/api/projects/$POSTHOG_PROJECT_ID/query/" \
-H "Authorization: Bearer $POSTHOG_PERSONAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": {
"kind": "HogQLQuery",
"query": "SELECT event, count() AS total FROM events WHERE timestamp > now() - interval 7 day AND properties.migration_source = '"'"'dual-write'"'"' GROUP BY event ORDER BY total DESC LIMIT 20"
}
}' | jq '.results[] | {event: .[0], count: .[1]}'
| Issue | Cause | Solution |
|---|---|---|
| Event counts don't match | Sampling, identity, or timing differences | Stop and compare against the migration's approved reconciliation tolerance |
| Historical import slow | Single-threaded | Use batch endpoint, increase flushAt |
| Identity mismatch | Different user ID formats | Normalize IDs in event map |
| Duplicate events | Dual-write without dedup | Use migration_source property to filter |
For a Mixpanel migration, freeze the event and identity map, run a tiny historical sample with historical_migration enabled, compare counts and properties, then expand in bounded batches. Keep live dual-write and historical import evidence separate, and stop on identity or timestamp drift.
See official PostHog references for current authority and verification boundaries.