api-flags-posthog-flags
PostHog feature flags, rollouts, A/B testing. Use when implementing gradual rollouts, A/B tests, kill switches, remote configuration, beta features, or user targeting with PostHog.
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-flags-posthog-flags/skills/api-flags-posthog-flags
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Feature Flags with PostHog
Quick Guide: Use PostHog feature flags for gradual rollouts, A/B testing, and remote configuration. Client-side:
useFeatureFlagEnabledhook. Server-side:posthog-nodewith local evaluation. Always pairuseFeatureFlagPayloadwithuseFeatureFlagEnabledfor experiments. Handle theundefinedloading state on every flag check.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST always pair useFeatureFlagPayload with useFeatureFlagEnabled or useFeatureFlagVariantKey for experiments - payload hooks don't send exposure events)
(You MUST use the feature flags secure API key (phs_*) for server-side local evaluation - personal API keys are deprecated for this use)
(You MUST handle the undefined state when flags are loading - never assume a flag is immediately available)
(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)
(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)
</critical_requirements>
Auto-detection: PostHog feature flags, useFeatureFlagEnabled, useFeatureFlagPayload, useFeatureFlagVariantKey, PostHogFeature, isFeatureEnabled, getFeatureFlag, gradual rollout, A/B test, experiment, multivariate flag
When to use:
- Gradual rollouts (deploy to 10% users, then 50%, then 100%)
- A/B testing with experiments (measure impact of changes)
- Kill switches (instantly disable features without deploy)
- Remote configuration (change behavior without code changes)
- Beta features opt-in (let users try new features)
- User targeting (show features to specific cohorts)
When NOT to use:
- Simple on/off switches that never change (use environment variables)
- Configuration that must be compile-time (use build flags)
- Secrets or sensitive data (use secret management)
- Features that should always be on (just ship the code)
Key patterns covered:
- Client-side flag evaluation with React hooks
- Server-side local evaluation for performance
- Boolean vs multivariate flags
- Gradual rollouts with percentage targeting
- A/B testing and experiments
- Payloads for remote configuration
- Local development overrides
- Flag cleanup and lifecycle management
Detailed Resources:
- examples/core.md - Boolean flags, multivariate flags, PostHogFeature component, payloads, experiments, rollouts, lifecycle management
- examples/server-side.md - Server-side evaluation, local evaluation setup, distributed environments
- examples/development.md - Local overrides, bootstrapping, onFeatureFlags callback
- reference.md - Decision frameworks and anti-patterns
<red_flags>
RED FLAGS
High Priority Issues:
- Using
useFeatureFlagPayloadalone for experiments (no exposure tracking) - Exposing Feature Flags Secure API key (
phs_*) on client (security violation) - No loading state handling (causes UI flash)
- Flags without owners or expiry dates (becomes permanent debt)
Medium Priority Issues:
- Magic string flag keys instead of constants (typos, hard to grep)
- Complex targeting rules on high-traffic flags (performance hit)
- Local evaluation in serverless/edge without external cache (cold start issues)
- Not using PostHog toolbar for local testing (harder debugging)
Common Mistakes:
- Checking flag in multiple places instead of wrapper function
- Not bootstrapping flags for SSR (content flash on hydration)
- Running experiments without defined primary metric
- Peeking at experiment results before completion
- Rolling out to 100% without cleanup plan
Gotchas & Edge Cases:
- PostHog uses deterministic hashing - same user always gets same variant
- Decreasing rollout percentage can remove users who were previously included
- Local evaluation requires Feature Flags Secure API Key (
phs_*) - personal API keys are deprecated - Flags load asynchronously - first render always has undefined
- GeoIP targeting uses server IP by default in posthog-node v3+
- Experiments need minimum 50 exposures per variant for results
- Stale flag = 100% rollout + not evaluated in 30 days
onFeatureFlagscallback receives three parameters:flags,flagVariants,{ errorsLoading }(third parameter)- External cache providers (Redis, KV) are experimental - Node.js/Python SDKs only
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST always pair useFeatureFlagPayload with useFeatureFlagEnabled or useFeatureFlagVariantKey for experiments - payload hooks don't send exposure events)
(You MUST use the feature flags secure API key (phs_*) for server-side local evaluation - personal API keys are deprecated for this use)
(You MUST handle the undefined state when flags are loading - never assume a flag is immediately available)
(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)
(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)
Failure to follow these rules will cause incorrect experiment results, security vulnerabilities, UI flashing, and technical debt.
</critical_reminders>
Sources
- PostHog React Integration
- PostHog Feature Flags
- PostHog Feature Flag Best Practices
- PostHog Server-Side Local Evaluation
- PostHog Creating Feature Flags
- PostHog How to Do a Phased Rollout
- PostHog Feature Flag Testing
- PostHog Experiments
- PostHog Feature Flag Overrides
- Don't Make These Feature Flag Mistakes
Files (skills)
-
examples
-
core.md 13.8 KB
# Feature Flags Core Examples > Essential patterns for PostHog feature flags. Reference from [SKILL.md](../SKILL.md). **Extended examples:** - [server-side.md](server-side.md) - Server-side evaluation with posthog-node - [development.md](development.md) - Local development overrides and bootstrapping --- ## Pattern 1: Client-Side Boolean Flags Use `useFeatureFlagEnabled` for simple on/off features. Handle the `undefined` loading state. ### Constants ```typescript // lib/feature-flags.ts export const FLAG_NEW_CHECKOUT = "new-checkout-flow"; export const FLAG_DARK_MODE = "dark-mode-enabled"; export const FLAG_BETA_DASHBOARD = "beta-dashboard"; ``` ### Good Example - Handle undefined state ```typescript // components/checkout-button.tsx import { useFeatureFlagEnabled } from "posthog-js/react"; import { FLAG_NEW_CHECKOUT } from "../lib/feature-flags"; export const CheckoutButton = () => { const isNewCheckout = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT); // Flag is loading - show nothing or skeleton if (isNewCheckout === undefined) { return <ButtonSkeleton />; } // Flag resolved - render appropriate UI if (isNewCheckout) { return <NewCheckoutButton />; } return <LegacyCheckoutButton />; }; ``` **Why good:** Named constant prevents typos, undefined check prevents flash of wrong content, explicit handling of all states ### Bad Example - No loading state, magic string ```typescript import { useFeatureFlagEnabled } from "posthog-js/react"; export const CheckoutButton = () => { // BAD: Magic string for flag key // BAD: Treating undefined as false causes flash of legacy UI const isNewCheckout = useFeatureFlagEnabled("new-checkout-flow"); if (isNewCheckout) { return <NewCheckoutButton />; } return <LegacyCheckoutButton />; // Shows briefly while loading! }; ``` **Why bad:** Magic string causes typos and makes flag hard to find for cleanup, undefined treated as false shows wrong UI briefly before flag loads --- ## Pattern 2: Multivariate Flags and Variants Use `useFeatureFlagVariantKey` for A/B tests with multiple variants. ### Constants ```typescript // lib/feature-flags.ts export const FLAG_PRICING_PAGE = "pricing-page-experiment"; // Variant constants prevent typos export const VARIANT_CONTROL = "control"; export const VARIANT_SIMPLE = "simple"; export const VARIANT_DETAILED = "detailed"; ``` ### Good Example - Multivariate flag with loading state ```typescript // components/pricing-page.tsx import { useFeatureFlagVariantKey } from "posthog-js/react"; import { FLAG_PRICING_PAGE, VARIANT_CONTROL, VARIANT_SIMPLE, VARIANT_DETAILED, } from "../lib/feature-flags"; export const PricingPage = () => { const variant = useFeatureFlagVariantKey(FLAG_PRICING_PAGE); // Loading state if (variant === undefined) { return <PricingPageSkeleton />; } // Render based on variant switch (variant) { case VARIANT_SIMPLE: return <SimplePricing />; case VARIANT_DETAILED: return <DetailedPricing />; case VARIANT_CONTROL: default: return <ControlPricing />; } }; ``` **Why good:** Variant constants prevent typos, switch statement handles all cases, default fallback to control variant, loading state prevents flash --- ## Pattern 3: PostHogFeature Component Use the `PostHogFeature` component for automatic exposure tracking and cleaner code. ### Good Example - PostHogFeature handles exposure tracking ```typescript import { PostHogFeature } from "posthog-js/react"; import { FLAG_BETA_DASHBOARD } from "../lib/feature-flags"; export const Dashboard = () => { return ( <PostHogFeature flag={FLAG_BETA_DASHBOARD} match={true} // Show when flag is true fallback={<LegacyDashboard />} > <BetaDashboard /> </PostHogFeature> ); }; ``` **Why good:** Automatic exposure event tracking, cleaner JSX, built-in fallback handling, less boilerplate than hooks ### Good Example - PostHogFeature with variant matching ```typescript import { PostHogFeature } from "posthog-js/react"; import { FLAG_PRICING_PAGE, VARIANT_SIMPLE } from "../lib/feature-flags"; export const PricingSection = () => { return ( <PostHogFeature flag={FLAG_PRICING_PAGE} match={VARIANT_SIMPLE} // Show when variant is "simple" fallback={<DefaultPricing />} > <SimplePricing /> </PostHogFeature> ); }; ``` **Why good:** Match specific variant values, automatic exposure tracking for experiment --- ## Pattern 4: Feature Flag Payloads for Remote Configuration Use `useFeatureFlagPayload` for dynamic configuration. Always pair with `useFeatureFlagEnabled` for experiments. ### Constants ```typescript // lib/feature-flags.ts export const FLAG_BANNER_CONFIG = "homepage-banner"; // Default payload for fallback export const DEFAULT_BANNER_CONFIG = { title: "Welcome", ctaText: "Get Started", ctaUrl: "/signup", backgroundColor: "#3B82F6", }; ``` ### Good Example - Payload with enabled check for experiment tracking ```typescript // components/homepage-banner.tsx import { useFeatureFlagEnabled, useFeatureFlagPayload } from "posthog-js/react"; import { FLAG_BANNER_CONFIG, DEFAULT_BANNER_CONFIG } from "../lib/feature-flags"; interface BannerConfig { title: string; ctaText: string; ctaUrl: string; backgroundColor: string; } export const HomepageBanner = () => { // CRITICAL: Call useFeatureFlagEnabled to send exposure event const isEnabled = useFeatureFlagEnabled(FLAG_BANNER_CONFIG); // Get the payload configuration const payload = useFeatureFlagPayload(FLAG_BANNER_CONFIG) as BannerConfig | undefined; // Not enabled or loading if (!isEnabled) { return null; } // Use payload or fall back to defaults const config = payload ?? DEFAULT_BANNER_CONFIG; return ( <div style={{ backgroundColor: config.backgroundColor }}> <h2>{config.title}</h2> <a href={config.ctaUrl}>{config.ctaText}</a> </div> ); }; ``` **Why good:** useFeatureFlagEnabled sends $feature_flag_called event for experiment tracking, payload provides dynamic config, default config prevents undefined errors ### Bad Example - Payload without enabled check ```typescript export const HomepageBanner = () => { // BAD: Only using payload - no $feature_flag_called event sent! // Experiments won't track exposure correctly const payload = useFeatureFlagPayload("homepage-banner"); if (!payload) return null; return <Banner {...payload} />; }; ``` **Why bad:** useFeatureFlagPayload alone does NOT send exposure events, A/B test results will be incorrect, PostHog won't know user was exposed to experiment --- ## Pattern 5: A/B Testing with Experiments Run experiments to measure feature impact. ### Setting Up an Experiment ```typescript // lib/feature-flags.ts // Experiment flag with variants export const FLAG_SIGNUP_EXPERIMENT = "signup-flow-experiment"; export const VARIANT_CONTROL = "control"; export const VARIANT_STREAMLINED = "streamlined"; export const VARIANT_SOCIAL_FIRST = "social-first"; // Minimum exposures for statistical significance export const MIN_EXPERIMENT_EXPOSURES = 50; ``` ### Good Example - Experiment with variant tracking ```typescript // components/signup-flow.tsx import { useFeatureFlagVariantKey } from "posthog-js/react"; import { FLAG_SIGNUP_EXPERIMENT, VARIANT_CONTROL, VARIANT_STREAMLINED, VARIANT_SOCIAL_FIRST, } from "../lib/feature-flags"; export const SignupFlow = () => { const variant = useFeatureFlagVariantKey(FLAG_SIGNUP_EXPERIMENT); // Handle loading if (variant === undefined) { return <SignupSkeleton />; } // Render variant - PostHog automatically tracks exposure switch (variant) { case VARIANT_STREAMLINED: return <StreamlinedSignup />; case VARIANT_SOCIAL_FIRST: return <SocialFirstSignup />; case VARIANT_CONTROL: default: return <ControlSignup />; } }; ``` ### Tracking Experiment Goals ```typescript // components/signup-success.tsx import { usePostHog } from "posthog-js/react"; const EVENT_SIGNUP_COMPLETED = "signup_completed"; export const SignupSuccess = () => { const posthog = usePostHog(); useEffect(() => { // Track conversion event - PostHog links to experiment exposure posthog.capture(EVENT_SIGNUP_COMPLETED, { method: "email", // Additional properties for analysis }); }, [posthog]); return <SuccessMessage />; }; ``` **Why good:** useFeatureFlagVariantKey sends exposure event, capture sends conversion event, PostHog calculates statistical significance automatically **Experiment requirements:** - Minimum 50 exposures per variant for results - Define primary metric before starting - Don't peek at results early (statistical errors) - Run for predetermined duration --- ## Pattern 6: Gradual Rollouts and User Targeting Use percentage-based rollouts for safe feature releases. ### Rollout Strategy ```typescript // lib/feature-flags.ts // Document rollout plan in constants export const FLAG_NEW_PAYMENT_FLOW = "new-payment-flow"; // Rollout phases (configured in PostHog dashboard) // Phase 1: 5% - Internal testing // Phase 2: 25% - Beta users // Phase 3: 50% - Half of users // Phase 4: 100% - General availability ``` ### Implementation ```typescript // components/payment-form.tsx import { useFeatureFlagEnabled } from "posthog-js/react"; import { FLAG_NEW_PAYMENT_FLOW } from "../lib/feature-flags"; export const PaymentForm = () => { const isNewFlow = useFeatureFlagEnabled(FLAG_NEW_PAYMENT_FLOW); // Loading state if (isNewFlow === undefined) { return <PaymentFormSkeleton />; } // PostHog assigns users deterministically based on distinct_id // Same user always gets same variant (sticky assignment) if (isNewFlow) { return <NewPaymentFlow />; } return <LegacyPaymentFlow />; }; ``` **Why good:** Deterministic assignment ensures consistent experience per user, gradual rollout catches issues before 100% exposure **Rollout best practices:** 1. Start at 5% and monitor error rates 2. Increase to 25% after 24 hours if stable 3. Increase to 50% after another 24 hours 4. Roll out to 100% if all metrics look good 5. Keep kill switch ready for instant rollback ### User and Cohort Targeting Target specific users based on properties or cohorts. ```typescript // lib/posthog-server.ts export async function checkFeatureForUser( userId: string, userProperties: { email: string; plan: string; company?: string; }, ) { const isEnabled = await posthog.isFeatureEnabled( FLAG_ENTERPRISE_FEATURE, userId, { // Properties used for targeting rules in PostHog personProperties: { email: userProperties.email, plan: userProperties.plan, company: userProperties.company, }, }, ); return isEnabled; } ``` **Why good:** Person properties enable targeting (e.g., plan = "enterprise"), server-side evaluation prevents client manipulation of targeting ### Cohort Targeting in PostHog Dashboard ```markdown ## PostHog Dashboard Configuration 1. Create cohort: "Beta Users" - Condition: email contains "@beta.example.com" OR - Condition: has property beta_tester = true 2. Create feature flag: "beta-feature" - Release condition: cohort = "Beta Users" - Rollout: 100% 3. Add second condition set: - Condition: All users - Rollout: 0% (default off for everyone else) ``` **Why good:** Cohorts are reusable across flags, easy to add/remove users from cohort, phased rollouts just swap cohorts --- ## Pattern 7: Flag Cleanup and Lifecycle Management Plan for flag removal from day one. Stale flags become technical debt. ### Flag Documentation Pattern ```typescript // lib/feature-flags.ts /** * Flag: new-checkout-flow * Owner: @john-doe * Created: 2025-01-15 * Expected Removal: 2025-02-15 (after 30 days at 100%) * Purpose: Test streamlined checkout with fewer steps * Rollout Status: 50% as of 2025-01-20 */ export const FLAG_NEW_CHECKOUT = "new-checkout-flow"; /** * Flag: holiday-banner * Owner: @marketing-team * Created: 2025-12-01 * Expected Removal: 2025-01-02 (seasonal) * Purpose: Holiday promotion banner */ export const FLAG_HOLIDAY_BANNER = "holiday-banner"; ``` **Why good:** Documentation makes ownership clear, expected removal date prevents flags from becoming stale, purpose helps future developers understand intent ### Wrapper Pattern for Easy Cleanup ```typescript // lib/feature-flags.ts // Good Example - Single wrapper function for flag export function isNewCheckoutEnabled(flagValue: boolean | undefined): boolean { // Single point of truth for this flag's behavior // When removing flag, only update this function return flagValue === true; } // Usage in components const flagValue = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT); if (isNewCheckoutEnabled(flagValue)) { // New checkout code } // When removing flag, change to: export function isNewCheckoutEnabled(_flagValue: boolean | undefined): boolean { return true; // Always enabled, ready for code cleanup } ``` **Why good:** Single function to update when removing flag, grep for function name finds all usages, gradual cleanup possible ### Stale Flag Detection ```typescript // scripts/check-stale-flags.ts // Run in CI to detect stale flags /** * PostHog marks a flag as stale when: * 1. Rolled out to 100% AND * 2. Not evaluated in last 30 days * * Check stale flags in PostHog dashboard: * Feature Flags > Filter by "Stale" */ const STALE_FLAGS_QUERY = ` SELECT key, created_at, last_evaluated_at FROM feature_flags WHERE rollout_percentage = 100 AND last_evaluated_at < NOW() - INTERVAL '30 days' `; ``` **Best practices for flag cleanup:** 1. Set expiry date when creating flag 2. Assign owner who is responsible for removal 3. Create cleanup ticket when flag hits 100% 4. Wrap flag in single function for easy grep 5. Run weekly "flag debt" review 6. Archive flag in PostHog before removing code --- _Back to [SKILL.md](../SKILL.md) | Extended examples: [server-side.md](server-side.md) | [development.md](development.md)_ -
development.md 3.3 KB
# Local Development Overrides Examples > Override flags locally without affecting other users. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern: Local Development Overrides Override flags locally without affecting other users. ### Method 1: Toolbar Override (Browser) ```typescript // Use PostHog toolbar in development // 1. Enable toolbar in PostHog project settings // 2. Click feature flags icon in toolbar // 3. Toggle flags on/off for your session only // This only affects YOUR browser, not other users // Does NOT affect server-side evaluation ``` ### Method 2: Code Override (Development Only) ```typescript // lib/posthog-client.ts import posthog from "posthog-js"; const IS_DEVELOPMENT = process.env.NODE_ENV === "development"; // Good Example - Development-only flag overrides export function initPostHog() { posthog.init(process.env.POSTHOG_PUBLIC_KEY!, { api_host: process.env.POSTHOG_PUBLIC_HOST, loaded: (posthog) => { if (IS_DEVELOPMENT) { // Override specific flags for local development posthog.featureFlags.overrideFeatureFlags({ "new-checkout-flow": true, "beta-dashboard": "variant-a", }); } }, }); } ``` **Why good:** IS_DEVELOPMENT check ensures production isn't affected, overrideFeatureFlags works immediately, supports both boolean and variant values ### Method 3: Bootstrapping for SSR ```typescript // lib/posthog-client.ts import posthog from "posthog-js"; // Good Example - Bootstrap flags for immediate availability export function initPostHogWithBootstrap( bootstrapFlags: Record<string, boolean | string>, distinctId: string, ) { posthog.init(process.env.POSTHOG_PUBLIC_KEY!, { api_host: process.env.POSTHOG_PUBLIC_HOST, bootstrap: { featureFlags: bootstrapFlags, distinctID: distinctId, // Match server-side ID isIdentifiedID: true, // True if using email/DB ID, false for anonymous }, }); } // Usage in server component // Fetch flags server-side, pass to client for immediate availability ``` **Why good:** Bootstrap eliminates flash of wrong content, flags available immediately on page load, essential for SSR/SSG, distinctID ensures consistency with server evaluation --- ### Method 4: onFeatureFlags Callback ```typescript // lib/posthog-client.ts import posthog from "posthog-js"; // Good Example - Wait for flags to load before using export function initPostHogWithCallback() { posthog.init(process.env.POSTHOG_PUBLIC_KEY!, { api_host: process.env.POSTHOG_PUBLIC_HOST, loaded: (posthog) => { // Called when PostHog SDK is loaded posthog.onFeatureFlags((flags, flagVariants, { errorsLoading }) => { // flags: string[] - list of flag keys // flagVariants: Record<string, string | boolean> // errorsLoading: boolean | undefined - true if request failed/timed out if (errorsLoading) { console.warn("Feature flags failed to load, using defaults"); return; } // Flags are now available console.log("Feature flags loaded:", flagVariants); }); }, }); } ``` **Why good:** `errorsLoading` parameter enables graceful degradation, callback fires on every flag reload, essential for handling network failures --- _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_ -
experiments.md 2.3 KB
# A/B Testing with Experiments Examples > A/B testing patterns with PostHog experiments. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern: A/B Testing with Experiments Run experiments to measure feature impact. ### Setting Up an Experiment ```typescript // lib/feature-flags.ts // Experiment flag with variants export const FLAG_SIGNUP_EXPERIMENT = "signup-flow-experiment"; export const VARIANT_CONTROL = "control"; export const VARIANT_STREAMLINED = "streamlined"; export const VARIANT_SOCIAL_FIRST = "social-first"; // Minimum exposures for statistical significance export const MIN_EXPERIMENT_EXPOSURES = 50; ``` ### Good Example - Experiment with variant tracking ```typescript // components/signup-flow.tsx import { useFeatureFlagVariantKey } from "posthog-js/react"; import { FLAG_SIGNUP_EXPERIMENT, VARIANT_CONTROL, VARIANT_STREAMLINED, VARIANT_SOCIAL_FIRST, } from "@/lib/feature-flags"; export const SignupFlow = () => { const variant = useFeatureFlagVariantKey(FLAG_SIGNUP_EXPERIMENT); // Handle loading if (variant === undefined) { return <SignupSkeleton />; } // Render variant - PostHog automatically tracks exposure switch (variant) { case VARIANT_STREAMLINED: return <StreamlinedSignup />; case VARIANT_SOCIAL_FIRST: return <SocialFirstSignup />; case VARIANT_CONTROL: default: return <ControlSignup />; } }; ``` ### Tracking Experiment Goals ```typescript // components/signup-success.tsx import { usePostHog } from "posthog-js/react"; const EVENT_SIGNUP_COMPLETED = "signup_completed"; export const SignupSuccess = () => { const posthog = usePostHog(); useEffect(() => { // Track conversion event - PostHog links to experiment exposure posthog.capture(EVENT_SIGNUP_COMPLETED, { method: "email", // Additional properties for analysis }); }, [posthog]); return <SuccessMessage />; }; ``` **Why good:** useFeatureFlagVariantKey sends exposure event, capture sends conversion event, PostHog calculates statistical significance automatically **Experiment requirements:** - Minimum 50 exposures per variant for results - Define primary metric before starting - Don't peek at results early (statistical errors) - Run for predetermined duration --- _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_ -
lifecycle.md 2.6 KB
# Flag Cleanup and Lifecycle Management Examples > Plan for flag removal from day one. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern: Flag Cleanup and Lifecycle Management Plan for flag removal from day one. Stale flags become technical debt. ### Flag Documentation Pattern ```typescript // lib/feature-flags.ts /** * Flag: new-checkout-flow * Owner: @john-doe * Created: 2025-01-15 * Expected Removal: 2025-02-15 (after 30 days at 100%) * Purpose: Test streamlined checkout with fewer steps * Rollout Status: 50% as of 2025-01-20 */ export const FLAG_NEW_CHECKOUT = "new-checkout-flow"; /** * Flag: holiday-banner * Owner: @marketing-team * Created: 2025-12-01 * Expected Removal: 2025-01-02 (seasonal) * Purpose: Holiday promotion banner */ export const FLAG_HOLIDAY_BANNER = "holiday-banner"; ``` **Why good:** Documentation makes ownership clear, expected removal date prevents flags from becoming stale, purpose helps future developers understand intent ### Wrapper Pattern for Easy Cleanup ```typescript // lib/feature-flags.ts // Good Example - Single wrapper function for flag export function isNewCheckoutEnabled(flagValue: boolean | undefined): boolean { // Single point of truth for this flag's behavior // When removing flag, only update this function return flagValue === true; } // Usage in components const flagValue = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT); if (isNewCheckoutEnabled(flagValue)) { // New checkout code } // When removing flag, change to: export function isNewCheckoutEnabled(_flagValue: boolean | undefined): boolean { return true; // Always enabled, ready for code cleanup } ``` **Why good:** Single function to update when removing flag, grep for function name finds all usages, gradual cleanup possible ### Stale Flag Detection ```typescript // scripts/check-stale-flags.ts // Run in CI to detect stale flags /** * PostHog marks a flag as stale when: * 1. Rolled out to 100% AND * 2. Not evaluated in last 30 days * * Check stale flags in PostHog dashboard: * Feature Flags > Filter by "Stale" */ const STALE_FLAGS_QUERY = ` SELECT key, created_at, last_evaluated_at FROM feature_flags WHERE rollout_percentage = 100 AND last_evaluated_at < NOW() - INTERVAL '30 days' `; ``` **Best practices for flag cleanup:** 1. Set expiry date when creating flag 2. Assign owner who is responsible for removal 3. Create cleanup ticket when flag hits 100% 4. Wrap flag in single function for easy grep 5. Run weekly "flag debt" review 6. Archive flag in PostHog before removing code --- _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_ -
payloads.md 2.3 KB
# Feature Flag Payloads Examples > Remote configuration with JSON payloads. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern: Feature Flag Payloads for Remote Configuration Use `useFeatureFlagPayload` for dynamic configuration. Always pair with `useFeatureFlagEnabled` for experiments. ### Constants ```typescript // lib/feature-flags.ts export const FLAG_BANNER_CONFIG = "homepage-banner"; // Default payload for fallback export const DEFAULT_BANNER_CONFIG = { title: "Welcome", ctaText: "Get Started", ctaUrl: "/signup", backgroundColor: "#3B82F6", }; ``` ### Good Example - Payload with enabled check for experiment tracking ```typescript // components/homepage-banner.tsx import { useFeatureFlagEnabled, useFeatureFlagPayload } from "posthog-js/react"; import { FLAG_BANNER_CONFIG, DEFAULT_BANNER_CONFIG } from "@/lib/feature-flags"; interface BannerConfig { title: string; ctaText: string; ctaUrl: string; backgroundColor: string; } export const HomepageBanner = () => { // CRITICAL: Call useFeatureFlagEnabled to send exposure event const isEnabled = useFeatureFlagEnabled(FLAG_BANNER_CONFIG); // Get the payload configuration const payload = useFeatureFlagPayload(FLAG_BANNER_CONFIG) as BannerConfig | undefined; // Not enabled or loading if (!isEnabled) { return null; } // Use payload or fall back to defaults const config = payload ?? DEFAULT_BANNER_CONFIG; return ( <div style={{ backgroundColor: config.backgroundColor }}> <h2>{config.title}</h2> <a href={config.ctaUrl}>{config.ctaText}</a> </div> ); }; ``` **Why good:** useFeatureFlagEnabled sends $feature_flag_called event for experiment tracking, payload provides dynamic config, default config prevents undefined errors ### Bad Example - Payload without enabled check ```typescript export const HomepageBanner = () => { // BAD: Only using payload - no $feature_flag_called event sent! // Experiments won't track exposure correctly const payload = useFeatureFlagPayload("homepage-banner"); if (!payload) return null; return <Banner {...payload} />; }; ``` **Why bad:** useFeatureFlagPayload alone does NOT send exposure events, A/B test results will be incorrect, PostHog won't know user was exposed to experiment --- _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_ -
rollouts.md 2.9 KB
# Gradual Rollouts and Targeting Examples > Gradual rollouts and user targeting patterns. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern 1: Gradual Rollouts Use percentage-based rollouts for safe feature releases. ### Rollout Strategy ```typescript // lib/feature-flags.ts // Document rollout plan in constants export const FLAG_NEW_PAYMENT_FLOW = "new-payment-flow"; // Rollout phases (configured in PostHog dashboard) // Phase 1: 5% - Internal testing // Phase 2: 25% - Beta users // Phase 3: 50% - Half of users // Phase 4: 100% - General availability ``` ### Implementation ```typescript // components/payment-form.tsx import { useFeatureFlagEnabled } from "posthog-js/react"; import { FLAG_NEW_PAYMENT_FLOW } from "@/lib/feature-flags"; export const PaymentForm = () => { const isNewFlow = useFeatureFlagEnabled(FLAG_NEW_PAYMENT_FLOW); // Loading state if (isNewFlow === undefined) { return <PaymentFormSkeleton />; } // PostHog assigns users deterministically based on distinct_id // Same user always gets same variant (sticky assignment) if (isNewFlow) { return <NewPaymentFlow />; } return <LegacyPaymentFlow />; }; ``` **Why good:** Deterministic assignment ensures consistent experience per user, gradual rollout catches issues before 100% exposure **Rollout best practices:** 1. Start at 5% and monitor error rates 2. Increase to 25% after 24 hours if stable 3. Increase to 50% after another 24 hours 4. Roll out to 100% if all metrics look good 5. Keep kill switch ready for instant rollback --- ## Pattern 2: User and Cohort Targeting Target specific users based on properties or cohorts. ### Good Example - Targeting with person properties ```typescript // lib/posthog-server.ts export async function checkFeatureForUser( userId: string, userProperties: { email: string; plan: string; company?: string; }, ) { const isEnabled = await posthog.isFeatureEnabled( FLAG_ENTERPRISE_FEATURE, userId, { // Properties used for targeting rules in PostHog personProperties: { email: userProperties.email, plan: userProperties.plan, company: userProperties.company, }, }, ); return isEnabled; } ``` **Why good:** Person properties enable targeting (e.g., plan = "enterprise"), server-side evaluation prevents client manipulation of targeting ### Cohort Targeting in PostHog Dashboard ```markdown ## PostHog Dashboard Configuration 1. Create cohort: "Beta Users" - Condition: email contains "@beta.example.com" OR - Condition: has property beta_tester = true 2. Create feature flag: "beta-feature" - Release condition: cohort = "Beta Users" - Rollout: 100% 3. Add second condition set: - Condition: All users - Rollout: 0% (default off for everyone else) ``` **Why good:** Cohorts are reusable across flags, easy to add/remove users from cohort, phased rollouts just swap cohorts --- _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_ -
server-side.md 7.2 KB
# Server-Side Flag Evaluation Examples > Server-side evaluation with posthog-node. See [SKILL.md](../SKILL.md) for core concepts. --- ## Pattern: Server-Side Flag Evaluation Use `posthog-node` for server-side evaluation. Use local evaluation for performance. ### Setup ```typescript // lib/posthog-server.ts import { PostHog } from "posthog-node"; const POSTHOG_POLL_INTERVAL_MS = 30_000; // 30 seconds (SDK default) // Initialize with local evaluation // Use Feature Flags Secure API Key (phs_*) from project settings > Feature Flags tab // Personal API keys are DEPRECATED for local evaluation export const posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: process.env.POSTHOG_HOST || "https://us.i.posthog.com", // Enable local evaluation with feature flags secure key (phs_*) personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY, // Poll for flag definition updates (default: 30000ms / 30 seconds) featureFlagsPollingInterval: POSTHOG_POLL_INTERVAL_MS, }); ``` **Note:** Get the Feature Flags Secure API Key from PostHog Project Settings > Feature Flags tab. The key starts with `phs_`. Personal API keys are deprecated for local evaluation. ### Good Example - Server-side flag evaluation in an API handler ```typescript // api/dashboard.ts (adapt to your server framework) import { posthog } from "../lib/posthog-server"; import { FLAG_BETA_DASHBOARD } from "../lib/feature-flags"; export async function handleGetDashboard(request: Request) { const userId = request.headers.get("x-user-id"); if (!userId) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, }); } // Evaluate flag server-side with user context const isBetaDashboard = await posthog.isFeatureEnabled( FLAG_BETA_DASHBOARD, userId, { // Provide person properties for targeting rules personProperties: { email: request.headers.get("x-user-email"), plan: request.headers.get("x-user-plan"), }, }, ); const dashboard = isBetaDashboard ? { dashboard: "beta" } : { dashboard: "stable" }; return new Response(JSON.stringify(dashboard)); } ``` **Why good:** Local evaluation reduces latency (500ms to 50ms), personProperties enable targeting, server-side prevents client manipulation ### Good Example - Local-only evaluation for performance ```typescript const flag = await posthog.isFeatureEnabled(FLAG_BETA_DASHBOARD, userId, { personProperties: { plan: "enterprise" }, // Only evaluate locally - don't fall back to server onlyEvaluateLocally: true, }); // Returns undefined if local evaluation fails if (flag === undefined) { // Fall back to default behavior return defaultDashboard(); } ``` **Why good:** onlyEvaluateLocally prevents server roundtrips, explicit undefined handling for fallback ### Bad Example - Server-side without local evaluation ```typescript import { PostHog } from "posthog-node"; // BAD: No personalApiKey = no local evaluation const posthog = new PostHog(process.env.POSTHOG_API_KEY!); // BAD: Every call makes a network request (500ms latency) const flag = await posthog.isFeatureEnabled("beta-dashboard", userId); ``` **Why bad:** No local evaluation = network request per flag check, high latency (500ms vs 10-50ms), increases PostHog API costs --- ## Pattern: Distributed/Stateless Environments (Serverless/Edge) In serverless/edge environments, the default in-memory cache causes performance issues due to per-request initialization. Use an external cache provider to share flag definitions. ### The Problem ```typescript // BAD: In Lambda/Edge, each invocation may cold start // Flag definitions are fetched on EVERY cold start = slow + expensive const posthog = new PostHog(process.env.POSTHOG_API_KEY!, { personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY, }); // Cold start: +500ms to fetch definitions ``` ### Solution 1: External Cache (Redis) - Node.js/Python Only ```typescript // lib/posthog-server-redis.ts import { PostHog } from "posthog-node"; import type { FlagDefinitionCacheProvider, FlagDefinitionCacheData, } from "posthog-node"; import { createClient } from "redis"; const CACHE_TTL_SECONDS = 300; // 5 minutes const CACHE_KEY = "posthog:flag-definitions"; const LOCK_KEY = "posthog:flag-definitions:lock"; const LOCK_TTL_SECONDS = 60; // Create a Redis-based cache provider (experimental feature) const createRedisCache = (redisUrl: string): FlagDefinitionCacheProvider => { const client = createClient({ url: redisUrl }); client.connect(); return { // Retrieve cached flag definitions async getFlagDefinitions(): Promise<FlagDefinitionCacheData | undefined> { const data = await client.get(CACHE_KEY); return data ? JSON.parse(data) : undefined; }, // Only one instance should fetch definitions at a time async shouldFetchFlagDefinitions(): Promise<boolean> { const acquired = await client.set(LOCK_KEY, "1", { NX: true, EX: LOCK_TTL_SECONDS, }); return acquired !== null; }, // Store definitions after a successful fetch async onFlagDefinitionsReceived( data: FlagDefinitionCacheData, ): Promise<void> { await client.setEx(CACHE_KEY, CACHE_TTL_SECONDS, JSON.stringify(data)); }, // Release locks and close connections async shutdown(): Promise<void> { await client.del(LOCK_KEY); await client.quit(); }, }; }; export const posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: process.env.POSTHOG_HOST || "https://us.i.posthog.com", personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY, // Use Redis for shared flag definitions across instances flagDefinitionCacheProvider: createRedisCache(process.env.REDIS_URL!), }); ``` **Why good:** All Lambda/Edge instances share one cache, `shouldFetchFlagDefinitions` ensures only one instance fetches, eliminates cold start penalty ### Solution 2: Split Read/Write Pattern (Cloudflare KV) For storage backends without atomic locking (Cloudflare KV), use a cron job to write and workers to read. ```typescript // scheduled-worker.ts (cron job - writes to KV) import { PostHog } from "posthog-node"; const CACHE_KEY = "posthog-flags"; export default { async scheduled(event: ScheduledEvent, env: Env) { const posthog = new PostHog(env.POSTHOG_API_KEY, { personalApiKey: env.POSTHOG_FEATURE_FLAGS_KEY, }); // Fetch latest flag definitions const definitions = await posthog.getAllFlags("system"); // Write to KV for all workers to read await env.FLAGS_KV.put(CACHE_KEY, JSON.stringify(definitions)); }, }; ``` ```typescript // request-worker.ts (handles requests - reads from KV) const CACHE_KEY = "posthog-flags"; export default { async fetch(request: Request, env: Env) { // Read flag definitions from KV (no API call) const definitions = await env.FLAGS_KV.get(CACHE_KEY, "json"); // Evaluate locally without network request const isEnabled = definitions?.["beta-feature"] === true; return new Response(JSON.stringify({ isEnabled })); }, }; ``` **Why good:** Zero network requests for flag evaluation, cron controls update frequency, works with any KV-like storage **Note:** External cache providers are currently experimental and available in Node.js and Python SDKs only. --- _Back to [SKILL.md](../SKILL.md) | [core.md](core.md)_
-
-
reference.md 4 KB
# Feature Flags Reference > Decision frameworks, anti-patterns, and red flags for PostHog feature flags. --- ## Decision Framework ### Flag Type Selection ``` What kind of feature flag do you need? Is it a simple on/off switch? +-- YES -> Boolean flag | +-- Need to target specific users? | | +-- YES -> Add release conditions (cohorts, properties) | | +-- NO -> Use percentage rollout only | +-- Need remote configuration? | +-- YES -> Add JSON payload to flag | +-- NO -> Boolean value is sufficient +-- NO -> Is it an A/B test with variants? +-- YES -> Multivariate flag | +-- How many variants? | +-- 2 (A/B) -> control + test | +-- 3+ (MVT) -> control + multiple tests +-- NO -> Is it for experiments with metrics? +-- YES -> Create as Experiment in PostHog +-- NO -> Consider if you really need a flag ``` ### Client vs Server Evaluation ``` Where should you evaluate the flag? Is the feature visible in UI? +-- YES -> Client-side (useFeatureFlagEnabled) | +-- Need to prevent content flash? | +-- YES -> Bootstrap flags from server | +-- NO -> Handle undefined loading state +-- NO -> Is it API/backend behavior? +-- YES -> Server-side (posthog-node) | +-- High traffic endpoint? | +-- YES -> Use local evaluation | +-- NO -> Regular evaluation is fine +-- NO -> Evaluate in component Is the flag security-sensitive? +-- YES -> Server-side only (prevents client manipulation) +-- NO -> Either works ``` ### Quick Reference | Use Case | Flag Type | Evaluation | | ------------------ | ----------------- | ----------- | | Gradual rollout | Boolean | Client | | A/B test | Multivariate | Client | | Kill switch | Boolean | Both | | Beta access | Boolean + cohort | Client | | API behavior | Boolean | Server | | Remote config | Boolean + payload | Client | | Pricing experiment | Multivariate | Client | | Security feature | Boolean | Server only | --- ## Anti-Patterns ### Using Payload Without Enabled Check ```typescript // ANTI-PATTERN: Payload alone doesn't send exposure event const payload = useFeatureFlagPayload("experiment-flag"); // PostHog won't know user was exposed! // CORRECT: Always pair with enabled check const isEnabled = useFeatureFlagEnabled("experiment-flag"); const payload = useFeatureFlagPayload("experiment-flag"); ``` **Why it's wrong:** Experiments require exposure events to calculate results. Payload hook doesn't send them. --- ### Ignoring Loading State ```typescript // ANTI-PATTERN: Treating undefined as false const isEnabled = useFeatureFlagEnabled("new-feature"); if (isEnabled) { /* new */ } else { /* old - shows briefly! */ } // CORRECT: Handle all three states if (isEnabled === undefined) return <Skeleton />; if (isEnabled) return <NewFeature />; return <OldFeature />; ``` **Why it's wrong:** Users see flash of old UI before flag loads, looks buggy. --- ### Flags Without Owners ```typescript // ANTI-PATTERN: No documentation export const FLAG_SOMETHING = "some-feature"; // CORRECT: Document owner and lifecycle /** * Owner: @jane-doe * Created: 2025-01-15 * Expected Removal: 2025-02-15 */ export const FLAG_SOMETHING = "some-feature"; ``` **Why it's wrong:** Orphaned flags become permanent technical debt. --- ### Flag Sprawl Across Codebase ```typescript // ANTI-PATTERN: Flag checked in multiple places // file1.tsx if (useFeatureFlagEnabled("new-feature")) { ... } // file2.tsx if (useFeatureFlagEnabled("new-feature")) { ... } // file3.tsx if (useFeatureFlagEnabled("new-feature")) { ... } // CORRECT: Wrapper function in one place // lib/feature-flags.ts export function isNewFeatureEnabled(flag: boolean | undefined) { return flag === true; } ``` **Why it's wrong:** Hard to find all usages during cleanup, easy to miss one. > See [SKILL.md](SKILL.md) for red flags, gotchas, and edge cases. -
SKILL.md 11.3 KB
--- name: api-flags-posthog-flags description: PostHog feature flags, rollouts, A/B testing. Use when implementing gradual rollouts, A/B tests, kill switches, remote configuration, beta features, or user targeting with PostHog. --- # Feature Flags with PostHog > **Quick Guide:** Use PostHog feature flags for gradual rollouts, A/B testing, and remote configuration. Client-side: `useFeatureFlagEnabled` hook. Server-side: `posthog-node` with local evaluation. Always pair `useFeatureFlagPayload` with `useFeatureFlagEnabled` for experiments. Handle the `undefined` loading state on every flag check. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST always pair `useFeatureFlagPayload` with `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` for experiments - payload hooks don't send exposure events)** **(You MUST use the feature flags secure API key (phs\_\*) for server-side local evaluation - personal API keys are deprecated for this use)** **(You MUST handle the `undefined` state when flags are loading - never assume a flag is immediately available)** **(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)** **(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)** </critical_requirements> --- **Auto-detection:** PostHog feature flags, useFeatureFlagEnabled, useFeatureFlagPayload, useFeatureFlagVariantKey, PostHogFeature, isFeatureEnabled, getFeatureFlag, gradual rollout, A/B test, experiment, multivariate flag **When to use:** - Gradual rollouts (deploy to 10% users, then 50%, then 100%) - A/B testing with experiments (measure impact of changes) - Kill switches (instantly disable features without deploy) - Remote configuration (change behavior without code changes) - Beta features opt-in (let users try new features) - User targeting (show features to specific cohorts) **When NOT to use:** - Simple on/off switches that never change (use environment variables) - Configuration that must be compile-time (use build flags) - Secrets or sensitive data (use secret management) - Features that should always be on (just ship the code) **Key patterns covered:** - Client-side flag evaluation with React hooks - Server-side local evaluation for performance - Boolean vs multivariate flags - Gradual rollouts with percentage targeting - A/B testing and experiments - Payloads for remote configuration - Local development overrides - Flag cleanup and lifecycle management --- **Detailed Resources:** - [examples/core.md](examples/core.md) - Boolean flags, multivariate flags, PostHogFeature component, payloads, experiments, rollouts, lifecycle management - [examples/server-side.md](examples/server-side.md) - Server-side evaluation, local evaluation setup, distributed environments - [examples/development.md](examples/development.md) - Local overrides, bootstrapping, onFeatureFlags callback - [reference.md](reference.md) - Decision frameworks and anti-patterns --- <philosophy> ## Philosophy Feature flags decouple deployment from release. You can ship code to production but control who sees it and when. This enables: 1. **Safe releases** - Roll out to 1% first, monitor, then expand 2. **Fast rollback** - Toggle off instantly without deploying 3. **Data-driven decisions** - A/B test to measure impact 4. **Progressive delivery** - Beta users first, then everyone **Core principles:** - Flags are temporary - plan for cleanup from day one - Flags have owners - someone is responsible for each flag - Simple flags are better - percentage rollouts over complex conditions - Handle undefined - flags load asynchronously **When to use feature flags:** - Risky features that need gradual rollout - Features requiring A/B testing for validation - Features that may need instant rollback - Beta programs with user opt-in **When NOT to use feature flags:** - Every feature (creates maintenance burden) - Permanent configuration (use config files) - Features that are ready for 100% release </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Client-Side Boolean Flags Use `useFeatureFlagEnabled` for simple on/off features. Always handle the `undefined` loading state -- treating it as `false` causes a flash of wrong UI. ```typescript const isNewCheckout = useFeatureFlagEnabled(FLAG_NEW_CHECKOUT); if (isNewCheckout === undefined) return <Skeleton />; // Loading if (isNewCheckout) return <NewCheckout />; // Enabled return <LegacyCheckout />; // Disabled ``` Store flag keys as named constants in `lib/feature-flags.ts` to prevent typos and enable cleanup-by-grep. See [examples/core.md](examples/core.md#pattern-1-client-side-boolean-flags) for full good/bad examples. --- ### Pattern 2: Multivariate Flags and Variants Use `useFeatureFlagVariantKey` for A/B tests with multiple variants. Define variant constants alongside the flag key. Switch on variants with a default fallback to `control`. ```typescript const variant = useFeatureFlagVariantKey(FLAG_PRICING_PAGE); if (variant === undefined) return <Skeleton />; switch (variant) { case VARIANT_SIMPLE: return <SimplePricing />; case VARIANT_DETAILED: return <DetailedPricing />; default: return <ControlPricing />; } ``` See [examples/core.md](examples/core.md#pattern-2-multivariate-flags-and-variants) for full example. --- ### Pattern 3: PostHogFeature Component The `PostHogFeature` component provides automatic exposure tracking and built-in fallback handling with less boilerplate. Use `match={true}` for boolean flags or `match={VARIANT_KEY}` for specific variants. ```typescript <PostHogFeature flag={FLAG_BETA} match={true} fallback={<Legacy />}> <NewFeature /> </PostHogFeature> ``` See [examples/core.md](examples/core.md#pattern-3-posthogfeature-component) for boolean and variant examples. --- ### Pattern 4: Payloads for Remote Configuration Use `useFeatureFlagPayload` for dynamic JSON configuration. **Always pair with `useFeatureFlagEnabled`** -- the payload hook alone does NOT send exposure events, breaking experiment tracking. ```typescript const isEnabled = useFeatureFlagEnabled(FLAG_BANNER); // Sends exposure event const payload = useFeatureFlagPayload(FLAG_BANNER); // Gets config const config = payload ?? DEFAULT_BANNER_CONFIG; ``` See [examples/core.md](examples/core.md#pattern-4-feature-flag-payloads-for-remote-configuration) for full good/bad examples. --- ### Pattern 5: Server-Side Flag Evaluation Use `posthog-node` with the Feature Flags Secure API Key (`phs_*`) for local evaluation. This reduces latency from ~500ms (network call) to ~10-50ms (local). The `personalApiKey` config option takes the `phs_*` key despite its legacy name. ```typescript export const posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: process.env.POSTHOG_HOST || "https://us.i.posthog.com", personalApiKey: process.env.POSTHOG_FEATURE_FLAGS_KEY, // phs_* key featureFlagsPollingInterval: POSTHOG_POLL_INTERVAL_MS, // default 30s }); ``` See [examples/server-side.md](examples/server-side.md) for API handler usage, local-only evaluation, and distributed/serverless environments. --- ### Pattern 6: Flag Lifecycle and Cleanup Every flag needs an owner, a creation date, and an expected removal date. Wrap flag checks in a single helper function so cleanup is a one-file change. ```typescript /** * Owner: @john-doe | Created: 2025-01-15 | Remove by: 2025-02-15 */ export const FLAG_NEW_CHECKOUT = "new-checkout-flow"; export function isNewCheckoutEnabled(flag: boolean | undefined): boolean { return flag === true; // When removing: change to `return true;` } ``` See [examples/core.md](examples/core.md#pattern-7-flag-cleanup-and-lifecycle-management) for full documentation patterns and stale flag detection. </patterns> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using `useFeatureFlagPayload` alone for experiments (no exposure tracking) - Exposing Feature Flags Secure API key (`phs_*`) on client (security violation) - No loading state handling (causes UI flash) - Flags without owners or expiry dates (becomes permanent debt) **Medium Priority Issues:** - Magic string flag keys instead of constants (typos, hard to grep) - Complex targeting rules on high-traffic flags (performance hit) - Local evaluation in serverless/edge without external cache (cold start issues) - Not using PostHog toolbar for local testing (harder debugging) **Common Mistakes:** - Checking flag in multiple places instead of wrapper function - Not bootstrapping flags for SSR (content flash on hydration) - Running experiments without defined primary metric - Peeking at experiment results before completion - Rolling out to 100% without cleanup plan **Gotchas & Edge Cases:** - PostHog uses deterministic hashing - same user always gets same variant - Decreasing rollout percentage can remove users who were previously included - Local evaluation requires Feature Flags Secure API Key (`phs_*`) - personal API keys are deprecated - Flags load asynchronously - first render always has undefined - GeoIP targeting uses server IP by default in posthog-node v3+ - Experiments need minimum 50 exposures per variant for results - Stale flag = 100% rollout + not evaluated in 30 days - `onFeatureFlags` callback receives three parameters: `flags`, `flagVariants`, `{ errorsLoading }` (third parameter) - External cache providers (Redis, KV) are experimental - Node.js/Python SDKs only </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST always pair `useFeatureFlagPayload` with `useFeatureFlagEnabled` or `useFeatureFlagVariantKey` for experiments - payload hooks don't send exposure events)** **(You MUST use the feature flags secure API key (phs\_\*) for server-side local evaluation - personal API keys are deprecated for this use)** **(You MUST handle the `undefined` state when flags are loading - never assume a flag is immediately available)** **(You MUST include flag owner and expiry date in flag metadata - flags without owners become orphaned debt)** **(You MUST wrap flag usage in a single function when used in multiple places - prevents orphaned flag code on cleanup)** **Failure to follow these rules will cause incorrect experiment results, security vulnerabilities, UI flashing, and technical debt.** </critical_reminders> --- ## Sources - [PostHog React Integration](https://posthog.com/docs/libraries/react) - [PostHog Feature Flags](https://posthog.com/docs/feature-flags) - [PostHog Feature Flag Best Practices](https://posthog.com/docs/feature-flags/best-practices) - [PostHog Server-Side Local Evaluation](https://posthog.com/docs/feature-flags/local-evaluation) - [PostHog Creating Feature Flags](https://posthog.com/docs/feature-flags/creating-feature-flags) - [PostHog How to Do a Phased Rollout](https://posthog.com/tutorials/phased-rollout) - [PostHog Feature Flag Testing](https://posthog.com/docs/feature-flags/testing) - [PostHog Experiments](https://posthog.com/ab-testing) - [PostHog Feature Flag Overrides](https://posthog.com/docs/toolbar/override-feature-flags) - [Don't Make These Feature Flag Mistakes](https://posthog.com/newsletter/feature-flag-mistakes)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.