Skip to content

Commit f5162d9

Browse files
committed
fix: Implicit identity key not supported for % Split operator
1 parent 38c6673 commit f5162d9

File tree

4 files changed

+95
-53
lines changed

4 files changed

+95
-53
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "tests/engine_tests/engine-test-data"]
22
path = tests/engine_tests/engine-test-data
33
url = https://github.com/flagsmith/engine-test-data.git
4-
tag = v3.1.0
4+
branch = feat/implicit-identity-key-for-percentage-split

flag_engine/segments/evaluator.py

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
from flag_engine.utils.types import SupportsStr, get_casting_function
3737

3838

39-
class FeatureContextWithSegmentName(TypedDict, typing.Generic[FeatureMetadataT]):
39+
class SegmentOverride(TypedDict, typing.Generic[FeatureMetadataT]):
4040
feature_context: FeatureContext[FeatureMetadataT]
4141
segment_name: str
4242

4343

44+
SegmentOverrides = dict[str, SegmentOverride[FeatureMetadataT]]
45+
4446
# Type alias for EvaluationContext with any metadata types
4547
# used in internal evaluation logic
4648
_EvaluationContextAnyMeta = EvaluationContext[typing.Any, typing.Any]
@@ -55,15 +57,45 @@ def get_evaluation_result(
5557
:param context: the evaluation context
5658
:return: EvaluationResult containing the context, flags, and segments
5759
"""
58-
segments: list[SegmentResult[SegmentMetadataT]] = []
59-
flags: dict[str, FlagResult[FeatureMetadataT]] = {}
60+
enrich_context(context)
61+
segments, segment_overrides = evaluate_segments(context)
62+
flags = evaluate_features(context, segment_overrides)
63+
64+
return {
65+
"flags": flags,
66+
"segments": segments,
67+
}
68+
69+
70+
def enrich_context(
71+
context: _EvaluationContextAnyMeta,
72+
) -> None:
73+
"""
74+
Enrich the evaluation context in-place by ensuring that:
75+
- `$.identity.key` is set
76+
77+
:param context: the evaluation context to enrich
78+
"""
79+
if identity_context := context.get("identity"):
80+
if not identity_context.get("key"):
81+
identity_context["key"] = (
82+
f"{context['environment']['key']}_{identity_context['identifier']}"
83+
)
6084

61-
segment_feature_contexts: dict[
62-
SupportsStr,
63-
FeatureContextWithSegmentName[FeatureMetadataT],
64-
] = {}
6585

66-
for segment_context in (context.get("segments") or {}).values():
86+
def evaluate_segments(
87+
context: EvaluationContext[SegmentMetadataT, FeatureMetadataT],
88+
) -> typing.Tuple[
89+
list[SegmentResult[SegmentMetadataT]],
90+
SegmentOverrides[FeatureMetadataT],
91+
]:
92+
if not (segment_contexts := context.get("segments")):
93+
return [], {}
94+
95+
segment_results: list[SegmentResult[SegmentMetadataT]] = []
96+
segment_overrides: SegmentOverrides[FeatureMetadataT] = {}
97+
98+
for segment_context in segment_contexts.values():
6799
if not is_context_in_segment(context, segment_context):
68100
continue
69101

@@ -72,69 +104,75 @@ def get_evaluation_result(
72104
}
73105
if segment_metadata := segment_context.get("metadata"):
74106
segment_result["metadata"] = segment_metadata
75-
segments.append(segment_result)
107+
segment_results.append(segment_result)
76108

77109
if overrides := segment_context.get("overrides"):
78110
for override_feature_context in overrides:
79111
feature_name = override_feature_context["name"]
80112
if (
81-
feature_name not in segment_feature_contexts
113+
feature_name not in segment_overrides
82114
or override_feature_context.get(
83115
"priority",
84116
constants.DEFAULT_PRIORITY,
85117
)
86-
< (segment_feature_contexts[feature_name]["feature_context"]).get(
118+
< (segment_overrides[feature_name]["feature_context"]).get(
87119
"priority",
88120
constants.DEFAULT_PRIORITY,
89121
)
90122
):
91-
segment_feature_contexts[feature_name] = (
92-
FeatureContextWithSegmentName(
93-
feature_context=override_feature_context,
94-
segment_name=segment_context["name"],
95-
)
123+
segment_overrides[feature_name] = SegmentOverride(
124+
feature_context=override_feature_context,
125+
segment_name=segment_context["name"],
96126
)
97127

98-
identity_key = _get_identity_key(context)
128+
return segment_results, segment_overrides
129+
130+
131+
def evaluate_features(
132+
context: EvaluationContext[typing.Any, FeatureMetadataT],
133+
segment_overrides: SegmentOverrides[FeatureMetadataT],
134+
) -> dict[str, FlagResult[FeatureMetadataT]]:
135+
flags: dict[str, FlagResult[FeatureMetadataT]] = {}
136+
99137
for feature_context in (context.get("features") or {}).values():
100138
feature_name = feature_context["name"]
101-
if feature_context_with_segment_name := segment_feature_contexts.get(
139+
if segment_override := segment_overrides.get(
102140
feature_context["name"],
103141
):
104-
feature_context = feature_context_with_segment_name["feature_context"]
142+
feature_context = segment_override["feature_context"]
105143
flag_result: FlagResult[FeatureMetadataT]
106144
flags[feature_name] = flag_result = {
107145
"enabled": feature_context["enabled"],
108146
"name": feature_context["name"],
109-
"reason": f"TARGETING_MATCH; segment={feature_context_with_segment_name['segment_name']}",
147+
"reason": f"TARGETING_MATCH; segment={segment_override['segment_name']}",
110148
"value": feature_context.get("value"),
111149
}
112150
if feature_metadata := feature_context.get("metadata"):
113151
flag_result["metadata"] = feature_metadata
114152
continue
115-
flags[feature_name] = get_flag_result_from_feature_context(
116-
feature_context=feature_context,
117-
key=identity_key,
153+
flags[feature_name] = get_flag_result_from_context(
154+
context=context,
155+
feature_name=feature_name,
118156
)
119157

120-
return {
121-
"flags": flags,
122-
"segments": segments,
123-
}
158+
return flags
124159

125160

126-
def get_flag_result_from_feature_context(
127-
feature_context: FeatureContext[FeatureMetadataT],
128-
key: typing.Optional[SupportsStr],
161+
def get_flag_result_from_context(
162+
context: EvaluationContext[typing.Any, FeatureMetadataT],
163+
feature_name: str,
129164
) -> FlagResult[FeatureMetadataT]:
130165
"""
131-
Get a feature value from the feature context
132-
for a given key.
166+
Get a feature value from the evaluation context
167+
for a given feature name.
133168
134-
:param feature_context: the feature context
135-
:param key: the key to get the value for
136-
:return: the value for the key in the feature context
169+
:param context: the evaluation context
170+
:param feature_name: the feature name to get the value for
171+
:return: the value for the feature name in the evaluation context
137172
"""
173+
feature_context = context["features"][feature_name]
174+
key = _get_identity_key(context)
175+
138176
flag_result: typing.Optional[FlagResult[FeatureMetadataT]] = None
139177

140178
if key is not None and (variants := feature_context.get("variants")):
@@ -253,8 +291,8 @@ def context_matches_condition(
253291
if condition["operator"] == constants.PERCENTAGE_SPLIT:
254292
if context_value is not None:
255293
object_ids = [segment_key, context_value]
256-
elif identity_context := context.get("identity"):
257-
object_ids = [segment_key, identity_context["key"]]
294+
elif identity_key := _get_identity_key(context):
295+
object_ids = [segment_key, identity_key]
258296
else:
259297
return False
260298

@@ -376,10 +414,7 @@ def _get_identity_key(
376414
context: _EvaluationContextAnyMeta,
377415
) -> typing.Optional[SupportsStr]:
378416
if identity_context := context.get("identity"):
379-
return (
380-
identity_context.get("key")
381-
or f"{context['environment']['key']}_{identity_context['identifier']}"
382-
)
417+
return identity_context.get("key")
383418
return None
384419

385420

tests/unit/segments/test_segments_evaluator.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
_matches_context_value,
1818
context_matches_condition,
1919
get_context_value,
20-
get_flag_result_from_feature_context,
20+
get_flag_result_from_context,
2121
is_context_in_segment,
2222
)
2323
from flag_engine.segments.types import ConditionOperator
@@ -828,7 +828,8 @@ def test_segment_condition_matches_context_value_for_modulo(
828828
),
829829
),
830830
)
831-
def test_get_flag_result_from_feature_context__calls_returns_expected(
831+
def test_get_flag_result_from_context__calls_returns_expected(
832+
context: EvaluationContext,
832833
percentage_value: int,
833834
expected_result: FlagResult,
834835
mocker: MockerFixture,
@@ -855,11 +856,13 @@ def test_get_flag_result_from_feature_context__calls_returns_expected(
855856
{"value": "bar", "weight": 30, "priority": 2},
856857
],
857858
}
859+
context["features"]["my_feature"] = feature_context
860+
context["identity"] = {"identifier": expected_key, "key": expected_key}
858861

859862
# When
860-
result = get_flag_result_from_feature_context(
861-
feature_context=feature_context,
862-
key=expected_key,
863+
result = get_flag_result_from_context(
864+
context=context,
865+
feature_name="my_feature",
863866
)
864867

865868
# the value of the feature state is correct based on the percentage value returned
@@ -875,12 +878,11 @@ def test_get_flag_result_from_feature_context__calls_returns_expected(
875878

876879

877880
def test_get_flag_result_from_feature_context__null_key__calls_returns_expected(
881+
context: EvaluationContext,
878882
mocker: MockerFixture,
879883
) -> None:
880884
# Given
881885
expected_feature_context_key = "2"
882-
# a None key is provided (no identity context present)
883-
expected_key = None
884886

885887
get_hashed_percentage_for_object_ids_mock = mocker.patch(
886888
"flag_engine.segments.evaluator.get_hashed_percentage_for_object_ids",
@@ -897,10 +899,15 @@ def test_get_flag_result_from_feature_context__null_key__calls_returns_expected(
897899
],
898900
}
899901

902+
context["features"]["my_feature"] = feature_context
903+
904+
# no identity context present
905+
context["identity"] = None
906+
900907
# When
901-
result = get_flag_result_from_feature_context(
902-
feature_context=feature_context,
903-
key=expected_key,
908+
result = get_flag_result_from_context(
909+
context=context,
910+
feature_name="my_feature",
904911
)
905912

906913
# Then

0 commit comments

Comments
 (0)