PostHog Analytics skill for Codex
One person per user, flags without flicker. Codex sets up PostHog with a pinned defaults date, a reverse proxy and consent-aware opt-out, identifies users by stable database IDs and resets on logout so people never merge. Key actions become explicit events, server SDKs flush before serverless functions exit, flags are bootstrapped from server evaluation to stop flicker, and replays mask inputs and private elements.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- posthog-analytics Installs to ~/.agents/skills/posthog-analytics/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install posthog-analytics The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project posthog-analytics Installs to .agents/skills/posthog-analytics/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/posthog-analytics.zipunzip -o posthog-analytics.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) posthog-analytics PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $posthog-analytics, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Implements PostHog product analytics in web apps and backends — posthog-js and posthog-node/posthog-python setup, reverse proxy, identify/reset/group, custom events alongside autocapture, feature flags with bootstrapping and local evaluation, experiments, session replay masking, consent and opt-out, and funnel/retention insights. Use when adding PostHog to a React/Next.js or other app, events or users look wrong (duplicate persons, anonymous users), flags flicker or disagree between client and server, or someone asks for a PostHog funnel, retention or experiment setup.
Use it when
- Installing PostHog in a web app or backend
- Person, identify or group data looks wrong
- Feature flags flicker or disagree between client and server
- Configuring session replay privacy or building funnels and retention
Use something else when
- Marketing site analytics run on Google Analytics: use ga4-analytics
- You are designing the experiment itself: use ab-testing
How to check the result
The Activity view shows each planned event with correct properties, a two-device login lands on one person, logout does not merge users, and consent denial stops ingestion requests.
The mistake it prevents
Forgetting posthog.reset() on logout, so the next person on a shared device is merged into the previous user's profile.
Done well: each real person is one PostHog person across devices after login, key product actions are explicit events with stable names and typed properties, feature flags evaluate identically on client and server without flicker, recordings never contain secrets, and tracking respects the consent the user gave.
Before writing code
- Read
AGENTS.md,package.json/pyproject.toml, and search for existing usage:grep -rnE "posthog|phc_[A-Za-z0-9]+" --exclude-dir=node_modules . - Confirm region (US
https://us.i.posthog.comor EUhttps://eu.i.posthog.com) and whether the org self-hosts. - Get or write the event list (taxonomy design belongs to
analytics-tracking-plan). - Installing packages (
posthog-js,posthog-node,posthog) needs network and may need approval in the sandbox. - The project API key (
phc_...) is public by design; personal API keys and feature flag secure keys are secrets: server-side env vars only.
Workflow
- [ ] 1. Client init (with defaults date, proxy, consent mode)
- [ ] 2. Identity: identify on login, reset on logout, groups for B2B
- [ ] 3. Custom events for key actions; tune autocapture
- [ ] 4. Server-side events and flags
- [ ] 5. Feature flags and experiments without flicker
- [ ] 6. Session replay with masking
- [ ] 7. Verify in Activity and build the core insights
1. Client init
Next.js 15.3+ can initialize in instrumentation-client.ts; other React apps init once
at the entry point.
// instrumentation-client.ts (Next.js) or src/analytics.ts
import posthog from "posthog-js";
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest", // reverse proxy, see below
ui_host: "https://us.posthog.com",
defaults: "2026-01-30", // pin a defaults date; bump deliberately after reading the changelog
person_profiles: "identified_only",
opt_out_capturing_by_default: requiresConsent(), // your CMP decides
});
defaultsopts into PostHog’s current recommended behaviours as of that date (including SPA page views viacapture_pageview: 'history_change'). Pinning a date keeps behaviour stable across SDK upgrades.- Do not call
posthog.capture('$pageview')on route changes when history-based page views are on; that double counts.
Reverse proxy (reduces ad-blocker loss) for Next.js:
// next.config.js
module.exports = {
skipTrailingSlashRedirect: true,
async rewrites() {
return [
{ source: "/ingest/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*" },
{ source: "/ingest/:path*", destination: "https://us.i.posthog.com/:path*" },
];
},
};
2. Identity
// after successful login or signup (not on every render)
posthog.identify(user.id, { plan: user.plan, signup_date: user.createdAt });
// B2B: attach to the company so insights can aggregate by account
posthog.group("company", org.id, { name: org.name, seats: org.seats });
// on logout
posthog.reset();
- Use the stable database ID as
distinct_id, never email (emails change). - Call
identifywith the same ID on the server (distinctId) so backend events join the same person. - Forgetting
reset()on logout merges the next user on a shared device into the previous person. - Person properties may hold contact details only if the privacy policy and consent allow it; never put passwords, tokens or payment data anywhere.
3. Events
- Autocapture is useful for exploration, but build key metrics on explicit events: names are stable even when the UI changes.
- Naming:
object_verborverb_objectconsistently, lowercase with spaces or underscores per the team’s convention (e.g.project_created,invite_sent). Propertiessnake_case, typed consistently (a number stays a number). - Capture outcomes server-side when the server is the source of truth (payment succeeded, subscription renewed); client-side for UI interactions.
posthog.capture("report_exported", { format: "csv", row_count: rows.length, source: "dashboard" });
4. Server side
// lib/posthog-server.ts
import { PostHog } from "posthog-node";
export function serverPostHog() {
return new PostHog(process.env.POSTHOG_KEY!, {
host: "https://us.i.posthog.com",
flushAt: 1, flushInterval: 0, // serverless: send immediately
});
}
// in a handler
const ph = serverPostHog();
ph.capture({ distinctId: user.id, event: "subscription_renewed", properties: { plan: "pro", mrr: 49 } });
const newCheckout = await ph.isFeatureEnabled("new-checkout", user.id);
await ph.shutdown(); // flush before the function exits
Python (keyword arguments work across SDK major versions):
from posthog import Posthog
ph = Posthog(os.environ["POSTHOG_KEY"], host="https://us.i.posthog.com")
ph.capture(distinct_id=str(user.id), event="subscription_renewed", properties={"plan": "pro"})
ph.shutdown()
For high-volume flag checks, enable local evaluation by giving the server SDK a feature-flag secure key or personal API key (secret) so it evaluates without a network call per request.
5. Feature flags and experiments
import { useFeatureFlagEnabled, useFeatureFlagVariantKey } from "posthog-js/react";
const showNewNav = useFeatureFlagEnabled("new-nav"); // undefined until loaded
const pricingVariant = useFeatureFlagVariantKey("pricing-test");
- Flags load asynchronously;
undefinedmeans “not loaded yet”, not “off”. Render a neutral state or bootstrap. - Avoid flicker by evaluating on the server and bootstrapping the client:
posthog.init(key, { bootstrap: { distinctID: user.id, featureFlags: flagsFromServer } }). - Release conditions on person or group properties need those properties sent before evaluation (or passed as override properties in server evaluation).
- Experiments: create in PostHog with a primary metric before launch; exposure is
recorded when the flag is evaluated (
$feature_flag_called), so evaluate it only where the variant is actually shown. Test design belongs toab-testing. - Lifecycle and cleanup of flags belong to
feature-flags.
6. Session replay
- Enable in project settings; mask by default:
posthog.init(key, { /* ... */ session_recording: { maskAllInputs: true, maskTextSelector: "[data-private]" } });
- Add the
ph-no-captureclass to elements that must never be recorded (payment iframes are cross-origin and not recorded anyway; card fields in your own DOM must be masked). - Sample or trigger recordings (by flag or URL) on high-traffic apps to control cost.
7. Verify and analyze
- Run
posthog.debug()in the browser console for verbose logging; check the Activity (live events) view for event names, properties and the person they attach to. - Log in on two browsers as the same user: both sessions must show under one person.
- Log out then in as another user: no merge.
- With consent denied: no requests to
/ingest(or only what the chosen cookieless configuration allows). - Core insights: activation funnel (signup → key action within 7 days, conversion window set explicitly), weekly retention on a meaningful repeat action, trends on key events broken down by plan or group. Use SQL insights for anything the UI cannot express.
Gotchas
- Calling
identifywith different IDs for the same person (email then UUID) creates merge problems that cannot be undone cleanly. Pick one ID scheme. - Ad blockers block
*.posthog.com; without a proxy expect material client-side loss. - Server events without the identified
distinctIdcreate anonymous persons; pass the same ID as the client. - Bootstrapped flags must use the same distinct ID as
identify, or evaluations differ. - Multiple
posthog.initcalls (e.g. in a component that re-renders) cause duplicate events; init once.
Output
**Setup:** posthog-js in instrumentation-client.ts (defaults 2026-01-30), /ingest proxy,
opt-out until consent; posthog-node helper for API routes.
**Identity:** identify on login (user.id), group "company", reset on logout.
**Events:** 9 explicit events (table: name, properties, file:line, client/server).
**Flags:** new-checkout evaluated server-side and bootstrapped; no flicker.
**Replay:** maskAllInputs, [data-private] masked, 20% sampling.
**Verified:** Activity view shows all 9; two-device test merges into one person. What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "PostHog Analytics"
short_description: "One person per user, flags without flicker"
default_prompt: "$posthog-analytics Add PostHog to our Next.js app with a reverse proxy and consent banner support" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.