Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

if TYPE_CHECKING:
# prevent circular dependenacy by skipping import at runtime
from .helpers.types import ExperimentDict, TrafficAllocation, VariableDict, VariationDict, CmabDict
from .helpers.types import ExperimentDict, ExperimentType, TrafficAllocation, VariableDict, VariationDict, CmabDict


class BaseEntity:
Expand Down Expand Up @@ -87,6 +87,7 @@ def __init__(
groupId: Optional[str] = None,
groupPolicy: Optional[str] = None,
cmab: Optional[CmabDict] = None,
type: Optional[ExperimentType] = None,
**kwargs: Any
):
self.id = id
Expand All @@ -101,6 +102,7 @@ def __init__(
self.groupId = groupId
self.groupPolicy = groupPolicy
self.cmab = cmab
self.type = type

def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
""" Returns audienceConditions if present, otherwise audienceIds. """
Expand Down
2 changes: 2 additions & 0 deletions optimizely/helpers/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ class CmabDict(BaseEntity):
trafficAllocation: int


ExperimentType = Literal['a/b', 'mab', 'cmab', 'feature_rollout']

HoldoutStatus = Literal['Draft', 'Running', 'Concluded', 'Archived']


Expand Down
66 changes: 66 additions & 0 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,37 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
self.experiment_feature_map[exp_id] = [feature.id]
rules.append(self.experiment_id_map[exp_id])

# Feature Rollout support: inject the "everyone else" variation
# into any experiment with type == "feature_rollout"
everyone_else_variation = self._get_everyone_else_variation(feature)
if everyone_else_variation is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this condition to the ticket prompt -
if everyone_else is null, we need handle specially. Let's discuss.

for experiment in rules:
if experiment.type == 'feature_rollout':
experiment.variations.append({
'id': everyone_else_variation.id,
'key': everyone_else_variation.key,
'featureEnabled': everyone_else_variation.featureEnabled,
'variables': cast(
list[types.VariableDict],
everyone_else_variation.variables,
),
})
experiment.trafficAllocation.append({
'entityId': everyone_else_variation.id,
'endOfRange': 10000,
})
self.variation_key_map[experiment.key][everyone_else_variation.key] = everyone_else_variation
self.variation_id_map[experiment.key][everyone_else_variation.id] = everyone_else_variation
self.variation_id_map_by_experiment_id[experiment.id][everyone_else_variation.id] = (
everyone_else_variation
)
self.variation_key_map_by_experiment_id[experiment.id][everyone_else_variation.key] = (
everyone_else_variation
)
self.variation_variable_usage_map[everyone_else_variation.id] = self._generate_key_map(
everyone_else_variation.variables, 'id', entities.Variation.VariableUsage
)

flag_id = feature.id
applicable_holdouts: list[entities.Holdout] = []

Expand Down Expand Up @@ -667,6 +698,41 @@ def get_rollout_from_id(self, rollout_id: str) -> Optional[entities.Layer]:
self.logger.error(f'Rollout with ID "{rollout_id}" is not in datafile.')
return None

def _get_everyone_else_variation(self, flag: entities.FeatureFlag) -> Optional[entities.Variation]:
""" Get the "everyone else" variation for a feature flag.

The "everyone else" rule is the last experiment in the flag's rollout,
and its first variation is the "everyone else" variation.

Args:
flag: The feature flag to get the everyone else variation for.

Returns:
The "everyone else" Variation entity, or None if not available.
"""
if not flag.rolloutId:
return None

rollout = self.get_rollout_from_id(flag.rolloutId)
if not rollout or not rollout.experiments:
return None

everyone_else_rule = rollout.experiments[-1]
variations = everyone_else_rule.get('variations', [])
if not variations:
return None

variation_dict = variations[0]
return entities.Variation(
id=variation_dict['id'],
key=variation_dict['key'],
featureEnabled=bool(variation_dict.get('featureEnabled', False)),
variables=cast(
Optional[list[entities.Variable]],
variation_dict.get('variables'),
),
)

def get_variable_value_for_variation(
self, variable: Optional[entities.Variable], variation: Optional[Union[entities.Variation, VariationDict]]
) -> Optional[str]:
Expand Down
Loading