Skip to content

Commit 159ee1b

Browse files
[AI-FSSDK] [FSSDK-12337] Add Feature Rollout support to project config parsing
1 parent d651911 commit 159ee1b

File tree

3 files changed

+962
-0
lines changed

3 files changed

+962
-0
lines changed

optimizely/entities.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def __init__(
8787
groupId: Optional[str] = None,
8888
groupPolicy: Optional[str] = None,
8989
cmab: Optional[CmabDict] = None,
90+
type: Optional[str] = None,
9091
**kwargs: Any
9192
):
9293
self.id = id
@@ -101,6 +102,7 @@ def __init__(
101102
self.groupId = groupId
102103
self.groupPolicy = groupPolicy
103104
self.cmab = cmab
105+
self.type = type
104106

105107
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
106108
""" Returns audienceConditions if present, otherwise audienceIds. """

optimizely/project_config.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,34 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
232232
self.experiment_feature_map[exp_id] = [feature.id]
233233
rules.append(self.experiment_id_map[exp_id])
234234

235+
# Feature Rollout support: inject the "everyone else" variation
236+
# into any experiment with type == "feature_rollout"
237+
rollout_for_flag = (
238+
None if len(feature.rolloutId) == 0
239+
else self.rollout_id_map.get(feature.rolloutId)
240+
)
241+
if rollout_for_flag:
242+
everyone_else_variation = self._get_everyone_else_variation(rollout_for_flag)
243+
if everyone_else_variation is not None:
244+
for experiment in rules:
245+
if getattr(experiment, 'type', None) == 'feature_rollout':
246+
# Append the everyone else variation to the experiment
247+
experiment.variations.append(everyone_else_variation)
248+
# Add traffic allocation entry with endOfRange=10000
249+
experiment.trafficAllocation.append({
250+
'entityId': everyone_else_variation['id'],
251+
'endOfRange': 10000,
252+
})
253+
# Update variation maps for this experiment
254+
var_entity = entities.Variation(**everyone_else_variation)
255+
self.variation_key_map[experiment.key][var_entity.key] = var_entity
256+
self.variation_id_map[experiment.key][var_entity.id] = var_entity
257+
self.variation_id_map_by_experiment_id[experiment.id][var_entity.id] = var_entity
258+
self.variation_key_map_by_experiment_id[experiment.id][var_entity.key] = var_entity
259+
self.variation_variable_usage_map[var_entity.id] = self._generate_key_map(
260+
var_entity.variables, 'id', entities.Variation.VariableUsage
261+
)
262+
235263
flag_id = feature.id
236264
applicable_holdouts: list[entities.Holdout] = []
237265

@@ -304,6 +332,32 @@ def _generate_key_map(
304332

305333
return key_map
306334

335+
@staticmethod
336+
def _get_everyone_else_variation(rollout: entities.Layer) -> Optional[types.VariationDict]:
337+
""" Get the "everyone else" variation from a rollout.
338+
339+
The "everyone else" rule is the last experiment in the rollout,
340+
and its first variation is the "everyone else" variation.
341+
342+
Args:
343+
rollout: The rollout (Layer) entity to get the variation from.
344+
345+
Returns:
346+
The "everyone else" variation dict, or None if not available.
347+
"""
348+
if not rollout.experiments:
349+
return None
350+
351+
everyone_else_rule = rollout.experiments[-1]
352+
variations = everyone_else_rule.get('variations', []) if isinstance(
353+
everyone_else_rule, dict
354+
) else getattr(everyone_else_rule, 'variations', [])
355+
356+
if not variations:
357+
return None
358+
359+
return variations[0]
360+
307361
@staticmethod
308362
def _deserialize_audience(audience_map: dict[str, entities.Audience]) -> dict[str, entities.Audience]:
309363
""" Helper method to de-serialize and populate audience map with the condition list and structure.

0 commit comments

Comments
 (0)