Skip to content

Commit 71128a4

Browse files
authored
feat: Surface engine evaluation reason on flags (#236)
1 parent 2c5e20e commit 71128a4

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": "SPLIT; weight=50.0",
2324
"variant": "treatment",
2425
"enabled": true,
2526
"environment": 1,

tests/test_flagsmith.py

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

7980

8081
@responses.activate()
@@ -114,6 +115,7 @@ def test_get_environment_flags_omits_segments_from_evaluation_context(
114115
"name": "some_feature",
115116
"enabled": True,
116117
"value": "some-feature-state-value",
118+
"reason": "DEFAULT",
117119
"metadata": {"id": 1},
118120
}
119121
},
@@ -154,6 +156,7 @@ def test_get_identity_flags_calls_api_when_no_local_environment_no_traits(
154156
assert identity_flags[0].enabled is True
155157
assert identity_flags[0].value == "some-value"
156158
assert identity_flags[0].feature_name == "some_feature"
159+
assert identity_flags[0].reason == "SPLIT; weight=50.0"
157160

158161

159162
@responses.activate()
@@ -208,6 +211,7 @@ def test_get_identity_flags_uses_local_environment_when_available(
208211
"name": "some_feature",
209212
"enabled": True,
210213
"value": "some-feature-state-value",
214+
"reason": "DEFAULT",
211215
"metadata": {"id": 1},
212216
}
213217
},
@@ -250,6 +254,7 @@ def test_get_identity_flags_includes_segments_in_evaluation_context(
250254
"name": "some_feature",
251255
"enabled": True,
252256
"value": "some-feature-state-value",
257+
"reason": "DEFAULT",
253258
"metadata": {"id": 1},
254259
}
255260
},

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)