Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .sampo/changesets/evaluate-flags-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
pypi/posthog: minor
---

Add `evaluate_flags()` and a new `flags` option on `capture()` so a single `/flags` call can power both flag branching and event enrichment per request:

```python
flags = posthog.evaluate_flags(distinct_id, person_properties={"plan": "enterprise"})
if flags.is_enabled("new-dashboard"):
render_new_dashboard()
posthog.capture("page_viewed", distinct_id=distinct_id, flags=flags)
```

The returned `FeatureFlagEvaluations` snapshot exposes `is_enabled()`, `get_flag()`, `get_flag_payload()` for branching and `only_accessed()` / `only([keys])` filter helpers. Pass `flag_keys=[...]` to `evaluate_flags()` to scope the underlying `/flags` request itself.

Deprecates `feature_enabled()`, `get_feature_flag()`, `get_feature_flag_payload()`, and `capture(send_feature_flags=...)`. They continue to work but now emit a `DeprecationWarning` pointing at `evaluate_flags()`. Removal is planned for the next major version.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's deprecate get_feature_flag_result as well. It lived a short life, but better to clean it up now.

58 changes: 58 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
)
from posthog.feature_flag_evaluations import (
FeatureFlagEvaluations as FeatureFlagEvaluations,
)
from posthog.feature_flags import (
InconclusiveMatchError as InconclusiveMatchError,
)
Expand Down Expand Up @@ -770,6 +773,61 @@ def get_all_flags_and_payloads(
)


def evaluate_flags(
distinct_id=None, # type: Optional[str]
groups=None, # type: Optional[Dict[str, str]]
person_properties=None, # type: Optional[Dict[str, Any]]
group_properties=None, # type: Optional[Dict[str, Dict[str, Any]]]
only_evaluate_locally=False, # type: bool
disable_geoip=None, # type: Optional[bool]
flag_keys=None, # type: Optional[list]
) -> FeatureFlagEvaluations:
"""Evaluate all feature flags for a user in a single call and return a
:class:`FeatureFlagEvaluations` snapshot. Branch on ``.is_enabled()`` /
``.get_flag()`` and pass the same snapshot to ``capture()`` via the
``flags`` option so events carry the exact flag values the code branched on.

Prefer this over repeated ``get_feature_flag()`` calls and over
``capture(send_feature_flags=True)`` — it consolidates flag evaluation into
a single ``/flags`` request per incoming request.

Args:
distinct_id: The user's distinct ID. If ``None``, falls back to the context
distinct_id. If still unresolvable, returns an empty snapshot.
groups: Mapping of group type to group key.
person_properties: Person properties to use for evaluation.
group_properties: Group properties keyed by group type.
only_evaluate_locally: If ``True``, never fall back to remote evaluation.
disable_geoip: Whether to disable GeoIP lookup.
flag_keys: Optional list of flag keys. When provided, only these flags are
evaluated — the underlying ``/flags`` request asks the server for just
this subset, which makes the response smaller and the request cheaper.
Use this when you only need a handful of flags out of many.

Examples:
```python
from posthog import evaluate_flags, capture
flags = evaluate_flags("user_123", person_properties={"plan": "enterprise"})
if flags.is_enabled("new-dashboard"):
render_new_dashboard()
capture("page_viewed", distinct_id="user_123", flags=flags)
```

Category:
Feature flags
"""
return _proxy(
"evaluate_flags",
distinct_id=distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
disable_geoip=disable_geoip,
flag_keys=flag_keys,
)


def feature_flag_definitions():
"""
Returns loaded feature flags.
Expand Down
17 changes: 13 additions & 4 deletions posthog/args.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TypedDict, Optional, Any, Dict, Union, Tuple, Type
from typing import TYPE_CHECKING, TypedDict, Optional, Any, Dict, Union, Tuple, Type
from types import TracebackType
from typing_extensions import NotRequired # For Python < 3.11 compatibility
from datetime import datetime
Expand All @@ -7,6 +7,9 @@

from posthog.types import SendFeatureFlagsOptions

if TYPE_CHECKING:
from posthog.feature_flag_evaluations import FeatureFlagEvaluations

ID_TYPES = Union[numbers.Number, str, UUID, int]


Expand All @@ -23,9 +26,14 @@ class OptionalCaptureArgs(TypedDict):
UUID is returned, so you can correlate it with actions in your app (like showing users an
error ID if you capture an exception).
groups: Group identifiers to associate with this event (format: {group_type: group_key})
send_feature_flags: Whether to include currently active feature flags in the event properties.
Can be a boolean (True/False) or a SendFeatureFlagsOptions object for advanced configuration.
Defaults to False.
flags: A ``FeatureFlagEvaluations`` snapshot from ``evaluate_flags()``. The exact flag
values from the snapshot are attached to the event with no additional network call —
prefer this over ``send_feature_flags``.
send_feature_flags: Deprecated — prefer ``flags`` with a ``FeatureFlagEvaluations``
snapshot. Whether to include currently active feature flags in the event properties.
Can be a boolean or a SendFeatureFlagsOptions object. Defaults to False. Fires a
hidden ``/flags`` request on capture and may return different values than the ones
the code branched on.
disable_geoip: Whether to disable GeoIP lookup for this event. Defaults to False.
"""

Expand All @@ -34,6 +42,7 @@ class OptionalCaptureArgs(TypedDict):
timestamp: NotRequired[Optional[Union[datetime, str]]]
uuid: NotRequired[Optional[str]]
groups: NotRequired[Optional[Dict[str, str]]]
flags: NotRequired[Optional["FeatureFlagEvaluations"]]
send_feature_flags: NotRequired[
Optional[Union[bool, SendFeatureFlagsOptions]]
] # Updated to support both boolean and options object
Expand Down
Loading
Loading