AthenodeAthenode

Back to Product Ops (Stripe, Sentry, PostHog)

instrument-feature-flags

Created here

Add PostHog feature flags to gate new functionality. Use after implementing features or reviewing PRs to ensure safe rollouts with feature flag controls. Also handles initial PostHog SDK setup if not yet installed.

SKILL.md

Add PostHog feature flags

Use this skill to add PostHog feature flags that gate new or changed functionality. Use it after implementing features or reviewing PRs to ensure safe rollouts with feature flag controls. If PostHog is not yet installed, this skill also covers initial SDK setup. Supports any platform or language.

Supported platforms: React, Next.js, React Native, Web (JavaScript), Node.js, Python, PHP, Ruby, Go, Java, Rust, .NET, Elixir, Android, iOS, Flutter, and the REST API.

Instructions

Follow these steps IN ORDER:

STEP 1: Analyze the codebase and detect the platform.

Look for dependency files (package.json, pubspec.yaml, Podfile, Package.swift, requirements.txt, go.mod, Gemfile, composer.json, mix.exs, etc.) to determine the language and framework.

Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb, go.sum, pubspec.lock, Podfile.lock, Package.resolved, mix.lock) to determine the package manager.

  • Check for existing PostHog setup (SDK initialization, env vars, etc.). If PostHog is already installed and initialized, skip to STEP 3.

STEP 2: Research instrumentation. (Skip if PostHog is already set up.) 2.1. Find the reference file below that matches the detected platform — it is the source of truth for SDK initialization, flag evaluation methods, and framework-specific patterns. Read it now. 2.2. If no reference matches, fall back to your general knowledge and web search. Use posthog.com/docs as the primary search source.

STEP 3: Create or find the feature flag.

  • Check if a PostHog MCP server is connected. If available, use its tools to search for an existing feature flag the user wants to instrument, or create a new one.
  • If no MCP server is available, instruct the user to create the flag in the PostHog dashboard.

STEP 4: Plan release conditions.

  • Determine the rollout strategy (percentage rollout, user targeting, group targeting, etc.).
  • Plan how the feature flag will gate the new functionality in code.

STEP 5: Instrument the feature.

  • Add the feature flag code following the platform-specific reference patterns.
  • Use server-side evaluation when possible to avoid UI flicker.
  • Do not alter the fundamental architecture of existing files. Make additions minimal and targeted.
  • You must read a file immediately before attempting to write it.

STEP 6: Set up environment variables.

  • Check if the project already has PostHog environment variables configured (e.g. in .env, .env.local, or framework-specific env files). If valid values already exist, skip this step.
  • If the PostHog project token is missing, use the PostHog MCP server's projects-get tool to retrieve the project's api_token. If multiple projects are returned, ask the user which project to use. If the MCP server is not connected or not authenticated, ask the user for their PostHog project token instead.
  • For the PostHog host URL: check the projects-get MCP response for a region field — US maps to https://us.i.posthog.com, EU maps to https://eu.i.posthog.com. If the region is not available from the MCP response or from existing project configuration, ask the user: "Are you on PostHog US Cloud or EU Cloud?" Do not assume US Cloud.
  • Write these values to the appropriate env file using the framework's naming convention.
  • Reference these environment variables in code instead of hardcoding them.

Reference files

  • references/react.md - React feature flags installation
  • references/react-native.md - React native feature flags installation
  • references/web.md - Web feature flags installation
  • references/nodejs.md - Node.js feature flags installation
  • references/python.md - Python feature flags installation
  • references/django.md - Django
  • references/flask.md - Flask
  • references/php.md - Php feature flags installation
  • references/laravel.md - Laravel
  • references/ruby.md - Ruby feature flags installation
  • references/ruby-on-rails.md - Ruby on rails
  • references/go.md - Go feature flags installation
  • references/java.md - Java feature flags installation
  • references/rust.md - Rust feature flags installation
  • references/dotnet.md - .net feature flags installation
  • references/dotnet.md - .net
  • references/elixir.md - Elixir feature flags installation
  • references/android.md - Android feature flags installation
  • references/ios.md - Ios feature flags installation
  • references/usage.md - Ios SDK usage
  • references/flutter.md - Flutter feature flags installation
  • references/api.md - API feature flags installation
  • references/next-js.md - Next.js
  • references/adding-feature-flag-code.md - Adding feature flag code
  • references/best-practices.md - Best practices for production-ready flags
  • references/COMMANDMENTS.md - Framework-specific rules the integration must follow

Each platform reference contains SDK-specific installation, flag evaluation, and code examples. Find the one matching the user's stack. If unlisted, use the API reference as a fallback.

Key principles

  • Environment variables: Always use environment variables for PostHog keys. Never hardcode them.
  • Minimal changes: Add feature flag code alongside existing logic. Don't replace or restructure existing code.
  • Boolean flags first: Default to boolean flag checks unless the user specifically asks for multivariate flags.
  • Server-side when possible: Prefer server-side flag evaluation to avoid UI flicker.

SKILL.md

SKILL.md holds the skill's instructions; it is edited on the Instructions tab.

references/COMMANDMENTS.md

Framework rules

Follow these when integrating PostHog into this framework.

  • A missing PostHog configuration must never break the app — read keys optionally (never a required setting), guard init and capture behind their presence, and keep build and boot working with no PostHog environment set — but never silently: in development or debug builds fail loudly, using the language's idiomatic error, with the message "<VAR> variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once <VAR> is configured" (substituting the actual variable name); production stays a no-op

references/adding-feature-flag-code.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Adding feature flag code

Once you've created your feature flag in PostHog, the next step is to add your code:

Web

Boolean feature flags

Web

const result = posthog.getFeatureFlagResult('flag-key')
if (result?.enabled) {
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    const matchedFlagPayload = result?.payload
}
Multivariate feature flags

Web

const result = posthog.getFeatureFlagResult('flag-key')
if (result?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    const matchedFlagPayload = result?.payload
}
Inspecting all feature flags

You can inspect all currently loaded feature flags with getAllFeatureFlags(). It returns each flag's key, enabled state, variant, and payload, and does not send a $feature_flag_called event, so calling it won't affect your experiment results or flag usage analytics:

Web

for (const flag of posthog.getAllFeatureFlags()) {
    console.log(flag.key, flag.enabled, flag.variant, flag.payload)
}
Ensuring flags are loaded before usage

Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default).

This means that for most pages, the feature flags are available immediately — except for the first time a user visits.

To handle this, you can use the onFeatureFlags callback to wait for the feature flag request to finish:

Web

posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) {
    // feature flags are guaranteed to be available at this point
    if (posthog.isFeatureEnabled('flag-key')) {
        // do something
    }
})
Callback parameters

The onFeatureFlags callback receives the following parameters:

  • flags: string[]: An object containing the feature flags that apply to the user.

  • flagVariants: Record<string, string | boolean>: An object containing the variants that apply to the user.

  • { errorsLoading }: { errorsLoading?: boolean }: An object containing a boolean indicating if an error occurred during the request to load the feature flags. This is true if the request timed out or if there was an error. It will be false or undefined if the request was successful.

You won't usually need to use these, but they are useful if you want to be extra careful about feature flags not being loaded yet because of a network error and/or a network timeout (see feature_flag_request_timeout_ms).

Evaluating only specific flags

By default, the JavaScript SDK requests that every eligible feature flag be evaluated for the current user. If you'd only like to evaluate and return a subset of flags, pass flag_keys when initializing PostHog:

Web

posthog.init('<ph_project_token>', {
  api_host: 'https://us.i.posthog.com',
  defaults: '2026-05-30',
  flag_keys: ['checkout-flow', 'new-dashboard'],
})

PostHog scopes evaluation and the response to those keys for this SDK instance. Dependency flags required to evaluate requested flags may also be evaluated and returned. Leave flag_keys unset to evaluate all eligible flags.

Reloading feature flags

Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call:

Web

posthog.reloadFeatureFlags()
Overriding server properties

Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls:

Web

posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'})

Note: These are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation.

Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading:

Web

posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false)

At any point, you can reset these properties by calling resetPersonPropertiesForFlags:

Web

posthog.resetPersonPropertiesForFlags()

The same holds for group (/manual/group-analytics.md) properties:

Web

// set properties for a group
posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}})

// reset properties for a given group:
posthog.resetGroupPropertiesForFlags('company')

// reset properties for all groups:
posthog.resetGroupPropertiesForFlags()

Note: You don't need to add the group names here, since these properties are automatically attached to the current group (set via posthog.group()). When you change the group, these properties are reset.

Automatic overrides

Whenever you call posthog.identify with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call posthog.group().

Default overridden properties

By default, we always override some properties based on the user IP address.

The list of properties that this overrides:

  1. $geoip_city_name
  2. $geoip_country_name
  3. $geoip_country_code
  4. $geoip_continent_name
  5. $geoip_continent_code
  6. $geoip_postal_code
  7. $geoip_time_zone

This enables any geolocation-based flags to work without manually setting these properties.

Request timeout

You can configure the feature_flag_request_timeout_ms parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds.

JavaScript

posthog.init('<ph_project_token>', {
  api_host: 'https://us.i.posthog.com',
  defaults: '2026-05-30',
  feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds).
})
Feature flag error handling

When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler:

JavaScript

function handleFeatureFlag(client, flagKey, distinctId) {
  try {
    const isEnabled = client.isFeatureEnabled(flagKey, distinctId);
    console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`);
    return isEnabled;
  } catch (error) {
    console.error(`Error fetching feature flag '${flagKey}': ${error.message}`);
    // Optionally, you can return a default value or throw the error
    // return false; // Default to disabled
    throw error;
  }
}

// Usage example
try {
  const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123');
  if (flagEnabled) {
    // Implement new feature logic
  } else {
    // Implement old feature logic
  }
} catch (error) {
  // Handle the error at a higher level
  console.error('Feature flag check failed, using default behavior');
  // Implement fallback logic
}

React

There are two ways to implement feature flags in React:

  1. Using hooks.
  2. Using the <PostHogFeature> component.
Method 1: Using hooks

PostHog provides several hooks to make it easy to use feature flags in your React app.

Hook Description
useFeatureFlagEnabled Returns whether the feature flag is enabled. This sends a $feature_flag_called event. Without a default value, it returns boolean | undefined while flags are loading or absent. Pass an optional default value to return that value instead and narrow the return type to boolean.
useFeatureFlagVariantKey Returns the variant key of the feature flag. This sends a $feature_flag_called event.
useActiveFeatureFlags Returns an array of active feature flags. This does not send a $feature_flag_called event.
useFeatureFlagPayload Returns the payload of the feature flag. This does not send a $feature_flag_called event. Always use this with useFeatureFlagEnabled or useFeatureFlagVariantKey.
Example 1: Using a boolean feature flag

React

import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react'

function App() {
  const showWelcomeMessage = useFeatureFlagEnabled('flag-key')
  const payload = useFeatureFlagPayload('flag-key')

  return (
    <div className="App">
      {
        showWelcomeMessage ? (
          <div>
            <h1>Welcome!</h1>
            <p>Thanks for trying out our feature flags.</p>
          </div>
        ) : (
          <div>
            <h2>No welcome message</h2>
            <p>Because the feature flag evaluated to false.</p>
          </div>
        )
      }
    </div>
  );
}

export default App;

To avoid handling undefined while flags are loading, pass a default value as the second argument:

React

const showWelcomeMessage = useFeatureFlagEnabled('flag-key', false)
Example 2: Using a multivariate feature flag

React

import { useFeatureFlagVariantKey } from '@posthog/react'

function App() {
  const variantKey = useFeatureFlagVariantKey('show-welcome-message')
  let welcomeMessage = ''
  if (variantKey === 'variant-a') {
    welcomeMessage = 'Welcome to the Alpha!'
  } else if (variantKey === 'variant-b') {
    welcomeMessage = 'Welcome to the Beta!'
  }

  return (
    <div className="App">
      {
        welcomeMessage ? (
          <div>
            <h1>{welcomeMessage}</h1>
            <p>Thanks for trying out our feature flags.</p>
          </div>
        ) : (
          <div>
            <h2>No welcome message</h2>
            <p>Because the feature flag evaluated to false.</p>
          </div>
        )
      }
    </div>
  );
}

export default App;
Example 3: Using a flag payload

Payload hook

The useFeatureFlagPayload hook does not send a $feature_flag_called event, which is required for the experiment to be tracked. To ensure the exposure event is sent, you should always use the useFeatureFlagPayload hook with either the useFeatureFlagEnabled or useFeatureFlagVariantKey hook.

React

import { useFeatureFlagEnabled, useFeatureFlagPayload } from '@posthog/react'

function App() {
  const variant = useFeatureFlagEnabled('show-welcome-message')
  const payload = useFeatureFlagPayload('show-welcome-message')

    return (
                <>
                {
                    variant ? (
                        <div className="welcome-message">
                            <h2>{payload?.welcomeTitle}</h2>
                            <p>{payload?.welcomeMessage}</p>
                        </div>
                    ) : <div>
                        <h2>No custom welcome message</h2>
                        <p>Because the feature flag evaluated to false.</p>
                    </div>
                }
        </>
    )
}
Method 2: Using the PostHogFeature component

The PostHogFeature component simplifies code by handling feature flag related logic.

It also automatically captures metrics, like how many times a user interacts with this feature.

Note: You still need the PostHogProvider (/docs/libraries/react.md#installation) at the top level for this to work.

Here is an example:

React

import { PostHogFeature } from '@posthog/react'

function App() {

    return (
        <PostHogFeature flag='show-welcome-message' match={true}>
            <div>
                <h1>Hello</h1>
                <p>Thanks for trying out our feature flags.</p>
            </div>
        </PostHogFeature>
    )
}
  • The match on the component can be either true, or the variant key, to match on a specific variant.

  • If you also want to show a default message, you can pass these in the fallback attribute.

If you wish to customise logic around when the component is considered visible, you can pass in visibilityObserverOptions to the feature. These take the same options as the IntersectionObserver API. By default, we use a threshold of 0.1.

Payloads

If your flag has a payload, you can pass a function to children whose first argument is the payload. For example:

React

import { PostHogFeature } from '@posthog/react'

function App() {

    return (
        <PostHogFeature flag='show-welcome-message' match={true}>
           {(payload) => {
                return (
                    <div>
                        <h1>{payload.welcomeMessage}</h1>
                        <p>Thanks for trying out our feature flags.</p>
                    </div>
                )
           }}
        </PostHogFeature>
    )
}
Request timeout

You can configure the feature_flag_request_timeout_ms parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds.

JavaScript

posthog.init('<ph_project_token>', {
  api_host: 'https://us.i.posthog.com',
  defaults: '2026-05-30',
  feature_flag_request_timeout_ms: 3000 // Time in milliseconds. Default is 3000 (3 seconds).
}
)
Error handling

When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler:

JavaScript

function handleFeatureFlag(client, flagKey, distinctId) {
    try {
        const isEnabled = client.isFeatureEnabled(flagKey, distinctId);
        console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`);
        return isEnabled;
    } catch (error) {
        console.error(`Error fetching feature flag '${flagKey}': ${error.message}`);
        // Optionally, you can return a default value or throw the error
        // return false; // Default to disabled
        throw error;
    }
}

// Usage example
try {
    const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123');
    if (flagEnabled) {
        // Implement new feature logic
    } else {
        // Implement old feature logic
    }
} catch (error) {
    // Handle the error at a higher level
    console.error('Feature flag check failed, using default behavior');
    // Implement fallback logic
}

Node.js

There are two steps to implement feature flags in Node:

Step 1: Evaluate flags once

Call client.evaluateFlags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Node.js

const flags = await client.evaluateFlags('distinct_id_of_your_user')

if (flags.isEnabled('flag-key')) {
    // Do something differently for this user
    // Optional: fetch the payload
    const matchedFlagPayload = flags.getFlagPayload('flag-key')
}
Multivariate feature flags

Node.js

const flags = await client.evaluateFlags('distinct_id_of_your_user')
const enabledVariant = flags.getFlag('flag-key')

if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant
    // Do something differently for this user
    // Optional: fetch the payload
    const matchedFlagPayload = flags.getFlagPayload('flag-key')
}

flags.getFlag() returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and undefined when the flag wasn't returned by the evaluation.

Note: client.isFeatureEnabled(), client.getFeatureFlag(), client.getFeatureFlagPayload(), and capture({ sendFeatureFlags: true }) still work during the migration period, but they're deprecated. Prefer evaluateFlags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Node.js

const flags = await client.evaluateFlags('distinct_id_of_your_user')

if (flags.isEnabled('flag-key')) {
    // Do something differently for this user
}

client.capture({
    distinctId: 'distinct_id_of_your_user',
    event: 'event_name',
    flags,
})

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Node.js

// Attach only flags accessed with isEnabled() or getFlag() before this call
client.capture({
    distinctId: 'distinct_id_of_your_user',
    event: 'event_name',
    flags: flags.onlyAccessed(),
})

// Attach only specific flags
client.capture({
    distinctId: 'distinct_id_of_your_user',
    event: 'event_name',
    flags: flags.only(['checkout-flow', 'new-dashboard']),
})

onlyAccessed() is order-dependent. If you call it before accessing any flags with isEnabled() or getFlag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Node.js

client.capture({
    distinctId: 'distinct_id_of_your_user',
    event: 'event_name',
    properties: {
        // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant
        '$feature/feature-flag-key': 'variant-key',
    },
})
Evaluating only specific flags

By default, evaluateFlags() evaluates every flag for the user. If you only need a few flags, pass flagKeys to request only those flags:

Node.js

const flags = await client.evaluateFlags('distinct_id_of_your_user', {
    flagKeys: ['checkout-flow', 'new-dashboard'],
})
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluateFlags(), the SDK sends this event when you call flags.isEnabled() or flags.getFlag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.getFlagPayload() doesn't send $feature_flag_called events and doesn't count as an access for onlyAccessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

Node.js

const flags = await client.evaluateFlags('distinct_id_of_the_user', {
    personProperties: {
        property_name: 'value',
    },
    groups: {
        your_group_type: 'your_group_id',
        another_group_type: 'your_group_id',
    },
    groupProperties: {
        your_group_type: {
            group_property_name: 'value',
        },
        another_group_type: {
            group_property_name: 'value',
        },
    },
})

if (flags.isEnabled('flag-key')) {
    // Do something differently for this user
}
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Request timeout

You can configure the featureFlagsRequestTimeoutMs parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds.

JavaScript

const client = new PostHog('<ph_project_token>', {
    host: 'https://us.i.posthog.com',
    featureFlagsRequestTimeoutMs: 3000, // Time in milliseconds. Defaults to 3000 (3 seconds).
})

Python

There are two steps to implement feature flags in Python:

Step 1: Evaluate flags once

Call posthog.evaluate_flags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Python

flags = posthog.evaluate_flags("distinct_id_of_your_user")

if flags.is_enabled("flag-key"):
    # Do something differently for this user
    # Optional: fetch the payload
    matched_flag_payload = flags.get_flag_payload("flag-key")
Multivariate feature flags

Python

flags = posthog.evaluate_flags("distinct_id_of_your_user")

enabled_variant = flags.get_flag("flag-key")

if enabled_variant == "variant-key":  # replace "variant-key" with the key of your variant
    # Do something differently for this user
    # Optional: fetch the payload
    matched_flag_payload = flags.get_flag_payload("flag-key")

flags.get_flag() returns the variant string for multivariate flags, True for enabled boolean flags, False for disabled flags, and None when the flag wasn't returned by the evaluation.

Note: posthog.feature_enabled(), posthog.get_feature_flag(), posthog.get_feature_flag_payload(), and posthog.capture(send_feature_flags=True) still work during the migration period, but they're deprecated. Prefer posthog.evaluate_flags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Python

flags = posthog.evaluate_flags("distinct_id_of_your_user")

if flags.is_enabled("flag-key"):
    # Do something differently for this user
    pass

posthog.capture(
    "event_name",
    distinct_id="distinct_id_of_your_user",
    flags=flags,
)

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Python

# Attach only flags accessed with is_enabled() or get_flag() before this call
posthog.capture(
    "event_name",
    distinct_id="distinct_id_of_your_user",
    flags=flags.only_accessed(),
)

# Attach only specific flags
posthog.capture(
    "event_name",
    distinct_id="distinct_id_of_your_user",
    flags=flags.only(["checkout-flow", "new-dashboard"]),
)

only_accessed() is order-dependent. If you call it before accessing any flags with is_enabled() or get_flag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Python

posthog.capture(
    "event_name",
    distinct_id="distinct_id_of_the_user",
    properties={
        # Replace feature-flag-key with your flag key and "variant-key" with the key of your variant
        "$feature/feature-flag-key": "variant-key",
    },
)
Evaluating only specific flags

By default, posthog.evaluate_flags() evaluates every flag for the user. If you only need a few flags, pass flag_keys to request only those flags:

Python

flags = posthog.evaluate_flags(
    "distinct_id_of_your_user",
    flag_keys=["checkout-flow", "new-dashboard"],
)
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With posthog.evaluate_flags(), the SDK sends this event when you call flags.is_enabled() or flags.get_flag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.get_flag_payload() doesn't send $feature_flag_called events and doesn't count as an access for only_accessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

Python

flags = posthog.evaluate_flags(
    "distinct_id_of_the_user",
    person_properties={"property_name": "value"},
    groups={
        "your_group_type": "your_group_id",
        "another_group_type": "your_group_id",
    },
    group_properties={
        "your_group_type": {"group_property_name": "value"},
        "another_group_type": {"group_property_name": "value"},
    },
)

if flags.is_enabled("flag-key"):
    # Do something differently for this user
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Request timeout

You can configure the feature_flags_request_timeout_seconds parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds.

Python

posthog = Posthog(
    "<ph_project_token>",
    host="https://us.i.posthog.com",
    feature_flags_request_timeout_seconds=3,  # Time in seconds. Defaults to 3.
)

PHP

There are two steps to implement feature flags in PHP:

Step 1: Evaluate flags once

Call PostHog::evaluateFlags() once for the user, then read values from the returned snapshot.

Boolean feature flags

PHP

$flags = PostHog::evaluateFlags('distinct_id_of_your_user');

if ($flags->isEnabled('flag-key')) {
    // Do something differently for this user
    // Optional: fetch the payload
    $matchedFlagPayload = $flags->getFlagPayload('flag-key');
}
Multivariate feature flags

PHP

$flags = PostHog::evaluateFlags('distinct_id_of_your_user');

$enabledVariant = $flags->getFlag('flag-key');

if ($enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant
    // Do something differently for this user
    // Optional: fetch the payload
    $matchedFlagPayload = $flags->getFlagPayload('flag-key');
}

$flags->getFlag() returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and null when the flag wasn't returned by the evaluation.

You can also call $flags->getKeys() to list the evaluated flag keys, or $flags->getEventProperties() to get the $feature/<flag-key> and $active_feature_flags properties that would be attached to a captured event.

Note: PostHog::isFeatureEnabled(), PostHog::getFeatureFlag(), PostHog::getFeatureFlagPayload(), and capture(['send_feature_flags' => true]) still work during the migration period, but they're deprecated. Prefer evaluateFlags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

PHP

$flags = PostHog::evaluateFlags('distinct_id_of_your_user');

if ($flags->isEnabled('flag-key')) {
    // Do something differently for this user
}

PostHog::capture([
    'distinctId' => 'distinct_id_of_your_user',
    'event' => 'event_name',
    'flags' => $flags,
]);

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

PHP

// Attach only flags accessed with isEnabled() or getFlag() before this call
PostHog::capture([
    'distinctId' => 'distinct_id_of_your_user',
    'event' => 'event_name',
    'flags' => $flags->onlyAccessed(),
]);

// Attach only specific flags
PostHog::capture([
    'distinctId' => 'distinct_id_of_your_user',
    'event' => 'event_name',
    'flags' => $flags->only(['checkout-flow', 'new-dashboard']),
]);

onlyAccessed() is order-dependent. If you call it before accessing any flags with isEnabled() or getFlag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

PHP

PostHog::capture([
    'distinctId' => 'distinct_id_of_your_user',
    'event' => 'event_name',
    'properties' => [
        // Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant
        '$feature/feature-flag-key' => 'variant-key',
    ],
]);
Evaluating only specific flags

By default, evaluateFlags() evaluates every flag for the user. If you only need a few flags, pass flagKeys to request only those flags:

PHP

$flags = PostHog::evaluateFlags(
    distinctId: 'distinct_id_of_your_user',
    flagKeys: ['checkout-flow', 'new-dashboard'],
);
Optional evaluation parameters

evaluateFlags() also accepts optional parameters for local evaluation and GeoIP behavior:

PHP

$flags = PostHog::evaluateFlags(
    distinctId: 'distinct_id_of_your_user',
    groups: ['company' => 'company_id_in_your_db'],
    personProperties: ['plan' => 'pro'],
    groupProperties: ['company' => ['employees' => 11]],
    onlyEvaluateLocally: false, // Defaults to false. Set to true to avoid a remote fallback.
    disableGeoip: false, // Defaults to false. Set to true to disable GeoIP enrichment during remote evaluation.
    flagKeys: ['checkout-flow', 'new-dashboard'],
);
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluateFlags(), the SDK sends this event when you call $flags->isEnabled() or $flags->getFlag() for a flag.

The SDK deduplicates these events per (flag key, distinct_id) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

$flags->getFlagPayload() doesn't send $feature_flag_called events and doesn't count as an access for onlyAccessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

PHP

$flags = PostHog::evaluateFlags(
    distinctId: 'distinct_id_of_the_user',
    groups: [
        'your_group_type' => 'your_group_id',
        'another_group_type' => 'your_group_id',
    ],
    personProperties: ['property_name' => 'value'],
    groupProperties: [
        'your_group_type' => ['group_property_name' => 'value'],
        'another_group_type' => ['group_property_name' => 'value'],
    ],
);

if ($flags->isEnabled('flag-key')) {
    // Do something differently for this user
}
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Request timeout

You can configure the feature_flag_request_timeout_ms parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds.

PHP

PostHog::init("<ph_project_token>",
    [
        'host' => 'https://us.i.posthog.com',
        'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 3000 (3 seconds).
    ]
);

Ruby

There are two steps to implement feature flags in Ruby:

Step 1: Evaluate flags once

Call posthog.evaluate_flags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Ruby

flags = posthog.evaluate_flags('distinct_id_of_your_user')

if flags.enabled?('flag-key')
    # Do something differently for this user
    # Optional: fetch the payload
    matched_flag_payload = flags.get_flag_payload('flag-key')
end
Multivariate feature flags

Ruby

flags = posthog.evaluate_flags('distinct_id_of_your_user')

enabled_variant = flags.get_flag('flag-key')

if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant
    # Do something differently for this user
    # Optional: fetch the payload
    matched_flag_payload = flags.get_flag_payload('flag-key')
end

flags.get_flag() returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and nil when the flag wasn't returned by the evaluation.

Note: posthog.is_feature_enabled(), posthog.get_feature_flag(), posthog.get_feature_flag_result(), posthog.get_feature_flag_payload(), and capture({ ..., send_feature_flags: true }) still work during the migration period, but they're deprecated. Prefer evaluate_flags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Ruby

flags = posthog.evaluate_flags('distinct_id_of_your_user')

if flags.enabled?('flag-key')
    # Do something differently for this user
end

posthog.capture({
    distinct_id: 'distinct_id_of_your_user',
    event: 'event_name',
    flags: flags,
})

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Ruby

# Attach only flags accessed with enabled?() or get_flag() before this call
posthog.capture({
    distinct_id: 'distinct_id_of_your_user',
    event: 'event_name',
    flags: flags.only_accessed,
})

# Attach only specific flags
posthog.capture({
    distinct_id: 'distinct_id_of_your_user',
    event: 'event_name',
    flags: flags.only(['checkout-flow', 'new-dashboard']),
})

only_accessed is order-dependent. If you call it before accessing any flags with enabled?() or get_flag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Ruby

posthog.capture({
    distinct_id: 'distinct_id_of_your_user',
    event: 'event_name',
    properties: {
        # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant
        '$feature/feature-flag-key': 'variant-key',
    },
})
Evaluating only specific flags

By default, evaluate_flags() evaluates every flag for the user. If you only need a few flags, pass flag_keys to request only those flags:

Ruby

flags = posthog.evaluate_flags(
    'distinct_id_of_your_user',
    flag_keys: ['checkout-flow', 'new-dashboard'],
)
Evaluating locally only

If you want to skip the remote /flags request and only use locally cached definitions, pass only_evaluate_locally: true:

Ruby

flags = posthog.evaluate_flags(
    'distinct_id_of_your_user',
    only_evaluate_locally: true,
)
Disabling GeoIP for flag evaluation

Pass disable_geoip: true to disable GeoIP lookup for remote flag evaluation:

Ruby

flags = posthog.evaluate_flags(
    'distinct_id_of_your_user',
    disable_geoip: true,
)
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluate_flags(), the SDK sends this event when you call flags.enabled?() or flags.get_flag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.get_flag_payload() doesn't send $feature_flag_called events and doesn't count as an access for only_accessed.

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

Ruby

flags = posthog.evaluate_flags(
    'distinct_id_of_the_user',
    person_properties: {
        property_name: 'value'
    },
    groups: {
        your_group_type: 'your_group_id',
        another_group_type: 'your_group_id',
    },
    group_properties: {
        your_group_type: {
            group_property_name: 'value'
        },
        another_group_type: {
            group_property_name: 'value'
        },
    },
)

if flags.enabled?('flag-key')
    # Do something differently for this user
end
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Request timeout

You can configure the feature_flag_request_timeout_seconds parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds.

Ruby

posthog = PostHog::Client.new({
    # rest of your configuration...
    feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3.
})

Go

There are two steps to implement feature flags in Go:

Step 1: Evaluate flags once

Call client.EvaluateFlags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Go

flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{
    DistinctId: "distinct_id_of_your_user",
})
if err != nil {
    // Handle error (e.g. capture error and fallback to default behavior)
}

if flags.IsEnabled("flag-key") {
    // Do something differently for this user
    // Optional: fetch the payload
    matchedFlagPayload := flags.GetFlagPayload("flag-key")
}
Multivariate feature flags

Go

flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{
    DistinctId: "distinct_id_of_your_user",
})
if err != nil {
    // Handle error (e.g. capture error and fallback to default behavior)
}

enabledVariant := flags.GetFlag("flag-key")

if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant
    // Do something differently for this user
    // Optional: fetch the payload
    matchedFlagPayload := flags.GetFlagPayload("flag-key")
}

flags.GetFlag() returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and nil when the flag wasn't returned by the evaluation.

Note: client.IsFeatureEnabled(), client.GetFeatureFlag(), client.GetFeatureFlagPayload(), and Capture.SendFeatureFlags still work during the migration period, but they're deprecated. Prefer EvaluateFlags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to Capture

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Go

flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{
    DistinctId: "distinct_id_of_your_user",
})
if err != nil {
    // Handle error
}

if flags.IsEnabled("flag-key") {
    // Do something differently for this user
}

client.Enqueue(posthog.Capture{
    DistinctId: "distinct_id_of_your_user",
    Event:      "event_name",
    Flags:      flags,
})

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Go

// Attach only flags accessed with IsEnabled() or GetFlag() before this call
client.Enqueue(posthog.Capture{
    DistinctId: "distinct_id_of_your_user",
    Event:      "event_name",
    Flags:      flags.OnlyAccessed(),
})

// Attach only specific flags
client.Enqueue(posthog.Capture{
    DistinctId: "distinct_id_of_your_user",
    Event:      "event_name",
    Flags:      flags.Only([]string{"checkout-flow", "new-dashboard"}),
})

OnlyAccessed() is order-dependent. If you call it before accessing any flags with IsEnabled() or GetFlag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Go

client.Enqueue(posthog.Capture{
    DistinctId: "distinct_id_of_your_user",
    Event:      "event_name",
    Properties: posthog.NewProperties().
        Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant
})
Evaluating only specific flags

By default, EvaluateFlags() evaluates every flag for the user. If you only need a few flags, pass FlagKeys to request only those flags:

Go

flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{
    DistinctId: "distinct_id_of_your_user",
    FlagKeys:   []string{"checkout-flow", "new-dashboard"},
})
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With EvaluateFlags(), the SDK sends this event when you call flags.IsEnabled() or flags.GetFlag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.GetFlagPayload() doesn't send $feature_flag_called events and doesn't count as an access for OnlyAccessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

Go

flags, err := client.EvaluateFlags(posthog.EvaluateFlagsPayload{
    DistinctId: "distinct_id_of_the_user",
    Groups: posthog.NewGroups().
        Set("your_group_type", "your_group_id").
        Set("another_group_type", "your_group_id"),
    PersonProperties: posthog.NewProperties().
        Set("property_name", "value"),
    GroupProperties: map[string]posthog.Properties{
        "your_group_type": posthog.NewProperties().
            Set("group_property_name", "value"),
        "another_group_type": posthog.NewProperties().
            Set("group_property_name", "value"),
    },
})
if err != nil {
    // Handle error
}

if flags.IsEnabled("flag-key") {
    // Do something differently for this user
}
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Request timeout

You can configure the FeatureFlagRequestTimeout parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked if PostHog's servers are too slow to respond. By default, this is set to 3 seconds.

Go

// import "time"

client, _ := posthog.NewWithConfig(
    os.Getenv("<ph_project_token>"),
    posthog.Config{
        PersonalApiKey:            "your personal API key", // Optional, but much more performant. If this token is not supplied, then fetching feature flag values will be slower.
        Endpoint:                  "https://us.i.posthog.com",
        FeatureFlagRequestTimeout: 3 * time.Second, // Defaults to 3 seconds.
    },
)

React Native

There are two ways to implement feature flags in React Native:

  1. Using hooks.
  2. Loading the flag directly.
Method 1: Using hooks
Example 1: Boolean feature flags

React Native

import { useFeatureFlag } from 'posthog-react-native'

const MyComponent = () => {
    const booleanFlag = useFeatureFlag('key-for-your-boolean-flag')

    if (booleanFlag === undefined) {
        // the response is undefined if the flags are being loaded
        return null
    }

    // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload

    return booleanFlag ? <Text>Testing feature 😄</Text> : <Text>Not Testing feature 😢</Text>
}
Example 2: Multivariate feature flags

React Native

import { useFeatureFlag } from 'posthog-react-native'

const MyComponent = () => {
    const multiVariantFeature = useFeatureFlag('key-for-your-multivariate-flag')

    if (multiVariantFeature === undefined) {
        // the response is undefined if the flags are being loaded
        return null
    } else if (multiVariantFeature === 'variant-name') { // replace 'variant-name' with the name of your variant
      // Do something
    }

    // Optional use the 'useFeatureFlagWithPayload' hook for fetching the feature flag payload

    return <div/>
}
Method 2: Loading the flag directly

React Native

// Defaults to undefined if not loaded yet or if there was a problem loading
posthog.isFeatureEnabled('key-for-your-boolean-flag')

// Defaults to undefined if not loaded yet or if there was a problem loading
posthog.getFeatureFlag('key-for-your-boolean-flag')

// Multivariant feature flags are returned as a string
posthog.getFeatureFlag('key-for-your-multivariate-flag')

// Optional: fetch the payload (returns 'JsonType' or undefined if not loaded yet or if there was a problem loading)
posthog.getFeatureFlagResult('key-for-your-multivariate-flag')?.payload
Inspecting all feature flags

You can inspect all currently loaded feature flags with getAllFeatureFlags(). It returns each flag's key, enabled state, variant, and payload, and does not send a $feature_flag_called event, so calling it won't affect your experiment results or flag usage analytics:

React Native

for (const flag of posthog.getAllFeatureFlags()) {
    console.log(flag.key, flag.enabled, flag.variant, flag.payload)
}
Ensuring flags are loaded before usage

Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage.

This means that for most screens, the feature flags are available immediately — except for the first time a user visits.

To handle this, you can use the onFeatureFlags callback to wait for the feature flag request to finish:

React Native

posthog.onFeatureFlags((flags) => {
  // feature flags are guaranteed to be available at this point
  if (posthog.isFeatureEnabled('flag-key')) {
    // do something
  }
})
Reloading flags

PostHog loads feature flags when instantiated and refreshes whenever methods are called that affect the flag.

If want to manually trigger a refresh, you can call reloadFeatureFlagsAsync():

React Native

posthog.reloadFeatureFlagsAsync().then((refreshedFlags) => console.log(refreshedFlags))

Or when you want to trigger the reload, but don't care about the result:

React Native

posthog.reloadFeatureFlags()
Feature flag caching

The React Native SDK caches feature flag values in AsyncStorage. Cached values persist indefinitely with no TTL until updated by a successful API call. This enables offline support and reduces latency, but means inactive users may see stale flag values from their last session.

For example, if a user last opened your app when a flag was false, that value remains cached even after you roll it out to 100%. When they reopen the app, the SDK returns the cached false first, then fetches the fresh true value from the API.

To ensure fresh flag values:

React Native

// Force refresh on app start
await posthog.reloadFeatureFlagsAsync()

Or clear cached values for inactive users:

React Native

if (lastActiveDate < migrationDate) {
  posthog.reset() // Clears all cached data
}
Request timeout

You can configure the featureFlagsRequestTimeoutMs parameter when initializing your PostHog client to set a flag request timeout. This helps prevent your code from being blocked in the case when PostHog's servers are too slow to respond. By default, this is set at 10 seconds.

React Native

export const posthog = new PostHog('<ph_project_token>', {
  // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
  host: 'https://us.i.posthog.com',
  featureFlagsRequestTimeoutMs: 10000 // Time in milliseconds. Default is 10000 (10 seconds).
})
Error handling

When using the PostHog SDK, it's important to handle potential errors that may occur during feature flag operations. Here's an example of how to wrap PostHog SDK methods in an error handler:

React Native

function handleFeatureFlag(client, flagKey, distinctId) {
    try {
        const isEnabled = client.isFeatureEnabled(flagKey, distinctId);
        console.log(`Feature flag '${flagKey}' for user '${distinctId}' is ${isEnabled ? 'enabled' : 'disabled'}`);
        return isEnabled;
    } catch (error) {
        console.error(`Error fetching feature flag '${flagKey}': ${error.message}`);
        // Optionally, you can return a default value or throw the error
        // return false; // Default to disabled
        throw error;
    }
}

// Usage example
try {
    const flagEnabled = handleFeatureFlag(client, 'new-feature', 'user-123');
    if (flagEnabled) {
        // Implement new feature logic
    } else {
        // Implement old feature logic
    }
} catch (error) {
    // Handle the error at a higher level
    console.error('Feature flag check failed, using default behavior');
    // Implement fallback logic
}
Overriding server properties

Sometimes, you might want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can do so by setting properties the flag depends on with these calls:

React Native

posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'})

Note that these are set for the entire session. Successive calls are additive: all properties you set are combined together and sent for flag evaluation.

Whenever you set these properties, we also trigger a reload of feature flags to ensure we have the latest values. You can disable this by passing in the optional parameter for reloading:

React Native

posthog.setPersonPropertiesForFlags({'property1': 'value', property2: 'value2'}, false)

At any point, you can reset these properties by calling resetPersonPropertiesForFlags:

React Native

posthog.resetPersonPropertiesForFlags()

The same holds for group (/docs/product-analytics/group-analytics.md) properties:

React Native

// set properties for a group
posthog.setGroupPropertiesForFlags({'company': {'property1': 'value', property2: 'value2'}})

// reset properties for all groups:
posthog.resetGroupPropertiesForFlags()

Note: You don't need to add the group names here, since these properties are automatically attached to the current group (set via posthog.group()). When you change the group, these properties are reset.

Automatic overrides

Whenever you call posthog.identify with person properties, we automatically add these properties to flag evaluation calls to help determine the correct flag values. The same is true for when you call posthog.group().

Default overridden properties

By default, we always override some properties based on the user IP address.

The list of properties that this overrides:

  1. $geoip_city_name
  2. $geoip_country_name
  3. $geoip_country_code
  4. $geoip_continent_name
  5. $geoip_continent_code
  6. $geoip_postal_code
  7. $geoip_time_zone

This enables any geolocation-based flags to work without manually setting these properties.

Android

Boolean feature flags

Kotlin

import com.posthog.PostHog

val result = PostHog.getFeatureFlagResult("flag-key")
if (result?.enabled == true) {
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    val matchedFlagPayload = result.payload
}
Multivariate feature flags

Kotlin

import com.posthog.PostHog

val result = PostHog.getFeatureFlagResult("flag-key")
if (result?.variant == "variant-key") { // replace "variant-key" with the key of your variant
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    val matchedFlagPayload = result.payload
}
Inspecting all feature flags

You can inspect all currently loaded feature flags with PostHog.getAllFeatureFlags(). It returns each flag's key, enabled state, variant, and payload, and does not send a $feature_flag_called event, so calling it won't affect your experiment results or flag usage analytics:

Kotlin

import com.posthog.PostHog

PostHog.getAllFeatureFlags()?.forEach { flag ->
    println("${flag.key} ${flag.enabled} ${flag.variant} ${flag.payload}")
}
Ensuring flags are loaded before usage

Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage.

This means that for most screens, the feature flags are available immediately – except for the first time a user visits.

To handle this, you can use the onFeatureFlags callback to wait for the feature flag request to finish:

Kotlin

import com.posthog.PostHog
import com.posthog.android.PostHogAndroidConfig
import com.posthog.PostHogOnFeatureFlags

// During SDK initialization
val config = PostHogAndroidConfig(apiKey = "<ph_project_token>").apply {
    onFeatureFlags = PostHogOnFeatureFlags {
        if (PostHog.isFeatureEnabled("flag-key")) {
            // do something
        }
    }
}

// And/or after the SDK is initialized
PostHog.reloadFeatureFlags {
    if (PostHog.isFeatureEnabled("flag-key")) {
        // do something
    }
}
Reloading feature flags

Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call:

Kotlin

import com.posthog.PostHog

PostHog.reloadFeatureFlags()
Tracking feature usage

To track when someone sees or interacts with a feature, use captureFeatureView and captureFeatureInteraction.

Kotlin

import com.posthog.PostHog

PostHog.captureFeatureView("flag-key", flagVariant = "variant-key")
PostHog.captureFeatureInteraction("flag-key", flagVariant = "variant-key")

iOS

Boolean feature flags

Swift

if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled {
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    let matchedFlagPayload = result.payload
}
Multivariate feature flags

Swift

if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    let matchedFlagPayload = result.payload
}
Typed payloads

If your payload is a JSON object, you can decode it into a Decodable type:

Swift

struct FlagPayload: Decodable {
    let title: String
}

if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"),
   let payload = result.payloadAs(FlagPayload.self) {
    // Use payload.title
}
Inspecting all feature flags

You can inspect all currently loaded feature flags with getAllFeatureFlags(). It returns each flag's key, enabled state, variant, and payload, and does not send a $feature_flag_called event, so calling it won't affect your experiment results or flag usage analytics:

Swift

for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] {
    print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any)
}
Reloading feature flags

Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call:

Swift

PostHogSDK.shared.reloadFeatureFlags()
Ensuring flags are loaded before usage

Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage.

This means that for most screens, the feature flags are available immediately – except for the first time a user visits.

To handle this, you can use the didReceiveFeatureFlags notification to wait for the feature flag request to finish:

Swift

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // register for `didReceiveFeatureFlags` notification before SDK initialization
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(receiveFeatureFlags),
            name: PostHogSDK.didReceiveFeatureFlags,
            object: nil
        )

        let POSTHOG_PROJECT_TOKEN = "<ph_project_token>"
        // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
        let POSTHOG_HOST = "https://us.i.posthog.com"

        let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST)

        PostHogSDK.shared.setup(config)

        return true
    }

    // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server.
    @objc func receiveFeatureFlags() {
        print("receiveFeatureFlags called")
    }
}

Alternatively, you can use the completion block of the reloadFeatureFlags(_:) method. This allows you to execute logic immediately after the flags are reloaded:

Swift

// Reload feature flags and check if a specific feature is enabled
PostHogSDK.shared.reloadFeatureFlags {
    if PostHogSDK.shared.isFeatureEnabled("flag-key") {
        // do something
    }
}
Tracking feature usage

To track when someone sees or interacts with a feature, use captureFeatureView and captureFeatureInteraction.

Swift

PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key")
PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key")

Flutter

Boolean feature flags

Dart

final result = await Posthog().getFeatureFlagResult('flag-key');
if (result != null && result.enabled) {
  // Do something differently for this user

  // Optional: fetch the payload from the same evaluation result
  final matchedFlagPayload = result.payload;
}
Multivariate feature flags

Dart

final result = await Posthog().getFeatureFlagResult('flag-key');
if (result != null && result.variant == 'variant-key') { // replace 'variant-key' with the key of your variant
  // Do something differently for this user

  // Optional: fetch the payload from the same evaluation result
  final matchedFlagPayload = result.payload;
}
Ensuring flags are loaded before usage

To use the onFeatureFlags callback, you must set up the SDK manually (#installation). On Android and iOS, disable com.posthog.posthog.AUTO_INIT first.

Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage.

This means that for most screens, the feature flags are available immediately – except for the first time a user visits.

To handle this, you can use the onFeatureFlags callback in your config to be notified when flags are loaded:

Dart

final config = PostHogConfig('<ph_project_token>');
config.host = 'https://us.i.posthog.com';
config.onFeatureFlags = () async {
  if (await Posthog().isFeatureEnabled('flag-key')) {
    // do something
  }
};
await Posthog().setup(config);
Reloading feature flags

Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call:

Dart

await Posthog().reloadFeatureFlags();

Java

There are two steps to implement feature flags in Java:

Step 1: Evaluate flags once

Call posthog.evaluateFlags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Java

PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user");

if (flags.isEnabled("flag-key")) {
    // Do something differently for this user

    // Optional: fetch the payload
    String matchedFlagPayload = flags.getFlagPayload("flag-key");
}
Multivariate feature flags

Java

PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user");

Object flagValue = flags.getFlag("flag-key");
String enabledVariant = flagValue instanceof String ? (String) flagValue : null;

if ("variant-key".equals(enabledVariant)) { // replace "variant-key" with the key of your variant
    // Do something differently for this user

    // Optional: fetch the payload
    String matchedFlagPayload = flags.getFlagPayload("flag-key");
}

flags.getFlag() returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and null when the flag wasn't returned by the evaluation.

Note: posthog.isFeatureEnabled(), posthog.getFeatureFlag(), posthog.getFeatureFlagPayload(), and PostHogCaptureOptions.builder().appendFeatureFlags(true) still work during the migration period, but they're deprecated. Prefer evaluateFlags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Java

PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user");

if (flags.isEnabled("flag-key")) {
    // Do something differently for this user
}

posthog.capture(
    "distinct_id_of_your_user",
    "event_name",
    PostHogCaptureOptions.builder()
        .flags(flags)
        .build()
);

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Java

// Attach only flags accessed with isEnabled() or getFlag() before this call
posthog.capture(
    "distinct_id_of_your_user",
    "event_name",
    PostHogCaptureOptions.builder()
        .flags(flags.onlyAccessed())
        .build()
);

// Attach only specific flags
posthog.capture(
    "distinct_id_of_your_user",
    "event_name",
    PostHogCaptureOptions.builder()
        .flags(flags.only("checkout-flow", "new-dashboard"))
        .build()
);

onlyAccessed() is order-dependent. If you call it before accessing any flags with isEnabled() or getFlag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Java

posthog.capture(
    "distinct_id_of_your_user",
    "event_name",
    PostHogCaptureOptions.builder()
        .property("$feature/feature-flag-key", "variant-key") // replace feature-flag-key with your flag key. Replace "variant-key" with the key of your variant
        .build()
);
Evaluating only specific flags

By default, evaluateFlags() evaluates every flag for the user. If you only need a few flags, pass flagKeys to request only those flags:

Java

import java.util.Arrays;

PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags(
    "distinct_id_of_your_user",
    PostHogEvaluateFlagsOptions.builder()
        .flagKeys(Arrays.asList("checkout-flow", "new-dashboard"))
        .build()
);
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluateFlags(), the SDK sends this event when you call flags.isEnabled() or flags.getFlag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.getFlagPayload() doesn't send $feature_flag_called events and doesn't count as an access for onlyAccessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

Java

import com.posthog.server.PostHogEvaluateFlagsOptions;

PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags(
    "distinct_id_of_the_user",
    PostHogEvaluateFlagsOptions.builder()
        .group("your_group_type", "your_group_id")
        .group("another_group_type", "your_group_id")
        .groupProperty("your_group_type", "group_property_name", "value")
        .groupProperty("another_group_type", "group_property_name", "value")
        .personProperty("property_name", "value")
        .build()
);

if (flags.isEnabled("flag-key")) {
    // Do something differently for this user
}
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Rust

There are two steps to implement feature flags in Rust:

Step 1: Evaluate flags once

Call client.evaluate_flags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Rust

use posthog_rs::EvaluateFlagsOptions;

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).await.unwrap();

if flags.is_enabled("flag-key") {
    // Do something differently for this user
    // Optional: fetch the payload
    let matched_flag_payload = flags.get_flag_payload("flag-key");
}
Multivariate feature flags

Rust

use posthog_rs::{EvaluateFlagsOptions, FlagValue};

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).await.unwrap();

match flags.get_flag("flag-key") {
    Some(FlagValue::String(variant)) if variant == "variant-key" => {
        // Do something differently for this user
        // Optional: fetch the payload
        let matched_flag_payload = flags.get_flag_payload("flag-key");
    }
    _ => {}
}

flags.get_flag() returns Some(FlagValue::String(...)) for multivariate flags, Some(FlagValue::Boolean(true)) for enabled boolean flags, Some(FlagValue::Boolean(false)) for disabled flags, and None when the flag wasn't returned by the evaluation.

Note: client.is_feature_enabled(), client.get_feature_flag(), client.get_feature_flag_payload(), and client.get_feature_flags() still work during the migration period, but they're deprecated. Prefer evaluate_flags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to the event

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Rust

use posthog_rs::{EvaluateFlagsOptions, Event};

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).await.unwrap();

if flags.is_enabled("flag-key") {
    // Do something differently for this user
}

let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.with_flags(&flags);
client.capture(event);

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Rust

// Attach only flags accessed with is_enabled() or get_flag() before this call
let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.with_flags(&flags.only_accessed());
client.capture(event);

// Attach only specific flags
let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"]));
client.capture(event);

only_accessed() is order-dependent. If you call it before accessing any flags with is_enabled() or get_flag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Rust

use posthog_rs::Event;

let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap();
client.capture(event);
Evaluating only specific flags

By default, evaluate_flags() evaluates every flag for the user. If you only need a few flags, pass flag_keys to request only those flags:

Rust

use posthog_rs::EvaluateFlagsOptions;

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions {
        flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]),
        ..Default::default()
    },
).await.unwrap();
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluate_flags(), the SDK sends this event when you call flags.is_enabled() or flags.get_flag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.get_flag_payload() doesn't send $feature_flag_called events and doesn't count as an access for only_accessed().

Blocking client

If you're using the blocking client (with default-features = false), the API is the same but without .await:

Rust

use posthog_rs::EvaluateFlagsOptions;

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).unwrap();

if flags.is_enabled("flag-key") {
    // Do something differently for this user
}

Elixir

There are two steps to implement feature flags in Elixir:

Step 1: Evaluate flags once

Call PostHog.FeatureFlags.evaluate_flags/1 once for the user, then read values from the returned snapshot.

Boolean feature flags

Elixir

{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")

if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do
  # Do something differently for this user
  # Optional: fetch the payload
  payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key")
end
Multivariate feature flags

Elixir

{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")

enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key")

if enabled_variant == "variant-key" do
  # Do something differently for this user
  # Optional: fetch the payload
  payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key")
end

PostHog.FeatureFlags.Evaluations.get_flag/2 returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and nil when the flag wasn't returned by the evaluation.

Note: PostHog.FeatureFlags.check/2, PostHog.FeatureFlags.check!/2, PostHog.FeatureFlags.get_feature_flag_result/2, and PostHog.FeatureFlags.get_feature_flag_result!/2 still work during the migration period, but they're deprecated. Prefer evaluate_flags/1 for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Put the evaluated flags snapshot in context

Put the same snapshot object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don't make another /flags request.

Elixir

{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")

if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do
  # Do something differently for this user
end

PostHog.FeatureFlags.set_in_context(snapshot)
PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"})

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, put a filtered snapshot in context:

Elixir

{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")

# Attach only flags accessed with enabled?/2 or get_flag/2 before this call
PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key")
PostHog.FeatureFlags.set_in_context(
  PostHog.FeatureFlags.Evaluations.only_accessed(snapshot)
)

# Or attach only specific flags
PostHog.FeatureFlags.set_in_context(
  PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"])
)

only_accessed/1 is order-dependent. If you call it before accessing any flags with enabled?/2 or get_flag/2, no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Elixir

PostHog.capture("event_name", %{
  "$feature/feature-flag-key" => "variant-key",
  distinct_id: "distinct_id_of_your_user"
})
Evaluating only specific flags

By default, evaluate_flags/1 evaluates every flag for the user. If you only need a few flags, pass flag_keys to request only those flags:

Elixir

{:ok, snapshot} =
  PostHog.FeatureFlags.evaluate_flags(%{
    distinct_id: "distinct_id_of_your_user",
    flag_keys: ["checkout-flow", "new-dashboard"]
  })
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluate_flags/1, the SDK sends this event when you call PostHog.FeatureFlags.Evaluations.enabled?/2 or PostHog.FeatureFlags.Evaluations.get_flag/2 for a flag.

PostHog.FeatureFlags.Evaluations.get_flag_payload/2 doesn't send $feature_flag_called events.

.NET

There are two steps to implement feature flags in .NET:

Step 1: Evaluate flags once

Call EvaluateFlagsAsync() once for the user, then read values from the returned snapshot.

Boolean feature flags

C#

var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user");

if (flags.IsEnabled("flag-key"))
{
    // Do something differently for this user
    // Optional: fetch the payload
    var matchedPayload = flags.GetFlagPayload("flag-key");
}
Multivariate feature flags

C#

var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user");

var enabledVariant = flags.GetFlag("flag-key")?.VariantKey;

if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant
{
    // Do something differently for this user
    // Optional: fetch the payload
    var matchedPayload = flags.GetFlagPayload("flag-key");
}

flags.GetFlag() returns a nullable FeatureFlag object. Check VariantKey for multivariate flags and IsEnabled for boolean flags. It returns null when the flag wasn't returned by the evaluation.

Note: posthog.IsFeatureEnabledAsync(), posthog.GetFeatureFlagAsync(), and Capture(..., sendFeatureFlags: true, ...) still work during the migration period, but they're deprecated. Prefer EvaluateFlagsAsync() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to Capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

C#

var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user");

if (flags.IsEnabled("flag-key"))
{
    // Do something differently for this user
}

posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: null,
    groups: null,
    flags: flags
);

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

C#

// Attach only flags accessed with IsEnabled() or GetFlag() before this call
posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: null,
    groups: null,
    flags: flags.OnlyAccessed()
);

// Attach only specific flags
posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: null,
    groups: null,
    flags: flags.Only("checkout-flow", "new-dashboard")
);
Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

C#

posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: new()
    {
        // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant
        ["$feature/feature-flag-key"] = "variant-key",
    }
);
Evaluating only specific flags

By default, EvaluateFlagsAsync() evaluates every flag for the user. If you only need a few flags, pass FlagKeysToEvaluate to request only those flags:

C#

var flags = await posthog.EvaluateFlagsAsync(
    "distinct_id_of_your_user",
    options: new AllFeatureFlagsOptions
    {
        FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" },
    }
);
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With EvaluateFlagsAsync(), the SDK sends this event when you call flags.IsEnabled() or flags.GetFlag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.GetFlagPayload() doesn't send $feature_flag_called events and doesn't count as an access for OnlyAccessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

C#

var flags = await posthog.EvaluateFlagsAsync(
    "distinct_id_of_the_user",
    options: new AllFeatureFlagsOptions
    {
        PersonProperties = new()
        {
            ["property_name"] = "value",
        },
        Groups = new()
        {
            new Group("your_group_type", "your_group_id")
            {
                ["group_property_name"] = "value",
            },
            new Group("another_group_type", "another_group_id")
            {
                ["group_property_name"] = "another value",
            },
        },
    }
);

if (flags.IsEnabled("flag-key"))
{
    // Do something differently for this user
}
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

API

There are 3 steps to implement feature flags using the PostHog API:

Step 1: Evaluate the feature flag value using flags

flags is the endpoint used to determine if a given flag is enabled for a certain user or not.

Request
Terminal
# Basic request (flags only)
curl -v -L --header "Content-Type: application/json" -d '  {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user",
    "groups" : {
        "group_type": "group_id"
    }
}' "https://us.i.posthog.com/flags?v=2"

# With configuration (flags + PostHog config)
curl -v -L --header "Content-Type: application/json" -d '  {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user",
    "groups" : {
        "group_type": "group_id"
    }
}' "https://us.i.posthog.com/flags?v=2&config=true"
Python
import requests
import json

# Basic request (flags only)
url = "https://us.i.posthog.com/flags?v=2"
headers = {
    "Content-Type": "application/json"
}
payload = {
    "api_key": "<ph_project_token>",
    "distinct_id": "user distinct id",
    "groups": {
        "group_type": "group_id"
    }
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.json())

# With configuration (flags + PostHog config)
url_with_config = "https://us.i.posthog.com/flags?v=2&config=true"
response_with_config = requests.post(url_with_config, headers=headers, data=json.dumps(payload))
print(response_with_config.json())
Node.js
import fetch from "node-fetch";

async function sendFlagsRequest() {
    const headers = {
        "Content-Type": "application/json",
    };
    const payload = {
        api_key: "<ph_project_token>",
        distinct_id: "user distinct id",
        groups: {
            group_type: "group_id",
        },
    };

    // Basic request (flags only)
    const url = "https://us.i.posthog.com/flags?v=2";
    const response = await fetch(url, {
        method: "POST",
        headers: headers,
        body: JSON.stringify(payload),
    });
    const data = await response.json();
    console.log(data);

    // With configuration (flags + PostHog config)
    const urlWithConfig = "https://us.i.posthog.com/flags?v=2&config=true";
    const responseWithConfig = await fetch(urlWithConfig, {
        method: "POST",
        headers: headers,
        body: JSON.stringify(payload),
    });
    const dataWithConfig = await responseWithConfig.json();
    console.log(dataWithConfig);
}

sendFlagsRequest();

Note: The groups key is only required for group-based feature flags. If you use it, replace group_type and group_id with the values for your group such as company: "Twitter".

Using evaluation context tags and runtime filtering without SDKs

When making direct API calls to the /flags endpoint, you can control which flags are evaluated using evaluation context tags and runtime filtering.

Evaluation contexts

To filter flags by evaluation context, include the evaluation_contexts field in your request body:

Note: The legacy parameter evaluation_environments is also supported for backward compatibility.

Terminal
curl -v -L --header "Content-Type: application/json" -d '  {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user",
    "evaluation_contexts": ["production", "web"]
}' "https://us.i.posthog.com/flags?v=2"
Python
import requests
import json

url = "https://us.i.posthog.com/flags?v=2"
headers = {
    "Content-Type": "application/json"
}
payload = {
    "api_key": "<ph_project_token>",
    "distinct_id": "user distinct id",
    "evaluation_contexts": ["production", "web"]
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.json())
JavaScript
const response = await fetch("https://us.i.posthog.com/flags?v=2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        api_key: "<ph_project_token>",
        distinct_id: "user-distinct-id",
        evaluation_contexts: ["production", "web"]
    }),
});
const data = await response.json();

Only flags where at least one evaluation tag matches (or flags with no tags at all) will be returned. For example:

  • Flag with evaluation context tags ["production", "api", "backend"] + request with ["production", "web"] = ✅ Flag evaluates ("production" matches)
  • Flag with evaluation context tags ["staging", "api"] + request with ["production", "web"] = ❌ Flag doesn't evaluate (no tags match)
  • Flag with evaluation context tags ["web", "mobile"] + request with ["production", "web"] = ✅ Flag evaluates ("web" matches)
  • Flag with no evaluation context tags = ✅ Always evaluates (backward compatibility)
Runtime detection

Evaluation runtime (server vs. client) is automatically detected based on your request headers and user-agent. This determines which flags are available based on their runtime setting (server-only, client-only, or all).

How runtime is detected:

  1. User-Agent patterns - The system analyzes the User-Agent header:

    • Client-side patterns: Mozilla/, Chrome/, Safari/, Firefox/, Edge/ (browsers), or mobile SDKs like posthog-android/, posthog-ios/, posthog-react-native/, posthog-flutter/
    • Server-side patterns: posthog-python/, posthog-ruby/, posthog-php/, posthog-java/, posthog-go/, posthog-node/, posthog-dotnet/, posthog-elixir/, python-requests/, curl/
  2. Browser-specific headers - Presence of these headers indicates client-side:

    • Origin header
    • Referer header
    • Sec-Fetch-Mode header
    • Sec-Fetch-Site header
  3. Default behavior - If runtime can't be determined, the system includes flags with no runtime requirement and those set to "all"

Examples of runtime detection:

JavaScript

// Browser fetch - Detected as CLIENT runtime
// Will receive: client-only flags + "all" flags
// Won't receive: server-only flags
const response = await fetch("https://us.i.posthog.com/flags?v=2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        // Browser automatically adds Origin, Referer, Sec-Fetch-* headers
    },
    body: JSON.stringify({
        api_key: "<ph_project_token>",
        distinct_id: "user-id"
    })
});

Python

# Python requests - Detected as SERVER runtime
# Will receive: server-only flags + "all" flags
# Won't receive: client-only flags
import requests

response = requests.post(
    "https://us.i.posthog.com/flags?v=2",
    json={
        "api_key": "<ph_project_token>",
        "distinct_id": "user-id"
    }
    # python-requests/ in User-Agent indicates server-side
)

Terminal

# curl - Detected as SERVER runtime
# Will receive: server-only flags + "all" flags
# Won't receive: client-only flags
curl -v -L --header "Content-Type: application/json" -d '{
    "api_key": "<ph_project_token>",
    "distinct_id": "user-id"
}' "https://us.i.posthog.com/flags?v=2"
# curl/ in User-Agent indicates server-side

JavaScript

// Node.js with custom User-Agent - Control runtime detection
const response = await fetch("https://us.i.posthog.com/flags?v=2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "User-Agent": "posthog-node/3.0.0"  // Explicitly indicates server-side
    },
    body: JSON.stringify({
        api_key: "<ph_project_token>",
        distinct_id: "user-id"
    })
});
Combining evaluation context tags and runtime filtering

Both features work together as sequential filters:

JavaScript

// Example: Production web client
const response = await fetch("https://us.i.posthog.com/flags?v=2", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        // Browser headers will trigger client runtime detection
    },
    body: JSON.stringify({
        api_key: "<ph_project_token>",
        distinct_id: "user-id",
        evaluation_contexts: ["production", "web"]
    })
});

// This request will only receive flags that:
// 1. Have runtime set to "client" OR "all" (due to browser headers)
// AND
// 2. Have evaluation context tags matching "production" OR "web" (or no tags)
// Note: You can also use the legacy "evaluation_environments" parameter

This allows precise control over which flags are evaluated in different contexts, helping optimize costs and improve security by ensuring flags only evaluate where intended.

Response

The response varies depending on whether you include the config=true query parameter:

Basic response (/flags?v=2)

Use this endpoint when you only need to evaluate feature flags. It returns a response with just the flag evaluation results.

Note: If a feature flag is associated with an experiment that has a holdout group (/docs/experiments/holdouts.md), users in the holdout receive a variant value in the format holdout-{holdout_id} (e.g., holdout-727). You can detect holdout users by checking if the variant starts with holdout-.

JSON

{
  "flags": {
    "my-awesome-flag": {
      "key": "my-awesome-flag",
      "enabled": true,
      "reason": {
        "code": "condition_match",
        "condition_index": 0,
        "description": "Condition set 1 matched"
      },
      "metadata": {
        "id": 1,
        "version": 1,
        "payload": "{\"example\": \"json\", \"payload\": \"value\"}"
      }
    },
    "my-multivariate-flag" :{
      "key":"my-multivariate-flag",
      "enabled": true,
      "variant": "some-string-value",
      "reason": {
        "code": "condition_match",
        "condition_index": 1,
        "description": "Condition set 2 matched"
      },
      "metadata": {
        "id": 2,
        "version": 42,
      }
    },
    "flag-thats-not-on": {
      "key": "flag-thats-not-on",
      "enabled": false,
      "reason": {
        "code": "no_condition_match",
        "condition_index": 0,
        "description": "No condition sets matched"
      },
      "metadata": {
        "id": 3,
        "version": 1
      }
    }
  },
  "errorsWhileComputingFlags": false,
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
Full response with configuration (/flags?v=2&config=true)

Use this endpoint when you need both feature flag evaluation and PostHog configuration information (useful for client-side SDKs that need to initialize PostHog):

JSON

{
  "config": {
    "enable_collect_everything": true
  },
  "toolbarParams": {},
  "errorsWhileComputingFlags": false,
  "isAuthenticated": false,
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "supportedCompression": [
    "gzip",
    "lz64"
  ],
  "flags": {
    "my-awesome-flag": {
      "key": "my-awesome-flag",
      "enabled": true,
      "reason": {
        "code": "condition_match",
        "condition_index": 0,
        "description": "Condition set 1 matched"
      },
      "metadata": {
        "id": 1,
        "version": 1,
        "payload": "{\"example\": \"json\", \"payload\": \"value\"}"
      }
    },
    "my-multivariate-flag" :{
      "key":"my-multivariate-flag",
      "enabled": true,
      "variant": "some-string-value",
      "reason": {
        "code": "condition_match",
        "condition_index": 1,
        "description": "Condition set 2 matched"
      },
      "metadata": {
        "id": 2,
        "version": 42,
      }
    },
    "flag-thats-not-on": {
      "key": "flag-thats-not-on",
      "enabled": false,
      "reason": {
        "code": "no_condition_match",
        "condition_index": 0,
        "description": "No condition sets matched"
      },
      "metadata": {
        "id": 3,
        "version": 1
      }
    }
  }
}

Note: errorsWhileComputingFlags will return true if we didn't manage to compute some flags (for example, if there's an ongoing incident involving flag evaluation).

This enables partial updates to currently active flags in your clients.

Quota limiting

If your organization exceeds its feature flag quota, the /flags endpoint will return a modified response with quotaLimited.

For basic response (/flags?v=2):

JSON

{
  "flags": {},
  "errorsWhileComputingFlags": false,
  "quotaLimited": ["feature_flags"],
  "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e"
}

For full response with configuration (/flags?v=2&config=true):

JSON

{
  "config": {
    "enable_collect_everything": true
  },
  "toolbarParams": {},
  "isAuthenticated": false,
  "supportedCompression": [
    "gzip",
    "lz64"
  ],
  "flags": {},
  "errorsWhileComputingFlags": false,
  "quotaLimited": ["feature_flags"],
  "requestId": "d4d89b14-9619-4627-adf2-01b761691c2e"
  // ... other fields, not relevant to feature flags
}

When you receive a response with quotaLimited containing "feature_flags", it means:

  1. Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota
  2. If you want to continue evaluating feature flags, you can increase your quota in your billing settings under Feature flags & Experiments or contact support
Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

To do this, include the $feature/feature_flag_name property in your event:

Terminal
curl -v -L --header "Content-Type: application/json" -d '  {
    "api_key": "<ph_project_token>",
    "event": "your_event_name",
    "distinct_id": "distinct_id_of_your_user",
    "properties": {
      "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
    }
}' https://us.i.posthog.com/i/v0/e/
Python
import requests
import json

url = "https://us.i.posthog.com/i/v0/e/"
headers = {
    "Content-Type": "application/json"
}
payload = {
    "api_key": "<ph_project_token>",
    "event": "your_event_name",
    "distinct_id": "distinct_id_of_your_user",
    "properties": {
      "$feature/feature-flag-key": "variant-key" # Replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
    }
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response)
Step 3: Send a $feature_flag_called event

To track usage of your feature flag and view related analytics in PostHog, submit the $feature_flag_called event whenever you check a feature flag value in your code.

You need to include two properties with this event:

  1. $feature_flag_response: This is the name of the variant the user has been assigned to e.g., "control" or "test"
  2. $feature_flag: This is the key of the feature flag in your experiment.
Terminal
curl -v -L --header "Content-Type: application/json" -d '  {
    "api_key": "<ph_project_token>",
    "event": "$feature_flag_called",
    "distinct_id": "distinct_id_of_your_user",
    "properties": {
      "$feature_flag": "feature-flag-key",
      "$feature_flag_response": "variant-name"
    }
}' https://us.i.posthog.com/i/v0/e/
Python
import requests
import json

url = "https://us.i.posthog.com/i/v0/e/"
headers = {
    "Content-Type": "application/json"
}
payload = {
    "api_key": "<ph_project_token>",
    "event": "feature_flag_called",
    "distinct_id": "distinct_id_of_your_user",
    "properties": {
      "$feature_flag": "feature-flag-key",
      "$feature_flag_response": "variant-name"
    }
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response)
Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

Terminal
curl -v -L --header "Content-Type: application/json" -d '  {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user",
    "groups" : { # Required only for group-based feature flags
      "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group.
    },
    "person_properties": {"<personProp1>": "<personVal1>"}, # Optional. Include any properties used to calculate the value of the feature flag.
    "group_properties": {"group type": {"<groupProp1>":"<groupVal1>"}} # Optional. Include any properties used to calculate the value of the feature flag.
}' https://us.i.posthog.com/flags?v=2
Python
import requests
import json

url = "https://us.i.posthog.com/flags?v=2"
headers = {
    "Content-Type": "application/json"
}
payload = {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user",
    "groups" : { # Required only for group-based feature flags
      "group_type": "group_id" # Replace "group_type" with the name of your group type. Replace "group_id" with the id of your group.
    },
    "person_properties": {"<personProp1>": "<personVal1>"}, # Optional. Include any properties used to calculate the value of the feature flag.
    "group_properties": {"group type": {"<groupProp1>":"<groupVal1>"}} # Optional. Include any properties used to calculate the value of the feature flag.
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.json())
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

To override the GeoIP properties used to evaluate a feature flag, provide an IP address in the HTTP_X_FORWARDED_FOR when making your /flags request:

Terminal
curl -v -L \
--header "Content-Type: application/json" \
--header "HTTP_X_FORWARDED_FOR: the_client_ip_address_to_use " \
-d '  {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user"
}' https://us.i.posthog.com/flags?v=2
Python
import requests
import json

url = "https://us.i.posthog.com/flags?v=2"
headers = {
    "Content-Type": "application/json",
    "HTTP_X_FORWARDED_FOR": "the_client_ip_address_to_use"
}
payload = {
    "api_key": "<ph_project_token>",
    "distinct_id": "distinct_id_of_your_user"
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.json())

The list of properties that this overrides:

  1. $geoip_city_name
  2. $geoip_country_name
  3. $geoip_country_code
  4. $geoip_continent_name
  5. $geoip_continent_code
  6. $geoip_postal_code
  7. $geoip_time_zone
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/android.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Android Feature Flags installation

  1. 1

    Install the dependency

    Required

    Add the PostHog Android SDK to your build.gradle dependencies:

    build.gradle

    dependencies {
        implementation("com.posthog:posthog-android:3.+")
    }
  2. 2

    Configure PostHog

    Required

    Initialize PostHog in your Application class:

    SampleApp.kt

    class SampleApp : Application() {
    
        companion object {
            const val POSTHOG_PROJECT_TOKEN = "<ph_project_token>"
            const val POSTHOG_HOST = "https://us.i.posthog.com"
        }
    
        override fun onCreate() {
            super.onCreate()
    
            // Create a PostHog Config with the given project token and host
            val config = PostHogAndroidConfig(
                apiKey = POSTHOG_PROJECT_TOKEN,
                host = POSTHOG_HOST
            )
    
            // Setup PostHog with the given Context and Config
            PostHogAndroid.setup(this, config)
        }
    }
  3. 3

    Send events

    Recommended

    Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration:

    Kotlin

    import com.posthog.PostHog
    
    PostHog.capture(
        event = "button_clicked",
        properties = mapOf(
            "button_name" to "signup"
        )
    )
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    Kotlin

    val isMyFlagEnabled = PostHog.isFeatureEnabled("flag-key")
    if (isMyFlagEnabled) {
        // Do something differently for this user
        // Optional: fetch the payload
        val matchedFlagPayload = PostHog.getFeatureFlagResult("flag-key")?.payload
    }
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    Kotlin

    val enabledVariant = PostHog.getFeatureFlag("flag-key")
    if (enabledVariant == "variant-key") { // replace 'variant-key' with the key of your variant
        // Do something differently for this user
        // Optional: fetch the payload
        val matchedFlagPayload = PostHog.getFeatureFlagResult("flag-key")?.payload
    }
  6. 6

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  7. 7

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/api.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

API Feature Flags installation

  1. 1

    Evaluate the feature flag value using flags

    Required

    flags is the endpoint used to determine if a given flag is enabled for a certain user or not.

    Basic request (flags only)
    curl -v -L --header "Content-Type: application/json" -d '{
        "token": "<ph_project_token>",
        "distinct_id": "distinct_id_of_your_user",
        "groups" : {
            "group_type": "group_id"
        }
    }' "https://us.i.posthog.com/flags?v=2"
    Python
    import requests
    import json
    
    url = "https://us.i.posthog.com/flags?v=2"
    headers = {
        "Content-Type": "application/json"
    }
    payload = {
        "token": "<ph_project_token>",
        "distinct_id": "user distinct id",
        "groups": {
            "group_type": "group_id"
        }
    }
    response = requests.post(url, headers=headers, data=json.dumps(payload))
    print(response.json())
    Node.js
    const response = await fetch("https://us.i.posthog.com/flags?v=2", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            token: "<ph_project_token>",
            distinct_id: "user distinct id",
            groups: {
                group_type: "group_id",
            },
        }),
    });
    const data = await response.json();
    console.log(data);

    Note: The groups key is only required for group-based feature flags. If you use it, replace group_type and group_id with the values for your group such as company: "Twitter".

  2. 2

    Include feature flag information when capturing events

    Required

    If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

    Note: This step is only required for events captured using our server-side SDKs or API.

    Terminal
    curl -v -L --header "Content-Type: application/json" -d '{
        "token": "<ph_project_token>",
        "event": "your_event_name",
        "distinct_id": "distinct_id_of_your_user",
        "properties": {
            "$feature/feature-flag-key": "variant-key"
        }
    }' https://us.i.posthog.com/i/v0/e/
    Python
    import requests
    import json
    
    url = "https://us.i.posthog.com/i/v0/e/"
    headers = {
        "Content-Type": "application/json"
    }
    payload = {
        "token": "<ph_project_token>",
        "event": "your_event_name",
        "distinct_id": "distinct_id_of_your_user",
        "properties": {
            "$feature/feature-flag-key": "variant-key"
        }
    }
    response = requests.post(url, headers=headers, data=json.dumps(payload))
    print(response)
  3. 3

    Send a $feature_flag_called event

    Optional

    To track usage of your feature flag and view related analytics in PostHog, submit the $feature_flag_called event whenever you check a feature flag value in your code.

    You need to include two properties with this event:

    1. $feature_flag_response: This is the name of the variant the user has been assigned to e.g., "control" or "test"
    2. $feature_flag: This is the key of the feature flag in your experiment.
    Terminal
    curl -v -L --header "Content-Type: application/json" -d '{
        "token": "<ph_project_token>",
        "event": "$feature_flag_called",
        "distinct_id": "distinct_id_of_your_user",
        "properties": {
            "$feature_flag": "feature-flag-key",
            "$feature_flag_response": "variant-name"
        }
    }' https://us.i.posthog.com/i/v0/e/
    Python
    import requests
    import json
    
    url = "https://us.i.posthog.com/i/v0/e/"
    headers = {
        "Content-Type": "application/json"
    }
    payload = {
        "token": "<ph_project_token>",
        "event": "$feature_flag_called",
        "distinct_id": "distinct_id_of_your_user",
        "properties": {
            "$feature_flag": "feature-flag-key",
            "$feature_flag_response": "variant-name"
        }
    }
    response = requests.post(url, headers=headers, data=json.dumps(payload))
    print(response)
  4. 4

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  5. 5

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/best-practices.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Best practices for production-ready flags

Checklist

  • Call identify() before evaluating flags (#resolve-identity-before-evaluating-flags) – the hash uses the wrong ID otherwise. This is the most common input problem.
  • Evaluate flags server-side with local evaluation (#server-side-local-evaluation-is-the-recommended-default) – explicit inputs, your data right there, no workarounds.
  • Bootstrap client-side flags (#have-the-value-before-you-need-it) – client-side evaluation is async. Bootstrap (/docs/feature-flags/bootstrapping.md) to eliminate the gap.
  • Handle undefined explicitly (#undefined-is-not-false) – it means "not evaluated yet," not false.
  • Evaluate once, record the result (#evaluate-once-not-continuously) – a flag is a one-time signal. Re-evaluate only on meaningful state changes.
  • Evaluate where the data lives (#evaluate-where-the-data-lives) – if the data is on your server, evaluate there.
  • Choose evaluation context deliberately (#choose-a-flag-type-intentionally) – "server and client" is the default for compatibility, not because it's the right choice for your flag.
  • Clean up flags that have done their job (#clean-up-flags-that-have-done-their-job) – a flag at 100% is done. Remove it or archive it.
  • Disable client-side evaluation for server-side flags (#disable-client-side-evaluation-for-server-side-flags) – don't let the client SDK re-evaluate what your server already decided.
  • Use a reverse proxy (#use-a-reverse-proxy) – prevent ad blockers from disabling your flags.
  • Call your flag in as few places as possible (#call-your-flag-in-as-few-places-as-possible) – wrap in a single function if used in multiple places.
  • Name flags clearly (#name-flags-clearly) – descriptive names, types, positive language.
  • Roll out progressively (#roll-out-progressively) – start small, monitor, then increase.

The mental model: Flags are pure functions (#flags-are-pure-functions) – same flag key + same distinct ID = same result. Always. Unexpected results are almost always input problems (#unexpected-results-are-almost-always-input-problems) – if the result changed, an input changed.


Flags are pure functions

A flag hashes two things – the flag key and the distinct ID – and returns a deterministic result. Same inputs, same output. Every time.

hash("my-experiment", "user-123") → 0.31 → always 0.31

On top of that, PostHog layers property targeting (does this user match?), rollout percentage (is their position below the threshold?), and variant assignment. But the foundation is the hash: same flag key + same distinct ID = same result.

Technically

"Pure function" means deterministic given a stable flag definition. The definition (rollout %, targeting rules, variants) is external state. Given the same definition, evaluation is fully deterministic on flag_key + distinct_id. Some features like experience continuity (#dont-rely-on-flag-persistence-to-fix-identity-gaps) add persistence layers that introduce side effects on the server, but from your perspective as the caller, the model holds: same inputs, same output.

How the hash works

PostHog uses SHA-1:

hash_key = "{flag_key}.{distinct_id}"
position = parseInt(sha1(hash_key).slice(0, 15), 16) / LONG_SCALE  → float in [0, 1]
in_rollout = position <= rollout_percentage / 100

For variants, a second hash with salt "variant" maps to variant ranges independently. The flag key is included so the same user gets independent assignments across different flags.

If the flag has property targeting, PostHog first checks whether the person matches the conditions. If they don't match, the hash never runs – the flag returns false.

Unexpected results are almost always input problems

If you evaluate the same flag with the same distinct ID a million times, you will get the same result a million times. It's how the math works. The hash is deterministic. It doesn't drift, it doesn't have off days, and it doesn't return different values on Tuesdays.

So when a flag returns something you didn't expect, the flag is fine, the problem is in the inputs passed to the flag. Something about the identity, the properties, or the flag definition wasn't what you assumed. Find what changed, and you've found the problem.

If you keep running into flag issues and they're not incidents, the conversation isn't about PostHog's flag behavior – it's about how your application coordinates the data that flags depend on. That's an engineering conversation about identity flows, property syncing, and evaluation architecture. No single config tweak fixes it.

We're here to help with that – this guide, PostHog AI (/docs/feature-flags/manage-flags-ai.md), and professional services all exist for exactly this. But the starting point is always the same: look at the inputs.

When something goes wrong, in order of likelihood:

  1. Input problems (most common). Wrong distinct ID, missing properties, changed flag definition. PostHog gives you tools to get the coordination right – bootstrapping (/docs/feature-flags/bootstrapping.md), property overrides (/docs/feature-flags/property-overrides.md), server-side evaluation (/docs/feature-flags/local-evaluation.md).
  2. Output problems. The flag returned the right value but your code misread it – undefined treated as false, no handling for the loading gap, evaluating repeatedly instead of recording the result.
  3. Actual incidents. Check status.posthog.com. If nothing there, it's #1 or #2. And even here: with server-side local evaluation (/docs/feature-flags/local-evaluation.md), the SDK evaluates against cached flag definitions locally. PostHog being unreachable doesn't affect flags that are already cached. Add per-flag safe defaults and even a cold start during an outage returns usable values. An incident only breaks your flags if your implementation depends on PostHog being available at request time – which is itself an implementation gap you can close.

Resolve identity before evaluating flags

Identity is the most common input problem. The hash takes two inputs: the flag key (stable) and the distinct ID (your responsibility). If the distinct ID is wrong at the moment of evaluation, the hash produces a valid but incorrect result. The flag is working perfectly – it just answered a question about the wrong person.

If you call identify() after a flag has already been evaluated, the flag likely used the anonymous ID. The hash produced one result. After identify(), the distinct ID changes, the hash changes, and the next evaluation returns a different variant. You see a "flip" – but it's because the input changed.

Call identify() (/docs/product-analytics/identify.md) before any flag evaluation in auth flows. If you can't guarantee that timing, bootstrap (/docs/feature-flags/bootstrapping.md) with the stable ID at init so the distinct ID is correct from the first millisecond. See keeping flag evaluations stable (/docs/feature-flags/stable-identity-for-flags.md) for the full picture.

SPA-specific timing. In single-page applications, identify() and event captures often fire from different components during the same navigation in unpredictable order. The SDK updates the distinct_id synchronously when identify() runs, but if capture() was called first in the same execution frame, that event uses the anonymous ID. The fix: call identify() before the navigation that mounts post-auth components – in Vue, in beforeEach before next(); in React, before navigate(), not in a useEffect inside the target route.

Don't rely on flag persistence to fix identity gaps

If you've enabled experience continuity (/docs/feature-flags/creating-feature-flags.md#persisting-feature-flags-across-authentication-steps-optional) (flag persistence across authentication), consider what that's telling you: the distinct ID is changing during your session, and you need PostHog to paper over it.

That comes at a cost. Experience continuity couples flag evaluation with database writes – every evaluation reads and writes to the DB to persist the result. This mixes two concerns (evaluation and storage) that should be separate, and it's the source of known bugs where values can still change after identify(). It also means no support for local evaluation (/docs/feature-flags/local-evaluation.md) and slower flag responses.

The better fix is to make persistence unnecessary. Use device bucketing (/docs/feature-flags/device-bucketing.md) for single-device consistency, or design your identity flow so the distinct ID never changes (/docs/feature-flags/stable-identity-for-flags.md). If you need experience continuity today, treat it as a migration path toward proper identity resolution (/docs/product-analytics/identity-resolution.md), not a permanent solution. The identity gap it papers over is the root cause of the most common flag issues – closing that gap eliminates the need for persistence entirely.

Evaluation architecture

How you evaluate flags – where, when, and how often – determines the complexity of your implementation. Most workarounds exist because the evaluation happens in the wrong place or at the wrong time.

Evaluate once, not continuously

A flag is a one-time signal, not a continuous dependency. Evaluate it once, record the result, serve from that recording. Re-evaluate only when something meaningful changes.

Re-evaluating on every request creates cost, latency, and the conditions for "flipping" – you're giving the system repeated chances to return a different answer when inputs shift. That's not a bug. That's the pure function doing its job with different inputs.

  • Feature rollouts – Evaluate when your user's state changes (upgrades, joins a cohort). Between triggers, your app already knows the answer.
  • Experiments – One exposure per user. Evaluate once, record the variant, deliver that experience. If a user flips variants, the app re-asked a question it already had the answer to.
Evaluate where the data lives

If you target a flag on plan_type: "pro", your app originally told PostHog this person is Pro. Evaluate the flag from the same place that has that knowledge – your server. PostHog does the distribution math; your app provides the targeting data.

If you evaluate client-side instead, the SDK needs to fetch that property from PostHog's servers – a round-trip to look up what you originally sent it. Any flag check before that completes evaluates against incomplete data.

If you must evaluate client-side, use setPersonPropertiesForFlags() (/docs/feature-flags/property-overrides.md#manual-overrides-with-setpersonpropertiesforflags) to set properties locally before evaluation. This avoids the round-trip when you already have the data in the browser.

Property targeting is fine – just understand that the further the evaluation is from the data, the more async complexity you take on.

Server-side local evaluation (/docs/feature-flags/local-evaluation.md) is where the pure function model is fully legible:

  • All inputs are explicit. You pass the distinct ID and properties directly. When something's wrong, you log what you passed.
  • Your data is right there. User plan, account type, permissions – it's in your database at request time. No syncing, no fetching.
  • No workarounds needed. Client-side evaluation often requires setPersonPropertiesForFlags(), onFeatureFlags(), and bootstrap to bridge the gap between where the data lives and where the flag evaluates. Server-side eliminates the gap.

Client-side evaluation is right when you need properties only available in the browser, real-time flag changes, or have no server. But you're trading explicit inputs for implicit ones, and every workaround bridges that gap.

Have the value before you need it

Client-side flag evaluation is async – the SDK needs to fetch values from PostHog. Any flag check before that completes returns undefined, not false.

Bootstrap (/docs/feature-flags/bootstrapping.md) is the fix. Evaluate flags server-side and pass values to the client at init. The value exists before the page renders – no gap, no flicker.

If you can't bootstrap, use onFeatureFlags() to wait. This means you will need a loading state (spinner, skeleton) until flags arrive – it prevents showing the wrong variant but doesn't prevent a delay.

undefined is not "flag is off" nor false

posthog.getFeatureFlag() returns undefined before flags load. That means "not evaluated yet," not "flag is off."

JavaScript

// Returns undefined before flags load – not false
if (posthog.getFeatureFlag('my-experiment') === 'test') {
  // Never runs during the loading gap
}

Handle it with bootstrap (/docs/feature-flags/bootstrapping.md) (preferred) or onFeatureFlags() (adds a loading state). You can check the current identity with posthog.get_distinct_id().

The "not loaded yet" return value varies across SDKs – some return undefined/nil/None, others return false or a defaultValue you provide. Don't assume that a falsy return means the flag is off. Check your SDK's documentation for the exact return type of getFeatureFlag() and isFeatureEnabled() when flags haven't loaded, and handle that state explicitly. If your goal is to programmatically check whether a flag exists at all, use the Feature Flags API (/docs/api/feature-flags.md) to query flag definitions directly.

Flag hygiene

Flags are infrastructure. Like any infrastructure, they accumulate cost when left unattended. These are operational practices that keep your flag system clean and efficient.

Choose a flag type intentionally

Every flag in PostHog is configured as client-side, server-side, or both via evaluation contexts (/docs/feature-flags/evaluation-contexts.md). New flags default to "server and client" – this exists for backwards compatibility (it's how all flags worked before we added evaluation contexts) and to avoid blocking users who haven't thought about their implementation yet. It's a safe starting point, not a recommendation.

If all your flags are set to both, that usually means the decision was never revisited after creation – and you're paying for client-side evaluation on flags that only need to exist on your server.

Pick the context based on where the flag is actually consumed. Server-side flags that drive backend logic don't need client SDKs fetching and evaluating them. Client-side flags for UI variations don't need server-side evaluation. "Both" is valid when a flag genuinely needs to be evaluated in both contexts – but it should be a deliberate choice, not the default you never changed.

Clean up flags that have done their job

A flag set to 100% of all users with no property targeting is a flag that has finished its job. It's always returning the same value – the rollout is complete, the experiment concluded, the feature is live. If your SDK still evaluates that flag, it can keep making billable /flags requests, keep appearing in SDK payloads, and add clutter to your codebase.

Remove the flag and hardcode the winning path. If you're not ready to remove it from code, at least archive it in PostHog so it stops being evaluated. Stale flags are the most common source of unnecessary flag evaluation. See cleaning up stale flags (/docs/feature-flags/cleaning-up-stale-flags.md) for the full workflow and cutting costs (/docs/feature-flags/cutting-costs.md) for more on reducing your bill.

An idea worth considering: design your flag code paths with an escape hatch you control outside of PostHog. For example, a "gate flag" that your server reads once every 30 seconds (not per user) – when it's true, the feature is fully rolled out and your code skips the per-user flag evaluation entirely. This means you stop making per-user /flags requests for that rollout as soon as it's complete, even before you remove the flag from code. And you can dial it back by setting the gate flag to false. This is also another application of "evaluate once, not continuously" – if you cache flag results, your per-user evaluation cost drops while you wait for the code cleanup.

Disable client-side evaluation for server-side flags

If a flag is evaluated server-side and the result is passed to your frontend through your own application logic, the client SDK doesn't need to evaluate it independently. But unless you explicitly disable the flag on the client, the SDK will still fetch and evaluate it – duplicating work your server already did.

This is the practical extension of "evaluate once, not continuously." Your server evaluates, your application propagates the result, and the client consumes it as application state rather than re-asking PostHog. Disable flags in the client SDK that your server already handles to eliminate redundant evaluation and reduce payload size.

Use a reverse proxy

Ad blockers can disable your Feature Flags, leading to users seeing the wrong version of your app or missing a rollout. Deploy a reverse proxy (/docs/advanced/proxy.md) so requests go through your own domain. PostHog offers a free managed reverse proxy (/docs/advanced/proxy/managed-reverse-proxy.md), or you can run your own.

Call your flag in as few places as possible

The more locations a flag appears in your code, the more likely it is to cause problems – a developer removes it in one place but forgets another. If you use a flag in multiple places, wrap it in a single function:

JavaScript

function useBetaFeature() {
    return posthog.isFeatureEnabled('beta-feature')
}
Name flags clearly

Good naming makes flags easier to understand and maintain:

  • Use descriptive names. is_v2_billing_dashboard_enabled is clearer than is_dashboard_enabled.
  • Use name types. Suffix with the purpose: new-billing-experiment, new-billing-release.
  • Reflect the return type. is_premium_user for a boolean, selected_theme for a string.
  • Use positive language for booleans. is_premium_user instead of is_not_premium_user – avoids double negatives.
Roll out progressively

Start at 5-10% of users, monitor metrics, then gradually increase. This is a phased rollout (/tutorials/phased-rollout.md). At PostHog, we typically roll out to the developer first, then the internal team, then beta users, then everyone.

Use dependencies for complex rollouts

Feature flag dependencies (/docs/feature-flags/dependencies.md) let one flag's activation depend on another flag's state – useful for enabling complex features only after foundational components are active, or running Experiments only on users with specific features enabled. Keep dependency chains simple and avoid circular dependencies.

Be careful with "Latest" person properties

PostHog automatically creates person properties like "Latest Current URL" and "Latest Referring Domain" — these are derived from the corresponding event properties (like $current_url) and update every time a new event comes in. If you target a flag on one of these, the flag value can change with every new event. If you need to target based on a value like this, capture it once as a stable person property (e.g., first_landing_page via $set_once) and target that instead.

Reducing your bill

Stale flags are the most common source of unnecessary cost. Beyond cleaning up flags, see our dedicated guide to cutting costs (/docs/feature-flags/cutting-costs.md) for estimating and reducing your feature flag bill.

Further reading

  • Identity resolution (/docs/product-analytics/identity-resolution.md) – how PostHog resolves who a user is
  • Keeping flag evaluations stable (/docs/feature-flags/stable-identity-for-flags.md) – preventing the hash input from changing across auth transitions
  • Local evaluation (/docs/feature-flags/local-evaluation.md) – server-side evaluation for explicit input control
  • Bootstrapping (/docs/feature-flags/bootstrapping.md) – having flag values before the page renders
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/django.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Django

PostHog makes it easy to get data about traffic and usage of your Django app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more.

This guide walks you through integrating PostHog into your Django app using the Python SDK (/docs/libraries/python.md).

Beta: integration via LLM

Install PostHog for Django in seconds with our wizard by running this prompt with LLM coding agents (/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal.

npx @posthog/wizard

Learn more (/wizard.md)

Or, to integrate manually, continue with the rest of this guide.

These docs cover version 7.x of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See supported versions (#supported-versions).

Installation

To start, run pip install posthog to install PostHog’s Python SDK.

Then, configure PostHog in your app config so it's initialized when Django starts:

your_app/apps.py

from django.apps import AppConfig
import posthog

class YourAppConfig(AppConfig):
    name = 'your_app_name'

    def ready(self):
        posthog.api_key = '<ph_project_token>'
        posthog.host = 'https://us.i.posthog.com'

Next, if you haven't done so already, add your AppConfig to INSTALLED_APPS in settings.py:

settings.py

INSTALLED_APPS = [
    # ... other apps
    'your_app_name.apps.YourAppConfig',
]

You can find your project token and instance address in your project settings.

To capture events from any file, import posthog and call the method you need. For example:

Python

import posthog
from posthog import identify_context

def some_request(request):
    with posthog.new_context():
        # Django includes request.user for anonymous visitors too. Only identify
        # the context when the visitor is logged in.
        if request.user.is_authenticated:
            identify_context(str(request.user.pk))

        posthog.capture('event_name')

Events captured without a context or explicit distinct_id are sent as anonymous events (/docs/data/anonymous-vs-identified-events.md) with an auto-generated distinct_id. See the Python SDK docs (/docs/libraries/python.md#person-profiles-and-properties) for more details.

Identifying users

Identifying users is required. Backend events need a distinct_id to associate events with the correct user.

In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct distinct_id. Typically, you would set a fresh context and identify at the top of each route.

Python

from posthog import new_context, identify_context, capture

@app.get("/foo")
def foo(current_user: User = Depends(get_current_user)):
    with new_context(): # Set context at the top of a route
        identify_context(current_user.id)
        capture("foo_viewed")
    return {"status": "ok"}

When possible, write a small piece of middleware that resolves your authenticated user, wrap a context around the request, and identifies it. Every capture() downstream is then attributed automatically. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK.

Django contexts middleware

The Python SDK provides a Django middleware that automatically wraps all requests with a context (/docs/libraries/python.md#contexts). This middleware extracts session and user information from each request and tags all events captured during that request with relevant metadata.

Basic setup

Add the middleware to your Django settings. If your app uses Django authentication, place it after django.contrib.auth.middleware.AuthenticationMiddleware so the middleware can use the authenticated Django user as a distinct ID fallback and capture the user's email.

Python

MIDDLEWARE = [
    # ... other middleware
    'posthog.integrations.django.PosthogContextMiddleware',
    # ... other middleware
]

The middleware uses the globally configured posthog client by default, so you don't need to create or pass it a separate client instance.

The middleware automatically extracts and uses:

  • Session ID from the X-POSTHOG-SESSION-ID header, if present
  • Distinct ID from the X-POSTHOG-DISTINCT-ID header, if present, falling back to the authenticated Django user's pk (Django's primary-key alias, which works with custom user models)
  • User email from the authenticated Django user's email as email
  • Current URL as $current_url
  • Request method as $request_method
  • Request path as $request_path
  • Forwarded IP address from X-Forwarded-For as $ip
  • User agent from User-Agent as $user_agent

The session and distinct ID headers are sanitized before use. Empty values are ignored, control characters are removed, values are trimmed, and values are capped at 1000 characters.

All events captured during the request (including exceptions) include these properties and are associated with the extracted session and distinct ID.

Login and signup views

The middleware reads request.user once, before your view runs. On a login or signup request the visitor is still anonymous at that point, so the request's context has no distinct ID. Calling login() inside the view doesn't change that. Everything captured during that request stays anonymous, including the login event itself.

Identify the context from inside the request once you know who the user is. Django's auth signals are the natural place:

Python

from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from posthog import identify_context

@receiver(user_logged_in)
def identify_posthog_user(sender, request, user, **kwargs):
    identify_context(str(user.pk))

Every capture later in that request is then attributed to the user who just logged in. Requests made after login don't need this. The middleware sees the authenticated user from the start.

If you're using PostHog JavaScript Web (/docs/libraries/js.md) on the frontend, configure tracing_headers (/docs/libraries/js/config.md#tracing-headers) for your Django backend hostname so browser requests include the session and distinct ID headers.

Exception capture

By default, the middleware captures exceptions and sends them to PostHog's error tracking using the globally configured posthog client. This includes Django view exceptions that Django converts into error responses.

Disable this by setting:

Python

# settings.py
POSTHOG_MW_CAPTURE_EXCEPTIONS = False
Adding custom tags

Use POSTHOG_MW_EXTRA_TAGS to add custom properties to all requests:

Python

# settings.py
def add_user_tags(request):
    # type: (HttpRequest) -> Dict[str, Any]
    tags = {}
    if hasattr(request, 'user') and request.user.is_authenticated:
        # Use pk instead of id so this works with custom User primary keys.
        tags['user_id'] = str(request.user.pk)
        tags['email'] = request.user.email
    return tags

POSTHOG_MW_EXTRA_TAGS = add_user_tags
Filtering requests

Skip tracking for certain requests using POSTHOG_MW_REQUEST_FILTER:

Python

# settings.py
def should_track_request(request):
    # type: (HttpRequest) -> bool
    # Don't track health checks or admin requests
    if request.path.startswith('/health') or request.path.startswith('/admin'):
        return False
    return True

POSTHOG_MW_REQUEST_FILTER = should_track_request
Modifying default tags

Use POSTHOG_MW_TAG_MAP to modify or remove default tags:

Python

# settings.py
def customize_tags(tags):
    # type: (Dict[str, Any]) -> Dict[str, Any]
    # Remove URL for privacy
    tags.pop('$current_url', None)
    # Add custom prefix to method
    if '$request_method' in tags:
        tags['http_method'] = tags.pop('$request_method')
    return tags

POSTHOG_MW_TAG_MAP = customize_tags
Complete configuration example

Python

# settings.py
def add_request_context(request):
    # type: (HttpRequest) -> Dict[str, Any]
    tags = {}
    if hasattr(request, 'user') and request.user.is_authenticated:
        tags['user_type'] = 'authenticated'
        # Use pk instead of id so this works with custom User primary keys.
        tags['user_id'] = str(request.user.pk)
    else:
        tags['user_type'] = 'anonymous'

    # Add request info
    tags['user_agent'] = request.META.get('HTTP_USER_AGENT', '')
    return tags

def filter_tracking(request):
    # type: (HttpRequest) -> bool
    # Skip internal endpoints
    return not request.path.startswith(('/health', '/metrics', '/admin'))

def clean_tags(tags):
    # type: (Dict[str, Any]) -> Dict[str, Any]
    # Remove sensitive data
    tags.pop('user_agent', None)
    return tags

POSTHOG_MW_EXTRA_TAGS = add_request_context
POSTHOG_MW_REQUEST_FILTER = filter_tracking
POSTHOG_MW_TAG_MAP = clean_tags
POSTHOG_MW_CAPTURE_EXCEPTIONS = True

All events captured within the request context automatically include the configured tags and are associated with the session and user identified from the request headers or Django authentication.

The middleware supports both sync (WSGI) and async (ASGI) Django applications. In async mode, it uses Django's request.auser() API when available to avoid synchronous user access.

Next steps

For any technical questions for how to integrate specific PostHog features into Django (such as analytics, feature flags, A/B testing, etc.), have a look at our Python SDK docs (/docs/libraries/python.md).

Alternatively, the following tutorials can help you get started:

  • Setting up Django analytics, feature flags, and more (/tutorials/django-analytics.md)
  • How to set up A/B tests in Django (/tutorials/django-ab-tests.md)

Supported versions

These docs cover version 7.x of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on 7.x.x and higher — pin to the 6.x line with pip install 'posthog<7', where 6.9.3 is the final release.

Everything on this page works the same way on 6.9.3. Event capture, the context API (new_context, identify_context, set_context_session), and PosthogContextMiddleware are identical on 6.9.3 and 7.0.0 — 7.0.0 only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the X-POSTHOG-DISTINCT-ID header and falling back to the authenticated user, which behaves the same across both lines.

Later 7.x releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and set_context_device_id. They also changed the middleware's own captured properties: 7.x sends the request IP as $ip, where 6.9.3 sends it as $ip_address, and 7.x additionally captures $request_path, $raw_user_agent, and the authenticated user's email.

Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/dotnet.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

.NET

This is an optional library you can install if you're working with .NET Core. It uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your web app or other server side application that needs performance.

Installation

The PostHog package supports any .NET platform that targets .NET Standard 2.1 or .NET 8+, including MAUI, Blazor, and console applications. The PostHog.AspNetCore package provides additional conveniences for ASP.NET Core applications such as streamlined registration, request-scoped caching, and integration with .NET Feature Management.

Note: We actively test with ASP.NET Core. Other platforms should work but haven't been specifically tested. If you encounter issues, please report them on GitHub.

Not supported: Classic UWP (requires .NET Standard 2.0 only). Microsoft has deprecated UWP in favor of the Windows App SDK. For Unity projects, see our dedicated Unity SDK (/docs/libraries/unity.md).

Terminal

dotnet add package PostHog.AspNetCore

In your Program.cs (or Startup.cs for ASP.NET Core 2.x) file, add the following code:

C#

using PostHog;

var builder = WebApplication.CreateBuilder(args);

// Add PostHog to the dependency injection container as a singleton.
builder.AddPostHog();

Make sure to configure PostHog with your project token, instance address, and optional personal API key. For example, in appsettings.json:

JSON

{
  "PostHog": {
    "ProjectToken": "<ph_project_token>",
    "HostUrl": "https://us.i.posthog.com"
  }
}

Note: If the host is not specified, the default host https://us.i.posthog.com is used.

Use a secrets manager to store your personal API key. For example, when developing locally you can use the UserSecrets feature of the dotnet CLI:

Terminal

dotnet user-secrets init
dotnet user-secrets set "PostHog:PersonalApiKey" "phx_..."

You can find your project token and instance address in the project settings page in PostHog.

Working with .NET Feature Management

PostHog.AspNetCore supports .NET Feature Management. This enables you to use the <feature /> tag helper and the FeatureGateAttribute in your ASP.NET Core applications to gate access to certain features using PostHog feature flags.

To use feature flags with the .NET Feature Management library, you'll need to implement the IPostHogFeatureFlagContextProvider interface. The quickest way to do that is to inherit from the PostHogFeatureFlagContextProvider class and override the GetDistinctId and GetFeatureFlagOptionsAsync methods.

C#

public class MyFeatureFlagContextProvider(IHttpContextAccessor httpContextAccessor)
    : PostHogFeatureFlagContextProvider
{
    protected override string? GetDistinctId()
        => httpContextAccessor.HttpContext?.User.Identity?.Name;

    protected override ValueTask<FeatureFlagOptions> GetFeatureFlagOptionsAsync()
    {
        // In a real app, you might get this information from a
        // database or other source for the current user.
        return ValueTask.FromResult(
            new FeatureFlagOptions
            {
                PersonProperties = new Dictionary<string, object?>
                {
                    ["email"] = "some-test@example.com"
                },
                OnlyEvaluateLocally = true
            });
    }
}

Then, register your implementation in Program.cs (or Startup.cs):

C#

var builder = WebApplication.CreateBuilder(args);
builder.AddPostHog(options => {
    options.UseFeatureManagement<MyFeatureFlagContextProvider>();
});

With this in place, you can now use feature tag helpers in your Razor views:

HTML

<feature name="awesome-new-feature">
    <p>This is the new feature!</p>
</feature>
<feature name="awesome-new-feature" negate="true">
    <p>Sorry, no awesome new feature for you.</p>
</feature>

Multivariate feature flags are also supported:

HTML

<feature name="awesome-new-feature" value="variant-a">
    <p>This is the new feature variant A!</p>
</feature>
<feature name="awesome-new-feature" value="variant-b">
    <p>This is the new feature variant B!</p>
</feature>

You can also use the FeatureGateAttribute to gate access to controllers or actions:

C#

[FeatureGate("awesome-new-feature")]
public class NewFeatureController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

Using the core package without ASP.NET Core

If you're not using ASP.NET Core (for example, in a console application, MAUI app, or Blazor WebAssembly), install the PostHog package instead of PostHog.AspNetCore. This package has no ASP.NET Core dependencies and can be used in any .NET project targeting .NET Standard 2.1 or .NET 8+.

Terminal

dotnet add package PostHog

The PostHogClient class must be implemented as a singleton in your project. For PostHog.AspNetCore, this is handled by the builder.AddPostHog(); method. For the PostHog package, you can do the following if you're using dependency injection:

C#

builder.Services.AddPostHog();

If you're not using a builder (such as in a console application), you can do the following:

C#

using PostHog;

var services = new ServiceCollection();
services.AddPostHog();
var serviceProvider = services.BuildServiceProvider();
var posthog = serviceProvider.GetRequiredService<IPostHogClient>();

The AddPostHog methods accept an optional Action<PostHogOptions> parameter that you can use to configure the client.

If you're not using dependency injection, you can create a static instance of the PostHogClient class and use that everywhere in your project:

C#

using PostHog;

public static readonly PostHogClient PostHog = new(new PostHogOptions {
    ProjectToken = "<ph_project_token>",
    HostUrl = new Uri("https://us.i.posthog.com"),
    PersonalApiKey = Environment.GetEnvironmentVariable(
      "PostHog__PersonalApiKey")
});

Debug mode

If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening.

To see detailed logging, set the log level to Debug or Trace in appsettings.json:

JSON

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "PostHog": "Trace"
    }
  },
  ...
}

Identifying users

Identifying users is required. Backend events need a distinct_id that matches the ID your frontend uses when calling posthog.identify(). Without this, backend events are orphaned — they can't be linked to frontend event captures, session replays (/docs/session-replay.md), LLM traces (/docs/ai-engineering.md), or error tracking (/docs/error-tracking.md).

See our guide on identifying users (/docs/getting-started/identify-users.md) for how to set this up.

Capturing events

You can send custom events using capture:

C#

posthog.Capture("distinct_id_of_the_user", "user_signed_up");

Tip: We recommend using a [object] [verb] format for your event names, where [object] is the entity that the behavior relates to, and [verb] is the behavior itself. For example, project created, user signed up, or invite sent.

Setting event properties

Optionally, you can include additional information with the event by including a properties (/docs/data/events.md#event-properties) object:

C#

posthog.Capture(
    "distinct_id_of_the_user",
    "user_signed_up",
    properties: new() {
        ["login_type"] = "email",
        ["is_free_trial"] = "true"
    }
);
Sending page views

If you're aiming for a backend-only implementation of PostHog and won't be capturing events from your frontend, you can send $pageview events from your backend like so:

C#

using PostHog;
using Microsoft.AspNetCore.Http.Extensions;

posthog.CapturePageView(
    "distinct_id_of_the_user",
    HttpContext.Request.GetDisplayUrl());

Request context

For ASP.NET Core apps using PostHog.AspNetCore, add request context middleware before routes that call PostHog. This reads incoming PostHog tracing headers and attaches request metadata to captures, exceptions, and feature flag evaluation inside the request.

Program.cs

using PostHog;
using PostHog.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.AddPostHog();

var app = builder.Build();

app.UsePostHogRequestContext();

If you're using PostHog JS (/docs/libraries/js.md) on the frontend, configure tracing_headers (/docs/libraries/js/config.md#tracing-headers) for your ASP.NET Core backend hostname so browser requests include the session and distinct ID headers.

The middleware reads X-PostHog-Distinct-Id and X-PostHog-Session-Id as request-scoped analytics context. It also adds request metadata such as $current_url, $request_method, $request_path, $user_agent, and $ip. Explicit distinct IDs and event properties always override request context.

Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side decisions, pass an authenticated distinct ID explicitly. You can ignore tracing headers while still collecting request metadata:

C#

app.UsePostHogRequestContext(options =>
{
    options.UseTracingHeaders = false;
});

Request-context overloads like posthog.Capture("checkout started") and posthog.EvaluateFlagsAsync() use the current request distinct ID when one is available.

Error tracking

You can manually capture exceptions using CaptureException. This sends a $exception event with stack frames, inner exceptions, aggregate exceptions, source context when available, and .NET runtime metadata.

File names, line numbers, and source context depend on debug information already available from the captured .NET stack trace. PostHog doesn't support uploading .NET PDB files yet, so production builds without runtime-accessible debug information may show less detailed stack frames.

C#

try
{
    ProcessOrder(orderId);
}
catch (Exception exception)
{
    posthog.CaptureException(exception, "user_distinct_id");
}

Add custom properties to include request, tenant, or domain context:

C#

posthog.CaptureException(
    exception,
    "user_distinct_id",
    new Dictionary<string, object>
    {
        ["order_id"] = orderId,
        ["environment"] = "production",
    }
);

For the full setup guide, see the .NET error tracking installation docs (/docs/error-tracking/installation/dotnet.md).

Automatic exception capture is not available in the .NET SDK yet.

Logs

PostHog Logs (/docs/logs.md) doesn't use this SDK. Logs are ingested over OpenTelemetry, so you attach an OTLP exporter to the standard ILogger pipeline instead — see the .NET logs installation guide (/docs/logs/installation/dotnet.md).

Person profiles and properties

The .NET SDK captures identified events by default. These create person profiles (/docs/data/persons.md). To set person properties (/docs/product-analytics/person-properties.md) in these profiles, include them when capturing an event:

C#

posthog.Capture(
    "distinct_id",
    "event_name",
    personPropertiesToSet: new() { ["name"] = "Max Hedgehog" },
    personPropertiesToSetOnce: new() { ["initial_url"] = "/blog" }
);

For more details on the difference between $set and $set_once, see our person properties docs (/docs/product-analytics/person-properties.md#what-is-the-difference-between-set-and-set_once).

To capture anonymous events (/docs/data/anonymous-vs-identified-events.md) without person profiles, set the event's $process_person_profile property to false:

C#

posthog.Capture(
    "distinct_id",
    "event_name",
    properties: new() {
        ["$process_person_profile"] = false
    }
)

Alias

Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend.

In this case, you can use alias to assign another distinct ID to the same user.

C#

await posthog.AliasAsync("current_distinct_id", "new_distinct_id");

We strongly recommend reading our docs on alias (/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method.

Group analytics

Group analytics allows you to associate an event with a group (e.g. teams, organizations, etc.). Read the group analytics (/docs/product-analytics/group-analytics.md) guide for more information.

Note: This is a paid feature and is not available on the open-source or free cloud plan. Learn more on our pricing page (/pricing.md).

To capture an event and associate it with a group, add the groups argument to your Capture call:

C#

posthog.Capture(
    "user_distinct_id",
    "some_event",
    groups: [new Group("company", "company_id_in_your_db")]);

Update properties on a group, use the GroupIdentifyAsync method:

C#

await posthog.GroupIdentifyAsync(
    type: "company",
    key: "company_id_in_your_db",
    name: "Awesome Inc.",
    properties: new()
    {
        ["employees"] = 11
    }
);

The name is a special property which is used in the PostHog UI for the name of the group. If you don't specify a name property, the group ID will be used instead.

Feature flags

PostHog's feature flags (/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them.

There are two steps to implement feature flags in .NET:

Step 1: Evaluate flags once

Call EvaluateFlagsAsync() once for the user, then read values from the returned snapshot.

Boolean feature flags

C#

var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user");

if (flags.IsEnabled("flag-key"))
{
    // Do something differently for this user
    // Optional: fetch the payload
    var matchedPayload = flags.GetFlagPayload("flag-key");
}
Multivariate feature flags

C#

var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user");

var enabledVariant = flags.GetFlag("flag-key")?.VariantKey;

if (enabledVariant == "variant-key") // replace "variant-key" with the key of your variant
{
    // Do something differently for this user
    // Optional: fetch the payload
    var matchedPayload = flags.GetFlagPayload("flag-key");
}

flags.GetFlag() returns a nullable FeatureFlag object. Check VariantKey for multivariate flags and IsEnabled for boolean flags. It returns null when the flag wasn't returned by the evaluation.

Note: posthog.IsFeatureEnabledAsync(), posthog.GetFeatureFlagAsync(), and Capture(..., sendFeatureFlags: true, ...) still work during the migration period, but they're deprecated. Prefer EvaluateFlagsAsync() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to Capture()

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

C#

var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user");

if (flags.IsEnabled("flag-key"))
{
    // Do something differently for this user
}

posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: null,
    groups: null,
    flags: flags
);

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

C#

// Attach only flags accessed with IsEnabled() or GetFlag() before this call
posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: null,
    groups: null,
    flags: flags.OnlyAccessed()
);

// Attach only specific flags
posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: null,
    groups: null,
    flags: flags.Only("checkout-flow", "new-dashboard")
);
Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

C#

posthog.Capture(
    "distinct_id_of_your_user",
    "event_name",
    properties: new()
    {
        // Replace feature-flag-key with your flag key and "variant-key" with the key of your variant
        ["$feature/feature-flag-key"] = "variant-key",
    }
);
Evaluating only specific flags

By default, EvaluateFlagsAsync() evaluates every flag for the user. If you only need a few flags, pass FlagKeysToEvaluate to request only those flags:

C#

var flags = await posthog.EvaluateFlagsAsync(
    "distinct_id_of_your_user",
    options: new AllFeatureFlagsOptions
    {
        FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" },
    }
);
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With EvaluateFlagsAsync(), the SDK sends this event when you call flags.IsEnabled() or flags.GetFlag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.GetFlagPayload() doesn't send $feature_flag_called events and doesn't count as an access for OnlyAccessed().

Advanced: Overriding server properties

Sometimes, you may want to evaluate feature flags using person properties (/docs/product-analytics/person-properties.md), groups (/docs/product-analytics/group-analytics.md), or group properties that haven't been ingested yet, or were set incorrectly earlier.

You can provide properties to evaluate the flag with by using the person properties, groups, and group properties arguments. PostHog will then use these values to evaluate the flag, instead of any properties currently stored on your PostHog server.

For example:

C#

var flags = await posthog.EvaluateFlagsAsync(
    "distinct_id_of_the_user",
    options: new AllFeatureFlagsOptions
    {
        PersonProperties = new()
        {
            ["property_name"] = "value",
        },
        Groups = new()
        {
            new Group("your_group_type", "your_group_id")
            {
                ["group_property_name"] = "value",
            },
            new Group("another_group_type", "another_group_id")
            {
                ["group_property_name"] = "another value",
            },
        },
    }
);

if (flags.IsEnabled("flag-key"))
{
    // Do something differently for this user
}
Overriding GeoIP properties

By default, a user's GeoIP properties are set using the IP address they use to capture events on the frontend. You may want to override the these properties when evaluating feature flags. A common reason to do this is when you're not using PostHog on your frontend, so the user has no GeoIP properties.

You can override GeoIP properties by including them in the person_properties parameter when evaluating feature flags. This is useful when you're evaluating flags on your backend and want to use the client's location instead of your server's location.

The following GeoIP properties can be overridden:

  • $geoip_country_code
  • $geoip_country_name
  • $geoip_city_name
  • $geoip_city_confidence
  • $geoip_continent_code
  • $geoip_continent_name
  • $geoip_latitude
  • $geoip_longitude
  • $geoip_postal_code
  • $geoip_subdivision_1_code
  • $geoip_subdivision_1_name
  • $geoip_subdivision_2_code
  • $geoip_subdivision_2_name
  • $geoip_subdivision_3_code
  • $geoip_subdivision_3_name
  • $geoip_time_zone

Simply include any of these properties in the person_properties parameter alongside your other person properties when calling feature flags.

Evaluation contexts

Configure evaluation contexts so this SDK only evaluates flags intended for the matching application, platform, or product area. For ASP.NET Core apps using PostHog.AspNetCore, add them to the PostHog configuration section:

JSON

{
    "PostHog": {
        "ProjectToken": "<ph_project_token>",
        "HostUrl": "https://us.i.posthog.com",
        "EvaluationContexts": ["main-app", "api", "backend"]
    }
}

For code-based configuration, set EvaluationContexts on PostHogOptions:

C#

var posthog = new PostHogClient(new PostHogOptions
{
    ProjectToken = "<ph_project_token>",
    HostUrl = new Uri("https://us.i.posthog.com"),
    EvaluationContexts = ["main-app", "api", "backend"],
});

Remote /flags requests from EvaluateFlagsAsync() include evaluation_contexts when configured.

For more details, see the evaluation contexts guide (/docs/feature-flags/evaluation-contexts.md).

Local evaluation

Evaluating feature flags requires making a request to PostHog for each flag. However, you can improve performance by evaluating flags locally. Instead of making a request for each flag, PostHog will periodically request and store feature flag definitions locally, enabling you to evaluate flags without making additional requests.

It is best practice to use local evaluation flags when possible, since this enables you to resolve flags faster and with fewer API calls.

For details on how to implement local evaluation, see our local evaluation guide (/docs/feature-flags/local-evaluation.md).

Experiments (A/B tests)

Since experiments (/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code:

C#

var flags = await posthog.EvaluateFlagsAsync("user_distinct_id");
var variant = flags.GetFlag("experiment-feature-flag-key")?.VariantKey;

if (variant == "variant-name")
{
    // Do something
}

It's also possible to run experiments without using feature flags (/docs/experiments/running-experiments-without-feature-flags.md).

AI observability

PostHog.AI adds AI observability (/docs/ai-observability.md) for .NET applications using OpenAI or Azure OpenAI. It is currently pre-release, so expect breaking changes before a stable release.

For installation instructions, see the OpenAI guide for .NET (/docs/ai-observability/installation/openai.md#net-support) or the Azure OpenAI guide for .NET (/docs/ai-observability/installation/azure-openai.md#net-support).

GeoIP properties

The posthog-dotnet library disregards the server IP, does not add the GeoIP properties, and does not use the values for feature flag evaluations.

Serverless environments (Azure Functions/Render/Lambda/...)

By default, the library buffers events before sending them to the /batch endpoint for better performance. This can lead to lost events in serverless environments if the .NET process is terminated by the platform before the buffer is fully flushed.

To avoid this, call await posthog.FlushAsync() after processing every request by adding it as a middleware to your server. This allows posthog.Capture() to remain asynchronous for better performance.

Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/elixir.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Elixir Feature Flags installation

This library was built by the community but it's being maintained by the PostHog core team since v1.0.0. Thank you to Nick Kezhaya for building it originally. Thank you to Alex Martsinovich for contributing v2.0.0.

The package can be installed by adding posthog to your list of dependencies in mix.exs:

Elixir

def deps do
  [
    {:posthog, "~> 2.0"}
  ]
end
Configuration

config/config.exs

config :posthog,
  enable: true,
  api_host: "https://us.i.posthog.com",
  api_key: "<ph_project_token>",
  in_app_otp_apps: [:my_app]

You can see all the available configuration options in the PostHog.Config module.

Optionally, you might want to enable the Plug integration to attach request metadata and tracing context in Plug-based applications including Phoenix. You still need to capture events explicitly with PostHog.capture/2 or PostHog.capture/3.

Development/Test mode

For a test environment, you can pass in test_mode: true value to the config. This causes events to be dropped instead of sent to PostHog.

Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/flask.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Flask

PostHog makes it easy to get data about traffic and usage of your Flask app. Integrating PostHog enables analytics, custom events capture, feature flags, error tracking, and more.

This guide walks you through integrating PostHog into your Flask app using the Python SDK (/docs/libraries/python.md).

These docs cover version 7.x of the Python SDK, which requires Python 3.10 or higher. On Python 3.9? See supported versions (#supported-versions).

Installation

To start, run pip install posthog to install PostHog’s Python SDK.

Then, initialize PostHog where you'd like to use it. For example, here's how to capture an event in a simple route:

app.py

from flask import Flask
from posthog import Posthog

app = Flask(__name__)

posthog = Posthog(
    '<ph_project_token>',
    host='https://us.i.posthog.com',
)

@app.route('/api/dashboard', methods=['POST'])
def api_dashboard():
    posthog.capture(
        'dashboard_api_called',
        distinct_id='distinct_id_of_your_user',
    )
    return '', 204

You can find your project token and instance address in your project settings.

Identifying users

Identifying users is required. Backend events need a distinct_id to associate events with the correct user.

In Python, you can do this through a context. All event captures in the same context will be tagged automatically with the correct distinct_id. Typically, you would set a fresh context and identify at the top of each route.

Python

from posthog import new_context, identify_context, capture

@app.get("/foo")
def foo(current_user: User = Depends(get_current_user)):
    with new_context(): # Set context at the top of a route
        identify_context(current_user.id)
        capture("foo_viewed")
    return {"status": "ok"}

When possible, write a small piece of middleware that resolves your authenticated user, wrap a context around the request, and identifies it. Every capture() downstream is then attributed automatically. The SDK's Django middleware does this automatically and you can replicate it when using the plain Python SDK.

Request contexts

Use contexts (/docs/libraries/python.md#contexts) to share identity, session IDs, and tags across multiple captures during a request.

If you're using PostHog JavaScript Web (/docs/libraries/js.md) on the frontend, configure tracing_headers (/docs/libraries/js/config.md#tracing-headers) for your Flask backend hostname so browser requests include the session and distinct ID headers.

Then read the incoming headers in your Flask request handler. Tracing headers are client-controlled analytics context, not authentication or authorization, so prefer your authenticated user ID when one is available:

Python

from flask import request, session
from posthog import identify_context, set_context_session, tag

@app.route('/api/dashboard', methods=['POST'])
def api_dashboard():
    with posthog.new_context(fresh=True):
        distinct_id = session.get('user_id') or request.headers.get('X-POSTHOG-DISTINCT-ID')
        if distinct_id:
            identify_context(str(distinct_id))

        session_id = request.headers.get('X-POSTHOG-SESSION-ID')
        if session_id:
            set_context_session(session_id)

        tag('$current_url', request.url)
        tag('$request_method', request.method)
        tag('$request_path', request.path)

        posthog.capture('dashboard_api_called')

    return '', 204

Events captured without a context or explicit distinct_id are sent as anonymous events (/docs/data/anonymous-vs-identified-events.md) with an auto-generated distinct_id. See the Python SDK docs (/docs/libraries/python.md#person-profiles-and-properties) for more details.

Error tracking

Flask has built-in error handlers. This means PostHog’s default exception autocapture won’t work and we need to manually capture errors instead using capture_exception():

Python

from flask import Flask, jsonify
from posthog import Posthog

app = Flask(__name__)
posthog = Posthog('<ph_project_token>', host='https://us.i.posthog.com')

@app.errorhandler(Exception)
def handle_exception(e):
    # Capture methods, including capture_exception, return the UUID of the captured event,
    # which you can use to find specific errors users encountered
    event_id = posthog.capture_exception(e)

    # You can show the event ID to your user, and ask them to include it in bug reports
    response = jsonify({'message': str(e), 'error_id': event_id})
    response.status_code = 500
    return response

Next steps

For any technical questions for how to integrate specific PostHog features into Flask (such as analytics, feature flags, A/B testing, etc.), have a look at our Python SDK docs (/docs/libraries/python.md).

Alternatively, the following tutorials can help you get started:

  • How to set up analytics in Python and Flask (/tutorials/python-analytics.md)
  • How to set up feature flags in Python and Flask (/tutorials/python-feature-flags.md)
  • How to set up A/B tests in Python and Flask (/tutorials/python-ab-testing.md)

Supported versions

These docs cover version 7.x of the PostHog Python SDK, which requires Python 3.10 or higher. Python 3.9 is no longer supported on 7.x.x and higher — pin to the 6.x line with pip install 'posthog<7', where 6.9.3 is the final release.

Everything on this page works the same way on 6.9.3. Event capture, the context API (new_context, identify_context, set_context_session), and PosthogContextMiddleware are identical on 6.9.3 and 7.0.0 — 7.0.0 only dropped Python 3.9 and bumped the optional LLM provider SDKs. That includes the middleware identifying the request context from the X-POSTHOG-DISTINCT-ID header and falling back to the authenticated user, which behaves the same across both lines.

Later 7.x releases add what the 6.x line does not receive, such as the Celery integration, tracing header sanitization, and set_context_device_id. They also changed the middleware's own captured properties: 7.x sends the request IP as $ip, where 6.9.3 sends it as $ip_address, and 7.x additionally captures $request_path, $raw_user_agent, and the authenticated user's email.

Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/flutter.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Flutter Feature Flags installation

  1. 1

    Install the package

    Required

    Add the PostHog Flutter SDK to your pubspec.yaml:

    pubspec.yaml

    posthog_flutter: ^5.24.0
  2. 2

    Platform setup

    Required

    Tab

    Add these values to your AndroidManifest.xml:

    android/app/src/main/AndroidManifest.xml

    <application>
      <activity>
        [...]
      </activity>
      <meta-data android:name="com.posthog.posthog.PROJECT_TOKEN" android:value="<ph_project_token>" />
      <meta-data android:name="com.posthog.posthog.POSTHOG_HOST" android:value="https://us.i.posthog.com" />
      <meta-data android:name="com.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS" android:value="true" />
      <meta-data android:name="com.posthog.posthog.DEBUG" android:value="true" />
    </application>

    Update the minimum Android SDK version to 21 in android/app/build.gradle:

    android/app/build.gradle

    defaultConfig {
      minSdkVersion 23
      // rest of your config
    }

    Tab

    Add these values to your Info.plist:

    ios/Runner/Info.plist

    <dict>
      [...]
      <key>com.posthog.posthog.PROJECT_TOKEN</key>
      <string><ph_project_token></string>
      <key>com.posthog.posthog.POSTHOG_HOST</key>
      <string>https://us.i.posthog.com</string>
      <key>com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS</key>
      <true/>
      <key>com.posthog.posthog.DEBUG</key>
      <true/>
    </dict>

    Update the minimum platform version to iOS 13.0 in your Podfile:

    Podfile

    platform :ios, '13.0'
    # rest of your config

    Tab

    Add these values in index.html:

    web/index.html

    <!DOCTYPE html>
    <html>
      <head>
        ...
        <script>
          !function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}p||((p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",p.onerror=function(){p=null},(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r));var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],Object.defineProperty(u,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e}}),Object.defineProperty(u.people,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(){return u.toString(1)+".people (stub)"}}),o="init capture register register_once register_for_session unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group identify setPersonProperties setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags resetGroups onFeatureFlags addFeatureFlagsHandler onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
          posthog.init('<ph_project_token>', {
              api_host: 'https://us.i.posthog.com',
              defaults: '2026-05-30',
          })
        </script>
      </head>
      <body>
        ...
      </body>
    </html>
  3. 3

    Send events

    Recommended

    Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration:

    Dart

    import 'package:posthog_flutter/posthog_flutter.dart';
    
    await Posthog().capture(
        eventName: 'button_clicked',
        properties: {
          'button_name': 'signup'
        }
    );
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    Dart

    final isMyFlagEnabled = await Posthog().isFeatureEnabled('flag-key');
    if (isMyFlagEnabled) {
        // Do something differently for this user
        // Optional: fetch the payload
        final matchedFlagPayload = (await Posthog().getFeatureFlagResult('flag-key'))?.payload;
    }
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    Dart

    final enabledVariant = await Posthog().getFeatureFlag('flag-key');
    if (enabledVariant == 'variant-key') { // replace 'variant-key' with the key of your variant
        // Do something differently for this user
        // Optional: fetch the payload
        final matchedFlagPayload = (await Posthog().getFeatureFlagResult('flag-key'))?.payload;
    }
  6. 6

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  7. 7

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/go.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Go Feature Flags installation

  1. 1

    Install the package

    Required

    Install the PostHog Go library:

    Terminal

    go get "github.com/posthog/posthog-go"
  2. 2

    Configure PostHog

    Required

    Initialize the PostHog client with your project token and host:

    main.go

    package main
    
    import (
        "github.com/posthog/posthog-go"
    )
    
    func main() {
        client, _ := posthog.NewWithConfig("<ph_project_token>", posthog.Config{Endpoint: "https://us.i.posthog.com"})
        defer client.Close()
    }
  3. 3

    Send events

    Recommended

    Once installed, you can manually send events to test your integration:

    Go

    client.Enqueue(posthog.Capture{
        DistinctId: "user_123",
        Event: "button_clicked",
        Properties: posthog.NewProperties().
            Set("button_name", "signup"),
    })
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{
        Key:        "flag-key",
        DistinctId: "distinct_id_of_your_user",
    })
    if err != nil {
        // Handle error (e.g. capture error and fallback to default behaviour)
    }
    if isMyFlagEnabled == true {
        // Do something differently for this user
    }
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{
        Key:        "flag-key",
        DistinctId: "distinct_id_of_your_user",
    })
    if err != nil {
        // Handle error (e.g. capture error and fallback to default behaviour)
    }
    if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant
        // Do something differently for this user
    }
  6. 6

    Include feature flag information in events

    Required

    If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

    Note: This step is only required for events captured using our server-side SDKs or API.

    Set SendFeatureFlags to true in your capture call:

    Go

    client.Enqueue(posthog.Capture{
        DistinctId: "distinct_id_of_your_user",
        Event:      "event_name",
        SendFeatureFlags: true,
    })

    Include $feature property

    Include the $feature/feature_flag_name property in your event properties:

    Go

    client.Enqueue(posthog.Capture{
        DistinctId: "distinct_id_of_your_user",
        Event:      "event_name",
        Properties: posthog.NewProperties().
            Set("$feature/feature-flag-key", "variant-key"), // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
    })
  7. 7

    Override server properties

    Optional

    Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with:

    enabledVariant, err := client.GetFeatureFlag(
        FeatureFlagPayload{
            Key:        "flag-key",
            DistinctId: "distinct_id_of_the_user",
            Groups: posthog.NewGroups().
                Set("your_group_type", "your_group_id").
                Set("another_group_type", "your_group_id"),
            PersonProperties: posthog.NewProperties().
                Set("property_name", "value"),
            GroupProperties: map[string]map[string]interface{}{
                "your_group_type": {
                    "group_property_name": "value",
                },
                "another_group_type": {
                    "group_property_name": "value",
                },
            },
        },
    )
  8. 8

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  9. 9

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/ios.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

iOS Feature Flags installation

  1. 1

    Install dependency

    Required

    Install via Swift Package Manager:

    Package.swift

    dependencies: [
      .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.56.0")
    ]

    Or add PostHog to your Podfile:

    Podfile

    pod "PostHog", "~> 3.56"
  2. 2

    Configure PostHog

    Required

    Initialize PostHog in your AppDelegate:

    AppDelegate.swift

    import Foundation
    import PostHog
    import UIKit
    
    class AppDelegate: NSObject, UIApplicationDelegate {
        func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
            let POSTHOG_PROJECT_TOKEN = "<ph_project_token>"
            let POSTHOG_HOST = "https://us.i.posthog.com"
    
            let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST)
            PostHogSDK.shared.setup(config)
    
            return true
        }
    }
  3. 3

    Send events

    Recommended

    Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration:

    Swift

    PostHogSDK.shared.capture("button_clicked", properties: ["button_name": "signup"])
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    Swift

    let isMyFlagEnabled = PostHogSDK.shared.isFeatureEnabled("flag-key")
    if isMyFlagEnabled {
        // Do something differently for this user
        // Optional: fetch the payload
        let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagResult("flag-key")?.payload
    }
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    Swift

    let enabledVariant = PostHogSDK.shared.getFeatureFlag("flag-key")
    if enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant
        // Do something differently for this user
        // Optional: fetch the payload
        let matchedFlagPayload = PostHogSDK.shared.getFeatureFlagResult("flag-key")?.payload
    }
  6. 6

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  7. 7

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/java.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Java Feature Flags installation

The best way to install the PostHog Java SDK is with a build system like Gradle or Maven. This ensures you can easily upgrade to the latest versions.

Look up the latest version of com.posthog.posthog-server.

Gradle

All you need to do is add the posthog-server module to your build.gradle:

build.gradle

dependencies {
  implementation 'com.posthog:posthog-server:2.+'
}
Maven

All you need to do is add the posthog-server module to your pom.xml:

pom.xml

<dependency>
  <groupId>com.posthog</groupId>
  <artifactId>posthog-server</artifactId>
  <version>LATEST</version>
</dependency>
Other

See com.posthog.posthog-server in the Maven Central Repository. Clicking on the latest version shows you options for adding dependencies for other build systems.

Setup

Java

import com.posthog.server.PostHog;
import com.posthog.server.PostHogConfig;
import com.posthog.server.PostHogInterface;

class Sample {
  private static final String POSTHOG_API_KEY = "<ph_project_token>";
  private static final String POSTHOG_HOST = "https://us.i.posthog.com";

  public static void main(String args[]) {
    PostHogConfig config = PostHogConfig
            .builder(POSTHOG_API_KEY)
            .host(POSTHOG_HOST)
            .build();

    PostHogInterface posthog = PostHog.with(config);

    posthog.flush(); // send any remaining events
    posthog.close(); // shut down the client
  }
}

Integrating with Spring

To see how to integrate the PostHog SDK with Spring, check out this sample project.

Debug mode

If you're not seeing the expected events being captured, or the feature flags being evaluated, you can enable debug mode to see what's happening.

To see detailed logging, set the debug configuration option to true.

Java

PostHogConfig config = PostHogConfig
          .builder(POSTHOG_API_KEY)
          .host(POSTHOG_HOST)
          .debug(true)
          .build();
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/laravel.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Laravel

PostHog integrates with Laravel through the PostHog PHP SDK (/docs/libraries/php.md). This page covers Laravel-specific setup. For SDK features such as event capture, identifying users, feature flags, group analytics, and configuration options, see the PHP SDK docs (/docs/libraries/php.md).

Installation

Install the PHP SDK as described in the PHP installation guide (/docs/libraries/php.md#installation), then add your project token and host to .env:

.env

POSTHOG_API_KEY=<ph_project_token>
POSTHOG_HOST=https://us.i.posthog.com

Add PostHog to Laravel's services config:

config/services.php

'posthog' => [
    'api_key' => env('POSTHOG_API_KEY'),
    'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'),
],

Initialize PostHog in the boot method of app/Providers/AppServiceProvider.php:

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use PostHog\PostHog;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if (! config('services.posthog.api_key')) {
            return;
        }

        PostHog::init(
            config('services.posthog.api_key'),
            [
                'host' => config('services.posthog.host'),
            ]
        );
    }
}

Request context middleware

Client SDKs such as PostHog JS (/docs/libraries/js.md) can send tracing headers to your Laravel backend. Configure tracing_headers (/docs/libraries/js/config.md#tracing-headers) for your Laravel backend hostname so browser requests include the session and distinct ID headers.

The PHP SDK can read X-PostHog-Distinct-Id and X-PostHog-Session-Id headers and apply them to events captured during the request. Tracing headers are client-controlled analytics context, not authentication or authorization. For security-sensitive server-side events or decisions, pass an authenticated distinctId explicitly, such as auth()->id(). For the lower-level context APIs, see the PHP request context docs (/docs/libraries/php.md#request-context).

Add middleware like this:

app/Http/Middleware/PostHogRequestContext.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use PostHog\PostHog;
use Symfony\Component\HttpFoundation\Response;

final class PostHogRequestContext
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! config('services.posthog.api_key')) {
            return $next($request);
        }

        $context = PostHog::contextFromHeaders($request->headers->all());

        $context['properties'] = array_merge(
            $context['properties'] ?? [],
            array_filter([
                '$current_url' => $request->fullUrl(),
                '$request_method' => $request->method(),
                '$request_path' => $request->getPathInfo(),
                '$user_agent' => $request->userAgent(),
                '$ip' => $request->ip(),
            ], static fn ($value): bool => $value !== null && $value !== '')
        );

        return PostHog::withContext(
            $context,
            static fn (): Response => $next($request),
            ['fresh' => true]
        );
    }
}

Register this middleware using your Laravel version's normal middleware registration.

Error tracking in Laravel

The PHP SDK supports error tracking (/docs/libraries/php.md#error-tracking), but Laravel handles most request exceptions before they become uncaught PHP exceptions. Capture Laravel-reported exceptions explicitly.

In Laravel 11 and later, add a report callback in bootstrap/app.php:

bootstrap/app.php

use Illuminate\Foundation\Configuration\Exceptions;
use PostHog\PostHog;
use Throwable;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->report(function (Throwable $e): void {
        if (! config('services.posthog.api_key')) {
            return;
        }

        PostHog::captureException(
            $e,
            auth()->id() !== null ? (string) auth()->id() : null,
            [
                '$current_url' => request()->fullUrl(),
                '$request_method' => request()->method(),
            ]
        );
    });
})

For older Laravel versions, call PostHog::captureException() from your exception handler's report method.

Long-running processes

In normal PHP request lifecycles, queued events flush when the client is destroyed. In long-running Laravel processes such as queue workers, Horizon, or Octane, call PostHog::flush() after capturing important events or at the end of a job/request.

If you prefer immediate delivery in queue workers, configure the PHP SDK with batch_size set to 1 for those workers:

PHP

PostHog::init(
    '<ph_project_token>',
    [
        'host' => config('services.posthog.host'),
        'batch_size' => 1,
    ]
);

Next steps

See the PHP SDK docs (/docs/libraries/php.md) for usage examples and the full API reference.

Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/next-js.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Next.js

PostHog makes it easy to get data about traffic and usage of your Next.js app. Integrating PostHog into your site enables analytics about user behavior, custom events capture, session recordings, feature flags, and more.

This guide walks you through integrating PostHog into your Next.js app using the React (/docs/libraries/react.md) and the Node.js (/docs/libraries/node.md) SDKs.

You can see a working example of this integration in our Next.js demo app.

Next.js has both client and server-side rendering, as well as pages and app routers. We'll cover all of these options in this guide.

Try @posthog/next (pre-release): A simplified Next.js integration with synchronized client/server identity, server-side flag bootstrapping, and a built-in API proxy. Read the setup guide → (/docs/libraries/next-js/posthog-next.md)

Prerequisites

To follow this guide along, you need:

  1. A PostHog instance (either Cloud or self-hosted (/docs/self-host.md))
  2. A Next.js application

Beta: integration via LLM

Install PostHog for Next.js in seconds with our wizard by running this prompt with LLM coding agents (/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal.

npx @posthog/wizard

Learn more (/wizard.md)

Or, to integrate manually, continue with the rest of this guide.

Client-side setup

Install posthog-js using your package manager:

npm
npm install --save posthog-js
Yarn
yarn add posthog-js
pnpm
pnpm add posthog-js
Bun
bun add posthog-js

If your site sets a Content-Security-Policy, it needs to allow PostHog. This applies to the snippet and to package installs alike: the SDK lazy-loads extra bundles (session replay, surveys) from PostHog's CDN, and sends events to the ingestion host. PostHog serves from subdomains of posthog.com that change over time, so allow the wildcard:

script-src 'self' https://*.posthog.com;
connect-src 'self' https://*.posthog.com;
worker-src 'self' blob: data:;

script-src covers the snippet and the lazy-loaded bundles, connect-src covers event ingestion and feature flags, and worker-src covers session replay. The toolbar needs a few more (/docs/advanced/content-security-policy.md), or use a reverse proxy (/docs/advanced/proxy.md) so everything is first-party. Failing to do so causes silent failures where capture and identify calls never send, so the integration looks complete while zero events arrive. Remember connect-src falls back to default-src, so default-src 'self' blocks event delivery even when the script itself is bundled.

Add your environment variables to your .env.local file and to your hosting provider (e.g. Vercel, Netlify, AWS). You can find your project token in your project settings.

.env.local

NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=<ph_project_token>
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

These values need to start with NEXT_PUBLIC_ to be accessible on the client-side.

Integration

Next.js provides the instrumentation-client.ts|js file for client-side setup. Add it to the root of your Next.js app (for both app and pages router) and initialize PostHog in it like this:

instrumentation-client.js
import posthog from 'posthog-js'

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, {
  api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
  defaults: '2026-05-30'
});
instrumentation-client.ts
import posthog from 'posthog-js'

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
  api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
  defaults: '2026-05-30'
});

Bootstrapping with instrumentation-client

When using instrumentation-client, the values you pass to posthog.init remain fixed for the entire session. This means bootstrapping only works if you evaluate flags before your app renders (for example, on the server).

If you need flag values after the app has rendered, you’ll want to:

  • Evaluate the flag on the server and pass the value into your app, or
  • Evaluate the flag in an earlier page/state, then store and re-use it when needed.

Both approaches avoid flicker and give you the same outcome as bootstrapping, as long as you use the same distinct_id across client and server.

See the bootstrapping guide (/docs/feature-flags/bootstrapping.md) for more information.

Identifying users

Identifying users is required. Call posthog.identify('your-user-id') after login to link events to a known user. This is what connects frontend event captures, session replays (/docs/session-replay.md), LLM traces (/docs/ai-engineering.md), and error tracking (/docs/error-tracking.md) to the same person — and lets backend events link back too.

Use a stable ID from your auth system when possible, not an email or display name. Send those as person properties instead. If your app has no other stable key, email works as a fallback if they are unique. Never a shared literal like "anonymous" or "user", which pools many people onto one person and corrupts their data. When no ID is available at all, skip the identify and retain the anonymous distinct ID that's automatically assigned.

Call posthog.reset() on logout, so the next person to use the browser doesn't inherit the last one's identity.

See our guide on identifying users (/docs/getting-started/identify-users.md) for how to set this up.

Linking client and server events

Next.js apps usually capture on both sides. To keep them on the same person, use the same distinct ID in both, and let the browser tell your server which one that is.

If your app calls your own backend, tracing_headers adds X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID to matching fetch and XMLHttpRequest requests. This lets server-side SDKs link backend events, errors, and LLM traces back to frontend sessions and replays. Use hostnames only, without protocols or paths.

JavaScript

posthog.init('<ph_project_token>', {
  api_host: 'https://us.i.posthog.com',
  // Optional: send PostHog session/user context to your backend
  tracing_headers: ['api.example.com'],
})

This works in local development too, but match on the hostname alone: use 'localhost', not 'localhost:3000'. Ports are never part of a hostname, so a value with one in it never matches anything. localhost and 127.0.0.1 are also different hostnames — use whichever your app actually calls.

Tracing headers help you attribute events across front and backend consistently. When this isn't available, use your server-side stable IDs to deduce the matching distinctId, and pass it in when capturing the event.

Set up a reverse proxy (recommended)

We recommend setting up a reverse proxy (/docs/advanced/proxy.md), so that events are less likely to be intercepted by tracking blockers.

We have our own managed reverse proxy service (/docs/advanced/proxy/managed-reverse-proxy.md), which is free for all PostHog Cloud users, routes through our infrastructure, and makes setting up your proxy easy.

If you don't want to use our managed service then there are several other options for creating a reverse proxy, including using Cloudflare (/docs/advanced/proxy/cloudflare.md), AWS Cloudfront (/docs/advanced/proxy/cloudfront.md), and Vercel (/docs/advanced/proxy/vercel.md).

Grouping products in one project (recommended)

If you have multiple customer-facing products (e.g. a marketing website + mobile app + web app), it's best to install PostHog on them all and group them in one project (/docs/settings/projects.md).

This makes it possible to track users across their entire journey (e.g. from visiting your marketing website to signing up for your product), or how they use your product across multiple platforms.

Add IPs to Firewall/WAF allowlists (recommended)

For certain features like heatmaps (/docs/toolbar/heatmaps.md), your Web Application Firewall (WAF) may be blocking PostHog's requests to your site. Add these IP addresses to your WAF allowlist or rules to let PostHog access your site.

EU: 3.75.65.221, 18.197.246.42, 3.120.223.253

US: 44.205.89.55, 52.4.194.122, 44.208.188.173

These are public, stable IPs used by PostHog services.

PostHog captures heatmap screenshots using Browserless, which has its own IP addresses. Browserless publishes the current list here.

An allowlist does not help when your app has a private address. For apps on an internal network, see internal and intranet applications (/docs/session-replay/troubleshooting.md#internal-and-intranet-applications).

Accessing PostHog

Once initialized in instrumentation-client.js|ts, import posthog from posthog-js anywhere and call the methods you need on the posthog object.

JavaScript

"use client";
import posthog from "posthog-js";

export default function Home() {
  return (
    <div>
      <button onClick={() => posthog.capture("test_event")}>Click me for an event</button>
    </div>
  );
}
Using React hooks

The React feature flag hooks (/docs/libraries/react.md#feature-flags) work automatically when PostHog is initialized via instrumentation-client.ts. The hooks use the initialized posthog-js singleton:

JavaScript

"use client";
import { useFeatureFlagEnabled } from "@posthog/react";

export default function FeatureComponent() {
  const showNewFeature = useFeatureFlagEnabled("new-feature");

  return showNewFeature ? <NewFeature /> : <OldFeature />;
}
Usage

See the React SDK docs (/docs/libraries/react.md) for examples of how to use:

  • posthog-js functions like custom event capture, user identification, and more. (/docs/libraries/react.md#using-posthog-js-functions)
  • Feature flags including variants and payloads. (/docs/libraries/react.md#feature-flags)

You can also read the full posthog-js documentation (/docs/libraries/js/usage.md) for all the usable functions.

Server-side analytics

Next.js enables you to both server-side render pages and add server-side functionality. To integrate PostHog into your Next.js app on the server-side, you can use the Node SDK (/docs/libraries/node.md).

First, install the posthog-node library:

npm
npm install posthog-node --save
Yarn
yarn add posthog-node
pnpm
pnpm add posthog-node
Bun
bun add posthog-node
Router-specific instructions

App router

For the app router, we can initialize the posthog-node SDK once with a PostHogClient function, and import it into files.

This enables us to send events and fetch data from PostHog on the server – without making client-side requests.

JavaScript

// app/posthog.js
import { PostHog } from 'posthog-node'

export default function PostHogClient() {
  const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, {
    host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
    flushAt: 1,
    flushInterval: 0
  })
  return posthogClient
}

Note: Because server-side functions in Next.js can be short-lived, we set flushAt to 1 and flushInterval to 0.

  • flushAt sets how many capture calls we should flush the queue (in one batch).
  • flushInterval sets how many milliseconds we should wait before flushing the queue. Setting them to the lowest number ensures events are sent immediately and not batched. We also need to call await posthog.shutdown() once done.

To use this client, we import it into our pages and call it with the PostHogClient function:

JavaScript

import Link from 'next/link'
import PostHogClient from '../posthog'

export default async function About() {

  const posthog = PostHogClient()
  const flags = await posthog.getAllFlags(
    'user_distinct_id' // replace with a user's distinct ID
  );
  await posthog.shutdown()

  return (
    <main>
      <h1>About</h1>
      <Link href="/">Go home</Link>
      { flags['main-cta'] &&
        <Link href="http://posthog.com/">Go to PostHog</Link>
      }
    </main>
  )
}

Pages router

For the pages router, we can use the getServerSideProps function to access PostHog on the server-side, send events, evaluate feature flags, and more.

This looks like this:

JavaScript

// pages/posts/[id].js
import { useContext, useEffect, useState } from 'react'
import { getServerSession } from "next-auth/next"
import { authOptions } from '@/lib/auth'
import { PostHog } from 'posthog-node'

export default function Post({ post, flags }) {
  const [ctaState, setCtaState] = useState()

  useEffect(() => {
    if (flags) {
      setCtaState(flags['blog-cta'])
    }
  })

  return (
    <div>
      <h1>{post.title}</h1>
      <p>By: {post.author}</p>
      <p>{post.content}</p>
      {ctaState &&
        <p><a href="/">Go to PostHog</a></p>
      }
      <button onClick={likePost}>Like</button>
    </div>
  )
}

export async function getServerSideProps(ctx) {

  // Pass authOptions, or your session callbacks don't run.
  const session = await getServerSession(ctx.req, ctx.res, authOptions)
  let flags = null

  if (session) {
    const client = new PostHog(
      process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN,
      {
        host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
      }
    )

    // A stable ID from your auth system, not an email. See the note below.
    const distinctId = session.user.id

    flags = await client.getAllFlags(distinctId);
    client.capture({
      distinctId,
      event: 'loaded blog article',
      properties: {
        $current_url: ctx.req.url,
      },
    });

    await client.shutdown()
  }

  const { posts } = await import('../../blog.json')
  const post = posts.find((post) => post.id.toString() === ctx.params.id)
  return {
    props: {
      post,
      flags
    },
  }
}

Note: next-auth doesn't put a user ID on the session by default. Its session is { name, email, image }, so session.user.id is undefined until you add it yourself with a session callback in your authOptions:

JavaScript

// lib/auth.js
export const authOptions = {
  callbacks: {
    session({ session, token, user }) {
      // JWT sessions (the default) carry the user ID in token.sub.
      // Database sessions get it from user.id instead.
      session.user.id = token?.sub ?? user.id
      return session
    },
  },
}

Capturing with an undefined distinct ID creates events that belong to nobody, so check that the ID arrives before relying on it.

Note: Make sure to always call await client.shutdown() after sending events from the server-side. PostHog queues events into larger batches, and this call forces all batched events to be flushed immediately.

Server-side configuration

Next.js overrides the default fetch behavior on the server to introduce their own cache. PostHog ignores that cache by default, as this is Next.js's default behavior for any fetch call.

You can override that configuration when initializing PostHog, but make sure you understand the pros/cons of using Next.js's cache and that you might get cached results rather than the actual result our server would return. This is important for feature flags, for example.

TSX

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, {
  // ... your configuration
  fetch_options: {
    cache: 'force-cache', // Use Next.js cache
    next_options: {       // Passed to the `next` option for `fetch`
      revalidate: 60,     // Cache for 60 seconds
      tags: ['posthog'],  // Can be used with Next.js `revalidateTag` function
    },
  }
})

Configuring a reverse proxy to PostHog

To improve the reliability of client-side tracking and make requests less likely to be intercepted by tracking blockers, you can setup a reverse proxy in Next.js. Read more about deploying a reverse proxy using Next.js rewrites (/docs/advanced/proxy/nextjs.md), Next.js middleware (/docs/advanced/proxy/nextjs-middleware.md), and Vercel rewrites (/docs/advanced/proxy/vercel.md).

Further reading

  • How to set up Next.js analytics, feature flags, and more (/tutorials/nextjs-analytics.md)
  • How to set up Next.js pages router analytics, feature flags, and more (/tutorials/nextjs-pages-analytics.md)
  • How to set up Next.js A/B tests (/tutorials/nextjs-ab-tests.md)
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/nodejs.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Node.js Feature Flags installation

  1. 1

    Install the package

    Required

    Install the PostHog Node.js library using your package manager:

    npm
    npm install posthog-node
    yarn
    yarn add posthog-node
    pnpm
    pnpm add posthog-node
    bun
    bun add posthog-node
  2. 2

    Initialize PostHog

    Required

    Initialize the PostHog client with your project token:

    Node.js

    import { PostHog } from 'posthog-node'
    
    const client = new PostHog(
        '<ph_project_token>',
        {
            host: 'https://us.i.posthog.com'
        }
    )
  3. 3

    Send an event

    Recommended

    Once installed, you can manually send events to test your integration:

    Node.js

    client.capture({
        distinctId: 'distinct_id_of_the_user',
        event: 'event_name',
        properties: {
            property1: 'value',
            property2: 'value',
        },
    })
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    const isFeatureFlagEnabled = await client.isFeatureEnabled('flag-key', 'distinct_id_of_your_user')
    if (isFeatureFlagEnabled) {
        // Your code if the flag is enabled
        // Optional: fetch the payload
        const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', isFeatureFlagEnabled)
    }
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    const enabledVariant = await client.getFeatureFlag('flag-key', 'distinct_id_of_your_user')
    if (enabledVariant === 'variant-key') {  // replace 'variant-key' with the key of your variant
        // Do something differently for this user
        // Optional: fetch the payload
        const matchedFlagPayload = await client.getFeatureFlagPayload('flag-key', 'distinct_id_of_your_user', enabledVariant)
    }
  6. 6

    Include feature flag information in events

    Required

    If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

    Note: This step is only required for events captured using our server-side SDKs or API.

    Set sendFeatureFlags to true in your capture call:

    Node.js

    client.capture({
        distinctId: 'distinct_id_of_your_user',
        event: 'event_name',
        sendFeatureFlags: true,
    })

    Include $feature property

    Include the $feature/feature_flag_name property in your event properties:

    Node.js

    client.capture({
        distinctId: 'distinct_id_of_your_user',
        event: 'event_name',
        properties: {
            '$feature/feature-flag-key': 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
        },
    })
  7. 7

    Override server properties

    Optional

    Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with:

    await client.getFeatureFlag(
        'flag-key',
        'distinct_id_of_the_user',
        {
            personProperties: {
                'property_name': 'value'
            },
            groups: {
                "your_group_type": "your_group_id",
                "another_group_type": "your_group_id",
            },
            groupProperties: {
                'your_group_type': {
                    'group_property_name': 'value'
                },
                'another_group_type': {
                    'group_property_name': 'value'
                },
            },
        }
    )
  8. 8

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  9. 9

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/php.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

PHP Feature Flags installation

  1. 1

    Install the package

    Required

    Install the PostHog PHP library using Composer:

    Terminal

    composer require posthog/posthog-php
  2. 2

    Configure PostHog

    Required

    Initialize the PostHog client with your project token and host:

    PHP

    PostHog\PostHog::init(
        '<ph_project_token>',
        ['host' => 'https://us.i.posthog.com']
    );
  3. 3

    Send events

    Recommended

    Once installed, you can manually send events to test your integration:

    PHP

    PostHog::capture([
        'distinctId' => 'test-user',
        'event' => 'test-event',
    ]);
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    $isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user')
    if ($isMyFlagEnabledForUser) {
        // Do something differently for this user
    }
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    $enabledVariant = PostHog::getFeatureFlag('flag-key', 'distinct_id_of_your_user')
    if ($enabledVariant === 'variant-key') { # replace 'variant-key' with the key of your variant
        # Do something differently for this user
    }
  6. 6

    Include feature flag information in events

    Required

    If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

    Note: This step is only required for events captured using our server-side SDKs or API.

    Set send_feature_flags to true in your capture call:

    PHP

    PostHog::capture(array(
        'distinctId' => 'distinct_id_of_your_user',
        'event' => 'event_name',
        'send_feature_flags' => true
    ));

    Include $feature property

    Include the $feature/feature_flag_name property in your event properties:

    PHP

    PostHog::capture(array(
        'distinctId' => 'distinct_id_of_your_user',
        'event' => 'event_name',
        'properties' => array(
            '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
        )
    ));
  7. 7

    Override server properties

    Optional

    Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with:

    PostHog::getFeatureFlag(
        'flag-key',
        'distinct_id_of_the_user',
        [
            'your_group_type' => 'your_group_id',
            'another_group_type' => 'your_group_id'
        ], // groups
        ['property_name' => 'value'], // person properties
        [
            'your_group_type' => ['group_property_name' => 'value'],
            'another_group_type' => ['group_property_name' => 'value']
        ], // group properties
        false, // onlyEvaluateLocally, Optional. Defaults to false.
        true // sendFeatureFlagEvents
    )
  8. 8

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  9. 9

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/python.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Python Feature Flags installation

  1. 1

    Install the package

    Required

    Install the PostHog Python library using pip:

    Terminal

    pip install posthog
  2. 2

    Initialize PostHog

    Required

    Initialize the PostHog client with your project token and host from your project settings:

    Python

    from posthog import Posthog
    
    posthog = Posthog(
        project_api_key='<ph_project_token>',
        host='https://us.i.posthog.com'
    )

    Django integration

    If you're using Django, check out our Django integration (/docs/libraries/django.md) for automatic request tracking.

  3. 3

    Send events

    Recommended

    Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration:

    Capture custom events by calling the capture method with an event name and properties:

    Python

    import posthog
    posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'})
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    is_my_flag_enabled = posthog.feature_enabled('flag-key', 'distinct_id_of_your_user')
    if is_my_flag_enabled:
        # Do something differently for this user
        # Optional: fetch the payload
        matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user')
    if enabled_variant == 'variant-key': # replace 'variant-key' with the key of your variant
        # Do something differently for this user
        # Optional: fetch the payload
        matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
  6. 6

    Include feature flag information in events

    Required

    If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

    Note: This step is only required for events captured using our server-side SDKs or API.

    Set send_feature_flags to True in your capture call:

    Python

    posthog.capture(
        distinct_id="distinct_id_of_the_user",
        event='event_name',
        send_feature_flags=True
    )

    Include $feature property

    Include the $feature/feature_flag_name property in your event properties:

    Python

    posthog.capture(
        "event_name",
        distinct_id="distinct_id_of_the_user",
        properties={
            "$feature/feature-flag-key": "variant-key"  # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
        },
    )
  7. 7

    Override server properties

    Optional

    Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with:

    posthog.get_feature_flag(
        'flag-key',
        'distinct_id_of_the_user',
        person_properties={'property_name': 'value'},
        groups={
            'your_group_type': 'your_group_id',
            'another_group_type': 'your_group_id'},
        group_properties={
            'your_group_type': {'group_property_name': 'value'},
            'another_group_type': {'group_property_name': 'value'}
        },
    )
  8. 8

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  9. 9

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/react-native.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

React Native Feature Flags installation

  1. 1

    Install the package

    Required

    Install the PostHog React Native library and its dependencies:

    Expo
    npx expo install posthog-react-native expo-file-system expo-application expo-device expo-localization
    yarn
    yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info react-native-localize
    
    # for iOS
    cd ios && pod install
    npm
    npm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info react-native-localize
    
    # for iOS
    cd ios && pod install
  2. 2

    Configure PostHog

    Required

    PostHog is most easily used via the PostHogProvider component. Wrap your app with the provider:

    App.tsx

    import { PostHogProvider } from 'posthog-react-native'
    
    export function MyApp() {
        return (
            <PostHogProvider
                apiKey="<ph_project_token>"
                options={{
                    host: "https://us.i.posthog.com",
                }}
            >
                <RestOfApp />
            </PostHogProvider>
        )
    }
  3. 3

    Send events

    Recommended

    Once installed, PostHog will automatically start capturing events. You can also manually send events using the usePostHog hook:

    Component.tsx

    import { usePostHog } from 'posthog-react-native'
    
    function MyComponent() {
        const posthog = usePostHog()
    
        const handlePress = () => {
            posthog.capture('button_pressed', {
                button_name: 'signup'
            })
        }
    
        return <Button onPress={handlePress} title="Sign Up" />
    }
  4. 4

    Use feature flags

    Required

    PostHog provides hooks to make it easy to use feature flags in your React Native app. Use useFeatureFlagEnabled for boolean flags:

    Component.tsx

    import { usePostHog } from 'posthog-react-native'
    
    function MyComponent() {
        const posthog = usePostHog()
        const isMyFlagEnabled = posthog.isFeatureEnabled('flag-key')
    
        if (isMyFlagEnabled) {
            // Do something differently for this user
            // Optional: fetch the payload
            const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload
        }
    
        return <View>...</View>
    }
    Multivariate flags

    For multivariate flags, use getFeatureFlag:

    Component.tsx

    import { usePostHog } from 'posthog-react-native'
    
    function MyComponent() {
        const posthog = usePostHog()
        const enabledVariant = posthog.getFeatureFlag('flag-key')
    
        if (enabledVariant === 'variant-key') { // replace 'variant-key' with the key of your variant
            // Do something differently for this user
            // Optional: fetch the payload
            const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload
        }
    
        return <View>...</View>
    }
  5. 5

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  6. 6

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/react.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

React Feature Flags installation

  1. 1

    Install the package

    Required

    Install posthog-js and @posthog/react using your package manager:

    npm
    npm install posthog-js @posthog/react
    yarn
    yarn add posthog-js @posthog/react
    pnpm
    pnpm add posthog-js @posthog/react
    bun
    bun add posthog-js @posthog/react
  2. 2

    Add environment variables

    Required

    Add your PostHog project token and host to your environment variables. For Vite-based React apps, use the VITE_ prefix to expose them to the client:

    .env

    VITE_POSTHOG_PROJECT_TOKEN=<ph_project_token>
    VITE_POSTHOG_HOST=https://us.i.posthog.com
  3. 3

    Initialize PostHog

    Required

    Wrap your app with the PostHogProvider component at the root of your application (such as main.tsx if you're using Vite):

    main.tsx

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import './index.css'
    import App from './App.jsx'
    import { PostHogProvider } from '@posthog/react'
    
    const options = {
      api_host: import.meta.env.VITE_POSTHOG_HOST,
      defaults: '2026-05-30',
    } as const
    
    createRoot(document.getElementById('root')).render(
      <StrictMode>
        <PostHogProvider apiKey={import.meta.env.VITE_POSTHOG_PROJECT_TOKEN} options={options}>
          <App />
        </PostHogProvider>
      </StrictMode>
    )

    defaults option

    The defaults option automatically configures PostHog with recommended settings for new projects. See SDK defaults (/docs/libraries/js.md#sdk-defaults) for details.

  4. 4

    Accessing PostHog in your code

    Recommended

    Use the usePostHog hook to access the PostHog instance in any component wrapped by PostHogProvider:

    MyComponent.tsx

    import { usePostHog } from '@posthog/react'
    
    function MyComponent() {
        const posthog = usePostHog()
    
        function handleClick() {
            posthog.capture('button_clicked', { button_name: 'signup' })
        }
    
        return <button onClick={handleClick}>Sign up</button>
    }

    You can also import posthog directly for non-React code or utility functions:

    utils/analytics.ts

    import posthog from 'posthog-js'
    
    export function trackPurchase(amount: number) {
        posthog.capture('purchase_completed', { amount })
    }
  5. 5

    Send events

    Recommended

    Click around and view a couple pages to generate some events. PostHog automatically captures pageviews, clicks, and other interactions for you.

    If you'd like, you can also manually capture custom events:

    JavaScript

    posthog.capture('my_custom_event', { property: 'value' })
  6. 6

    Use feature flags

    Required

    Using hooks

    PostHog provides several hooks to make it easy to use feature flags in your React app. Use useFeatureFlagEnabled for boolean flags:

    import { useFeatureFlagEnabled } from '@posthog/react'
    
    function App() {
        const showWelcomeMessage = useFeatureFlagEnabled('flag-key')
        const payload = useFeatureFlagPayload('flag-key')
        return (
            <div className="App">
                {showWelcomeMessage ? (
                    <div>
                        <h1>Welcome!</h1>
                        <p>Thanks for trying out our feature flags.</p>
                    </div>
                ) : (
                    <div>
                        <h2>No welcome message</h2>
                        <p>Because the feature flag evaluated to false.</p>
                    </div>
                )}
            </div>
        )
    }
    Multivariate flags

    For multivariate flags, use useFeatureFlagVariantKey:

    import { useFeatureFlagVariantKey } from '@posthog/react'
    
    function App() {
        const variantKey = useFeatureFlagVariantKey('show-welcome-message')
        let welcomeMessage = ''
        if (variantKey === 'variant-a') {
            welcomeMessage = 'Welcome to the Alpha!'
        } else if (variantKey === 'variant-b') {
            welcomeMessage = 'Welcome to the Beta!'
        }
        return (
            <div className="App">
                {welcomeMessage ? (
                    <div>
                        <h1>{welcomeMessage}</h1>
                        <p>Thanks for trying out our feature flags.</p>
                    </div>
                ) : (
                    <div>
                        <h2>No welcome message</h2>
                        <p>Because the feature flag evaluated to false.</p>
                    </div>
                )}
            </div>
        )
    }
    Flag payloads

    The useFeatureFlagPayload hook does not send a $feature_flag_called event, which is required for experiments. Always use it with useFeatureFlagEnabled or useFeatureFlagVariantKey:

    import { useFeatureFlagPayload, useFeatureFlagEnabled } from '@posthog/react'
    
    function App() {
        const variant = useFeatureFlagEnabled('show-welcome-message')
        const payload = useFeatureFlagPayload('show-welcome-message')
        return (
            <>
                {variant ? (
                    <div className="welcome-message">
                        <h2>{payload?.welcomeTitle}</h2>
                        <p>{payload?.welcomeMessage}</p>
                    </div>
                ) : (
                    <div>
                        <h2>No custom welcome message</h2>
                        <p>Because the feature flag evaluated to false.</p>
                    </div>
                )}
            </>
        )
    }

    Using PostHogFeature component

    The PostHogFeature component simplifies code by handling feature flag related logic:

    App.tsx

    import { PostHogFeature } from '@posthog/react'
    
    function App() {
        return (
            <PostHogFeature flag='show-welcome-message' match={true}>
                <div>
                    <h1>Hello</h1>
                    <p>Thanks for trying out our feature flags.</p>
                </div>
            </PostHogFeature>
        )
    }

    The match prop can be either true, or the variant key, to match on a specific variant. If you also want to show a default message, you can pass these in the fallback prop.

    If your flag has a payload, you can pass a function to children whose first argument is the payload:

    App.tsx

    <PostHogFeature flag='show-welcome-message' match={true}>
        {(payload) => {
            return (
                <div>
                    <h1>{payload.welcomeMessage}</h1>
                    <p>Thanks for trying out our feature flags.</p>
                </div>
            )
        }}
    </PostHogFeature>
  7. 7

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  8. 8

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/ruby-on-rails.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Ruby on Rails

PostHog makes it easy to get data about traffic and usage of your Ruby on Rails app. Integrating PostHog enables analytics, custom event capture, feature flags, and automatic exception tracking.

This guide walks you through integrating PostHog into your Rails app using the posthog-rails gem.

Beta: integration via LLM

Install PostHog for Rails in seconds with our wizard by running this prompt with LLM coding agents (/blog/envoy-wizard-llm-agent.md) like Cursor and Bolt, or by running it in your terminal.

npx @posthog/wizard

Learn more (/wizard.md)

Or, to integrate manually, continue with the rest of this guide.

Features

  • Automatic exception tracking – Captures unhandled and rescued exceptions
  • ActiveJob instrumentation – Tracks background job exceptions
  • User context – Automatically associates exceptions with the current user
  • Smart filtering – Excludes common Rails exceptions (404s, etc.) by default
  • Request context – Adds request metadata and optional PostHog tracing header identity/session context to captured events
  • Rails 7.0+ error reporter – Integrates with Rails' built-in error reporting
  • Log forwarding – Optionally forwards Rails.logger output to PostHog Logs (/docs/logs.md) over OpenTelemetry, automatically correlated with request context (Ruby 3.3+)

Installation

Add both gems to your Gemfile:

Gemfile

gem 'posthog-ruby', require: 'posthog'
gem 'posthog-rails'

Then run:

Terminal

bundle install

Identifying users

Identifying users is required. Backend events need a distinct_id that matches the ID your frontend uses when calling posthog.identify(). Without this, backend events are orphaned — they can't be linked to frontend event captures, session replays (/docs/session-replay.md), LLM traces (/docs/ai-engineering.md), or error tracking (/docs/error-tracking.md).

See our guide on identifying users (/docs/getting-started/identify-users.md) for how to set this up.

Generate the initializer

Run the install generator to create the PostHog initializer:

Terminal

rails generate posthog:install

This creates config/initializers/posthog.rb with sensible defaults and documentation.

Configuration

PostHog.init creates a single client instance used across your app. Avoid creating multiple PostHog::Client instances with the same API key, as this can cause dropped events and inconsistent behavior.

The generated initializer includes the most common options:

config/initializers/posthog.rb

# Rails-specific configuration
PostHog::Rails.configure do |config|
  config.auto_capture_exceptions = true           # Enable automatic exception capture (default: false)
  config.report_rescued_exceptions = true         # Report exceptions Rails rescues (default: false)
  config.auto_instrument_active_job = true        # Instrument background jobs (default: false)
  config.use_tracing_headers = true               # Use PostHog tracing headers for identity/session context (default: true)
  config.capture_user_context = true              # Include authenticated user info in exceptions (default: true)
  config.current_user_method = :current_user      # Method to get current user (default: :current_user)
  config.user_id_method = nil                     # Method to get ID from user object (default: auto-detect)

  # Add additional exceptions to ignore
  config.excluded_exceptions = ['MyCustomError']
end

# Core PostHog client initialization
PostHog.init do |config|
  # Required: Your PostHog project API key
  config.api_key = '<ph_project_token>'

  # Optional: Your PostHog instance URL
  config.host = 'https://us.i.posthog.com'

  # Optional: Personal API key for feature flags
  config.personal_api_key = 'phx_xxxxxxxxx'

  # Maximum number of events to queue before dropping (default: 10000)
  config.max_queue_size = 10_000

  # Send events synchronously on the calling thread (default: false)
  config.sync_mode = false

  # Feature flags polling interval in seconds (default: 30)
  config.feature_flags_polling_interval = 30

  # Feature flag request timeout in seconds (default: 3)
  config.feature_flag_request_timeout_seconds = 3

  # Error callback to detect misconfiguration
  config.on_error = proc { |status, msg|
    Rails.logger.error("PostHog error: #{msg}")
  }

  # Before-send callback to modify or drop events
  config.before_send = proc { |event|
    event[:properties] ||= {}
    event[:properties]['environment'] = Rails.env
    event
  }

  # Disable network calls in test mode
  config.test_mode = true if Rails.env.test?
end

You can find your project token and instance address in your project settings.

Tip: Use Rails.application.credentials to avoid hardcoding API keys. First, add your keys and then reference them in your initializer:

Terminal

rails credentials:edit

config/credentials.yml.enc

posthog:
  api_key: <ph_project_token>
  host: https://us.i.posthog.com
  personal_api_key: phx_xxxxxxxxx

config/initializers/posthog.rb

config.api_key = Rails.application.credentials.posthog[:api_key]
config.host = Rails.application.credentials.posthog[:host]
config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key]

Capturing events

Track custom events anywhere in your Rails app:

Ruby

PostHog.capture({
  distinct_id: current_user.id,
  event: 'post_created',
  properties: { title: @post.title }
})

Identify a user and set their person properties:

Ruby

PostHog.identify({
  distinct_id: current_user.id,
  properties: {
    email: current_user.email,
    plan: current_user.plan
  }
})

The Rails integration delegates methods like capture, identify, alias, group_identify, evaluate_flags, capture_exception, flush, and shutdown to the initialized PostHog::Client.

Request context

PostHog Rails automatically applies request-scoped context to events captured during web requests. Request metadata such as $current_url, $request_method, $request_path, $user_agent, and $ip is added to event properties.

When use_tracing_headers is enabled, PostHog tracing headers (X-PostHog-Distinct-Id and X-PostHog-Session-Id) are also used as default distinct_id and $session_id values. Explicit distinct_id and properties passed to PostHog.capture always take precedence.

If you're using PostHog JS (/docs/libraries/js.md) on the frontend, configure tracing_headers (/docs/libraries/js/config.md#tracing-headers) for your Rails backend hostname so browser requests include the session and distinct ID headers.

Tracing headers are client-controlled analytics context, not authentication or authorization. Pass an authenticated distinct_id explicitly for security-sensitive server-side decisions.

Disable tracing header identity/session capture if you do not want client-supplied tracing headers used for server-side events. Request metadata is still captured:

Ruby

PostHog::Rails.config.use_tracing_headers = false

Logs

To set up PostHog Logs (/docs/logs.md) in your Rails app, follow the Ruby on Rails logs installation guide (/docs/logs/installation/ruby-on-rails.md). The integration forwards Rails.logger output to PostHog Logs over OpenTelemetry, automatically correlated with each request's distinct ID and session ID. Requires Ruby 3.3+.

Error tracking

For full details on setting up error tracking with Rails, see our Rails error tracking installation guide (/docs/error-tracking/installation/ruby-on-rails.md).

Automatic exception tracking

When auto_capture_exceptions is enabled, exceptions are automatically captured:

Ruby

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    # Any exception here is automatically captured
  end
end

report_rescued_exceptions controls whether exceptions Rails rescues (for example, exceptions rendered by Rails error pages) are captured. Enable it along with auto_capture_exceptions for complete error visibility, or leave it disabled to capture only unhandled exceptions.

Manual exception capture

You can also manually capture exceptions:

Ruby

PostHog.capture_exception(
  exception,
  current_user.id,
  { custom_property: 'value' }
)

If you evaluated feature flags for the request, pass the same snapshot to include matching flag properties on the exception event:

Ruby

flags = PostHog.evaluate_flags(current_user.id)

PostHog.capture_exception(
  exception,
  current_user.id,
  { custom_property: 'value' },
  flags: flags
)
Background job exceptions

When auto_instrument_active_job is enabled, ActiveJob exceptions are automatically captured with job context:

Ruby

class EmailJob < ApplicationJob
  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
    # Exceptions are automatically captured
  end
end
Associating jobs with users

By default, PostHog extracts a distinct_id from job arguments by looking for a user_id key in hash arguments:

Ruby

# PostHog will automatically use options[:user_id] as the distinct_id
ProcessOrderJob.perform_later(order.id, user_id: current_user.id)

For more control, use the posthog_distinct_id class method. The proc or block receives the same arguments as perform:

Ruby

class SendWelcomeEmailJob < ApplicationJob
  posthog_distinct_id ->(user, _options) { user.id }

  def perform(user, options = {})
    UserMailer.welcome(user).deliver_now
  end
end

You can also use a block:

Ruby

class ProcessOrderJob < ApplicationJob
  posthog_distinct_id do |_order, notify_user_id|
    notify_user_id
  end

  def perform(order, notify_user_id)
    # Process the order...
  end
end
Rails 7.0+ error reporter

PostHog integrates with Rails' built-in error reporting:

Ruby

# These errors are automatically sent to PostHog
Rails.error.handle do
  # Code that might raise an error
end

Rails.error.record(exception, context: { user_id: current_user.id })

PostHog automatically extracts the user's distinct ID from user_id or distinct_id in the context hash. Other context keys are included as properties on the exception event.

User context

PostHog Rails automatically captures authenticated user information from your controllers for exceptions. Authenticated Rails user context takes precedence over client-supplied tracing headers for exception identity.

If your user method has a different name, configure it:

Ruby

PostHog::Rails.config.current_user_method = :logged_in_user
User ID extraction

By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order:

  1. posthog_distinct_id – Define this on your User model for full control
  2. distinct_id – Common analytics convention
  3. id – Standard ActiveRecord primary key
  4. pk – Primary key alias
  5. uuid – For UUID-based primary keys

It also checks hash-like users for id, pk, and uuid keys.

You can configure a specific method:

Ruby

PostHog::Rails.config.user_id_method = :email

Or define a method on your User model:

Ruby

class User < ApplicationRecord
  def posthog_distinct_id
    "user_#{id}"  # or external_id, or any unique identifier
  end
end
Excluded exceptions

The following exceptions are not reported by default (common 4xx errors):

  • AbstractController::ActionNotFound
  • ActionController::BadRequest
  • ActionController::InvalidAuthenticityToken
  • ActionController::InvalidCrossOriginRequest
  • ActionController::MethodNotAllowed
  • ActionController::NotImplemented
  • ActionController::ParameterMissing
  • ActionController::RoutingError
  • ActionController::UnknownFormat
  • ActionController::UnknownHttpMethod
  • ActionDispatch::Http::Parameters::ParseError
  • ActiveRecord::RecordNotFound
  • ActiveRecord::RecordNotUnique

Add more with:

Ruby

PostHog::Rails.config.excluded_exceptions = ['MyException']

Feature flags

Evaluate flags once for the current user, then read values from the returned snapshot:

Ruby

class PostsController < ApplicationController
  def show
    flags = PostHog.evaluate_flags(current_user.id)

    if flags.enabled?('new-post-design')
      render 'posts/show_new'
    else
      render 'posts/show'
    end
  end
end

For multivariate flags and experiments, use get_flag:

Ruby

flags = PostHog.evaluate_flags(current_user.id)
variant = flags.get_flag('checkout-experiment')

if variant == 'test'
  # Do something differently
end

When capturing an event after branching on a flag, pass the same flags snapshot so the event includes the exact flag values used by your code:

Ruby

flags = PostHog.evaluate_flags(current_user.id)

PostHog.capture({
  distinct_id: current_user.id,
  event: 'checkout_started',
  flags: flags.only_accessed
})

For local evaluation, ensure you've set personal_api_key:

Ruby

config.personal_api_key = Rails.application.credentials.posthog[:personal_api_key]

See our Ruby SDK docs (/docs/libraries/ruby.md#local-evaluation) for details on local evaluation with Puma and Unicorn servers.

Note: PostHog.is_feature_enabled, PostHog.get_feature_flag, PostHog.get_feature_flag_result, PostHog.get_feature_flag_payload, and PostHog.capture({ ..., send_feature_flags: true }) still work during the migration period, but they're deprecated. Prefer PostHog.evaluate_flags for new code.

Testing

In your test environment, disable network calls with test mode:

config/environments/test.rb

PostHog.init do |config|
  config.api_key = '<ph_project_token>'
  config.test_mode = true
end

Or in your specs:

spec/rails_helper.rb

RSpec.configure do |config|
  config.before(:each) do
    allow(PostHog).to receive(:capture)
  end
end

Configuration reference

Core PostHog options
Option Type Default Description
api_key String required Your PostHog project token.
host String https://us.i.posthog.com Fully qualified PostHog API host.
personal_api_key String nil Personal API key for local feature flag evaluation and remote config payloads.
max_queue_size Integer 10000 Maximum number of events to keep in the async queue before dropping new events.
test_mode Boolean false Keep events queued and do not send them. Useful for tests.
sync_mode Boolean false Send events synchronously on the calling thread.
on_error Proc no-op Callback called as on_error.call(status, error).
feature_flags_polling_interval Integer 30 Seconds between local feature flag definition polls.
feature_flag_request_timeout_seconds Integer 3 Timeout, in seconds, for feature flag requests.
before_send Proc nil Callback that receives the event hash before it is queued or sent. Return a modified event hash, or nil to drop the event.

The PostHog.init block supports the options above. Less common core options like batch_size, disable_singleton_warning, skip_ssl_verification, and flag_definition_cache_provider can be passed as an options hash to PostHog.init(...); see the Ruby SDK docs (/docs/libraries/ruby.md#configuration) for details.

Rails-specific options

Configure these via PostHog::Rails.configure or PostHog::Rails.config:

Option Type Default Description
auto_capture_exceptions Boolean false Automatically capture exceptions.
report_rescued_exceptions Boolean false Report exceptions Rails rescues.
auto_instrument_active_job Boolean false Capture ActiveJob exceptions with job context.
excluded_exceptions Array [] Additional exception class names to ignore.
use_tracing_headers Boolean true Use X-PostHog-Distinct-Id and X-PostHog-Session-Id as request-scoped defaults.
capture_user_context Boolean true Include authenticated user info in exceptions.
current_user_method Symbol :current_user Controller method used to fetch the current user.
user_id_method Symbol nil Method used to extract the distinct ID from the user object. Auto-detects when nil.

Troubleshooting

Exceptions not being captured
  1. Verify PostHog is initialized:

    Ruby

    Rails.console
    > PostHog.initialized?
    => true
  2. Check your excluded exceptions list.

  3. Verify middleware is installed:

    Ruby

    Rails.application.middleware
User context not working
  1. Verify current_user_method matches your controller method.
  2. Check that the user object responds to posthog_distinct_id, distinct_id, id, pk, or uuid.
  3. If using a custom identifier, set PostHog::Rails.config.user_id_method = :your_method.
Feature flags not working

Ensure you've set personal_api_key in your configuration.

Next steps

For any technical questions for how to integrate specific PostHog features into Rails (such as analytics, feature flags, A/B testing, etc.), have a look at our Ruby SDK docs (/docs/libraries/ruby.md).

Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/ruby.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Ruby Feature Flags installation

  1. 1

    Install the gem

    Required

    Add the PostHog Ruby gem to your Gemfile:

    Gemfile

    gem "posthog-ruby"
  2. 2

    Configure PostHog

    Required

    Initialize the PostHog client with your project token and host:

    Ruby

    require 'posthog'
    
    posthog = PostHog::Client.new({
        api_key: "<ph_project_token>",
        host: "https://us.i.posthog.com",
        on_error: Proc.new { |status, msg| print msg }
    })
  3. 3

    Send events

    Recommended

    Once installed, you can manually send events to test your integration:

    Ruby

    posthog.capture({
        distinct_id: 'user_123',
        event: 'button_clicked',
        properties: {
            button_name: 'signup'
        }
    })
  4. 4

    Evaluate boolean feature flags

    Required

    Check if a feature flag is enabled:

    is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user')
    if is_my_flag_enabled
        # Do something differently for this user
        # Optional: fetch the payload
        matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
    end
  5. 5

    Evaluate multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user')
    if enabled_variant == 'variant-key' # replace 'variant-key' with the key of your variant
        # Do something differently for this user
        # Optional: fetch the payload
        matched_flag_payload = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user')
    end
  6. 6

    Include feature flag information in events

    Required

    If you want to use your feature flag to breakdown or filter events in your insights, you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

    Note: This step is only required for events captured using our server-side SDKs or API.

    Set send_feature_flags to true in your capture call:

    Ruby

    posthog.capture({
        distinct_id: 'distinct_id_of_your_user',
        event: 'event_name',
        send_feature_flags: true,
    })

    Include $feature property

    Include the $feature/feature_flag_name property in your event properties:

    Ruby

    posthog.capture({
        distinct_id: 'distinct_id_of_your_user',
        event: 'event_name',
        properties: {
            '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant
        }
    })
  7. 7

    Override server properties

    Optional

    Sometimes, you may want to evaluate feature flags using properties that haven't been ingested yet, or were set incorrectly earlier. You can provide properties to evaluate the flag with:

    posthog.get_feature_flag(
        'flag-key',
        'distinct_id_of_the_user',
        person_properties: {
            'property_name': 'value'
        },
        groups: {
            'your_group_type': 'your_group_id',
            'another_group_type': 'your_group_id',
        },
        group_properties: {
            'your_group_type': {
                'group_property_name': 'value'
            },
            'another_group_type': {
                'group_property_name': 'value'
            },
        },
    )
  8. 8

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  9. 9

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/rust.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Rust Feature Flags installation

Install the posthog-rs crate by adding it to your Cargo.toml.

Cargo.toml

[dependencies]
posthog-rs = "0.14"

Next, set up the client with your PostHog project key.

Rust

let client = posthog_rs::client("<ph_project_token>").await;
Blocking client

Our Rust SDK supports both blocking and async clients. The async client is the default and is recommended for most use cases.

If you need to use a synchronous client instead – like we do in our CLI –, you can opt into it by disabling the asynchronous feature on your Cargo.toml file.

toml

[dependencies]
posthog-rs = { version = "0.14", default-features = false }

With the blocking client, the same methods are available without .await. Either way, capture is non-blocking: it hands the event to a background worker that batches and sends it, so it returns immediately instead of waiting on the network. Because delivery happens in the background, call flush() or shutdown() before your program exits, or buffered events may be lost.

Using feature flags

There are two steps to implement feature flags in Rust:

Step 1: Evaluate flags once

Call client.evaluate_flags() once for the user, then read values from the returned snapshot.

Boolean feature flags

Rust

use posthog_rs::EvaluateFlagsOptions;

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).await.unwrap();

if flags.is_enabled("flag-key") {
    // Do something differently for this user
    // Optional: fetch the payload
    let matched_flag_payload = flags.get_flag_payload("flag-key");
}
Multivariate feature flags

Rust

use posthog_rs::{EvaluateFlagsOptions, FlagValue};

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).await.unwrap();

match flags.get_flag("flag-key") {
    Some(FlagValue::String(variant)) if variant == "variant-key" => {
        // Do something differently for this user
        // Optional: fetch the payload
        let matched_flag_payload = flags.get_flag_payload("flag-key");
    }
    _ => {}
}

flags.get_flag() returns Some(FlagValue::String(...)) for multivariate flags, Some(FlagValue::Boolean(true)) for enabled boolean flags, Some(FlagValue::Boolean(false)) for disabled flags, and None when the flag wasn't returned by the evaluation.

Note: client.is_feature_enabled(), client.get_feature_flag(), client.get_feature_flag_payload(), and client.get_feature_flags() still work during the migration period, but they're deprecated. Prefer evaluate_flags() for new code.

Step 2: Include feature flag information when capturing events

If you want use your feature flag to breakdown or filter events in your insights (/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.

Note: This step is only required for events captured using our server-side SDKs or API (/docs/api.md).

There are two methods you can use to include feature flag information in your events:

Method 1: Pass the evaluated flags snapshot to the event

Pass the same flags object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another /flags request.

Rust

use posthog_rs::{EvaluateFlagsOptions, Event};

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).await.unwrap();

if flags.is_enabled("flag-key") {
    // Do something differently for this user
}

let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.with_flags(&flags);
client.capture(event);

By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.

To reduce event property bloat, pass a filtered snapshot:

Rust

// Attach only flags accessed with is_enabled() or get_flag() before this call
let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.with_flags(&flags.only_accessed());
client.capture(event);

// Attach only specific flags
let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.with_flags(&flags.only(&["checkout-flow", "new-dashboard"]));
client.capture(event);

only_accessed() is order-dependent. If you call it before accessing any flags with is_enabled() or get_flag(), no feature flag properties are attached.

Method 2: Include the $feature/feature_flag_name property manually

In the event properties, include $feature/feature_flag_name: variant_key:

Rust

use posthog_rs::Event;

let mut event = Event::new("event_name", "distinct_id_of_your_user");
event.insert_prop("$feature/feature-flag-key", "variant-key").unwrap();
client.capture(event);
Evaluating only specific flags

By default, evaluate_flags() evaluates every flag for the user. If you only need a few flags, pass flag_keys to request only those flags:

Rust

use posthog_rs::EvaluateFlagsOptions;

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions {
        flag_keys: Some(vec!["checkout-flow".to_string(), "new-dashboard".to_string()]),
        ..Default::default()
    },
).await.unwrap();
Sending $feature_flag_called events

Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (/docs/product-analytics/insights.md) on the flag. With evaluate_flags(), the SDK sends this event when you call flags.is_enabled() or flags.get_flag() for a flag.

The SDK deduplicates these events per (distinct_id, flag, value) in a local cache. If you reinitialize the PostHog client, the cache resets and $feature_flag_called events may be sent again. PostHog handles duplicates, so duplicate $feature_flag_called events don't affect your analytics.

flags.get_flag_payload() doesn't send $feature_flag_called events and doesn't count as an access for only_accessed().

Blocking client

If you're using the blocking client (with default-features = false), the API is the same but without .await:

Rust

use posthog_rs::EvaluateFlagsOptions;

let flags = client.evaluate_flags(
    "distinct_id_of_your_user",
    EvaluateFlagsOptions::default(),
).unwrap();

if flags.is_enabled("flag-key") {
    // Do something differently for this user
}

Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

Resource Description
Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/usage.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

iOS SDK usage

Capturing events

You can send custom events using capture:

Swift

PostHogSDK.shared.capture("user_signed_up")

Tip: We recommend using a [object] [verb] format for your event names, where [object] is the entity that the behavior relates to, and [verb] is the behavior itself. For example, project created, user signed up, or invite sent.

Setting event properties

Optionally, you can include additional information with the event by including a properties (/docs/data/events.md#event-properties) object:

Swift

PostHogSDK.shared.capture("user_signed_up", properties: ["login_type": "email"], userProperties: ["is_free_trial": true])

Autocapture

PostHog autocapture automatically tracks the following events for you:

  • Application Opened – when the app is opened from a closed state or when the app comes to the foreground (e.g. from the app switcher)
  • Application Backgrounded – when the app is sent to the background by the user
  • Application Installed – when the app is installed
  • Application Updated – when the app is updated
  • $screen – when the user navigates (if using UIViewController)
  • $autocapture – when the user interacts with elements in a screen (UIKit based) and captureElementInteractions is enabled
  • $rageclick – when the user rapidly taps in the same area (iOS/macCatalyst, UIKit based)

🚧 Note: $autocapture and $rageclick are captured from UIKit interactions. Some SwiftUI views use UIKit under the hood (for example, TextField → UITextField and Toggle → UISwitch), so those interactions may also be autocaptured. In other SwiftUI cases, interactions might still be captured, but element metadata (such as $elements_chain) may be incomplete.

Capturing screen views

With configuration.captureScreenViews (/docs/libraries/ios/configuration.md#all-configuration-options) set as true, PostHog will try to record all screen changes automatically.

If you want to manually send a new screen capture event, use the screen function.

Swift

PostHogSDK.shared.screen("Dashboard", properties: ["fromIcon": "bottom"])

Important: While captureScreenViews works with both UIKit and SwiftUI, the screen names captured in SwiftUI may not be very meaningful as they are based on internal SwiftUI view identifiers. For SwiftUI applications, we recommend turning this option off and instead using the .postHogScreenView() view modifier (see next section) to capture screen views with meaningful names.

Note: You can use the BeforeSendBlock to filter or drop any undesired screen events, giving you control over which screen views are sent to PostHog. See Amending, dropping or sampling events (/docs/libraries/ios.md#amending-dropping-or-sampling-events) for implementation examples.

Capturing screen views in SwiftUI

To track a screen view in SwiftUI, apply the postHogScreenView modifier to your full-screen views. PostHog will send a $screen event when the onAppear action is executed and will infer a screen name based on the view's type. You can provide a custom name and event properties if needed.

HomeView.swift

// This will trigger a screen view event with $screen_name: "HomeViewContent"
struct HomeView: View {
    var body: some View {
        HomeViewContent()
            .postHogScreenView()
    }
}

// This will trigger a screen view event with $screen_name: "My Home View" and an additional event property from_button: "start"
struct HomeView: View {
    var body: some View {
        HomeViewContent()
            .postHogScreenView("My Home View", ["from_button": "start"])
    }
}

In SwiftUI, views can range from entire screens to small UI components. Unlike UIKit, SwiftUI doesn't clearly distinguish between these levels, which makes automatic tracking of full-screen views harder.

Adding a custom label on autocaptured elements

PostHog automatically captures interactions with various UI elements in your app, but these interactions are often identified by element type names (e.g., UIButton, UITextField, UILabel).

While this provides basic tracking, it can be challenging to pinpoint specific interactions with particular elements in your analytics. To make your data more meaningful and actionable, you can assign custom labels to any autocaptured element. These labels act as descriptive identifiers, making it easier to identify, filter, and analyze events in your reports.

Adding a custom label in UIKit

To assign a custom label to a UIView, use the postHogLabel property:

Swift

let view = UIView()
view.postHogLabel = "usernameTextField"

In this example, interactions with the UITextField will be captured with an additional identifier "usernameTextField".

Adding a custom label in SwiftUI

In SwiftUI, use the .postHogLabel(_:) modifier instead:

Swift

var body: some View {
    ...
    TextField("username", text: $username)
        .postHogLabel("usernameTextField")
}

Since SwiftUI's TextField uses UITextField under the hood, interactions with it will be autocaptured with the additional identifier "usernameTextField".

Example of generated analytics data

The generated analytics element in the examples above will have the following form:

Swift

<UITextField id="usernameTextField">text value</UITextField>

Filtering for labeled autocaptured elements in reports

To locate and filter interactions with specific elements in PostHog reports, you can use Autocapture element filters, such as:

  • Tag Name (UITextField in this example)
  • Text (text value in this example)
  • CSS Selector (the generated id attribute in this example)

In the examples above, we can filter for the specific text field using the CSS Selector #usernameTextField

Interaction autocapture

Interaction autocapture records when users interact with UI elements in your app. This includes:

  • User interactions like touch, swipe, pan, pinch, rotation, long_press, scroll
  • Control types value_changed, submit, toggle, primary_action, menu_action, change

Interaction autocapture is not enabled by default. You can enable it by setting captureElementInteractions to true in the config.

Swift

let config = PostHogConfig(projectToken: "<ph_project_token>", host: "https://us.i.posthog.com")
config.captureElementInteractions = true // Disabled by default
PostHogSDK.shared.setup(config)
Rage click autocapture

Note: Rage click autocapture for iOS/macCatalyst is available in version 3.51.0+.

A rage click is when a user taps an area multiple times in quick succession (e.g more than 3 taps in 1 second).

This is captured as a $rageclick event. You can use this event to identify opportunities to improve your UI, since it's a good indication that users may be frustrated with your product.

It is enabled by default (rageClickConfig.enabled = true).

Swift

let config = PostHogConfig(projectToken: "<ph_project_token>", host: "https://us.i.posthog.com")
config.rageClickConfig.enabled = true // Enabled by default
config.rageClickConfig.minimumTapCount = 3 // Optional, default is 3
config.rageClickConfig.thresholdPoints = 30 // Optional, default is 30
config.rageClickConfig.timeoutInterval = 1.0 // Optional, default is 1.0s
PostHogSDK.shared.setup(config)
Autocapture configuration

You can enable or disable autocapture through the PostHogConfig object. Find more details about autocapture configuration in the configuration page (/docs/libraries/ios/configuration.md#autocapture-configuration).

Preventing sensitive data capture

To exclude specific UI elements from autocapture or Session Replay, add ph-no-capture as either an accessibilityLabel or accessibilityIdentifier. See privacy controls (/docs/session-replay/privacy?tab=iOS.md) for masking behavior and iOS examples.

Identifying users

We highly recommend reading our section on Identifying users (/docs/integrate/identifying-users.md) to better understand how to correctly use this method.

Using identify, you can associate events with specific users. This enables you to gain full insights as to how they're using your product across different sessions, devices, and platforms.

An identify call has the following arguments:

  • distinct_id which uniquely identifies your user in your database

  • userProperties: Optional. A dictionary with key:value pairs to set the person properties (/docs/product-analytics/person-properties.md)

  • userPropertiesSetOnce: Optional. Similar to userProperties. See the difference between userProperties and userPropertiesSetOnce (/docs/product-analytics/person-properties.md#what-is-the-difference-between-set-and-set_once)

Swift

PostHogSDK.shared.identify("user_id_from_your_database",
                            userProperties: ["name": "Peter Griffin", "email": "peter@familyguy.com"],
                            userPropertiesSetOnce: ["date_of_first_log_in": "2024-03-01"])

You should call identify as soon as you're able to. Typically, this is after your user logs in. This ensures that events sent during your user's sessions are correctly associated with them.

When you call identify, all previously tracked anonymous events will be linked to the user.

Get the current user's distinct ID

You may find it helpful to get the current user's distinct ID. For example, to check whether you've already called identify for a user or not.

To do this, call getDistinctId(). This returns either the ID automatically generated by PostHog or the ID that has been passed by a call to identify().

Alias

Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend.

In this case, you can use alias to assign another distinct ID to the same user.

Swift

PostHogSDK.shared.alias("alias_id")

We strongly recommend reading our docs on alias (/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method.

Anonymous vs identified events

PostHog captures two types of events: anonymous and identified (/docs/data/anonymous-vs-identified-events.md)

Identified events enable you to attribute events to specific users, and attach person properties (/docs/product-analytics/person-properties.md). They're best suited for logged-in users.

Scenarios where you want to capture identified events are:

  • Tracking logged-in users in B2B and B2C SaaS apps
  • Doing user segmented product analysis
  • Growth and marketing teams wanting to analyze the complete conversion lifecycle

Anonymous events are events without individually identifiable data. They're best suited for web analytics (/docs/web-analytics.md) or apps where users aren't logged in.

Scenarios where you want to capture anonymous events are:

  • Tracking a marketing website
  • Content-focused sites
  • B2C apps where users don't sign up or log in

Under the hood, the key difference between identified and anonymous events is that for identified events we create a person profile (/docs/data/persons.md) for the user, whereas for anonymous events we do not.

Important: Due to the reduced cost of processing them, anonymous events can be up to 4x cheaper than identified ones, so we recommended you only capture identified events when needed.

How to capture anonymous events

The iOS SDK captures anonymous events by default. However, this may change depending on your personProfiles config (/docs/libraries/ios/configuration.md#all-configuration-options) when initializing PostHog:

  1. personProfiles: .identifiedOnly (recommended) (default) - Anonymous events are captured by default. PostHog only captures identified events for users where person profiles (/docs/data/persons.md) have already been created.

  2. personProfiles: .always - Capture identified events for all events.

  3. personProfiles: .never - Capture anonymous events for all events.

For example:

iOS

let config = PostHogConfig(
    projectToken: POSTHOG_PROJECT_TOKEN,
    host: POSTHOG_HOST
)
config.personProfiles = .identifiedOnly
PostHogSDK.shared.setup(config)
How to capture identified events

If you've set the personProfiles config (/docs/libraries/ios/configuration.md#all-configuration-options) to .identifiedOnly (the default option), anonymous events are captured by default. Then, to capture identified events, call any of the following functions:

  • identify() (/docs/product-analytics/identify.md)
  • alias() (/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user)
  • group() (/docs/product-analytics/group-analytics.md)

When you call any of these functions, it creates a person profile (/docs/data/persons.md) for the user. Once this profile is created, all subsequent events for this user will be captured as identified events.

Alternatively, you can set personProfiles to .always to capture identified events by default.

Setting person properties

To set properties (/docs/product-analytics/person-properties.md) on your users via an event, you can leverage the event properties userProperties and userPropertiesSetOnce.

When capturing an event, you can pass a property called $set as an event property, and specify its value to be an object with properties to be set on the user that will be associated with the user who triggered the event.

Swift

PostHogSDK.shared.capture("signed_up", properties: ["plan": "Pro++"], userProperties: ["user_property_name": "your_value"])

userPropertiesSetOnce works just like userProperties, except that it will only set the property if the user doesn't already have that property set.

Swift

PostHogSDK.shared.capture("signed_up", properties: ["plan": "Pro++"], userPropertiesSetOnce: ["user_property_name": "your_value"])

Use setPersonProperties when you want to update the current person's profile without also capturing a custom event. This sends a $set event to PostHog.

Swift

PostHogSDK.shared.setPersonProperties(userPropertiesToSet: ["plan": "Pro++"])

PostHogSDK.shared.setPersonProperties(
    userPropertiesToSet: ["plan": "Pro++"],
    userPropertiesToSetOnce: ["first_seen_source": "ios"]
)

Super properties

Super properties are properties associated with events that are set once and then sent with every capture call, be it a $screen, or anything else.

They are set using PostHogSDK.shared.register, which takes a properties object as a parameter, and they persist across sessions.

For example, take a look at the following call:

Swift

PostHogSDK.shared.register(["team_id": 22])

The call above ensures that every event sent by the user will include "team_id": 22. This way, if you filtered events by property using team_id = 22, it would display all events captured on that user after the PostHogSDK.shared.register call, since they all include the specified Super Property.

However, please note that this does not store properties against the User, only against their events. To store properties against the User object, you should use PostHogSDK.shared.identify. More information on this can be found on the Sending User Information section (#sending-user-information).

Removing stored super properties

Super properties persist across sessions so you have to explicitly remove them if they are no longer relevant. To stop sending a super property with events, you can use PostHogSDK.shared.unregister, like so:

Swift

PostHogSDK.shared.unregister("team_id")

This removes the super property and subsequent events will not include it.

If you are doing this as part of a user logging out, you can instead simply use PostHogSDK.shared.reset which clears all super properties and more.

Reset after logout

To reset the user's ID and anonymous ID after logout, call reset. See Identifying users (/docs/product-analytics/identify.md#reset) for the shared reset guidance and iOS example.

Group analytics

Group analytics allows you to associate the events for that person's session with a group (e.g. teams, organizations, etc.). See Group Analytics (/docs/product-analytics/group-analytics.md) for iOS examples and implementation details.

Note: This is a paid feature and is not available on the open-source or free cloud plan. Learn more on the pricing page (/pricing.md).

Opt out of data capture

You can completely opt users out from data capture by default or on a per-person basis. See Complete opt-out (/docs/product-analytics/privacy.md#complete-opt-out) for iOS examples.

Feature flags

PostHog's feature flags (/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them.

Boolean feature flags

Swift

if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled {
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    let matchedFlagPayload = result.payload
}
Multivariate feature flags

Swift

if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant
    // Do something differently for this user

    // Optional: fetch the payload from the same evaluation result
    let matchedFlagPayload = result.payload
}
Typed payloads

If your payload is a JSON object, you can decode it into a Decodable type:

Swift

struct FlagPayload: Decodable {
    let title: String
}

if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"),
   let payload = result.payloadAs(FlagPayload.self) {
    // Use payload.title
}
Inspecting all feature flags

You can inspect all currently loaded feature flags with getAllFeatureFlags(). It returns each flag's key, enabled state, variant, and payload, and does not send a $feature_flag_called event, so calling it won't affect your experiment results or flag usage analytics:

Swift

for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] {
    print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any)
}
Reloading feature flags

Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call:

Swift

PostHogSDK.shared.reloadFeatureFlags()
Ensuring flags are loaded before usage

Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage.

This means that for most screens, the feature flags are available immediately – except for the first time a user visits.

To handle this, you can use the didReceiveFeatureFlags notification to wait for the feature flag request to finish:

Swift

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // register for `didReceiveFeatureFlags` notification before SDK initialization
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(receiveFeatureFlags),
            name: PostHogSDK.didReceiveFeatureFlags,
            object: nil
        )

        let POSTHOG_PROJECT_TOKEN = "<ph_project_token>"
        // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
        let POSTHOG_HOST = "https://us.i.posthog.com"

        let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST)

        PostHogSDK.shared.setup(config)

        return true
    }

    // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server.
    @objc func receiveFeatureFlags() {
        print("receiveFeatureFlags called")
    }
}

Alternatively, you can use the completion block of the reloadFeatureFlags(_:) method. This allows you to execute logic immediately after the flags are reloaded:

Swift

// Reload feature flags and check if a specific feature is enabled
PostHogSDK.shared.reloadFeatureFlags {
    if PostHogSDK.shared.isFeatureEnabled("flag-key") {
        // do something
    }
}
Tracking feature usage

To track when someone sees or interacts with a feature, use captureFeatureView and captureFeatureInteraction.

Swift

PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key")
PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key")
Bootstrapping flags

Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag.

To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. After the SDK fetches feature flags from PostHog, it will use those flag values instead of bootstrapped ones.

Set config.bootstrap before calling setup() to seed identity and flag values before the first /flags response (requires iOS SDK 3.66.0+):

Swift

let config = PostHogConfig(projectToken: "<ph_project_token>", host: "https://us.i.posthog.com")
config.bootstrap = PostHogBootstrapConfig(
    distinctId: "distinct_id_of_your_user",
    isIdentifiedId: true,
    featureFlags: [
        "flag-1": true,
        "variant-flag": "control"
    ],
    featureFlagPayloads: nil
)
PostHogSDK.shared.setup(config)
  • Bootstrapped identity applies during setup. On a fresh install, setting it before setup() means events captured synchronously during initialization (like Application Installed) carry your distinct ID instead of the SDK-generated UUID.
    • An anonymous bootstrap (isIdentifiedId: false, the default) seeds the anonymous ID only when none is persisted yet. Once an anonymous ID exists on disk, or the person has been identified, the SDK ignores it.
    • An identified bootstrap (isIdentifiedId: true) is for a signed-in identity available to your app (for example, from a backend session token). On a fresh install, it seeds the distinct ID, marks the person identified, and generates a separate device ID. On a returning install, a matching anonymous ID is marked identified without emitting $identify; a different anonymous ID is merged via identify() when person profiles are enabled. This emits $identify unless capturing is opted out. A different, already-identified person is left untouched.
  • Bootstrapped flags are served until the first /flags response, then replaced. A complete /flags response takes over entirely, so bootstrapped-only keys don't persist past it. Only enabled flags are seeded: a true boolean or a non-empty variant string. A false or empty value is dropped, matching posthog-js. Seed payloads with the separate featureFlagPayloads option. Flag values and payloads must be JSON-serializable, or they're dropped. Bootstrapped flags are cleared on reset().

The feature-flags-loaded signal fires as soon as bootstrapped flags are applied, so startup logic can read them immediately. These SDKs don't support the sessionID bootstrap option. When person profiles are set to never, the SDK preserves a different anonymous identity instead of merging it into an identified bootstrap.

See the SDK bootstrapping guide (/docs/libraries/bootstrapping.md) for the cross-SDK overview.

Experiments (A/B tests)

Since experiments (/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code. See adding experiment code (/docs/experiments/adding-experiment-code.md) for iOS examples.

It's also possible to run experiments without using feature flags (/docs/experiments/running-experiments-without-feature-flags.md).

A note about IDFA (identifier for advertisers) collection in iOS 14

Starting with iOS 14, Apple will further restrict apps that track users. Any references to Apple's AdSupport framework, even in strings, will trip the App Store's static analysis.

Hence starting with posthog-ios version 1.2.0 we have removed all references to Apple's AdSupport framework.

Session replay

Note: Session replay is currently only available on iOS. For future macOS support, please follow and upvote this GitHub issue.

To set up session replay (/docs/session-replay/mobile.md) in your project, all you need to do is install the iOS SDK, enable "Record user sessions" in your project settings and enable the sessionReplay option.

Surveys

Surveys (/docs/surveys.md) launched with popover presentation (/docs/surveys/creating-surveys.md#presentation) are automatically shown to users matching the display conditions (/docs/surveys/creating-surveys.md#display-conditions) you set up.

Error tracking

To set up error tracking in your project, see the error tracking docs (/docs/error-tracking.md).

Debug mode

If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening.

You can enable debug mode by setting the debug option to true in the PostHogConfig object. A common pattern is to set this to true in development environments only for local development.

Swift

let config = PostHogConfig(projectToken: "<ph_project_token>", host: "https://us.i.posthog.com")
config.debug = true
PostHogSDK.shared.setup(config)

This will enable verbose logs about the inner workings of the SDK.

You can also toggle debug by calling the PostHogSDK.shared.debug() method in your code.

Swift

// Enable debug mode
PostHogSDK.shared.debug(true)

// Disable debug mode
PostHogSDK.shared.debug(false)
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

references/web.md

AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt

Web Feature Flags installation

  1. 1

    Choose an installation method

    Required

    You can either add the JavaScript snippet directly to your HTML or install the JavaScript SDK via your package manager.

    HTML snippet

    Add this snippet to your website within the <head> tag. This can also be used in services like Google Tag Manager:

    HTML

    <script>
        !function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}p||((p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",p.onerror=function(){p=null},(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r));var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],Object.defineProperty(u,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e}}),Object.defineProperty(u.people,"toString",{configurable:!0,enumerable:!0,writable:!0,value:function(){return u.toString(1)+".people (stub)"}}),o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagResult isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
        posthog.init('<ph_project_token>', {
            api_host: 'https://us.i.posthog.com',
            defaults: '2026-05-30',
        })
    </script>

    JavaScript SDK

    Install the PostHog JavaScript library using your package manager. Then, import and initialize the PostHog library with your project token and host:

    npm
    npm install posthog-js
    yarn
    yarn add posthog-js
    pnpm
    pnpm add posthog-js
    bun
    bun add posthog-js

    JavaScript

    import posthog from 'posthog-js'
    
    posthog.init('<ph_project_token>', {
        api_host: 'https://us.i.posthog.com',
        defaults: '2026-05-30'
    })
  2. 2

    Send events

    Recommended

    Once installed, PostHog will automatically start capturing events. You can also manually send events to test your integration:

    Click around and view a couple pages to generate some events. PostHog automatically captures pageviews, clicks, and other interactions for you.

    If you'd like, you can also manually capture custom events:

    JavaScript

    posthog.capture('my_custom_event', { property: 'value' })
  3. 3

    Use boolean feature flags

    Required

    Check if a feature flag is enabled:

    if (posthog.isFeatureEnabled('flag-key')) {
        // Do something differently for this user
        // Optional: fetch the payload
        const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload
    }
  4. 4

    Use multivariate feature flags

    Optional

    For multivariate flags, check which variant the user has been assigned:

    const matchedFlag = posthog.getFeatureFlagResult('flag-key')
    if (matchedFlag?.variant == 'variant-key') { // replace 'variant-key' with the key of your variant
        // Do something differently for this user
        // Optional: read the payload from the same result
        const matchedFlagPayload = matchedFlag?.payload
    }
  5. 5

    Use feature flag payloads

    Optional

    Feature flags can include payloads with additional data. Fetch the payload like this:

    const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload
  6. 6

    Ensure flags are loaded

    Optional

    Every time a user loads a page, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in your chosen persistence option (local storage by default).

    This means that for most pages, the feature flags are available immediately — except for the first time a user visits.

    To handle this, you can use the onFeatureFlags callback to wait for the feature flag request to finish:

    posthog.onFeatureFlags(function (flags, flagVariants, { errorsLoading }) {
        // feature flags are guaranteed to be available at this point
        if (posthog.isFeatureEnabled('flag-key')) {
            // do something
        }
    })
  7. 7

    Reload feature flags

    Optional

    Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values:

    posthog.reloadFeatureFlags()
  8. 8

    Running experiments

    Optional

    Experiments run on top of our feature flags. Once you've implemented the flag in your code, you run an experiment by creating a new experiment in the PostHog dashboard.

  9. 9

    Next steps

    Recommended

    Now that you're evaluating flags, continue with the resources below to learn what else Feature Flags enables within the PostHog platform.

    Resource Description
    Creating a feature flag (/docs/feature-flags/creating-feature-flags.md) How to create a feature flag in PostHog
    Adding feature flag code (/docs/feature-flags/adding-feature-flag-code.md) How to check flags in your code for all platforms
    Framework-specific guides (/docs/feature-flags/tutorials.md#framework-guides) Setup guides for React Native, Next.js, Flutter, and other frameworks
    How to do a phased rollout (/tutorials/phased-rollout.md) Gradually roll out features to minimize risk
    More tutorials (/docs/feature-flags/tutorials.md) Other real-world examples and use cases
Still have questions?

Ask PostHog AI

Was this page useful?

HelpfulCould be better

Frontmatter written into each target's SKILL.md.

Common

No fields set for this target.

Ready to ship better, together?

Spec it. Decompose it. Ship it. All with your AI agent.

Start for free

Join engineers building with Athenode today.