PostHog - All-in-One Product Analytics
Czym jest PostHog?
PostHog to open-source platforma product analytics, która łączy w sobie wszystkie narzędzia potrzebne do zrozumienia użytkowników: event tracking, session replay, feature flags, A/B testing i surveys. Zamiast korzystać z wielu rozproszonych narzędzi (Google Analytics, Mixpanel, LaunchDarkly, Hotjar), możesz mieć wszystko w jednym miejscu.
PostHog został założony w 2020 roku przez byłych inżynierów Y Combinator i szybko zdobył popularność dzięki modelowi open-source i privacy-first. Możesz hostować PostHog na własnej infrastrukturze lub korzystać z ich chmury - dane zawsze należą do Ciebie.
Dlaczego PostHog?
Kluczowe zalety
- All-in-one - Analytics, feature flags, A/B testing, surveys w jednym
- Open-source - MIT License, możesz hostować sam
- Privacy-first - GDPR compliant, kontrola nad danymi
- Self-hosted - Hostuj na własnej infrastrukturze
- Generous free tier - 1M events/month za darmo
- No sampling - Wszystkie dane, nie tylko próbki
- SQL access - Bezpośredni dostęp do danych przez SQL
PostHog vs inne rozwiązania
| Cecha | PostHog | Mixpanel | Amplitude | Google Analytics |
|---|---|---|---|---|
| Open-source | Tak | Nie | Nie | Nie |
| Self-hosted | Tak | Nie | Nie | Nie |
| Free tier | 1M events/mo | 20K MTU | 10M events/mo | Unlimited* |
| Feature flags | Wbudowane | Nie | Nie | Nie |
| A/B Testing | Wbudowane | Nie | Tak ($) | Tak (GA4) |
| Session Replay | Wbudowane | Nie | Tak ($) | Nie |
| Surveys | Wbudowane | Nie | Nie | Nie |
| SQL Access | Tak | Tak ($) | Nie | Nie |
| Funnels | Tak | Tak | Tak | Tak |
| Retention | Tak | Tak | Tak | Ograniczone |
Kiedy wybrać PostHog?
Idealne dla:
- Startupów i firm potrzebujących all-in-one solution
- Zespołów dbających o prywatność użytkowników
- Projektów wymagających self-hosting
- Firm chcących uniknąć vendor lock-in
- Zespołów potrzebujących feature flags + analytics
Rozważ alternatywy gdy:
- Potrzebujesz tylko prostych statystyk (użyj Plausible)
- Masz duży zespół analytics (rozważ enterprise tools)
- Wymagasz advanced ML/AI features
Instalacja
Next.js (App Router)
npm install posthog-js// lib/posthog.ts
import posthog from 'posthog-js'
export const initPostHog = () => {
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
capture_pageview: false, // Manualne tracking w Next.js
capture_pageleave: true,
persistence: 'localStorage',
autocapture: true,
session_recording: {
recordCrossOriginIframes: true,
},
})
}
}
export { posthog }// app/providers.tsx
'use client'
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import { initPostHog, posthog } from '@/lib/posthog'
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
initPostHog()
}, [])
// Track page views
useEffect(() => {
if (pathname) {
let url = window.origin + pathname
if (searchParams?.toString()) {
url = url + `?${searchParams.toString()}`
}
posthog.capture('$pageview', { $current_url: url })
}
}, [pathname, searchParams])
return <>{children}</>
}// app/layout.tsx
import { PostHogProvider } from './providers'
import { Suspense } from 'react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Suspense fallback={null}>
<PostHogProvider>{children}</PostHogProvider>
</Suspense>
</body>
</html>
)
}React (Vite/CRA)
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { PostHogProvider } from 'posthog-js/react'
import posthog from 'posthog-js'
import App from './App'
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://app.posthog.com',
capture_pageview: true,
capture_pageleave: true,
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<PostHogProvider client={posthog}>
<App />
</PostHogProvider>
</React.StrictMode>
)Node.js (Backend)
npm install posthog-node// lib/posthog-server.ts
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
host: process.env.POSTHOG_HOST || 'https://app.posthog.com',
flushAt: 20,
flushInterval: 10000,
})
export { posthog }
// Ważne: zamknij klienta przy shutdown
process.on('SIGTERM', async () => {
await posthog.shutdown()
})// Użycie w API route
import { posthog } from '@/lib/posthog-server'
export async function POST(request: Request) {
const { userId, email } = await request.json()
// Track event
posthog.capture({
distinctId: userId,
event: 'user_signed_up',
properties: {
email,
$set: {
email,
name: 'Jan Kowalski',
},
},
})
return Response.json({ success: true })
}Event Tracking
Podstawowe eventy
import { posthog } from '@/lib/posthog'
// Track prosty event
posthog.capture('button_clicked')
// Track event z properties
posthog.capture('button_clicked', {
button_name: 'signup',
page: '/home',
variant: 'blue',
})
// Track purchase
posthog.capture('purchase_completed', {
product_id: 'prod_123',
product_name: 'Premium Plan',
price: 99.99,
currency: 'PLN',
quantity: 1,
})
// Track search
posthog.capture('search_performed', {
query: 'react hooks',
results_count: 42,
category: 'tutorials',
})Page Views
// Automatyczne (domyślnie włączone)
posthog.init('key', {
capture_pageview: true,
})
// Manualne (zalecane dla SPA)
posthog.init('key', {
capture_pageview: false,
})
// W route change handler
useEffect(() => {
posthog.capture('$pageview', {
$current_url: window.location.href,
})
}, [pathname])Autocapture
// Automatyczne zbieranie kliknięć, form submissions itp.
posthog.init('key', {
autocapture: true, // Domyślnie włączone
})
// Wyłącz dla konkretnych elementów
// Dodaj atrybut data-ph-no-capture
<button data-ph-no-capture>Don't track this click</button>
// Lub CSS class
<button className="ph-no-capture">Don't track this</button>Custom Events Best Practices
// Konwencja nazewnictwa: snake_case, action_object
posthog.capture('user_signed_up')
posthog.capture('article_read')
posthog.capture('feature_activated')
posthog.capture('checkout_started')
posthog.capture('payment_completed')
// Unikaj
posthog.capture('UserSignedUp') // PascalCase
posthog.capture('user-signed-up') // kebab-case
posthog.capture('User Signed Up') // Spaces
// Grupuj powiązane eventy
posthog.capture('onboarding_step_completed', { step: 1, step_name: 'profile' })
posthog.capture('onboarding_step_completed', { step: 2, step_name: 'preferences' })
posthog.capture('onboarding_step_completed', { step: 3, step_name: 'team' })Identyfikacja użytkowników
Identify
// Po zalogowaniu użytkownika
posthog.identify(user.id, {
email: user.email,
name: user.name,
plan: user.subscription,
company: user.company,
created_at: user.createdAt,
})
// Aliasy dla tego samego użytkownika
posthog.alias(newUserId, oldUserId)
// Reset po wylogowaniu
posthog.reset()User Properties
// Ustaw properties raz (przy identify)
posthog.identify(userId, {
email: 'user@example.com',
plan: 'premium',
})
// Aktualizuj properties później
posthog.capture('$set', {
$set: {
plan: 'enterprise',
team_size: 50,
},
})
// Ustaw properties tylko jeśli nie istnieją
posthog.capture('$set', {
$set_once: {
first_seen: new Date().toISOString(),
acquisition_source: 'organic',
},
})Person Profiles
// Włącz person profiles dla pełnej historii
posthog.init('key', {
person_profiles: 'always', // lub 'identified_only'
})
// W dashboardzie możesz zobaczyć:
// - Pełną historię eventów użytkownika
// - Session recordings przypisane do użytkownika
// - Cohorts do których należyFeature Flags
Podstawowe użycie
import { posthog } from '@/lib/posthog'
// Sprawdź czy flag jest włączony
if (posthog.isFeatureEnabled('new-dashboard')) {
return <NewDashboard />
} else {
return <OldDashboard />
}
// Z React hook
import { useFeatureFlagEnabled } from 'posthog-js/react'
function Dashboard() {
const showNewDashboard = useFeatureFlagEnabled('new-dashboard')
if (showNewDashboard) {
return <NewDashboard />
}
return <OldDashboard />
}Multivariate Flags
import { useFeatureFlagVariantKey } from 'posthog-js/react'
function PricingPage() {
const variant = useFeatureFlagVariantKey('pricing-test')
switch (variant) {
case 'control':
return <PricingOriginal />
case 'variant-a':
return <PricingSimplified />
case 'variant-b':
return <PricingDetailed />
default:
return <PricingOriginal />
}
}Feature Flag Payloads
// Flagi mogą zawierać dodatkowe dane (JSON payload)
import { useFeatureFlagPayload } from 'posthog-js/react'
function Banner() {
const bannerConfig = useFeatureFlagPayload('promotional-banner')
if (!bannerConfig) return null
return (
<div style={{ backgroundColor: bannerConfig.backgroundColor }}>
<h2>{bannerConfig.title}</h2>
<p>{bannerConfig.message}</p>
<a href={bannerConfig.ctaUrl}>{bannerConfig.ctaText}</a>
</div>
)
}Server-side Feature Flags
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_API_KEY!)
// W API route
export async function GET(request: Request) {
const userId = request.headers.get('x-user-id')
const isEnabled = await posthog.isFeatureEnabled(
'new-api-version',
userId!,
{
personProperties: {
plan: 'enterprise',
},
}
)
if (isEnabled) {
return Response.json({ version: 'v2', data: newApiData })
}
return Response.json({ version: 'v1', data: oldApiData })
}Local Evaluation (szybsze)
// Pobierz wszystkie flagi raz i ewaluuj lokalnie
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY,
})
// Teraz flagi są ewaluowane lokalnie bez request do API
const flags = await posthog.getAllFlags(userId)A/B Testing (Experiments)
Tworzenie eksperymentu
// 1. Stwórz eksperyment w PostHog Dashboard
// 2. Zdefiniuj warianty (control, test)
// 3. Ustaw cel (conversion event)
// 4. Użyj w kodzie
import { useFeatureFlagVariantKey } from 'posthog-js/react'
function CheckoutPage() {
const checkoutVariant = useFeatureFlagVariantKey('checkout-flow-experiment')
// Track exposure (automatyczne dla feature flags)
switch (checkoutVariant) {
case 'control':
return <CheckoutClassic />
case 'simplified':
return <CheckoutSimplified />
case 'one-page':
return <CheckoutOnePage />
default:
return <CheckoutClassic />
}
}Tracking Conversions
// Track cel eksperymentu
function handlePurchase() {
// ... process purchase
// To jest cel zdefiniowany w eksperymencie
posthog.capture('purchase_completed', {
value: total,
currency: 'PLN',
})
}Analiza wyników
W PostHog Dashboard zobaczysz:
- Conversion rate dla każdego wariantu
- Statistical significance
- Confidence intervals
- Recommended winnerSession Replay
Konfiguracja
posthog.init('key', {
api_host: 'https://app.posthog.com',
// Session Replay settings
session_recording: {
// Włącz nagrywanie
recordCrossOriginIframes: true,
// Maskowanie wrażliwych danych
maskAllInputs: true,
maskInputOptions: {
password: true,
email: true,
},
// Blokuj nagrywanie konkretnych elementów
blockSelector: '.private-content, [data-private]',
// Ignoruj elementy (nie maskuj, po prostu pomiń)
ignoreSelector: '.noise-element',
},
// Sampling
session_recording: {
sampleRate: 0.5, // Nagrywaj 50% sesji
},
})Privacy Controls
<!-- Maskuj wrażliwe dane -->
<input type="password" data-ph-mask />
<!-- Blokuj całkowicie (czarny prostokąt) -->
<div data-ph-capture-attribute-private>
<CreditCardForm />
</div>
<!-- CSS class -->
<div className="ph-no-capture">
Sensitive content
</div>// Programowe maskowanie
posthog.init('key', {
session_recording: {
maskTextSelector: '.pii-data, [data-pii]',
blockSelector: '.payment-form',
},
})Nagrywanie on-demand
// Domyślnie wyłączone, włącz gdy potrzeba
posthog.init('key', {
disable_session_recording: true,
})
// Włącz dla konkretnego użytkownika
if (user.isPremium) {
posthog.startSessionRecording()
}
// Zatrzymaj
posthog.stopSessionRecording()Surveys
In-app Surveys
// Surveys konfiguruje się w PostHog Dashboard
// Możesz programowo pokazać survey
posthog.showSurvey('nps-survey')
// Lub ukryć
posthog.hideSurvey('nps-survey')
// Tracking odpowiedzi (automatyczne)
// PostHog automatycznie śledzi survey_shown, survey_dismissed, survey_sentCustom Survey UI
import { usePostHog } from 'posthog-js/react'
function CustomNPSSurvey() {
const posthog = usePostHog()
const handleSubmit = (score: number, feedback: string) => {
posthog.capture('survey_sent', {
$survey_id: 'nps-survey',
$survey_response: score,
$survey_response_1: feedback,
})
}
return (
<div>
<h3>How likely are you to recommend us?</h3>
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((score) => (
<button key={score} onClick={() => handleSubmit(score, '')}>
{score}
</button>
))}
</div>
)
}Funnels
Definiowanie funneli (w Dashboard)
Przykładowy funnel e-commerce:
1. page_viewed (page = /products)
2. product_added_to_cart
3. checkout_started
4. payment_entered
5. purchase_completed
PostHog automatycznie obliczy:
- Conversion rate między krokami
- Drop-off points
- Time to convert
- Breakdown by propertiesTracking dla funneli
// Upewnij się że trackujesz wszystkie kroki
// Krok 1: View products
posthog.capture('page_viewed', {
page: '/products',
category: 'electronics',
})
// Krok 2: Add to cart
posthog.capture('product_added_to_cart', {
product_id: 'prod_123',
product_name: 'iPhone 15',
price: 4999,
currency: 'PLN',
})
// Krok 3: Start checkout
posthog.capture('checkout_started', {
cart_value: 4999,
items_count: 1,
})
// Krok 4: Payment
posthog.capture('payment_entered', {
payment_method: 'card',
})
// Krok 5: Purchase
posthog.capture('purchase_completed', {
order_id: 'ord_123',
total: 4999,
payment_method: 'card',
})Cohorts
Tworzenie cohort (w Dashboard)
Przykładowe kohorty:
1. Power Users
- Więcej niż 10 sesji w ostatnich 30 dniach
- Użył funkcji X więcej niż 5 razy
2. Churned Users
- Ostatnia aktywność > 30 dni temu
- Miał aktywną subskrypcję
3. Enterprise Customers
- Property: plan = 'enterprise'
4. Mobile Users
- Property: $device_type = 'mobile'Targeting z cohorts
// Feature flags z cohort targeting (konfiguracja w Dashboard)
// Włącz flagę tylko dla Power Users
// W kodzie standardowe użycie
const isPowerUser = posthog.isFeatureEnabled('power-user-feature')Data Platform
HogQL (SQL)
-- Przykładowe zapytania w PostHog
-- Top pages
SELECT
properties.$current_url as url,
count() as views
FROM events
WHERE event = '$pageview'
AND timestamp > now() - interval 7 day
GROUP BY url
ORDER BY views DESC
LIMIT 10
-- Retention analysis
SELECT
dateTrunc('week', min(timestamp)) as cohort_week,
dateTrunc('week', timestamp) as activity_week,
count(DISTINCT person_id) as users
FROM events
WHERE event = 'user_active'
GROUP BY cohort_week, activity_week
ORDER BY cohort_week, activity_week
-- Feature flag usage
SELECT
properties.$feature_flag as flag,
properties.$feature_flag_response as variant,
count() as impressions
FROM events
WHERE event = '$feature_flag_called'
AND timestamp > now() - interval 30 day
GROUP BY flag, variant
ORDER BY impressions DESCData Export
// Export do własnego warehouse
// 1. Batch Export (S3, GCS, BigQuery)
// Konfiguracja w Dashboard > Data Pipeline
// 2. API Export
const response = await fetch(
'https://app.posthog.com/api/projects/@current/events?limit=1000',
{
headers: {
Authorization: `Bearer ${POSTHOG_PERSONAL_API_KEY}`,
},
}
)
const events = await response.json()Self-hosting
Docker
# docker-compose.yml
version: '3'
services:
posthog:
image: posthog/posthog:latest
depends_on:
- db
- redis
ports:
- '8000:8000'
environment:
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
REDIS_URL: 'redis://redis:6379/'
SECRET_KEY: 'your-secret-key-here'
SITE_URL: 'https://analytics.yoursite.com'
db:
image: postgres:12-alpine
environment:
POSTGRES_USER: posthog
POSTGRES_PASSWORD: posthog
POSTGRES_DB: posthog
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:6-alpine
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:Kubernetes (Helm)
# Add PostHog Helm repo
helm repo add posthog https://posthog.github.io/charts-clickhouse/
helm repo update
# Install
helm install posthog posthog/posthog \
--set cloud=aws \
--set ingress.hostname=analytics.yoursite.com \
--set posthog.SITE_URL=https://analytics.yoursite.comIntegracje
Slack
// Automatyczne alerty do Slack (konfiguracja w Dashboard)
// - Nowe insights
// - Feature flag changes
// - Experiment resultsZapier / Make
// Webhook do automatyzacji
// W PostHog: Data > Webhooks
// Trigger: Nowy event 'user_signed_up'
// Action: Send to Zapier webhook
// W Zapier:
// 1. Add to Mailchimp list
// 2. Create Slack notification
// 3. Add to CRMSegment
// PostHog jako destination w Segment
// W Segment Dashboard:
// 1. Add PostHog destination
// 2. Enter PostHog API key
// 3. Map events
// Wszystkie Segment events będą w PostHogReverse ETL
// Wyślij cohorts do innych narzędzi
// 1. W PostHog: Data > Destinations
// 2. Wybierz destination (HubSpot, Intercom, etc.)
// 3. Wybierz cohort do sync
// 4. Mapuj propertiesBest Practices
Event Naming
// Używaj spójnej konwencji
// Format: [object]_[action] lub [action]_[object]
// Dobre przykłady
posthog.capture('user_signed_up')
posthog.capture('article_viewed')
posthog.capture('checkout_completed')
posthog.capture('feature_activated')
// Unikaj
posthog.capture('click') // Zbyt ogólne
posthog.capture('UserSignedUp') // Niespójna konwencja
posthog.capture('signed_up_user') // Niespójna kolejnośćProperties
// Zawsze dodawaj kontekst
posthog.capture('purchase_completed', {
// Identyfikatory
order_id: 'ord_123',
product_id: 'prod_456',
// Wartości
value: 99.99,
currency: 'PLN',
// Kategorie
category: 'electronics',
subcategory: 'phones',
// Metadata
payment_method: 'card',
is_first_purchase: true,
coupon_code: 'SAVE20',
})Performance
// Batch events (domyślnie)
posthog.init('key', {
flush_interval: 10000, // 10 sekund
})
// Wyłącz autocapture jeśli nie potrzebujesz
posthog.init('key', {
autocapture: false,
})
// Użyj sampling dla session replay
posthog.init('key', {
session_recording: {
sampleRate: 0.1, // 10% sesji
},
})Privacy
// Respectuj user preferences
posthog.init('key', {
opt_out_capturing_by_default: true,
respect_dnt: true, // Respect Do Not Track
})
// User consent flow
function handleCookieConsent(accepted: boolean) {
if (accepted) {
posthog.opt_in_capturing()
} else {
posthog.opt_out_capturing()
}
}
// Nie trackuj wrażliwych danych
posthog.init('key', {
sanitize_properties: (properties, event) => {
// Usuń emaile z URL
if (properties.$current_url) {
properties.$current_url = properties.$current_url.replace(
/email=[^&]*/,
'email=REDACTED'
)
}
return properties
},
})Cennik
Free
- 1 million events/month
- 5,000 session recordings/month
- 1 million feature flag requests/month
- 250 survey responses/month
- Unlimited team members
- Community support
Paid (Pay as you go)
- Events: $0.00045 per event after 1M
- Session Replay: $0.04 per recording after 5K
- Feature Flags: $0.0001 per request after 1M
- Surveys: $0.20 per response after 250
- Priority support
- Dedicated CSM (wyższe wolumeny)
Self-hosted
- Free forever (MIT License)
- Własna infrastruktura
- Brak limitów (oprócz Twojego hardware)
- Community support
FAQ - Najczęściej zadawane pytania
Czy PostHog jest GDPR compliant?
Tak. PostHog oferuje:
- Self-hosting w EU
- EU Cloud (serwery w Frankfurcie)
- Wbudowane privacy controls
- Data deletion API
- Cookie-less tracking option
Ile kosztuje PostHog?
1M events/month jest darmowe. Powyżej płacisz $0.00045/event. Dla większości startupów i małych firm darmowy plan wystarcza.
Czy mogę migrować z Google Analytics?
Tak. PostHog ma import tool dla GA4. Możesz też używać obu równolegle podczas migracji.
Jak działa sampling?
PostHog nie stosuje sampling dla event data - zbierasz 100% eventów. Sampling jest dostępny tylko dla session recordings jako opcja.
Czy PostHog zastąpi LaunchDarkly?
Tak, feature flags w PostHog są fully-featured i mogą zastąpić dedykowane narzędzia. Masz też dodatkową korzyść - analytics feature flag usage są już zintegrowane.
Podsumowanie
PostHog to kompleksowe rozwiązanie product analytics, które łączy:
- Event Tracking - Pełne śledzenie zachowań użytkowników
- Feature Flags - Kontrolowane rollouts i A/B testing
- Session Replay - Nagrywanie i odtwarzanie sesji
- Surveys - Zbieranie feedbacku od użytkowników
- Funnels & Retention - Analiza konwersji i retencji
Jako open-source i privacy-first platforma, PostHog jest idealnym wyborem dla firm, które chcą mieć pełną kontrolę nad swoimi danymi analytics bez vendor lock-in.
PostHog - All-in-One Product Analytics
What is PostHog?
PostHog is an open-source product analytics platform that combines all the tools you need to understand your users: event tracking, session replay, feature flags, A/B testing, and surveys. Instead of using multiple scattered tools (Google Analytics, Mixpanel, LaunchDarkly, Hotjar), you can have everything in one place.
PostHog was founded in 2020 by former Y Combinator engineers and quickly gained popularity thanks to its open-source model and privacy-first approach. You can host PostHog on your own infrastructure or use their cloud - your data always belongs to you.
Why PostHog?
Key advantages
- All-in-one - Analytics, feature flags, A/B testing, surveys in one place
- Open-source - MIT License, you can self-host
- Privacy-first - GDPR compliant, full data control
- Self-hosted - Host on your own infrastructure
- Generous free tier - 1M events/month for free
- No sampling - All data, not just samples
- SQL access - Direct data access via SQL
PostHog vs other solutions
| Feature | PostHog | Mixpanel | Amplitude | Google Analytics |
|---|---|---|---|---|
| Open-source | Yes | No | No | No |
| Self-hosted | Yes | No | No | No |
| Free tier | 1M events/mo | 20K MTU | 10M events/mo | Unlimited* |
| Feature flags | Built-in | No | No | No |
| A/B Testing | Built-in | No | Yes ($) | Yes (GA4) |
| Session Replay | Built-in | No | Yes ($) | No |
| Surveys | Built-in | No | No | No |
| SQL Access | Yes | Yes ($) | No | No |
| Funnels | Yes | Yes | Yes | Yes |
| Retention | Yes | Yes | Yes | Limited |
When to choose PostHog?
Ideal for:
- Startups and companies needing an all-in-one solution
- Teams that care about user privacy
- Projects requiring self-hosting
- Companies wanting to avoid vendor lock-in
- Teams needing feature flags + analytics
Consider alternatives when:
- You only need simple statistics (use Plausible)
- You have a large analytics team (consider enterprise tools)
- You require advanced ML/AI features
Installation
Next.js (App Router)
npm install posthog-js// lib/posthog.ts
import posthog from 'posthog-js'
export const initPostHog = () => {
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
capture_pageview: false, // Manual tracking in Next.js
capture_pageleave: true,
persistence: 'localStorage',
autocapture: true,
session_recording: {
recordCrossOriginIframes: true,
},
})
}
}
export { posthog }// app/providers.tsx
'use client'
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import { initPostHog, posthog } from '@/lib/posthog'
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
initPostHog()
}, [])
// Track page views
useEffect(() => {
if (pathname) {
let url = window.origin + pathname
if (searchParams?.toString()) {
url = url + `?${searchParams.toString()}`
}
posthog.capture('$pageview', { $current_url: url })
}
}, [pathname, searchParams])
return <>{children}</>
}// app/layout.tsx
import { PostHogProvider } from './providers'
import { Suspense } from 'react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Suspense fallback={null}>
<PostHogProvider>{children}</PostHogProvider>
</Suspense>
</body>
</html>
)
}React (Vite/CRA)
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { PostHogProvider } from 'posthog-js/react'
import posthog from 'posthog-js'
import App from './App'
posthog.init(import.meta.env.VITE_POSTHOG_KEY, {
api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://app.posthog.com',
capture_pageview: true,
capture_pageleave: true,
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<PostHogProvider client={posthog}>
<App />
</PostHogProvider>
</React.StrictMode>
)Node.js (Backend)
npm install posthog-node// lib/posthog-server.ts
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
host: process.env.POSTHOG_HOST || 'https://app.posthog.com',
flushAt: 20,
flushInterval: 10000,
})
export { posthog }
// Important: close the client on shutdown
process.on('SIGTERM', async () => {
await posthog.shutdown()
})// Usage in API route
import { posthog } from '@/lib/posthog-server'
export async function POST(request: Request) {
const { userId, email } = await request.json()
// Track event
posthog.capture({
distinctId: userId,
event: 'user_signed_up',
properties: {
email,
$set: {
email,
name: 'John Smith',
},
},
})
return Response.json({ success: true })
}Event Tracking
Basic events
import { posthog } from '@/lib/posthog'
// Track simple event
posthog.capture('button_clicked')
// Track event with properties
posthog.capture('button_clicked', {
button_name: 'signup',
page: '/home',
variant: 'blue',
})
// Track purchase
posthog.capture('purchase_completed', {
product_id: 'prod_123',
product_name: 'Premium Plan',
price: 99.99,
currency: 'PLN',
quantity: 1,
})
// Track search
posthog.capture('search_performed', {
query: 'react hooks',
results_count: 42,
category: 'tutorials',
})Page Views
// Automatic (enabled by default)
posthog.init('key', {
capture_pageview: true,
})
// Manual (recommended for SPA)
posthog.init('key', {
capture_pageview: false,
})
// In route change handler
useEffect(() => {
posthog.capture('$pageview', {
$current_url: window.location.href,
})
}, [pathname])Autocapture
// Automatic collection of clicks, form submissions, etc.
posthog.init('key', {
autocapture: true, // Enabled by default
})
// Disable for specific elements
// Add the data-ph-no-capture attribute
<button data-ph-no-capture>Don't track this click</button>
// Or CSS class
<button className="ph-no-capture">Don't track this</button>Custom Events Best Practices
// Naming convention: snake_case, action_object
posthog.capture('user_signed_up')
posthog.capture('article_read')
posthog.capture('feature_activated')
posthog.capture('checkout_started')
posthog.capture('payment_completed')
// Avoid
posthog.capture('UserSignedUp') // PascalCase
posthog.capture('user-signed-up') // kebab-case
posthog.capture('User Signed Up') // Spaces
// Group related events
posthog.capture('onboarding_step_completed', { step: 1, step_name: 'profile' })
posthog.capture('onboarding_step_completed', { step: 2, step_name: 'preferences' })
posthog.capture('onboarding_step_completed', { step: 3, step_name: 'team' })User identification
Identify
// After user logs in
posthog.identify(user.id, {
email: user.email,
name: user.name,
plan: user.subscription,
company: user.company,
created_at: user.createdAt,
})
// Aliases for the same user
posthog.alias(newUserId, oldUserId)
// Reset after logout
posthog.reset()User Properties
// Set properties once (during identify)
posthog.identify(userId, {
email: 'user@example.com',
plan: 'premium',
})
// Update properties later
posthog.capture('$set', {
$set: {
plan: 'enterprise',
team_size: 50,
},
})
// Set properties only if they don't already exist
posthog.capture('$set', {
$set_once: {
first_seen: new Date().toISOString(),
acquisition_source: 'organic',
},
})Person Profiles
// Enable person profiles for full history
posthog.init('key', {
person_profiles: 'always', // or 'identified_only'
})
// In the dashboard you can see:
// - Full event history for the user
// - Session recordings assigned to the user
// - Cohorts the user belongs toFeature Flags
Basic usage
import { posthog } from '@/lib/posthog'
// Check if flag is enabled
if (posthog.isFeatureEnabled('new-dashboard')) {
return <NewDashboard />
} else {
return <OldDashboard />
}
// With React hook
import { useFeatureFlagEnabled } from 'posthog-js/react'
function Dashboard() {
const showNewDashboard = useFeatureFlagEnabled('new-dashboard')
if (showNewDashboard) {
return <NewDashboard />
}
return <OldDashboard />
}Multivariate Flags
import { useFeatureFlagVariantKey } from 'posthog-js/react'
function PricingPage() {
const variant = useFeatureFlagVariantKey('pricing-test')
switch (variant) {
case 'control':
return <PricingOriginal />
case 'variant-a':
return <PricingSimplified />
case 'variant-b':
return <PricingDetailed />
default:
return <PricingOriginal />
}
}Feature Flag Payloads
// Flags can contain additional data (JSON payload)
import { useFeatureFlagPayload } from 'posthog-js/react'
function Banner() {
const bannerConfig = useFeatureFlagPayload('promotional-banner')
if (!bannerConfig) return null
return (
<div style={{ backgroundColor: bannerConfig.backgroundColor }}>
<h2>{bannerConfig.title}</h2>
<p>{bannerConfig.message}</p>
<a href={bannerConfig.ctaUrl}>{bannerConfig.ctaText}</a>
</div>
)
}Server-side Feature Flags
import { PostHog } from 'posthog-node'
const posthog = new PostHog(process.env.POSTHOG_API_KEY!)
// In API route
export async function GET(request: Request) {
const userId = request.headers.get('x-user-id')
const isEnabled = await posthog.isFeatureEnabled(
'new-api-version',
userId!,
{
personProperties: {
plan: 'enterprise',
},
}
)
if (isEnabled) {
return Response.json({ version: 'v2', data: newApiData })
}
return Response.json({ version: 'v1', data: oldApiData })
}Local Evaluation (faster)
// Fetch all flags once and evaluate locally
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY,
})
// Now flags are evaluated locally without API requests
const flags = await posthog.getAllFlags(userId)A/B Testing (Experiments)
Creating an experiment
// 1. Create an experiment in PostHog Dashboard
// 2. Define variants (control, test)
// 3. Set the goal (conversion event)
// 4. Use in code
import { useFeatureFlagVariantKey } from 'posthog-js/react'
function CheckoutPage() {
const checkoutVariant = useFeatureFlagVariantKey('checkout-flow-experiment')
// Track exposure (automatic for feature flags)
switch (checkoutVariant) {
case 'control':
return <CheckoutClassic />
case 'simplified':
return <CheckoutSimplified />
case 'one-page':
return <CheckoutOnePage />
default:
return <CheckoutClassic />
}
}Tracking Conversions
// Track experiment goal
function handlePurchase() {
// ... process purchase
// This is the goal defined in the experiment
posthog.capture('purchase_completed', {
value: total,
currency: 'PLN',
})
}Analyzing results
In PostHog Dashboard you will see:
- Conversion rate for each variant
- Statistical significance
- Confidence intervals
- Recommended winnerSession Replay
Configuration
posthog.init('key', {
api_host: 'https://app.posthog.com',
// Session Replay settings
session_recording: {
// Enable recording
recordCrossOriginIframes: true,
// Masking sensitive data
maskAllInputs: true,
maskInputOptions: {
password: true,
email: true,
},
// Block recording of specific elements
blockSelector: '.private-content, [data-private]',
// Ignore elements (don't mask, just skip)
ignoreSelector: '.noise-element',
},
// Sampling
session_recording: {
sampleRate: 0.5, // Record 50% of sessions
},
})Privacy Controls
<!-- Mask sensitive data -->
<input type="password" data-ph-mask />
<!-- Block entirely (black rectangle) -->
<div data-ph-capture-attribute-private>
<CreditCardForm />
</div>
<!-- CSS class -->
<div className="ph-no-capture">
Sensitive content
</div>// Programmatic masking
posthog.init('key', {
session_recording: {
maskTextSelector: '.pii-data, [data-pii]',
blockSelector: '.payment-form',
},
})On-demand recording
// Disabled by default, enable when needed
posthog.init('key', {
disable_session_recording: true,
})
// Enable for a specific user
if (user.isPremium) {
posthog.startSessionRecording()
}
// Stop
posthog.stopSessionRecording()Surveys
In-app Surveys
// Surveys are configured in PostHog Dashboard
// You can programmatically show a survey
posthog.showSurvey('nps-survey')
// Or hide it
posthog.hideSurvey('nps-survey')
// Response tracking (automatic)
// PostHog automatically tracks survey_shown, survey_dismissed, survey_sentCustom Survey UI
import { usePostHog } from 'posthog-js/react'
function CustomNPSSurvey() {
const posthog = usePostHog()
const handleSubmit = (score: number, feedback: string) => {
posthog.capture('survey_sent', {
$survey_id: 'nps-survey',
$survey_response: score,
$survey_response_1: feedback,
})
}
return (
<div>
<h3>How likely are you to recommend us?</h3>
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((score) => (
<button key={score} onClick={() => handleSubmit(score, '')}>
{score}
</button>
))}
</div>
)
}Funnels
Defining funnels (in Dashboard)
Example e-commerce funnel:
1. page_viewed (page = /products)
2. product_added_to_cart
3. checkout_started
4. payment_entered
5. purchase_completed
PostHog will automatically calculate:
- Conversion rate between steps
- Drop-off points
- Time to convert
- Breakdown by propertiesTracking for funnels
// Make sure you track all steps
// Step 1: View products
posthog.capture('page_viewed', {
page: '/products',
category: 'electronics',
})
// Step 2: Add to cart
posthog.capture('product_added_to_cart', {
product_id: 'prod_123',
product_name: 'iPhone 15',
price: 4999,
currency: 'PLN',
})
// Step 3: Start checkout
posthog.capture('checkout_started', {
cart_value: 4999,
items_count: 1,
})
// Step 4: Payment
posthog.capture('payment_entered', {
payment_method: 'card',
})
// Step 5: Purchase
posthog.capture('purchase_completed', {
order_id: 'ord_123',
total: 4999,
payment_method: 'card',
})Cohorts
Creating cohorts (in Dashboard)
Example cohorts:
1. Power Users
- More than 10 sessions in the last 30 days
- Used feature X more than 5 times
2. Churned Users
- Last activity > 30 days ago
- Had an active subscription
3. Enterprise Customers
- Property: plan = 'enterprise'
4. Mobile Users
- Property: $device_type = 'mobile'Targeting with cohorts
// Feature flags with cohort targeting (configured in Dashboard)
// Enable flag only for Power Users
// Standard usage in code
const isPowerUser = posthog.isFeatureEnabled('power-user-feature')Data Platform
HogQL (SQL)
-- Example queries in PostHog
-- Top pages
SELECT
properties.$current_url as url,
count() as views
FROM events
WHERE event = '$pageview'
AND timestamp > now() - interval 7 day
GROUP BY url
ORDER BY views DESC
LIMIT 10
-- Retention analysis
SELECT
dateTrunc('week', min(timestamp)) as cohort_week,
dateTrunc('week', timestamp) as activity_week,
count(DISTINCT person_id) as users
FROM events
WHERE event = 'user_active'
GROUP BY cohort_week, activity_week
ORDER BY cohort_week, activity_week
-- Feature flag usage
SELECT
properties.$feature_flag as flag,
properties.$feature_flag_response as variant,
count() as impressions
FROM events
WHERE event = '$feature_flag_called'
AND timestamp > now() - interval 30 day
GROUP BY flag, variant
ORDER BY impressions DESCData Export
// Export to your own warehouse
// 1. Batch Export (S3, GCS, BigQuery)
// Configuration in Dashboard > Data Pipeline
// 2. API Export
const response = await fetch(
'https://app.posthog.com/api/projects/@current/events?limit=1000',
{
headers: {
Authorization: `Bearer ${POSTHOG_PERSONAL_API_KEY}`,
},
}
)
const events = await response.json()Self-hosting
Docker
# docker-compose.yml
version: '3'
services:
posthog:
image: posthog/posthog:latest
depends_on:
- db
- redis
ports:
- '8000:8000'
environment:
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
REDIS_URL: 'redis://redis:6379/'
SECRET_KEY: 'your-secret-key-here'
SITE_URL: 'https://analytics.yoursite.com'
db:
image: postgres:12-alpine
environment:
POSTGRES_USER: posthog
POSTGRES_PASSWORD: posthog
POSTGRES_DB: posthog
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:6-alpine
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:Kubernetes (Helm)
# Add PostHog Helm repo
helm repo add posthog https://posthog.github.io/charts-clickhouse/
helm repo update
# Install
helm install posthog posthog/posthog \
--set cloud=aws \
--set ingress.hostname=analytics.yoursite.com \
--set posthog.SITE_URL=https://analytics.yoursite.comIntegrations
Slack
// Automatic alerts to Slack (configured in Dashboard)
// - New insights
// - Feature flag changes
// - Experiment resultsZapier / Make
// Webhook for automation
// In PostHog: Data > Webhooks
// Trigger: New event 'user_signed_up'
// Action: Send to Zapier webhook
// In Zapier:
// 1. Add to Mailchimp list
// 2. Create Slack notification
// 3. Add to CRMSegment
// PostHog as a destination in Segment
// In Segment Dashboard:
// 1. Add PostHog destination
// 2. Enter PostHog API key
// 3. Map events
// All Segment events will be in PostHogReverse ETL
// Send cohorts to other tools
// 1. In PostHog: Data > Destinations
// 2. Choose destination (HubSpot, Intercom, etc.)
// 3. Select cohort to sync
// 4. Map propertiesBest Practices
Event Naming
// Use a consistent convention
// Format: [object]_[action] or [action]_[object]
// Good examples
posthog.capture('user_signed_up')
posthog.capture('article_viewed')
posthog.capture('checkout_completed')
posthog.capture('feature_activated')
// Avoid
posthog.capture('click') // Too generic
posthog.capture('UserSignedUp') // Inconsistent convention
posthog.capture('signed_up_user') // Inconsistent orderProperties
// Always add context
posthog.capture('purchase_completed', {
// Identifiers
order_id: 'ord_123',
product_id: 'prod_456',
// Values
value: 99.99,
currency: 'PLN',
// Categories
category: 'electronics',
subcategory: 'phones',
// Metadata
payment_method: 'card',
is_first_purchase: true,
coupon_code: 'SAVE20',
})Performance
// Batch events (default)
posthog.init('key', {
flush_interval: 10000, // 10 seconds
})
// Disable autocapture if you don't need it
posthog.init('key', {
autocapture: false,
})
// Use sampling for session replay
posthog.init('key', {
session_recording: {
sampleRate: 0.1, // 10% of sessions
},
})Privacy
// Respect user preferences
posthog.init('key', {
opt_out_capturing_by_default: true,
respect_dnt: true, // Respect Do Not Track
})
// User consent flow
function handleCookieConsent(accepted: boolean) {
if (accepted) {
posthog.opt_in_capturing()
} else {
posthog.opt_out_capturing()
}
}
// Don't track sensitive data
posthog.init('key', {
sanitize_properties: (properties, event) => {
// Remove emails from URL
if (properties.$current_url) {
properties.$current_url = properties.$current_url.replace(
/email=[^&]*/,
'email=REDACTED'
)
}
return properties
},
})Pricing
Free
- 1 million events/month
- 5,000 session recordings/month
- 1 million feature flag requests/month
- 250 survey responses/month
- Unlimited team members
- Community support
Paid (Pay as you go)
- Events: $0.00045 per event after 1M
- Session Replay: $0.04 per recording after 5K
- Feature Flags: $0.0001 per request after 1M
- Surveys: $0.20 per response after 250
- Priority support
- Dedicated CSM (higher volumes)
Self-hosted
- Free forever (MIT License)
- Your own infrastructure
- No limits (except your hardware)
- Community support
FAQ - Frequently asked questions
Is PostHog GDPR compliant?
Yes. PostHog offers:
- Self-hosting in EU
- EU Cloud (servers in Frankfurt)
- Built-in privacy controls
- Data deletion API
- Cookie-less tracking option
How much does PostHog cost?
1M events/month is free. Above that, you pay $0.00045/event. For most startups and small companies, the free plan is sufficient.
Can I migrate from Google Analytics?
Yes. PostHog has an import tool for GA4. You can also use both in parallel during migration.
How does sampling work?
PostHog does not apply sampling for event data - you collect 100% of events. Sampling is only available for session recordings as an option.
Will PostHog replace LaunchDarkly?
Yes, feature flags in PostHog are fully-featured and can replace dedicated tools. You also get an additional benefit - analytics for feature flag usage are already integrated.
Summary
PostHog is a comprehensive product analytics solution that combines:
- Event Tracking - Full tracking of user behavior
- Feature Flags - Controlled rollouts and A/B testing
- Session Replay - Recording and playback of sessions
- Surveys - Collecting feedback from users
- Funnels & Retention - Conversion and retention analysis
As an open-source and privacy-first platform, PostHog is the ideal choice for companies that want full control over their analytics data without vendor lock-in.