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 integrations to PostHog.
Trigger with phrases like "migrate posthog", "posthog migration",
"switch to posthog", "posthog replatform", "posthog upgrade major".
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.
// Use a PostHog feature flag to gradually shift traffic
import { PostHog } from 'posthog-node';
const posthog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
host: 'https://us.i.posthog.com',
personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY,
});
async function getAnalyticsBackend(userId: string): Promise<'posthog' | 'legacy' | 'dual'> {
const migrationPhase = await posthog.getFeatureFlag('analytics-migration', userId);
switch (migrationPhase) {
case 'posthog-only':
return 'posthog'; // Phase 3: PostHog only
case 'dual-write':
return 'dual'; // Phase 2: Both platforms
default:
return 'legacy'; // Phase 1: Old platform only
}
}
// Rollout plan:
// Week 1: Flag at 0% → all traffic to legacy
// Week 2: Flag "dual-write" at 10% → dual-write for 10%
// Week 3: Flag "dual-write" at 100% → dual-write for everyone
// Week 4: Validate PostHog data matches legacy
// Week 5: Flag "posthog-only" at 10% → PostHog only for 10%
// Week 6: Flag "posthog-only" at 100% → migration complete
Step 6: Validation
set -euo pipefail
# Compare event counts between old platform and PostHog
echo "=== PostHog Event Counts (last 7 days) ==="
curl "https://app.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]}'
Error Handling
Issue
Cause
Solution
Event counts don't match
Sampling or timing differences
Compare daily totals, allow 5% variance
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
Output
Event name and property mapping from source platform