Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 9 additions & 3 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Configuration docs omit provider-only polling eligibility

The constructor and module configuration docs (posthog/client.py:793–796 and posthog/__init__.py:365–368) describe enable_local_evaluation polling when a personal API key is configured. This patch also permits loading with a provider alone and, by default, starts background refresh after loading (client.py:3482–3486,3515–3525).

The existing description is incomplete rather than wholly false. Provider-only clients previously returned before poller creation; the changed eligibility check makes this lifecycle distinction relevant.

Consider documenting that either privileged configuration or a cache provider permits definition loading, that provider refreshes consult its fetch decision, and that enable_local_evaluation=False suppresses background polling without preventing explicit or first-use hydration.

Verification is a static comparison of these descriptions against the eligibility predicate and poller-start condition. This documentation-only recommendation does not require an automated behavioral regression.

self.log.warning(
"[FEATURE FLAGS] You have to specify a secret_key to use feature flags."
)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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] = {}
Expand Down
50 changes: 50 additions & 0 deletions posthog/test/test_flag_definition_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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."""
Expand Down
Loading