From 4818f239650400f37a70c740eebeaf0408fbb4dd Mon Sep 17 00:00:00 2001 From: Frank Hamand Date: Thu, 8 Oct 2026 14:08:48 +0100 Subject: [PATCH] fix(flags): load definitions from a cache provider without a secret key Generated-By: PostHog Desktop Task-Id: 380b019d-762a-419d-a407-c985b2876490 --- ...d-flags-from-cache-provider-without-key.md | 5 ++ posthog/client.py | 12 +++-- posthog/test/test_flag_definition_cache.py | 50 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 .sampo/changesets/load-flags-from-cache-provider-without-key.md diff --git a/.sampo/changesets/load-flags-from-cache-provider-without-key.md b/.sampo/changesets/load-flags-from-cache-provider-without-key.md new file mode 100644 index 000000000..0909cc367 --- /dev/null +++ b/.sampo/changesets/load-flags-from-cache-provider-without-key.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Load feature flag definitions from a `flag_definition_cache_provider` when no `secret_key` is set, so local evaluation works with a cache provider alone. diff --git a/posthog/client.py b/posthog/client.py index d74ea56e3..7c4fb029d 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -3479,6 +3479,12 @@ def _fetch_feature_flags_from_api(self): self._last_feature_flag_poll = datetime.now(tz=timezone.utc) + def _can_load_feature_flags(self) -> bool: + return ( + bool(self.personal_api_key) + or self._flag_definition_cache_provider is not None + ) + def load_feature_flags(self): """ Load feature flags for local evaluation. @@ -3497,7 +3503,7 @@ def load_feature_flags(self): self._property_matching_version = 1 return - if not self.personal_api_key: + if not self._can_load_feature_flags(): self.log.warning( "[FEATURE FLAGS] You have to specify a secret_key to use feature flags." ) @@ -4007,7 +4013,7 @@ def _locally_evaluate_flag( device_id: Optional[str] = None, ) -> tuple[Optional[FlagValue], _LocalEvaluationSnapshot]: """Return the local value and its definitions for consistent payload lookup.""" - if self.feature_flags is None and self.personal_api_key: + if self.feature_flags is None and self._can_load_feature_flags(): self.load_feature_flags() response = None @@ -4734,7 +4740,7 @@ def _get_all_flags_and_payloads_locally( person_properties = person_properties or {} group_properties = group_properties or {} - if self.feature_flags is None and self.personal_api_key: + if self.feature_flags is None and self._can_load_feature_flags(): self.load_feature_flags() flags: dict[str, FlagValue] = {} diff --git a/posthog/test/test_flag_definition_cache.py b/posthog/test/test_flag_definition_cache.py index 2d05997d4..a689a7b52 100644 --- a/posthog/test/test_flag_definition_cache.py +++ b/posthog/test/test_flag_definition_cache.py @@ -10,6 +10,8 @@ from typing import Optional from unittest import mock +from parameterized import parameterized + from posthog.client import Client from posthog.flag_definition_cache import ( FlagDefinitionCacheData, @@ -155,6 +157,54 @@ def test_uses_cached_data_when_should_fetch_returns_false(self, mock_get): client.join() + @parameterized.expand( + [ + ("explicit_load", lambda client: client.load_feature_flags()), + ( + "lazy_load_on_first_evaluation", + lambda client: client.get_feature_flag( + "rolled-out-flag", "some-user", only_evaluate_locally=True + ), + ), + ] + ) + @mock.patch("posthog.client.get") + def test_loads_from_cache_provider_without_secret_key(self, _name, load, mock_get): + self.cache_provider.should_fetch_return_value = False + self.cache_provider.stored_data = { + "flags": [ + { + "key": "rolled-out-flag", + "active": True, + "filters": { + "groups": [{"properties": [], "rollout_percentage": 100}] + }, + } + ], + "group_type_mapping": {}, + "cohorts": {}, + } + client = Client( + FAKE_TEST_API_KEY, + flag_definition_cache_provider=self.cache_provider, + sync_mode=True, + enable_local_evaluation=False, + ) + + load(client) + + mock_get.assert_not_called() + self.assertEqual( + [flag["key"] for flag in client.feature_flags], ["rolled-out-flag"] + ) + self.assertTrue( + client.get_feature_flag( + "rolled-out-flag", "some-user", only_evaluate_locally=True + ) + ) + + client.join() + @mock.patch("posthog.client.get") def test_fetches_from_api_when_should_fetch_returns_true(self, mock_get): """When should_fetch returns True, fetch from API."""