Instrumenting PostHog in the development process
Contents
A guide for product engineering teams that want every feature to ship with its tracking. Customer-facing roles can send it to a customer's engineering team as is.
The problem is the process, not the tool
Most tracking gaps have one cause: no step in the development process owns tracking. Nobody decides what to track before the build starts. Engineers add events late, name them in different ways, and drop them when time runs short. Then a feature launches, and nobody can say if it worked.
A better tool does not fix this. A better process does. This guide puts tracking into the steps your team already runs, so that a skipped event becomes a visible choice instead of a silent gap.
Running example. A team builds scheduled reports. A user picks a report, a frequency, and recipients, and the app emails the report on that schedule. The PRD asks one question: do users who schedule a report come back more often?
The development loop
- Write the tracking requirement into the PRD. Name the question the launch answers, the metric that grades it, and the events that feed the metric.
- Turn the PRD into an event spec. An agent skill or a planning prompt can draft the spec. A person approves it.
- Assign an owner. The PM and the lead engineer own the spec, the launch dashboard, and the grade.
- Build behind a feature flag. The events ship in the same pull request as the feature code.
- Verify the events. Trigger the feature locally and confirm that each event arrives in the activity tab with the correct properties.
- Launch. Roll out the flag.
- Grade the launch. Compare the result to the PRD metric.
- Hold a retro. Decide what the data changes about your next piece of work.
For the running example, the spec has two events:
| Event | Fired from | Properties |
|---|---|---|
scheduled_reports:schedule_create | Frontend, when the user saves a schedule | report_type, frequency, recipient_count |
scheduled_reports:report_send | Backend, when the email goes out | report_type, delivery_status |
The second event closes the loop. It shows that the feature did its job, not only that a user clicked a button.
Five practices
1. Agree on a naming convention
Follow the naming convention in our product analytics best practices. The short version:
- Name events
category:object_action, in lowercase snake case. - Take the action from a fixed list of present-tense verbs, such as
create,view,submit,send, andfail. - Give every text property a fixed list of allowed values.
frequencyisdaily,weekly, ormonthly, never free text. - Never put personal data in an event name or a property. Send
recipient_count, not the recipients' email addresses. - Never rename an event that has history. Version the event instead, and create an action that matches the old and the new name. The action gives you one trend line across the change, and the history stays intact.
2. Send every event through one module
Put every event name and its property types in one file. Give the file one function that calls PostHog, and let nothing else call posthog.capture. A misspelled event name or a wrong property type then fails your normal typecheck, before the code reaches review.
A feature then calls track():
The typecheck rejects 'scheduled_reports:schedule_crate', a frequency of 'hourly', and a call with no recipient_count. A lint rule stops a direct posthog.capture call anywhere outside the module:
The module is also the one place for shared behavior, such as error handling or default properties. Put that logic in track(), not around each call.
Your backend needs the same pattern in its own language. Schema management can generate TypeScript, Go, and Python types from event definitions in PostHog. It is experimental today, so treat it as an option for later, not a dependency.
How PostHog does it: PostHog's own frontend keeps its usage events in one file, eventUsageLogic.ts.
3. Give your coding agents the same rules
Agents write a growing share of feature code. Put the tracking rules where your agent reads them: AGENTS.md, CLAUDE.md, or your tool's equivalent. The agent then follows the same convention as the team.
How PostHog does it: PostHog's AGENTS.md sends agents to feature-usage-tracking.md, which requires end-to-end tracking for every feature.
4. Put a tracking line on every pull request
Add a tracking section to your pull request template. The author keeps exactly one line and deletes the other two:
A small CI job reads the pull request description. It fails when the description does not hold exactly one tracking line. The job does not judge the answer. It only makes sure that somebody gave one.
The job reads the description through an environment variable, not by inline expansion, so text in a pull request cannot run as a shell command.
How PostHog does it: the PostHog pull request template has a release status section with three lines. The author keeps exactly one.
5. Keep one launch dashboard
Keep one dashboard for launches, and rotate it. While a feature launches, the dashboard holds the insights for that feature: the funnel from entry to outcome, the PRD metric, and the failure events. Filter or break down each insight by the release flag. The web SDK adds the active flags to each event. On the server, set sendFeatureFlags: true when you capture an event.
After the launch window, move the insights that still matter to the dashboard for that product area. Then clear the launch dashboard for the next launch, and clean up the flag.
A large number of dashboards is not the real problem. The real problem is that nobody can find the one they need. A few core dashboards for top-level questions, plus one launch dashboard, keep each answer easy to find.
Start backend events with one closed loop
Frontend events show what users try. Backend events show what happened. Backend tracking is also more reliable, because ad blockers and browser network errors do not drop it.
If your team sends few backend events today, do not instrument the whole backend at once. Pick one workflow with a clear outcome, and send its outcome event from the server. In the running example, that event is scheduled_reports:report_send. This call belongs in your backend events module:
Send the account ID and plan as properties on each backend event. You can then break down any metric by account_id or plan with standard breakdowns. To count unique accounts instead of users, or to use account-level properties, add group analytics. Group analytics is a paid add-on.
Anti-patterns
- A flag evaluated in a latency hot path. Each remote evaluation is a network request. Use server-side local evaluation, and evaluate the flag once, not on every request.
- A launch with no list of the events it fires. If nobody can name the events, nobody can grade the launch. Step 5 of the loop prevents this.
- An agent that builds a whole feature with no tracking plan. The agent invents names and properties, and nobody checks them against the PRD metric. Give the agent the event spec and the rules first.
Common questions
Do more events mean a bigger bill? Product analytics bills by the number of events, so a property adds no cost. A clean event with useful properties costs the same as a junk event. The goal is fewer and better events, not fewer properties. For more ways to reduce cost, see cutting costs.
Our features change often. Should we add events after the design settles? No. Ship the events with each version of the feature. Then you can compare each version to the last one. If a change alters what an event means, version the event.
We already have events with inconsistent names. Do we fix them? Leave the old events as they are, and apply the convention to new events from now on. When an old event and a new event track the same thing, create an action that matches both.
The one-line version
Decide what to track before you build, send every event through one typed module, and make every pull request say what it tracks.





