Skip to content

Commit 5e0f9e3

Browse files
authored
feat(feature-flags): support quota limiting for feature flags (#195)
* haha okay * tests workin * format * use case-sensitive comparisons * omg LOL * fix tests * jeez * this will probably work * now do local eval * okay * yo * formatting * fix import order * type check * ai yi yi * code review * format
1 parent 337f7da commit 5e0f9e3

File tree

6 files changed

+87
-3
lines changed

6 files changed

+87
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.15.0 - 2025-02-19
2+
3+
1. Support quota-limited feature flags
4+
15
## 3.14.2 - 2025-02-19
26

37
1. Evaluate feature flag payloads with case sensitivity correctly. Fixes <https://github.com/PostHog/posthog-python/issues/178>

posthog/client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,19 @@ def _load_feature_flags(self):
600600
"To use feature flags, please set a personal_api_key "
601601
"More information: https://posthog.com/docs/api/overview",
602602
)
603+
elif e.status == 402:
604+
self.log.warning("[FEATURE FLAGS] PostHog feature flags quota limited")
605+
# Reset all feature flag data when quota limited
606+
self.feature_flags = []
607+
self.feature_flags_by_key = {}
608+
self.group_type_mapping = {}
609+
self.cohorts = {}
610+
611+
if self.debug:
612+
raise APIError(
613+
status=402,
614+
message="PostHog feature flags quota limited",
615+
)
603616
else:
604617
self.log.error(f"[FEATURE FLAGS] Error loading feature flags: {e}")
605618
except Exception as e:

posthog/request.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,19 @@ def _process_response(
6868
log = logging.getLogger("posthog")
6969
if res.status_code == 200:
7070
log.debug(success_message)
71-
return res.json() if return_json else res
71+
response = res.json() if return_json else res
72+
# Handle quota limited decide responses by raising a specific error
73+
# NB: other services also put entries into the quotaLimited key, but right now we only care about feature flags
74+
# since most of the other services handle quota limiting in other places in the application.
75+
if (
76+
isinstance(response, dict)
77+
and "quotaLimited" in response
78+
and isinstance(response["quotaLimited"], list)
79+
and "feature_flags" in response["quotaLimited"]
80+
):
81+
log.warning("PostHog feature flags quota limited")
82+
raise QuotaLimitError(res.status_code, "Feature flags quota limited")
83+
return response
7284
try:
7385
payload = res.json()
7486
log.debug("received response: %s", payload)
@@ -112,6 +124,10 @@ def __str__(self):
112124
return msg.format(self.message, self.status)
113125

114126

127+
class QuotaLimitError(APIError):
128+
pass
129+
130+
115131
class DatetimeSerializer(json.JSONEncoder):
116132
def default(self, obj: Any):
117133
if isinstance(obj, (date, datetime)):

posthog/test/test_client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import six
88

99
from posthog.client import Client
10+
from posthog.request import APIError
1011
from posthog.test.test_utils import FAKE_TEST_API_KEY
1112
from posthog.version import VERSION
1213

@@ -384,6 +385,25 @@ def test_basic_capture_with_locally_evaluated_feature_flags(self, patch_decide):
384385
assert "$feature/false-flag" not in msg["properties"]
385386
assert "$active_feature_flags" not in msg["properties"]
386387

388+
@mock.patch("posthog.client.get")
389+
def test_load_feature_flags_quota_limited(self, patch_get):
390+
mock_response = {
391+
"type": "quota_limited",
392+
"detail": "You have exceeded your feature flag request quota",
393+
"code": "payment_required",
394+
}
395+
patch_get.side_effect = APIError(402, mock_response["detail"])
396+
397+
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
398+
with self.assertLogs("posthog", level="WARNING") as logs:
399+
client._load_feature_flags()
400+
401+
self.assertEqual(client.feature_flags, [])
402+
self.assertEqual(client.feature_flags_by_key, {})
403+
self.assertEqual(client.group_type_mapping, {})
404+
self.assertEqual(client.cohorts, {})
405+
self.assertIn("PostHog feature flags quota limited", logs.output[0])
406+
387407
@mock.patch("posthog.client.decide")
388408
def test_dont_override_capture_with_local_flags(self, patch_decide):
389409
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}

posthog/test/test_request.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import unittest
33
from datetime import date, datetime
44

5+
import mock
56
import pytest
67
import requests
78

8-
from posthog.request import DatetimeSerializer, batch_post, determine_server_host
9+
from posthog.request import DatetimeSerializer, QuotaLimitError, batch_post, decide, determine_server_host
910
from posthog.test.test_utils import TEST_API_KEY
1011

1112

@@ -44,6 +45,36 @@ def test_should_timeout(self):
4445
"key", batch=[{"distinct_id": "distinct_id", "event": "python event", "type": "track"}], timeout=0.0001
4546
)
4647

48+
def test_quota_limited_response(self):
49+
mock_response = requests.Response()
50+
mock_response.status_code = 200
51+
mock_response._content = json.dumps(
52+
{
53+
"quotaLimited": ["feature_flags"],
54+
"featureFlags": {},
55+
"featureFlagPayloads": {},
56+
"errorsWhileComputingFlags": False,
57+
}
58+
).encode("utf-8")
59+
60+
with mock.patch("posthog.request._session.post", return_value=mock_response):
61+
with self.assertRaises(QuotaLimitError) as cm:
62+
decide("fake_key", "fake_host")
63+
64+
self.assertEqual(cm.exception.status, 200)
65+
self.assertEqual(cm.exception.message, "Feature flags quota limited")
66+
67+
def test_normal_decide_response(self):
68+
mock_response = requests.Response()
69+
mock_response.status_code = 200
70+
mock_response._content = json.dumps(
71+
{"featureFlags": {"flag1": True}, "featureFlagPayloads": {}, "errorsWhileComputingFlags": False}
72+
).encode("utf-8")
73+
74+
with mock.patch("posthog.request._session.post", return_value=mock_response):
75+
response = decide("fake_key", "fake_host")
76+
self.assertEqual(response["featureFlags"], {"flag1": True})
77+
4778

4879
@pytest.mark.parametrize(
4980
"host, expected",

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "3.14.2"
1+
VERSION = "3.15.0"
22

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

0 commit comments

Comments
 (0)