Skip to content
Merged
60 changes: 7 additions & 53 deletions optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,39 +413,6 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes

return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons

def get_experiment_in_group(self, project_config, group, bucketing_id):
""" Determine which experiment in the group the user is bucketed into.

Args:
project_config: Instance of ProjectConfig.
group: The group to bucket the user into.
bucketing_id: ID to be used for bucketing the user.

Returns:
Experiment if the user is bucketed into an experiment in the specified group. None otherwise
and array of log messages representing decision making.
"""
decide_reasons = []
experiment_id = self.bucketer.find_bucket(
project_config, bucketing_id, group.id, group.trafficAllocation)
if experiment_id:
experiment = project_config.get_experiment_from_id(experiment_id)
if experiment:
message = 'User with bucketing ID "%s" is in experiment %s of group %s.' % \
(bucketing_id, experiment.key, group.id)
self.logger.info(
message
)
decide_reasons.append(message)
return experiment, decide_reasons
message = 'User with bucketing ID "%s" is not in any experiments of group %s.' % (bucketing_id, group.id)
self.logger.info(
message
)
decide_reasons.append(message)

return None, decide_reasons

def get_variation_for_feature(self, project_config, feature, user_id, attributes=None, ignore_user_profile=False):
""" Returns the experiment/variation the user is bucketed in for the given feature.

Expand All @@ -462,31 +429,18 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes
decide_reasons = []
bucketing_id, reasons = self._get_bucketing_id(user_id, attributes)
decide_reasons += reasons
# First check if the feature is in a mutex group
if feature.groupId:
group = project_config.get_group(feature.groupId)
if group:
experiment, reasons = self.get_experiment_in_group(project_config, group, bucketing_id)
decide_reasons += reasons
if experiment and experiment.id in feature.experimentIds:

# Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments
if feature.experimentIds:
# Evaluate each experiment ID and return the first bucketed experiment variation
for experiment in feature.experimentIds:
experiment = project_config.get_experiment_from_id(experiment)
if experiment:
variation, variation_reasons = self.get_variation(
project_config, experiment, user_id, attributes, ignore_user_profile)
decide_reasons += variation_reasons
if variation:
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons
else:
self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature'))

# Next check if the feature is being experimented on
elif feature.experimentIds:
# If an experiment is not in a group, then the feature can only be associated with one experiment
experiment = project_config.get_experiment_from_id(feature.experimentIds[0])
if experiment:
variation, variation_reasons = self.get_variation(
project_config, experiment, user_id, attributes, ignore_user_profile)
decide_reasons += variation_reasons
if variation:
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST), decide_reasons

# Next check if user is part of a rollout
if feature.rolloutId:
Expand Down
132 changes: 38 additions & 94 deletions tests/test_decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1309,58 +1309,41 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_
mock_decision_service_logging,
)

def test_get_variation_for_feature__returns_variation_for_feature_in_group(self):
""" Test that get_variation_for_feature returns the variation of
the experiment the user is bucketed in the feature's group. """
def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self):
""" Test that get_variation_for_feature returns None for user not in the associated experiment. """

feature = self.project_config.get_feature_from_key("test_feature_in_group")
feature = self.project_config.get_feature_from_key("test_feature_in_experiment")

expected_experiment = self.project_config.get_experiment_from_key("group_exp_1")
expected_variation = self.project_config.get_variation_from_id(
"group_exp_1", "28901"
)
with mock.patch(
"optimizely.decision_service.DecisionService.get_experiment_in_group",
return_value=(self.project_config.get_experiment_from_key("group_exp_1"), []),
) as mock_get_experiment_in_group, mock.patch(
"optimizely.decision_service.DecisionService.get_variation",
return_value=(expected_variation, []),
return_value=[None, []],
) as mock_decision:
variation_received, _ = self.decision_service.get_variation_for_feature(
self.project_config, feature, "test_user"
)
self.assertEqual(
decision_service.Decision(
expected_experiment,
expected_variation,
enums.DecisionSources.FEATURE_TEST,
),
decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT),
variation_received,
)

mock_get_experiment_in_group.assert_called_once_with(
self.project_config, self.project_config.get_group("19228"), 'test_user')

mock_decision.assert_called_once_with(
self.project_config,
self.project_config.get_experiment_from_key("group_exp_1"),
self.project_config.get_experiment_from_key("test_experiment"),
"test_user",
None,
False
)

def test_get_variation_for_feature__returns_none_for_user_not_in_group(self):
""" Test that get_variation_for_feature returns None for
user not in group and the feature is not part of a rollout. """
user not in group and the feature is not part of a rollout. """
Copy link
Contributor

Choose a reason for hiding this comment

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

indent like before.


feature = self.project_config.get_feature_from_key("test_feature_in_group")

with mock.patch(
"optimizely.decision_service.DecisionService.get_experiment_in_group",
return_value=[None, []],
) as mock_get_experiment_in_group, mock.patch(
"optimizely.decision_service.DecisionService.get_variation"
) as mock_decision:
"optimizely.decision_service.DecisionService.get_variation",
Copy link
Contributor

Choose a reason for hiding this comment

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

This mock does not give a test for "group". It tests the cases for feature-tests not bucketed.
Can we mock bucket to control group-exclusion cases?

return_value=[None, []],
):
variation_received, _ = self.decision_service.get_variation_for_feature(
self.project_config, feature, "test_user"
)
Expand All @@ -1369,65 +1352,50 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_group(self):
variation_received,
)

mock_get_experiment_in_group.assert_called_once_with(
self.project_config, self.project_config.get_group("19228"), "test_user")

self.assertFalse(mock_decision.called)

def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self):
""" Test that get_variation_for_feature returns None for user not in the associated experiment. """
def test_get_variation_for_feature__returns_variation_for_feature_in_group(self):
""" Test that get_variation_for_feature returns the variation of
the experiment the user is bucketed in the feature's group. """

feature = self.project_config.get_feature_from_key("test_feature_in_experiment")
feature = self.project_config.get_feature_from_key("test_feature_in_group")

expected_experiment = self.project_config.get_experiment_from_key("group_exp_1")
expected_variation = self.project_config.get_variation_from_id(
"group_exp_1", "28901"
)
with mock.patch(
"optimizely.decision_service.DecisionService.get_variation",
return_value=[None, []],
return_value=(expected_variation, []),
) as mock_decision:
variation_received, _ = self.decision_service.get_variation_for_feature(
self.project_config, feature, "test_user"
)
self.assertEqual(
decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT),
decision_service.Decision(
expected_experiment,
expected_variation,
enums.DecisionSources.FEATURE_TEST,
),
variation_received,
)

mock_decision.assert_called_once_with(
self.project_config,
self.project_config.get_experiment_from_key("test_experiment"),
self.project_config.get_experiment_from_key("group_exp_1"),
"test_user",
None,
False
)

def test_get_variation_for_feature__returns_none_for_invalid_group_id(self):
""" Test that get_variation_for_feature returns None for unknown group ID. """

feature = self.project_config.get_feature_from_key("test_feature_in_group")
feature.groupId = "aabbccdd"

with self.mock_decision_logger as mock_decision_service_logging:
variation_received, _ = self.decision_service.get_variation_for_feature(
self.project_config, feature, "test_user"
)
self.assertEqual(
decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT),
variation_received,
)
mock_decision_service_logging.error.assert_called_once_with(
enums.Errors.INVALID_GROUP_ID.format("_get_variation_for_feature")
)

def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_not_associated_with_feature(
self,
):
""" Test that if a user is in the mutex group but the experiment is
not targeting a feature, then None is returned. """
not targeting a feature, then None is returned. """

feature = self.project_config.get_feature_from_key("test_feature_in_group")

with mock.patch(
"optimizely.decision_service.DecisionService.get_experiment_in_group",
return_value=[self.project_config.get_experiment_from_key("group_exp_2"), []],
"optimizely.decision_service.DecisionService.get_variation",
return_value=[None, []],
) as mock_decision:
Copy link
Contributor

Choose a reason for hiding this comment

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

Same test as above.

variation_received, _ = self.decision_service.get_variation_for_feature(
self.project_config, feature, "test_user"
Expand All @@ -1438,43 +1406,19 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no
)

mock_decision.assert_called_once_with(
self.project_config, self.project_config.get_group("19228"), "test_user"
self.project_config, self.project_config.get_experiment_from_id("32222"), "test_user", None, False
)

def test_get_experiment_in_group(self):
""" Test that get_experiment_in_group returns the bucketed experiment for the user. """
def test_get_variation_for_feature__returns_none_for_invalid_group_id(self):
""" Test that get_variation_for_feature returns None for unknown group ID. """

group = self.project_config.get_group("19228")
experiment = self.project_config.get_experiment_from_id("32222")
with mock.patch(
"optimizely.bucketer.Bucketer.find_bucket", return_value="32222"
), self.mock_decision_logger as mock_decision_service_logging:
variation_received, _ = self.decision_service.get_experiment_in_group(
self.project_config, group, "test_user"
)
self.assertEqual(
experiment,
variation_received,
)
feature = self.project_config.get_feature_from_key("test_feature_in_group")
feature.groupId = "aabbccdd"

mock_decision_service_logging.info.assert_called_once_with(
'User with bucketing ID "test_user" is in experiment group_exp_1 of group 19228.'
variation_received, _ = self.decision_service.get_variation_for_feature(
self.project_config, feature, "test_user"
)

def test_get_experiment_in_group__returns_none_if_user_not_in_group(self):
""" Test that get_experiment_in_group returns None if the user is not bucketed into the group. """

group = self.project_config.get_group("19228")
with mock.patch(
"optimizely.bucketer.Bucketer.find_bucket", return_value=None
), self.mock_decision_logger as mock_decision_service_logging:
variation_received, _ = self.decision_service.get_experiment_in_group(
self.project_config, group, "test_user"
)
self.assertIsNone(
variation_received
)

mock_decision_service_logging.info.assert_called_once_with(
'User with bucketing ID "test_user" is not in any experiments of group 19228.'
self.assertEqual(
decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT),
variation_received,
)