Skip to content

Commit 888b048

Browse files
committed
feat: Surface engine evaluation reason on flags
beep boop
1 parent 9a85c05 commit 888b048

5 files changed

Lines changed: 46 additions & 0 deletions

File tree

flagsmith/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class Flag(BaseFlag):
5252
feature_id: int
5353
feature_name: str
5454
variant: typing.Optional[str] = None
55+
reason: typing.Optional[str] = None
5556
is_default: bool = field(default=False)
5657

5758
@classmethod
@@ -66,6 +67,7 @@ def from_evaluation_result(
6667
feature_name=flag_result["name"],
6768
feature_id=metadata["id"],
6869
variant=flag_result.get("variant"),
70+
reason=flag_result["reason"],
6971
)
7072
raise ValueError(
7173
"FlagResult metadata is missing. Cannot create Flag instance. "
@@ -80,6 +82,7 @@ def from_api_flag(cls, flag_data: typing.Mapping[str, typing.Any]) -> Flag:
8082
feature_name=flag_data["feature"]["name"],
8183
feature_id=flag_data["feature"]["id"],
8284
variant=flag_data.get("variant"),
85+
reason=flag_data.get("reason"),
8386
)
8487

8588

tests/data/flags.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"project": 1
1313
},
1414
"feature_state_value": "some-value",
15+
"reason": "DEFAULT",
1516
"enabled": true,
1617
"environment": 1,
1718
"identity": null,

tests/data/identities.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"project": 1
2121
},
2222
"feature_state_value": "some-value",
23+
"reason": "TARGETING_MATCH; segment=some_segment",
2324
"enabled": true,
2425
"environment": 1,
2526
"identity": null,

tests/test_flagsmith.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def test_get_environment_flags_calls_api_when_no_local_environment(
7474
assert all_flags[0].enabled is True
7575
assert all_flags[0].value == "some-value"
7676
assert all_flags[0].feature_name == "some_feature"
77+
assert all_flags[0].reason == "DEFAULT"
7778

7879

7980
@responses.activate()
@@ -113,6 +114,7 @@ def test_get_environment_flags_omits_segments_from_evaluation_context(
113114
"name": "some_feature",
114115
"enabled": True,
115116
"value": "some-feature-state-value",
117+
"reason": "DEFAULT",
116118
"metadata": {"id": 1},
117119
}
118120
},
@@ -153,6 +155,7 @@ def test_get_identity_flags_calls_api_when_no_local_environment_no_traits(
153155
assert identity_flags[0].enabled is True
154156
assert identity_flags[0].value == "some-value"
155157
assert identity_flags[0].feature_name == "some_feature"
158+
assert identity_flags[0].reason == "TARGETING_MATCH; segment=some_segment"
156159

157160

158161
@responses.activate()
@@ -207,6 +210,7 @@ def test_get_identity_flags_uses_local_environment_when_available(
207210
"name": "some_feature",
208211
"enabled": True,
209212
"value": "some-feature-state-value",
213+
"reason": "DEFAULT",
210214
"metadata": {"id": 1},
211215
}
212216
},
@@ -249,6 +253,7 @@ def test_get_identity_flags_includes_segments_in_evaluation_context(
249253
"name": "some_feature",
250254
"enabled": True,
251255
"value": "some-feature-state-value",
256+
"reason": "DEFAULT",
252257
"metadata": {"id": 1},
253258
}
254259
},

tests/test_models.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test_flag_from_evaluation_result() -> None:
3636
assert flag.feature_id == 123
3737
assert flag.is_default is False
3838
assert flag.variant == "control"
39+
assert flag.reason == "DEFAULT"
3940

4041

4142
def test_flag_from_evaluation_result__no_variant__is_none() -> None:
@@ -218,6 +219,37 @@ def test_flag_from_api_flag__no_variant__is_none() -> None:
218219
assert flag.variant is None
219220

220221

222+
def test_flag_from_api_flag__sets_reason() -> None:
223+
# Given
224+
flag_data = {
225+
"enabled": True,
226+
"feature_state_value": "test-value",
227+
"feature": {"name": "test_feature", "id": 123},
228+
"reason": "TARGETING_MATCH; segment=premium",
229+
}
230+
231+
# When
232+
flag = Flag.from_api_flag(flag_data)
233+
234+
# Then
235+
assert flag.reason == "TARGETING_MATCH; segment=premium"
236+
237+
238+
def test_flag_from_api_flag__no_reason__is_none() -> None:
239+
# Given
240+
flag_data = {
241+
"enabled": True,
242+
"feature_state_value": "test-value",
243+
"feature": {"name": "test_feature", "id": 123},
244+
}
245+
246+
# When
247+
flag = Flag.from_api_flag(flag_data)
248+
249+
# Then
250+
assert flag.reason is None
251+
252+
221253
def test_get_flag_without_pipeline_processor() -> None:
222254
flags = Flags(
223255
flags={
@@ -335,8 +367,10 @@ def test_lazy_flags__get_flag__applies_matching_segment_override(
335367
# When: we read the targeted feature.
336368
target = lazy_flags.get_flag("target")
337369
# Then: the override wins over the base feature value.
370+
assert isinstance(target, Flag)
338371
assert target.enabled is True
339372
assert target.value == "premium-value"
373+
assert target.reason == "TARGETING_MATCH; segment=premium_segment"
340374

341375

342376
def test_lazy_flags__get_flag__skips_non_matching_segment_override(
@@ -355,8 +389,10 @@ def test_lazy_flags__get_flag__skips_non_matching_segment_override(
355389
target = flags.get_flag("target")
356390

357391
# Then: the override doesn't win and base-value comes through.
392+
assert isinstance(target, Flag)
358393
assert target.enabled is False
359394
assert target.value == "base-value"
395+
assert target.reason == "DEFAULT"
360396

361397

362398
def test_lazy_flags__get_flag__caches_per_feature(

0 commit comments

Comments
 (0)