Skip to content

Commit 83794b2

Browse files
committed
Refactor shared feature flag evaluation helpers
1 parent d9e30ec commit 83794b2

3 files changed

Lines changed: 210 additions & 205 deletions

File tree

posthog/async_client.py

Lines changed: 28 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import inspect
3-
import json
43
import logging
54
import sys
65
import warnings
@@ -45,14 +44,17 @@
4544
)
4645
from .feature_flag_evaluations import (
4746
FeatureFlagEvaluations,
48-
_EvaluatedFlagRecord,
4947
_FeatureFlagEvaluationsHost,
48+
_feature_flag_called_properties,
49+
_flag_details_metadata,
50+
_local_evaluation_records,
51+
_remote_evaluation_records,
5052
)
5153
from .types import (
5254
FeatureFlag,
5355
FeatureFlagError,
5456
FeatureFlagResult,
55-
FlagMetadata,
57+
FlagMetadata as FlagMetadata,
5658
FlagsAndPayloads,
5759
FlagsResponse,
5860
FlagValue,
@@ -1232,12 +1234,10 @@ async def evaluate_flags( # type: ignore[override]
12321234
)
12331235
groups = groups or {}
12341236

1235-
records: Dict[str, _EvaluatedFlagRecord] = {}
12361237
request_id: Optional[str] = None
12371238
evaluated_at: Optional[int] = None
12381239
errors_while_computing = False
12391240
quota_limited = False
1240-
locally_evaluated_keys: set[str] = set()
12411241

12421242
local_result, fallback_to_server = self._get_all_flags_and_payloads_locally(
12431243
distinct_id,
@@ -1247,22 +1247,9 @@ async def evaluate_flags( # type: ignore[override]
12471247
flag_keys_to_evaluate=flag_keys,
12481248
device_id=device_id,
12491249
)
1250-
feature_flags_by_key: Dict[str, Any] = self.feature_flags_by_key or {}
1251-
local_flags = local_result.get("featureFlags") or {}
1252-
local_payloads = local_result.get("featureFlagPayloads") or {}
1253-
for key, value in local_flags.items():
1254-
flag_def = feature_flags_by_key.get(key) or {}
1255-
records[key] = _EvaluatedFlagRecord(
1256-
key=key,
1257-
enabled=value is not False,
1258-
variant=value if isinstance(value, str) else None,
1259-
payload=local_payloads.get(key),
1260-
id=flag_def.get("id"),
1261-
version=None,
1262-
reason="Evaluated locally",
1263-
locally_evaluated=True,
1264-
)
1265-
locally_evaluated_keys.add(key)
1250+
records, locally_evaluated_keys = _local_evaluation_records(
1251+
local_result, self.feature_flags_by_key or {}
1252+
)
12661253

12671254
if fallback_to_server and not only_evaluate_locally:
12681255
try:
@@ -1275,46 +1262,13 @@ async def evaluate_flags( # type: ignore[override]
12751262
flag_keys_to_evaluate=flag_keys,
12761263
device_id=device_id,
12771264
)
1278-
request_id = response.get("requestId")
1279-
raw_evaluated_at = response.get("evaluatedAt")
1280-
evaluated_at = (
1281-
raw_evaluated_at if isinstance(raw_evaluated_at, int) else None
1282-
)
1283-
errors_while_computing = bool(
1284-
response.get("errorsWhileComputingFlags", False)
1285-
)
1286-
for key, detail in response.get("flags", {}).items():
1287-
if key in locally_evaluated_keys:
1288-
continue
1289-
raw_payload = (
1290-
detail.metadata.payload
1291-
if isinstance(detail.metadata, FlagMetadata)
1292-
else getattr(detail.metadata, "payload", None)
1293-
)
1294-
payload: Optional[Any] = None
1295-
if isinstance(raw_payload, str) and raw_payload:
1296-
try:
1297-
payload = json.loads(raw_payload)
1298-
except (json.JSONDecodeError, TypeError):
1299-
payload = raw_payload
1300-
elif raw_payload is not None:
1301-
payload = raw_payload
1302-
records[key] = _EvaluatedFlagRecord(
1303-
key=key,
1304-
enabled=detail.enabled,
1305-
variant=detail.variant,
1306-
payload=payload,
1307-
id=detail.metadata.id
1308-
if isinstance(detail.metadata, FlagMetadata)
1309-
else None,
1310-
version=detail.metadata.version
1311-
if isinstance(detail.metadata, FlagMetadata)
1312-
else None,
1313-
reason=detail.reason.description
1314-
if detail.reason and detail.reason.description
1315-
else None,
1316-
locally_evaluated=False,
1317-
)
1265+
(
1266+
remote_records,
1267+
request_id,
1268+
evaluated_at,
1269+
errors_while_computing,
1270+
) = _remote_evaluation_records(response, locally_evaluated_keys)
1271+
records.update(remote_records)
13181272
except QuotaLimitError as e:
13191273
self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}")
13201274
quota_limited = True
@@ -1394,28 +1348,19 @@ async def _capture_feature_flag_called_async(
13941348
flag_details: Optional[FeatureFlag],
13951349
feature_flag_error: Optional[str] = None,
13961350
) -> None:
1397-
properties: dict[str, Any] = {
1398-
"$feature_flag": key,
1399-
"$feature_flag_response": response,
1400-
"locally_evaluated": flag_was_locally_evaluated,
1401-
f"$feature/{key}": response,
1402-
}
1403-
if payload is not None:
1404-
properties["$feature_flag_payload"] = payload
1405-
if request_id:
1406-
properties["$feature_flag_request_id"] = request_id
1407-
if evaluated_at:
1408-
properties["$feature_flag_evaluated_at"] = evaluated_at
1409-
if isinstance(flag_details, FeatureFlag):
1410-
if flag_details.reason and flag_details.reason.description:
1411-
properties["$feature_flag_reason"] = flag_details.reason.description
1412-
if isinstance(flag_details.metadata, FlagMetadata):
1413-
if flag_details.metadata.version:
1414-
properties["$feature_flag_version"] = flag_details.metadata.version
1415-
if flag_details.metadata.id:
1416-
properties["$feature_flag_id"] = flag_details.metadata.id
1417-
if feature_flag_error:
1418-
properties["$feature_flag_error"] = feature_flag_error
1351+
flag_id, flag_version, flag_reason = _flag_details_metadata(flag_details)
1352+
properties = _feature_flag_called_properties(
1353+
key=key,
1354+
response=response,
1355+
locally_evaluated=flag_was_locally_evaluated,
1356+
payload=payload,
1357+
request_id=request_id,
1358+
evaluated_at=evaluated_at,
1359+
flag_id=flag_id,
1360+
flag_version=flag_version,
1361+
flag_reason=flag_reason,
1362+
feature_flag_error=feature_flag_error,
1363+
)
14191364

14201365
await self._capture_feature_flag_called_if_needed_async(
14211366
distinct_id=distinct_id,

posthog/client.py

Lines changed: 28 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import atexit
22
import inspect
3-
import json
43
import logging
54
import os
65
import sys
@@ -51,8 +50,11 @@
5150
)
5251
from posthog.feature_flag_evaluations import (
5352
FeatureFlagEvaluations,
54-
_EvaluatedFlagRecord,
5553
_FeatureFlagEvaluationsHost,
54+
_feature_flag_called_properties,
55+
_flag_details_metadata,
56+
_local_evaluation_records,
57+
_remote_evaluation_records,
5658
)
5759
from posthog.feature_flags import (
5860
InconclusiveMatchError,
@@ -85,7 +87,7 @@
8587
FeatureFlag,
8688
FeatureFlagError,
8789
FeatureFlagResult,
88-
FlagMetadata,
90+
FlagMetadata as FlagMetadata,
8991
FlagsAndPayloads,
9092
FlagsResponse,
9193
FlagValue,
@@ -2530,31 +2532,19 @@ def _capture_feature_flag_called(
25302532
flag_details: Optional[FeatureFlag],
25312533
feature_flag_error: Optional[str] = None,
25322534
):
2533-
properties: dict[str, Any] = {
2534-
"$feature_flag": key,
2535-
"$feature_flag_response": response,
2536-
"locally_evaluated": flag_was_locally_evaluated,
2537-
f"$feature/{key}": response,
2538-
}
2539-
2540-
if payload is not None:
2541-
# if payload is not a string, json serialize it to a string
2542-
properties["$feature_flag_payload"] = payload
2543-
2544-
if request_id:
2545-
properties["$feature_flag_request_id"] = request_id
2546-
if evaluated_at:
2547-
properties["$feature_flag_evaluated_at"] = evaluated_at
2548-
if isinstance(flag_details, FeatureFlag):
2549-
if flag_details.reason and flag_details.reason.description:
2550-
properties["$feature_flag_reason"] = flag_details.reason.description
2551-
if isinstance(flag_details.metadata, FlagMetadata):
2552-
if flag_details.metadata.version:
2553-
properties["$feature_flag_version"] = flag_details.metadata.version
2554-
if flag_details.metadata.id:
2555-
properties["$feature_flag_id"] = flag_details.metadata.id
2556-
if feature_flag_error:
2557-
properties["$feature_flag_error"] = feature_flag_error
2535+
flag_id, flag_version, flag_reason = _flag_details_metadata(flag_details)
2536+
properties = _feature_flag_called_properties(
2537+
key=key,
2538+
response=response,
2539+
locally_evaluated=flag_was_locally_evaluated,
2540+
payload=payload,
2541+
request_id=request_id,
2542+
evaluated_at=evaluated_at,
2543+
flag_id=flag_id,
2544+
flag_version=flag_version,
2545+
flag_reason=flag_reason,
2546+
feature_flag_error=feature_flag_error,
2547+
)
25582548

25592549
self._capture_feature_flag_called_if_needed(
25602550
distinct_id=distinct_id,
@@ -2875,12 +2865,10 @@ def evaluate_flags(
28752865
)
28762866
groups = groups or {}
28772867

2878-
records: Dict[str, _EvaluatedFlagRecord] = {}
28792868
request_id: Optional[str] = None
28802869
evaluated_at: Optional[int] = None
28812870
errors_while_computing = False
28822871
quota_limited = False
2883-
locally_evaluated_keys: set[str] = set()
28842872

28852873
# Try local evaluation first when the poller has loaded definitions.
28862874
local_result, fallback_to_server = self._get_all_flags_and_payloads_locally(
@@ -2891,24 +2879,9 @@ def evaluate_flags(
28912879
flag_keys_to_evaluate=flag_keys,
28922880
)
28932881

2894-
feature_flags_by_key: Dict[str, Any] = self.feature_flags_by_key or {}
2895-
local_flags = local_result.get("featureFlags") or {}
2896-
local_payloads = local_result.get("featureFlagPayloads") or {}
2897-
for key, value in local_flags.items():
2898-
flag_def = feature_flags_by_key.get(key) or {}
2899-
records[key] = _EvaluatedFlagRecord(
2900-
key=key,
2901-
enabled=value is not False,
2902-
variant=value if isinstance(value, str) else None,
2903-
payload=local_payloads.get(key),
2904-
id=flag_def.get("id"),
2905-
# The local-evaluation flag definition does not carry a version field;
2906-
# only the remote ``/flags`` response does via ``metadata.version``.
2907-
version=None,
2908-
reason="Evaluated locally",
2909-
locally_evaluated=True,
2910-
)
2911-
locally_evaluated_keys.add(key)
2882+
records, locally_evaluated_keys = _local_evaluation_records(
2883+
local_result, self.feature_flags_by_key or {}
2884+
)
29122885

29132886
# Fall back to remote evaluation for any flags the poller couldn't resolve locally.
29142887
# Use the flags decision path directly so the resulting records carry id/version/reason
@@ -2924,52 +2897,13 @@ def evaluate_flags(
29242897
flag_keys_to_evaluate=flag_keys,
29252898
device_id=device_id,
29262899
)
2927-
request_id = response.get("requestId")
2928-
raw_evaluated_at = response.get("evaluatedAt")
2929-
evaluated_at = (
2930-
raw_evaluated_at if isinstance(raw_evaluated_at, int) else None
2931-
)
2932-
errors_while_computing = bool(
2933-
response.get("errorsWhileComputingFlags", False)
2934-
)
2935-
for key, detail in response.get("flags", {}).items():
2936-
if key in locally_evaluated_keys:
2937-
continue
2938-
payload: Optional[Any] = None
2939-
raw_payload = (
2940-
detail.metadata.payload
2941-
if isinstance(detail.metadata, FlagMetadata)
2942-
else getattr(detail.metadata, "payload", None)
2943-
)
2944-
if isinstance(raw_payload, str) and raw_payload:
2945-
try:
2946-
payload = json.loads(raw_payload)
2947-
except (json.JSONDecodeError, TypeError):
2948-
payload = raw_payload
2949-
elif raw_payload is not None:
2950-
payload = raw_payload
2951-
records[key] = _EvaluatedFlagRecord(
2952-
key=key,
2953-
enabled=detail.enabled,
2954-
variant=detail.variant,
2955-
payload=payload,
2956-
id=(
2957-
detail.metadata.id
2958-
if isinstance(detail.metadata, FlagMetadata)
2959-
else None
2960-
),
2961-
version=(
2962-
detail.metadata.version
2963-
if isinstance(detail.metadata, FlagMetadata)
2964-
else None
2965-
),
2966-
reason=(
2967-
detail.reason.description
2968-
if detail.reason and detail.reason.description
2969-
else None
2970-
),
2971-
locally_evaluated=False,
2972-
)
2900+
(
2901+
remote_records,
2902+
request_id,
2903+
evaluated_at,
2904+
errors_while_computing,
2905+
) = _remote_evaluation_records(response, locally_evaluated_keys)
2906+
records.update(remote_records)
29732907
except QuotaLimitError as e:
29742908
self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}")
29752909
quota_limited = True

0 commit comments

Comments
 (0)