From e18951f48addf7dc81de072920ddf8e8b4ac3e7c Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:34:53 +0000 Subject: [PATCH 1/2] feat(flags): expose a flag's evaluation runtime The local-evaluation payload already carries `evaluation_runtime` per flag, but only on the untyped `client.feature_flags` dicts. There was no typed accessor, no filter and no documentation, so a backend that serves flags to its own frontend had to call `/api/feature_flag/local_evaluation` directly to read the value. Adds `FeatureFlagEvaluationRuntime` and two read methods on the client, both served from the definitions local evaluation already holds: - `get_feature_flag_evaluation_runtime(key)` - `get_feature_flag_keys_by_evaluation_runtime(runtime)` A definition with no runtime reports `ALL`, the default PostHog applies, and a flag set to `ALL` matches both the client and the server runtime. Generated-By: PostHog Desktop Task-Id: f3696295-459a-4066-bb52-5eea7ee16f37 --- .../feature-flag-evaluation-runtime.md | 5 + posthog/__init__.py | 49 ++++++++ posthog/client.py | 76 ++++++++++++ posthog/test/test_feature_flags.py | 111 ++++++++++++++++++ posthog/types.py | 36 ++++++ references/public_api_snapshot.txt | 12 ++ 6 files changed, 289 insertions(+) create mode 100644 .sampo/changesets/feature-flag-evaluation-runtime.md diff --git a/.sampo/changesets/feature-flag-evaluation-runtime.md b/.sampo/changesets/feature-flag-evaluation-runtime.md new file mode 100644 index 000000000..79f5ccbe4 --- /dev/null +++ b/.sampo/changesets/feature-flag-evaluation-runtime.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +Read a feature flag's evaluation runtime from the SDK: `get_feature_flag_evaluation_runtime(key)` returns the `FeatureFlagEvaluationRuntime` on a locally loaded flag definition, and `get_feature_flag_keys_by_evaluation_runtime(runtime)` lists the keys a given runtime can evaluate. The local-evaluation payload already carried the value, but only on the untyped `client.feature_flags` dicts, so a backend that serves flags to its own frontend had to call `/api/feature_flag/local_evaluation` itself to see it. diff --git a/posthog/__init__.py b/posthog/__init__.py index 092fb00b2..5d6ae1193 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -80,6 +80,7 @@ from posthog.types import ( BeforeSendCallback as BeforeSendCallback, FeatureFlag as FeatureFlag, + FeatureFlagEvaluationRuntime as FeatureFlagEvaluationRuntime, FlagValue as FlagValue, FlagsAndPayloads as FlagsAndPayloads, ) @@ -1157,6 +1158,54 @@ def feature_flag_definitions(): return _proxy("feature_flag_definitions") +def get_feature_flag_evaluation_runtime( + key: str, +) -> Optional[FeatureFlagEvaluationRuntime]: + """ + Return where a locally loaded feature flag is meant to be evaluated. + + Details: + Reads the `evaluation_runtime` each flag definition carries, so no extra + request is made. Returns `None` when local evaluation has not loaded a + definition for this key. A definition that carries no runtime reports + `FeatureFlagEvaluationRuntime.ALL`, the default PostHog applies. + + Examples: + ```python + from posthog import FeatureFlagEvaluationRuntime, get_feature_flag_evaluation_runtime + runtime = get_feature_flag_evaluation_runtime("my-flag") + ``` + + Category: + Feature flags + """ + return _proxy("get_feature_flag_evaluation_runtime", key) + + +def get_feature_flag_keys_by_evaluation_runtime( + evaluation_runtime: Union[FeatureFlagEvaluationRuntime, str], +) -> list[str]: + """ + Return the keys of locally loaded flags that a runtime can evaluate. + + Details: + A flag set to `FeatureFlagEvaluationRuntime.ALL` suits either runtime, so + it is returned for `CLIENT` and for `SERVER`. Use this to decide which + flags to hand to a browser when a backend serves flags to its own + frontend. + + Examples: + ```python + from posthog import FeatureFlagEvaluationRuntime, get_feature_flag_keys_by_evaluation_runtime + client_keys = get_feature_flag_keys_by_evaluation_runtime(FeatureFlagEvaluationRuntime.CLIENT) + ``` + + Category: + Feature flags + """ + return _proxy("get_feature_flag_keys_by_evaluation_runtime", evaluation_runtime) + + def load_feature_flags(): """ Load feature flag definitions from PostHog. diff --git a/posthog/client.py b/posthog/client.py index 857bd743e..c73266f7d 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -103,6 +103,7 @@ from posthog.types import ( FeatureFlag, FeatureFlagError, + FeatureFlagEvaluationRuntime, FeatureFlagResult, FlagMetadata, FlagsAndPayloads, @@ -4704,6 +4705,81 @@ def feature_flag_definitions(self): """ return self.feature_flags + def get_feature_flag_evaluation_runtime( + self, key: str + ) -> Optional[FeatureFlagEvaluationRuntime]: + """ + Return where a locally loaded feature flag is meant to be evaluated. + + Args: + key: The feature flag key. + + Returns: + The flag's evaluation runtime, or ``None`` when local evaluation has + not loaded a definition for this key. A definition that carries no + runtime reports ``FeatureFlagEvaluationRuntime.ALL``, the default + PostHog applies. + + Examples: + ```python + from posthog import FeatureFlagEvaluationRuntime + + runtime = posthog.get_feature_flag_evaluation_runtime("my-flag") + if runtime is FeatureFlagEvaluationRuntime.SERVER: + ... + ``` + + Category: + Feature flags + """ + definition = (self.feature_flags_by_key or {}).get(key) + if definition is None: + return None + return FeatureFlagEvaluationRuntime.from_value( + definition.get("evaluation_runtime") + ) + + def get_feature_flag_keys_by_evaluation_runtime( + self, evaluation_runtime: Union[FeatureFlagEvaluationRuntime, str] + ) -> list[str]: + """ + Return the keys of locally loaded flags that a runtime can evaluate. + + A flag set to ``FeatureFlagEvaluationRuntime.ALL`` suits either runtime, + so it is returned for ``CLIENT`` and for ``SERVER``, and asking for + ``ALL`` returns every loaded flag. Use this to decide which flags to hand + to a browser when a backend serves flags to its own frontend. + + Args: + evaluation_runtime: The runtime to match, as a + ``FeatureFlagEvaluationRuntime`` or its string value. + + Returns: + The matching flag keys, in the order local evaluation loaded them. + Empty when no definitions are loaded. + + Examples: + ```python + from posthog import FeatureFlagEvaluationRuntime + + client_keys = posthog.get_feature_flag_keys_by_evaluation_runtime( + FeatureFlagEvaluationRuntime.CLIENT + ) + ``` + + Category: + Feature flags + """ + wanted = FeatureFlagEvaluationRuntime(evaluation_runtime) + return [ + definition["key"] + for definition in (self.feature_flags or []) + if definition.get("key") is not None + and FeatureFlagEvaluationRuntime.from_value( + definition.get("evaluation_runtime") + ).matches(wanted) + ] + def _person_properties_for_local_evaluation(self, distinct_id, person_properties): local_person_properties = dict(person_properties or {}) local_person_properties.setdefault("distinct_id", distinct_id) diff --git a/posthog/test/test_feature_flags.py b/posthog/test/test_feature_flags.py index 107f9a930..0241f6849 100644 --- a/posthog/test/test_feature_flags.py +++ b/posthog/test/test_feature_flags.py @@ -24,6 +24,7 @@ relative_date_parse_for_feature_flag_matching, ) from posthog.request import APIError, GetResponse +from posthog.types import FeatureFlagEvaluationRuntime from posthog.test.test_utils import FAKE_TEST_API_KEY from posthog.utils import FlagCache @@ -5052,6 +5053,116 @@ def test_get_all_flags_fallback_when_device_id_missing_for_some_flags( self.assertEqual(patch_flags.call_count, 1) +class TestEvaluationRuntime(unittest.TestCase): + def setUp(self): + self.client = Client(FAKE_TEST_API_KEY) + self.client.feature_flags = [ + {"id": 1, "key": "everywhere-flag", "evaluation_runtime": "all"}, + {"id": 2, "key": "browser-flag", "evaluation_runtime": "client"}, + {"id": 3, "key": "backend-flag", "evaluation_runtime": "server"}, + {"id": 4, "key": "unset-flag", "evaluation_runtime": None}, + {"id": 5, "key": "legacy-flag"}, + ] + + @parameterized.expand( + [ + ("everywhere-flag", FeatureFlagEvaluationRuntime.ALL), + ("browser-flag", FeatureFlagEvaluationRuntime.CLIENT), + ("backend-flag", FeatureFlagEvaluationRuntime.SERVER), + # A definition without a runtime reports the default PostHog applies. + ("unset-flag", FeatureFlagEvaluationRuntime.ALL), + ("legacy-flag", FeatureFlagEvaluationRuntime.ALL), + ] + ) + def test_runtime_of_loaded_flag(self, key, expected): + self.assertEqual(self.client.get_feature_flag_evaluation_runtime(key), expected) + + def test_runtime_of_unknown_flag_is_none(self): + self.assertIsNone( + self.client.get_feature_flag_evaluation_runtime("no-such-flag") + ) + + def test_runtime_before_definitions_are_loaded_is_none(self): + client = Client(FAKE_TEST_API_KEY) + self.assertIsNone(client.get_feature_flag_evaluation_runtime("browser-flag")) + self.assertEqual( + client.get_feature_flag_keys_by_evaluation_runtime("client"), [] + ) + + @parameterized.expand( + [ + ( + FeatureFlagEvaluationRuntime.CLIENT, + ["everywhere-flag", "browser-flag", "unset-flag", "legacy-flag"], + ), + ( + FeatureFlagEvaluationRuntime.SERVER, + ["everywhere-flag", "backend-flag", "unset-flag", "legacy-flag"], + ), + ( + FeatureFlagEvaluationRuntime.ALL, + [ + "everywhere-flag", + "browser-flag", + "backend-flag", + "unset-flag", + "legacy-flag", + ], + ), + ( + "client", + ["everywhere-flag", "browser-flag", "unset-flag", "legacy-flag"], + ), + ] + ) + def test_keys_by_evaluation_runtime(self, runtime, expected): + self.assertEqual( + self.client.get_feature_flag_keys_by_evaluation_runtime(runtime), expected + ) + + def test_unrecognized_runtime_argument_raises(self): + with self.assertRaises(ValueError): + self.client.get_feature_flag_keys_by_evaluation_runtime("serverless") + + @mock.patch("posthog.client.Poller") + @mock.patch("posthog.client.get") + def test_runtime_survives_a_local_evaluation_fetch(self, patch_get, patch_poll): + patch_get.return_value = GetResponse( + data={ + "flags": [ + { + "id": 1, + "name": "Browser Feature", + "key": "browser-feature", + "active": True, + "evaluation_runtime": "client", + } + ], + "group_type_mapping": {}, + "cohorts": {}, + } + ) + client = Client(FAKE_TEST_API_KEY, secret_key="test") + client.load_feature_flags() + + self.assertEqual( + client.get_feature_flag_evaluation_runtime("browser-feature"), + FeatureFlagEvaluationRuntime.CLIENT, + ) + self.assertEqual( + client.get_feature_flag_keys_by_evaluation_runtime( + FeatureFlagEvaluationRuntime.CLIENT + ), + ["browser-feature"], + ) + self.assertEqual( + client.get_feature_flag_keys_by_evaluation_runtime( + FeatureFlagEvaluationRuntime.SERVER + ), + [], + ) + + class TestMatchProperties(unittest.TestCase): def property(self, key, value, operator=None): result = {"key": key, "value": value} diff --git a/posthog/types.py b/posthog/types.py index b0a4bd1e8..ae151a0e6 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -1,5 +1,6 @@ import json from dataclasses import dataclass +from enum import Enum from typing import Any, Callable, List, Optional, TypedDict, Union, cast FlagValue = Union[bool, str] @@ -42,6 +43,41 @@ class SendFeatureFlagsOptions(TypedDict, total=False): flag_keys_filter: Optional[list[str]] +class FeatureFlagEvaluationRuntime(str, Enum): + """Where a feature flag is meant to be evaluated. + + Set per flag in PostHog and carried on every locally cached flag definition. + ``ALL`` means the flag suits both client-side and server-side evaluation, so + it matches either runtime. Inheriting from ``str`` keeps the members directly + comparable to their ``"all"`` / ``"client"`` / ``"server"`` values. + """ + + ALL = "all" + CLIENT = "client" + SERVER = "server" + + @classmethod + def from_value(cls, value: Any) -> "FeatureFlagEvaluationRuntime": + """Coerce a raw ``evaluation_runtime`` value to a member. + + A missing, null or unrecognized value becomes ``ALL``, which is the + default PostHog applies to a flag that does not set a runtime. + """ + if isinstance(value, str): + try: + return cls(value.strip().lower()) + except ValueError: + pass + return cls.ALL + + def matches(self, other: "FeatureFlagEvaluationRuntime") -> bool: + """Whether a flag set to one of these runtimes suits the other runtime. + + ``ALL`` matches every runtime, so the check is symmetric. + """ + return self is other or FeatureFlagEvaluationRuntime.ALL in (self, other) + + @dataclass(frozen=True) class FlagReason: """Reason metadata returned by the feature flag API. diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 3eb20c41f..9df7c2bf1 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -16,6 +16,7 @@ alias posthog.DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS -> posthog.exception_u alias posthog.ExceptionArg -> posthog.args.ExceptionArg alias posthog.ExceptionCapture -> posthog.exception_capture.ExceptionCapture alias posthog.FeatureFlag -> posthog.types.FeatureFlag +alias posthog.FeatureFlagEvaluationRuntime -> posthog.types.FeatureFlagEvaluationRuntime alias posthog.FeatureFlagEvaluations -> posthog.feature_flag_evaluations.FeatureFlagEvaluations alias posthog.FeatureFlagResult -> posthog.types.FeatureFlagResult alias posthog.FlagDefinitionCacheData -> posthog.flag_definition_cache.FlagDefinitionCacheData @@ -246,6 +247,7 @@ alias posthog.client.ExceptionArg -> posthog.args.ExceptionArg alias posthog.client.ExceptionCapture -> posthog.exception_capture.ExceptionCapture alias posthog.client.FeatureFlag -> posthog.types.FeatureFlag alias posthog.client.FeatureFlagError -> posthog.types.FeatureFlagError +alias posthog.client.FeatureFlagEvaluationRuntime -> posthog.types.FeatureFlagEvaluationRuntime alias posthog.client.FeatureFlagEvaluations -> posthog.feature_flag_evaluations.FeatureFlagEvaluations alias posthog.client.FeatureFlagResult -> posthog.types.FeatureFlagResult alias posthog.client.FlagCache -> posthog.utils.FlagCache @@ -907,6 +909,9 @@ attribute posthog.types.FeatureFlagError.FLAG_MISSING = 'flag_missing' attribute posthog.types.FeatureFlagError.QUOTA_LIMITED = 'quota_limited' attribute posthog.types.FeatureFlagError.TIMEOUT = 'timeout' attribute posthog.types.FeatureFlagError.UNKNOWN_ERROR = 'unknown_error' +attribute posthog.types.FeatureFlagEvaluationRuntime.ALL = 'all' +attribute posthog.types.FeatureFlagEvaluationRuntime.CLIENT = 'client' +attribute posthog.types.FeatureFlagEvaluationRuntime.SERVER = 'server' attribute posthog.types.FeatureFlagResult.enabled: bool attribute posthog.types.FeatureFlagResult.key: str attribute posthog.types.FeatureFlagResult.payload: Optional[Any] @@ -1057,6 +1062,7 @@ class posthog.request.QuotaLimitError class posthog.tracing.span.Span class posthog.types.FeatureFlag(key: str, enabled: bool, variant: Optional[str], reason: Optional[FlagReason], metadata: Union[FlagMetadata, LegacyFlagMetadata]) class posthog.types.FeatureFlagError +class posthog.types.FeatureFlagEvaluationRuntime class posthog.types.FeatureFlagResult(key: str, enabled: bool, variant: Optional[str], payload: Optional[Any], reason: Optional[str]) class posthog.types.FlagMetadata(id: int, payload: Optional[str], version: int, description: str, has_experiment: Optional[bool] = None) class posthog.types.FlagReason(code: str, condition_index: Optional[int], description: str) @@ -1226,6 +1232,8 @@ function posthog.get_active_span() -> Optional[Span] function posthog.get_all_flags(distinct_id: ID_TYPES, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None, flag_keys_to_evaluate: Optional[list[str]] = None) -> Optional[dict[str, FlagValue]] function posthog.get_all_flags_and_payloads(distinct_id: ID_TYPES, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None, flag_keys_to_evaluate: Optional[list[str]] = None) -> FlagsAndPayloads function posthog.get_feature_flag(key: str, distinct_id: ID_TYPES, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[FlagValue] +function posthog.get_feature_flag_evaluation_runtime(key: str) -> Optional[FeatureFlagEvaluationRuntime] +function posthog.get_feature_flag_keys_by_evaluation_runtime(evaluation_runtime: Union[FeatureFlagEvaluationRuntime, str]) -> list[str] function posthog.get_feature_flag_payload(key: str, distinct_id: ID_TYPES, match_value: Optional[FlagValue] = None, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[object] function posthog.get_feature_flag_result(key: str, distinct_id: ID_TYPES, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[FeatureFlagResult] function posthog.get_remote_config_payload(key: str) @@ -1389,6 +1397,8 @@ method posthog.client.Client.get_active_span() -> Optional[Span] method posthog.client.Client.get_all_flags(distinct_id: ID_TYPES, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, disable_geoip: Optional[bool] = None, flag_keys_to_evaluate: Optional[list[str]] = None, device_id: Optional[str] = None) -> Optional[dict[str, Union[bool, str]]] method posthog.client.Client.get_all_flags_and_payloads(distinct_id: ID_TYPES, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, disable_geoip: Optional[bool] = None, flag_keys_to_evaluate: Optional[list[str]] = None, device_id: Optional[str] = None) -> FlagsAndPayloads method posthog.client.Client.get_feature_flag(key: str, distinct_id: ID_TYPES, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[FlagValue] +method posthog.client.Client.get_feature_flag_evaluation_runtime(key: str) -> Optional[FeatureFlagEvaluationRuntime] +method posthog.client.Client.get_feature_flag_keys_by_evaluation_runtime(evaluation_runtime: Union[FeatureFlagEvaluationRuntime, str]) -> list[str] method posthog.client.Client.get_feature_flag_payload(key: str, distinct_id: ID_TYPES, *, match_value: Optional[FlagValue] = None, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = False, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[object] method posthog.client.Client.get_feature_flag_result(key: str, distinct_id: ID_TYPES, *, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, only_evaluate_locally: bool = False, send_feature_flag_events: bool = True, disable_geoip: Optional[bool] = None, device_id: Optional[str] = None) -> Optional[FeatureFlagResult] method posthog.client.Client.get_feature_flags_and_payloads(distinct_id: ID_TYPES, groups: Optional[Mapping[str, Union[str, int]]] = None, person_properties: Optional[Dict[str, Any]] = None, group_properties: Optional[Dict[str, Dict[str, Any]]] = None, disable_geoip: Optional[bool] = None, flag_keys_to_evaluate: Optional[list[str]] = None, device_id: Optional[str] = None) -> FlagsAndPayloads @@ -1495,6 +1505,8 @@ method posthog.types.FeatureFlag.from_json(resp: Any) -> FeatureFlag method posthog.types.FeatureFlag.from_value_and_payload(key: str, value: FlagValue, payload: Any) -> FeatureFlag method posthog.types.FeatureFlag.get_value() -> FlagValue method posthog.types.FeatureFlagError.api_error(status: Union[int, str]) -> str +method posthog.types.FeatureFlagEvaluationRuntime.from_value(value: Any) -> FeatureFlagEvaluationRuntime +method posthog.types.FeatureFlagEvaluationRuntime.matches(other: FeatureFlagEvaluationRuntime) -> bool method posthog.types.FeatureFlagResult.from_flag_details(details: Union[FeatureFlag, None], override_match_value: Optional[FlagValue] = None) -> FeatureFlagResult | None method posthog.types.FeatureFlagResult.from_value_and_payload(key: str, value: Union[FlagValue, None], payload: Any) -> Union[FeatureFlagResult, None] method posthog.types.FeatureFlagResult.get_value() -> FlagValue From edfb4369579cb023ba9aa5420dee2888c9dd84ec Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:41:10 +0000 Subject: [PATCH 2/2] chore(flags): read runtime keys from the keyed definitions Both read methods now go through `feature_flags_by_key`, which drops the redundant missing-key guard. The dict is built by iterating the definition list, so load order is unchanged. Generated-By: PostHog Desktop Task-Id: f3696295-459a-4066-bb52-5eea7ee16f37 --- posthog/client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/posthog/client.py b/posthog/client.py index c73266f7d..86ab951e6 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -4772,10 +4772,9 @@ def get_feature_flag_keys_by_evaluation_runtime( """ wanted = FeatureFlagEvaluationRuntime(evaluation_runtime) return [ - definition["key"] - for definition in (self.feature_flags or []) - if definition.get("key") is not None - and FeatureFlagEvaluationRuntime.from_value( + key + for key, definition in (self.feature_flags_by_key or {}).items() + if FeatureFlagEvaluationRuntime.from_value( definition.get("evaluation_runtime") ).matches(wanted) ]