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
Section titled “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(130 dashboards on 2026-09-03),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
Section titled “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.
CV2 creation and management share six recipe-neutral events. See Character recipe analytics for their ownership, metric denominators, legacy mappings, and rollout checks.
3. Event taxonomy
Section titled “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)
Section titled “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:314 + paymentmethods.go recordCardFingerprintOnPayment (fraud, observe-only since FOXY-629 — no flag/block) |
payment_id, card_fingerprint, accounts |
high_risk_decline_flagged |
pollprimerfailedpayments.go:442 (fraud) |
payment_id, bin6, reason_code |
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)
Section titled “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 |
|---|---|---|
funnel_step_viewed |
apps/brisket/src/features/funnel/renderer/use-screen-effects.ts |
Step reached, deduplicated by step/branch while mounted. Properties: funnel_id, string funnel_version, step_id, and branch_id for branch-owned steps. Replaces internal screen-state events. |
funnel_step_completed |
apps/brisket/src/features/funnel/machine.ts |
Successful advance only. Same identifiers plus native selected answer, photo_count, or skipped: true as applicable. No typed text, files or signup credentials; signup completion remains a server conversion. See funnel event reference. |
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 |
Explore Preset Clicked |
apps/brisket/src/features/analytics/posthog/hooks/use-explore-preset-click-track/index.tsx |
Explore card tap; carries example_id, available, and the view_id + position of the card so it joins the impression batches below |
Explore Card Impressions |
apps/brisket/src/features/analytics/posthog/hooks/use-explore-impression-track/index.tsx, buffered by apps/brisket/src/features/explore-page/explore-impressions.ts |
One batch of qualified card impressions per Explore feed view, sent every 60s while non-empty, at 100 cards, on a feed change, on grid unmount, when the tab hides, and on pagehide |
Shop VI Character Purchased |
hooks/use-vi-character-purchase-track/index.tsx:21 |
Shop checkout success |
Pro Tier Selected |
features/checkout/components/v2/pro-tier-slider/use-track-pro-tier-selected.ts |
User lands on a Pro tier via the slider (paywall / change-plan / usage / upgrade); carries surface, plan_type, plan_display_name, subscription_duration, credits_per_month, price. Also re-fires select_item for the slid-to tier (pricing-v3) |
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 |
Explore impressions. A card qualifies once per feed view after being at
least 50% visible for 1s with the tab visible; qualified cards are buffered and
sent as one Explore Card Impressions event. Batch-level properties:
view_id (new for every Explore visit and for every feed query: mode,
category, search or media-type change), count, flush_reason (interval |
cap | feed_change |
unmount | hidden | pagehide), mode (social | full_access),
ordering (newest, top_day, top_week, or top_month), category_id,
parent_category_id, search_term, media_type_filter, is_subscribed,
has_subscribed, is_mobile. Each entry of the impressions array is
{ example_id, position, media_type, aspect_ratio, explicitness }; position
is the card’s index in the whole loaded feed, not within a page, and
explicitness is standard or full_access. Per-card metrics unnest the
array in a SQL insight:
SELECT JSONExtractString(arrayJoin(JSONExtractArrayRaw(properties.impressions)), 'example_id') AS example_id, count() AS impressionsFROM eventsWHERE event = 'Explore Card Impressions'GROUP BY example_idCards covered by an open dialog still count, so early click-through rates run slightly low until that is addressed.
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.
Welcome quiz retirement
Section titled “Welcome quiz retirement”Phase 2 removes the welcome quiz emitters WELCOME_QUIZ_STARTED, WELCOME_QUIZ_FINISHED, WELCOME_QUIZ_QUESTION_ANSWERED and new welcome_quiz_* property writes. Historical analytics are retained. The quiz-entry user_signup emitter is removed rather than moved to /hop; existing Clerk-webhook ACCOUNT_CREATED remains the account-creation signal. Login, funnel impression/conversion, Klaviyo, referral and purchase tracking remain. No remote flags or dashboards are changed by this code release. See onboarding funnels.
4. Person properties
Section titled “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
Section titled “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 |
onboarding-quiz-v2 |
Code retired | No consumer after welcome quiz removal; remote retirement is separate | n/a |
having-problems-message |
implied ACTIVE | apps/brisket/src/features/global-issue-message/index.tsx:8 |
off |
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 |
Code retired | No consumer after welcome quiz removal; remote retirement is separate | n/a |
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 |
Create navigation, pages, and overlays no longer consume create-flow or faceted-product rollout flags; they always use the shared faceted flow.
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
Section titled “6. Dashboards”Verified via mcp__posthog__dashboards-get-all on 2026-09-03 (130 dashboards
total, 9 pinned). The pinned set is the canonical “we look at this” surface:
| Operating order | Dashboard | URL |
|---|---|---|
| 0 | 0. CEO Operating Dashboard | /dashboard/1990337 |
| 1 | 1. Growth, Acquisition & Unit Economics | /dashboard/2051295 |
| 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. Monetization (2026) | /dashboard/1492686 |
| 8 | 8. GPU & Credit Economics | CEO Cockpit | /dashboard/2054440 |
The numbered operating set is 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.
6a. Fal spend warehouse data
Section titled “6a. Fal spend warehouse data”Brain stores the Fal daily total in inference_provider_spend and its
endpoint allocation in inference_provider_spend_breakdown. The schema and
replacement behavior are defined in apps/brain/prisma/schema.prisma and
apps/brain/src/modules/domain/spend/repositories/spend.repository.ts.
The breakdown table is intentionally not part of the Brain HTTP contract. PostHog reads both spend tables directly from the existing Fennec Postgres warehouse source, so no Brain or Fennec application endpoint is required. The source does not automatically sync newly discovered schemas. Follow the Fal spend to PostHog rollout runbook for the migration, bounded backfill, warehouse refresh, query change, billing reconciliation, and rollback procedure.
7. Failure modes
Section titled “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. - Browser error filtering. Brisket’s browser telemetry excludes known PostHog fetch failures from error reporting. Check analytics delivery separately when dashboards have gaps; missing error reports do not prove successful analytics ingestion.
- 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.