From 2f1c8a858624b57559bccfc744b95c3ae097db1e Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:50:43 +0000 Subject: [PATCH] feat: is_enabled() accepts a caller-supplied default_value The sdk-specs `is-feature-enabled` contract requires the boolean flag check to accept a caller-supplied boolean default and return it whenever the flag has no value. `FeatureFlagEvaluations.is_enabled()` hard-coded `False` for that case, so callers could not force a specific fallback. Add `default_value: bool = False`. A flag that has a value still wins, so a disabled flag returns `False` even with `default_value=True`. The default keeps existing calls behaving exactly as before. The deprecated `Client.feature_enabled()` is intentionally left unchanged. Generated-By: PostHog Code Task-Id: e433896a-0f63-478e-bf90-2c6d7c538d37 --- .sampo/changesets/is-enabled-default-value.md | 5 +++ posthog/feature_flag_evaluations.py | 15 +++++-- posthog/test/test_evaluate_flags.py | 45 +++++++++++++++++++ references/public_api_snapshot.txt | 2 +- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 .sampo/changesets/is-enabled-default-value.md diff --git a/.sampo/changesets/is-enabled-default-value.md b/.sampo/changesets/is-enabled-default-value.md new file mode 100644 index 00000000..a9e3b7bc --- /dev/null +++ b/.sampo/changesets/is-enabled-default-value.md @@ -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. diff --git a/posthog/feature_flag_evaluations.py b/posthog/feature_flag_evaluations.py index f622906b..c260fbde 100644 --- a/posthog/feature_flag_evaluations.py +++ b/posthog/feature_flag_evaluations.py @@ -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. diff --git a/posthog/test/test_evaluate_flags.py b/posthog/test/test_evaluate_flags.py index 9eb3a9fd..1e69f1b8 100644 --- a/posthog/test/test_evaluate_flags.py +++ b/posthog/test/test_evaluate_flags.py @@ -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( [ ( diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 90f4f940..af147e89 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -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]]]