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
5 changes: 5 additions & 0 deletions .sampo/changesets/bold-firebringer-erika.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: patch
---

Return an empty feature flag snapshot without evaluation when feature flag keys are explicitly empty.
12 changes: 6 additions & 6 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,12 +1077,12 @@ def evaluate_flags(
only_evaluate_locally: If ``True``, never fall back to remote evaluation and
omit flags that cannot be evaluated locally.
disable_geoip: Whether to disable GeoIP lookup.
flag_keys: Optional non-empty list that scopes local evaluation, the underlying
``/flags`` request, and the returned snapshot. An empty list is treated like ``None``
and evaluates all flags. A requested key absent from loaded local definitions is
included in one remote fallback per ``evaluate_flags`` call unless
``only_evaluate_locally`` is ``True``. If the server also does not know the key, it is
omitted from the snapshot.
flag_keys: Optional list that scopes local evaluation, the underlying ``/flags``
request, and the returned snapshot. When omitted or ``None``, all flags are evaluated.
An empty list returns an empty snapshot without evaluating flags. A requested key
absent from loaded local definitions is included in one remote fallback per
``evaluate_flags`` call unless ``only_evaluate_locally`` is ``True``. If the server
also does not know the key, it is omitted from the snapshot.
device_id: Optional device ID override. If not provided, falls back to the
context device_id (which may be set via tracing headers). Used by
experience-continuity flags to match users across distinct_id changes.
Expand Down
22 changes: 15 additions & 7 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4044,12 +4044,12 @@ def evaluate_flags(
only_evaluate_locally: If True, never fall back to remote evaluation —
flags that can't be evaluated locally are simply omitted from the snapshot.
disable_geoip: Whether to disable GeoIP lookup.
flag_keys: Optional non-empty list that scopes local evaluation, the underlying
``/flags`` request, and the returned snapshot. An empty list is treated like
``None`` and evaluates all flags. A requested key absent from loaded local
definitions is included in one remote fallback per ``evaluate_flags`` call unless
``only_evaluate_locally`` is True. If the server also does not know the key, it is
omitted from the snapshot.
flag_keys: Optional list that scopes local evaluation, the underlying
``/flags`` request, and the returned snapshot. When omitted or ``None``, all
flags are evaluated. An empty list returns an empty snapshot without evaluating
flags. A requested key absent from loaded local definitions is included in one
remote fallback per ``evaluate_flags`` call unless ``only_evaluate_locally`` is
True. If the server also does not know the key, it is omitted from the snapshot.
device_id: Optional device ID override. If not provided, falls back to the
context device_id (which may be set via tracing headers). Used by
experience-continuity flags to match users across distinct_id changes.
Expand Down Expand Up @@ -4088,6 +4088,15 @@ def evaluate_flags(
# is_enabled()/get_flag() on it won't emit events.
return FeatureFlagEvaluations(host=host, distinct_id="", flags={})

if flag_keys == []:
return FeatureFlagEvaluations(
host=host,
distinct_id=str(distinct_id),
flags={},
groups=groups,
disable_geoip=disable_geoip,
)

person_properties, group_properties = (
self._add_local_person_and_group_properties(
groups or {},
Expand All @@ -4096,7 +4105,6 @@ def evaluate_flags(
)
)
groups = groups or {}
# Keep the existing API convention that an empty list means no scope.
requested_keys = set(flag_keys) if flag_keys else None

records: Dict[str, _EvaluatedFlagRecord] = {}
Expand Down
33 changes: 33 additions & 0 deletions posthog/test/test_evaluate_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,39 @@ def test_returns_a_FeatureFlagEvaluations_instance(self, patch_flags):
self.assertIsInstance(flags, FeatureFlagEvaluations)
self.assertEqual(patch_flags.call_count, 1)

def test_empty_flag_keys_returns_empty_without_evaluation_work(self):
self.client.flag_cache = mock.Mock()

with (
mock.patch.object(
self.client, "_add_local_person_and_group_properties"
) as add_local_properties,
mock.patch.object(
self.client, "_person_properties_for_local_evaluation"
) as local_person_properties,
mock.patch.object(
self.client, "_get_all_flags_and_payloads_locally"
) as local_evaluation,
mock.patch.object(self.client, "load_feature_flags") as load_definitions,
mock.patch.object(self.client, "_get_flags_decision") as remote_evaluation,
):
flags = self.client.evaluate_flags(
"user-1",
groups={"organization": "org-1"},
person_properties={"plan": "enterprise"},
flag_keys=[],
)

self.assertIsInstance(flags, FeatureFlagEvaluations)
self.assertEqual(flags.keys, [])
self.assertEqual(flags._get_event_properties(), {})
self.assertEqual(self.client.flag_cache.mock_calls, [])
add_local_properties.assert_not_called()
local_person_properties.assert_not_called()
local_evaluation.assert_not_called()
load_definitions.assert_not_called()
remote_evaluation.assert_not_called()

@mock.patch("posthog.client.flags")
@mock.patch.object(Client, "capture")
def test_does_not_fire_events_for_unaccessed_flags(
Expand Down