Skip to content

Commit 643a810

Browse files
authored
fix: no-op flag helpers on API errors (#639)
* fix: no-op flag helpers on API errors * fix: preserve flag error handling on flag API failures
1 parent 17952a6 commit 643a810

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: patch
3+
---
4+
5+
Return empty flag defaults from Client flag helpers when the flags API fails.

posthog/client.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,30 @@ def get_flags_decision(
611611
Category:
612612
Feature flags
613613
"""
614+
try:
615+
return self._get_flags_decision(
616+
distinct_id,
617+
groups,
618+
person_properties,
619+
group_properties,
620+
disable_geoip,
621+
flag_keys_to_evaluate,
622+
device_id=device_id,
623+
)
624+
except Exception as err:
625+
self.log.exception("Unable to get feature flags: %s", err)
626+
return normalize_flags_response({})
627+
628+
def _get_flags_decision(
629+
self,
630+
distinct_id: Optional[ID_TYPES] = None,
631+
groups: Optional[dict] = None,
632+
person_properties=None,
633+
group_properties=None,
634+
disable_geoip=None,
635+
flag_keys_to_evaluate: Optional[list[str]] = None,
636+
device_id: Optional[str] = None,
637+
) -> FlagsResponse:
614638
if self.disabled:
615639
return normalize_flags_response({})
616640

@@ -2175,7 +2199,7 @@ def _get_feature_flag_details_from_server(
21752199
Calls /flags and returns the flag details, request id, evaluated at timestamp,
21762200
and whether there were errors while computing flags.
21772201
"""
2178-
resp_data = self.get_flags_decision(
2202+
resp_data = self._get_flags_decision(
21792203
distinct_id,
21802204
groups,
21812205
person_properties,
@@ -2447,7 +2471,7 @@ def get_all_flags_and_payloads(
24472471

24482472
if fallback_to_flags and not only_evaluate_locally:
24492473
try:
2450-
decide_response = self.get_flags_decision(
2474+
decide_response = self._get_flags_decision(
24512475
distinct_id,
24522476
groups=groups,
24532477
person_properties=person_properties,
@@ -2584,11 +2608,11 @@ def evaluate_flags(
25842608
locally_evaluated_keys.add(key)
25852609

25862610
# Fall back to remote evaluation for any flags the poller couldn't resolve locally.
2587-
# Use ``get_flags_decision`` directly so the resulting records carry id/version/reason
2611+
# Use the flags decision path directly so the resulting records carry id/version/reason
25882612
# and fired ``$feature_flag_called`` events match what ``get_feature_flag()`` emits.
25892613
if fallback_to_server and not only_evaluate_locally:
25902614
try:
2591-
response = self.get_flags_decision(
2615+
response = self._get_flags_decision(
25922616
distinct_id,
25932617
groups=groups,
25942618
person_properties=person_properties,

posthog/test/test_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,38 @@ def test_disabled_client_does_not_get_flags_decision(self, patch_flags):
106106
)
107107
patch_flags.assert_not_called()
108108

109+
@mock.patch("posthog.client.flags")
110+
def test_client_flag_helpers_return_defaults_on_api_error(self, patch_flags):
111+
patch_flags.side_effect = APIError(401, "Unauthorized")
112+
client = Client(FAKE_TEST_API_KEY, send=False)
113+
114+
test_cases = [
115+
(
116+
"get_flags_decision",
117+
lambda: client.get_flags_decision("distinct_id")["flags"],
118+
{},
119+
),
120+
(
121+
"get_feature_variants",
122+
lambda: client.get_feature_variants("distinct_id"),
123+
{},
124+
),
125+
(
126+
"get_feature_payloads",
127+
lambda: client.get_feature_payloads("distinct_id"),
128+
{},
129+
),
130+
(
131+
"get_feature_flags_and_payloads",
132+
lambda: client.get_feature_flags_and_payloads("distinct_id"),
133+
{"featureFlags": {}, "featureFlagPayloads": {}},
134+
),
135+
]
136+
137+
for method_name, call_helper, expected in test_cases:
138+
with self.subTest(method=method_name):
139+
self.assertEqual(call_helper(), expected)
140+
109141
def test_empty_flush(self):
110142
self.client.flush()
111143

0 commit comments

Comments
 (0)