diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a587f0..104b07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 3.14.2 - 2025-02-19 + +1. Evaluate feature flag payloads with case sensitivity correctly. Fixes ## 3.14.1 - 2025-02-18 diff --git a/posthog/client.py b/posthog/client.py index 056d7aa..4ba2573 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -822,7 +822,7 @@ def get_feature_flag_payload( distinct_id, groups, person_properties, group_properties, disable_geoip ) response = responses_and_payloads["featureFlags"].get(key, None) - payload = responses_and_payloads["featureFlagPayloads"].get(str(key).lower(), None) + payload = responses_and_payloads["featureFlagPayloads"].get(str(key), None) except Exception as e: self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}") @@ -875,10 +875,14 @@ def _compute_payload_locally(self, key, match_value): if self.feature_flags_by_key is None: return payload - flag_definition = self.feature_flags_by_key.get(key) or {} - flag_filters = flag_definition.get("filters") or {} - flag_payloads = flag_filters.get("payloads") or {} - payload = flag_payloads.get(str(match_value).lower(), None) + flag_definition = self.feature_flags_by_key.get(key) + if flag_definition: + flag_filters = flag_definition.get("filters") or {} + flag_payloads = flag_filters.get("payloads") or {} + # For boolean flags, convert True to "true" + # For multivariate flags, use the variant string as-is + lookup_value = "true" if isinstance(match_value, bool) and match_value else str(match_value) + payload = flag_payloads.get(lookup_value, None) return payload def get_all_flags( diff --git a/posthog/test/test_feature_flags.py b/posthog/test/test_feature_flags.py index 10cdb94..4fff516 100644 --- a/posthog/test/test_feature_flags.py +++ b/posthog/test/test_feature_flags.py @@ -4640,3 +4640,84 @@ def test_multivariate_flag_consistency(self, patch_get): self.assertEqual(feature_flag_match, results[i]) else: self.assertFalse(feature_flag_match) + + @mock.patch("posthog.client.decide") + def test_feature_flag_case_sensitive(self, mock_decide): + mock_decide.return_value = {"featureFlags": {}} # Ensure decide returns empty flags + + client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY) + client.feature_flags = [ + { + "id": 1, + "key": "Beta-Feature", + "active": True, + "filters": { + "groups": [{"properties": [], "rollout_percentage": 100}], + }, + } + ] + + # Test that flag evaluation is case-sensitive + self.assertTrue(client.feature_enabled("Beta-Feature", "user1")) + self.assertFalse(client.feature_enabled("beta-feature", "user1")) + self.assertFalse(client.feature_enabled("BETA-FEATURE", "user1")) + + @mock.patch("posthog.client.decide") + def test_feature_flag_payload_case_sensitive(self, mock_decide): + mock_decide.return_value = { + "featureFlags": {"Beta-Feature": True}, + "featureFlagPayloads": {"Beta-Feature": {"some": "value"}}, + } + + client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY) + client.feature_flags = [ + { + "id": 1, + "key": "Beta-Feature", + "active": True, + "filters": { + "groups": [{"properties": [], "rollout_percentage": 100}], + "payloads": { + "true": {"some": "value"}, + }, + }, + } + ] + + # Test that payload retrieval is case-sensitive + self.assertEqual(client.get_feature_flag_payload("Beta-Feature", "user1"), {"some": "value"}) + self.assertIsNone(client.get_feature_flag_payload("beta-feature", "user1")) + self.assertIsNone(client.get_feature_flag_payload("BETA-FEATURE", "user1")) + + @mock.patch("posthog.client.decide") + def test_feature_flag_case_sensitive_consistency(self, mock_decide): + mock_decide.return_value = { + "featureFlags": {"Beta-Feature": True}, + "featureFlagPayloads": {"Beta-Feature": {"some": "value"}}, + } + + client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY) + client.feature_flags = [ + { + "id": 1, + "key": "Beta-Feature", + "active": True, + "filters": { + "groups": [{"properties": [], "rollout_percentage": 100}], + "payloads": { + "true": {"some": "value"}, + }, + }, + } + ] + + # Test that flag evaluation and payload retrieval are consistently case-sensitive + # Only exact match should work + self.assertTrue(client.feature_enabled("Beta-Feature", "user1")) + self.assertEqual(client.get_feature_flag_payload("Beta-Feature", "user1"), {"some": "value"}) + + # Different cases should not match + test_cases = ["beta-feature", "BETA-FEATURE", "bEtA-FeAtUrE"] + for case in test_cases: + self.assertFalse(client.feature_enabled(case, "user1")) + self.assertIsNone(client.get_feature_flag_payload(case, "user1")) diff --git a/posthog/version.py b/posthog/version.py index 1ba1236..fcf6bfe 100644 --- a/posthog/version.py +++ b/posthog/version.py @@ -1,4 +1,4 @@ -VERSION = "3.14.1" +VERSION = "3.14.2" if __name__ == "__main__": print(VERSION, end="") # noqa: T201