diff --git a/.sampo/changesets/fearless-lady-kalma.md b/.sampo/changesets/fearless-lady-kalma.md new file mode 100644 index 00000000..d49676ec --- /dev/null +++ b/.sampo/changesets/fearless-lady-kalma.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +`feature_enabled()` now accepts a `default_value` parameter, returned when the flag has no value — not loaded, a failed `/flags` request, or no flag with that key. A flag that has a value, including `False` and variant strings, still always wins over the default. Existing calls that don't pass `default_value` are unaffected (it defaults to `None`, preserving the current three-state return). diff --git a/posthog/client.py b/posthog/client.py index 7f423130..373c8c27 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -3050,6 +3050,7 @@ def feature_enabled( key: str, distinct_id: ID_TYPES, *, + default_value: Optional[bool] = None, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, @@ -3064,6 +3065,10 @@ def feature_enabled( Args: key: The feature flag key. distinct_id: The distinct ID of the user. + default_value: Returned when the flag has no value — not loaded, a failed + flags request, or no flag with that key. A flag that has a value, + including `False` and variant strings, always wins over this default. + Defaults to `None`, preserving the pre-existing three-state return. groups: A dictionary of group information. person_properties: A dictionary of person properties. group_properties: A dictionary of group properties. @@ -3108,7 +3113,7 @@ def feature_enabled( response = flag_result.get_value() if flag_result else None if response is None: - return None + return default_value return bool(response) def _get_stale_flag_fallback( diff --git a/posthog/test/test_feature_flags.py b/posthog/test/test_feature_flags.py index c14c4188..1bde0020 100644 --- a/posthog/test/test_feature_flags.py +++ b/posthog/test/test_feature_flags.py @@ -3703,6 +3703,40 @@ def test_feature_enabled_doesnt_exist(self, patch_flags, patch_poll): patch_flags.side_effect = APIError(401, "flags error") self.assertIsNone(client.feature_enabled("doesnt-exist", "distinct_id")) + @mock.patch("posthog.client.Poller") + @mock.patch("posthog.client.flags") + def test_feature_enabled_doesnt_exist_returns_caller_default( + self, patch_flags, patch_poll + ): + client = Client(FAKE_TEST_API_KEY) + client.feature_flags = [] + + patch_flags.return_value = {"featureFlags": {}} + self.assertTrue( + client.feature_enabled("doesnt-exist", "distinct_id", default_value=True) + ) + self.assertFalse( + client.feature_enabled("doesnt-exist", "distinct_id", default_value=False) + ) + + patch_flags.side_effect = APIError(401, "flags error") + self.assertTrue( + client.feature_enabled("doesnt-exist", "distinct_id", default_value=True) + ) + + @mock.patch("posthog.client.Poller") + @mock.patch("posthog.client.flags") + def test_feature_enabled_present_value_wins_over_default( + self, patch_flags, patch_poll + ): + client = Client(FAKE_TEST_API_KEY) + client.feature_flags = [] + + patch_flags.return_value = {"featureFlags": {"beta-feature": False}} + self.assertFalse( + client.feature_enabled("beta-feature", "distinct_id", default_value=True) + ) + @mock.patch("posthog.client.Poller") @mock.patch("posthog.client.flags") def test_personal_api_key_doesnt_exist(self, patch_flags, patch_poll): diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index cd0a1dda..0c2c9dfd 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -1241,7 +1241,7 @@ method posthog.client.Client.alias(previous_id: ID_TYPES, distinct_id: Optional[ method posthog.client.Client.capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str] method posthog.client.Client.capture_exception(exception: Optional[ExceptionArg], **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str] method posthog.client.Client.evaluate_flags(distinct_id: Optional[ID_TYPES] = None, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, disable_geoip: Optional[bool] = None, flag_keys: Optional[List[str]] = None, device_id: Optional[str] = None) -> FeatureFlagEvaluations -method posthog.client.Client.feature_enabled(key: str, distinct_id: ID_TYPES, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[bool] +method posthog.client.Client.feature_enabled(key: str, distinct_id: ID_TYPES, *, default_value: Optional[bool] = None, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[bool] method posthog.client.Client.feature_flag_definitions() method posthog.client.Client.flush(timeout_seconds: Optional[float] = 10) -> None method posthog.client.Client.get_all_flags(distinct_id: ID_TYPES, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, disable_geoip: Optional[bool] = None, flag_keys_to_evaluate: Optional[list[str]] = None, device_id: Optional[str] = None) -> Optional[dict[str, Union[bool, str]]]