Skip to content

Commit 7e89734

Browse files
committed
Add FeatureFlagError constants class for error type values
- Add FeatureFlagError class to types.py with constants: - ERRORS_WHILE_COMPUTING, FLAG_MISSING, QUOTA_LIMITED - TIMEOUT, CONNECTION_ERROR, UNKNOWN_ERROR - api_error(status) static method for dynamic error strings - Update client.py to use FeatureFlagError constants instead of magic strings - Update all tests to use constants for maintainability This improves maintainability by: - Single source of truth for error values - IDE autocomplete and typo detection - Documentation of analytics-stable values
1 parent a474f9c commit 7e89734

File tree

3 files changed

+67
-21
lines changed

3 files changed

+67
-21
lines changed

posthog/client.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
)
5757
from posthog.types import (
5858
FeatureFlag,
59+
FeatureFlagError,
5960
FeatureFlagResult,
6061
FlagMetadata,
6162
FlagsAndPayloads,
@@ -1592,9 +1593,9 @@ def _get_feature_flag_result(
15921593
)
15931594
errors = []
15941595
if errors_while_computing:
1595-
errors.append("errors_while_computing_flags")
1596+
errors.append(FeatureFlagError.ERRORS_WHILE_COMPUTING)
15961597
if flag_details is None:
1597-
errors.append("flag_missing")
1598+
errors.append(FeatureFlagError.FLAG_MISSING)
15981599
if errors:
15991600
feature_flag_error = ",".join(errors)
16001601

@@ -1613,23 +1614,23 @@ def _get_feature_flag_result(
16131614
)
16141615
except QuotaLimitError as e:
16151616
self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}")
1616-
feature_flag_error = "quota_limited"
1617+
feature_flag_error = FeatureFlagError.QUOTA_LIMITED
16171618
flag_result = self._get_stale_flag_fallback(distinct_id, key)
16181619
except RequestsTimeout as e:
16191620
self.log.warning(f"[FEATURE FLAGS] Request timed out: {e}")
1620-
feature_flag_error = "timeout"
1621+
feature_flag_error = FeatureFlagError.TIMEOUT
16211622
flag_result = self._get_stale_flag_fallback(distinct_id, key)
16221623
except RequestsConnectionError as e:
16231624
self.log.warning(f"[FEATURE FLAGS] Connection error: {e}")
1624-
feature_flag_error = "connection_error"
1625+
feature_flag_error = FeatureFlagError.CONNECTION_ERROR
16251626
flag_result = self._get_stale_flag_fallback(distinct_id, key)
16261627
except APIError as e:
16271628
self.log.warning(f"[FEATURE FLAGS] API error: {e}")
1628-
feature_flag_error = f"api_error_{e.status}"
1629+
feature_flag_error = FeatureFlagError.api_error(e.status)
16291630
flag_result = self._get_stale_flag_fallback(distinct_id, key)
16301631
except Exception as e:
16311632
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
1632-
feature_flag_error = "unknown_error"
1633+
feature_flag_error = FeatureFlagError.UNKNOWN_ERROR
16331634
flag_result = self._get_stale_flag_fallback(distinct_id, key)
16341635

16351636
if send_feature_flag_events:

posthog/test/test_feature_flag_result.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
from posthog.client import Client
66
from posthog.test.test_utils import FAKE_TEST_API_KEY
7-
from posthog.types import FeatureFlag, FeatureFlagResult, FlagMetadata, FlagReason
7+
from posthog.types import (
8+
FeatureFlag,
9+
FeatureFlagError,
10+
FeatureFlagResult,
11+
FlagMetadata,
12+
FlagReason,
13+
)
814

915

1016
class TestFeatureFlagResult(unittest.TestCase):
@@ -450,7 +456,7 @@ def test_get_feature_flag_result_unknown_flag(self, patch_capture, patch_flags):
450456
"$feature_flag_response": None,
451457
"locally_evaluated": False,
452458
"$feature/no-person-flag": None,
453-
"$feature_flag_error": "flag_missing",
459+
"$feature_flag_error": FeatureFlagError.FLAG_MISSING,
454460
},
455461
groups={},
456462
disable_geoip=None,
@@ -496,7 +502,7 @@ def test_get_feature_flag_result_with_errors_while_computing_flags(
496502
"$feature_flag_reason": "Matched condition set 1",
497503
"$feature_flag_id": 1,
498504
"$feature_flag_version": 1,
499-
"$feature_flag_error": "errors_while_computing_flags",
505+
"$feature_flag_error": FeatureFlagError.ERRORS_WHILE_COMPUTING,
500506
},
501507
groups={},
502508
disable_geoip=None,
@@ -538,7 +544,7 @@ def test_get_feature_flag_result_flag_not_in_response(
538544
"locally_evaluated": False,
539545
"$feature/missing-flag": None,
540546
"$feature_flag_request_id": "test-request-id-456",
541-
"$feature_flag_error": "flag_missing",
547+
"$feature_flag_error": FeatureFlagError.FLAG_MISSING,
542548
},
543549
groups={},
544550
disable_geoip=None,
@@ -574,7 +580,7 @@ def test_get_feature_flag_result_errors_computing_and_flag_missing(
574580
"locally_evaluated": False,
575581
"$feature/missing-flag": None,
576582
"$feature_flag_request_id": "test-request-id-999",
577-
"$feature_flag_error": "errors_while_computing_flags,flag_missing",
583+
"$feature_flag_error": f"{FeatureFlagError.ERRORS_WHILE_COMPUTING},{FeatureFlagError.FLAG_MISSING}",
578584
},
579585
groups={},
580586
disable_geoip=None,
@@ -597,7 +603,7 @@ def test_get_feature_flag_result_unknown_error(self, patch_capture, patch_flags)
597603
"$feature_flag_response": None,
598604
"locally_evaluated": False,
599605
"$feature/my-flag": None,
600-
"$feature_flag_error": "unknown_error",
606+
"$feature_flag_error": FeatureFlagError.UNKNOWN_ERROR,
601607
},
602608
groups={},
603609
disable_geoip=None,
@@ -622,7 +628,7 @@ def test_get_feature_flag_result_timeout_error(self, patch_capture, patch_flags)
622628
"$feature_flag_response": None,
623629
"locally_evaluated": False,
624630
"$feature/my-flag": None,
625-
"$feature_flag_error": "timeout",
631+
"$feature_flag_error": FeatureFlagError.TIMEOUT,
626632
},
627633
groups={},
628634
disable_geoip=None,
@@ -647,7 +653,7 @@ def test_get_feature_flag_result_connection_error(self, patch_capture, patch_fla
647653
"$feature_flag_response": None,
648654
"locally_evaluated": False,
649655
"$feature/my-flag": None,
650-
"$feature_flag_error": "connection_error",
656+
"$feature_flag_error": FeatureFlagError.CONNECTION_ERROR,
651657
},
652658
groups={},
653659
disable_geoip=None,
@@ -672,7 +678,7 @@ def test_get_feature_flag_result_api_error(self, patch_capture, patch_flags):
672678
"$feature_flag_response": None,
673679
"locally_evaluated": False,
674680
"$feature/my-flag": None,
675-
"$feature_flag_error": "api_error_500",
681+
"$feature_flag_error": FeatureFlagError.api_error(500),
676682
},
677683
groups={},
678684
disable_geoip=None,
@@ -697,7 +703,7 @@ def test_get_feature_flag_result_quota_limited(self, patch_capture, patch_flags)
697703
"$feature_flag_response": None,
698704
"locally_evaluated": False,
699705
"$feature/my-flag": None,
700-
"$feature_flag_error": "quota_limited",
706+
"$feature_flag_error": FeatureFlagError.QUOTA_LIMITED,
701707
},
702708
groups={},
703709
disable_geoip=None,
@@ -769,7 +775,7 @@ def test_timeout_error_returns_stale_cached_value(self, patch_capture, patch_fla
769775
"locally_evaluated": False,
770776
"$feature/my-flag": "cached-variant",
771777
"$feature_flag_payload": {"from": "cache"},
772-
"$feature_flag_error": "timeout",
778+
"$feature_flag_error": FeatureFlagError.TIMEOUT,
773779
},
774780
groups={},
775781
disable_geoip=None,
@@ -806,7 +812,7 @@ def test_connection_error_returns_stale_cached_value(
806812
"$feature_flag_response": True,
807813
"locally_evaluated": False,
808814
"$feature/my-flag": True,
809-
"$feature_flag_error": "connection_error",
815+
"$feature_flag_error": FeatureFlagError.CONNECTION_ERROR,
810816
},
811817
groups={},
812818
disable_geoip=None,
@@ -842,7 +848,7 @@ def test_api_error_returns_stale_cached_value(self, patch_capture, patch_flags):
842848
"$feature_flag_response": "control",
843849
"locally_evaluated": False,
844850
"$feature/my-flag": "control",
845-
"$feature_flag_error": "api_error_503",
851+
"$feature_flag_error": FeatureFlagError.api_error(503),
846852
},
847853
groups={},
848854
disable_geoip=None,
@@ -872,7 +878,7 @@ def test_error_without_cache_returns_none(self, patch_capture, patch_flags):
872878
"$feature_flag_response": None,
873879
"locally_evaluated": False,
874880
"$feature/my-flag": None,
875-
"$feature_flag_error": "timeout",
881+
"$feature_flag_error": FeatureFlagError.TIMEOUT,
876882
},
877883
groups={},
878884
disable_geoip=None,

posthog/types.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,42 @@ def to_payloads(response: FlagsResponse) -> Optional[dict[str, str]]:
307307
and value.enabled
308308
and value.metadata.payload is not None
309309
}
310+
311+
312+
class FeatureFlagError:
313+
"""Error type constants for the $feature_flag_error property.
314+
315+
These values are sent in analytics events to track flag evaluation failures.
316+
They should not be changed without considering impact on existing dashboards
317+
and queries that filter on these values.
318+
319+
Error values:
320+
ERRORS_WHILE_COMPUTING: Server returned errorsWhileComputingFlags=true
321+
FLAG_MISSING: Requested flag not in API response
322+
QUOTA_LIMITED: Rate/quota limit exceeded
323+
TIMEOUT: Request timed out
324+
CONNECTION_ERROR: Network connectivity issue
325+
UNKNOWN_ERROR: Unexpected exceptions
326+
327+
For API errors with status codes, use the api_error() method which returns
328+
a string like "api_error_500".
329+
"""
330+
331+
ERRORS_WHILE_COMPUTING = "errors_while_computing_flags"
332+
FLAG_MISSING = "flag_missing"
333+
QUOTA_LIMITED = "quota_limited"
334+
TIMEOUT = "timeout"
335+
CONNECTION_ERROR = "connection_error"
336+
UNKNOWN_ERROR = "unknown_error"
337+
338+
@staticmethod
339+
def api_error(status: int) -> str:
340+
"""Generate API error string with status code.
341+
342+
Args:
343+
status: HTTP status code from the API error
344+
345+
Returns:
346+
Error string like "api_error_500"
347+
"""
348+
return f"api_error_{status}"

0 commit comments

Comments
 (0)