From 37812035013ae867f66697662fb5285608d7ff8b Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 14 Sep 2026 08:34:26 +0200 Subject: [PATCH 1/3] fix: Return None for malformed feature flag payloads --- .../changesets/steadfast-baroness-vellamo.md | 5 ++ posthog/client.py | 18 +---- posthog/test/test_evaluate_flags.py | 4 +- posthog/test/test_flag_payload_parsing.py | 69 +++++++++++++++++++ posthog/types.py | 21 +++--- 5 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 .sampo/changesets/steadfast-baroness-vellamo.md create mode 100644 posthog/test/test_flag_payload_parsing.py diff --git a/.sampo/changesets/steadfast-baroness-vellamo.md b/.sampo/changesets/steadfast-baroness-vellamo.md new file mode 100644 index 000000000..71ce26f28 --- /dev/null +++ b/.sampo/changesets/steadfast-baroness-vellamo.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Return None for malformed JSON feature flag payloads instead of returning the raw string or raising a JSONDecodeError. diff --git a/posthog/client.py b/posthog/client.py index 59ccb9dca..159a0e266 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -1,6 +1,5 @@ import atexit import inspect -import json import logging import os import sys @@ -93,7 +92,8 @@ remote_config, reset_sessions, ) -from posthog.types import ( +from .types import ( + _parse_flag_payload, FeatureFlag, FeatureFlagError, FeatureFlagResult, @@ -326,20 +326,6 @@ def _parse_has_experiment(value: Any) -> Optional[bool]: return value if isinstance(value, bool) else None -def _parse_flag_payload(raw_payload: Any) -> Optional[Any]: - """Flag payloads are stored as JSON strings, both in the ``/flags`` response - metadata and in the local-evaluation flag definitions, so decode them before - handing them to callers. A string that isn't valid JSON is passed through as-is.""" - if isinstance(raw_payload, str): - if not raw_payload: - return None - try: - return json.loads(raw_payload) - except (json.JSONDecodeError, TypeError): - return raw_payload - return raw_payload - - def _metadata_has_experiment(metadata: Any) -> Optional[bool]: """Server-reported experiment linkage from flag metadata; ``None`` when absent (e.g. ``LegacyFlagMetadata``, which doesn't carry the field).""" diff --git a/posthog/test/test_evaluate_flags.py b/posthog/test/test_evaluate_flags.py index 31d332661..32a63ab27 100644 --- a/posthog/test/test_evaluate_flags.py +++ b/posthog/test/test_evaluate_flags.py @@ -404,10 +404,10 @@ def test_local_payloads_are_parsed(self, patch_flags): self.assertEqual(patch_flags.call_count, 0) @mock.patch("posthog.client.flags") - def test_non_json_local_payload_is_passed_through(self, patch_flags): + def test_non_json_local_payload_returns_none(self, patch_flags): flags = self.client.evaluate_flags("user-1") - self.assertEqual(flags.get_flag_payload("plain-payload"), "not json") + self.assertIsNone(flags.get_flag_payload("plain-payload")) self.assertEqual(patch_flags.call_count, 0) @mock.patch("posthog.client.flags") diff --git a/posthog/test/test_flag_payload_parsing.py b/posthog/test/test_flag_payload_parsing.py new file mode 100644 index 000000000..678694c1c --- /dev/null +++ b/posthog/test/test_flag_payload_parsing.py @@ -0,0 +1,69 @@ +from unittest.mock import patch + +import pytest + +from posthog.client import Client + + +@pytest.mark.parametrize("local", [True, False]) +@pytest.mark.parametrize("legacy", [True, False]) +@pytest.mark.parametrize( + "raw, expected", + [ + ('{"broken":', None), + ("not json", None), + (" ", None), + ("", None), + ("[1, 2]", [1, 2]), + ('{"ok": true}', {"ok": True}), + ('"text"', "text"), + ("false", False), + ("0", 0), + ("null", None), + ({"decoded": True}, {"decoded": True}), + (None, None), + ], +) +def test_payload_parsing(local, legacy, raw, expected): + client = Client("test-key", send=False) + if local: + client.feature_flags = [ + { + "id": 1, + "key": "test-flag", + "active": True, + "filters": { + "groups": [{"properties": [], "rollout_percentage": 100}], + "payloads": {"true": raw}, + }, + } + ] + response = { + "flags": { + "test-flag": { + "key": "test-flag", + "enabled": True, + "variant": None, + "reason": {"code": "condition_match", "description": "Matched"}, + "metadata": {"id": 1, "version": 1, "payload": raw}, + } + } + } + try: + with ( + patch.object(client, "load_feature_flags"), + patch("posthog.client.flags", return_value=response) as request, + ): + if legacy: + with pytest.warns(DeprecationWarning): + result = client.get_feature_flag_payload( + "test-flag", "user", only_evaluate_locally=local + ) + else: + result = client.evaluate_flags( + "user", only_evaluate_locally=local + ).get_flag_payload("test-flag") + assert result == expected + assert request.call_count == (0 if local else 1) + finally: + client.shutdown() diff --git a/posthog/types.py b/posthog/types.py index 8e8d7a637..0f7ed131e 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -4,6 +4,16 @@ FlagValue = Union[bool, str] + +def _parse_flag_payload(raw_payload: Any) -> Optional[Any]: + if isinstance(raw_payload, str): + try: + return json.loads(raw_payload) + except json.JSONDecodeError: + return None + return raw_payload + + # Type alias for the before_send callback function # Takes an event dictionary and returns the modified event or None to drop it BeforeSendCallback = Callable[[dict[str, Any]], Optional[dict[str, Any]]] @@ -231,9 +241,7 @@ def from_value_and_payload( key=key, enabled=enabled, variant=variant, - payload=json.loads(payload) - if isinstance(payload, str) and payload - else payload, + payload=_parse_flag_payload(payload), reason=None, ) @@ -271,12 +279,7 @@ def from_flag_details( key=details.key, enabled=enabled, variant=variant, - payload=( - json.loads(details.metadata.payload) - if isinstance(details.metadata.payload, str) - and details.metadata.payload - else details.metadata.payload - ), + payload=_parse_flag_payload(details.metadata.payload), reason=details.reason.description if details.reason else None, ) From eae7347fde10dde2176323e5d495f328b9d4901b Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 21 Sep 2026 12:39:11 +0200 Subject: [PATCH 2/3] fix: align bulk flag payload parsing and address review feedback --- .../changesets/steadfast-baroness-vellamo.md | 2 +- posthog/client.py | 7 +- posthog/test/test_client.py | 7 +- posthog/test/test_feature_flags.py | 4 +- posthog/test/test_flag_payload_parsing.py | 101 +++++++++++++++++- posthog/types.py | 4 + 6 files changed, 112 insertions(+), 13 deletions(-) diff --git a/.sampo/changesets/steadfast-baroness-vellamo.md b/.sampo/changesets/steadfast-baroness-vellamo.md index 71ce26f28..244f0cef0 100644 --- a/.sampo/changesets/steadfast-baroness-vellamo.md +++ b/.sampo/changesets/steadfast-baroness-vellamo.md @@ -2,4 +2,4 @@ pypi/posthog: patch --- -Return None for malformed JSON feature flag payloads instead of returning the raw string or raising a JSONDecodeError. +Return None for malformed, empty, or whitespace-only serialized JSON feature flag payloads instead of returning the raw string or raising a JSONDecodeError. Single and bulk getters consistently decode valid JSON payloads, preserving JSON strings (including `""`), false, and zero. diff --git a/posthog/client.py b/posthog/client.py index 159a0e266..d2725ce25 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -4039,12 +4039,17 @@ def get_all_flags_and_payloads( flag_keys_to_evaluate=flag_keys_to_evaluate, device_id=device_id, ) - return to_flags_and_payloads(decide_response) + response = to_flags_and_payloads(decide_response) except Exception as e: self.log.exception( f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}" ) + payloads = response.get("featureFlagPayloads") + if payloads is not None: + response["featureFlagPayloads"] = { + key: _parse_flag_payload(payload) for key, payload in payloads.items() + } return response def evaluate_flags( diff --git a/posthog/test/test_client.py b/posthog/test/test_client.py index 087a79ca1..47513b5da 100644 --- a/posthog/test/test_client.py +++ b/posthog/test/test_client.py @@ -4547,7 +4547,7 @@ def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_pos self.assertIsNotNone(result) self.assertEqual(result.key, "test-flag") self.assertEqual(result.get_value(), "empty-variant") - self.assertEqual(result.payload, "") # Should be empty string, not None + self.assertIsNone(result.payload) @mock.patch("posthog.client.batch_post") def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post): @@ -4585,7 +4585,7 @@ def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post): "multivariate": { "variants": [{"key": "variant2", "rollout_percentage": 100}] }, - "payloads": {"variant2": "normal payload"}, + "payloads": {"variant2": '"normal payload"'}, }, }, ] @@ -4598,9 +4598,8 @@ def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post): self.assertEqual(result["featureFlags"]["empty-payload-flag"], "variant1") self.assertEqual(result["featureFlags"]["normal-payload-flag"], "variant2") - # Check that empty string payload is included (not filtered out) self.assertIn("empty-payload-flag", result["featureFlagPayloads"]) - self.assertEqual(result["featureFlagPayloads"]["empty-payload-flag"], "") + self.assertIsNone(result["featureFlagPayloads"]["empty-payload-flag"]) self.assertEqual( result["featureFlagPayloads"]["normal-payload-flag"], "normal payload" ) diff --git a/posthog/test/test_feature_flags.py b/posthog/test/test_feature_flags.py index 107f9a930..ddaa21600 100644 --- a/posthog/test/test_feature_flags.py +++ b/posthog/test/test_feature_flags.py @@ -1263,7 +1263,7 @@ def test_get_all_flags_and_payloads_with_no_fallback( ] self.assertEqual( client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"], - {"beta-feature": "new"}, + {"beta-feature": None}, ) # /flags is not called because this can be evaluated locally self.assertEqual(patch_flags.call_count, 0) @@ -1403,7 +1403,7 @@ def test_get_all_flags_and_payloads_with_fallback_but_only_local_evaluation_set( client.get_all_flags_and_payloads( "distinct_id", only_evaluate_locally=True )["featureFlagPayloads"], - {"beta-feature": "some-payload"}, + {"beta-feature": None}, ) self.assertEqual(patch_flags.call_count, 0) self.assertEqual(patch_capture.call_count, 0) diff --git a/posthog/test/test_flag_payload_parsing.py b/posthog/test/test_flag_payload_parsing.py index 678694c1c..1e5f16aab 100644 --- a/posthog/test/test_flag_payload_parsing.py +++ b/posthog/test/test_flag_payload_parsing.py @@ -3,10 +3,11 @@ import pytest from posthog.client import Client +from posthog.types import _parse_flag_payload @pytest.mark.parametrize("local", [True, False]) -@pytest.mark.parametrize("legacy", [True, False]) +@pytest.mark.parametrize("api", ["payload", "snapshot", "bulk"]) @pytest.mark.parametrize( "raw, expected", [ @@ -17,6 +18,7 @@ ("[1, 2]", [1, 2]), ('{"ok": true}', {"ok": True}), ('"text"', "text"), + ('""', ""), ("false", False), ("0", 0), ("null", None), @@ -24,7 +26,7 @@ (None, None), ], ) -def test_payload_parsing(local, legacy, raw, expected): +def test_payload_parsing(local, api, raw, expected): client = Client("test-key", send=False) if local: client.feature_flags = [ @@ -36,7 +38,16 @@ def test_payload_parsing(local, legacy, raw, expected): "groups": [{"properties": [], "rollout_percentage": 100}], "payloads": {"true": raw}, }, - } + }, + { + "id": 2, + "key": "healthy", + "active": True, + "filters": { + "groups": [{"properties": [], "rollout_percentage": 100}], + "payloads": {"true": '{"ok": true}'}, + }, + }, ] response = { "flags": { @@ -46,7 +57,11 @@ def test_payload_parsing(local, legacy, raw, expected): "variant": None, "reason": {"code": "condition_match", "description": "Matched"}, "metadata": {"id": 1, "version": 1, "payload": raw}, - } + }, + "healthy": { + "enabled": True, + "metadata": {"payload": '{"ok": true}'}, + }, } } try: @@ -54,7 +69,14 @@ def test_payload_parsing(local, legacy, raw, expected): patch.object(client, "load_feature_flags"), patch("posthog.client.flags", return_value=response) as request, ): - if legacy: + if api == "bulk": + bulk = client.get_all_flags_and_payloads( + "user", only_evaluate_locally=local + ) + assert bulk["featureFlags"] == {"test-flag": True, "healthy": True} + assert bulk["featureFlagPayloads"]["healthy"] == {"ok": True} + result = bulk["featureFlagPayloads"].get("test-flag") + elif api == "payload": with pytest.warns(DeprecationWarning): result = client.get_feature_flag_payload( "test-flag", "user", only_evaluate_locally=local @@ -67,3 +89,72 @@ def test_payload_parsing(local, legacy, raw, expected): assert request.call_count == (0 if local else 1) finally: client.shutdown() + + +@pytest.mark.parametrize("raw", ['{"private":', "", " "]) +def test_parse_failure_logs_without_payload(raw, caplog): + with caplog.at_level("WARNING", logger="posthog"): + assert _parse_flag_payload(raw) is None + assert len(caplog.records) == 1 + assert caplog.records[0].getMessage().removeprefix("[PostHog] ") == ( + "[FEATURE FLAGS] Unable to parse flag payload as JSON" + ) + assert caplog.records[0].exc_info is None + + +@pytest.mark.parametrize("local", [True, False]) +@pytest.mark.parametrize("value", [True, False, "blue"]) +@pytest.mark.parametrize("raw", ['{"broken":', "", " "]) +def test_invalid_payload_preserves_flag_getters(local, value, raw): + client = Client("test-key", send=False) + filters = { + "groups": [{"properties": [], "rollout_percentage": 100 if value else 0}], + "payloads": {str(value).lower(): raw}, + } + if isinstance(value, str): + filters["multivariate"] = { + "variants": [{"key": value, "rollout_percentage": 100}] + } + if local: + client.feature_flags = [ + {"id": 1, "key": "test-flag", "active": True, "filters": filters} + ] + response = { + "flags": { + "test-flag": { + "enabled": value is not False, + "variant": value if isinstance(value, str) else None, + "reason": {"code": "condition_match", "description": "Matched"}, + "metadata": {"id": 1, "version": 1, "payload": raw}, + } + } + } + try: + with ( + patch.object(client, "load_feature_flags"), + patch("posthog.client.flags", return_value=response) as request, + ): + result = client.get_feature_flag_result( + "test-flag", "user", only_evaluate_locally=local + ) + assert result is not None + assert result.key == "test-flag" + assert result.get_value() == value + assert result.enabled is (value is not False) + assert result.variant == (value if isinstance(value, str) else None) + assert result.payload is None + assert result.reason == (None if local else "Matched") + with pytest.warns(DeprecationWarning): + assert ( + client.get_feature_flag( + "test-flag", "user", only_evaluate_locally=local + ) + == value + ) + with pytest.warns(DeprecationWarning): + assert client.feature_enabled( + "test-flag", "user", only_evaluate_locally=local + ) is (value is not False) + assert request.call_count == (0 if local else 3) + finally: + client.shutdown() diff --git a/posthog/types.py b/posthog/types.py index 0f7ed131e..c680187ff 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -1,4 +1,5 @@ import json +import logging from dataclasses import dataclass from typing import Any, Callable, List, Optional, TypedDict, Union, cast @@ -10,6 +11,9 @@ def _parse_flag_payload(raw_payload: Any) -> Optional[Any]: try: return json.loads(raw_payload) except json.JSONDecodeError: + logging.getLogger("posthog").warning( + "[FEATURE FLAGS] Unable to parse flag payload as JSON" + ) return None return raw_payload From 00feca57d428e24d16248457ef414fa10f6253cf Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 22 Sep 2026 09:45:49 +0200 Subject: [PATCH 3/3] fix: isolate flag payload decoding failures across public getters --- .../changesets/steadfast-baroness-vellamo.md | 2 +- posthog/client.py | 17 ++++-- posthog/test/test_flag_payload_parsing.py | 61 ++++++++++++++++--- posthog/types.py | 8 ++- references/public_api_snapshot.txt | 2 +- 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/.sampo/changesets/steadfast-baroness-vellamo.md b/.sampo/changesets/steadfast-baroness-vellamo.md index 244f0cef0..953928785 100644 --- a/.sampo/changesets/steadfast-baroness-vellamo.md +++ b/.sampo/changesets/steadfast-baroness-vellamo.md @@ -2,4 +2,4 @@ pypi/posthog: patch --- -Return None for malformed, empty, or whitespace-only serialized JSON feature flag payloads instead of returning the raw string or raising a JSONDecodeError. Single and bulk getters consistently decode valid JSON payloads, preserving JSON strings (including `""`), false, and zero. +Return None for malformed, empty, or whitespace-only serialized JSON feature flag payloads instead of returning the raw string or raising a JSONDecodeError. Single and bulk getters (including `get_feature_payloads` and `get_feature_flags_and_payloads`) consistently decode valid JSON payloads, preserving JSON strings (including `""`), false, and zero. Reject non-JSON constants such as NaN and Infinity and isolate decoder-limit failures so flag values and healthy sibling payloads remain available. diff --git a/posthog/client.py b/posthog/client.py index d2725ce25..07e15c759 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -1330,9 +1330,9 @@ def get_feature_payloads( disable_geoip: Optional[bool] = None, flag_keys_to_evaluate: Optional[list[str]] = None, device_id: Optional[str] = None, - ) -> dict[str, str]: + ) -> dict[str, Any]: """ - Get feature flag payloads for a user. + Get decoded feature flag payloads for a user. Args: distinct_id: The distinct ID of the user. @@ -1361,7 +1361,10 @@ def get_feature_payloads( flag_keys_to_evaluate, device_id=device_id, ) - return to_payloads(resp_data) or {} + return { + key: _parse_flag_payload(payload) + for key, payload in (to_payloads(resp_data) or {}).items() + } def get_feature_flags_and_payloads( self, @@ -1403,7 +1406,13 @@ def get_feature_flags_and_payloads( flag_keys_to_evaluate, device_id=device_id, ) - return to_flags_and_payloads(resp) + response = to_flags_and_payloads(resp) + payloads = response.get("featureFlagPayloads") + if payloads is not None: + response["featureFlagPayloads"] = { + key: _parse_flag_payload(payload) for key, payload in payloads.items() + } + return response def get_flags_decision( self, diff --git a/posthog/test/test_flag_payload_parsing.py b/posthog/test/test_flag_payload_parsing.py index 1e5f16aab..af0a6e59d 100644 --- a/posthog/test/test_flag_payload_parsing.py +++ b/posthog/test/test_flag_payload_parsing.py @@ -1,3 +1,4 @@ +import sys from unittest.mock import patch import pytest @@ -6,8 +7,19 @@ from posthog.types import _parse_flag_payload -@pytest.mark.parametrize("local", [True, False]) -@pytest.mark.parametrize("api", ["payload", "snapshot", "bulk"]) +@pytest.mark.parametrize( + "api, local", + [ + ("payload", True), + ("payload", False), + ("snapshot", True), + ("snapshot", False), + ("bulk", True), + ("bulk", False), + ("remote_bulk", False), + ("remote_payloads", False), + ], +) @pytest.mark.parametrize( "raw, expected", [ @@ -15,9 +27,28 @@ ("not json", None), (" ", None), ("", None), + ("NaN", None), + ("Infinity", None), + ("-Infinity", None), + ('{"nested": [NaN]}', None), + ("[Infinity, -Infinity]", None), + pytest.param("[" * 20000, None, id="decoder-recursion-limit"), + pytest.param( + "9" * (getattr(sys, "get_int_max_str_digits", lambda: 0)() + 1), + None, + id="decoder-integer-limit", + marks=pytest.mark.skipif( + not getattr(sys, "get_int_max_str_digits", lambda: 0)(), + reason="Integer conversion limit is unavailable or disabled", + ), + ), ("[1, 2]", [1, 2]), ('{"ok": true}', {"ok": True}), ('"text"', "text"), + ('"123"', "123"), + ('"true"', "true"), + ('"NaN"', "NaN"), + ('"Infinity"', "Infinity"), ('""', ""), ("false", False), ("0", 0), @@ -69,29 +100,41 @@ def test_payload_parsing(local, api, raw, expected): patch.object(client, "load_feature_flags"), patch("posthog.client.flags", return_value=response) as request, ): - if api == "bulk": - bulk = client.get_all_flags_and_payloads( - "user", only_evaluate_locally=local + if api in ("bulk", "remote_bulk"): + bulk = ( + client.get_all_flags_and_payloads( + "user", only_evaluate_locally=local + ) + if api == "bulk" + else client.get_feature_flags_and_payloads("user") ) assert bulk["featureFlags"] == {"test-flag": True, "healthy": True} assert bulk["featureFlagPayloads"]["healthy"] == {"ok": True} result = bulk["featureFlagPayloads"].get("test-flag") + elif api == "remote_payloads": + payloads = client.get_feature_payloads("user") + assert payloads["healthy"] == {"ok": True} + result = payloads.get("test-flag") elif api == "payload": with pytest.warns(DeprecationWarning): result = client.get_feature_flag_payload( "test-flag", "user", only_evaluate_locally=local ) else: - result = client.evaluate_flags( - "user", only_evaluate_locally=local - ).get_flag_payload("test-flag") + result = client.evaluate_flags("user", only_evaluate_locally=local) + assert result.get_flag_payload("healthy") == {"ok": True} + assert result.get_flag("test-flag") is True + result = result.get_flag_payload("test-flag") assert result == expected + assert type(result) is type(expected) assert request.call_count == (0 if local else 1) finally: client.shutdown() -@pytest.mark.parametrize("raw", ['{"private":', "", " "]) +@pytest.mark.parametrize( + "raw", ['{"private":', "", " ", "NaN", "Infinity", "-Infinity"] +) def test_parse_failure_logs_without_payload(raw, caplog): with caplog.at_level("WARNING", logger="posthog"): assert _parse_flag_payload(raw) is None diff --git a/posthog/types.py b/posthog/types.py index c680187ff..c1bad40b1 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -6,11 +6,15 @@ FlagValue = Union[bool, str] +def _reject_json_constant(value: str) -> None: + raise ValueError("Invalid JSON constant") + + def _parse_flag_payload(raw_payload: Any) -> Optional[Any]: if isinstance(raw_payload, str): try: - return json.loads(raw_payload) - except json.JSONDecodeError: + return json.loads(raw_payload, parse_constant=_reject_json_constant) + except (ValueError, RecursionError): logging.getLogger("posthog").warning( "[FEATURE FLAGS] Unable to parse flag payload as JSON" ) diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 26c37dc44..d994e6533 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -1369,7 +1369,7 @@ method posthog.client.Client.get_feature_flag(key: str, distinct_id: ID_TYPES, * 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 -method posthog.client.Client.get_feature_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) -> dict[str, str] +method posthog.client.Client.get_feature_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) -> dict[str, Any] method posthog.client.Client.get_feature_variants(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) -> dict[str, Union[bool, str]] method posthog.client.Client.get_flags_decision(distinct_id: Optional[ID_TYPES] = 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, disable_geoip: Optional[bool] = None, flag_keys_to_evaluate: Optional[list[str]] = None, device_id: Optional[str] = None) -> FlagsResponse method posthog.client.Client.get_remote_config_payload(key: str)