Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 6.1.0 - 2025-07-10

- feat: decouple feature flag local evaluation from personal API keys; support decrypting remote config payloads without relying on the feature flags poller

# 6.0.4 - 2025-07-09

- fix: add POSTHOG_MW_CLIENT setting to django middleware, to support custom clients for exception capture.
Expand Down
4 changes: 4 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ def tag(name: str, value: Any):
project_root = None # type: Optional[str]
# Used for our AI observability feature to not capture any prompt or output just usage + metadata
privacy_mode = False # type: bool
# Whether to enable feature flag polling for local evaluation by default. Defaults to True.
# 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.
enable_local_evaluation = True # type: bool
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

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

style: default bool settings should follow the pattern DEFAULT_ENABLE_LOCAL_EVALUATION = True to match Python conventions. Current mutable module-level variable can be modified accidentally

Suggested change
# Whether to enable feature flag polling for local evaluation by default. Defaults to True.
# 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.
enable_local_evaluation = True # type: bool
# Whether to enable feature flag polling for local evaluation by default. Defaults to True.
# 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.
DEFAULT_ENABLE_LOCAL_EVALUATION = True # type: bool

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nope wrong; this is a config entry you IDIOT


default_client = None # type: Optional[Client]

Expand Down Expand Up @@ -465,6 +468,7 @@ def setup():
# 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)
enable_exception_autocapture=enable_exception_autocapture,
log_captured_exceptions=log_captured_exceptions,
enable_local_evaluation=enable_local_evaluation,
)

# always set incase user changes it
Expand Down
8 changes: 7 additions & 1 deletion posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def __init__(
privacy_mode=False,
before_send=None,
flag_fallback_cache_url=None,
enable_local_evaluation=True,
):
self.queue = queue.Queue(max_queue_size)

Expand Down Expand Up @@ -187,6 +188,7 @@ def __init__(
self.log_captured_exceptions = log_captured_exceptions
self.exception_capture = None
self.privacy_mode = privacy_mode
self.enable_local_evaluation = enable_local_evaluation

if project_root is None:
try:
Expand Down Expand Up @@ -807,7 +809,11 @@ def load_feature_flags(self):
return

self._load_feature_flags()
if not (self.poller and self.poller.is_alive()):

# Only start the poller if local evaluation is enabled
if self.enable_local_evaluation and not (
self.poller and self.poller.is_alive()
):
self.poller = Poller(
interval=timedelta(seconds=self.poll_interval),
execute=self._load_feature_flags,
Expand Down
90 changes: 90 additions & 0 deletions posthog/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1903,3 +1903,93 @@ def test_set_context_session_override_in_capture(self):
self.assertEqual(
msg["properties"]["$session_id"], "explicit-session-override"
)

@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_enable_local_evaluation_false_disables_poller(
self, patch_get, patch_poller
):
"""Test that when enable_local_evaluation=False, the poller is not started"""
patch_get.return_value = {
"flags": [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
],
"group_type_mapping": {},
"cohorts": {},
}

client = Client(
FAKE_TEST_API_KEY,
personal_api_key="test-personal-key",
enable_local_evaluation=False,
)

# Load feature flags should not start the poller
client.load_feature_flags()

# Assert that the poller was not created/started
patch_poller.assert_not_called()
# But the feature flags should still be loaded
patch_get.assert_called_once()
self.assertEqual(len(client.feature_flags), 1)
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")

@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_enable_local_evaluation_true_starts_poller(self, patch_get, patch_poller):
"""Test that when enable_local_evaluation=True (default), the poller is started"""
patch_get.return_value = {
"flags": [
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
],
"group_type_mapping": {},
"cohorts": {},
}

client = Client(
FAKE_TEST_API_KEY,
personal_api_key="test-personal-key",
enable_local_evaluation=True,
)

# Load feature flags should start the poller
client.load_feature_flags()

# Assert that the poller was created and started
patch_poller.assert_called_once()
patch_get.assert_called_once()
self.assertEqual(len(client.feature_flags), 1)
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")

@mock.patch("posthog.client.remote_config")
def test_get_remote_config_payload_works_without_poller(self, patch_remote_config):
"""Test that get_remote_config_payload works without local evaluation enabled"""
patch_remote_config.return_value = {"test": "payload"}

client = Client(
FAKE_TEST_API_KEY,
personal_api_key="test-personal-key",
enable_local_evaluation=False,
)

# Should work without poller
result = client.get_remote_config_payload("test-flag")

self.assertEqual(result, {"test": "payload"})
patch_remote_config.assert_called_once_with(
"test-personal-key",
client.host,
"test-flag",
timeout=client.feature_flags_request_timeout_seconds,
)

def test_get_remote_config_payload_requires_personal_api_key(self):
"""Test that get_remote_config_payload requires personal API key"""
client = Client(
FAKE_TEST_API_KEY,
enable_local_evaluation=False,
)

result = client.get_remote_config_payload("test-flag")

self.assertIsNone(result)
2 changes: 1 addition & 1 deletion posthog/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = "6.0.4"
VERSION = "6.1.0"

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