Skip to content

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:

  1. Product analytics — funnel, retention, revenue, NSFW, acquisition.
  2. Feature flags — experiment gating and remote configuration consumed from both browser (posthog-js) and server (posthog-node, posthog-go).
  3. Session replay + web vitals — for product debugging, sampled.
  • Project: Phoenix (id: 111262) under organization Foxy AI (018cac02-779a-0000-316e-837fe06b96a4).
  • Region: US Cloud (https://us.i.posthog.com for ingestion, us.posthog.com for 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

ServiceSDKInit / client pathWhat is captured
brisket (Next.js, browser)posthog-js ^1.265.1 (apps/brisket/package.json)apps/brisket/src/features/analytics/posthog/PosthogProvider.tsxposthog.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.tsxPageviews, 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.tsnew 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.goposthog.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 wiredConfirmed: no posthog-* package in apps/brain/package.json.
fennec (admin React)None wiredConfirmed: no posthog-* package in apps/fennec/package.json.
strip / round (Go)None wiredOut of scope for product analytics.
flank / chuck / shankNone wiredOut 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 nameWhere emittedProperties
subscription_createdevents.go:104 TrackSubscriptionCreated, called from Chargebee event pollerplan_id, plan_type, duration, amount, currency, valid_until, invoice_id, discount_code
subscription_renewedevents.go:118 TrackSubscriptionRenewed, Chargebee renewal eventssame as subscription_created
subscription_cancelledevents.go:132 TrackSubscriptionCancelledplan_id, cancellation_reason, months_subscribed, credits_provided
subscription_upgradedevents.go:142 TrackSubscriptionUpgradedfrom_plan_type, to_plan_type, effective_date, ...
subscription_downgradedevents.go:155 TrackSubscriptionDowngradedfrom_plan_type, to_plan_type, effective_date, ...
payment_succeededevents.go:167 TrackPaymentSucceeded, Primer payment poller / renewal workeramount, currency, payment_method_type, transaction_type, invoice_id, subscription_id, attempt_number, psp, country
payment_failedevents.go:181 TrackPaymentFailed, Primer failed-payment polleramount, currency, decline_code, decline_message, decline_type, processor_decline_code, network_decline_code, reason_type, payment_method_type, ...
billing.payment.outcomeTrackPaymentOutcome (unified success/fail stream)union of payment props + status: succeeded | failed
refund_issuedTrackRefundIssuedamount, currency, original_payment_id, refund_type, invoice_id
top_up_purchasedTrackTopUpPurchased (credit packs)item_price_id, pack_name, credits_amount, amount, currency, invoice_id
card_fingerprint_limit_exceededpollprimerfailedpayments.go:278 (fraud)payment_id, user_id, card_fingerprint, seen_at
high_risk_decline_flaggedpollprimerfailedpayments.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_failedsirloin fraud + verification flowsper-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 nameSource fileTrigger
Generation Requestedfeatures/analytics/posthog/hooks/use-media-generation-request-track/index.tsx:139,183User submits a generation
Generation Downloadedhooks/use-generation-downloaded-track/index.tsx:28User downloads output
Generation Favoritedhooks/use-generation-favorited-track/index.tsx:27User favorites a media
Generation Archivedhooks/use-generation-archived-track/index.tsx:27User archives a generation
Generation Review Submittedhooks/use-generation-review-submit-track/index.tsx:25Generation review form
Character Media Prescreened / Character Media Acceptedhooks/use-media-prescreened/index.tsx:42Onboarding image gates
Character Dataset Edit Savedhooks/use-dataset-edit-track/index.tsx:14Character dataset save
Welcome Media Review Submittedhooks/use-welcome-media-review-track/index.tsx:30Welcome flow review
Explore Search Submittedhooks/use-explore-search-track/index.tsx:27Explore-page search
Explore Category Clickedhooks/use-explore-category-click-track/index.tsx:26Explore category tap
Shop VI Character Purchasedhooks/use-vi-character-purchase-track/index.tsx:21Shop checkout success
Character Required Dialog Viewed / ... Proceed to Shop / ... Proceed to Creationfeatures/explore-page/components/character-required-dialog/index.tsx:58,71,160Gating dialog interactions
sign_in_with_email, sign_in_with_google, sign_in_with_apple, sign_in_with_facebook, sign_in_with_twitterapp/(auth)/sign-{in,up}/...Auth method chosen
Deleted Accountapp/(main-layout)/profile/account/page.tsx:118Self-serve delete
Referral Link Copiedapp/(main-layout)/referrals/page.tsx:48Referral CTA
nsfw_* (e.g. nsfw_funnel_entry_clicked, nsfw_paywall_viewed, nsfw_kyc_started)features/analytics/posthog/hooks/use-nsfw-funnel-track/index.ts:24NSFW 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:38SPA 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 keyStatusConsumed inDefault
nsfw-onboardingACTIVEapps/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-viACTIVEapps/brisket/src/hooks/use-shop-enabled.ts:5off
new-create-flowACTIVEapps/brisket/src/hooks/use-new-create-flow.ts:4off
onboarding-quiz-v2ACTIVEapps/brisket/src/features/welcome-page/steps/what-type-of-content/index.tsx:33 (via ONBOARDING_QUIZ_V2_FLAG)off
having-problems-messageimplied ACTIVEapps/brisket/src/features/global-issue-message/index.tsx:8off
internal-test-pspACTIVE (payload flag)apps/brisket/src/features/checkout/components/v2/primer-components-checkout/test-psp-dropdown/index.tsx:8 (useFeatureFlagPayload)none
allow-primary-payment-removalACTIVE (test only)apps/brisket/src/features/billing-page/primer/primer-payments/payment-methods/payment-method/{index,delete-payment-method/index}.tsxoff
credits-and-subscription-upgrade-togetherACTIVEapps/brisket/src/features/usage/index.tsx:29off
yamber-warningsimplied ACTIVEapps/brisket/src/features/character-page-ccv4/hooks/use-yamber-enabled-ff.tsx:19 (posthog?.isFeatureEnabled?.('yamber-warnings'))off
welcome-paywall-slider-variantACTIVENo code consumer (grep across apps/); likely remote-config / GTM-onlyn/a
landing-of-creators-a-b-c-testACTIVENo code consumer (grep across apps/); likely remote-config / GTM-onlyn/a
sizzle-video-displayACTIVEapps/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.tsxoff
sirloin-tester-skip-identity-lockACTIVENo code consumer (grep across apps/sirloin/** returned no match); likely Posthog-only operational togglen/a
Paywall-AB-test, welcome-quiz-variant, magic-link-methodSTALEdrop or removen/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:

#DashboardURL
11. Acquisition (2026)/dashboard/1475849
22. Conversion (2026)/dashboard/1471691
33. Activation (2026)/dashboard/1475850
44. Engagement (2026)/dashboard/1475851
55. Retention (2026)/dashboard/1292835
66. Revenue (2026)/dashboard/1475852
77. Unit Economics (2026)/dashboard/1475853
8Activation - Weekly Subscriber Cohorts/dashboard/1517386
9Credit Value Analysis/dashboard/1505947
10Meta Ads Performance/dashboard/1515537
11Revenue Monitor/dashboard/1513385
12Video 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: 100 and flushes every Interval: 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 a useEffect keyed on user.user?.id. Events captured between hydration and the effect firing are anonymous and only attach to a person retroactively via Posthog’s $anon_distinct_id merge. person_profiles: 'identified_only' keeps anonymous traffic from creating ghost persons but means pre-identify funnel steps depend on alias merging.
  • Flag eval failures. useFeatureFlagEnabled returns undefined while flags load and on network failure. All consumers must coerce with !! or use useFeatureFlagReady. Server-side posthog.isFeatureEnabled(key, userId) is await-ed in tRPC routers — a Posthog outage adds latency to those calls; current code does not have a per-call timeout. TODO(@law): add AbortSignal.timeout wrapper.
  • Sentry filter. apps/brisket/instrumentation-client.ts matches /Failed to fetch.*ph\.foxy\.ai/i and /posthog.*failed/i and 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_vitals events are dropped client-side.

8. Secrets

VariableWhere it’s usedNotes
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_HOSTbrisket SDK configDefaults to https://us.i.posthog.com per apps/brisket/.env.example.
SIRLOIN_POSTHOG_API_KEYsirloin Go client (apps/sirloin/internal/pkg/posthog/client.go)Project key. Empty = no-op client (dev/test).
SIRLOIN_FOXY360_MCP_POSTHOG_URLsirloin Foxy360 connector (read path)Used by the Posthog source tool in Foxy360.
SIRLOIN_FOXY360_MCP_POSTHOG_AUTH_HEADERsirloin Foxy360 connectorBearer <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_vitals sampling (currently 20% kept).
  • Drop $dead_click / $rageclick if not used by any pinned dashboard.
  • Consolidate near-duplicate funnel events (the nsfw_* lower-snake-case set vs the NSFW * 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_id are set as person properties on identify — classified as Account PII in standards/security-model.md#data-classification. These flow only over TLS to us.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 in security-model.md.
  • apps/brisket/src/features/analytics/tracking/useTrackEvent.tsx scrubs NSFW-related strings before pushing to GTM dataLayer (the scrubber does not apply to Posthog capture — Posthog is the authoritative store and keeps the raw values).
  • Session replay: brisket loads posthog-js with no session_recording / disable_session_recording config in PosthogProvider.tsx (grep returned no replay-related keys), so the posthog-js default 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