Posthog Integration Playbook
This page is the playbook for Posthog in the beef monorepo. It documents
which services emit what, where the SDK is configured, the canonical event
taxonomy actually emitted by code (cross-checked against the live project),
the feature flags in use, and the operational guardrails. Logs / metrics /
traces are documented separately in
operations/observability.md — this page
covers product analytics only.
1. Overview
Posthog provides three things to beef:
- Product analytics — funnel, retention, revenue, NSFW, acquisition.
- Feature flags — experiment gating and remote configuration consumed
from both browser (
posthog-js) and server (posthog-node,posthog-go). - Session replay + web vitals — for product debugging, sampled.
- Project:
Phoenix(id:111262) under organizationFoxy AI(018cac02-779a-0000-316e-837fe06b96a4). - Region: US Cloud (
https://us.i.posthog.comfor ingestion,us.posthog.comfor the app). - Verified via:
mcp__posthog__dashboards-get-all(96 dashboards),mcp__posthog__feature-flag-get-all(14 active/stale flags),mcp__posthog__read-data-schema(200+ events surfaced for the team).
2. Per-service instrumentation
| Service | SDK | Init / client path | What is captured |
|---|---|---|---|
| brisket (Next.js, browser) | posthog-js ^1.265.1 (apps/brisket/package.json) | apps/brisket/src/features/analytics/posthog/PosthogProvider.tsx — posthog.init(NEXT_PUBLIC_POSTHOG_KEY, { api_host, person_profiles: 'identified_only', capture_pageview: true, capture_pageleave: false, before_send: sampleByEvent(['$web_vitals'], 0.2) }); provider wired in apps/brisket/src/app/layout.tsx | Pageviews, autocapture (off in dev), web vitals (sampled 20%), explicit product events via posthog?.capture(...) and useTrackEvent (apps/brisket/src/features/analytics/tracking/useTrackEvent.tsx). Feature flag reads via useFeatureFlagEnabled / useFeatureFlagPayload. |
| brisket (Next.js, server) | posthog-node ^4.18.0 (apps/brisket/package.json) | apps/brisket/src/lib/posthog.ts — new PostHog(NEXT_PUBLIC_POSTHOG_KEY, { host }) | Server-side posthog.capture({...}) from Clerk webhooks (apps/brisket/src/app/api/webhook/clerk/route.ts) and tRPC routers; posthog.isFeatureEnabled('nsfw-onboarding', userId) in apps/brisket/src/server/api/routers/{character,subscription}.ts. |
| sirloin (Go) | github.com/posthog/posthog-go v1.11.2 (apps/sirloin/go.mod) | apps/sirloin/internal/pkg/posthog/client.go — posthog.NewWithConfig(apiKey, posthog.Config{Interval: 30s, BatchSize: 100}). If SIRLOIN_POSTHOG_API_KEY is empty the constructor returns a nil-safe no-op client. | Billing/payment lifecycle events emitted from apps/sirloin/internal/app/worker/{pollchargebeeevents,pollprimerpayments,pollprimerfailedpayments,retryrenewalpayments}.go. Sets revenue properties on identify (SetRevenueProperties). |
| brain (NestJS) | None wired | — | Confirmed: no posthog-* package in apps/brain/package.json. |
| fennec (admin React) | None wired | — | Confirmed: no posthog-* package in apps/fennec/package.json. |
| strip / round (Go) | None wired | — | Out of scope for product analytics. |
| flank / chuck / shank | None wired | — | Out of scope. |
Only brisket and sirloin are first-class Posthog producers today.
3. Event taxonomy
This taxonomy is the union of what code actually emits today, cross-referenced
with what Posthog has recorded for the project (verified via
mcp__posthog__read-data-schema {kind: events} — 200+ events present in the
project, of which the table below is the subset we own from code).
3a. Server-emitted events (sirloin)
Names live in apps/sirloin/internal/pkg/posthog/events.go. All include
distinct_id = userID (the Clerk user id from sirloin’s user model).
| Event name | Where emitted | Properties |
|---|---|---|
subscription_created | events.go:104 TrackSubscriptionCreated, called from Chargebee event poller | plan_id, plan_type, duration, amount, currency, valid_until, invoice_id, discount_code |
subscription_renewed | events.go:118 TrackSubscriptionRenewed, Chargebee renewal events | same as subscription_created |
subscription_cancelled | events.go:132 TrackSubscriptionCancelled | plan_id, cancellation_reason, months_subscribed, credits_provided |
subscription_upgraded | events.go:142 TrackSubscriptionUpgraded | from_plan_type, to_plan_type, effective_date, ... |
subscription_downgraded | events.go:155 TrackSubscriptionDowngraded | from_plan_type, to_plan_type, effective_date, ... |
payment_succeeded | events.go:167 TrackPaymentSucceeded, Primer payment poller / renewal worker | amount, currency, payment_method_type, transaction_type, invoice_id, subscription_id, attempt_number, psp, country |
payment_failed | events.go:181 TrackPaymentFailed, Primer failed-payment poller | amount, currency, decline_code, decline_message, decline_type, processor_decline_code, network_decline_code, reason_type, payment_method_type, ... |
billing.payment.outcome | TrackPaymentOutcome (unified success/fail stream) | union of payment props + status: succeeded | failed |
refund_issued | TrackRefundIssued | amount, currency, original_payment_id, refund_type, invoice_id |
top_up_purchased | TrackTopUpPurchased (credit packs) | item_price_id, pack_name, credits_amount, amount, currency, invoice_id |
card_fingerprint_limit_exceeded | pollprimerfailedpayments.go:278 (fraud) | payment_id, user_id, card_fingerprint, seen_at |
high_risk_decline_flagged | pollprimerfailedpayments.go:333 (fraud) | payment_id, user_id, bin6 |
verified_unfinished_character_removal, velocity_blocked_cooldown, velocity_triggered, coupon_abuse_blocked, payment_ineligible_flagged, payment_ineligible_disposable_email, payment_throttle_blocked, fraud_payment_failed | sirloin fraud + verification flows | per-event |
3b. Client-emitted events (brisket)
Names are centralised in apps/brisket/src/lib/constants.ts as POSTHOG_EVENTS
and consumed via the posthog?.capture(POSTHOG_EVENTS.X, props) pattern, plus
the generic useTrackEvent wrapper at
apps/brisket/src/features/analytics/tracking/useTrackEvent.tsx.
Selected canonical events emitted from code (representative subset; see
POSTHOG_EVENTS for the full enumeration):
| Event name | Source file | Trigger |
|---|---|---|
Generation Requested | features/analytics/posthog/hooks/use-media-generation-request-track/index.tsx:139,183 | User submits a generation |
Generation Downloaded | hooks/use-generation-downloaded-track/index.tsx:28 | User downloads output |
Generation Favorited | hooks/use-generation-favorited-track/index.tsx:27 | User favorites a media |
Generation Archived | hooks/use-generation-archived-track/index.tsx:27 | User archives a generation |
Generation Review Submitted | hooks/use-generation-review-submit-track/index.tsx:25 | Generation review form |
Character Media Prescreened / Character Media Accepted | hooks/use-media-prescreened/index.tsx:42 | Onboarding image gates |
Character Dataset Edit Saved | hooks/use-dataset-edit-track/index.tsx:14 | Character dataset save |
Welcome Media Review Submitted | hooks/use-welcome-media-review-track/index.tsx:30 | Welcome flow review |
Explore Search Submitted | hooks/use-explore-search-track/index.tsx:27 | Explore-page search |
Explore Category Clicked | hooks/use-explore-category-click-track/index.tsx:26 | Explore category tap |
Shop VI Character Purchased | hooks/use-vi-character-purchase-track/index.tsx:21 | Shop checkout success |
Character Required Dialog Viewed / ... Proceed to Shop / ... Proceed to Creation | features/explore-page/components/character-required-dialog/index.tsx:58,71,160 | Gating dialog interactions |
sign_in_with_email, sign_in_with_google, sign_in_with_apple, sign_in_with_facebook, sign_in_with_twitter | app/(auth)/sign-{in,up}/... | Auth method chosen |
Deleted Account | app/(main-layout)/profile/account/page.tsx:118 | Self-serve delete |
Referral Link Copied | app/(main-layout)/referrals/page.tsx:48 | Referral CTA |
nsfw_* (e.g. nsfw_funnel_entry_clicked, nsfw_paywall_viewed, nsfw_kyc_started) | features/analytics/posthog/hooks/use-nsfw-funnel-track/index.ts:24 | NSFW funnel hooks |
Pageview events (Explore viewed, Create viewed, Shop viewed, Profile viewed, Billing viewed, Change Plan viewed) | features/analytics/posthog/posthog-page-visit-tracking/index.tsx:38 | SPA route changes |
Plus Posthog system events: $pageview, $pageleave (disabled),
$web_vitals (sampled 20%), $identify, $feature_flag_called,
$exception (autocapture).
Drift watch. The live Posthog project also reports events that don’t
appear to be emitted from the current monorepo, e.g. welcome_quiz_*,
Logged In, Account Created, purchase, add_payment_info,
begin_checkout, select_item, credit_purchase, download_image,
download_video, Subscription WinBack Offer Viewed. Some of these are
GTM-mirrored e-commerce events (begin_checkout, purchase,
add_payment_info, select_item) emitted from the same useTrackEvent
sanitiser; others are likely older events still in dashboards. TODO(@law):
audit dashboards using these events and either re-add the emitter or replace
the dashboard with the canonical name.
4. Person properties
Set in brisket on every signed-in render via
apps/brisket/src/features/analytics/posthog/PosthogProvider.tsx:
posthog.identify(user.user.id, { email: user.user.primaryEmailAddress, signed_in_date: createdAt.toISOString(), // when present auth_user_id: user.user.id, auth_session_id: session.session.id, primary_email_address: primaryEmailAddress,});Set in sirloin on revenue-relevant events via
apps/sirloin/internal/pkg/posthog/events.go SetRevenueProperties:
posthog.Properties{ "current_plan": props.CurrentPlan, "mrr": props.MRR, "total_payments": props.TotalPayments, "billing_period": props.BillingPeriod, "currency": props.Currency, "subscription_start_date": ...RFC3339,}person_profiles is identified_only, so anonymous traffic does not create
person rows — anonymous events are still captured but only resolve to a
person on identify.
5. Feature flags in use
Verified via mcp__posthog__feature-flag-get-all (14 entries): 11 ACTIVE,
3 STALE. Code-consumed flags:
| Flag key | Status | Consumed in | Default |
|---|---|---|---|
nsfw-onboarding | ACTIVE | apps/brisket/src/hooks/use-is-nsfw-ff-enabled.ts:4; server-side apps/brisket/src/server/api/routers/character.ts:368 and subscription.ts:92 (posthog.isFeatureEnabled('nsfw-onboarding', userId)) | off |
shop-vi | ACTIVE | apps/brisket/src/hooks/use-shop-enabled.ts:5 | off |
new-create-flow | ACTIVE | apps/brisket/src/hooks/use-new-create-flow.ts:4 | off |
onboarding-quiz-v2 | ACTIVE | apps/brisket/src/features/welcome-page/steps/what-type-of-content/index.tsx:33 (via ONBOARDING_QUIZ_V2_FLAG) | off |
having-problems-message | implied ACTIVE | apps/brisket/src/features/global-issue-message/index.tsx:8 | off |
internal-test-psp | ACTIVE (payload flag) | apps/brisket/src/features/checkout/components/v2/primer-components-checkout/test-psp-dropdown/index.tsx:8 (useFeatureFlagPayload) | none |
allow-primary-payment-removal | ACTIVE (test only) | apps/brisket/src/features/billing-page/primer/primer-payments/payment-methods/payment-method/{index,delete-payment-method/index}.tsx | off |
credits-and-subscription-upgrade-together | ACTIVE | apps/brisket/src/features/usage/index.tsx:29 | off |
yamber-warnings | implied ACTIVE | apps/brisket/src/features/character-page-ccv4/hooks/use-yamber-enabled-ff.tsx:19 (posthog?.isFeatureEnabled?.('yamber-warnings')) | off |
welcome-paywall-slider-variant | ACTIVE | No code consumer (grep across apps/); likely remote-config / GTM-only | n/a |
landing-of-creators-a-b-c-test | ACTIVE | No code consumer (grep across apps/); likely remote-config / GTM-only | n/a |
sizzle-video-display | ACTIVE | apps/brisket/src/features/welcome-page/hooks/use-sizzle-video-display.ts:4; consumed in welcome-page/steps/video-sizzle/index.tsx and what-do-you-want-to-make-content-of/index.tsx | off |
sirloin-tester-skip-identity-lock | ACTIVE | No code consumer (grep across apps/sirloin/** returned no match); likely Posthog-only operational toggle | n/a |
Paywall-AB-test, welcome-quiz-variant, magic-link-method | STALE | drop or remove | n/a |
Flags listed without a code consumer exist in Posthog but no source-grep hit — they may be remote-config-only or owned by a GTM-side experiment.
The brisket useFeatureFlagEnabled hook returns undefined until flags
load; always coerce with !! (already done in
use-shop-enabled.ts, use-is-nsfw-ff-enabled.ts) or gate behind
useFeatureFlagReady (apps/brisket/src/hooks/use-feature-flag-ready.ts)
to avoid flicker.
6. Dashboards
Verified via mcp__posthog__dashboards-get-all (96 dashboards total, 16
pinned). The pinned set is the canonical “we look at this” surface:
| # | Dashboard | URL |
|---|---|---|
| 1 | 1. Acquisition (2026) | /dashboard/1475849 |
| 2 | 2. Conversion (2026) | /dashboard/1471691 |
| 3 | 3. Activation (2026) | /dashboard/1475850 |
| 4 | 4. Engagement (2026) | /dashboard/1475851 |
| 5 | 5. Retention (2026) | /dashboard/1292835 |
| 6 | 6. Revenue (2026) | /dashboard/1475852 |
| 7 | 7. Unit Economics (2026) | /dashboard/1475853 |
| 8 | Activation - Weekly Subscriber Cohorts | /dashboard/1517386 |
| 9 | Credit Value Analysis | /dashboard/1505947 |
| 10 | Meta Ads Performance | /dashboard/1515537 |
| 11 | Revenue Monitor | /dashboard/1513385 |
| 12 | Video Generation Analysis | /dashboard/1503585 |
The 1.–7. numbered set is the AARRR-style acquisition→revenue funnel
maintained by growth. Chargebee is the subscriber source of truth (per the
Acquisition dashboard description); CAC currently includes only Meta —
Google/TikTok/Snap spend is not yet wired.
7. Failure modes
- Capture queue overflow. sirloin batches at
BatchSize: 100and flushes everyInterval: 30s. If the Posthog API is unreachable longer than the worker container’s lifetime, in-flight events are lost — there is no disk spool. Mitigation: workers run continuously; restart-induced loss is bounded to 30 s. - Identify race. brisket calls
posthog.identify(user.user.id, ...)inside auseEffectkeyed onuser.user?.id. Events captured between hydration and the effect firing are anonymous and only attach to a person retroactively via Posthog’s$anon_distinct_idmerge.person_profiles: 'identified_only'keeps anonymous traffic from creating ghost persons but means pre-identify funnel steps depend on alias merging. - Flag eval failures.
useFeatureFlagEnabledreturnsundefinedwhile flags load and on network failure. All consumers must coerce with!!or useuseFeatureFlagReady. Server-sideposthog.isFeatureEnabled(key, userId)isawait-ed in tRPC routers — a Posthog outage adds latency to those calls; current code does not have a per-call timeout. TODO(@law): addAbortSignal.timeoutwrapper. - Sentry filter.
apps/brisket/instrumentation-client.tsmatches/Failed to fetch.*ph\.foxy\.ai/iand/posthog.*failed/iand drops these from Sentry. Posthog ingest hiccups will not page; they will only show as gaps in dashboards. - Autocapture noise in dev. Disabled when
NEXT_PUBLIC_DEV_FEATURES === 'true'(PosthogProvider). - Web-vitals sampling.
before_send: sampleByEvent(['$web_vitals'], 0.2)— 80% of$web_vitalsevents are dropped client-side.
8. Secrets
| Variable | Where it’s used | Notes |
|---|---|---|
BRISKET_NEXT_PUBLIC_POSTHOG_KEY (project key, public) | brisket browser + server (apps/brisket/src/lib/posthog.ts, PosthogProvider.tsx) | Bundled into the JS bundle; safe to expose. |
BRISKET_NEXT_PUBLIC_POSTHOG_HOST | brisket SDK config | Defaults to https://us.i.posthog.com per apps/brisket/.env.example. |
SIRLOIN_POSTHOG_API_KEY | sirloin Go client (apps/sirloin/internal/pkg/posthog/client.go) | Project key. Empty = no-op client (dev/test). |
SIRLOIN_FOXY360_MCP_POSTHOG_URL | sirloin Foxy360 connector (read path) | Used by the Posthog source tool in Foxy360. |
SIRLOIN_FOXY360_MCP_POSTHOG_AUTH_HEADER | sirloin Foxy360 connector | Bearer <personal-api-key> for read-only queries. |
The personal API key (held only in SIRLOIN_FOXY360_MCP_POSTHOG_AUTH_HEADER
and in operator credentials for Posthog MCP usage) is not ingest-scoped
and grants read access to the whole project. Treat it as a secret. See
standards/security-model.md for the
broader secrets posture.
9. Cost model
TODO(@law): event volume budget and retention contract. Posthog billing tier is not documented in-repo. Hot levers if costs spike:
- Drop or sample
$autocapture(already off in dev). - Increase
$web_vitalssampling (currently 20% kept). - Drop
$dead_click/$rageclickif not used by any pinned dashboard. - Consolidate near-duplicate funnel events (the
nsfw_*lower-snake-case set vs theNSFW *Title-Case set looks like duplication — TODO(@law) with growth before pruning).
10. Privacy
person_profiles: 'identified_only'— anonymous events do not create person rows.email,primary_email_address,auth_user_id,auth_session_idare set as person properties on identify — classified as Account PII instandards/security-model.md#data-classification. These flow only over TLS tous.i.posthog.com.- Payment instrument data (PAN, CVV) is never sent to Posthog. sirloin
emits only Primer’s
transactionId, BIN6, and card fingerprint — see the Data Classification table insecurity-model.md. apps/brisket/src/features/analytics/tracking/useTrackEvent.tsxscrubs NSFW-related strings before pushing to GTMdataLayer(the scrubber does not apply to Posthogcapture— Posthog is the authoritative store and keeps the raw values).- Session replay: brisket loads
posthog-jswith nosession_recording/disable_session_recordingconfig inPosthogProvider.tsx(grep returned no replay-related keys), so theposthog-jsdefault applies — replay follows the project’s Posthog dashboard setting. TODO(@law): confirm the project-level session-recording toggle in the Posthog UI is off in production.
11. Runbook hooks
- Service-level observability (logs/metrics/traces):
operations/observability.md. - Auth boundaries (where
auth_user_idoriginates):standards/auth-boundaries.md. - Billing event glossary (the source of
subscription_*andbilling.payment.outcomeevents):standards/billing-pricing.md. - Security and PII classification:
standards/security-model.md. - Linear epics for analytics changes: tag with
area:analytics. - For ad-hoc data pulls, prefer the Foxy360 Posthog source tool
(
SIRLOIN_FOXY360_MCP_POSTHOG_URL) over scattering personal API keys.