|
77 | 77 | ) |
78 | 78 | from posthog.feature_flag_evaluations import ( |
79 | 79 | FeatureFlagEvaluations, |
| 80 | + _LocalFlagRecord, |
80 | 81 | _EvaluatedFlagRecord, |
| 82 | + _RemoteFlagRecord, |
81 | 83 | _FeatureFlagEvaluationsHost, |
82 | 84 | ) |
83 | 85 | from posthog.feature_flags import ( |
|
123 | 125 | FlagValue, |
124 | 126 | SendFeatureFlagsOptions, |
125 | 127 | UnresolvedFlagReason, |
| 128 | + _has_untyped_flag_records, |
| 129 | + _UNTYPED_FLAGS_RESPONSE_MESSAGE, |
126 | 130 | normalize_flags_response, |
127 | 131 | to_flags_and_payloads, |
128 | 132 | to_payloads, |
@@ -1016,6 +1020,8 @@ def __init__( |
1016 | 1020 | # the /flags v2 response and the local-evaluation payload. False until the |
1017 | 1021 | # server reports it, so full events are the fail-safe. |
1018 | 1022 | self._minimal_flag_called_events: bool = False |
| 1023 | + # Whether this client has logged that /flags sent records without typed values. |
| 1024 | + self._logged_untyped_flags_response = False |
1019 | 1025 |
|
1020 | 1026 | self.capture_trace_context = capture_trace_context |
1021 | 1027 | self.capture_exception_code_variables = capture_exception_code_variables |
@@ -1716,6 +1722,11 @@ def _get_flags_decision( |
1716 | 1722 | **request_data, |
1717 | 1723 | ) |
1718 | 1724 |
|
| 1725 | + if not self._logged_untyped_flags_response and _has_untyped_flag_records( |
| 1726 | + resp_data |
| 1727 | + ): |
| 1728 | + self._logged_untyped_flags_response = True |
| 1729 | + self.log.info(_UNTYPED_FLAGS_RESPONSE_MESSAGE) |
1719 | 1730 | response = normalize_flags_response(resp_data) |
1720 | 1731 | # Server-controlled gate for minimal $feature_flag_called events. Only the |
1721 | 1732 | # v2 response shape carries it; absent (legacy shape, older server, team |
@@ -4172,9 +4183,23 @@ def _get_feature_flag_result( |
4172 | 4183 | flag_details, override_match_value |
4173 | 4184 | ) |
4174 | 4185 |
|
| 4186 | + if ( |
| 4187 | + flag_details is not None |
| 4188 | + and flag_details._malformed_field is not None |
| 4189 | + ): |
| 4190 | + # The record could not be read, so it reads as disabled. It must |
| 4191 | + # not replace the last good cached value, which it falls back to |
| 4192 | + # like a failed request, and it reports what a failed request |
| 4193 | + # reports: an unknown error and no request details. |
| 4194 | + feature_flag_error = FeatureFlagError.UNKNOWN_ERROR |
| 4195 | + request_id = None |
| 4196 | + evaluated_at = None |
| 4197 | + flag_result = ( |
| 4198 | + self._get_stale_flag_fallback(distinct_id, key) or flag_result |
| 4199 | + ) |
4175 | 4200 | # The request-start generation is an invalidation boundary, not |
4176 | 4201 | # a claim about the server's definitions. Refresh rejects late writes. |
4177 | | - if self.flag_cache and flag_result: |
| 4202 | + elif self.flag_cache and flag_result: |
4178 | 4203 | self.flag_cache._set_cached_remote_flag( |
4179 | 4204 | distinct_id, key, flag_result, local_definition_version |
4180 | 4205 | ) |
@@ -4991,7 +5016,7 @@ def evaluate_flags( |
4991 | 5016 |
|
4992 | 5017 | for key, value in local_flags.items(): |
4993 | 5018 | flag_def = feature_flags_by_key.get(key) or {} |
4994 | | - records[key] = _EvaluatedFlagRecord( |
| 5019 | + records[key] = _LocalFlagRecord( |
4995 | 5020 | key=key, |
4996 | 5021 | enabled=value is not False, |
4997 | 5022 | variant=value if isinstance(value, str) else None, |
@@ -5034,34 +5059,7 @@ def evaluate_flags( |
5034 | 5059 | continue |
5035 | 5060 | if key in locally_evaluated_keys: |
5036 | 5061 | continue |
5037 | | - payload = _parse_flag_payload( |
5038 | | - detail.metadata.payload |
5039 | | - if isinstance(detail.metadata, FlagMetadata) |
5040 | | - else getattr(detail.metadata, "payload", None) |
5041 | | - ) |
5042 | | - records[key] = _EvaluatedFlagRecord( |
5043 | | - key=key, |
5044 | | - enabled=detail.enabled, |
5045 | | - variant=detail.variant, |
5046 | | - payload=payload, |
5047 | | - id=( |
5048 | | - detail.metadata.id |
5049 | | - if isinstance(detail.metadata, FlagMetadata) |
5050 | | - else None |
5051 | | - ), |
5052 | | - version=( |
5053 | | - detail.metadata.version |
5054 | | - if isinstance(detail.metadata, FlagMetadata) |
5055 | | - else None |
5056 | | - ), |
5057 | | - reason=( |
5058 | | - detail.reason.description |
5059 | | - if detail.reason and detail.reason.description |
5060 | | - else None |
5061 | | - ), |
5062 | | - locally_evaluated=False, |
5063 | | - has_experiment=_metadata_has_experiment(detail.metadata), |
5064 | | - ) |
| 5062 | + records[key] = _RemoteFlagRecord(detail) |
5065 | 5063 | except QuotaLimitError as e: |
5066 | 5064 | self.log.warning(f"[FEATURE FLAGS] Quota limit exceeded: {e}") |
5067 | 5065 | quota_limited = True |
|
0 commit comments