instrument-feature-flags
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.
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-gettool to retrieve the project'sapi_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-getMCP response for aregionfield —USmaps tohttps://us.i.posthog.com,EUmaps tohttps://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 installationreferences/react-native.md- React native feature flags installationreferences/web.md- Web feature flags installationreferences/nodejs.md- Node.js feature flags installationreferences/python.md- Python feature flags installationreferences/django.md- Djangoreferences/flask.md- Flaskreferences/php.md- Php feature flags installationreferences/laravel.md- Laravelreferences/ruby.md- Ruby feature flags installationreferences/ruby-on-rails.md- Ruby on railsreferences/go.md- Go feature flags installationreferences/java.md- Java feature flags installationreferences/rust.md- Rust feature flags installationreferences/dotnet.md- .net feature flags installationreferences/dotnet.md- .netreferences/elixir.md- Elixir feature flags installationreferences/android.md- Android feature flags installationreferences/ios.md- Ios feature flags installationreferences/usage.md- Ios SDK usagereferences/flutter.md- Flutter feature flags installationreferences/api.md- API feature flags installationreferences/next-js.md- Next.jsreferences/adding-feature-flag-code.md- Adding feature flag codereferences/best-practices.md- Best practices for production-ready flagsreferences/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
- references/COMMANDMENTS.md
- references/adding-feature-flag-code.md
- references/android.md
- references/api.md
- references/best-practices.md
- references/django.md
- references/dotnet.md
- references/elixir.md
- references/flask.md
- references/flutter.md
- references/go.md
- references/ios.md
- references/java.md
- references/laravel.md
- references/next-js.md
- references/nodejs.md
- references/php.md
- references/python.md
- references/react-native.md
- references/react.md
- references/ruby-on-rails.md
- references/ruby.md
- references/rust.md
- references/usage.md
- references/web.md
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 istrueif the request timed out or if there was an error. It will befalseorundefinedif 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:
$geoip_city_name$geoip_country_name$geoip_country_code$geoip_continent_name$geoip_continent_code$geoip_postal_code$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:
- Using hooks.
- 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
matchon the component can be eithertrue, 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
fallbackattribute.
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(), andcapture({ sendFeatureFlags: true })still work during the migration period, but they're deprecated. PreferevaluateFlags()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(), andposthog.capture(send_feature_flags=True)still work during the migration period, but they're deprecated. Preferposthog.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 userOverriding 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(), andcapture(['send_feature_flags' => true])still work during the migration period, but they're deprecated. PreferevaluateFlags()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')
endMultivariate 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')
endflags.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(), andcapture({ ..., send_feature_flags: true })still work during the migration period, but they're deprecated. Preferevaluate_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
endOverriding 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(), andCapture.SendFeatureFlagsstill work during the migration period, but they're deprecated. PreferEvaluateFlags()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:
- Using hooks.
- 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')?.payloadInspecting 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:
- $geoip_city_name
- $geoip_country_name
- $geoip_country_code
- $geoip_continent_name
- $geoip_continent_code
- $geoip_postal_code
- $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
onFeatureFlagscallback, you must set up the SDK manually (#installation). On Android and iOS, disablecom.posthog.posthog.AUTO_INITfirst.
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(), andPostHogCaptureOptions.builder().appendFeatureFlags(true)still work during the migration period, but they're deprecated. PreferevaluateFlags()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(), andclient.get_feature_flags()still work during the migration period, but they're deprecated. Preferevaluate_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")
endMultivariate 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")
endPostHog.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, andPostHog.FeatureFlags.get_feature_flag_result!/2still work during the migration period, but they're deprecated. Preferevaluate_flags/1for 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(), andCapture(..., sendFeatureFlags: true, ...)still work during the migration period, but they're deprecated. PreferEvaluateFlagsAsync()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
groupskey is only required for group-based feature flags. If you use it, replacegroup_typeandgroup_idwith the values for your group such ascompany: "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_environmentsis 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:
User-Agent patterns - The system analyzes the User-Agent header:
- Client-side patterns:
Mozilla/,Chrome/,Safari/,Firefox/,Edge/(browsers), or mobile SDKs likeposthog-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/
- Client-side patterns:
Browser-specific headers - Presence of these headers indicates client-side:
OriginheaderRefererheaderSec-Fetch-ModeheaderSec-Fetch-Siteheader
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-sideJavaScript
// 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" parameterThis 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 withholdout-.
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:
errorsWhileComputingFlagswill returntrueif 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:
- Your feature flag evaluations have been temporarily paused because you've exceeded your feature flag quota
- 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:
$feature_flag_response: This is the name of the variant the user has been assigned to e.g., "control" or "test"$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=2Python
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=2Python
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:
$geoip_city_name$geoip_country_name$geoip_country_code$geoip_continent_name$geoip_continent_code$geoip_postal_code$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
Install the dependency
Required
Add the PostHog Android SDK to your
build.gradledependencies:build.gradle
dependencies { implementation("com.posthog:posthog-android:3.+") }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
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
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
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
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
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
Evaluate the feature flag value using flags
Required
flagsis 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
groupskey is only required for group-based feature flags. If you use it, replacegroup_typeandgroup_idwith the values for your group such ascompany: "Twitter".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
Send a $feature_flag_called event
Optional
To track usage of your feature flag and view related analytics in PostHog, submit the
$feature_flag_calledevent whenever you check a feature flag value in your code.You need to include two properties with this event:
$feature_flag_response: This is the name of the variant the user has been assigned to e.g., "control" or "test"$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
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
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
undefinedexplicitly (#undefined-is-not-false) – it means "not evaluated yet," notfalse. - 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.31On 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 / 100For 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:
- 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).
- Output problems. The flag returned the right value but your code misread it –
undefinedtreated asfalse, no handling for the loading gap, evaluating repeatedly instead of recording the result. - 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 is the recommended default
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_enabledis clearer thanis_dashboard_enabled. - Use name types. Suffix with the purpose:
new-billing-experiment,new-billing-release. - Reflect the return type.
is_premium_userfor a boolean,selected_themefor a string. - Use positive language for booleans.
is_premium_userinstead ofis_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.xof 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_idto 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-IDheader, if present - Distinct ID from the
X-POSTHOG-DISTINCT-IDheader, if present, falling back to the authenticated Django user'spk(Django's primary-key alias, which works with custom user models) - User email from the authenticated Django user's
emailasemail - Current URL as
$current_url - Request method as
$request_method - Request path as
$request_path - Forwarded IP address from
X-Forwarded-Foras$ip - User agent from
User-Agentas$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 = FalseAdding 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_tagsFiltering 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_requestModifying 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_tagsComplete 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 = TrueAll 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.AspNetCoreIn 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.comis 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 PostHogThe 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_idthat matches the ID your frontend uses when callingposthog.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, orinvite 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(), andCapture(..., sendFeatureFlags: true, ...)still work during the migration period, but they're deprecated. PreferEvaluateFlagsAsync()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"}
]
endConfiguration
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.xof 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 '', 204You can find your project token and instance address in your project settings.
Identifying users
Identifying users is required. Backend events need a
distinct_idto 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 '', 204Events 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 responseNext 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
Install the package
Required
Add the PostHog Flutter SDK to your
pubspec.yaml:pubspec.yaml
posthog_flutter: ^5.24.02
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 configTab
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
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
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
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
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
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
Install the package
Required
Install the PostHog Go library:
Terminal
go get "github.com/posthog/posthog-go"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
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
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
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
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 (recommended)
Set
SendFeatureFlagstotruein 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_nameproperty 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
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
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
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
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
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
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
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
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
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
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.comAdd 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:
- A PostHog instance (either Cloud or self-hosted (/docs/self-host.md))
- 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-jsYarn
yarn add posthog-jspnpm
pnpm add posthog-jsBun
bun add posthog-jsIf 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.comthat 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-srccovers the snippet and the lazy-loaded bundles,connect-srccovers event ingestion and feature flags, andworker-srccovers 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 wherecaptureandidentifycalls never send, so the integration looks complete while zero events arrive. Rememberconnect-srcfalls back todefault-src, sodefault-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.comThese 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-jsfunctions 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 --saveYarn
yarn add posthog-nodepnpm
pnpm add posthog-nodeBun
bun add posthog-nodeRouter-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
flushAtto1andflushIntervalto0.
flushAtsets how many capture calls we should flush the queue (in one batch).flushIntervalsets 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 callawait 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 }, sosession.user.idisundefineduntil you add it yourself with a session callback in yourauthOptions: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
undefineddistinct 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
Install the package
Required
Install the PostHog Node.js library using your package manager:
npm
npm install posthog-nodeyarn
yarn add posthog-nodepnpm
pnpm add posthog-nodebun
bun add posthog-node2
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
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
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
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
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 (recommended)
Set
sendFeatureFlagstotruein 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_nameproperty 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
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
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
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
Install the package
Required
Install the PostHog PHP library using Composer:
Terminal
composer require posthog/posthog-php2
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
Send events
Recommended
Once installed, you can manually send events to test your integration:
PHP
PostHog::capture([ 'distinctId' => 'test-user', 'event' => 'test-event', ]);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
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
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 (recommended)
Set
send_feature_flagstotruein 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_nameproperty 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
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
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
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
Install the package
Required
Install the PostHog Python library using pip:
Terminal
pip install posthog2
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
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
capturemethod with an event name and properties:Python
import posthog posthog.capture('user_signed_up', distinct_id='user_123', properties={'example_property': 'example_value'})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
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
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 (recommended)
Set
send_feature_flagstoTruein 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_nameproperty 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
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
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
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
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-localizationyarn
yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info react-native-localize # for iOS cd ios && pod installnpm
npm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info react-native-localize # for iOS cd ios && pod install2
Configure PostHog
Required
PostHog is most easily used via the
PostHogProvidercomponent. 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
Send events
Recommended
Once installed, PostHog will automatically start capturing events. You can also manually send events using the
usePostHoghook: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
Use feature flags
Required
PostHog provides hooks to make it easy to use feature flags in your React Native app. Use
useFeatureFlagEnabledfor 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
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
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
Install the package
Required
Install
posthog-jsand@posthog/reactusing your package manager:npm
npm install posthog-js @posthog/reactyarn
yarn add posthog-js @posthog/reactpnpm
pnpm add posthog-js @posthog/reactbun
bun add posthog-js @posthog/react2
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.com3
Initialize PostHog
Required
Wrap your app with the
PostHogProvidercomponent at the root of your application (such asmain.tsxif 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
defaultsoption automatically configures PostHog with recommended settings for new projects. See SDK defaults (/docs/libraries/js.md#sdk-defaults) for details.4
Accessing PostHog in your code
Recommended
Use the
usePostHoghook to access the PostHog instance in any component wrapped byPostHogProvider: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
posthogdirectly 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
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
Use feature flags
Required
Using hooks
PostHog provides several hooks to make it easy to use feature flags in your React app. Use
useFeatureFlagEnabledfor 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
useFeatureFlagPayloadhook does not send a$feature_flag_calledevent, which is required for experiments. Always use it withuseFeatureFlagEnabledoruseFeatureFlagVariantKey: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
PostHogFeaturecomponent 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
matchprop can be eithertrue, or the variant key, to match on a specific variant. If you also want to show a default message, you can pass these in thefallbackprop.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
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
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.loggeroutput 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 installIdentifying users
Identifying users is required. Backend events need a
distinct_idthat matches the ID your frontend uses when callingposthog.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:installThis 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?
endYou can find your project token and instance address in your project settings.
Tip: Use
Rails.application.credentialsto avoid hardcoding API keys. First, add your keys and then reference them in your initializer:Terminal
rails credentials:editconfig/credentials.yml.enc
posthog: api_key: <ph_project_token> host: https://us.i.posthog.com personal_api_key: phx_xxxxxxxxxconfig/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 = falseLogs
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
endreport_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
endAssociating 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
endYou 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
endRails 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_userUser ID extraction
By default, PostHog Rails auto-detects the user's distinct ID by trying these methods in order:
posthog_distinct_id– Define this on your User model for full controldistinct_id– Common analytics conventionid– Standard ActiveRecord primary keypk– Primary key aliasuuid– 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 = :emailOr 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
endExcluded exceptions
The following exceptions are not reported by default (common 4xx errors):
AbstractController::ActionNotFoundActionController::BadRequestActionController::InvalidAuthenticityTokenActionController::InvalidCrossOriginRequestActionController::MethodNotAllowedActionController::NotImplementedActionController::ParameterMissingActionController::RoutingErrorActionController::UnknownFormatActionController::UnknownHttpMethodActionDispatch::Http::Parameters::ParseErrorActiveRecord::RecordNotFoundActiveRecord::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
endFor 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
endWhen 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, andPostHog.capture({ ..., send_feature_flags: true })still work during the migration period, but they're deprecated. PreferPostHog.evaluate_flagsfor 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
endOr in your specs:
spec/rails_helper.rb
RSpec.configure do |config|
config.before(:each) do
allow(PostHog).to receive(:capture)
end
endConfiguration 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
Verify PostHog is initialized:
Ruby
Rails.console > PostHog.initialized? => trueCheck your excluded exceptions list.
Verify middleware is installed:
Ruby
Rails.application.middleware
User context not working
- Verify
current_user_methodmatches your controller method. - Check that the user object responds to
posthog_distinct_id,distinct_id,id,pk, oruuid. - 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
Install the gem
Required
Add the PostHog Ruby gem to your Gemfile:
Gemfile
gem "posthog-ruby"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
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
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') end5
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') end6
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 (recommended)
Set
send_feature_flagstotruein 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_nameproperty 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
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
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
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(), andclient.get_feature_flags()still work during the migration period, but they're deprecated. Preferevaluate_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, orinvite 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) andcaptureElementInteractionsis enabled - $rageclick – when the user rapidly taps in the same area (iOS/macCatalyst,
UIKit based)
🚧 Note:
$autocaptureand$rageclickare captured from UIKit interactions. Some SwiftUI views use UIKit under the hood (for example,TextField→UITextFieldandToggle→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
captureScreenViewsworks with bothUIKitandSwiftUI, the screen names captured inSwiftUImay not be very meaningful as they are based on internal SwiftUI view identifiers. ForSwiftUIapplications, 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
BeforeSendBlockto 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 (
UITextFieldin this example) - Text (
text valuein this example) - CSS Selector (the generated
idattribute 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_idwhich uniquely identifies your user in your databaseuserProperties: 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 betweenuserPropertiesanduserPropertiesSetOnce(/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:
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.personProfiles: .always- Capture identified events for all events.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 (likeApplication 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 viaidentify()when person profiles are enabled. This emits$identifyunless capturing is opted out. A different, already-identified person is left untouched.
- An anonymous bootstrap (
- Bootstrapped flags are served until the first
/flagsresponse, then replaced. A complete/flagsresponse takes over entirely, so bootstrapped-only keys don't persist past it. Only enabled flags are seeded: atrueboolean or a non-empty variant string. Afalseor empty value is dropped, matching posthog-js. Seed payloads with the separatefeatureFlagPayloadsoption. Flag values and payloads must be JSON-serializable, or they're dropped. Bootstrapped flags are cleared onreset().
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
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-jsyarn
yarn add posthog-jspnpm
pnpm add posthog-jsbun
bun add posthog-jsJavaScript
import posthog from 'posthog-js' posthog.init('<ph_project_token>', { api_host: 'https://us.i.posthog.com', defaults: '2026-05-30' })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
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
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
Use feature flag payloads
Optional
Feature flags can include payloads with additional data. Fetch the payload like this:
const matchedFlagPayload = posthog.getFeatureFlagResult('flag-key')?.payload6
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
onFeatureFlagscallback 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
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
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
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.