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
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
86 changes: 72 additions & 14 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
FlagsAndPayloads,
FlagsResponse,
FlagValue,
SendFeatureFlagsOptions,
normalize_flags_response,
to_flags_and_payloads,
to_payloads,
Expand Down Expand Up @@ -313,6 +314,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 +325,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 +348,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 +359,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 +371,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 +387,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 +398,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 +410,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 +426,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 +437,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 +469,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 +576,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 +586,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 @@ -586,16 +619,16 @@ def capture(

return self._enqueue(msg, disable_geoip)

def _parse_send_feature_flags(self, send_feature_flags) -> dict:
def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
"""
Parse and normalize send_feature_flags parameter into a standard format.

Args:
send_feature_flags: Either bool or SendFeatureFlagsOptions dict

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

Raises:
TypeError: If send_feature_flags is not bool or dict
Expand All @@ -608,13 +641,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 +1219,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 +1352,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 +1592,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 +1602,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 +1726,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 +1738,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 +1756,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 +1770,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 +1782,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 +1807,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 +1818,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 +1836,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 +1849,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 +1873,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