Why migrate
PostHog is what you switch to when GA4 has been the wrong tool all along. It’s not designed for “what is my organic traffic” questions — it’s designed for “where do users drop off in onboarding, what feature did the churned cohort never use, and which experiment variant won”. If your real questions are product-shaped, PostHog is a better tool. If they’re marketing-shaped, look at Matomo or Plausible.
Pre-migration checklist
- Confirm fit. If your top GA4 reports are all about traffic source / channel / campaign, PostHog is the wrong move. PostHog excels at user-level product behavior, not anonymous marketing-funnel analytics.
- Decide hosting. PostHog Cloud (US/EU) starts free, scales to $450+/mo. Self-host (Helm chart, Docker compose) is mature but heavy — needs Postgres, ClickHouse, Redis, Kafka.
- Plan identification. PostHog needs
posthog.identify(distinct_id)at the right moments. GA4 doesn’t expose stable user IDs by default; you have to wire this yourself. - Inventory current product events. If you don’t have product events in GA4 today, the migration is really a “start tracking product events” project. Budget accordingly.
- Plan retention period. PostHog defaults to 1-year event retention on paid; longer needs custom config or self-host.
Step-by-step
1. Audit (Days 1–4)
Categorize your GA4 events: marketing-funnel (page_view, source/medium) vs product (sign_up, feature_used, churned). Only the product events should migrate to PostHog. Keep marketing analytics on Plausible / Matomo or your ad platforms.
2. Install PostHog (Days 4–6)
<br />
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(“.”);2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement(“script”)).type=”text/javascript”,p.async=!0,p.src=s.api_host+”/static/array.js”,(r=t.getElementsByTagName(“script”)[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a=”posthog”,u.people=u.people||[],u.toString=function(t){var e=”posthog”;return”posthog”!==a&&(e+=”.”+a),t||(e+=” (stub)”),e},u.people.toString=function(){return u.toString(1)+”.people (stub)”},o=”capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys”.split(” “),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('phc_XXXXX', { api_host: 'https://eu.i.posthog.com' });
[/ma_code]
<h3>3. Identify users (Days 6–8)</h3>
<p>This is the work GA4 hides from you. Call <code>posthog.identify()</code> at sign-up, login, and anywhere you have a stable user ID. Without identification, PostHog is just GA4 with extra steps.</p>
[ma_code lang="js" title="PostHog identify pattern"]<br />
// On login or session restore<br />
posthog.identify(user.id, {<br />
email: user.email,<br />
plan: user.plan,<br />
signed_up_at: user.created_at<br />
});</p>
<p>// On logout<br />
posthog.reset();<br />
4. Map events (Days 8–12)
Each GA4 product event becomes a PostHog capture. PostHog accepts arbitrary event names — no GA4 reserved-event constraints.
<br />
// GA4<br />
gtag(‘event’, ‘feature_used’, { feature: ‘export’, plan: ‘pro’ });</p>
<p>// PostHog<br />
posthog.capture(‘feature_used’, { feature: ‘export’, plan: ‘pro’ });</p>
<p>// Cohort analysis comes for free in PostHog Insights — no GA4-equivalent.<br />
5. Wire feature flags + experiments (Days 12–18)
This is why most teams migrate. PostHog’s feature flag API replaces LaunchDarkly / Optimizely:
<br />
if (posthog.isFeatureEnabled(‘new-checkout’)) {<br />
// ship the variant<br />
}<br />
6. Dual-run + cutover (Days 18–28)
Run PostHog alongside GA4 for 14-30 days. Build the 3-5 funnels and cohorts you care about most. When PostHog answers product questions faster than GA4, drop GA4.
Common gotchas
- Event volume costs. PostHog Cloud bills per event. Ship $event_filter or sampling early or you’ll get a surprise bill.
- Self-host is heavy. ClickHouse + Kafka + Postgres + Redis + Zookeeper. Don’t self-host unless you have a sysadmin who wants this.
- Session replay is power + risk. It records DOM mutations. Mask PII fields aggressively or you’ll have a GDPR incident.
- Identification gaps. Anonymous → identified merge can lose data. Test the merge path with synthetic users.
Frequently asked
Is PostHog GDPR-compliant out of the box?
PostHog Cloud (EU) is GDPR-compliant for EU traffic. Session replay needs careful PII masking. Self-host gives full control but you own the compliance work.
Can PostHog replace GA4 entirely?
For product-centric companies (SaaS, mobile apps, marketplaces) — yes. For content sites and e-commerce that depend on marketing-funnel analytics — no. Most teams keep a marketing analytics tool (Plausible / Matomo) alongside PostHog.
What about feature flags pricing?
PostHog Cloud bundles feature flags free up to 1M flag evaluations/month. Above that, $0.0001 per evaluation. Cheaper than LaunchDarkly for most.
Does PostHog support server-side tracking?
Yes — node-posthog, posthog-python, posthog-go SDKs. Server-side identification + capture works the same as client-side. CAPI-equivalent for ad platforms still needs separate work.
Can I move historical GA4 data to PostHog?
Technically yes (PostHog has a CSV import), but rarely worth it. The data models are different enough that historical comparisons across the boundary are misleading.