Skip to content

Commit f5a95b4

Browse files
chore: update endpoint for flags local eval (#509)
* chore: update endpoint for flags local eval * remove imports
1 parent 055f8fd commit f5a95b4

3 files changed

Lines changed: 12 additions & 119 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+
feat(flags): switch local evaluation polling from `/api/feature_flag/local_evaluation` to `/flags/definitions`

posthog/client.py

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,55 +1287,19 @@ def _load_feature_flags(self):
12871287
if should_fetch:
12881288
self._fetch_feature_flags_from_api()
12891289

1290-
# Default (Django) endpoint for local evaluation
1291-
_DEFAULT_LOCAL_EVAL_ENDPOINT = "/api/feature_flag/local_evaluation/"
1292-
1293-
def _get_local_eval_endpoint(self):
1294-
"""Get the local evaluation endpoint URL, configurable via env var."""
1295-
return os.environ.get(
1296-
"POSTHOG_LOCAL_EVALUATION_ENDPOINT",
1297-
self._DEFAULT_LOCAL_EVAL_ENDPOINT,
1298-
)
1299-
13001290
def _fetch_feature_flags_from_api(self):
13011291
"""Fetch feature flags from the PostHog API."""
13021292
try:
13031293
# Store old flags to detect changes
13041294
old_flags_by_key: dict[str, dict] = self.feature_flags_by_key or {}
13051295

1306-
endpoint = self._get_local_eval_endpoint()
1307-
url = f"{endpoint}?token={self.api_key}&send_cohorts"
1308-
# Ensure URL has leading slash
1309-
if not url.startswith("/"):
1310-
url = f"/{url}"
1311-
1312-
try:
1313-
response = get(
1314-
self.personal_api_key,
1315-
url,
1316-
self.host,
1317-
timeout=10,
1318-
etag=self._flags_etag,
1319-
)
1320-
except Exception as e:
1321-
# Fall back to the stable Django endpoint when the custom endpoint
1322-
# (e.g. the Rust-backed /flags/definitions) fails. This enables a
1323-
# zero-downtime gradual migration: the custom endpoint is tried first
1324-
# and, on any error, flag evaluation degrades transparently to the
1325-
# default rather than being blocked entirely.
1326-
if endpoint != self._DEFAULT_LOCAL_EVAL_ENDPOINT:
1327-
self.log.warning(
1328-
f"[FEATURE FLAGS] Custom endpoint {endpoint} failed ({e}), falling back to {self._DEFAULT_LOCAL_EVAL_ENDPOINT}"
1329-
)
1330-
response = get(
1331-
self.personal_api_key,
1332-
f"{self._DEFAULT_LOCAL_EVAL_ENDPOINT}?token={self.api_key}&send_cohorts",
1333-
self.host,
1334-
timeout=10,
1335-
etag=self._flags_etag,
1336-
)
1337-
else:
1338-
raise
1296+
response = get(
1297+
self.personal_api_key,
1298+
f"/flags/definitions?token={self.api_key}&send_cohorts",
1299+
self.host,
1300+
timeout=10,
1301+
etag=self._flags_etag,
1302+
)
13391303

13401304
# Update stored ETag (clear if server stops sending one)
13411305
self._flags_etag = response.etag

posthog/test/test_feature_flags.py

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import datetime
2-
import os
32
import unittest
43

54
import mock
65
from dateutil import parser, tz
76
from freezegun import freeze_time
8-
from parameterized import parameterized
97

108
from posthog.client import Client
119
from posthog.feature_flags import (
@@ -3782,80 +3780,6 @@ def test_get_all_flags_fallback_when_device_id_missing_for_some_flags(
37823780
self.assertEqual(patch_flags.call_count, 1)
37833781

37843782

3785-
class TestLocalEvalEndpointConfig(unittest.TestCase):
3786-
@classmethod
3787-
def setUpClass(cls):
3788-
cls.capture_patch = mock.patch.object(Client, "capture")
3789-
cls.capture_patch.start()
3790-
3791-
@classmethod
3792-
def tearDownClass(cls):
3793-
cls.capture_patch.stop()
3794-
3795-
@parameterized.expand(
3796-
[
3797-
("custom_endpoint", "/flags/definitions", "/flags/definitions?"),
3798-
("default_endpoint", None, "/api/feature_flag/local_evaluation/"),
3799-
]
3800-
)
3801-
@mock.patch("posthog.client.get")
3802-
def test_endpoint_selection(self, _name, env_value, expected_prefix, patch_get):
3803-
patch_get.return_value = GetResponse(
3804-
data={"flags": [], "group_type_mapping": {}},
3805-
etag=None,
3806-
not_modified=False,
3807-
)
3808-
env = {"POSTHOG_LOCAL_EVALUATION_ENDPOINT": env_value} if env_value else {}
3809-
with mock.patch.dict("os.environ", env, clear=False):
3810-
if env_value is None:
3811-
os.environ.pop("POSTHOG_LOCAL_EVALUATION_ENDPOINT", None)
3812-
client = Client(FAKE_TEST_API_KEY, personal_api_key="test-key")
3813-
client._fetch_feature_flags_from_api()
3814-
call_url = patch_get.call_args[0][1]
3815-
self.assertTrue(
3816-
call_url.startswith(expected_prefix),
3817-
f"Expected URL starting with {expected_prefix}, got: {call_url}",
3818-
)
3819-
3820-
@parameterized.expand(
3821-
[
3822-
("custom_endpoint_falls_back", "/flags/definitions", 2),
3823-
("default_endpoint_no_fallback", None, 1),
3824-
]
3825-
)
3826-
@mock.patch("posthog.client.get")
3827-
def test_endpoint_fallback_on_failure(
3828-
self, _name, env_value, expected_call_count, patch_get
3829-
):
3830-
success_response = GetResponse(
3831-
data={"flags": [], "group_type_mapping": {}},
3832-
etag=None,
3833-
not_modified=False,
3834-
)
3835-
if expected_call_count == 2:
3836-
patch_get.side_effect = [Exception("connection refused"), success_response]
3837-
else:
3838-
patch_get.side_effect = Exception("connection refused")
3839-
3840-
env = {"POSTHOG_LOCAL_EVALUATION_ENDPOINT": env_value} if env_value else {}
3841-
with mock.patch.dict("os.environ", env, clear=False):
3842-
if env_value is None:
3843-
os.environ.pop("POSTHOG_LOCAL_EVALUATION_ENDPOINT", None)
3844-
client = Client(FAKE_TEST_API_KEY, personal_api_key="test-key")
3845-
client._fetch_feature_flags_from_api()
3846-
self.assertEqual(patch_get.call_count, expected_call_count)
3847-
if expected_call_count == 2:
3848-
# First call used custom endpoint, second fell back to default
3849-
self.assertTrue(
3850-
patch_get.call_args_list[0][0][1].startswith("/flags/definitions?")
3851-
)
3852-
self.assertTrue(
3853-
patch_get.call_args_list[1][0][1].startswith(
3854-
"/api/feature_flag/local_evaluation/"
3855-
)
3856-
)
3857-
3858-
38593783
class TestMatchProperties(unittest.TestCase):
38603784
def property(self, key, value, operator=None):
38613785
result = {"key": key, "value": value}

0 commit comments

Comments
 (0)