Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide28 min read

PostHog

PostHog is open-source product analytics with feature flags, session replay, and A/B testing. Complete guide.

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

  1. All-in-one - Analytics, feature flags, A/B testing, surveys w jednym
  2. Open-source - MIT License, możesz hostować sam
  3. Privacy-first - GDPR compliant, kontrola nad danymi
  4. Self-hosted - Hostuj na własnej infrastrukturze
  5. Generous free tier - 1M events/month za darmo
  6. No sampling - Wszystkie dane, nie tylko próbki
  7. SQL access - Bezpośredni dostęp do danych przez SQL

PostHog vs inne rozwiązania

CechaPostHogMixpanelAmplitudeGoogle Analytics
Open-sourceTakNieNieNie
Self-hostedTakNieNieNie
Free tier1M events/mo20K MTU10M events/moUnlimited*
Feature flagsWbudowaneNieNieNie
A/B TestingWbudowaneNieTak ($)Tak (GA4)
Session ReplayWbudowaneNieTak ($)Nie
SurveysWbudowaneNieNieNie
SQL AccessTakTak ($)NieNie
FunnelsTakTakTakTak
RetentionTakTakTakOgraniczone

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)

Code
Bash
npm install posthog-js
TSlib/posthog.ts
TypeScript
// 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 }
TSapp/providers.tsx
TypeScript
// 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}</>
}
TSapp/layout.tsx
TypeScript
// 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)

TSmain.tsx
TypeScript
// 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)

Code
Bash
npm install posthog-node
TSlib/posthog-server.ts
TypeScript
// 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()
})
Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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ży

Feature Flags

Podstawowe użycie

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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)

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// Track cel eksperymentu
function handlePurchase() {
  // ... process purchase

  // To jest cel zdefiniowany w eksperymencie
  posthog.capture('purchase_completed', {
    value: total,
    currency: 'PLN',
  })
}

Analiza wyników

Code
TEXT
W PostHog Dashboard zobaczysz:
- Conversion rate dla każdego wariantu
- Statistical significance
- Confidence intervals
- Recommended winner

Session Replay

Konfiguracja

Code
TypeScript
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

Code
HTML
<!-- 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>
Code
TypeScript
// Programowe maskowanie
posthog.init('key', {
  session_recording: {
    maskTextSelector: '.pii-data, [data-pii]',
    blockSelector: '.payment-form',
  },
})

Nagrywanie on-demand

Code
TypeScript
// 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

Code
TypeScript
// 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_sent

Custom Survey UI

Code
TypeScript
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)

Code
TEXT
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 properties

Tracking dla funneli

Code
TypeScript
// 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)

Code
TEXT
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

Code
TypeScript
// 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)

Code
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 DESC

Data Export

Code
TypeScript
// 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
YAML
# 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)

Code
Bash
# 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.com

Integracje

Slack

Code
TypeScript
// Automatyczne alerty do Slack (konfiguracja w Dashboard)
// - Nowe insights
// - Feature flag changes
// - Experiment results

Zapier / Make

Code
TypeScript
// 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 CRM

Segment

Code
TypeScript
// 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 PostHog

Reverse ETL

Code
TypeScript
// 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 properties

Best Practices

Event Naming

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

  1. All-in-one - Analytics, feature flags, A/B testing, surveys in one place
  2. Open-source - MIT License, you can self-host
  3. Privacy-first - GDPR compliant, full data control
  4. Self-hosted - Host on your own infrastructure
  5. Generous free tier - 1M events/month for free
  6. No sampling - All data, not just samples
  7. SQL access - Direct data access via SQL

PostHog vs other solutions

FeaturePostHogMixpanelAmplitudeGoogle Analytics
Open-sourceYesNoNoNo
Self-hostedYesNoNoNo
Free tier1M events/mo20K MTU10M events/moUnlimited*
Feature flagsBuilt-inNoNoNo
A/B TestingBuilt-inNoYes ($)Yes (GA4)
Session ReplayBuilt-inNoYes ($)No
SurveysBuilt-inNoNoNo
SQL AccessYesYes ($)NoNo
FunnelsYesYesYesYes
RetentionYesYesYesLimited

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)

Code
Bash
npm install posthog-js
TSlib/posthog.ts
TypeScript
// 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 }
TSapp/providers.tsx
TypeScript
// 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}</>
}
TSapp/layout.tsx
TypeScript
// 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)

TSmain.tsx
TypeScript
// 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)

Code
Bash
npm install posthog-node
TSlib/posthog-server.ts
TypeScript
// 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()
})
Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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 to

Feature Flags

Basic usage

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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)

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TEXT
In PostHog Dashboard you will see:
- Conversion rate for each variant
- Statistical significance
- Confidence intervals
- Recommended winner

Session Replay

Configuration

Code
TypeScript
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

Code
HTML
<!-- 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>
Code
TypeScript
// Programmatic masking
posthog.init('key', {
  session_recording: {
    maskTextSelector: '.pii-data, [data-pii]',
    blockSelector: '.payment-form',
  },
})

On-demand recording

Code
TypeScript
// 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

Code
TypeScript
// 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_sent

Custom Survey UI

Code
TypeScript
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)

Code
TEXT
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 properties

Tracking for funnels

Code
TypeScript
// 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)

Code
TEXT
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

Code
TypeScript
// 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)

Code
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 DESC

Data Export

Code
TypeScript
// 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
YAML
# 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)

Code
Bash
# 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.com

Integrations

Slack

Code
TypeScript
// Automatic alerts to Slack (configured in Dashboard)
// - New insights
// - Feature flag changes
// - Experiment results

Zapier / Make

Code
TypeScript
// 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 CRM

Segment

Code
TypeScript
// 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 PostHog

Reverse ETL

Code
TypeScript
// Send cohorts to other tools

// 1. In PostHog: Data > Destinations
// 2. Choose destination (HubSpot, Intercom, etc.)
// 3. Select cohort to sync
// 4. Map properties

Best Practices

Event Naming

Code
TypeScript
// 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 order

Properties

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
// 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.