From 7df70943a38fd0cd272196b6db1133841cb4feb0 Mon Sep 17 00:00:00 2001 From: "releaser-ai-plugin[bot]" <273148615+releaser-ai-plugin[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:34:39 +0000 Subject: [PATCH] chore: sync skills (agent-skills-v0.97.0, context-mill@v1.13.1) --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- gemini-extension.json | 2 +- skills/.sync-manifest | 3 + skills/formatting-insight-axes/SKILL.md | 180 +++ .../references/nextjs.md | 2 +- .../references/adding-feature-flag-code.md | 1104 ++++++++--------- .../references/rust.md | 174 ++- .../instrument-integration/references/ios.md | 4 +- .../references/posthog-js.md | 2 +- .../references/posthog-node.md | 2 +- .../references/posthog-python.md | 4 +- .../references/react-native.md | 2 +- .../instrument-integration/references/ruby.md | 180 +-- .../references/anthropic.md | 2 +- .../references/autogen.md | 2 +- .../references/azure-openai.md | 2 +- .../references/cerebras.md | 2 +- .../references/cohere.md | 2 +- .../references/deepseek.md | 2 +- .../references/fireworks-ai.md | 2 +- .../references/google.md | 2 +- .../references/groq.md | 2 +- .../references/helicone.md | 2 +- .../references/hugging-face.md | 2 +- .../references/instructor.md | 2 +- .../references/langchain.md | 2 +- .../references/langgraph.md | 2 +- .../references/llamaindex.md | 2 +- .../references/mirascope.md | 2 +- .../references/mistral.md | 2 +- .../references/ollama.md | 2 +- .../references/openai.md | 2 +- .../references/openrouter.md | 2 +- .../references/perplexity.md | 2 +- .../references/portkey.md | 2 +- .../references/pydantic-ai.md | 2 +- .../references/semantic-kernel.md | 2 +- .../references/smolagents.md | 2 +- .../references/together-ai.md | 2 +- .../references/xai.md | 2 +- .../instrument-logs/references/start-here.md | 2 + .../references/ios.md | 4 +- .../references/posthog-python.md | 4 +- .../references/react-native.md | 2 +- .../references/ruby.md | 180 +-- skills/managing-path-cleaning-rules/SKILL.md | 180 +++ skills/planning-user-interviews/SKILL.md | 187 +++ .../references/example-error-tracking.md | 4 +- .../references/example-logs.md | 2 +- .../references/example-session-replay.md | 8 +- .../references/example-sessions.md | 2 +- 53 files changed, 1354 insertions(+), 940 deletions(-) create mode 100644 skills/formatting-insight-axes/SKILL.md create mode 100644 skills/managing-path-cleaning-rules/SKILL.md create mode 100644 skills/planning-user-interviews/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index beee7e1..744673c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "posthog", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Claude Code. Optionally capture Claude Code sessions to PostHog LLM Analytics.", - "version": "1.1.21", + "version": "1.1.22", "author": { "name": "PostHog", "email": "hey@posthog.com", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 653d836..a748b41 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "posthog", - "version": "1.0.20", + "version": "1.0.21", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Codex", "author": { "name": "PostHog", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 82f6460..cc92e8c 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "posthog", "displayName": "PostHog", - "version": "1.1.17", + "version": "1.1.18", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Cursor", "author": { "name": "PostHog", diff --git a/gemini-extension.json b/gemini-extension.json index 11f168d..a3bccd0 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "posthog", - "version": "1.0.19", + "version": "1.0.20", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Gemini CLI", "mcpServers": { "posthog": { diff --git a/skills/.sync-manifest b/skills/.sync-manifest index f377bd6..f478f9f 100644 --- a/skills/.sync-manifest +++ b/skills/.sync-manifest @@ -21,6 +21,7 @@ exploring-llm-traces feature-usage-feed finding-experiments finding-replay-for-issue +formatting-insight-axes inbox-exploration instrument-error-tracking instrument-feature-flags @@ -31,7 +32,9 @@ instrument-product-analytics investigate-metric investigating-replay managing-experiment-lifecycle +managing-path-cleaning-rules managing-subscriptions +planning-user-interviews querying-posthog-data setting-up-a-data-warehouse-source signals diff --git a/skills/formatting-insight-axes/SKILL.md b/skills/formatting-insight-axes/SKILL.md new file mode 100644 index 0000000..ce7b54f --- /dev/null +++ b/skills/formatting-insight-axes/SKILL.md @@ -0,0 +1,180 @@ +--- +name: formatting-insight-axes +description: > + Pick the right y-axis unit when creating or updating a TrendsQuery insight + via `posthog:insight-create` or `posthog:insight-update`. Use when the agent + is about to add a `formula` purely to convert units (e.g. dividing seconds + by 60 to display minutes), when a `math_property` is a duration, currency, + ratio, or large count, or whenever the user mentions "format the y-axis", + "duration", "seconds", "minutes", "hours", "milliseconds", "ms", + "percentage", "currency", "decimals", "axis label", or "axis unit" in the + context of a graph insight. +--- + +# Formatting insight axes + +PostHog renders TrendsQuery insights with a built-in axis formatter. Use it +instead of contorting `formula` or `aggregationAxisPostfix` to fake units. + +## The anti-pattern + +If you are reaching for any of these, stop and pick a format below first: + +- `formula: "A / 60"` with `aggregationAxisPostfix: " mins"` — manual seconds -> minutes +- `formula: "A / 1000"` with `aggregationAxisPostfix: " s"` — manual ms -> seconds +- `formula: "A * 100"` with `aggregationAxisPostfix: "%"` — manual ratio -> percent +- `aggregationAxisPostfix: "ms"` / `"s"` / `"min"` / `"hr"` on raw values + +These freeze the unit at one scale. The built-in formatter picks a friendly +unit per value (1.5s, 2m 12s, 1h 4m) and keeps the underlying series numerically +correct for further math, breakdowns, and alerts. + +## Available formats + +Set `trendsFilter.aggregationAxisFormat` on the TrendsQuery: + +| Value | Use when the series is... | Renders as | +| ------------------- | ---------------------------------------- | --------------------------- | +| `numeric` (default) | a plain count | `1,234` | +| `duration` | **seconds** (any scale) | `45s`, `2m 12s`, `1h 4m` | +| `duration_ms` | **milliseconds** | `850ms`, `1.5s`, `1m 4s` | +| `percentage` | already 0-100 | `47.3%` | +| `percentage_scaled` | a ratio 0-1 | `47.3%` | +| `currency` | money in the **project's base currency** | `$1,234.56` (or local code) | +| `short` | large counts you want compacted | `1.2K`, `3.4M` | + +Companion fields on `trendsFilter`: + +- `aggregationAxisPrefix` — literal prefix (e.g. `"$"`) when you need a symbol + pinned to a specific currency or unit, regardless of project settings +- `aggregationAxisPostfix` — literal suffix; reserve for genuine units the + format can't express (e.g. `" req"`, `" events"`), never for `"mins"` / `"s"` +- `decimalPlaces` — cap decimals (1 or 2 is usually right for currency / ratios) + +### Currency — pick `format` or `prefix` carefully + +`aggregationAxisFormat: "currency"` renders with the **project's base currency** +(set in project settings, defaults to USD). Use it when the underlying values +are in that same currency — e.g. revenue events that PostHog auto-converts to +the project's base currency. + +If the values are pinned to a specific currency regardless of project (e.g. +`$ai_total_cost_usd` is always USD, even on a EUR-base project), use +`aggregationAxisPrefix: "$"` + `decimalPlaces: 2` so the symbol matches the +data. Using `format: "currency"` here would render USD values with `€` on a +EUR project. + +## When the series is in seconds + +If the series is in seconds (latency, session length, time-to-first-event, +processing time, page load, etc.), silently default to +`aggregationAxisFormat: "duration"`. Do not stop to ask — the formatter is +non-destructive (the underlying values stay in seconds either way, only the +labels change), so picking it is always at least as good as raw seconds. + +Only confirm with the user when they have **explicitly** named a fixed unit +they want pinned ("show this in minutes", "graph the average in hours"): + +> "I can pin the y-axis to minutes by dividing the series by 60, or use +> PostHog's `duration` formatter which auto-picks seconds / minutes / hours +> per value — `90s` renders as `1m 30s` and `5400s` as `1h 30m`. Which would +> you prefer?" + +In one-shot MCP contexts where no user is in the loop, just pick `duration` +and move on. + +## Examples + +### Latency — duration in milliseconds + +```json +{ + "kind": "TrendsQuery", + "series": [ + { + "kind": "EventsNode", + "event": "$pageview", + "math": "p95", + "math_property": "$performance_page_loaded" + } + ], + "trendsFilter": { + "aggregationAxisFormat": "duration_ms" + } +} +``` + +### Average session length — duration in seconds + +```json +{ + "kind": "TrendsQuery", + "series": [ + { + "kind": "EventsNode", + "event": "$pageleave", + "math": "avg", + "math_property": "$session_duration" + } + ], + "trendsFilter": { + "aggregationAxisFormat": "duration" + } +} +``` + +### Revenue — currency in the project's base currency + +```json +{ + "trendsFilter": { + "aggregationAxisFormat": "currency", + "decimalPlaces": 2 + } +} +``` + +### Fixed-currency value (e.g. LLM cost in USD) — pin the symbol + +```json +{ + "trendsFilter": { + "aggregationAxisPrefix": "$", + "decimalPlaces": 2 + } +} +``` + +### Conversion rate — percentage from a 0-1 formula + +```json +{ + "kind": "TrendsQuery", + "series": [ + { + "kind": "EventsNode", + "event": "checkout_completed", + "math": "dau" + }, + { + "kind": "EventsNode", + "event": "checkout_started", + "math": "dau" + } + ], + "trendsFilter": { + "formula": "A / B", + "aggregationAxisFormat": "percentage_scaled", + "decimalPlaces": 1 + } +} +``` + +## Updating an existing insight + +If you are updating an insight and notice it already uses the +`formula`/`postfix` anti-pattern, fix it in the same `posthog:insight-update` +call — drop the divide-by-N, drop the `aggregationAxisPostfix`, and set the +matching `aggregationAxisFormat`. The series values stay the same, only the +labels change. Do not go scanning unrelated insights for this pattern — +fix only the ones you are already touching. diff --git a/skills/instrument-error-tracking/references/nextjs.md b/skills/instrument-error-tracking/references/nextjs.md index e05dded..e3d4522 100644 --- a/skills/instrument-error-tracking/references/nextjs.md +++ b/skills/instrument-error-tracking/references/nextjs.md @@ -414,7 +414,7 @@ Importantly, you need to: - 1. Set up a `posthog-node` client in your server-side code. See our doc on [setting up Next.js server-side analytics](/docs/libraries/next-js.md#server-side-analytics.md) for more. + 1. Set up a `posthog-node` client in your server-side code. See our doc on [setting up Next.js server-side analytics](/docs/libraries/next-js.md#server-side-analytics) for more. 2. Check the request is running in the `nodejs` runtime to ensure PostHog works. You can call `posthog.debug()` to get verbose logging. 3. Get the `distinct_id` from the cookie to connect the error to a specific user. diff --git a/skills/instrument-feature-flags/references/adding-feature-flag-code.md b/skills/instrument-feature-flags/references/adding-feature-flag-code.md index a3ac971..718d475 100644 --- a/skills/instrument-feature-flags/references/adding-feature-flag-code.md +++ b/skills/instrument-feature-flags/references/adding-feature-flag-code.md @@ -857,9 +857,11 @@ posthog = Posthog( ## PHP -There are 2 steps to implement feature flags in PHP: +There are two steps to implement feature flags in PHP: -### Step 1: Evaluate the feature flag value +### Step 1: Evaluate flags once + +Call `PostHog::evaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -868,9 +870,11 @@ PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled('flag-key', 'distinct_id_of_your_user') -if ($isMyFlagEnabledForUser) { +$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'); } ``` @@ -881,12 +885,19 @@ PHP PostHog AI ```php -$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 +$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. + +> **Note:** `PostHog::isFeatureEnabled()`, `PostHog::getFeatureFlag()`, `PostHog::getFeatureFlagPayload()`, and `capture(['send_feature_flags' => true])` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -895,81 +906,93 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +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 PostHog AI ```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', - 'properties' => [ - '$feature/feature-flag-key' => 'variant-key' // replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - ] + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags, ]); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: PHP PostHog AI ```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', - 'send_feature_flags' => true + 'distinctId' => 'distinct_id_of_your_user', + 'event' => 'event_name', + 'flags' => $flags->only(['checkout-flow', 'new-dashboard']), ]); ``` -### Fetching all flags for a user +`onlyAccessed()` is order-dependent. If you call it before accessing any flags with `isEnabled()` or `getFlag()`, no feature flag properties are attached. -You can fetch all flag values for a single user by calling `getAllFlags()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: PHP PostHog AI ```php -PostHog::getAllFlags('distinct_id_of_your_user') +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', + ], +]); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `getFeatureFlag()` or `isFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `getFeatureFlag` or `isFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagEvents` argument in your function call, like so: +By default, `evaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `flagKeys` to request only those flags: PHP PostHog AI ```php -$isMyFlagEnabledForUser = PostHog::isFeatureEnabled( - key: 'flag-key', +$flags = PostHog::evaluateFlags( distinctId: 'distinct_id_of_your_user', - sendFeatureFlagEvents: false -) + 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. @@ -983,21 +1006,21 @@ PHP PostHog AI ```php -PostHog::getFeatureFlag( - 'flag-key', - 'distinct_id_of_the_user', - [ +$flags = PostHog::evaluateFlags( + distinctId: 'distinct_id_of_the_user', + groups: [ 'your_group_type' => 'your_group_id', - 'another_group_type' => 'your_group_id' - ], // groups - ['property_name' => 'value'], // person properties - [ + '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'] - ], // group properties - false, // onlyEvaluateLocally, Optional. Defaults to false. - true // sendFeatureFlagEvents + 'another_group_type' => ['group_property_name' => 'value'], + ], ); +if ($flags->isEnabled('flag-key')) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -1029,7 +1052,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### 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. +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 @@ -1037,54 +1060,20 @@ PostHog AI ```php PostHog::init("", - [ - 'host' => 'https://us.i.posthog.com', - 'feature_flag_request_timeout_ms' => 3000 // Time in milliseconds. Default is 3000 (3 seconds). - ] + [ + 'host' => 'https://us.i.posthog.com', + 'feature_flag_request_timeout_ms' => 3000, // Time in milliseconds. Defaults to 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: - -PHP - -PostHog AI - -```php -function handleFeatureFlag($client, $flagKey, $distinctId) { - try { - $isEnabled = $client->isFeatureEnabled($flagKey, $distinctId); - echo "Feature flag '$flagKey' for user '$distinctId' is " . ($isEnabled ? 'enabled' : 'disabled') . "\n"; - return $isEnabled; - } catch (Exception $e) { - echo "Error fetching feature flag '$flagKey': " . $e->getMessage() . "\n"; - // Optionally, you can return a default value or throw the error - // return false; // Default to disabled - throw $e; - } -} -// Usage example -try { - $flagEnabled = handleFeatureFlag($client, 'new-feature', 'user-123'); - if ($flagEnabled) { - // Implement new feature logic - } else { - // Implement old feature logic - } -} catch (Exception $e) { - // Handle the error at a higher level - echo 'Feature flag check failed, using default behavior'; - // Implement fallback logic -} -``` - ## Ruby -There are 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1093,11 +1082,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +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 = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -1108,14 +1097,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +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 = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `capture(send_feature_flags: true)` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1124,47 +1118,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +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 PostHog AI ```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', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```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', - send_feature_flags: true, + 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']), }) ``` -## Advanced usage (v3.1.0+) +`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 -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -1174,64 +1175,35 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v3.1.0 - -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +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 PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +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 enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +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. -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it, set the `send_feature_flag_events` argument in your function call, like so: - -Ruby +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. -PostHog AI - -```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', - 'distinct_id_of_your_user', - send_feature_flag_events: true) -``` +`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 @@ -1246,25 +1218,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + 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' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -1296,7 +1270,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### 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 in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +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 @@ -1304,52 +1278,18 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### 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: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ## Go -There are 2 steps to implement feature flags in Go: +There are two steps to implement feature flags in Go: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.EvaluateFlags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -1358,15 +1298,16 @@ Go PostHog AI ```go -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", +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 isMyFlagEnabled == true { +if flags.IsEnabled("flag-key") { // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` @@ -1377,18 +1318,24 @@ Go PostHog AI ```go -enabledVariant, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: "flag-key", +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 enabledVariant == "variant-key" { // replace 'variant-key' with the key of your variant +enabledVariant := flags.GetFlag("flag-key") +if enabledVariant == "variant-key" { // replace "variant-key" with the key of your variant // Do something differently for this user + // Optional: fetch the payload + matchedFlagPayload := flags.GetFlagPayload("flag-key") } ``` +`flags.GetFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `client.IsFeatureEnabled()`, `client.GetFeatureFlag()`, `client.GetFeatureFlagPayload()`, and `Capture.SendFeatureFlags` still work during the migration period, but they're deprecated. Prefer `EvaluateFlags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -1397,46 +1344,59 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture` -In the event properties, include `$feature/feature_flag_name: variant_key`: +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 PostHog AI ```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", - 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 + DistinctId: "distinct_id_of_your_user", + Event: "event_name", + Flags: flags, }) ``` -#### Method 2: Set `SendFeatureFlags` to `true` - -The `Capture` struct has an optional field `SendFeatureFlags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `SendFeatureFlags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Go PostHog AI ```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", - SendFeatureFlags: true, + 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"}), }) ``` -## Advanced usage (v1.6.1+) +`OnlyAccessed()` is order-dependent. If you call it before accessing any flags with `IsEnabled()` or `GetFlag()`, no feature flag properties are attached. -As of version 1.6.1, `SendFeatureFlags` can also accept a `SendFeatureFlagsOptions` struct for more granular control: +#### Method 2: Include the `$feature/feature_flag_name` property manually + +In the event properties, include `$feature/feature_flag_name: variant_key`: Go @@ -1444,77 +1404,35 @@ PostHog AI ```go client.Enqueue(posthog.Capture{ - DistinctId: "distinct_id_of_your_user", - Event: "event_name", - SendFeatureFlags: posthog.SendFeatureFlagsOptions{ - OnlyEvaluateLocally: true, - PersonProperties: map[string]interface{}{ - "plan": "premium", - }, - GroupProperties: map[string]map[string]interface{}{ - "org": { - "tier": "enterprise", - }, - }, - }, + 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 }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `SendFeatureFlags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v1.6.1 - -Prior to version 1.6.1, feature flags were automatically sent with events when using local evaluation, even when `SendFeatureFlags` was not explicitly set. This behavior has been **removed** in v1.6.1 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `SendFeatureFlags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFlags()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +By default, `EvaluateFlags()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeys` to request only those flags: Go PostHog AI ```go -featureVariants, err := client.GetAllFlags(posthog.FeatureFlagPayloadNoKey{ - DistinctId: "distinct_id_of_your_user", +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 enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: +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. -1. You call `GetFeatureFlag()` or `IsFeatureEnabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlag` or `IsFeatureEnabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. - -To disable it (pre v1.6.1), set the `SendFeatureFlagEvents` argument in your function call, like so: - -Go - -PostHog AI - -```go -sendFeatureFlags := false -isMyFlagEnabled, err := client.IsFeatureEnabled(posthog.FeatureFlagPayload{ - Key: "flag-key", - DistinctId: "distinct_id_of_your_user", - SendFeatureFlagEvents: &sendFeatureFlags, -}) -``` +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. -Versions after v1.6.1 have this feature disabled by default. +`flags.GetFlagPayload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `OnlyAccessed()`. ### Advanced: Overriding server properties @@ -1529,25 +1447,26 @@ Go PostHog AI ```go -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", - }, - }, +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 @@ -1579,7 +1498,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### 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 in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +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 @@ -1587,39 +1506,15 @@ PostHog AI ```go client, _ := posthog.NewWithConfig( - os.Getenv(""), - 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 in seconds. Default is 3. - }, + os.Getenv(""), + 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 in seconds. Defaults to 3. + }, ) ``` -### 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: - -Go - -PostHog AI - -```go -func handleFeatureFlag(client *posthog.Client, flagKey string, distinctId string) { - flag, err := client.GetFeatureFlag(posthog.FeatureFlagPayload{ - Key: flagKey, - DistinctId: distinctId, - }) - if err != nil { - // Handle the error appropriately - log.Printf("Error fetching feature flag: %v", err) - return - } - // Use the flag value as needed - fmt.Printf("Feature flag '%s' for user '%s': %s\n", flagKey, distinctId, flag) -} -``` - ## React Native There are two ways to implement feature flags in React Native: @@ -2120,34 +2015,151 @@ await Posthog().reloadFeatureFlags(); ## Java -### Boolean feature flags +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 PostHog AI ```java -if (posthog.isFeatureEnabled("distinct_id_of_your_user", "flag-key")) { +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags("distinct_id_of_your_user"); +if (flags.isEnabled("flag-key")) { // Do something differently for this user // Optional: fetch the payload - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); } ``` -### Multivariate feature flags +#### Multivariate feature flags Java PostHog AI ```java -if ("variant-key".equals(posthog.getFeatureFlag("distinct_id_of_your_user", "flag-key"))) { // replace 'variant-key' with the key of your variant +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 - Object matchedFlagPayload = posthog.getFeatureFlagPayload("distinct_id_of_your_user", "flag-key"); + String matchedFlagPayload = flags.getFlagPayload("flag-key"); +} +``` + +`flags.getFlag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `null` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.isFeatureEnabled()`, `posthog.getFeatureFlag()`, `posthog.getFeatureFlagPayload()`, and `PostHogCaptureOptions.builder().appendFeatureFlags(true)` still work during the migration period, but they're deprecated. Prefer `evaluateFlags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to `capture()` + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. + +Java + +PostHog AI + +```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/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: + +Java + +PostHog AI + +```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 AI + +```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 + +PostHog AI + +```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. @@ -2161,19 +2173,20 @@ Java PostHog AI ```java -import com.posthog.server.PostHogFeatureFlagOptions; -posthog.getFeatureFlag( +import com.posthog.server.PostHogEvaluateFlagsOptions; +PostHogFeatureFlagEvaluations flags = posthog.evaluateFlags( "distinct_id_of_the_user", - "flag-key", - PostHogFeatureFlagOptions - .builder() - .defaultValue(false) + 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()); + .build() +); +if (flags.isEnabled("flag-key")) { + // Do something differently for this user +} ``` ### Overriding GeoIP properties @@ -2205,9 +2218,11 @@ Simply include any of these properties in the `person_properties` parameter alon ## Rust -There are 2 steps to implement feature flags in Rust: +There are two steps to implement feature flags in Rust: + +### Step 1: Evaluate flags once -### Step 1: Evaluate the feature flag value +Call `client.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2216,15 +2231,15 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +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"); } ``` @@ -2235,29 +2250,24 @@ Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +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"); } + _ => {} } ``` -### Step 2: Include feature flag information in your events +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2267,130 +2277,90 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to the event -In the event properties, include `$feature/feature_flag_name: variant_key`: - -Rust - -PostHog AI - -```rust -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).unwrap(); -``` - -#### Method 2: Fetch and include all flags +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 PostHog AI ```rust -let (flags, _) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -let mut event = Event::new("event_name", "distinct_id_of_your_user"); -for (key, value) in flags { - let prop_key = format!("$feature/{}", key); - match value { - FlagValue::Boolean(b) => event.insert_prop(&prop_key, b).unwrap(), - FlagValue::String(s) => event.insert_prop(&prop_key, s).unwrap(), - }; +if flags.is_enabled("flag-key") { + // Do something differently for this user } -client.capture(event).unwrap(); +let mut event = Event::new("event_name", "distinct_id_of_your_user"); +event.with_flags(&flags); +client.capture(event).await.unwrap(); ``` -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_feature_flags()`. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties -).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); -} +// 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).await.unwrap(); +// 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).await.unwrap(); ``` -### Feature flag payloads +`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 -You can retrieve additional data associated with a feature flag using payloads: +In the event properties, include `$feature/feature_flag_name: variant_key`: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() -).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} +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).await.unwrap(); ``` -### With person properties +### Evaluating only specific flags -You can include person properties for more targeted flag evaluation: +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 PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - Some(person_props), - None, // group_properties +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(); ``` -### With groups (B2B) - -For B2B applications with group-based flags: +### Sending `$feature_flag_called` events -Rust +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. -PostHog AI +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. -```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, // person_properties - Some(group_props), -).await.unwrap(); -``` +`flags.get_flag_payload()` doesn't send `$feature_flag_called` events and doesn't count as an access for `only_accessed()`. ### Blocking client @@ -2401,40 +2371,13 @@ Rust PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).unwrap(); -``` - -### Error handling - -When using the PostHog SDK, handle potential errors that may occur during feature flag operations: - -Rust - -PostHog AI - -```rust -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await { - Ok(Some(value)) => { - // Use the flag value - println!("Flag value: {:?}", value); - } - Ok(None) => { - // Flag not found or disabled - println!("Flag not found"); - } - Err(e) => { - // Handle the error appropriately - eprintln!("Error fetching feature flag: {}", e); - // Fall back to default behavior - } +if flags.is_enabled("flag-key") { + // Do something differently for this user } ``` @@ -2568,9 +2511,11 @@ Capturing `$feature_flag_called` events enables PostHog to know when a flag was ## .NET -There are 2 steps to implement feature flags in .NET: +There are two steps to implement feature flags in .NET: -### Step 1: Evaluate the feature flag value +### Step 1: Evaluate flags once + +Call `EvaluateFlagsAsync()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -2579,15 +2524,12 @@ C# PostHog AI ```csharp -if (await posthog.IsFeatureEnabledAsync( - "flag-key", - "distinct_id_of_your_user")) -{ - // Feature is enabled -} -else +var flags = await posthog.EvaluateFlagsAsync("distinct_id_of_your_user"); +if (flags.IsEnabled("flag-key")) { - // Feature is disabled + // Do something differently for this user + // Optional: fetch the payload + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` @@ -2598,33 +2540,19 @@ C# PostHog AI ```csharp -var flag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user" -); -// replace "variant-key" with the key of your variant -if (flag is { VariantKey: "variant-key"} ) { +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 = flag.Payload; + var matchedPayload = flags.GetFlagPayload("flag-key"); } ``` -> **Note:** The `GetFeatureFlagAsync` method returns a nullable `FeatureFlag` object. If the flag is not found or evaluating it is inconclusive, it returns `null`. However, there is an implicit conversion to bool to make comparisons easier. +`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. -C# - -PostHog AI - -```csharp -if (await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_your_user") -) -{ - // Do something differently for this user -} -``` +> **Note:** `posthog.IsFeatureEnabledAsync()`, `posthog.GetFeatureFlagAsync()`, and `Capture(..., sendFeatureFlags: true, ...)` still work during the migration period, but they're deprecated. Prefer `EvaluateFlagsAsync()` for new code. ### Step 2: Include feature flag information when capturing events @@ -2634,91 +2562,102 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `Capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +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# PostHog AI ```csharp +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: new() { - // replace feature-flag-key with your flag key. - // Replace "variant-key" with the key of your variant - ["$feature/feature-flag-key"] = "variant-key" - } + properties: null, + groups: null, + flags: flags ); ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `Capture()` method has an optional argument `sendFeatureFlags`, which is set to `false` by default. By setting this to `true`, feature flag information will automatically be sent with the event. +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Note that by doing this, PostHog will make an additional request to fetch feature flag information before capturing the event. So this method is only recommended if you don't mind the extra API call and delay. +To reduce event property bloat, pass a filtered snapshot: C# PostHog AI ```csharp +// Attach only flags accessed with IsEnabled() or GetFlag() before this call posthog.Capture( "distinct_id_of_your_user", "event_name", properties: null, groups: null, - sendFeatureFlags: true + 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") ); ``` -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `GetAllFeatureFlagsAsync()`. +#### Method 2: Include the `$feature/feature_flag_name` property manually -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +In the event properties, include `$feature/feature_flag_name: variant_key`: C# PostHog AI ```csharp -var flags = await posthog.GetAllFeatureFlagsAsync( - "distinct_id_of_your_user" +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", + } ); ``` -### Sending `$feature_flag_called` events - -Capturing `$feature_flag_called` events enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.GetFeatureFlagAsync()` or `posthog.IsFeatureEnabledAsync()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `GetFeatureFlagAsync` or `IsFeatureEnabledAsync`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. - -You can disable automatically the additional request to capture `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +### Evaluating only specific flags -To disable it, set the `sendFeatureFlagsEvent` option in your function call, like so: +By default, `EvaluateFlagsAsync()` evaluates every flag for the user. If you only need a few flags, pass `FlagKeysToEvaluate` to request only those flags: C# PostHog AI ```csharp -var isMyFlagEnabled = await posthog.IsFeatureEnabledAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_your_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - SendFeatureFlagEvents = true + FlagKeysToEvaluate = new[] { "checkout-flow", "new-dashboard" }, } ); -// will not send `$feature_flag_called` events ``` +### 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. @@ -2732,50 +2671,31 @@ C# PostHog AI ```csharp -// Overriding Person Properties -var personFlag = await posthog.GetFeatureFlagAsync( - "flag-key", +var flags = await posthog.EvaluateFlagsAsync( "distinct_id_of_the_user", - personProperties: new() {["property_name"] = "value"}); -// Overriding Group Properties -var groupFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions + options: new AllFeatureFlagsOptions { - Groups = [ + PersonProperties = new() + { + ["property_name"] = "value", + }, + Groups = new() + { new Group("your_group_type", "your_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); -// Overriding both Person and Group Properties -var bothFlag = await posthog.GetFeatureFlagAsync( - "flag-key", - "distinct_id_of_the_user", - options: new FeatureFlagOptions - { - PersonProperties = new() { ["property_name"] = "value" }, - Groups = [ - new Group("your_group_type", "your_group_id") + new Group("another_group_type", "another_group_id") { - ["group_property_name"] = "your group value" + ["group_property_name"] = "another value", }, - new Group( - "another_group_type", - "another_group_id") - { - ["group_property_name"] = "another group value" - } - ] - }); + }, + } +); +if (flags.IsEnabled("flag-key")) +{ + // Do something differently for this user +} ``` ### Overriding GeoIP properties diff --git a/skills/instrument-feature-flags/references/rust.md b/skills/instrument-feature-flags/references/rust.md index 99fb2c7..a16bd75 100644 --- a/skills/instrument-feature-flags/references/rust.md +++ b/skills/instrument-feature-flags/references/rust.md @@ -40,129 +40,167 @@ In blocking mode, calls to `capture` and related methods will block until the Po ## Using feature flags -### Boolean 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 PostHog AI ```rust -let is_enabled = client.is_feature_enabled( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, // groups - None, // person_properties - None, // group_properties +use posthog_rs::EvaluateFlagsOptions; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -if is_enabled { +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 +#### Multivariate feature flags Rust PostHog AI ```rust -use posthog_rs::FlagValue; -match client.get_feature_flag( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string(), - None, None, None -).await.unwrap() { - Some(FlagValue::String(variant)) => { - if variant == "variant-key" { - // Do something for this variant - } - } - Some(FlagValue::Boolean(enabled)) => { - // Handle boolean flag - } - None => { - // Flag not found or disabled +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"); } + _ => {} } ``` -### Fetching all flags +`flags.get_flag()` returns `Some(FlagValue::String(...))` for multivariate flags, `Some(FlagValue::Boolean(true))` for enabled boolean flags, `Some(FlagValue::Boolean(false))` for disabled flags, and `None` when the flag wasn't returned by the evaluation. + +> **Note:** `client.is_feature_enabled()`, `client.get_feature_flag()`, `client.get_feature_flag_payload()`, and `client.get_feature_flags()` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + +### Step 2: Include feature flag information when capturing events + +If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. + +> **Note:** This step is only required for events captured using our server-side SDKs or [API](/docs/api.md). + +There are two methods you can use to include feature flag information in your events: + +#### Method 1: Pass the evaluated flags snapshot to the event + +Pass the same `flags` object that you used for branching. This attaches the exact flag values from that evaluation and doesn't make another `/flags` request. Rust PostHog AI ```rust -let (flags, payloads) = client.get_feature_flags( - "distinct_id_of_your_user".to_string(), - None, None, None +use posthog_rs::{EvaluateFlagsOptions, Event}; +let flags = client.evaluate_flags( + "distinct_id_of_your_user", + EvaluateFlagsOptions::default(), ).await.unwrap(); -for (key, value) in flags { - println!("Flag {}: {:?}", key, value); +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).await.unwrap(); ``` -### Feature flag payloads +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. + +To reduce event property bloat, pass a filtered snapshot: Rust PostHog AI ```rust -let payload = client.get_feature_flag_payload( - "flag-key".to_string(), - "distinct_id_of_your_user".to_string() -).await.unwrap(); -if let Some(data) = payload { - println!("Payload: {}", data); -} +// 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).await.unwrap(); +// 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).await.unwrap(); +``` + +`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 + +PostHog AI + +```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).await.unwrap(); ``` -### With person properties +### 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 PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut person_props = HashMap::new(); -person_props.insert("plan".to_string(), json!("enterprise")); -person_props.insert("country".to_string(), json!("US")); -let flag = client.get_feature_flag( - "premium-feature".to_string(), - "distinct_id_of_your_user".to_string(), - None, - Some(person_props), - None +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(); ``` -### With groups +### 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. -For B2B applications with group-based flags: +`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 PostHog AI ```rust -use std::collections::HashMap; -use serde_json::json; -let mut groups = HashMap::new(); -groups.insert("company".to_string(), "company-123".to_string()); -let mut group_props = HashMap::new(); -let mut company_props = HashMap::new(); -company_props.insert("size".to_string(), json!(500)); -group_props.insert("company".to_string(), company_props); -let flag = client.get_feature_flag( - "b2b-feature".to_string(), - "distinct_id_of_your_user".to_string(), - Some(groups), - None, - Some(group_props) -).await.unwrap(); +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. diff --git a/skills/instrument-integration/references/ios.md b/skills/instrument-integration/references/ios.md index a6b9f39..00b2e98 100644 --- a/skills/instrument-integration/references/ios.md +++ b/skills/instrument-integration/references/ios.md @@ -15,7 +15,7 @@ Podfile PostHog AI ```ruby -pod "PostHog", "~> 3.56.0" +pod "PostHog", "~> 3.58.0" ``` ### Swift Package Manager @@ -30,7 +30,7 @@ PostHog AI ```swift dependencies: [ - .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.56.0") + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.58.0") ], ``` diff --git a/skills/instrument-integration/references/posthog-js.md b/skills/instrument-integration/references/posthog-js.md index 92b564e..716a8e2 100644 --- a/skills/instrument-integration/references/posthog-js.md +++ b/skills/instrument-integration/references/posthog-js.md @@ -1,6 +1,6 @@ # PostHog JavaScript Web SDK -**SDK Version:** 1.372.8 +**SDK Version:** 1.373.4 Posthog-js allows you to automatically capture usage and send events to PostHog. diff --git a/skills/instrument-integration/references/posthog-node.md b/skills/instrument-integration/references/posthog-node.md index 0984b5d..bfe628d 100644 --- a/skills/instrument-integration/references/posthog-node.md +++ b/skills/instrument-integration/references/posthog-node.md @@ -1,6 +1,6 @@ # PostHog Node.js SDK -**SDK Version:** 5.33.2 +**SDK Version:** 5.34.1 PostHog Node.js SDK allows you to capture events and send them to PostHog from your Node.js applications. diff --git a/skills/instrument-integration/references/posthog-python.md b/skills/instrument-integration/references/posthog-python.md index 95853e9..50c8ae7 100644 --- a/skills/instrument-integration/references/posthog-python.md +++ b/skills/instrument-integration/references/posthog-python.md @@ -1,6 +1,6 @@ # PostHog Python SDK -**SDK Version:** 7.14.0 +**SDK Version:** 7.14.2 Integrate PostHog into any python application. @@ -1338,7 +1338,7 @@ Create a new context scope that will be active for the duration of the with bloc - **`fresh`** (`bool`) - Whether to start with a fresh context (default: False) - **`capture_exceptions`** (`bool`) - Whether to capture exceptions raised within the context (default: True) -- **`client`** (`any`) - Optional Posthog client instance to use for this context (default: None) +- **`client?`** (`Client`) - Optional Posthog client instance to use for this context (default: None) ### Returns diff --git a/skills/instrument-integration/references/react-native.md b/skills/instrument-integration/references/react-native.md index 43539f3..0671bb3 100644 --- a/skills/instrument-integration/references/react-native.md +++ b/skills/instrument-integration/references/react-native.md @@ -173,7 +173,7 @@ You can further customize how PostHog works through its configuration on initial | enableSessionReplayType: BooleanDefault: false | Enable Recording of Session replay for Android and iOS. | | sessionReplayConfigType: ObjectDefault: null | Session replay configuration. See the [replay install docs](/docs/session-replay/installation.md) for more details. | | enablePersistSessionIdAcrossRestartType: BooleanDefault: false | When true, persists the $session_id across app restarts. If false, $session_id always resets on app restart. | -| evaluationContextsType: Array of StringsDefault: undefined | Evaluation context tags that constrain which feature flags are evaluated. When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned. This helps reduce unnecessary flag evaluations and improves performance. See [evaluation contexts documentation](/docs/feature-flags/evaluation-contexts.md) for more details. Available in version 4.8.0+. The legacy parameter evaluationEnvironments (version 4.7.2+) is also supported for backward compatibility. | +| evaluationContextsType: Array of StringsDefault: undefined | Evaluation context tags that constrain which feature flags are evaluated. When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned. This helps reduce unnecessary flag evaluations and improves performance. See [evaluation contexts documentation](/docs/feature-flags/evaluation-contexts.md) for more details. Available in version 4.21.0+. The legacy parameter evaluationEnvironments (version 4.7.2+) is also supported for backward compatibility. | | before_sendType: FunctionDefault: undefined | A callback function that is called before each event is sent to PostHog. You can use it to modify, filter, or suppress events. Return null to drop the event, or return the modified event to send it. See [customizing exception capture](#customizing-exception-capture-with-before_send) for details. | ## Capturing events diff --git a/skills/instrument-integration/references/ruby.md b/skills/instrument-integration/references/ruby.md index c69fe46..8a70dac 100644 --- a/skills/instrument-integration/references/ruby.md +++ b/skills/instrument-integration/references/ruby.md @@ -153,9 +153,11 @@ We strongly recommend reading our docs on [alias](/docs/data/identify.md#alias-a 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 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: -### Step 1: Evaluate the feature flag value +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -164,11 +166,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +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 = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -179,14 +181,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +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 = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `capture(send_feature_flags: true)` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -195,47 +202,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +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 PostHog AI ```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', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```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', - send_feature_flags: true, + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`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 -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -245,64 +259,35 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v3.1.0 - -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +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 PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +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 enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +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. -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +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. -To disable it, set the `send_feature_flag_events` argument in your function call, like so: - -Ruby - -PostHog AI - -```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', - 'distinct_id_of_your_user', - send_feature_flag_events: true) -``` +`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 @@ -317,25 +302,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + 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' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -367,7 +354,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### 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 in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +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 @@ -375,47 +362,11 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### 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: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ### 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. @@ -471,7 +422,8 @@ Ruby PostHog AI ```ruby -variant = posthog.get_feature_flag('experiment-feature-flag-key', 'user_distinct_id') +flags = posthog.evaluate_flags('user_distinct_id') +variant = flags.get_flag('experiment-feature-flag-key') if variant == 'variant-name' # Do something end diff --git a/skills/instrument-llm-analytics/references/anthropic.md b/skills/instrument-llm-analytics/references/anthropic.md index 0d5b1fd..f41024f 100644 --- a/skills/instrument-llm-analytics/references/anthropic.md +++ b/skills/instrument-llm-analytics/references/anthropic.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install anthropic opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-anthropic + pip install anthropic opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-anthropic ``` ### Node diff --git a/skills/instrument-llm-analytics/references/autogen.md b/skills/instrument-llm-analytics/references/autogen.md index 7b0ca91..5a2a148 100644 --- a/skills/instrument-llm-analytics/references/autogen.md +++ b/skills/instrument-llm-analytics/references/autogen.md @@ -13,7 +13,7 @@ Install the OpenTelemetry SDK, the OpenAI instrumentation, and AutoGen. ```bash - pip install autogen-agentchat "autogen-ext[openai]" openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install autogen-agentchat "autogen-ext[openai]" openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` 2. 2 diff --git a/skills/instrument-llm-analytics/references/azure-openai.md b/skills/instrument-llm-analytics/references/azure-openai.md index 12c8b74..d09ebc5 100644 --- a/skills/instrument-llm-analytics/references/azure-openai.md +++ b/skills/instrument-llm-analytics/references/azure-openai.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/cerebras.md b/skills/instrument-llm-analytics/references/cerebras.md index 370cf3f..6bb8b05 100644 --- a/skills/instrument-llm-analytics/references/cerebras.md +++ b/skills/instrument-llm-analytics/references/cerebras.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/cohere.md b/skills/instrument-llm-analytics/references/cohere.md index e9f8ce2..c498f1e 100644 --- a/skills/instrument-llm-analytics/references/cohere.md +++ b/skills/instrument-llm-analytics/references/cohere.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/deepseek.md b/skills/instrument-llm-analytics/references/deepseek.md index 4df49f8..df0cf32 100644 --- a/skills/instrument-llm-analytics/references/deepseek.md +++ b/skills/instrument-llm-analytics/references/deepseek.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/fireworks-ai.md b/skills/instrument-llm-analytics/references/fireworks-ai.md index 11b552e..203d933 100644 --- a/skills/instrument-llm-analytics/references/fireworks-ai.md +++ b/skills/instrument-llm-analytics/references/fireworks-ai.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/google.md b/skills/instrument-llm-analytics/references/google.md index 3bb9f37..68f46db 100644 --- a/skills/instrument-llm-analytics/references/google.md +++ b/skills/instrument-llm-analytics/references/google.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install google-genai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-google-generativeai + pip install google-genai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-google-generativeai ``` ### Node diff --git a/skills/instrument-llm-analytics/references/groq.md b/skills/instrument-llm-analytics/references/groq.md index 1993669..bc76fda 100644 --- a/skills/instrument-llm-analytics/references/groq.md +++ b/skills/instrument-llm-analytics/references/groq.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/helicone.md b/skills/instrument-llm-analytics/references/helicone.md index 04700bf..c160878 100644 --- a/skills/instrument-llm-analytics/references/helicone.md +++ b/skills/instrument-llm-analytics/references/helicone.md @@ -21,7 +21,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/hugging-face.md b/skills/instrument-llm-analytics/references/hugging-face.md index 0f491b3..81ea9b7 100644 --- a/skills/instrument-llm-analytics/references/hugging-face.md +++ b/skills/instrument-llm-analytics/references/hugging-face.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/instructor.md b/skills/instrument-llm-analytics/references/instructor.md index 5d83d3d..58c8012 100644 --- a/skills/instrument-llm-analytics/references/instructor.md +++ b/skills/instrument-llm-analytics/references/instructor.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install instructor openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install instructor openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/langchain.md b/skills/instrument-llm-analytics/references/langchain.md index b25232b..6c4e9a9 100644 --- a/skills/instrument-llm-analytics/references/langchain.md +++ b/skills/instrument-llm-analytics/references/langchain.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install langchain langchain-core langchain-openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-langchain + pip install langchain langchain-core langchain-openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-langchain ``` ### Node diff --git a/skills/instrument-llm-analytics/references/langgraph.md b/skills/instrument-llm-analytics/references/langgraph.md index fbdd452..e180987 100644 --- a/skills/instrument-llm-analytics/references/langgraph.md +++ b/skills/instrument-llm-analytics/references/langgraph.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install langgraph langchain-core langchain-openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-langchain + pip install langgraph langchain-core langchain-openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-langchain ``` ### Node diff --git a/skills/instrument-llm-analytics/references/llamaindex.md b/skills/instrument-llm-analytics/references/llamaindex.md index 706c38b..249214f 100644 --- a/skills/instrument-llm-analytics/references/llamaindex.md +++ b/skills/instrument-llm-analytics/references/llamaindex.md @@ -13,7 +13,7 @@ Install LlamaIndex, OpenAI, and the OpenTelemetry SDK with the LlamaIndex instrumentation. ```bash - pip install llama-index llama-index-llms-openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-llamaindex + pip install llama-index llama-index-llms-openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-llamaindex ``` 2. 2 diff --git a/skills/instrument-llm-analytics/references/mirascope.md b/skills/instrument-llm-analytics/references/mirascope.md index fc481af..e2149e7 100644 --- a/skills/instrument-llm-analytics/references/mirascope.md +++ b/skills/instrument-llm-analytics/references/mirascope.md @@ -13,7 +13,7 @@ Install the OpenTelemetry SDK, the OpenAI instrumentation, and Mirascope. ```bash - pip install "mirascope[openai]" opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install "mirascope[openai]" opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` 2. 2 diff --git a/skills/instrument-llm-analytics/references/mistral.md b/skills/instrument-llm-analytics/references/mistral.md index 0e53b96..835ae1f 100644 --- a/skills/instrument-llm-analytics/references/mistral.md +++ b/skills/instrument-llm-analytics/references/mistral.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/ollama.md b/skills/instrument-llm-analytics/references/ollama.md index 4e3ee7f..5e7d847 100644 --- a/skills/instrument-llm-analytics/references/ollama.md +++ b/skills/instrument-llm-analytics/references/ollama.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/openai.md b/skills/instrument-llm-analytics/references/openai.md index b72607e..19603ab 100644 --- a/skills/instrument-llm-analytics/references/openai.md +++ b/skills/instrument-llm-analytics/references/openai.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/openrouter.md b/skills/instrument-llm-analytics/references/openrouter.md index a548874..6dba88c 100644 --- a/skills/instrument-llm-analytics/references/openrouter.md +++ b/skills/instrument-llm-analytics/references/openrouter.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/perplexity.md b/skills/instrument-llm-analytics/references/perplexity.md index 6d460e2..8153209 100644 --- a/skills/instrument-llm-analytics/references/perplexity.md +++ b/skills/instrument-llm-analytics/references/perplexity.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/portkey.md b/skills/instrument-llm-analytics/references/portkey.md index 681ac2c..04d7a8e 100644 --- a/skills/instrument-llm-analytics/references/portkey.md +++ b/skills/instrument-llm-analytics/references/portkey.md @@ -21,7 +21,7 @@ ### Python ```bash - pip install openai portkey-ai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai portkey-ai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/pydantic-ai.md b/skills/instrument-llm-analytics/references/pydantic-ai.md index 7b85e1d..a4b8190 100644 --- a/skills/instrument-llm-analytics/references/pydantic-ai.md +++ b/skills/instrument-llm-analytics/references/pydantic-ai.md @@ -13,7 +13,7 @@ Install the OpenTelemetry SDK and Pydantic AI. ```bash - pip install "pydantic-ai[openai]" opentelemetry-sdk posthog[otel] + pip install "pydantic-ai[openai]" opentelemetry-sdk "posthog[otel]" ``` 2. 2 diff --git a/skills/instrument-llm-analytics/references/semantic-kernel.md b/skills/instrument-llm-analytics/references/semantic-kernel.md index e82e898..aef8382 100644 --- a/skills/instrument-llm-analytics/references/semantic-kernel.md +++ b/skills/instrument-llm-analytics/references/semantic-kernel.md @@ -13,7 +13,7 @@ Install the OpenTelemetry SDK, the OpenAI instrumentation, and Semantic Kernel. ```bash - pip install semantic-kernel openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install semantic-kernel openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` 2. 2 diff --git a/skills/instrument-llm-analytics/references/smolagents.md b/skills/instrument-llm-analytics/references/smolagents.md index 3b7c20c..a32db3e 100644 --- a/skills/instrument-llm-analytics/references/smolagents.md +++ b/skills/instrument-llm-analytics/references/smolagents.md @@ -13,7 +13,7 @@ Install the OpenTelemetry SDK, the OpenAI instrumentation, and smolagents. ```bash - pip install smolagents openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install smolagents openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` 2. 2 diff --git a/skills/instrument-llm-analytics/references/together-ai.md b/skills/instrument-llm-analytics/references/together-ai.md index 6956523..156d31e 100644 --- a/skills/instrument-llm-analytics/references/together-ai.md +++ b/skills/instrument-llm-analytics/references/together-ai.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-llm-analytics/references/xai.md b/skills/instrument-llm-analytics/references/xai.md index 19b723f..16774bd 100644 --- a/skills/instrument-llm-analytics/references/xai.md +++ b/skills/instrument-llm-analytics/references/xai.md @@ -17,7 +17,7 @@ ### Python ```bash - pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2 + pip install openai opentelemetry-sdk "posthog[otel]" opentelemetry-instrumentation-openai-v2 ``` ### Node diff --git a/skills/instrument-logs/references/start-here.md b/skills/instrument-logs/references/start-here.md index 8c089e2..dd440b7 100644 --- a/skills/instrument-logs/references/start-here.md +++ b/skills/instrument-logs/references/start-here.md @@ -22,6 +22,8 @@ Follow the guides below to set up your logging client: - [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/react.svg)React Native](/docs/logs/installation/react-native.md) +- [![](https://res.cloudinary.com/dmukukwp6/image/upload/posthog.com/contents/images/docs/integrate/ios.svg)iOS](/docs/logs/installation/ios.md) + - [Datadog](/docs/logs/installation/datadog.md) - [Other languages](/docs/logs/installation/other.md) diff --git a/skills/instrument-product-analytics/references/ios.md b/skills/instrument-product-analytics/references/ios.md index a6b9f39..00b2e98 100644 --- a/skills/instrument-product-analytics/references/ios.md +++ b/skills/instrument-product-analytics/references/ios.md @@ -15,7 +15,7 @@ Podfile PostHog AI ```ruby -pod "PostHog", "~> 3.56.0" +pod "PostHog", "~> 3.58.0" ``` ### Swift Package Manager @@ -30,7 +30,7 @@ PostHog AI ```swift dependencies: [ - .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.56.0") + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.58.0") ], ``` diff --git a/skills/instrument-product-analytics/references/posthog-python.md b/skills/instrument-product-analytics/references/posthog-python.md index 95853e9..50c8ae7 100644 --- a/skills/instrument-product-analytics/references/posthog-python.md +++ b/skills/instrument-product-analytics/references/posthog-python.md @@ -1,6 +1,6 @@ # PostHog Python SDK -**SDK Version:** 7.14.0 +**SDK Version:** 7.14.2 Integrate PostHog into any python application. @@ -1338,7 +1338,7 @@ Create a new context scope that will be active for the duration of the with bloc - **`fresh`** (`bool`) - Whether to start with a fresh context (default: False) - **`capture_exceptions`** (`bool`) - Whether to capture exceptions raised within the context (default: True) -- **`client`** (`any`) - Optional Posthog client instance to use for this context (default: None) +- **`client?`** (`Client`) - Optional Posthog client instance to use for this context (default: None) ### Returns diff --git a/skills/instrument-product-analytics/references/react-native.md b/skills/instrument-product-analytics/references/react-native.md index 43539f3..0671bb3 100644 --- a/skills/instrument-product-analytics/references/react-native.md +++ b/skills/instrument-product-analytics/references/react-native.md @@ -173,7 +173,7 @@ You can further customize how PostHog works through its configuration on initial | enableSessionReplayType: BooleanDefault: false | Enable Recording of Session replay for Android and iOS. | | sessionReplayConfigType: ObjectDefault: null | Session replay configuration. See the [replay install docs](/docs/session-replay/installation.md) for more details. | | enablePersistSessionIdAcrossRestartType: BooleanDefault: false | When true, persists the $session_id across app restarts. If false, $session_id always resets on app restart. | -| evaluationContextsType: Array of StringsDefault: undefined | Evaluation context tags that constrain which feature flags are evaluated. When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned. This helps reduce unnecessary flag evaluations and improves performance. See [evaluation contexts documentation](/docs/feature-flags/evaluation-contexts.md) for more details. Available in version 4.8.0+. The legacy parameter evaluationEnvironments (version 4.7.2+) is also supported for backward compatibility. | +| evaluationContextsType: Array of StringsDefault: undefined | Evaluation context tags that constrain which feature flags are evaluated. When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned. This helps reduce unnecessary flag evaluations and improves performance. See [evaluation contexts documentation](/docs/feature-flags/evaluation-contexts.md) for more details. Available in version 4.21.0+. The legacy parameter evaluationEnvironments (version 4.7.2+) is also supported for backward compatibility. | | before_sendType: FunctionDefault: undefined | A callback function that is called before each event is sent to PostHog. You can use it to modify, filter, or suppress events. Return null to drop the event, or return the modified event to send it. See [customizing exception capture](#customizing-exception-capture-with-before_send) for details. | ## Capturing events diff --git a/skills/instrument-product-analytics/references/ruby.md b/skills/instrument-product-analytics/references/ruby.md index c69fe46..8a70dac 100644 --- a/skills/instrument-product-analytics/references/ruby.md +++ b/skills/instrument-product-analytics/references/ruby.md @@ -153,9 +153,11 @@ We strongly recommend reading our docs on [alias](/docs/data/identify.md#alias-a 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 2 steps to implement feature flags in Ruby: +There are two steps to implement feature flags in Ruby: -### Step 1: Evaluate the feature flag value +### Step 1: Evaluate flags once + +Call `posthog.evaluate_flags()` once for the user, then read values from the returned snapshot. #### Boolean feature flags @@ -164,11 +166,11 @@ Ruby PostHog AI ```ruby -is_my_flag_enabled = posthog.is_feature_enabled('flag-key', 'distinct_id_of_your_user') -if is_my_flag_enabled +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 = posthog.get_feature_flag_payload('flag-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` @@ -179,14 +181,19 @@ Ruby PostHog AI ```ruby -enabled_variant = posthog.get_feature_flag('flag-key', 'distinct_id_of_your_user') +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 = posthog.get_feature_flag_payload('variant-key', 'distinct_id_of_your_user') + matched_flag_payload = flags.get_flag_payload('flag-key') end ``` +`flags.get_flag()` returns the variant string for multivariate flags, `true` for enabled boolean flags, `false` for disabled flags, and `nil` when the flag wasn't returned by the evaluation. + +> **Note:** `posthog.is_feature_enabled()`, `posthog.get_feature_flag()`, `posthog.get_feature_flag_payload()`, and `capture(send_feature_flags: true)` still work during the migration period, but they're deprecated. Prefer `evaluate_flags()` for new code. + ### Step 2: Include feature flag information when capturing events If you want use your feature flag to breakdown or filter events in your [insights](/docs/product-analytics/insights.md), you'll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event. @@ -195,47 +202,54 @@ If you want use your feature flag to breakdown or filter events in your [insight There are two methods you can use to include feature flag information in your events: -#### Method 1: Include the `$feature/feature_flag_name` property +#### Method 1: Pass the evaluated flags snapshot to `capture()` -In the event properties, include `$feature/feature_flag_name: variant_key`: +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 PostHog AI ```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', - properties: { - '$feature/feature-flag-key': 'variant-key', # replace feature-flag-key with your flag key. Replace 'variant-key' with the key of your variant - } + flags: flags, }) ``` -#### Method 2: Set `send_feature_flags` to `true` - -The `capture()` method has an optional argument `send_feature_flags`, which is set to `false` by default. This parameter controls whether feature flag information is sent with the event. - -#### Basic usage +By default, this attaches every flag in the snapshot using `$feature/` properties and `$active_feature_flags`. -Setting `send_feature_flags` to `true` will include feature flag information with the event: +To reduce event property bloat, pass a filtered snapshot: Ruby PostHog AI ```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', - send_feature_flags: true, + flags: flags.only(['checkout-flow', 'new-dashboard']), }) ``` -## Advanced usage (v3.1.0+) +`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 -As of version 3.1.0, `send_feature_flags` can also accept a hash for more granular control: +In the event properties, include `$feature/feature_flag_name: variant_key`: Ruby @@ -245,64 +259,35 @@ PostHog AI posthog.capture({ distinct_id: 'distinct_id_of_your_user', event: 'event_name', - send_feature_flags: { - only_evaluate_locally: true, - person_properties: { plan: 'premium' }, - group_properties: { org: { tier: 'enterprise' } } - } + properties: { + # Replace feature-flag-key with your flag key and 'variant-key' with the key of your variant + '$feature/feature-flag-key': 'variant-key', + }, }) ``` -#### Performance considerations - -- **With local evaluation**: When [local evaluation](/docs/feature-flags/local-evaluation.md) is configured, setting `send_feature_flags: true` will **not** make additional server requests. Instead, it uses the locally cached feature flags, and it provides an interface for including person and/or group properties needed to evaluate the flags in the context of the event, if required. - -- **Without local evaluation**: PostHog will make an additional request to fetch feature flag information before capturing the event, which adds delay. - -#### Breaking change in v3.1.0 - -Prior to version 3.1.0, feature flags were automatically sent with events when using local evaluation, even when `send_feature_flags` was not explicitly set. This behavior has been **removed** in v3.1.0 to be more predictable and explicit. - -If you were relying on this automatic behavior, you must now explicitly set `send_feature_flags: true` to continue sending feature flags with your events. - -### Fetching all flags for a user - -You can fetch all flag values for a single user by calling `get_all_flags()` or `get_all_flags_and_payloads()`. +### Evaluating only specific flags -This is useful when you need to fetch multiple flag values and don't want to make multiple requests. +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 PostHog AI ```ruby -posthog.get_all_flags('distinct_id_of_your_user') -posthog.get_all_flags_and_payloads('distinct_id_of_your_user') +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 enable PostHog to know when a flag was accessed by a user and thus provide [analytics and insights](/docs/product-analytics/insights.md) on the flag. By default, we send a these event when: - -1. You call `posthog.get_feature_flag()` or `posthog.is_feature_enabled()`, AND -2. It's a new user, or the value of the flag has changed. - -> *Note:* Tracking whether it's a new user or if a flag value has changed happens in a local cache. This means that if you reinitialize the PostHog client, the cache resets as well – causing `$feature_flag_called` events to be sent again when calling `get_feature_flag` or `is_feature_enabled`. PostHog is built to handle this, and so duplicate `$feature_flag_called` events won't affect your analytics. +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. -You can disable automatically capturing `$feature_flag_called` events. For example, when you don't need the analytics, or it's being called at such a high volume that sending events slows things down. +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. -To disable it, set the `send_feature_flag_events` argument in your function call, like so: - -Ruby - -PostHog AI - -```ruby -is_my_flag_enabled = posthog.is_feature_enabled( - 'flag-key', - 'distinct_id_of_your_user', - send_feature_flag_events: true) -``` +`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 @@ -317,25 +302,27 @@ Ruby PostHog AI ```ruby -posthog.get_feature_flag( - 'flag-key', +flags = posthog.evaluate_flags( 'distinct_id_of_the_user', person_properties: { - 'property_name': 'value' + property_name: 'value' }, groups: { - 'your_group_type': 'your_group_id', - 'another_group_type': 'your_group_id', + 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' - } + your_group_type: { + group_property_name: 'value' + }, + another_group_type: { + group_property_name: 'value' + }, }, ) +if flags.enabled?('flag-key') + # Do something differently for this user +end ``` ### Overriding GeoIP properties @@ -367,7 +354,7 @@ Simply include any of these properties in the `person_properties` parameter alon ### 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 in the case when PostHog's servers are too slow to respond. By default, this is set at 3 seconds. +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 @@ -375,47 +362,11 @@ PostHog AI ```ruby posthog = PostHog::Client.new({ - # rest of your configuration... - feature_flag_request_timeout_seconds: 3 # Time in seconds. Default is 3. + # rest of your configuration... + feature_flag_request_timeout_seconds: 3 # Time in seconds. Defaults to 3. }) ``` -### 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: - -Ruby - -PostHog AI - -```ruby -def handle_feature_flag(client, flag_key, distinct_id) - begin - is_enabled = client.is_feature_enabled(flag_key, distinct_id) - puts "Feature flag '#{flag_key}' for user '#{distinct_id}' is #{is_enabled ? 'enabled' : 'disabled'}" - return is_enabled - rescue => e - puts "Error fetching feature flag '#{flag_key}': #{e.message}" - # Optionally, you can return a default value or throw the error - # return false # Default to disabled - raise e - end -end -# Usage example -try - flag_enabled = handle_feature_flag(client, 'new-feature', 'user-123') - if flag_enabled - # Implement new feature logic - else - # Implement old feature logic - end -rescue => e - # Handle the error at a higher level - puts 'Feature flag check failed, using default behavior' - # Implement fallback logic -end -``` - ### 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. @@ -471,7 +422,8 @@ Ruby PostHog AI ```ruby -variant = posthog.get_feature_flag('experiment-feature-flag-key', 'user_distinct_id') +flags = posthog.evaluate_flags('user_distinct_id') +variant = flags.get_flag('experiment-feature-flag-key') if variant == 'variant-name' # Do something end diff --git a/skills/managing-path-cleaning-rules/SKILL.md b/skills/managing-path-cleaning-rules/SKILL.md new file mode 100644 index 0000000..33fa475 --- /dev/null +++ b/skills/managing-path-cleaning-rules/SKILL.md @@ -0,0 +1,180 @@ +--- +name: managing-path-cleaning-rules +description: 'Inspects URL paths and proposes, tests, orders, and applies project-level path cleaning rules so dynamic segments (numeric IDs, UUIDs, slugs, dates) collapse into readable aliases. Use when the user says "clean the paths", "normalize URLs", "group similar pages", "too many distinct paths", "/users/123 and /users/456 are the same page", "set up path cleaning", or asks why a Web analytics or Paths breakdown is fragmented across thousands of nearly-identical URLs. Covers regex syntax (re2), alias placeholder convention, rule ordering, the test workflow, and applying rules via the project-settings-update MCP tool.' +--- + +# Managing path cleaning rules + +Path cleaning rules normalize `$pathname` and `$entry_pathname` so that pages +sharing the same template (`/users/123/profile`, `/users/456/profile`, …) collapse +into one row (`/users//profile`) in Web analytics tiles, Paths insights, and +any HogQL query that calls `apply_path_cleaning`. They are the right answer when +a breakdown is fragmented across thousands of near-identical URLs. + +This skill teaches you how to: + +- recognize when path cleaning is the right tool +- inspect real paths to find what needs cleaning +- write `regex` + `alias` rules in re2 syntax with the project's placeholder + convention +- test rules before saving them +- order rules so specific patterns aren't swallowed by generic ones +- apply the rules via MCP + +## Data model + +`Team.path_cleaning_filters` is a JSON list of `PathCleaningFilter` objects: + +```json +{ + "regex": "/users/\\d+/profile", + "alias": "/users//profile", + "order": 0 +} +``` + +- **`regex`** — a [re2](https://github.com/google/re2/wiki/Syntax) pattern. No + need to escape `/`. Anchor with `^` / `$` when you mean it. +- **`alias`** — the literal replacement. Use angle-bracket placeholders + (``, ``, ``, ``) by convention so the cleaned path stays + human-readable. The alias is _not_ a regex template — backreferences are not + supported. +- **`order`** — integer. Rules apply **sequentially** in `order` ascending, + each rule's output feeds the next. + +Application is `replaceRegexpAll(pathname, regex, alias)` per rule, chained. +Source: `posthog/hogql/property.py:613`. + +## Workflow + +### 1. Confirm path cleaning is the right move + +Ask yourself: is the user complaining about cardinality (too many distinct paths +in a chart), or do they want a per-URL drill-down? Path cleaning is for the +former. If they want per-URL data, suggest a property filter on `$pathname` +instead. + +### 2. Inspect the real paths + +Don't guess at patterns — query them. With the `execute-sql` MCP tool: + +```sql +SELECT properties.$pathname AS path, count() AS views +FROM events +WHERE event = '$pageview' + AND timestamp > now() - INTERVAL 7 DAY +GROUP BY path +ORDER BY views DESC +LIMIT 200 +``` + +Scan the result for: + +- numeric IDs: `/users/123`, `/orders/4242` +- UUIDs: `/sessions/8f3c1a3b-…` +- slugs: `/posts/why-i-love-posthog` +- dates: `/archive/2024-09-12` +- locales: `/en-US/`, `/fr-FR/` +- pagination: `?page=3`, `/page/3/` + +### 3. Draft regex + alias + +| Pattern | Example match | `regex` | `alias` | +| ------------------- | ---------------------- | ---------------------------- | ---------------------- | +| Numeric segment | `/users/123/profile` | `/users/\d+/profile` | `/users//profile` | +| UUID v4 | `/sessions/8f3c1a3b-…` | `/sessions/[0-9a-f-]{36}` | `/sessions/` | +| Slug | `/posts/why-posthog` | `/posts/[a-z0-9-]+$` | `/posts/` | +| ISO date | `/archive/2024-09-12` | `/archive/\d{4}-\d{2}-\d{2}` | `/archive/` | +| Locale prefix | `/en-US/about` | `^/[a-z]{2}-[A-Z]{2}/` | `//` | +| Trailing query/page | `/blog?page=3` | `\?page=\d+$` | (empty alias drops it) | + +Anchoring rules of thumb: + +- start the regex with `^` only when the segment must be at the beginning of + the path +- end with `$` to keep a generic rule (e.g. `\d+$`) from matching mid-path + segments + +### 4. Test before saving + +Three options, pick one: + +- **Settings page tester**: `/settings/project#path_cleaning` has a built-in + "test path" input that replays the full ordered chain. +- **Project HogQL** (via `execute-sql`): + + ```sql + SELECT replaceRegexpAll('/users/42/profile', '/users/\d+/profile', '/users//profile') + ``` + + Chain `replaceRegexpAll` calls in the same order the rules will run if you + want to verify multi-rule interaction. + +- **Built-in AI helper**: there is already an `AiRegexHelper` modal accessible + from the rule editor (`Help me with Regex` button) that turns natural + language into a regex. Suggest it to the user when they say "I don't know + regex" — but always validate the output against real paths via the tester. + +### 5. Order rules from most-specific to most-general + +Sequential application means a generic rule placed first will swallow +everything that should have hit a specific rule. + +```text +order=0 /users/me/profile → /users/me/profile (specific, runs first) +order=1 /users/\d+/profile → /users//profile +order=2 /users/[a-z0-9-]+ → /users/ (catch-all, runs last) +``` + +If `/users/[a-z0-9-]+` ran first it would also match `/users/me/profile` and +make the more specific rule unreachable. + +### 6. Apply via MCP + +Use the `project-settings-update` tool with the full list (the field is +replaced, not merged): + +```json +{ + "path_cleaning_filters": [ + { "regex": "/users/me/profile", "alias": "/users/me/profile", "order": 0 }, + { "regex": "/users/\\d+/profile", "alias": "/users//profile", "order": 1 }, + { "regex": "/users/[a-z0-9-]+", "alias": "/users/", "order": 2 } + ] +} +``` + +Always **read the existing rules first** (project settings include +`path_cleaning_filters`) and merge — overwriting silently destroys whatever the +team has already configured. + +## Where the rules apply + +When the user (or a HogQL query) opts in: + +- Web analytics: the **Path cleaning** toggle in the page header + (`PathCleaningToggle.tsx`) +- Paths insights: the path cleaning toggle in the insight filters +- HogQL: any query that calls `apply_path_cleaning(path_expr, team)` + +The rules are stored once per project — they are not insight-scoped. + +## Common pitfalls + +- **Backreferences in `alias` need double-escaping** — ClickHouse's + `replaceRegexpAll` supports `\0` (whole match) and `\1`–`\9` (capture + groups). In a JSON field or SQL string literal the backslash must be + doubled, so use `\\1` in `path_cleaning_filters` / HogQL to get the `\1` + backreference at the ClickHouse layer. +- **Forgetting `$`** — `\d+` without an end anchor matches every numeric run + in any path, so `/blog/2024-09-12/post` becomes + `/blog/--/post` when you only meant to match the year + segment. Use `\d+$` or `\d+(/|$)` depending on intent. +- **Escaping `/`** — re2 does not require it. `\/` works but adds noise. +- **Case sensitivity** — re2 is case-sensitive by default. Use `(?i)` at the + start of the pattern for case-insensitive matching, e.g. `(?i)/users/\d+`. +- **Replacing the whole list** — `path_cleaning_filters` is overwrite, not + append. Always start from the current list. +- **Rules apply globally** — adding a rule can change historical numbers in + every Web analytics / Paths chart that has cleaning enabled. Warn the user + before applying anything destructive. diff --git a/skills/planning-user-interviews/SKILL.md b/skills/planning-user-interviews/SKILL.md new file mode 100644 index 0000000..a8e0b4f --- /dev/null +++ b/skills/planning-user-interviews/SKILL.md @@ -0,0 +1,187 @@ +--- +name: planning-user-interviews +description: 'Plan a user interview topic in PostHog — pick who to target (cohort, emails, or PostHog distinct IDs), draft what to ask about, and prepare the voice-agent context plus a question list. Use when the user asks to "talk to users", "check how users feel about X", "interview some customers", "set up a user interview", "run a user-research call", "find users to ask about Y", or otherwise wants qualitative feedback through a conversation. Walks the user through targeting (cohorts-list, persons-list, or accepting emails / distinct IDs directly), captures the topic, and prompts for agent context and questions before calling user-interview-topics-create. Do NOT trigger when the user is uploading a recorded interview audio file (that''s the separate UserInterview/transcript flow) or only browsing existing topics with user-interview-topics-list.' +--- + +# Planning user interviews + +Use this skill when someone asks to set up a user interview — to talk to customers, check sentiment, or gather qualitative feedback through a voice conversation. The plan is captured as a `UserInterviewTopic` that a voice agent will later run through. + +## What a complete topic needs + +Before calling `user-interview-topics-create`, gather these: + +1. **Who to interview** — at least one of: + - `interviewee_cohort` — an existing cohort ID + - `interviewee_emails` — list of email addresses + - `interviewee_distinct_ids` — list of PostHog distinct IDs +2. **What to ask about** — `topic` (required free text) +3. **How the agent should frame the conversation** — optional `agent_context` (extra system prompt) +4. **The questions to work through** — optional ordered `questions` list + +The API rejects topics with no targeting, so at least one of the three audience fields must be set. They can be combined — a cohort plus a handful of extra emails is fine. + +## Step 1: Clarify intent + +If the request is vague, ask: + +- **Which feature or behavior?** "checkout" might be the button click, the page view, or the payment submission — narrow it down to one event. +- **What do you want to learn?** Why they bounced? What confused them? What alternatives they tried? The goal shapes both the audience and the questions. +- **Which kind of users?** Heavy users (what works), drop-offs (what blocks adoption), at-risk users (what breaks retention), or a mix. + +Skip these questions only when the user has already answered them. + +## Step 2: Pick the audience + +Map what the user said to one of these paths: + +- **They named a cohort** ("our power users", "trial signups last week") — use `cohorts-list` filtered by name to find the cohort, confirm the match, and pass the cohort ID as `interviewee_cohort`. +- **They described the kind of person but no cohort exists** — offer to either create the cohort first (`cohorts-create`) or fall back to finding people by behavior (see below). +- **They gave email addresses or distinct IDs** — accept them directly. Skip the cohort lookup. +- **They described a behavior, not a cohort** ("users who tried checkout but didn't finish", "people who used to use dark mode and stopped") — find them by querying their events (see below). +- **They were vague** ("a few customers", "some power users") — ask which they prefer: + - Pick an existing cohort → `cohorts-list` + - Look up specific persons by name or email → `persons-list` with a search query + - Find users by behavior → see below + - Paste a list of email addresses + +Each email passes through DRF email validation (display-name format `Paul D'Ambra ` is accepted alongside plain `paul@x.com`). + +### Finding users by behavior + +When the user describes who they want to talk to in behavioral terms, find them in the project's own data: + +1. **Find the right event.** Call `read-data-schema` to list events that actually exist in the project. Don't guess event names from training data — PostHog event taxonomies are bespoke. Match the user's description to one or two candidate events; if multiple plausible matches exist, list them and ask which behavior they care about. +2. **Query for users.** Call `execute-sql` with HogQL. Filter by the chosen event over the last 60 days, group by person — prefer `person.properties.email` (directly usable as `interviewee_emails`), fall back to `distinct_id` (for `interviewee_distinct_ids`). Keep both kinds of rows. The aggregates in each template (`event_count`, `last_seen`, `days_since_last_seen`) are what feed Step 5's per-interviewee context. Replace `` with the chosen event, and `` with `person.properties.email` or `distinct_id`: + - **Heavy users** — `SELECT AS id, count() AS event_count, max(timestamp) AS last_seen, dateDiff('day', max(timestamp), now()) AS days_since_last_seen FROM events WHERE event = '' AND timestamp > now() - INTERVAL 60 DAY GROUP BY HAVING count() >= 5 ORDER BY count() DESC LIMIT 20` + - **Drop-offs** (tried once or twice and never came back) — `SELECT AS id, count() AS event_count, max(timestamp) AS last_seen, dateDiff('day', max(timestamp), now()) AS days_since_last_seen FROM events WHERE event = '' AND timestamp > now() - INTERVAL 60 DAY GROUP BY HAVING count() <= 2 AND dateDiff('day', max(timestamp), now()) > 14 ORDER BY count() ASC LIMIT 20` + - **At-risk** (was active, now dormant) — `SELECT AS id, count() AS event_count, max(timestamp) AS last_seen, dateDiff('day', max(timestamp), now()) AS days_since_last_seen FROM events WHERE event = '' AND timestamp > now() - INTERVAL 60 DAY GROUP BY HAVING count() >= 3 AND dateDiff('day', max(timestamp), now()) > 14 ORDER BY days_since_last_seen DESC LIMIT 20` +3. **Build a balanced sample.** Unless the user asked for one specific segment, mixing 5 heavy users + 3 drop-offs + 2 at-risk users yields the most actionable interviews: you learn what works, what blocks adoption, and what breaks retention. Adjust counts to match what the user actually wants. + +Pass the email rows as `interviewee_emails` and the distinct-ID rows as `interviewee_distinct_ids` — both can be set on the same topic. Keep `event_count` and `days_since_last_seen` per person so Step 5 can synthesise context like "used checkout 47 times in last 60 days; last seen 2 days ago". + +## Step 3: Capture the topic + +`topic` is one or two sentences describing what the interview is about. Infer from context where possible — don't ask the user to repeat themselves. + +Example: "ask trial users why they didn't convert" → `topic: "Why trial users didn't convert in the first 14 days"`. + +## Step 4: Prepare the voice agent + +Two fields shape what the agent actually does on the call. **Always ask about both before creating the topic.** + +### Always ask: what questions do they want to ask? + +`questions` is an ordered list the agent works through. Anchors, not a script — the agent will adapt phrasing. Keep them open-ended: + +- ✅ "What made you decide to try PostHog?" +- ❌ "Did you like PostHog?" + +If the user already listed questions in their original request, use those and confirm. Otherwise, ask explicitly: _"What questions do you want the agent to ask?"_ + +If the user can't think of any, suggest 3–5 open-ended questions drawn from the `topic` and offer them for review before creating. + +The field is technically optional in the API, but don't skip it silently — an interview with no questions is rarely useful. + +Question templates by research goal: + +- **Why users dropped off / churned**: + - "Tell me about the last time you tried [feature] — what were you trying to do?" + - "Walk me through what happened step-by-step." + - "What did you expect vs what actually happened?" + - "What made you stop or decide not to continue?" + - "What would need to change for you to use [feature] regularly?" +- **Why heavy users love a feature**: + - "Tell me about how you use [feature] — what problem does it solve for you?" + - "Walk me through your typical workflow." + - "What would you do if [feature] didn't exist?" + - "What almost made you not use it when you first tried?" + - "What's one thing you wish it did differently?" +- **Why someone hasn't tried a feature yet**: + - "Have you noticed [feature] in the product?" + - "What's stopped you from trying it?" + - "What would have to be true for it to be worth trying?" + +### Always offer: extra context to guide the interview + +`agent_context` is optional, but a few sentences here make the conversation dramatically better. Always offer the user the chance to provide it, e.g.: + +> _"Want to give the agent any extra context? Things like tone, what to avoid, or background on the interviewee help guide the conversation. It's optional."_ + +Useful kinds of context: + +- **Tone**: "warm and conversational", "skip pleasantries — this is a 10-minute call" +- **Constraints**: "don't promise feature delivery", "do not discuss pricing" +- **Background the agent should know**: "the user just churned from the Scale plan; be empathetic", "this person tried PostHog 6 months ago and bounced" +- **Persona**: "you are Sam, a PostHog product researcher" + +If the user declines, that's fine — leave `agent_context` empty and continue. + +## Calling user-interview-topics-create + +Once you have the pieces: + +```json +{ + "topic": "Why trial users churned in week 2", + "interviewee_cohort": 42, + "interviewee_emails": ["paul@acme.com"], + "agent_context": "Be warm. The interviewee just churned — don't pitch.", + "questions": [ + "What were you hoping PostHog would help with?", + "Where did you get stuck?", + "What would have made you stay?" + ] +} +``` + +After creation, capture the returned topic ID — you'll need it for Step 5 and for handing off to the voice agent. + +## Step 5: Optionally attach per-interviewee context + +The topic-level `agent_context` applies to every interviewee. If the user knows something specific about individual interviewees that should shape that one conversation, attach it as a per-interviewee row via `user-interview-topics-interviewees-create`. This is optional — most topics won't need it. + +Each row pairs an `interviewee_identifier` (must match one of the emails or distinct IDs in the parent topic's targeting) with an `agent_context` string. At most one row per (topic, interviewee). A user can have zero rows. + +Good per-interviewee context looks like: + +- "uses the replay product but has never used summarization" +- "churned from Scale plan last month — be empathetic, don't pitch" +- "founder, very technical, skip basic product explanations" + +After Step 4 succeeds, ask the user: _"Want to add per-interviewee context? Useful when individual people have very different backgrounds. You can either dictate the rows or paste a CSV."_ + +If you found the audience via behavioral query in Step 2, you already have per-person context (usage counts, dormancy windows). Use it: e.g. `"used checkout 47 times in last 60 days; last seen 2 days ago"` for heavy users, `"tried checkout once 18 days ago, never returned"` for drop-offs. + +### Accepting CSV input + +If the user pastes a CSV, expect two columns: `identifier,context`. Either with or without a header row. Examples: + +```csv +paul@acme.com,uses replay but never summarization +steve@apple.com,founder; very technical; skip product basics +``` + +Or with a header: + +```csv +identifier,context +abc-distinct-id-1,churned from Scale last month — be empathetic +``` + +Parse the CSV, then call `user-interview-topics-interviewees-create` once per row with the captured `topic_id`. Skip blank lines. Quote-escape commas inside the context cell — standard CSV rules. + +If a row's identifier isn't present in the parent topic's `interviewee_emails` or `interviewee_distinct_ids`, warn the user before creating — the voice agent looks up context by exact string match, so a mismatched identifier just gets ignored at runtime. + +## Edge cases + +- **No users match the behavioral query.** Possible reasons: the event isn't firing, the date range is too narrow, or no users have email addresses captured as person properties. Offer to widen the date range, try a different event, or fall back to cohorts / explicit emails. +- **Users matched but few have emails.** PostHog stores whatever the SDK captures. If only a handful of matching users have email addresses on their person profile, surface the count and ask: take the smaller sample, fall back to `interviewee_distinct_ids` (the agent can still reach them via in-app delivery), or skip the behavioral query and let the user paste emails directly. +- **Ambiguous event name.** If `read-data-schema` returns multiple candidates (e.g. `checkout_started`, `checkout_completed`, `checkout_abandoned`), list them with counts and let the user pick the behavior they want to understand. Don't pick silently. +- **User asks to interview only drop-offs (or only one segment).** That works, but flag the tradeoff: interviewing only drop-offs tells you what's broken without telling you what works. Recommend including 2–3 successful users for contrast unless the user has a reason for the narrower sample. + +## What this skill is not for + +- **Uploading a recorded interview** — that's the separate `UserInterview` model (`user_interviews_create` with an audio file). Different flow, different model. +- **Listing existing topics** — `user-interview-topics-list` handles that directly with `search`, `limit`, and `offset`. No skill needed. +- **Analyzing transcripts after the interview** — out of scope here; that lives with the recorded `UserInterview` flow. diff --git a/skills/querying-posthog-data/references/example-error-tracking.md b/skills/querying-posthog-data/references/example-error-tracking.md index 7af4759..0345d9b 100644 --- a/skills/querying-posthog-data/references/example-error-tracking.md +++ b/skills/querying-posthog-data/references/example-error-tracking.md @@ -10,13 +10,13 @@ SELECT count(DISTINCT uuid) AS occurrences, count(DISTINCT nullIf($session_id, '')) AS sessions, count(DISTINCT coalesce(nullIf(toString(person_id), '00000000-0000-0000-0000-000000000000'), distinct_id)) AS users, - sumForEach(arrayMap(bin -> if(and(greater(timestamp, bin), lessOrEquals(dateDiff('seconds', bin, timestamp), divide(dateDiff('seconds', toDateTime(toDateTime('2026-05-08 01:25:35.268897')), toDateTime(toDateTime('2026-05-09 01:25:35.269594'))), 20))), 1, 0), arrayMap(i -> dateAdd(toDateTime(toDateTime('2026-05-08 01:25:35.268897')), toIntervalSecond(multiply(i, divide(dateDiff('seconds', toDateTime(toDateTime('2026-05-08 01:25:35.268897')), toDateTime(toDateTime('2026-05-09 01:25:35.269594'))), 20)))), range(0, 20)))) AS volumeRange, + sumForEach(arrayMap(bin -> if(and(greater(timestamp, bin), lessOrEquals(dateDiff('seconds', bin, timestamp), divide(dateDiff('seconds', toDateTime(toDateTime('2026-05-13 18:19:05.636154')), toDateTime(toDateTime('2026-05-14 18:19:05.636658'))), 20))), 1, 0), arrayMap(i -> dateAdd(toDateTime(toDateTime('2026-05-13 18:19:05.636154')), toIntervalSecond(multiply(i, divide(dateDiff('seconds', toDateTime(toDateTime('2026-05-13 18:19:05.636154')), toDateTime(toDateTime('2026-05-14 18:19:05.636658'))), 20)))), range(0, 20)))) AS volumeRange, argMin(tuple(uuid, distinct_id, timestamp, properties), timestamp) AS first_event, argMax(properties.$lib, timestamp) AS library FROM events AS e WHERE - and(equals(event, '$exception'), isNotNull(e.issue_id), equals(properties.tag, 'max_ai'), greaterOrEquals(timestamp, toDateTime(toDateTime('2026-05-08 01:25:35.268897'))), lessOrEquals(timestamp, toDateTime(toDateTime('2026-05-09 01:25:35.269594'))), or(greater(position(lower(properties.$exception_types), lower('constant')), 0), greater(position(lower(properties.$exception_values), lower('constant')), 0), greater(position(lower(properties.$exception_sources), lower('constant')), 0), greater(position(lower(properties.$exception_functions), lower('constant')), 0), greater(position(lower(properties.email), lower('constant')), 0), greater(position(lower(person.properties.email), lower('constant')), 0))) + and(equals(event, '$exception'), isNotNull(e.issue_id), equals(properties.tag, 'max_ai'), greaterOrEquals(timestamp, toDateTime(toDateTime('2026-05-13 18:19:05.636154'))), lessOrEquals(timestamp, toDateTime(toDateTime('2026-05-14 18:19:05.636658'))), or(greater(position(lower(properties.$exception_types), lower('constant')), 0), greater(position(lower(properties.$exception_values), lower('constant')), 0), greater(position(lower(properties.$exception_sources), lower('constant')), 0), greater(position(lower(properties.$exception_functions), lower('constant')), 0), greater(position(lower(properties.email), lower('constant')), 0), greater(position(lower(person.properties.email), lower('constant')), 0))) GROUP BY id ORDER BY diff --git a/skills/querying-posthog-data/references/example-logs.md b/skills/querying-posthog-data/references/example-logs.md index 382e8a2..3fad7ef 100644 --- a/skills/querying-posthog-data/references/example-logs.md +++ b/skills/querying-posthog-data/references/example-logs.md @@ -23,7 +23,7 @@ SELECT FROM logs WHERE - and(and(greaterOrEquals(toStartOfDay(time_bucket), toStartOfDay(assumeNotNull(toDateTime('2025-12-09 00:00:00')))), lessOrEquals(toStartOfDay(time_bucket), toStartOfDay(assumeNotNull(toDateTime('2025-12-10 00:00:00'))))), 1, greaterOrEquals(timestamp, toDateTime('2026-05-08 01:25:38.402844')), indexHint(like(lower(body), '%timeout%')), ilike(toString(body), '%timeout%'), in(severity_text, tuple('warn', 'error', 'fatal'))) + and(and(greaterOrEquals(toStartOfDay(time_bucket), toStartOfDay(assumeNotNull(toDateTime('2025-12-09 00:00:00')))), lessOrEquals(toStartOfDay(time_bucket), toStartOfDay(assumeNotNull(toDateTime('2025-12-10 00:00:00'))))), 1, greaterOrEquals(timestamp, toDateTime('2026-05-13 18:19:07.839359')), indexHint(like(lower(body), '%timeout%')), ilike(toString(body), '%timeout%'), in(severity_text, tuple('warn', 'error', 'fatal'))) ORDER BY timestamp DESC, uuid DESC diff --git a/skills/querying-posthog-data/references/example-session-replay.md b/skills/querying-posthog-data/references/example-session-replay.md index 86001ce..4769adc 100644 --- a/skills/querying-posthog-data/references/example-session-replay.md +++ b/skills/querying-posthog-data/references/example-session-replay.md @@ -19,17 +19,17 @@ SELECT sum(s.console_error_count) AS console_error_count, max(s.retention_period_days) AS retention_period_days, plus(dateTrunc('DAY', start_time), toIntervalDay(coalesce(retention_period_days, 30))) AS expiry_time, - date_diff('DAY', toDateTime('2026-05-09 01:25:39.591112'), expiry_time) AS recording_ttl, - greaterOrEquals(max(s._timestamp), toDateTime('2026-05-09 01:20:39.590287')) AS ongoing, + date_diff('DAY', toDateTime('2026-05-14 18:19:08.735267'), expiry_time) AS recording_ttl, + greaterOrEquals(max(s._timestamp), toDateTime('2026-05-14 18:14:08.734467')) AS ongoing, round(multiply(divide(plus(plus(plus(divide(sum(s.active_milliseconds), 1000), sum(s.click_count)), sum(s.keypress_count)), sum(s.console_error_count)), plus(plus(plus(plus(sum(s.mouse_activity_count), dateDiff('SECOND', start_time, end_time)), sum(s.console_error_count)), sum(s.console_log_count)), sum(s.console_warn_count))), 100), 2) AS activity_score FROM raw_session_replay_events AS s WHERE - and(greaterOrEquals(s.min_first_timestamp, toDateTime('2026-05-06 00:00:00.000000')), lessOrEquals(s.min_first_timestamp, toDateTime('2026-05-09 01:25:39.590452'))) + and(greaterOrEquals(s.min_first_timestamp, toDateTime('2026-05-11 00:00:00.000000')), lessOrEquals(s.min_first_timestamp, toDateTime('2026-05-14 18:19:08.734623'))) GROUP BY session_id HAVING - and(greaterOrEquals(expiry_time, toDateTime('2026-05-09 01:25:39.591005')), equals(max(s.is_deleted), 0), greater(active_seconds, 5.0)) + and(greaterOrEquals(expiry_time, toDateTime('2026-05-14 18:19:08.735161')), equals(max(s.is_deleted), 0), greater(active_seconds, 5.0)) ORDER BY start_time DESC, session_id DESC diff --git a/skills/querying-posthog-data/references/example-sessions.md b/skills/querying-posthog-data/references/example-sessions.md index c27d03d..0ab3610 100644 --- a/skills/querying-posthog-data/references/example-sessions.md +++ b/skills/querying-posthog-data/references/example-sessions.md @@ -13,7 +13,7 @@ SELECT FROM sessions WHERE - and(less($start_timestamp, toDateTime('2026-05-09 01:25:44.964556')), greater($start_timestamp, toDateTime('2026-05-08 01:25:39.965353'))) + and(less($start_timestamp, toDateTime('2026-05-14 18:19:14.065688')), greater($start_timestamp, toDateTime('2026-05-13 18:19:09.066319'))) ORDER BY $start_timestamp DESC LIMIT 50000