Skip to content

Commit 20b8825

Browse files
authored
feat(flags): support passing in lists of flag keys to the /flags endpoint instead of evaluating every flag every time we fall back (#307)
1 parent 818edc2 commit 20b8825

File tree

7 files changed

+256
-23
lines changed

7 files changed

+256
-23
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 6.6.0 - 2025-08-15
2+
3+
- feat: Add `flag_keys_to_evaluate` parameter to optimize feature flag evaluation performance by only evaluating specified flags
4+
- feat: Add `flag_keys_filter` option to `send_feature_flags` for selective flag evaluation in capture events
5+
16
# 6.5.0 - 2025-08-08
27

38
- feat: Add `$context_tags` to an event to know which properties were included as tags

example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112

113113
# Local Evaluation
114114

115-
# If flag has City=Sydney, this call doesn't go to `/decide`
115+
# If flag has City=Sydney, this call doesn't go to `/flags`
116116
print(
117117
posthog.feature_enabled(
118118
"test-flag",

posthog/client.py

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
FlagsAndPayloads,
4545
FlagsResponse,
4646
FlagValue,
47+
SendFeatureFlagsOptions,
4748
normalize_flags_response,
4849
to_flags_and_payloads,
4950
to_payloads,
@@ -313,6 +314,7 @@ def get_feature_variants(
313314
person_properties=None,
314315
group_properties=None,
315316
disable_geoip=None,
317+
flag_keys_to_evaluate: Optional[list[str]] = None,
316318
) -> dict[str, Union[bool, str]]:
317319
"""
318320
Get feature flag variants for a user by calling decide.
@@ -323,12 +325,19 @@ def get_feature_variants(
323325
person_properties: A dictionary of person properties.
324326
group_properties: A dictionary of group properties.
325327
disable_geoip: Whether to disable GeoIP for this request.
328+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
329+
only these flags will be evaluated, improving performance.
326330
327331
Category:
328332
Feature Flags
329333
"""
330334
resp_data = self.get_flags_decision(
331-
distinct_id, groups, person_properties, group_properties, disable_geoip
335+
distinct_id,
336+
groups,
337+
person_properties,
338+
group_properties,
339+
disable_geoip,
340+
flag_keys_to_evaluate,
332341
)
333342
return to_values(resp_data) or {}
334343

@@ -339,6 +348,7 @@ def get_feature_payloads(
339348
person_properties=None,
340349
group_properties=None,
341350
disable_geoip=None,
351+
flag_keys_to_evaluate: Optional[list[str]] = None,
342352
) -> dict[str, str]:
343353
"""
344354
Get feature flag payloads for a user by calling decide.
@@ -349,6 +359,8 @@ def get_feature_payloads(
349359
person_properties: A dictionary of person properties.
350360
group_properties: A dictionary of group properties.
351361
disable_geoip: Whether to disable GeoIP for this request.
362+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
363+
only these flags will be evaluated, improving performance.
352364
353365
Examples:
354366
```python
@@ -359,7 +371,12 @@ def get_feature_payloads(
359371
Feature Flags
360372
"""
361373
resp_data = self.get_flags_decision(
362-
distinct_id, groups, person_properties, group_properties, disable_geoip
374+
distinct_id,
375+
groups,
376+
person_properties,
377+
group_properties,
378+
disable_geoip,
379+
flag_keys_to_evaluate,
363380
)
364381
return to_payloads(resp_data) or {}
365382

@@ -370,6 +387,7 @@ def get_feature_flags_and_payloads(
370387
person_properties=None,
371388
group_properties=None,
372389
disable_geoip=None,
390+
flag_keys_to_evaluate: Optional[list[str]] = None,
373391
) -> FlagsAndPayloads:
374392
"""
375393
Get feature flags and payloads for a user by calling decide.
@@ -380,6 +398,8 @@ def get_feature_flags_and_payloads(
380398
person_properties: A dictionary of person properties.
381399
group_properties: A dictionary of group properties.
382400
disable_geoip: Whether to disable GeoIP for this request.
401+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
402+
only these flags will be evaluated, improving performance.
383403
384404
Examples:
385405
```python
@@ -390,7 +410,12 @@ def get_feature_flags_and_payloads(
390410
Feature Flags
391411
"""
392412
resp = self.get_flags_decision(
393-
distinct_id, groups, person_properties, group_properties, disable_geoip
413+
distinct_id,
414+
groups,
415+
person_properties,
416+
group_properties,
417+
disable_geoip,
418+
flag_keys_to_evaluate,
394419
)
395420
return to_flags_and_payloads(resp)
396421

@@ -401,6 +426,7 @@ def get_flags_decision(
401426
person_properties=None,
402427
group_properties=None,
403428
disable_geoip=None,
429+
flag_keys_to_evaluate: Optional[list[str]] = None,
404430
) -> FlagsResponse:
405431
"""
406432
Get feature flags decision.
@@ -411,6 +437,8 @@ def get_flags_decision(
411437
person_properties: A dictionary of person properties.
412438
group_properties: A dictionary of group properties.
413439
disable_geoip: Whether to disable GeoIP for this request.
440+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
441+
only these flags will be evaluated, improving performance.
414442
415443
Examples:
416444
```python
@@ -441,6 +469,9 @@ def get_flags_decision(
441469
"geoip_disable": disable_geoip,
442470
}
443471

472+
if flag_keys_to_evaluate:
473+
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
474+
444475
resp_data = flags(
445476
self.api_key,
446477
self.host,
@@ -545,6 +576,7 @@ def capture(
545576
group_properties=flag_options["group_properties"],
546577
disable_geoip=disable_geoip,
547578
only_evaluate_locally=True,
579+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
548580
)
549581
else:
550582
# Default behavior - use remote evaluation
@@ -554,6 +586,7 @@ def capture(
554586
person_properties=flag_options["person_properties"],
555587
group_properties=flag_options["group_properties"],
556588
disable_geoip=disable_geoip,
589+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
557590
)
558591
except Exception as e:
559592
self.log.exception(
@@ -586,16 +619,16 @@ def capture(
586619

587620
return self._enqueue(msg, disable_geoip)
588621

589-
def _parse_send_feature_flags(self, send_feature_flags) -> dict:
622+
def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
590623
"""
591624
Parse and normalize send_feature_flags parameter into a standard format.
592625
593626
Args:
594627
send_feature_flags: Either bool or SendFeatureFlagsOptions dict
595628
596629
Returns:
597-
dict: Normalized options with keys: should_send, only_evaluate_locally,
598-
person_properties, group_properties
630+
SendFeatureFlagsOptions: Normalized options with keys: should_send, only_evaluate_locally,
631+
person_properties, group_properties, flag_keys_filter
599632
600633
Raises:
601634
TypeError: If send_feature_flags is not bool or dict
@@ -608,13 +641,15 @@ def _parse_send_feature_flags(self, send_feature_flags) -> dict:
608641
),
609642
"person_properties": send_feature_flags.get("person_properties"),
610643
"group_properties": send_feature_flags.get("group_properties"),
644+
"flag_keys_filter": send_feature_flags.get("flag_keys_filter"),
611645
}
612646
elif isinstance(send_feature_flags, bool):
613647
return {
614648
"should_send": send_feature_flags,
615649
"only_evaluate_locally": None,
616650
"person_properties": None,
617651
"group_properties": None,
652+
"flag_keys_filter": None,
618653
}
619654
else:
620655
raise TypeError(
@@ -1184,12 +1219,12 @@ def _compute_flag_locally(
11841219
self.log.warning(
11851220
f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
11861221
)
1187-
# failover to `/decide/`
1222+
# failover to `/flags`
11881223
raise InconclusiveMatchError("Flag has unknown group type index")
11891224

11901225
if group_name not in groups:
11911226
# Group flags are never enabled in `groups` aren't passed in
1192-
# don't failover to `/decide/`, since response will be the same
1227+
# don't failover to `/flags`, since response will be the same
11931228
if warn_on_unknown_groups:
11941229
self.log.warning(
11951230
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
@@ -1317,7 +1352,7 @@ def _get_feature_flag_result(
13171352
)
13181353
elif not only_evaluate_locally:
13191354
try:
1320-
flag_details, request_id = self._get_feature_flag_details_from_decide(
1355+
flag_details, request_id = self._get_feature_flag_details_from_server(
13211356
key,
13221357
distinct_id,
13231358
groups,
@@ -1557,7 +1592,7 @@ def get_feature_flag_payload(
15571592
)
15581593
return feature_flag_result.payload if feature_flag_result else None
15591594

1560-
def _get_feature_flag_details_from_decide(
1595+
def _get_feature_flag_details_from_server(
15611596
self,
15621597
key: str,
15631598
distinct_id: ID_TYPES,
@@ -1567,10 +1602,15 @@ def _get_feature_flag_details_from_decide(
15671602
disable_geoip: Optional[bool],
15681603
) -> tuple[Optional[FeatureFlag], Optional[str]]:
15691604
"""
1570-
Calls /decide and returns the flag details and request id
1605+
Calls /flags and returns the flag details and request id
15711606
"""
15721607
resp_data = self.get_flags_decision(
1573-
distinct_id, groups, person_properties, group_properties, disable_geoip
1608+
distinct_id,
1609+
groups,
1610+
person_properties,
1611+
group_properties,
1612+
disable_geoip,
1613+
flag_keys_to_evaluate=[key],
15741614
)
15751615
request_id = resp_data.get("requestId")
15761616
flags = resp_data.get("flags")
@@ -1686,6 +1726,7 @@ def get_all_flags(
16861726
group_properties=None,
16871727
only_evaluate_locally=False,
16881728
disable_geoip=None,
1729+
flag_keys_to_evaluate: Optional[list[str]] = None,
16891730
) -> Optional[dict[str, Union[bool, str]]]:
16901731
"""
16911732
Get all feature flags for a user.
@@ -1697,6 +1738,8 @@ def get_all_flags(
16971738
group_properties: A dictionary of group properties.
16981739
only_evaluate_locally: Whether to only evaluate locally.
16991740
disable_geoip: Whether to disable GeoIP for this request.
1741+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
1742+
only these flags will be evaluated, improving performance.
17001743
17011744
Examples:
17021745
```python
@@ -1713,6 +1756,7 @@ def get_all_flags(
17131756
group_properties=group_properties,
17141757
only_evaluate_locally=only_evaluate_locally,
17151758
disable_geoip=disable_geoip,
1759+
flag_keys_to_evaluate=flag_keys_to_evaluate,
17161760
)
17171761

17181762
return response["featureFlags"]
@@ -1726,6 +1770,7 @@ def get_all_flags_and_payloads(
17261770
group_properties=None,
17271771
only_evaluate_locally=False,
17281772
disable_geoip=None,
1773+
flag_keys_to_evaluate: Optional[list[str]] = None,
17291774
) -> FlagsAndPayloads:
17301775
"""
17311776
Get all feature flags and their payloads for a user.
@@ -1737,6 +1782,8 @@ def get_all_flags_and_payloads(
17371782
group_properties: A dictionary of group properties.
17381783
only_evaluate_locally: Whether to only evaluate locally.
17391784
disable_geoip: Whether to disable GeoIP for this request.
1785+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
1786+
only these flags will be evaluated, improving performance.
17401787
17411788
Examples:
17421789
```python
@@ -1760,6 +1807,7 @@ def get_all_flags_and_payloads(
17601807
groups=groups,
17611808
person_properties=person_properties,
17621809
group_properties=group_properties,
1810+
flag_keys_to_evaluate=flag_keys_to_evaluate,
17631811
)
17641812

17651813
if fallback_to_decide and not only_evaluate_locally:
@@ -1770,6 +1818,7 @@ def get_all_flags_and_payloads(
17701818
person_properties=person_properties,
17711819
group_properties=group_properties,
17721820
disable_geoip=disable_geoip,
1821+
flag_keys_to_evaluate=flag_keys_to_evaluate,
17731822
)
17741823
return to_flags_and_payloads(decide_response)
17751824
except Exception as e:
@@ -1787,6 +1836,7 @@ def _get_all_flags_and_payloads_locally(
17871836
person_properties=None,
17881837
group_properties=None,
17891838
warn_on_unknown_groups=False,
1839+
flag_keys_to_evaluate: Optional[list[str]] = None,
17901840
) -> tuple[FlagsAndPayloads, bool]:
17911841
person_properties = person_properties or {}
17921842
group_properties = group_properties or {}
@@ -1799,7 +1849,15 @@ def _get_all_flags_and_payloads_locally(
17991849
fallback_to_decide = False
18001850
# If loading in previous line failed
18011851
if self.feature_flags:
1802-
for flag in self.feature_flags:
1852+
# Filter flags based on flag_keys_to_evaluate if provided
1853+
flags_to_process = self.feature_flags
1854+
if flag_keys_to_evaluate:
1855+
flag_keys_set = set(flag_keys_to_evaluate)
1856+
flags_to_process = [
1857+
flag for flag in self.feature_flags if flag["key"] in flag_keys_set
1858+
]
1859+
1860+
for flag in flags_to_process:
18031861
try:
18041862
flags[flag["key"]] = self._compute_flag_locally(
18051863
flag,
@@ -1815,7 +1873,7 @@ def _get_all_flags_and_payloads_locally(
18151873
if matched_payload is not None:
18161874
payloads[flag["key"]] = matched_payload
18171875
except InconclusiveMatchError:
1818-
# No need to log this, since it's just telling us to fall back to `/decide`
1876+
# No need to log this, since it's just telling us to fall back to `/flags`
18191877
fallback_to_decide = True
18201878
except Exception as e:
18211879
self.log.exception(

0 commit comments

Comments
 (0)