Track what users do, run feature flags, watch session replays — one tool, one signup. Free tier covers 1M events/month + 5K replays. The default analytics stack for indie products.
Alternatives: Plausible ↗ (lighter, privacy-first, page-view focused), Mixpanel ↗, Amplitude ↗. PostHog wins on "single tool covers everything" and a generous free tier.
Sign up at app.posthog.com ↗. Pick the US Cloud or EU Cloud region (matters for GDPR). Create a project; name it after your app.
Copy your Project API Key (looks like phc_xxxxxxxxxxxxxxxx). Visible at Settings → Project → General. Safe to ship to browsers — it's a write-only key with no access to your data.
Easiest: snippet in HTML (works for any web app):
<script>
!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.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/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 getNextSurveyStep onSessionId".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_YOUR_KEY_HERE", { api_host: "https://us.i.posthog.com" });
</script>
npm version (React/Next.js/Vue):
npm install posthog-js
import posthog from "posthog-js";
if (typeof window !== "undefined") {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "https://us.i.posthog.com",
});
}
The snippet auto-captures: page views, clicks, form fills, page-leave events. You'll see them appear in PostHog within seconds of loading the page.
All libraries follow the same pattern: init, capture, identify.
// Web / React
posthog.capture("upload_started", {
file_type: "image/png",
file_size_bytes: 12345,
});
Naming convention worth adopting: noun_verb with snake_case. upload_started, checkout_completed, tutorial_finished. Consistent names make funnels and dashboards readable a year from now.
See it in PostHog → Activity → Live Events. Latency to dashboard is usually under 5 seconds.
Right after sign-in, tell PostHog who this anonymous person actually is:
posthog.identify(user.id, {
email: user.email,
plan: user.plan,
signed_up_at: user.created_at,
});
From now on, every event from this browser is associated with that user. PostHog stitches the anonymous pre-sign-in events to the identified user — so the funnel "landed on page → signed up → purchased" stays intact.
On sign-out, reset:
posthog.reset(); // clears identity, generates a new anonymous ID
PostHog → Product analytics → Funnels → New funnel. Pick a sequence of events:
page_view (on landing page)sign_up_startedsign_up_completedupload_startedPostHog shows conversion at each step. The biggest drop-off is your highest-leverage UX problem. Funnels docs ↗.
PostHog → Session replay → Settings → toggle on. (Or pass session_recording: { recordCrossOriginIframes: false } in init.)
Sensitive inputs (passwords, credit cards) are auto-masked. For specific elements, add data-ph-no-capture:
<input data-ph-no-capture type="text" name="ssn">
Replays are scoped to events — you can click any event in PostHog and watch the session that contained it. Useful for "this user signed up but then bounced; what happened?"
Roll out a feature to N% of users without redeploying. PostHog → Feature flags → New feature flag. Pick a key (e.g., new-checkout), set rollout to 10%, save.
In your app:
// After init
posthog.onFeatureFlags(() => {
if (posthog.isFeatureEnabled("new-checkout")) {
renderNewCheckout();
} else {
renderOldCheckout();
}
});
Bump the percentage as confidence grows. Stop the rollout if metrics go bad. Feature flags docs ↗.
Feature flags can have multiple variants:
const variant = posthog.getFeatureFlag("checkout-button-color");
// returns "control" | "blue" | "green" — randomized per user
if (variant === "green") renderGreenButton();
else if (variant === "blue") renderBlueButton();
else renderControlButton();
PostHog records which variant each user saw. Run for a week, look at conversion per variant. Experiments docs ↗.
Three knobs that matter:
posthog.opt_out_capturing(). opt_in_capturing() to re-enable.PostHog is open-source. You can self-host ↗ via Docker Compose or Kubernetes. Production self-hosting is real work — Kafka, ClickHouse, Postgres, Redis, MinIO. Most projects don't bother and use PostHog Cloud.
Free tier: 1M events + 5K session recordings + 1M feature flag requests per month. Real for early stage. Pricing scales linearly past that. PostHog pricing ↗.