Skip to content

Commit c5e4ef7

Browse files
committed
Add failing tests
1 parent fb3dd2b commit c5e4ef7

File tree

3 files changed

+226
-5
lines changed

3 files changed

+226
-5
lines changed

posthog/test/test_feature_flags.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,6 +2342,91 @@ def test_capture_is_called(self, patch_decide, patch_capture):
23422342
disable_geoip=None,
23432343
)
23442344

2345+
@mock.patch.object(Client, "capture")
2346+
@mock.patch("posthog.client.decide")
2347+
def test_capture_is_called_with_flag_details(self, patch_decide, patch_capture):
2348+
patch_decide.return_value = {
2349+
"flags": {
2350+
"decide-flag": {
2351+
"key": "decide-flag",
2352+
"enabled": True,
2353+
"variant": "decide-variant",
2354+
"reason": {
2355+
"description": "Matched condition set 1",
2356+
},
2357+
"metadata": {
2358+
"id": 23,
2359+
"version": 42,
2360+
},
2361+
}
2362+
},
2363+
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
2364+
}
2365+
client = Client(FAKE_TEST_API_KEY)
2366+
2367+
self.assertEqual(client.get_feature_flag("decide-flag", "some-distinct-id"), "decide-variant")
2368+
self.assertEqual(patch_capture.call_count, 1)
2369+
patch_capture.assert_called_with(
2370+
"some-distinct-id",
2371+
"$feature_flag_called",
2372+
{
2373+
"$feature_flag": "decide-flag",
2374+
"$feature_flag_response": "decide-variant",
2375+
"locally_evaluated": False,
2376+
"$feature/decide-flag": "decide-variant",
2377+
"$feature_flag_reason": "Matched condition set 1",
2378+
"$feature_flag_id": 23,
2379+
"$feature_flag_version": 42,
2380+
"$feature_flag_request_id": "18043bf7-9cf6-44cd-b959-9662ee20d371",
2381+
},
2382+
groups={},
2383+
disable_geoip=None,
2384+
)
2385+
2386+
@mock.patch.object(Client, "capture")
2387+
@mock.patch("posthog.client.decide")
2388+
def test_capture_is_called_with_flag_details_and_payload(self, patch_decide, patch_capture):
2389+
patch_decide.return_value = {
2390+
"flags": {
2391+
"decide-flag-with-payload": {
2392+
"key": "decide-flag-with-payload",
2393+
"enabled": True,
2394+
"variant": None,
2395+
"reason": {
2396+
"description": "Matched condition set 1",
2397+
},
2398+
"metadata": {
2399+
"id": 23,
2400+
"version": 42,
2401+
"payload": "{\"foo\": \"bar\"}",
2402+
},
2403+
}
2404+
},
2405+
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
2406+
}
2407+
client = Client(FAKE_TEST_API_KEY)
2408+
2409+
self.assertEqual(client.get_feature_flag_payload("decide-flag-with-payload", "some-distinct-id"), "{\"foo\": \"bar\"}")
2410+
self.assertEqual(patch_capture.call_count, 1)
2411+
patch_capture.assert_called_with(
2412+
"some-distinct-id",
2413+
"$feature_flag_called",
2414+
{
2415+
"$feature_flag": "decide-flag-with-payload",
2416+
"$feature_flag_response": None,
2417+
"locally_evaluated": False,
2418+
"$feature/decide-flag-with-payload": None,
2419+
"$feature_flag_reason": "Matched condition set 1",
2420+
"$feature_flag_id": 23,
2421+
"$feature_flag_version": 42,
2422+
"$feature_flag_request_id": "18043bf7-9cf6-44cd-b959-9662ee20d371",
2423+
"$feature_flag_payload": "{\"foo\": \"bar\"}",
2424+
},
2425+
groups={},
2426+
disable_geoip=None,
2427+
)
2428+
2429+
23452430
@mock.patch("posthog.client.decide")
23462431
def test_capture_is_called_but_does_not_add_all_flags(self, patch_decide):
23472432
patch_decide.return_value = {"featureFlags": {"decide-flag": "decide-value"}}

posthog/test/test_types.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,101 @@ def test_to_flags_and_payloads_empty(self):
142142
result = to_flags_and_payloads(resp)
143143

144144
self.assertEqual(result["featureFlags"], {})
145-
self.assertEqual(result["featureFlagPayloads"], {})
145+
self.assertEqual(result["featureFlagPayloads"], {})
146+
147+
def test_to_flags_and_payloads_with_payload(self):
148+
resp = {
149+
"flags": {
150+
"decide-flag": {
151+
"key": "decide-flag",
152+
"enabled": True,
153+
"variant": "decide-variant",
154+
"reason": {
155+
"code": "matched_condition",
156+
"condition_index": 0,
157+
"description": "Matched condition set 1",
158+
},
159+
"metadata": {
160+
"id": 23,
161+
"version": 42,
162+
"payload": "{\"foo\": \"bar\"}",
163+
},
164+
}
165+
},
166+
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
167+
}
168+
169+
normalized = normalize_decide_response(resp)
170+
result = to_flags_and_payloads(normalized)
171+
172+
self.assertEqual(result["featureFlags"]["decide-flag"], "decide-variant")
173+
self.assertEqual(result["featureFlagPayloads"]["decide-flag"], "{\"foo\": \"bar\"}")
174+
175+
def test_feature_flag_from_json(self):
176+
# Test with full metadata
177+
resp = {
178+
"key": "test-flag",
179+
"enabled": True,
180+
"variant": "test-variant",
181+
"reason": {
182+
"code": "matched_condition",
183+
"condition_index": 0,
184+
"description": "Matched condition set 1"
185+
},
186+
"metadata": {
187+
"id": 1,
188+
"payload": '{"some": "json"}',
189+
"version": 2,
190+
"description": "test-description"
191+
}
192+
}
193+
194+
flag = FeatureFlag.from_json(resp)
195+
self.assertEqual(flag.key, "test-flag")
196+
self.assertTrue(flag.enabled)
197+
self.assertEqual(flag.variant, "test-variant")
198+
self.assertEqual(flag.get_value(), "test-variant")
199+
self.assertEqual(
200+
flag.reason, FlagReason(code="matched_condition", condition_index=0, description="Matched condition set 1")
201+
)
202+
self.assertEqual(
203+
flag.metadata, FlagMetadata(id=1, payload='{"some": "json"}', version=2, description="test-description")
204+
)
205+
206+
def test_feature_flag_from_json_minimal(self):
207+
# Test with minimal required fields
208+
resp = {
209+
"key": "test-flag",
210+
"enabled": True
211+
}
212+
213+
flag = FeatureFlag.from_json(resp)
214+
self.assertEqual(flag.key, "test-flag")
215+
self.assertTrue(flag.enabled)
216+
self.assertIsNone(flag.variant)
217+
self.assertEqual(flag.get_value(), True)
218+
self.assertIsNone(flag.reason)
219+
self.assertEqual(flag.metadata, LegacyFlagMetadata(payload=None))
220+
221+
def test_feature_flag_from_json_without_metadata(self):
222+
# Test with reason but no metadata
223+
resp = {
224+
"key": "test-flag",
225+
"enabled": True,
226+
"variant": "test-variant",
227+
"reason": {
228+
"code": "matched_condition",
229+
"condition_index": 0,
230+
"description": "Matched condition set 1"
231+
}
232+
}
233+
234+
flag = FeatureFlag.from_json(resp)
235+
self.assertEqual(flag.key, "test-flag")
236+
self.assertTrue(flag.enabled)
237+
self.assertEqual(flag.variant, "test-variant")
238+
self.assertEqual(flag.get_value(), "test-variant")
239+
self.assertEqual(
240+
flag.reason, FlagReason(code="matched_condition", condition_index=0, description="Matched condition set 1")
241+
)
242+
self.assertEqual(flag.metadata, LegacyFlagMetadata(payload=None))

posthog/types.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
FlagValue: TypeAlias = bool | str
55

66
@dataclass(frozen=True)
7-
class FlagReason(TypedDict):
7+
class FlagReason:
88
code: str
99
condition_index: int
1010
description: str
@@ -35,6 +35,35 @@ def get_value(self) -> FlagValue:
3535
assert self.variant is None or self.enabled
3636
return self.variant or self.enabled
3737

38+
@classmethod
39+
def from_json(cls, resp: Any) -> "FeatureFlag":
40+
reason = None
41+
if resp.get("reason"):
42+
reason = FlagReason(
43+
code=resp["reason"].get("code", ""),
44+
condition_index=resp["reason"].get("condition_index", 0),
45+
description=resp["reason"].get("description", "")
46+
)
47+
48+
metadata = None
49+
if resp.get("metadata"):
50+
metadata = FlagMetadata(
51+
id=resp["metadata"].get("id", 0),
52+
payload=resp["metadata"].get("payload"),
53+
version=resp["metadata"].get("version", 0),
54+
description=resp["metadata"].get("description", "")
55+
)
56+
else:
57+
metadata = LegacyFlagMetadata(payload=None)
58+
59+
return cls(
60+
key=resp["key"],
61+
enabled=resp["enabled"],
62+
variant=resp.get("variant"),
63+
reason=reason,
64+
metadata=metadata
65+
)
66+
3867
@classmethod
3968
def from_value_and_payload(cls, key: str, value: FlagValue, payload: Any) -> "FeatureFlag":
4069
enabled, variant = (True, value) if isinstance(value, str) else (value, None)
@@ -72,7 +101,16 @@ def normalize_decide_response(resp: Any) -> DecideResponse:
72101
"""
73102
if "requestId" not in resp:
74103
resp["requestId"] = None
75-
if "flags" not in resp:
104+
if "flags" in resp:
105+
flags = resp["flags"]
106+
# For each flag, create a FeatureFlag object
107+
for key, value in flags.items():
108+
if isinstance(value, FeatureFlag):
109+
continue
110+
value["key"] = key
111+
flags[key] = FeatureFlag.from_json(value)
112+
else:
113+
# Handle legacy format
76114
featureFlags = resp.get("featureFlags", {})
77115
featureFlagPayloads = resp.get("featureFlagPayloads", {})
78116
resp.pop("featureFlags", None)
@@ -106,10 +144,11 @@ def to_values(response: DecideResponse) -> dict[str, FlagValue] | None:
106144
if "flags" not in response:
107145
return None
108146

109-
return {key: value.get_value() for key, value in response.get("flags", {}).items()}
147+
flags = response.get("flags", {})
148+
return {key: value.get_value() for key, value in flags.items() if isinstance(value, FeatureFlag)}
110149

111150
def to_payloads(response: DecideResponse) -> dict[str, str] | None:
112151
if "flags" not in response:
113152
return None
114153

115-
return {key: value.metadata.payload for key, value in response.get("flags", {}).items() if value.enabled}
154+
return {key: value.metadata.payload for key, value in response.get("flags", {}).items() if isinstance(value, FeatureFlag) and value.enabled}

0 commit comments

Comments
 (0)