Skip to content
Draft
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 .sampo/changesets/is-enabled-default-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: minor
---

feat: `FeatureFlagEvaluations.is_enabled()` accepts a `default_value` returned when the flag has no value in the evaluation β€” the key was not part of the evaluated set, or the evaluation came back empty (failed `/flags` request, quota limit, no resolvable `distinct_id`). A flag that has a value still wins, so a disabled flag returns `False` even with `default_value=True`. The default is `False`, so existing calls behave exactly as before.
15 changes: 11 additions & 4 deletions posthog/feature_flag_evaluations.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,23 @@ def __init__(
self._minimal_flag_called_events = minimal_flag_called_events
self._accessed: Set[str] = set(accessed) if accessed is not None else set()

def is_enabled(self, key: str) -> bool:
def is_enabled(self, key: str, default_value: bool = False) -> bool:
"""Return whether the flag is enabled. Fires ``$feature_flag_called`` on the
first access per (distinct_id, flag, value) tuple, deduped via the SDK's cache.

Flags that were not returned from the underlying evaluation are treated as
disabled (returns ``False``).
Args:
key: The feature flag key.
default_value: Returned when the flag has no value in this evaluation β€”
the key was not part of the evaluated set, or the evaluation itself
came back empty (failed ``/flags`` request, quota limit, or no
resolvable ``distinct_id``). Defaults to ``False``.

A flag that has a value always wins over ``default_value``, including a
disabled flag, which returns ``False`` even when ``default_value=True``.
"""
flag = self._flags.get(key)
self._record_access(key)
return bool(flag.enabled) if flag else False
return bool(flag.enabled) if flag else default_value

def get_flag(self, key: str) -> Optional[FlagValue]:
"""Return the flag value. Fires ``$feature_flag_called`` on first access.
Expand Down
45 changes: 45 additions & 0 deletions posthog/test/test_evaluate_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,51 @@ def test_is_enabled(self, _name, key, expected, patch_capture, patch_flags):
]
self.assertEqual(len(flag_called), 1)

@parameterized.expand(
[
("default_false", False),
("default_true", True),
]
)
@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_is_enabled_returns_default_value_for_missing_flag(
self, _name, default_value, patch_capture, patch_flags
):
patch_flags.return_value = _flags_response_fixture()
flags = self.client.evaluate_flags("user-1")

self.assertEqual(flags.is_enabled("missing-flag", default_value), default_value)

@parameterized.expand(
[
("disabled_flag_beats_default_true", "disabled-flag", True, False),
("boolean_flag_beats_default_false", "boolean-flag", False, True),
("variant_flag_beats_default_false", "variant-flag", False, True),
]
)
@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_is_enabled_prefers_flag_value_over_default_value(
self, _name, key, default_value, expected, patch_capture, patch_flags
):
patch_flags.return_value = _flags_response_fixture()
flags = self.client.evaluate_flags("user-1")

self.assertEqual(flags.is_enabled(key, default_value), expected)

@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_is_enabled_returns_default_value_when_flags_request_fails(
self, patch_capture, patch_flags
):
patch_flags.side_effect = Exception("flags request failed")
flags = self.client.evaluate_flags("user-1")

self.assertEqual(flags.keys, [])
self.assertTrue(flags.is_enabled("boolean-flag", True))
self.assertFalse(flags.is_enabled("boolean-flag"))

@parameterized.expand(
[
(
Expand Down
2 changes: 1 addition & 1 deletion references/public_api_snapshot.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1295,7 +1295,7 @@ method posthog.exception_utils.VariableSizeLimiter.can_add(size)
method posthog.exception_utils.VariableSizeLimiter.get_remaining_space()
method posthog.feature_flag_evaluations.FeatureFlagEvaluations.get_flag(key: str) -> Optional[FlagValue]
method posthog.feature_flag_evaluations.FeatureFlagEvaluations.get_flag_payload(key: str) -> Optional[Any]
method posthog.feature_flag_evaluations.FeatureFlagEvaluations.is_enabled(key: str) -> bool
method posthog.feature_flag_evaluations.FeatureFlagEvaluations.is_enabled(key: str, default_value: bool = False) -> bool
method posthog.feature_flag_evaluations.FeatureFlagEvaluations.only(keys: List[str]) -> FeatureFlagEvaluations
method posthog.feature_flag_evaluations.FeatureFlagEvaluations.only_accessed() -> FeatureFlagEvaluations
method posthog.flag_definition_cache.FlagDefinitionCacheProvider.get_flag_definitions() -> Union[Optional[FlagDefinitionCacheData], Awaitable[Optional[FlagDefinitionCacheData]]]
Expand Down