From f53d58762f0f95f6c7bb12cc372c932e1a885032 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 25 Aug 2026 12:57:05 +0200 Subject: [PATCH 1/2] fix(flags): handle empty evaluation key scopes --- posthog/__init__.py | 12 +++++------ posthog/client.py | 22 +++++++++++++------ posthog/test/test_evaluate_flags.py | 33 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/posthog/__init__.py b/posthog/__init__.py index b423bd45..6096947d 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -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. diff --git a/posthog/client.py b/posthog/client.py index 2ddfd1fd..9f7968b1 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -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. @@ -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 {}, @@ -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] = {} diff --git a/posthog/test/test_evaluate_flags.py b/posthog/test/test_evaluate_flags.py index 8e3fe086..31d33266 100644 --- a/posthog/test/test_evaluate_flags.py +++ b/posthog/test/test_evaluate_flags.py @@ -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( From 0ac438834fb00c4fe829b143bd879f481c0e5c4b Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 25 Aug 2026 13:48:59 +0200 Subject: [PATCH 2/2] chore: add empty evaluation scope changeset --- .sampo/changesets/bold-firebringer-erika.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .sampo/changesets/bold-firebringer-erika.md diff --git a/.sampo/changesets/bold-firebringer-erika.md b/.sampo/changesets/bold-firebringer-erika.md new file mode 100644 index 00000000..93502509 --- /dev/null +++ b/.sampo/changesets/bold-firebringer-erika.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Return an empty feature flag snapshot without evaluation when feature flag keys are explicitly empty.