Skip to content

Commit c4e09cd

Browse files
authored
feat(flags): decouple local evaluation from personal API keys; support decrypting remote config payloads without relying on the feature flags poller (#282)
1 parent c61236b commit c4e09cd

File tree

5 files changed

+106
-2
lines changed

5 files changed

+106
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 6.1.0 - 2025-07-10
2+
3+
- feat: decouple feature flag local evaluation from personal API keys; support decrypting remote config payloads without relying on the feature flags poller
4+
15
# 6.0.4 - 2025-07-09
26

37
- fix: add POSTHOG_MW_CLIENT setting to django middleware, to support custom clients for exception capture.

posthog/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def tag(name: str, value: Any):
6060
project_root = None # type: Optional[str]
6161
# Used for our AI observability feature to not capture any prompt or output just usage + metadata
6262
privacy_mode = False # type: bool
63+
# Whether to enable feature flag polling for local evaluation by default. Defaults to True.
64+
# We recommend setting this to False if you are only using the personalApiKey for evaluating remote config payloads via `get_remote_config_payload` and not using local evaluation.
65+
enable_local_evaluation = True # type: bool
6366

6467
default_client = None # type: Optional[Client]
6568

@@ -465,6 +468,7 @@ def setup():
465468
# or deprecate this proxy option fully (it's already in the process of deprecation, no new clients should be using this method since like 5-6 months)
466469
enable_exception_autocapture=enable_exception_autocapture,
467470
log_captured_exceptions=log_captured_exceptions,
471+
enable_local_evaluation=enable_local_evaluation,
468472
)
469473

470474
# always set incase user changes it

posthog/client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def __init__(
152152
privacy_mode=False,
153153
before_send=None,
154154
flag_fallback_cache_url=None,
155+
enable_local_evaluation=True,
155156
):
156157
self.queue = queue.Queue(max_queue_size)
157158

@@ -187,6 +188,7 @@ def __init__(
187188
self.log_captured_exceptions = log_captured_exceptions
188189
self.exception_capture = None
189190
self.privacy_mode = privacy_mode
191+
self.enable_local_evaluation = enable_local_evaluation
190192

191193
if project_root is None:
192194
try:
@@ -807,7 +809,11 @@ def load_feature_flags(self):
807809
return
808810

809811
self._load_feature_flags()
810-
if not (self.poller and self.poller.is_alive()):
812+
813+
# Only start the poller if local evaluation is enabled
814+
if self.enable_local_evaluation and not (
815+
self.poller and self.poller.is_alive()
816+
):
811817
self.poller = Poller(
812818
interval=timedelta(seconds=self.poll_interval),
813819
execute=self._load_feature_flags,

posthog/test/test_client.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1903,3 +1903,93 @@ def test_set_context_session_override_in_capture(self):
19031903
self.assertEqual(
19041904
msg["properties"]["$session_id"], "explicit-session-override"
19051905
)
1906+
1907+
@mock.patch("posthog.client.Poller")
1908+
@mock.patch("posthog.client.get")
1909+
def test_enable_local_evaluation_false_disables_poller(
1910+
self, patch_get, patch_poller
1911+
):
1912+
"""Test that when enable_local_evaluation=False, the poller is not started"""
1913+
patch_get.return_value = {
1914+
"flags": [
1915+
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
1916+
],
1917+
"group_type_mapping": {},
1918+
"cohorts": {},
1919+
}
1920+
1921+
client = Client(
1922+
FAKE_TEST_API_KEY,
1923+
personal_api_key="test-personal-key",
1924+
enable_local_evaluation=False,
1925+
)
1926+
1927+
# Load feature flags should not start the poller
1928+
client.load_feature_flags()
1929+
1930+
# Assert that the poller was not created/started
1931+
patch_poller.assert_not_called()
1932+
# But the feature flags should still be loaded
1933+
patch_get.assert_called_once()
1934+
self.assertEqual(len(client.feature_flags), 1)
1935+
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
1936+
1937+
@mock.patch("posthog.client.Poller")
1938+
@mock.patch("posthog.client.get")
1939+
def test_enable_local_evaluation_true_starts_poller(self, patch_get, patch_poller):
1940+
"""Test that when enable_local_evaluation=True (default), the poller is started"""
1941+
patch_get.return_value = {
1942+
"flags": [
1943+
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
1944+
],
1945+
"group_type_mapping": {},
1946+
"cohorts": {},
1947+
}
1948+
1949+
client = Client(
1950+
FAKE_TEST_API_KEY,
1951+
personal_api_key="test-personal-key",
1952+
enable_local_evaluation=True,
1953+
)
1954+
1955+
# Load feature flags should start the poller
1956+
client.load_feature_flags()
1957+
1958+
# Assert that the poller was created and started
1959+
patch_poller.assert_called_once()
1960+
patch_get.assert_called_once()
1961+
self.assertEqual(len(client.feature_flags), 1)
1962+
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
1963+
1964+
@mock.patch("posthog.client.remote_config")
1965+
def test_get_remote_config_payload_works_without_poller(self, patch_remote_config):
1966+
"""Test that get_remote_config_payload works without local evaluation enabled"""
1967+
patch_remote_config.return_value = {"test": "payload"}
1968+
1969+
client = Client(
1970+
FAKE_TEST_API_KEY,
1971+
personal_api_key="test-personal-key",
1972+
enable_local_evaluation=False,
1973+
)
1974+
1975+
# Should work without poller
1976+
result = client.get_remote_config_payload("test-flag")
1977+
1978+
self.assertEqual(result, {"test": "payload"})
1979+
patch_remote_config.assert_called_once_with(
1980+
"test-personal-key",
1981+
client.host,
1982+
"test-flag",
1983+
timeout=client.feature_flags_request_timeout_seconds,
1984+
)
1985+
1986+
def test_get_remote_config_payload_requires_personal_api_key(self):
1987+
"""Test that get_remote_config_payload requires personal API key"""
1988+
client = Client(
1989+
FAKE_TEST_API_KEY,
1990+
enable_local_evaluation=False,
1991+
)
1992+
1993+
result = client.get_remote_config_payload("test-flag")
1994+
1995+
self.assertIsNone(result)

posthog/version.py

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

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

0 commit comments

Comments
 (0)