Skip to content

Commit b00f288

Browse files
Copilotmrm9084
andcommitted
Implement last feature flag wins behavior for duplicate IDs
Co-authored-by: mrm9084 <[email protected]>
1 parent 6c44174 commit b00f288

File tree

3 files changed

+213
-5
lines changed

3 files changed

+213
-5
lines changed

featuremanagement/_featuremanagerbase.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]:
2828
"""
2929
Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object.
30+
If multiple feature flags have the same id, the last one wins.
3031
3132
:param Mapping configuration: Configuration object.
3233
:param str feature_flag_name: Name of the feature flag.
@@ -40,32 +41,41 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str)
4041
if not feature_flags or not isinstance(feature_flags, list):
4142
return None
4243

44+
last_match = None
4345
for feature_flag in feature_flags:
4446
if feature_flag.get("id") == feature_flag_name:
45-
return FeatureFlag.convert_from_json(feature_flag)
47+
last_match = feature_flag
48+
49+
if last_match:
50+
return FeatureFlag.convert_from_json(last_match)
4651

4752
return None
4853

4954

5055
def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]:
5156
"""
52-
List of all feature flag names.
57+
List of all feature flag names. If there are duplicate names, only unique names are returned.
5358
5459
:param Mapping configuration: Configuration object.
5560
:return: List of feature flag names.
5661
"""
57-
feature_flag_names = []
5862
feature_management = configuration.get(FEATURE_MANAGEMENT_KEY)
5963
if not feature_management or not isinstance(feature_management, Mapping):
6064
return []
6165
feature_flags = feature_management.get(FEATURE_FLAG_KEY)
6266
if not feature_flags or not isinstance(feature_flags, list):
6367
return []
6468

69+
# Use a set to track unique names and a list to preserve order
70+
seen = set()
71+
unique_names = []
6572
for feature_flag in feature_flags:
66-
feature_flag_names.append(feature_flag.get("id"))
73+
flag_id = feature_flag.get("id")
74+
if flag_id not in seen:
75+
seen.add(flag_id)
76+
unique_names.append(flag_id)
6777

68-
return feature_flag_names
78+
return unique_names
6979

7080

7181
class FeatureManagerBase(ABC):

tests/test_feature_manager.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,132 @@ def fake_telemetry_callback(self, evaluation_event):
150150
assert evaluation_event
151151
self.called_telemetry = True
152152

153+
# method: duplicate_feature_flag_handling
154+
def test_duplicate_feature_flags_last_wins(self):
155+
"""Test that when multiple feature flags have the same ID, the last one wins."""
156+
feature_flags = {
157+
"feature_management": {
158+
"feature_flags": [
159+
{
160+
"id": "DuplicateFlag",
161+
"description": "First",
162+
"enabled": "true",
163+
"conditions": {"client_filters": []},
164+
},
165+
{
166+
"id": "DuplicateFlag",
167+
"description": "Second",
168+
"enabled": "false",
169+
"conditions": {"client_filters": []},
170+
},
171+
{
172+
"id": "DuplicateFlag",
173+
"description": "Third",
174+
"enabled": "true",
175+
"conditions": {"client_filters": []},
176+
},
177+
]
178+
}
179+
}
180+
feature_manager = FeatureManager(feature_flags)
181+
182+
# The last flag should win (enabled: true)
183+
assert feature_manager.is_enabled("DuplicateFlag") == True
184+
185+
# Should only list unique names
186+
flag_names = feature_manager.list_feature_flag_names()
187+
assert "DuplicateFlag" in flag_names
188+
# Count how many times DuplicateFlag appears in the list
189+
duplicate_count = flag_names.count("DuplicateFlag")
190+
assert duplicate_count == 1, f"Expected DuplicateFlag to appear once, but appeared {duplicate_count} times"
191+
192+
def test_duplicate_feature_flags_last_wins_disabled(self):
193+
"""Test that when multiple feature flags have the same ID, the last one wins even if disabled."""
194+
feature_flags = {
195+
"feature_management": {
196+
"feature_flags": [
197+
{
198+
"id": "DuplicateFlag",
199+
"description": "First",
200+
"enabled": "true",
201+
"conditions": {"client_filters": []},
202+
},
203+
{
204+
"id": "DuplicateFlag",
205+
"description": "Second",
206+
"enabled": "true",
207+
"conditions": {"client_filters": []},
208+
},
209+
{
210+
"id": "DuplicateFlag",
211+
"description": "Third",
212+
"enabled": "false",
213+
"conditions": {"client_filters": []},
214+
},
215+
]
216+
}
217+
}
218+
feature_manager = FeatureManager(feature_flags)
219+
220+
# The last flag should win (enabled: false)
221+
assert feature_manager.is_enabled("DuplicateFlag") == False
222+
223+
def test_duplicate_feature_flags_mixed_with_unique(self):
224+
"""Test behavior with a mix of duplicate and unique feature flags."""
225+
feature_flags = {
226+
"feature_management": {
227+
"feature_flags": [
228+
{
229+
"id": "UniqueFlag1",
230+
"description": "First unique",
231+
"enabled": "true",
232+
"conditions": {"client_filters": []},
233+
},
234+
{
235+
"id": "DuplicateFlag",
236+
"description": "First duplicate",
237+
"enabled": "false",
238+
"conditions": {"client_filters": []},
239+
},
240+
{
241+
"id": "UniqueFlag2",
242+
"description": "Second unique",
243+
"enabled": "false",
244+
"conditions": {"client_filters": []},
245+
},
246+
{
247+
"id": "DuplicateFlag",
248+
"description": "Second duplicate",
249+
"enabled": "true",
250+
"conditions": {"client_filters": []},
251+
},
252+
{
253+
"id": "UniqueFlag3",
254+
"description": "Third unique",
255+
"enabled": "true",
256+
"conditions": {"client_filters": []},
257+
},
258+
]
259+
}
260+
}
261+
feature_manager = FeatureManager(feature_flags)
262+
263+
# Test unique flags work as expected
264+
assert feature_manager.is_enabled("UniqueFlag1") == True
265+
assert feature_manager.is_enabled("UniqueFlag2") == False
266+
assert feature_manager.is_enabled("UniqueFlag3") == True
267+
268+
# Test duplicate flag - last should win (enabled: true)
269+
assert feature_manager.is_enabled("DuplicateFlag") == True
270+
271+
# Test list includes all unique names
272+
flag_names = feature_manager.list_feature_flag_names()
273+
expected_names = ["UniqueFlag1", "DuplicateFlag", "UniqueFlag2", "UniqueFlag3"]
274+
assert set(flag_names) == set(expected_names)
275+
# Ensure each name appears only once
276+
for name in expected_names:
277+
assert flag_names.count(name) == 1
278+
153279

154280
class AlwaysOn(FeatureFilter):
155281
def evaluate(self, context, **kwargs):

tests/test_feature_manager_async.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,78 @@ async def fake_telemetry_callback_async(self, evaluation_event):
179179
assert evaluation_event
180180
self.called_telemetry = True
181181

182+
# method: duplicate_feature_flag_handling
183+
@pytest.mark.asyncio
184+
async def test_duplicate_feature_flags_last_wins_async(self):
185+
"""Test that when multiple feature flags have the same ID, the last one wins."""
186+
feature_flags = {
187+
"feature_management": {
188+
"feature_flags": [
189+
{
190+
"id": "DuplicateFlag",
191+
"description": "First",
192+
"enabled": "true",
193+
"conditions": {"client_filters": []},
194+
},
195+
{
196+
"id": "DuplicateFlag",
197+
"description": "Second",
198+
"enabled": "false",
199+
"conditions": {"client_filters": []},
200+
},
201+
{
202+
"id": "DuplicateFlag",
203+
"description": "Third",
204+
"enabled": "true",
205+
"conditions": {"client_filters": []},
206+
},
207+
]
208+
}
209+
}
210+
feature_manager = FeatureManager(feature_flags)
211+
212+
# The last flag should win (enabled: true)
213+
assert await feature_manager.is_enabled("DuplicateFlag") == True
214+
215+
# Should only list unique names
216+
flag_names = feature_manager.list_feature_flag_names()
217+
assert "DuplicateFlag" in flag_names
218+
# Count how many times DuplicateFlag appears in the list
219+
duplicate_count = flag_names.count("DuplicateFlag")
220+
assert duplicate_count == 1, f"Expected DuplicateFlag to appear once, but appeared {duplicate_count} times"
221+
222+
@pytest.mark.asyncio
223+
async def test_duplicate_feature_flags_last_wins_disabled_async(self):
224+
"""Test that when multiple feature flags have the same ID, the last one wins even if disabled."""
225+
feature_flags = {
226+
"feature_management": {
227+
"feature_flags": [
228+
{
229+
"id": "DuplicateFlag",
230+
"description": "First",
231+
"enabled": "true",
232+
"conditions": {"client_filters": []},
233+
},
234+
{
235+
"id": "DuplicateFlag",
236+
"description": "Second",
237+
"enabled": "true",
238+
"conditions": {"client_filters": []},
239+
},
240+
{
241+
"id": "DuplicateFlag",
242+
"description": "Third",
243+
"enabled": "false",
244+
"conditions": {"client_filters": []},
245+
},
246+
]
247+
}
248+
}
249+
feature_manager = FeatureManager(feature_flags)
250+
251+
# The last flag should win (enabled: false)
252+
assert await feature_manager.is_enabled("DuplicateFlag") == False
253+
182254

183255
class AlwaysOn(FeatureFilter):
184256
async def evaluate(self, context, **kwargs):

0 commit comments

Comments
 (0)