TL;DR
You can have reliable custom event tracking running in PostHog in about 90 minutes if your dev environment is already set up. You need a PostHog account (free tier works), access to your codebase or tag manager, and a short list of the user actions that actually matter to your product. Getting this right saves you weeks of debugging bad data later.
What You Need Before You Start
- A PostHog account — the free tier covers up to 1 million events per month, which is enough to follow every step here
- Access to your product’s frontend codebase, or admin access to Google Tag Manager if you don’t write code directly
- Node.js 16+ if you’re installing the JavaScript SDK via npm, or just a
<head>tag if you’re using the HTML snippet - A rough list of 5-10 user actions you want to track (button clicks, form submissions, page transitions, API call outcomes)
- Optional: a staging environment so test events don’t pollute your production data
- Optional: the PostHog Toolbar browser extension for visual, no-code event creation
Step 1: Create Your PostHog Project and Grab Your API Key
Go to app.posthog.com and create an account. After signup, PostHog walks you through creating a project. Give it a clear name tied to your actual product, not something like “test” or “sandbox.” Name it the way you’d say it in a team standup.
Once your project is created, navigate to Settings > Project in the left sidebar. Copy your Project API Key — it starts with phc_. You need this in every installation method.
Also note your Host. PostHog Cloud US uses https://us.i.posthog.com. EU Cloud users use https://eu.i.posthog.com. Self-hosted instances use your own domain.
You should now see your project dashboard with zero events and an empty activity feed.
Step 2: Install the PostHog Snippet or SDK
Pick your installation method based on your stack.
Option A: HTML snippet (fastest path)
Paste this into the <head> of your HTML before other scripts:
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],
e.init=function(i,s,a){/* PostHog full snippet from their docs */})}
(document,window.posthog||[]);
posthog.init('phc_YOUR_API_KEY', {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only'
})
</script>
Copy the full minified snippet from the PostHog docs rather than typing it manually. Replace phc_YOUR_API_KEY with the key from Step 1.
Option B: npm (recommended for React, Next.js, Vue)
npm install posthog-js
Initialize once at your app’s entry point:
import posthog from 'posthog-js'
posthog.init('phc_YOUR_API_KEY', {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only'
})
You should now see a $pageview event appear in the PostHog Activity tab within 30 seconds of loading your app.
Step 3: Plan Your Event Taxonomy Before Writing More Code
This step has no code. It is also the step most product managers skip, and it is the reason event tracking turns into a mess six months later.
Open a spreadsheet or a Notion doc and define your events like this:
| Event Name | Trigger | Key Properties |
|---|---|---|
user_signed_up |
form submit on /register | plan, referral_source |
onboarding_step_completed |
each wizard step | step_number, step_name |
feature_used |
first use of any core feature | feature_name, user_role |
subscription_upgraded |
checkout success | from_plan, to_plan, amount_usd |
Use snake_case for every event name. Be consistent from day one. buttonClicked and button_clicked are two different events in PostHog and merging them later is painful.
Limit your initial list to 10-15 events. You can always add more. You cannot easily clean up a taxonomy that grew without a plan.
You should now have a written spec that your developer, or you, can implement event by event without guessing.
Step 4: Capture Your First Custom Event
With your taxonomy doc open, fire your first custom event using posthog.capture():
posthog.capture('user_signed_up', {
plan: 'free',
referral_source: 'google_organic'
})
The first argument is the event name. The second is an object of properties. Properties are optional, but you should almost always include them. A bare posthog.capture('button_clicked') tells you nothing useful at scale.
For a React component example:
<button
onClick={() => {
posthog.capture('upgrade_button_clicked', {
location: 'pricing_page',
current_plan: user.plan
})
}}
>
Upgrade Now
</button>
Place posthog.capture() at the moment the action completes, not when it starts. For form submissions, fire it after successful API response. For purchases, fire it in the payment success callback.
You should now see your custom event appear in the PostHog Activity tab with all the properties attached.
Step 5: Identify Users and Set Person Properties
Anonymous events are fine for aggregate charts. They are nearly useless for debugging individual user journeys. Call posthog.identify() as soon as a user logs in or completes signup:
posthog.identify(
user.id,
{
email: user.email,
name: user.full_name,
plan: user.subscription_plan,
created_at: user.created_at
}
)
Use your internal database ID as the distinct ID, not the user’s email. Emails change. Database IDs do not.
For properties that should be set exactly once and never overwritten, like signup_source, use:
posthog.people.set_once({ signup_source: 'organic_search' })
This prevents a later login from overwriting the original acquisition channel.
You should now see a named person in the Persons tab with their properties attached, and all prior anonymous events merged into their profile.
Step 6: Use Autocapture and Actions for Clicks You Did Not Instrument
PostHog’s autocapture records every click, form change, and submit on your page by default. It is a safety net, not a replacement for explicit tracking. But it lets you create “Actions” retroactively without a new deploy.
To create an Action from autocaptured data:
- Go to Data Management > Actions in the left sidebar
- Click New Action
- Choose Inspect element on your site
- The PostHog Toolbar launches on your live site
- Click the element you want to track
- Configure match rules by CSS selector, element text, or URL pattern
- Name your Action (e.g., “Clicked Pricing CTA”) and save
Actions appear as first-class events in funnels and insights, exactly like custom-captured events.
You should now see your new Action listed under Data Management > Actions with a count of matching historical events already populated.
Step 7: Validate Events in the Live Feed
Before building any funnels or dashboards, verify that every event looks exactly the way your taxonomy spec says it should.
Go to Activity > Live Events. Open your app in a separate browser tab. Trigger each event one by one. Events appear in the live feed within a few seconds.
For each event, check:
- The event name matches your spec exactly (no typos, correct case)
- All expected properties are present
- No property values are
null,undefined, or empty strings - The user shows as identified, not anonymous, if you called
identify()before the event
If you see duplicate events, one from posthog.capture() and one from autocapture, search the event name in the Activity tab to count them. You may need to disable autocapture for specific elements using the ph-no-capture CSS class.
You should now have a clean confirmed list of events with correct names, correct properties, and correct user associations.
Step 8: Build a Funnel to Confirm End-to-End Tracking
A funnel is the fastest way to catch missing events or broken steps across a full user flow.
Go to Insights > New Insight > Funnel and build a simple three-step signup-to-activation funnel:
- Step 1:
user_signed_up - Step 2:
onboarding_step_completedfiltered wherestep_numberequals 1 - Step 3:
feature_used
Set the conversion window to 7 days. Click Save.
If Step 3 shows 0 users converting when you know users are reaching that screen, either the event is not firing or your identify() call is broken and user identities are not merging correctly. Click View Persons on any funnel step to drill into individual paths.
You should now see real conversion rates with drop-off percentages at each step, which gives you immediate product insight from your fresh tracking setup.
Step 9: Create a Dashboard to Monitor Event Health
Go to Dashboards > New Dashboard. Name it “Event Health Monitor” or something equally boring and descriptive.
Add these tiles:
- Total events per day (Trends insight, all events): spot sudden drops caused by a broken snippet after a deploy
- Events by type (Trends, grouped by event name): see which events fire most and catch unexpected spikes
- Identified vs anonymous ratio (Trends): confirm your
identify()calls are working at scale - Top property values for your key event: for example, a
planbreakdown onuser_signed_up
Check this dashboard weekly for the first month. A sudden 50% drop in event volume almost always means a bad deploy broke your tracking, not that users stopped using the product.
You should now have a living dashboard that tells you whether your tracking infrastructure is healthy without digging through raw logs.
Common Mistakes To Avoid
- Using email as the distinct ID: emails change and you end up with split user profiles across rename events. Use your database primary key instead.
- Firing events before the action completes: calling
posthog.capture('form_submitted')on button click instead of after the API returns success inflates your counts with failed attempts. - Skipping the taxonomy doc: without a written spec, different developers ship events with different naming conventions and you end up with
ButtonClicked,button_click, andbtn_clickedall meaning the same thing with no way to merge them. - Disabling autocapture completely: autocapture is a retroactive safety net. If you disabled it and missed instrumenting something important three months ago, you have no historical data to build an Action from.
- Not filtering out internal team traffic: go to Settings > Project and add your company’s IP ranges or internal user IDs to the block list. Your own team’s usage will badly skew early-stage numbers.
- Instrumenting too many events on day one: 50 events with no clear ownership becomes unmanageable fast. Start with 10-15, build confidence in the data, then expand systematically.
When To Level Up
PostHog’s free tier and manual SDK setup work well up to about 1 million events per month and a team of 3-5 people who all understand the taxonomy. Past that, specific things break down.
When you have multiple products, multiple platforms (iOS, Android, web, backend), and a data team that needs events in a warehouse alongside CRM and billing data, you need a proper data pipeline. PostHog does export to BigQuery, S3, and Snowflake via its Data Pipelines feature on paid plans. But if you’re routing events from 10 different sources through one system, a dedicated CDP starts making more sense from an architecture standpoint.
You also hit limits when compliance requirements force you to audit exactly what data is collected, by whom, and when. PostHog’s self-hosted option helps here but adds infrastructure overhead that small teams often cannot absorb.
If your team is growing past that inflection point, the data-analysis tools overview at /category/data-analysis/ covers the full stack of event analytics, warehousing, and BI tools worth evaluating next. The PostHog vs Mixpanel comparison and the beginner’s guide to product analytics are good companion reads for understanding where event tracking fits in a broader data stack.
Frequently Asked Questions
Does PostHog autocapture replace custom event tracking?
Autocapture records clicks and page interactions automatically, but it does not capture backend events, form field values, or the business-logic properties (like plan or revenue) that make data actionable. Use autocapture as a safety net and retroactive tool, not your primary tracking method.
How do I track server-side events in PostHog?
Install the PostHog Python, Node.js, or Go SDK on your backend and call the same capture() method with the user’s distinct ID. Server-side tracking is essential for events like subscription_renewed or invoice_paid that happen without any browser interaction.
Will the PostHog snippet slow down my site?
The JavaScript snippet loads asynchronously and weighs roughly 70KB gzipped. For most sites that is negligible. If performance is a concern, you can serve PostHog from your own domain via a reverse proxy, which also improves load rates for users with ad blockers.
How do I handle GDPR and cookie consent with PostHog?
Call posthog.opt_out_capturing() before initializing for users who decline analytics cookies. You can also set persistence: 'memory' in the init config to avoid writing to localStorage or cookies entirely, though anonymous users will not be recognized across page loads with that setting.
Can I run PostHog alongside Google Analytics at the same time?
Yes, they operate independently and do not interfere with each other. Many product teams run PostHog for retention and funnel analysis while keeping GA4 for marketing attribution. Keep the event taxonomies separate since the data models are quite different between the two tools.
Bottom Line
Setting up event tracking in PostHog correctly comes down to three disciplines: plan your taxonomy in writing before you touch code, call posthog.identify() correctly so users are never permanently anonymous, and verify every event in the live activity feed before building any funnels or dashboards on top of it. The snippet installation takes ten minutes. The taxonomy planning takes an afternoon. The ongoing discipline of keeping event names clean and properties consistent is what separates product teams that trust their data from teams that spend every sprint debating whether the numbers are right. Follow the nine steps above and you will have a tracking setup that your whole team can build on confidently. For the next layer of your analytics stack, including warehousing, visualization, and multi-channel attribution, browse the full toolkit at /category/data-analysis/.