Skip to content
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done!

63 changes: 63 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,66 @@ 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]
device_id=None, # type: Optional[str]
) -> FeatureFlagEvaluations:
Comment on lines +776 to +785
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.

surfaced when documenting: we should include a device_id override for parity with the deprecated methods

this might be relevant for other sdks as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good shout! So most of the other SDKs didn't actually support this yet – it was only this one and JS that supported that operation. I do think we should support device_id in this method going forward for other SDKs, but given that only Python and JS have it now, it's only a concern for those SDKs. Definitely will update JS, though.

"""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.
device_id: Optional device ID override. If not provided, falls back to the
context device_id (which may be set via tracing headers). Used by
experience-continuity flags to match users across distinct_id changes.

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,
device_id=device_id,
)


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