Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 6.6.0 - 2025-08-15

- feat: Add `flag_keys_to_evaluate` parameter to optimize feature flag evaluation performance by only evaluating specified flags
- feat: Add `flag_keys_filter` option to `send_feature_flags` for selective flag evaluation in capture events

# 6.5.0 - 2025-08-08

- feat: Add `$context_tags` to an event to know which properties were included as tags
Expand Down
2 changes: 1 addition & 1 deletion example.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@

# Local Evaluation

# If flag has City=Sydney, this call doesn't go to `/decide`
# If flag has City=Sydney, this call doesn't go to `/flags`
print(
posthog.feature_enabled(
"test-flag",
Expand Down
81 changes: 69 additions & 12 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ def get_feature_variants(
person_properties=None,
group_properties=None,
disable_geoip=None,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> dict[str, Union[bool, str]]:
"""
Get feature flag variants for a user by calling decide.
Expand All @@ -323,12 +324,19 @@ def get_feature_variants(
person_properties: A dictionary of person properties.
group_properties: A dictionary of group properties.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
only these flags will be evaluated, improving performance.

Category:
Feature Flags
"""
resp_data = self.get_flags_decision(
distinct_id, groups, person_properties, group_properties, disable_geoip
distinct_id,
groups,
person_properties,
group_properties,
disable_geoip,
flag_keys_to_evaluate,
)
return to_values(resp_data) or {}

Expand All @@ -339,6 +347,7 @@ def get_feature_payloads(
person_properties=None,
group_properties=None,
disable_geoip=None,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> dict[str, str]:
"""
Get feature flag payloads for a user by calling decide.
Expand All @@ -349,6 +358,8 @@ def get_feature_payloads(
person_properties: A dictionary of person properties.
group_properties: A dictionary of group properties.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
only these flags will be evaluated, improving performance.

Examples:
```python
Expand All @@ -359,7 +370,12 @@ def get_feature_payloads(
Feature Flags
"""
resp_data = self.get_flags_decision(
distinct_id, groups, person_properties, group_properties, disable_geoip
distinct_id,
groups,
person_properties,
group_properties,
disable_geoip,
flag_keys_to_evaluate,
)
return to_payloads(resp_data) or {}

Expand All @@ -370,6 +386,7 @@ def get_feature_flags_and_payloads(
person_properties=None,
group_properties=None,
disable_geoip=None,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> FlagsAndPayloads:
"""
Get feature flags and payloads for a user by calling decide.
Expand All @@ -380,6 +397,8 @@ def get_feature_flags_and_payloads(
person_properties: A dictionary of person properties.
group_properties: A dictionary of group properties.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
only these flags will be evaluated, improving performance.

Examples:
```python
Expand All @@ -390,7 +409,12 @@ def get_feature_flags_and_payloads(
Feature Flags
"""
resp = self.get_flags_decision(
distinct_id, groups, person_properties, group_properties, disable_geoip
distinct_id,
groups,
person_properties,
group_properties,
disable_geoip,
flag_keys_to_evaluate,
)
return to_flags_and_payloads(resp)

Expand All @@ -401,6 +425,7 @@ def get_flags_decision(
person_properties=None,
group_properties=None,
disable_geoip=None,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> FlagsResponse:
"""
Get feature flags decision.
Expand All @@ -411,6 +436,8 @@ def get_flags_decision(
person_properties: A dictionary of person properties.
group_properties: A dictionary of group properties.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
only these flags will be evaluated, improving performance.

Examples:
```python
Expand Down Expand Up @@ -441,6 +468,9 @@ def get_flags_decision(
"geoip_disable": disable_geoip,
}

if flag_keys_to_evaluate:
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate

resp_data = flags(
self.api_key,
self.host,
Expand Down Expand Up @@ -545,6 +575,7 @@ def capture(
group_properties=flag_options["group_properties"],
disable_geoip=disable_geoip,
only_evaluate_locally=True,
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
)
else:
# Default behavior - use remote evaluation
Expand All @@ -554,6 +585,7 @@ def capture(
person_properties=flag_options["person_properties"],
group_properties=flag_options["group_properties"],
disable_geoip=disable_geoip,
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
)
except Exception as e:
self.log.exception(
Expand Down Expand Up @@ -595,7 +627,7 @@ def _parse_send_feature_flags(self, send_feature_flags) -> dict:

Returns:
dict: Normalized options with keys: should_send, only_evaluate_locally,
person_properties, group_properties
person_properties, group_properties, flag_keys_filter

Raises:
TypeError: If send_feature_flags is not bool or dict
Expand All @@ -608,13 +640,15 @@ def _parse_send_feature_flags(self, send_feature_flags) -> dict:
),
"person_properties": send_feature_flags.get("person_properties"),
"group_properties": send_feature_flags.get("group_properties"),
"flag_keys_filter": send_feature_flags.get("flag_keys_filter"),
}
elif isinstance(send_feature_flags, bool):
return {
"should_send": send_feature_flags,
"only_evaluate_locally": None,
"person_properties": None,
"group_properties": None,
"flag_keys_filter": None,
}
else:
raise TypeError(
Expand Down Expand Up @@ -1184,12 +1218,12 @@ def _compute_flag_locally(
self.log.warning(
f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
)
# failover to `/decide/`
# failover to `/flags`
raise InconclusiveMatchError("Flag has unknown group type index")

if group_name not in groups:
# Group flags are never enabled in `groups` aren't passed in
# don't failover to `/decide/`, since response will be the same
# don't failover to `/flags`, since response will be the same
if warn_on_unknown_groups:
self.log.warning(
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
Expand Down Expand Up @@ -1317,7 +1351,7 @@ def _get_feature_flag_result(
)
elif not only_evaluate_locally:
try:
flag_details, request_id = self._get_feature_flag_details_from_decide(
flag_details, request_id = self._get_feature_flag_details_from_server(
key,
distinct_id,
groups,
Expand Down Expand Up @@ -1557,7 +1591,7 @@ def get_feature_flag_payload(
)
return feature_flag_result.payload if feature_flag_result else None

def _get_feature_flag_details_from_decide(
def _get_feature_flag_details_from_server(
self,
key: str,
distinct_id: ID_TYPES,
Expand All @@ -1567,10 +1601,15 @@ def _get_feature_flag_details_from_decide(
disable_geoip: Optional[bool],
) -> tuple[Optional[FeatureFlag], Optional[str]]:
"""
Calls /decide and returns the flag details and request id
Calls /flags and returns the flag details and request id
"""
resp_data = self.get_flags_decision(
distinct_id, groups, person_properties, group_properties, disable_geoip
distinct_id,
groups,
person_properties,
group_properties,
disable_geoip,
flag_keys_to_evaluate=[key],
)
request_id = resp_data.get("requestId")
flags = resp_data.get("flags")
Expand Down Expand Up @@ -1686,6 +1725,7 @@ def get_all_flags(
group_properties=None,
only_evaluate_locally=False,
disable_geoip=None,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> Optional[dict[str, Union[bool, str]]]:
"""
Get all feature flags for a user.
Expand All @@ -1697,6 +1737,8 @@ def get_all_flags(
group_properties: A dictionary of group properties.
only_evaluate_locally: Whether to only evaluate locally.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
only these flags will be evaluated, improving performance.

Examples:
```python
Expand All @@ -1713,6 +1755,7 @@ def get_all_flags(
group_properties=group_properties,
only_evaluate_locally=only_evaluate_locally,
disable_geoip=disable_geoip,
flag_keys_to_evaluate=flag_keys_to_evaluate,
)

return response["featureFlags"]
Expand All @@ -1726,6 +1769,7 @@ def get_all_flags_and_payloads(
group_properties=None,
only_evaluate_locally=False,
disable_geoip=None,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> FlagsAndPayloads:
"""
Get all feature flags and their payloads for a user.
Expand All @@ -1737,6 +1781,8 @@ def get_all_flags_and_payloads(
group_properties: A dictionary of group properties.
only_evaluate_locally: Whether to only evaluate locally.
disable_geoip: Whether to disable GeoIP for this request.
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
only these flags will be evaluated, improving performance.

Examples:
```python
Expand All @@ -1760,6 +1806,7 @@ def get_all_flags_and_payloads(
groups=groups,
person_properties=person_properties,
group_properties=group_properties,
flag_keys_to_evaluate=flag_keys_to_evaluate,
)

if fallback_to_decide and not only_evaluate_locally:
Expand All @@ -1770,6 +1817,7 @@ def get_all_flags_and_payloads(
person_properties=person_properties,
group_properties=group_properties,
disable_geoip=disable_geoip,
flag_keys_to_evaluate=flag_keys_to_evaluate,
)
return to_flags_and_payloads(decide_response)
except Exception as e:
Expand All @@ -1787,6 +1835,7 @@ def _get_all_flags_and_payloads_locally(
person_properties=None,
group_properties=None,
warn_on_unknown_groups=False,
flag_keys_to_evaluate: Optional[list[str]] = None,
) -> tuple[FlagsAndPayloads, bool]:
person_properties = person_properties or {}
group_properties = group_properties or {}
Expand All @@ -1799,7 +1848,15 @@ def _get_all_flags_and_payloads_locally(
fallback_to_decide = False
# If loading in previous line failed
if self.feature_flags:
for flag in self.feature_flags:
# Filter flags based on flag_keys_to_evaluate if provided
flags_to_process = self.feature_flags
if flag_keys_to_evaluate:
flag_keys_set = set(flag_keys_to_evaluate)
flags_to_process = [
flag for flag in self.feature_flags if flag["key"] in flag_keys_set
]

for flag in flags_to_process:
try:
flags[flag["key"]] = self._compute_flag_locally(
flag,
Expand All @@ -1815,7 +1872,7 @@ def _get_all_flags_and_payloads_locally(
if matched_payload is not None:
payloads[flag["key"]] = matched_payload
except InconclusiveMatchError:
# No need to log this, since it's just telling us to fall back to `/decide`
# No need to log this, since it's just telling us to fall back to `/flags`
fallback_to_decide = True
except Exception as e:
self.log.exception(
Expand Down
Loading
Loading