Skip to content

Commit c8c4d26

Browse files
committed
Ensure we normalize get_decide
In a back compat manner.
1 parent 224518e commit c8c4d26

File tree

4 files changed

+87
-19
lines changed

4 files changed

+87
-19
lines changed

posthog/client.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from posthog.exception_utils import exc_info_from_error, exceptions_from_error_tuple, handle_in_app
1919
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
2020
from posthog.poller import Poller
21-
from posthog.request import DEFAULT_HOST, APIError, batch_post, decide, determine_server_host, get, remote_config
21+
from posthog.request import DEFAULT_HOST, APIError, batch_post, decide, determine_server_host, get, remote_config, normalize_decide_response, DecideResponse
2222
from posthog.utils import SizeLimitedDict, clean, guess_timezone, remove_trailing_slash
2323
from posthog.version import VERSION
2424

@@ -230,21 +230,21 @@ def get_feature_variants(
230230
self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None
231231
):
232232
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
233-
return resp_data["featureFlags"]
233+
return self.to_variants(resp_data)
234234

235235
def get_feature_payloads(
236236
self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None
237237
):
238238
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
239-
return resp_data["featureFlagPayloads"]
239+
return self.to_payloads(resp_data)
240240

241241
def get_feature_flags_and_payloads(
242242
self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None
243243
):
244244
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties, disable_geoip)
245245
return {
246-
"featureFlags": resp_data["featureFlags"],
247-
"featureFlagPayloads": resp_data["featureFlagPayloads"],
246+
"featureFlags": self.to_variants(resp_data),
247+
"featureFlagPayloads": self.to_payloads(resp_data),
248248
}
249249

250250
def get_decide(self, distinct_id, groups=None, person_properties=None, group_properties=None, disable_geoip=None):
@@ -267,7 +267,7 @@ def get_decide(self, distinct_id, groups=None, person_properties=None, group_pro
267267
}
268268
resp_data = decide(self.api_key, self.host, timeout=self.feature_flags_request_timeout_seconds, **request_data)
269269

270-
return resp_data
270+
return normalize_decide_response(resp_data)
271271

272272
def capture(
273273
self,
@@ -967,16 +967,17 @@ def get_all_flags(
967967
group_properties={},
968968
only_evaluate_locally=False,
969969
disable_geoip=None,
970-
):
971-
flags = self.get_all_flags_and_payloads(
970+
) -> dict[str, bool | str]:
971+
response = self.get_all_flags_and_payloads(
972972
distinct_id,
973973
groups=groups,
974974
person_properties=person_properties,
975975
group_properties=group_properties,
976976
only_evaluate_locally=only_evaluate_locally,
977977
disable_geoip=disable_geoip,
978978
)
979-
return flags["featureFlags"]
979+
980+
return self.to_variants(response)
980981

981982
def get_all_flags_and_payloads(
982983
self,
@@ -987,7 +988,7 @@ def get_all_flags_and_payloads(
987988
group_properties={},
988989
only_evaluate_locally=False,
989990
disable_geoip=None,
990-
):
991+
) -> DecideResponse:
991992
if self.disabled:
992993
return {"featureFlags": None, "featureFlagPayloads": None}
993994

@@ -998,7 +999,8 @@ def get_all_flags_and_payloads(
998999
flags, payloads, fallback_to_decide = self._get_all_flags_and_payloads_locally(
9991000
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
10001001
)
1001-
response = {"featureFlags": flags, "featureFlagPayloads": payloads}
1002+
1003+
response = normalize_decide_response({"featureFlags": flags, "featureFlagPayloads": payloads})
10021004

10031005
if fallback_to_decide and not only_evaluate_locally:
10041006
try:
@@ -1069,6 +1071,18 @@ def _add_local_person_and_group_properties(self, distinct_id, groups, person_pro
10691071

10701072
return all_person_properties, all_group_properties
10711073

1074+
def to_variants(self, response: any) -> dict[str, bool | str]:
1075+
if "flags" not in response:
1076+
return None
1077+
1078+
return {key: value.get_value() for key, value in response.get("flags", {}).items()}
1079+
1080+
def to_payloads(self, response: any) -> dict[str, str]:
1081+
if "flags" not in response:
1082+
return None
1083+
1084+
return {key: value.metadata.payload for key, value in response.get("flags", {}).items() if value.enabled}
1085+
10721086

10731087
def require(name, field, data_type):
10741088
"""Require that the named `field` has the right `data_type`"""

posthog/test/test_client.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from posthog.request import APIError
1212
from posthog.test.test_utils import FAKE_TEST_API_KEY
1313
from posthog.version import VERSION
14-
14+
from posthog.types import FeatureFlag, LegacyFlagMetadata
1515

1616
class TestClient(unittest.TestCase):
1717
@classmethod
@@ -1212,3 +1212,53 @@ def test_mock_system_context(
12121212
}
12131213

12141214
assert context == expected_context
1215+
1216+
@mock.patch("posthog.client.decide")
1217+
def test_get_decide_returns_normalized_decide_response(self, patch_decide):
1218+
patch_decide.return_value = {
1219+
"featureFlags": {"beta-feature": "random-variant", "alpha-feature": True, "off-feature": False},
1220+
"featureFlagPayloads": {"beta-feature": "{\"some\": \"data\"}"},
1221+
"errorsWhileComputingFlags": False,
1222+
"requestId": "test-id",
1223+
}
1224+
1225+
client = Client(FAKE_TEST_API_KEY)
1226+
distinct_id = "test_distinct_id"
1227+
groups = {"test_group_type": "test_group_id"}
1228+
person_properties = {"test_property": "test_value"}
1229+
1230+
response = client.get_decide(distinct_id, groups, person_properties)
1231+
1232+
assert response == {
1233+
"flags": {
1234+
"beta-feature": FeatureFlag(
1235+
key="beta-feature",
1236+
enabled=True,
1237+
variant="random-variant",
1238+
reason=None,
1239+
metadata=LegacyFlagMetadata(
1240+
payload="{\"some\": \"data\"}",
1241+
)
1242+
),
1243+
"alpha-feature": FeatureFlag(
1244+
key="alpha-feature",
1245+
enabled=True,
1246+
variant=None,
1247+
reason=None,
1248+
metadata=LegacyFlagMetadata(
1249+
payload=None,
1250+
)
1251+
),
1252+
"off-feature": FeatureFlag(
1253+
key="off-feature",
1254+
enabled=False,
1255+
variant=None,
1256+
reason=None,
1257+
metadata=LegacyFlagMetadata(
1258+
payload=None,
1259+
)
1260+
)
1261+
},
1262+
"errorsWhileComputingFlags": False,
1263+
"requestId": "test-id",
1264+
}

posthog/test/test_feature_flags.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -642,8 +642,9 @@ def test_get_all_flags_and_payloads_with_fallback(self, patch_decide, patch_capt
642642
},
643643
]
644644
# beta-feature value overridden by /decide
645+
flags_and_payloads = client.get_all_flags_and_payloads("distinct_id")
645646
self.assertEqual(
646-
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
647+
client.to_payloads(flags_and_payloads),
647648
{
648649
"beta-feature": 100,
649650
"beta-feature2": 300,
@@ -675,8 +676,9 @@ def test_get_all_flags_and_payloads_with_fallback_empty_local_flags(self, patch_
675676
client = self.client
676677
client.feature_flags = []
677678
# beta-feature value overridden by /decide
679+
flags_and_payloads = client.get_all_flags_and_payloads("distinct_id")
678680
self.assertEqual(
679-
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
681+
client.to_payloads(flags_and_payloads),
680682
{"beta-feature": 100, "beta-feature2": 300},
681683
)
682684
self.assertEqual(patch_decide.call_count, 1)
@@ -726,7 +728,6 @@ def test_get_all_flags_with_no_fallback(self, patch_decide, patch_capture):
726728
@mock.patch.object(Client, "capture")
727729
@mock.patch("posthog.client.decide")
728730
def test_get_all_flags_and_payloads_with_no_fallback(self, patch_decide, patch_capture):
729-
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}}
730731
client = self.client
731732
basic_flag = {
732733
"id": 1,
@@ -768,8 +769,9 @@ def test_get_all_flags_and_payloads_with_no_fallback(self, patch_decide, patch_c
768769
disabled_flag,
769770
]
770771
client.feature_flags_by_key = {"beta-feature": basic_flag, "disabled-feature": disabled_flag}
772+
all_flags_and_payloads = client.get_all_flags_and_payloads("distinct_id")
771773
self.assertEqual(
772-
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"], {"beta-feature": "new"}
774+
client.to_payloads(all_flags_and_payloads), {"beta-feature": "new"}
773775
)
774776
# decide not called because this can be evaluated locally
775777
self.assertEqual(patch_decide.call_count, 0)
@@ -900,8 +902,9 @@ def test_get_all_flags_and_payloads_with_fallback_but_only_local_evaluation_set(
900902
]
901903
client.feature_flags_by_key = {"beta-feature": flag_1, "disabled-feature": flag_2, "beta-feature2": flag_3}
902904
# beta-feature2 has no value
905+
flags_and_payloads = client.get_all_flags_and_payloads("distinct_id", only_evaluate_locally=True)
903906
self.assertEqual(
904-
client.get_all_flags_and_payloads("distinct_id", only_evaluate_locally=True)["featureFlagPayloads"],
907+
client.to_payloads(flags_and_payloads),
905908
{"beta-feature": "some-payload"},
906909
)
907910
self.assertEqual(patch_decide.call_count, 0)

posthog/types.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ def get_value(self):
3636

3737
@classmethod
3838
def from_value_and_payload(cls, key: str, value: any, payload: any) -> "FeatureFlag":
39+
enabled, variant = (True, value) if isinstance(value, str) else (value, None)
3940
return cls(
4041
key=key,
41-
enabled=True if isinstance(value, str) else value,
42-
variant=value if isinstance(value, str) else None,
42+
enabled=enabled,
43+
variant=variant,
4344
reason=None,
4445
metadata=LegacyFlagMetadata(
4546
payload=payload,

0 commit comments

Comments
 (0)