PostHog Handbook Library / Engineering

3,896 words. Estimated reading time: 18 min.

Shipping & releasing

Auto TL;DR

At a Glance

This long page covers these main areas. The list is generated from the article headings, so it updates with every handbook rebuild.

  1. How to decide what to build
  2. Set milestones
  3. Assign an owner
  4. Think about other teams
  5. Break up goals
  6. Iterate through the work
  7. Evaluate success
  8. What about the small stuff?

Any process is a balance between speed and control. If we have a long process that requires extensive QA and 10 approvals, we will never make mistakes because we will never release anything.

However, if we have no checks in place, we will release quickly but everything will be broken. For this reason we have some best practices for releasing things, and guidelines on how to ship.

How to decide what to build

Set milestones

To start, Product and Engineering should align on major milestones (e.g. Collaboration) which we have strong conviction will drive our success metrics for a feature.

There are two types of goals.

Goals should be time-bound, but since we primarily use goals for our two-weekly sprint planning we should consider them generally timebound to two weeks.

Use the following principles:

Assign an owner

A single engineer should be accountable for a milestone partnering closely with other functions to ensure it’s successful.

Think about other teams

Most things won't cause issues for other teams. However, if it will, don't "align teams" or write a huge RFC (unless that'd help you). Do a quick 1:1 with the most relevant person on another team.

Consider:

If this is a big feature which will need an announcement, content, or other marketing support then it's _never_ too early for the owner to let the Marketing team know. Drop a post in their Slack channel or tagging them on an issue.

Break up goals

The owner turns the ambiguous milestone into a roadmap of ambitious, meaningful, sprint-sized goals, thinking 2 - 3 sprints ahead to give other functions time. Goal principles still apply.

Iterate through the work

We used to have company-wide sprint planning sessions but as we've grown there were so many teams that it started being plan-reading and not planning.

PostHog works in two week iterations. Each team plans their work together and adds their sprint plan to a pinned issue in GitHub. If the issue for the next iteration doesn't exist when you come to comment on it then you create it.

When planning your work you should also have a retrospective for the previous iteration. Like most things at PostHog this can be a very low ceremony retro and ideally checking the team is working on the right things in the right way is a frequent thing not a once a fortnight thing.

Work in the iteration should:

As one of our values is Why not now?, during the iteration you might come across something that should be much higher priority than what was already planned. It's up to you to then decide to work on that as opposed to what was agreed in the planning session.

Evaluate success

Review impact of each major milestone and feedback into the planning process.

When we review the status of goals we classify them as follows:

What about the small stuff?

Not everything directly contributes to a company level goal. It’s important that the small stuff also gets done for us to succeed. Use the following principles:

Sizing tasks and reducing WIP

Efficient engineering organizations actively reduce Work In Progress (WIP) to avoid negative feedback loops that drive down productivity.

Hence, a PR should be optimized for two things:

  1. Quality of implementation
  2. The speed with which we can merge it in

PRs should ideally be sized to be doable in one day, including code review and QA. If it's not, you should break it down into smaller chunks until it fits into a day. Tasks of this size are easy to test, easy to deploy, easy to review and less likely to cause merge conflicts.

Sometimes, tasks need a few review cycles to get resolved, and PRs remain open for days. This is not ideal, but it happens. What else can you do to make sure your code gets merged quickly?

Writing code

We're big fans of Test Driven Development (TDD). We've tried to create test infrastructure that helps you rather than annoys you. If that isn't the case, please raise an issue! Keeping tests on point is a high priority to keep developer productivity high.

Other than that, you know what to do.

Creating PRs

When you have a piece of code ready to be reviewed, create a PR. Link the PR to the issue it solves, and add a clear description of what the PR does and how to test it. Follow PR templates if they exist for the area you're working on.

All PRs should be attributable to a human author as far as possible, even if they were assisted by an agent.

Fully automatically generated PRs might come from an agent like PostHog Code or from systems like Dependabot. These PRs are fine, but they should be clearly labelled as such and include a clear description of the changes being made and any relevant context about the generation process. These PRs should in turn never be attributed to a human author, as the changes were not directly or indirectly made by a human.

For external contributors, our AI contributions policy covers expectations around AI-assisted PRs.

To make sure our issues are linked correctly to the PRs, you can tag the issue in your commit.

git commit -m "Closes #289 add posthog logo to website"

Testing code

See: How we review.

Storybook Visual Regression Tests

In our CI pipeline, we use Playwright to load our Storybook stories and take snapshots. If any changes are detected, the updated snapshots are automatically committed to the PR. This helps you quickly verify whether you've introduced unexpected changes or if the UI has been altered in the intended way.

Check the test-runner.ts file to see how this is configured. We use the @storybook/test-runner package; you can find more details in the official Storybook documentation.

Running Tests Locally
  1. Start Storybook in one terminal:
    pnpm storybook
    # or
    pnpm --filter=@posthog/storybook
  1. Install Playwright and run the visual tests in debug mode in another terminal:
    pnpm exec playwright install
    pnpm --filter=@posthog/storybook test:visual:debug

This setup will help catch unintended UI regressions and ensure consistent visual quality.

If you wish to locally run test-runner.ts and output all snapshots:

pnpm --filter=@posthog/storybook test:visual:ci:update

Or if you wish to run one particular story:

pnpm --filter=@posthog/storybook test:visual:ci:update <path_to_story>

# example: pnpm --filter=@posthog/storybook test:visual:ci:update frontend/src/scenes/settings/stories/SettingsProject.stories.tsx
Merge conflicts with visual regression snapshots

It happens often that your PR will show conflics with our snapshots, as our CI pipeline will run test-runner.ts on every push, generating and pushing to your PR any significant visual changes.

Github does not allow for conflict resolution inside their website, so you must do it manually.

The following is done on your branch in question.

  1. Bring your branch up to date with master.
    git fetch origin
  1. Rebase master into your branch
    git rebase master
  1. Rebase your upstream into your local branch
    git pull --rebase <your branch>

In your terminal, it should show you the conflicts mimicking what you see in your Github PR.

warning: Cannot merge binary files: frontend/__snapshots__/<conflicted_file_1>.png (HEAD vs. xxx (Update UI snapshots for `chromium` (1)))
Auto-merging frontend/__snapshots__/<conflicted_file_1>.png
CONFLICT (content): Merge conflict in frontend/__snapshots__/<conflicted_file_1>.png
error: could not apply xxx... Update UI snapshots for `chromium` (1)
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".

If all your conflicts are only snapshots, you can simply skip it.

git rebase --skip

If all conflicts go away, then

git push origin --force <your branch>

Why does this work? As we mentioned earlier, our CI runs test-runner.ts on every push, so we don't really care if these images are conflicted as they are regenerated after you push to your branch.

Deployed Preview

You can spin up a real deployed PostHog instance to test your branch by adding the hobby-preview label to your PR. This uses the hobby (Docker Compose) self-hosted setup under the hood.

How it works:

  1. Add the hobby-preview label to your PR
  2. CI creates a DigitalOcean droplet and deploys PostHog with your branch
  3. A comment is posted to the PR with the preview URL (e.g., https://hobby-pr-12345.posthog.dev)
  4. The droplet persists across commits so you can iterate
  5. Remove the label or close the PR to clean up the droplet

When to use it:

The workflow also runs a smoke test (health check) automatically on PRs that touch deployment-related files.

Reviewing code

PRs can be written by humans or by agents (like PostHog Code). Either way, every PR needs a review before merging, and a human always merges.

Who should review depends on who wrote the code (see Creating PRs):

We encourage the use of AI review agents (Codex, Copilot, Greptile, etc.) on PRs. Their comments and suggestions don't count as an approval, but they catch things humans miss and speed up the review process.

When reviewing a PR, we look at the following things:

Things we do not care about during review:

Merging

Merge anytime. Friday afternoon? Merge.

Our testing, reviewing and building process should be good enough that we're comfortable merging any time.

Always request a review on your pull request (or leave unassigned for anyone to pick up when available). We avoid merging without any review unless it's an emergency fix and no one else is available (especially for posthog.com).

Once you merge a pull request, it will automatically deploy to all environments. The deployment process is documented in our charts repository. Check out the #platform-bots Slack channel to see how your deploy is progressing.

We're managing deployments with ArgoCD where you can also see individual resources and their status.

Deploy notification bot

After your PR is deployed to an environment, a bot automatically comments on the merged PR with the deployment status. The dev deployment triggers the initial comment. As prod-us and prod-eu finish deploying, the bot updates the same comment in-place rather than posting new ones.

If you don't see a comment on your PR after a deploy, give it a few minutes -- the notification runs after ArgoCD finishes syncing. If it still hasn't appeared, check the deploy workflow in PostHog/charts for failures.

Verifying your deployment

After merging, your code should deploy automatically. If you need to verify your changes are live (or troubleshoot why they're not), here's how:

1. Check state.yaml (what should be deployed)

The charts repository state.yaml is the source of truth for what ArgoCD is trying to deploy. Find your service (e.g., ingestion, posthog) and check the commit SHA listed.

2. Check running pods (what's actually deployed)

If you have cluster access, verify what's running:

# Find your service's pods
kubectl -n posthog get pods | grep <service-name>

# Get the image/commit running on a pod
kubectl -n posthog get pod <pod-name> -o jsonpath='{.spec.containers[0].image}'
3. Verify your commit is included

Use git merge-base to check if your commit is an ancestor of the deployed commit:

git fetch origin
git merge-base --is-ancestor <your-commit-sha> <deployed-commit-sha> && echo "Deployed" || echo "Not deployed"
4. Troubleshooting with ArgoCD

If state.yaml shows a newer commit than what's running on pods, check ArgoCD:

  1. Find the specific app - Don't just look at the parent grouping (e.g., ingestion). Drill down to the specific environment app like ingestion-events-prod-us or posthog-prod-eu.
  1. Check sync status - Is it "Synced" or "OutOfSync"? When was the last successful sync?
  1. Check if auto-sync is enabled - Some apps may have auto-sync disabled and require manual syncing.
  1. Look at the diff - Click "DIFF" to see what's different between desired and live state.
Common deployment issues

| Symptom | Likely cause | Solution | |---------|--------------|----------| | App shows "OutOfSync" | Auto-sync disabled or sync error | Check if auto-sync is enabled; try manual sync | | state.yaml updated but pods unchanged | ArgoCD hasn't synced yet | Check ArgoCD app status; may need manual intervention | | Pods running old commit | Rollout stuck or image not built | Check deployment rollout status; verify CI built the image | | Can't find your service in ArgoCD | Looking at wrong app grouping | Search for your specific service + environment (e.g., ingestion-events-prod-us) |

If a deployment appears stuck, reach out in #team-infrastructure.

Documenting

If you build it, document it. You're in the best position to do this, and it forces you to think things through from a user perspective.

It's not the responsibility of either or teams to document features.

See our docs style guide for tips on how to write great docs.

Releasing

There are a few different ways to release code here:

Best practices for full releases

Opt-in betas can have rough edges, but public betas and full releases should be more polished and user friendly.

Engineers should apply the following best practices for _all_ new releases:

When to A/B test

There are two broad categories of things we A/B test:

The former is an optimization scheme; the latter makes sure we don't break things. Just like we create tests in our codebase to make sure new code doesn't disrupt existing features, we also need to do behavioral testing to make sure our new features aren't disrupting existing user behaviors.

A/B tests make sense when:

If you're not sure something should be A/B tested, run one anyway. Feature flags (which experiments run on top of) are a great kill-switch for rolling back features in case something goes sidwways. And it's always nice to know how your changes might move the numbers!

It's easy to just think "this makes more sense, let's just roll it out." Sometimes that's okay, sometimes it has unintended consequences. We obviously can't and shouldn't test everything, but running A/B tests frequently gets you comfortable with being wrong, which is a _very_ handy skill to have.

A/B test metrics

Experiment design is a bit of an art. There are different types of metrics you can use in PostHog experiments. Another benefit of running experiments is forcing yourself to think through what other things your change might impact, which oftentimes doesn't happen in the regular release cycle!

Generally, a good pattern is to set up 1-2 primary metrics that you anticipate might be impacted by the A/B test, as well as 3+ secondary metrics that might also be good to keep an eye on, just in case. If you aren't sure what metrics to be testing, just ask! Lots of people are excited to help think this through (especially #team-growth and Raquel!).

Releasing a new product

We can release alphas and betas both as publicly available, or opt-in. See: Releasing new products and features.

It's always worth letting Marketing know about new betas so they can help raise awareness. The owner should tag them on an issue, or drop a message in the Marketing Slack channel.

Betas are usually announced as milestones on the public roadmap and included in the changelog by Marketing.

Product announcements

Announcements, whether for beta or final updates, are a Marketing responsibility. See: Product announcements.

In order to ensure a smooth launch the owner should tell Marketing about upcoming updates as soon as possible, or include them in an All-Hands update.

It's _never_ too early to give Marketing a heads-up about something by tagging them in an issue or via the Marketing Slack channel.

Self-hosted and hobby versions

We have sunset support for our kubernetes and helm chart managed self-hosted offering. This means we no longer offer support for fixing to specific versions of PostHog. A docker image is pushed for each commit to master. Each of those versions is immediately deployed to PostHog Cloud.

The deploy-hobby script allows you to set a POSTHOG_APP_TAG environment variable and fix your docker-compose deployed version of PostHog. Or you can edit your docker-compose file to replace each instance of image: posthog/posthog:$POSTHOG_APP_TAG with a specific tag e.g. image: posthog/posthog:9c68581779c78489cfe737cfa965b73f7fc5503c

Canonical URL: https://posthog.com/handbook/engineering/development-process

GitHub source: contents/handbook/engineering/development-process.md

Content hash: 13d9f277a8e98dd1

Static reader notes
  • MDX_COMPONENT_STATIC_ADAPTER: Adapted interactive MDX components for static reading: SmallTeam.