Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/src/scenes/feature-flags/FeatureFlagTestingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ export function FeatureFlagTestingTab({ featureFlag }: { featureFlag: FeatureFla
</div>
</div>

{/* Explains a non-match the coarse reason code can't convey, such as a
behavioral or realtime cohort whose membership isn't fully resolved here. */}
{result.reason_description && (
<LemonBanner type="info">{result.reason_description}</LemonBanner>
)}

{/* Distinct ID used for rollout/variant bucketing. Only shown when the
backend echoes an explicit value — it returns null when a different ID
was actually bucketed against, and falling back to the requested ID
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/scenes/persons/RelatedFeatureFlags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,18 @@ const columns: LemonTableColumns<RelatedFeatureFlag> = [
width: 150,
render: function Render(_, featureFlag: RelatedFeatureFlag) {
const matchesSet = featureFlag.evaluation.reason === FeatureFlagMatchReason.ConditionMatch
const description = featureFlag.evaluation.description
return (
<div>
<div className="inline-flex items-center gap-1">
{featureFlag.active ? <>{featureFlagMatchMapping[featureFlag.evaluation.reason]}</> : '--'}

{matchesSet && <LemonSnack>Set {(featureFlag.evaluation.condition_index ?? 0) + 1}</LemonSnack>}

{featureFlag.active && !matchesSet && description && (
<Tooltip title={description}>
<IconInfo className="text-secondary text-base" />
</Tooltip>
)}
</div>
)
},
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/persons/relatedFeatureFlagsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface RelatedFeatureFlag extends FeatureFlagType {
export interface FeatureFlagEvaluationType {
reason: FeatureFlagMatchReason
condition_index?: number
description?: string | null
}

export interface RelatedFeatureFlagResponse {
Expand Down
22 changes: 22 additions & 0 deletions products/feature_flags/backend/api/feature_flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -2558,6 +2558,15 @@ class EvaluationReasonSerializer(serializers.Serializer):
allow_null=True,
help_text="The index of the condition that matched, if applicable",
)
description = serializers.CharField(
required=False,
allow_null=True,
help_text=(
"Human-readable explanation of the evaluation result. Carries the extra signal when the "
"reason code is coarse, for example a non-match decided by a behavioral or realtime cohort "
"whose membership is not fully evaluated here, which can disagree with the cohort's member list."
),
)


class FlagEvaluationResultSerializer(serializers.Serializer):
Expand Down Expand Up @@ -2706,6 +2715,15 @@ class FeatureFlagTestEvaluationResponseSerializer(serializers.Serializer):
flag_key = serializers.CharField(help_text="Feature flag key")
result = serializers.JSONField(help_text="The evaluated value of the feature flag (boolean or variant key string)")
reason = serializers.CharField(help_text="The reason for the evaluation result")
reason_description = serializers.CharField(
required=False,
allow_null=True,
help_text=(
"Human-readable explanation of the evaluation result. Set when the reason code is coarse, "
"for example a non-match decided by a behavioral or realtime cohort whose membership is not "
"fully evaluated here, which can disagree with the cohort's member list."
),
)
condition_index = serializers.IntegerField(
allow_null=True, help_text="The index of the condition that matched, if applicable"
)
Expand Down Expand Up @@ -4153,6 +4171,7 @@ def evaluation_reasons(self, request: request.Request, **kwargs):
"evaluation": {
"reason": reason_data.get("code", "unknown"),
"condition_index": reason_data.get("condition_index"),
"description": reason_data.get("description"),
},
}

Expand Down Expand Up @@ -4479,6 +4498,7 @@ def test_evaluation(self, request: request.Request, **kwargs):

# Initialize defaults
condition_index = None
reason_description = None
payload = None
detailed_conditions: list[dict] = []
result: bool | str = False
Expand All @@ -4496,6 +4516,7 @@ def test_evaluation(self, request: request.Request, **kwargs):

# Extract values from the correct nested structures
reason = reason_data.get("code", "unknown") if reason_data else "unknown"
reason_description = reason_data.get("description") if reason_data else None
condition_index = reason_data.get("condition_index") if reason_data else None
payload = metadata.get("payload") if metadata else None
# Extract conditions from flag result (only valid path per Rust FlagDetails contract).
Expand Down Expand Up @@ -4552,6 +4573,7 @@ def test_evaluation(self, request: request.Request, **kwargs):
"flag_key": feature_flag.key,
"result": result,
"reason": reason,
"reason_description": reason_description,
"condition_index": condition_index,
"payload": payload,
"person_properties": _filter_person_properties_for_flag(
Expand Down
5 changes: 5 additions & 0 deletions products/feature_flags/frontend/generated/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading