Skip to content

Commit 337f7da

Browse files
authored
fix(flags): remove lower() when evaluating feature flag payloads – these payloads are case-sensitive! (#191)
* haha okay * tests workin * format * use case-sensitive comparisons * omg LOL * fix tests * jeez
1 parent 31652d5 commit 337f7da

File tree

4 files changed

+94
-6
lines changed

4 files changed

+94
-6
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 3.14.2 - 2025-02-19
2+
3+
1. Evaluate feature flag payloads with case sensitivity correctly. Fixes <https://github.com/PostHog/posthog-python/issues/178>
14

25
## 3.14.1 - 2025-02-18
36

posthog/client.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ def get_feature_flag_payload(
822822
distinct_id, groups, person_properties, group_properties, disable_geoip
823823
)
824824
response = responses_and_payloads["featureFlags"].get(key, None)
825-
payload = responses_and_payloads["featureFlagPayloads"].get(str(key).lower(), None)
825+
payload = responses_and_payloads["featureFlagPayloads"].get(str(key), None)
826826
except Exception as e:
827827
self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}")
828828

@@ -875,10 +875,14 @@ def _compute_payload_locally(self, key, match_value):
875875
if self.feature_flags_by_key is None:
876876
return payload
877877

878-
flag_definition = self.feature_flags_by_key.get(key) or {}
879-
flag_filters = flag_definition.get("filters") or {}
880-
flag_payloads = flag_filters.get("payloads") or {}
881-
payload = flag_payloads.get(str(match_value).lower(), None)
878+
flag_definition = self.feature_flags_by_key.get(key)
879+
if flag_definition:
880+
flag_filters = flag_definition.get("filters") or {}
881+
flag_payloads = flag_filters.get("payloads") or {}
882+
# For boolean flags, convert True to "true"
883+
# For multivariate flags, use the variant string as-is
884+
lookup_value = "true" if isinstance(match_value, bool) and match_value else str(match_value)
885+
payload = flag_payloads.get(lookup_value, None)
882886
return payload
883887

884888
def get_all_flags(

posthog/test/test_feature_flags.py

+81
Original file line numberDiff line numberDiff line change
@@ -4640,3 +4640,84 @@ def test_multivariate_flag_consistency(self, patch_get):
46404640
self.assertEqual(feature_flag_match, results[i])
46414641
else:
46424642
self.assertFalse(feature_flag_match)
4643+
4644+
@mock.patch("posthog.client.decide")
4645+
def test_feature_flag_case_sensitive(self, mock_decide):
4646+
mock_decide.return_value = {"featureFlags": {}} # Ensure decide returns empty flags
4647+
4648+
client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
4649+
client.feature_flags = [
4650+
{
4651+
"id": 1,
4652+
"key": "Beta-Feature",
4653+
"active": True,
4654+
"filters": {
4655+
"groups": [{"properties": [], "rollout_percentage": 100}],
4656+
},
4657+
}
4658+
]
4659+
4660+
# Test that flag evaluation is case-sensitive
4661+
self.assertTrue(client.feature_enabled("Beta-Feature", "user1"))
4662+
self.assertFalse(client.feature_enabled("beta-feature", "user1"))
4663+
self.assertFalse(client.feature_enabled("BETA-FEATURE", "user1"))
4664+
4665+
@mock.patch("posthog.client.decide")
4666+
def test_feature_flag_payload_case_sensitive(self, mock_decide):
4667+
mock_decide.return_value = {
4668+
"featureFlags": {"Beta-Feature": True},
4669+
"featureFlagPayloads": {"Beta-Feature": {"some": "value"}},
4670+
}
4671+
4672+
client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
4673+
client.feature_flags = [
4674+
{
4675+
"id": 1,
4676+
"key": "Beta-Feature",
4677+
"active": True,
4678+
"filters": {
4679+
"groups": [{"properties": [], "rollout_percentage": 100}],
4680+
"payloads": {
4681+
"true": {"some": "value"},
4682+
},
4683+
},
4684+
}
4685+
]
4686+
4687+
# Test that payload retrieval is case-sensitive
4688+
self.assertEqual(client.get_feature_flag_payload("Beta-Feature", "user1"), {"some": "value"})
4689+
self.assertIsNone(client.get_feature_flag_payload("beta-feature", "user1"))
4690+
self.assertIsNone(client.get_feature_flag_payload("BETA-FEATURE", "user1"))
4691+
4692+
@mock.patch("posthog.client.decide")
4693+
def test_feature_flag_case_sensitive_consistency(self, mock_decide):
4694+
mock_decide.return_value = {
4695+
"featureFlags": {"Beta-Feature": True},
4696+
"featureFlagPayloads": {"Beta-Feature": {"some": "value"}},
4697+
}
4698+
4699+
client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
4700+
client.feature_flags = [
4701+
{
4702+
"id": 1,
4703+
"key": "Beta-Feature",
4704+
"active": True,
4705+
"filters": {
4706+
"groups": [{"properties": [], "rollout_percentage": 100}],
4707+
"payloads": {
4708+
"true": {"some": "value"},
4709+
},
4710+
},
4711+
}
4712+
]
4713+
4714+
# Test that flag evaluation and payload retrieval are consistently case-sensitive
4715+
# Only exact match should work
4716+
self.assertTrue(client.feature_enabled("Beta-Feature", "user1"))
4717+
self.assertEqual(client.get_feature_flag_payload("Beta-Feature", "user1"), {"some": "value"})
4718+
4719+
# Different cases should not match
4720+
test_cases = ["beta-feature", "BETA-FEATURE", "bEtA-FeAtUrE"]
4721+
for case in test_cases:
4722+
self.assertFalse(client.feature_enabled(case, "user1"))
4723+
self.assertIsNone(client.get_feature_flag_payload(case, "user1"))

posthog/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "3.14.1"
1+
VERSION = "3.14.2"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

0 commit comments

Comments
 (0)