Skip to content

Commit 9223531

Browse files
committed
Add $feature_flag_error property to track flag evaluation failures
Track errors in feature flag evaluation by adding a $feature_flag_error property to the $feature_flag_called event. This helps identify: - "errors_while_computing_flags": Server returned errorsWhileComputingFlags=true - "request_error": The /flags API call failed (network error, timeout, etc.) This provides visibility into why flag evaluations may return null responses, making it easier to debug issues like missing request IDs.
1 parent b6dbff1 commit 9223531

File tree

2 files changed

+131
-4
lines changed

2 files changed

+131
-4
lines changed

posthog/client.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,7 @@ def _get_feature_flag_result(
15391539
flag_details = None
15401540
request_id = None
15411541
evaluated_at = None
1542+
feature_flag_error: Optional[str] = None
15421543

15431544
flag_value = self._locally_evaluate_flag(
15441545
key, distinct_id, groups, person_properties, group_properties
@@ -1563,7 +1564,7 @@ def _get_feature_flag_result(
15631564
)
15641565
elif not only_evaluate_locally:
15651566
try:
1566-
flag_details, request_id, evaluated_at = (
1567+
flag_details, request_id, evaluated_at, errors_while_computing = (
15671568
self._get_feature_flag_details_from_server(
15681569
key,
15691570
distinct_id,
@@ -1573,6 +1574,9 @@ def _get_feature_flag_result(
15731574
disable_geoip,
15741575
)
15751576
)
1577+
if errors_while_computing:
1578+
feature_flag_error = "errors_while_computing_flags"
1579+
15761580
flag_result = FeatureFlagResult.from_flag_details(
15771581
flag_details, override_match_value
15781582
)
@@ -1588,6 +1592,7 @@ def _get_feature_flag_result(
15881592
)
15891593
except Exception as e:
15901594
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
1595+
feature_flag_error = "request_error"
15911596

15921597
# Fallback to cached value if remote evaluation fails
15931598
if self.flag_cache:
@@ -1612,6 +1617,7 @@ def _get_feature_flag_result(
16121617
request_id,
16131618
evaluated_at,
16141619
flag_details,
1620+
feature_flag_error,
16151621
)
16161622

16171623
return flag_result
@@ -1814,9 +1820,10 @@ def _get_feature_flag_details_from_server(
18141820
person_properties: dict[str, str],
18151821
group_properties: dict[str, str],
18161822
disable_geoip: Optional[bool],
1817-
) -> tuple[Optional[FeatureFlag], Optional[str], Optional[int]]:
1823+
) -> tuple[Optional[FeatureFlag], Optional[str], Optional[int], bool]:
18181824
"""
1819-
Calls /flags and returns the flag details, request id, and evaluated at timestamp
1825+
Calls /flags and returns the flag details, request id, evaluated at timestamp,
1826+
and whether there were errors while computing flags.
18201827
"""
18211828
resp_data = self.get_flags_decision(
18221829
distinct_id,
@@ -1828,9 +1835,10 @@ def _get_feature_flag_details_from_server(
18281835
)
18291836
request_id = resp_data.get("requestId")
18301837
evaluated_at = resp_data.get("evaluatedAt")
1838+
errors_while_computing = resp_data.get("errorsWhileComputingFlags", False)
18311839
flags = resp_data.get("flags")
18321840
flag_details = flags.get(key) if flags else None
1833-
return flag_details, request_id, evaluated_at
1841+
return flag_details, request_id, evaluated_at, errors_while_computing
18341842

18351843
def _capture_feature_flag_called(
18361844
self,
@@ -1844,6 +1852,7 @@ def _capture_feature_flag_called(
18441852
request_id: Optional[str],
18451853
evaluated_at: Optional[int],
18461854
flag_details: Optional[FeatureFlag],
1855+
feature_flag_error: Optional[str] = None,
18471856
):
18481857
feature_flag_reported_key = (
18491858
f"{key}_{'::null::' if response is None else str(response)}"
@@ -1878,6 +1887,8 @@ def _capture_feature_flag_called(
18781887
)
18791888
if flag_details.metadata.id:
18801889
properties["$feature_flag_id"] = flag_details.metadata.id
1890+
if feature_flag_error:
1891+
properties["$feature_flag_error"] = feature_flag_error
18811892

18821893
self.capture(
18831894
"$feature_flag_called",

posthog/test/test_feature_flag_result.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,119 @@ def test_get_feature_flag_result_unknown_flag(self, patch_capture, patch_flags):
442442
groups={},
443443
disable_geoip=None,
444444
)
445+
446+
@mock.patch("posthog.client.flags")
447+
@mock.patch.object(Client, "capture")
448+
def test_get_feature_flag_result_with_errors_while_computing_flags(
449+
self, patch_capture, patch_flags
450+
):
451+
"""Test that errors_while_computing_flags is included in the $feature_flag_called event.
452+
453+
When the server returns errorsWhileComputingFlags=true, it indicates that there
454+
was an error computing one or more flags. We include this in the event so users
455+
can identify and debug flag evaluation issues.
456+
"""
457+
patch_flags.return_value = {
458+
"flags": {
459+
"my-flag": {
460+
"key": "my-flag",
461+
"enabled": True,
462+
"variant": None,
463+
"reason": {"description": "Matched condition set 1"},
464+
"metadata": {"id": 1, "version": 1, "payload": None},
465+
},
466+
},
467+
"requestId": "test-request-id-789",
468+
"errorsWhileComputingFlags": True,
469+
}
470+
471+
flag_result = self.client.get_feature_flag_result("my-flag", "some-distinct-id")
472+
473+
self.assertEqual(flag_result.enabled, True)
474+
patch_capture.assert_called_with(
475+
"$feature_flag_called",
476+
distinct_id="some-distinct-id",
477+
properties={
478+
"$feature_flag": "my-flag",
479+
"$feature_flag_response": True,
480+
"locally_evaluated": False,
481+
"$feature/my-flag": True,
482+
"$feature_flag_request_id": "test-request-id-789",
483+
"$feature_flag_reason": "Matched condition set 1",
484+
"$feature_flag_id": 1,
485+
"$feature_flag_version": 1,
486+
"$feature_flag_error": "errors_while_computing_flags",
487+
},
488+
groups={},
489+
disable_geoip=None,
490+
)
491+
492+
@mock.patch("posthog.client.flags")
493+
@mock.patch.object(Client, "capture")
494+
def test_get_feature_flag_result_flag_not_in_response(
495+
self, patch_capture, patch_flags
496+
):
497+
"""Test that when a flag is not in the API response, we still capture the request_id.
498+
499+
This is the normal case when a flag doesn't exist or the user doesn't match
500+
any conditions - the flag simply won't be in the response.
501+
"""
502+
patch_flags.return_value = {
503+
"flags": {
504+
"other-flag": {
505+
"key": "other-flag",
506+
"enabled": True,
507+
"variant": None,
508+
"reason": {"description": "Matched condition set 1"},
509+
"metadata": {"id": 1, "version": 1, "payload": None},
510+
},
511+
},
512+
"requestId": "test-request-id-456",
513+
}
514+
515+
flag_result = self.client.get_feature_flag_result(
516+
"missing-flag", "some-distinct-id"
517+
)
518+
519+
self.assertIsNone(flag_result)
520+
patch_capture.assert_called_with(
521+
"$feature_flag_called",
522+
distinct_id="some-distinct-id",
523+
properties={
524+
"$feature_flag": "missing-flag",
525+
"$feature_flag_response": None,
526+
"locally_evaluated": False,
527+
"$feature/missing-flag": None,
528+
"$feature_flag_request_id": "test-request-id-456",
529+
},
530+
groups={},
531+
disable_geoip=None,
532+
)
533+
534+
@mock.patch("posthog.client.flags")
535+
@mock.patch.object(Client, "capture")
536+
def test_get_feature_flag_result_request_error(self, patch_capture, patch_flags):
537+
"""Test that when the /flags call throws an exception, we capture it in the event.
538+
539+
This simulates what likely happened in production: the remote evaluation failed
540+
(network error, timeout, etc.), so request_id is never set, and we capture the
541+
$feature_flag_called event with a null response, no request_id, and an error.
542+
"""
543+
patch_flags.side_effect = Exception("Network timeout")
544+
545+
flag_result = self.client.get_feature_flag_result("my-flag", "some-distinct-id")
546+
547+
self.assertIsNone(flag_result)
548+
patch_capture.assert_called_with(
549+
"$feature_flag_called",
550+
distinct_id="some-distinct-id",
551+
properties={
552+
"$feature_flag": "my-flag",
553+
"$feature_flag_response": None,
554+
"locally_evaluated": False,
555+
"$feature/my-flag": None,
556+
"$feature_flag_error": "request_error",
557+
},
558+
groups={},
559+
disable_geoip=None,
560+
)

0 commit comments

Comments
 (0)