Skip to content

feat: add PostHog emulator#88

Open
tmchow wants to merge 1 commit intovercel-labs:mainfrom
tmchow:feat/posthog-emulator
Open

feat: add PostHog emulator#88
tmchow wants to merge 1 commit intovercel-labs:mainfrom
tmchow:feat/posthog-emulator

Conversation

@tmchow
Copy link
Copy Markdown

@tmchow tmchow commented Apr 29, 2026

Summary

Adds a PostHog emulator with stateful event capture, feature flag evaluation, and a tabbed inspector UI for browsing captured events and configured flags. Mirrors the structural pattern of the Resend emulator.

Why this matters

PostHog is the analytics tier of the modern Vercel-deployed stack: product events plus feature flag evaluation. emulate has zero coverage for that category today (the existing services are auth, payments, email, and storage). This PR opens the analytics class.

The pain it solves:

  • Event assertions. Tests verifying "did the signup flow fire user_signed_up with the right properties?" today have three bad options: stub fetch (brittle, hides serialization bugs), use the SDK's built-in mock (limited shape), or hit a real PostHog Cloud project (slow, requires network, leaks data into production analytics).
  • Feature flag testing. PostHog flags are evaluated server-side via /flags/?v=2 (or legacy /decide/). Most CI pipelines either bypass flags or hit real PostHog, both of which lose deterministic flag-gated UX testing.
  • Vercel ecosystem alignment. PostHog is on the Vercel Marketplace

posthog-js and posthog-node work against the emulator out of the box, including the default gzipped batch path and the browser SDK's ?compression=gzip-js + properties.token shape.

Demo

posthog-demo

Inspector at /_inspector showing empty events tab, then events captured after a few POST /capture/ calls, then the configured feature flags tab.

Changes

New package @emulators/posthog (entities, store, helpers, plugin export, flag evaluator, three route files, vitest suite). Wired into packages/emulate/src/registry.ts and packages/emulate/package.json. Root README.md and a new skills/posthog/SKILL.md document usage.

The architectural decisions worth flagging:

  • Body-token auth. PostHog puts the project token in the request body (api_key, token, or properties.token depending on SDK), not in Authorization. The framework's authMiddleware is Bearer-only, so capture and decide validate per-route after parsing the body. Lookup precedence is body.api_key, then body.token, then properties.token (the browser SDK pattern).
  • Property-based flag evaluation. Seeded flags accept conditions: [{ property, operator, value, variant }] evaluated against person_properties (exact, is_set, icontains, regex), plus overrides: { distinct_id: variant }. This is the minimum eval shape that lets posthog-node + posthog-js test flag-gated UX deterministically.
  • Body parser handles every shape PostHog SDKs send. JSON, application/x-www-form-urlencoded with data=<urlencoded-json> (sendBeacon legacy), text/plain (sendBeacon current), gzip via Content-Encoding: gzip (posthog-node default on Node 22), and gzip via ?compression=gzip-js query param (posthog-js default).
  • /decide/ and /flags/ aliased. Current SDKs use /flags/?v=2; /decide/ is kept for older clients. Same handler.
  • Safe-default decide response. sessionRecording: false, supportedCompression: [], surveys: false, toolbarParams: {}, etc. Without these fields some posthog-js versions enter an infinite-loading state.

Out of scope

Session replay, insights/queries, cohorts, surveys, group property evaluation, percentage rollouts, the /api/projects/:id/... admin REST API, and /engage/. These are larger surfaces or niche for CI; deferring them keeps this PR shippable.

Testing

15 vitest tests covering capture (single, batched, bad token, form-encoded, text/plain, header gzip, query-param gzip, browser SDK properties.token), cross-tenant isolation, /flags/?v=2 parity with /decide/, flag default and override paths, person-property condition matching, decide safe defaults, and inspector rendering.

Lint, type-check, format-check, sync-versions check, and full workspace build all pass locally.

Compound Engineering

Local emulation of PostHog event capture and feature flag evaluation,
plus a tabbed inspector UI for browsing captured events and configured
flags. Mirrors the structural pattern of the Resend emulator.

- POST /capture/, /batch/, /e/, /track/: accepts JSON, form-encoded,
  text/plain (sendBeacon), and gzipped (posthog-node default) bodies.
  Validates the project token from body.api_key, body.token, or
  properties.token (browser SDK pattern).
- POST /decide/ and /flags/?v=2: evaluates feature flags against
  distinct_id overrides and person_property conditions
  (exact, is_set, icontains, regex). Returns full safe-default
  response shape so posthog-js does not hang on missing fields.
  Both routes are aliased; current SDKs use /flags/.
- GET /_inspector: events and flags tabs via renderInspectorPage.

Out of scope: session replay, insights/queries, cohorts, surveys,
group property evaluation, percentage rollouts, admin REST API.

Tests cover capture (single, batched, bad token, form-encoded,
text/plain, gzipped, browser SDK properties.token), cross-tenant
isolation, /flags vs /decide parity, flag default and override paths,
person_property condition evaluation, decide safe defaults, and
inspector rendering.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 29, 2026

@tmchow is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant