Skip to content
Merged
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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
## 4.0.0 - 2025-04-24

1. Added new method `get_feature_flag_result` which returns a `FeatureFlagResult` object. This object breaks down the result of a feature flag into its enabled state, variant, and payload. The benefit of this method is it allows you to retrieve the result of a feature flag and its payload in a single API call. You can call `get_value` on the result to get the value of the feature flag, which is the same value returned by `get_feature_flag` (aka the string `variant` if the flag is a multivariate flag or the `boolean` value if the flag is a boolean flag).

Example:

```python
result = posthog.get_feature_flag_result("my-flag", "distinct_id")
print(result.enabled) # True or False
print(result.variant) # 'the-variant-value' or None
print(result.payload) # {'foo': 'bar'}
print(result.get_value()) # 'the-variant-value' or True or False
print(result.reason) # 'matched condition set 2' (Not available for local evaluation)
```

Breaking change:

1. `get_feature_flag_payload` now deserializes payloads from JSON strings to `Any`. Previously, it returned the payload as a JSON encoded string.

Before:

```python
payload = get_feature_flag_payload('key', 'distinct_id') # "{\"some\": \"payload\"}"
```

After:

```python
payload = get_feature_flag_payload('key', 'distinct_id') # {"some": "payload"}
```

## 3.25.0 – 2025-04-15

1. Roll out new `/flags` endpoint to 100% of `/decide` traffic, excluding the top 10 customers.
Expand Down
1 change: 1 addition & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ check_untyped_defs = True
warn_unreachable = True
strict_equality = True
ignore_missing_imports = True
exclude = env/.*|venv/.*

[mypy-django.*]
ignore_missing_imports = True
Expand Down
157 changes: 94 additions & 63 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
)
from posthog.types import (
FeatureFlag,
FeatureFlagResult,
FlagMetadata,
FlagsAndPayloads,
FlagsResponse,
Expand Down Expand Up @@ -939,25 +940,19 @@ def feature_enabled(
return None
return bool(response)

def get_feature_flag(
def _get_feature_flag_result(
self,
key,
distinct_id,
*,
override_match_value: Optional[FlagValue] = None,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
disable_geoip=None,
) -> Optional[FlagValue]:
"""
Get a feature flag value for a key by evaluating locally or remotely
depending on whether local evaluation is enabled and the flag can be
locally evaluated.

This also captures the $feature_flag_called event unless send_feature_flag_events is False.
"""
) -> Optional[FeatureFlagResult]:
require("key", key, string_types)
require("distinct_id", distinct_id, ID_TYPES)
require("groups", groups, dict)
Expand All @@ -969,36 +964,101 @@ def get_feature_flag(
distinct_id, groups, person_properties, group_properties
)

response = self._locally_evaluate_flag(key, distinct_id, groups, person_properties, group_properties)

flag_result = None
flag_details = None
request_id = None

flag_was_locally_evaluated = response is not None
if not flag_was_locally_evaluated and not only_evaluate_locally:
flag_value = self._locally_evaluate_flag(key, distinct_id, groups, person_properties, group_properties)
flag_was_locally_evaluated = flag_value is not None

if flag_was_locally_evaluated:
lookup_match_value = override_match_value or flag_value
payload = self._compute_payload_locally(key, lookup_match_value) if lookup_match_value else None
flag_result = FeatureFlagResult.from_value_and_payload(key, lookup_match_value, payload)
elif not only_evaluate_locally:
try:
flag_details, request_id = self._get_feature_flag_details_from_decide(
key, distinct_id, groups, person_properties, group_properties, disable_geoip
)
response = flag_details.get_value() if flag_details else False
self.log.debug(f"Successfully computed flag remotely: #{key} -> #{response}")
flag_result = FeatureFlagResult.from_flag_details(flag_details, override_match_value)
self.log.debug(f"Successfully computed flag remotely: #{key} -> #{flag_result}")
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")

if send_feature_flag_events:
self._capture_feature_flag_called(
distinct_id,
key,
response or False,
None,
flag_result.get_value() if flag_result else None,
flag_result.payload if flag_result else None,
flag_was_locally_evaluated,
groups,
disable_geoip,
request_id,
flag_details,
)

return response
return flag_result

def get_feature_flag_result(
self,
key,
distinct_id,
*,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
disable_geoip=None,
) -> Optional[FeatureFlagResult]:
"""
Get a FeatureFlagResult object which contains the flag result and payload for a key by evaluating locally or remotely
depending on whether local evaluation is enabled and the flag can be locally evaluated.

This also captures the $feature_flag_called event unless send_feature_flag_events is False.
"""
return self._get_feature_flag_result(
key,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
disable_geoip=disable_geoip,
)

def get_feature_flag(
self,
key,
distinct_id,
*,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
disable_geoip=None,
) -> Optional[FlagValue]:
"""
Get a feature flag value for a key by evaluating locally or remotely
depending on whether local evaluation is enabled and the flag can be
locally evaluated.

This also captures the $feature_flag_called event unless send_feature_flag_events is False.
"""
feature_flag_result = self.get_feature_flag_result(
key,
distinct_id,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
disable_geoip=disable_geoip,
)
return feature_flag_result.get_value() if feature_flag_result else None

def _locally_evaluate_flag(
self,
Expand Down Expand Up @@ -1039,56 +1099,26 @@ def get_feature_flag_payload(
key,
distinct_id,
*,
match_value=None,
match_value: Optional[FlagValue] = None,
groups={},
person_properties={},
group_properties={},
only_evaluate_locally=False,
send_feature_flag_events=True,
disable_geoip=None,
):
if self.disabled:
return None

if match_value is None:
person_properties, group_properties = self._add_local_person_and_group_properties(
distinct_id, groups, person_properties, group_properties
)
match_value = self._locally_evaluate_flag(key, distinct_id, groups, person_properties, group_properties)

response = None
payload = None
flag_details = None
request_id = None

if match_value is not None:
payload = self._compute_payload_locally(key, match_value)

flag_was_locally_evaluated = payload is not None
if not flag_was_locally_evaluated and not only_evaluate_locally:
try:
flag_details, request_id = self._get_feature_flag_details_from_decide(
key, distinct_id, groups, person_properties, group_properties, disable_geoip
)
payload = flag_details.metadata.payload if flag_details else None
response = flag_details.get_value() if flag_details else False
except Exception as e:
self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}")

if send_feature_flag_events:
self._capture_feature_flag_called(
distinct_id,
key,
response or False,
payload,
flag_was_locally_evaluated,
groups,
disable_geoip,
request_id,
flag_details,
)

return payload
feature_flag_result = self._get_feature_flag_result(
key,
distinct_id,
override_match_value=match_value,
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
send_feature_flag_events=send_feature_flag_events,
disable_geoip=disable_geoip,
)
return feature_flag_result.payload if feature_flag_result else None

def _get_feature_flag_details_from_decide(
self,
Expand All @@ -1112,15 +1142,15 @@ def _capture_feature_flag_called(
self,
distinct_id: str,
key: str,
response: FlagValue,
response: Optional[FlagValue],
payload: Optional[str],
flag_was_locally_evaluated: bool,
groups: dict[str, str],
disable_geoip: Optional[bool],
request_id: Optional[str],
flag_details: Optional[FeatureFlag],
):
feature_flag_reported_key = f"{key}_{str(response)}"
feature_flag_reported_key = f"{key}_{'::null::' if response is None else str(response)}"

if feature_flag_reported_key not in self.distinct_ids_feature_flags_reported[distinct_id]:
properties: dict[str, Any] = {
Expand All @@ -1131,6 +1161,7 @@ def _capture_feature_flag_called(
}

if payload:
# if payload is not a string, json serialize it to a string
properties["$feature_flag_payload"] = payload

if request_id:
Expand Down
Loading