diff --git a/optimizely/bucketer.py b/optimizely/bucketer.py index 38da3798..2328048f 100644 --- a/optimizely/bucketer.py +++ b/optimizely/bucketer.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: # prevent circular dependenacy by skipping import at runtime from .project_config import ProjectConfig - from .entities import Experiment, Variation + from .entities import Experiment, Variation, Group from .helpers.types import TrafficAllocation @@ -164,3 +164,67 @@ def bucket( decide_reasons.append(message) return None, decide_reasons + + def bucket_to_entity_id( + self, + bucketing_id: str, + experiment: Experiment, + traffic_allocations: list[TrafficAllocation], + group: Optional[Group] = None + ) -> tuple[Optional[str], list[str]]: + """ + Buckets the user and returns the entity ID (for CMAB experiments). + Args: + bucketing_id: The bucketing ID string for the user. + experiment: The experiment object (for group/groupPolicy logic if needed). + traffic_allocations: List of traffic allocation dicts (should have 'entity_id' and 'end_of_range' keys). + group: (optional) Group object for mutex group support. + + Returns: + Tuple of (entity_id or None, list of decide reasons). + """ + decide_reasons = [] + + group_id = getattr(experiment, 'groupId', None) + if group_id and group and getattr(group, 'policy', None) == 'random': + bucket_key = bucketing_id + group_id + bucket_val = self._generate_bucket_value(bucket_key) + decide_reasons.append(f'Generated group bucket value {bucket_val} for key "{bucket_key}".') + + matched = False + for allocation in group.trafficAllocation: + end_of_range = allocation['endOfRange'] + entity_id = allocation['entityId'] + if bucket_val < end_of_range: + matched = True + if entity_id != experiment.id: + decide_reasons.append( + f'User not bucketed into experiment "{experiment.id}" (got "{entity_id}").' + ) + return None, decide_reasons + decide_reasons.append( + f'User is bucketed into experiment "{experiment.id}" within group "{group_id}".' + ) + break + if not matched: + decide_reasons.append( + f'User not bucketed into any experiment in group "{group_id}".' + ) + return None, decide_reasons + + # Main experiment bucketing + bucket_key = bucketing_id + experiment.id + bucket_val = self._generate_bucket_value(bucket_key) + decide_reasons.append(f'Generated experiment bucket value {bucket_val} for key "{bucket_key}".') + + for allocation in traffic_allocations: + end_of_range = allocation['endOfRange'] + entity_id = allocation['entityId'] + if bucket_val < end_of_range: + decide_reasons.append( + f'User bucketed into entity id "{entity_id}".' + ) + return entity_id, decide_reasons + + decide_reasons.append('User not bucketed into any entity id.') + return None, decide_reasons diff --git a/optimizely/cmab/__init__.py b/optimizely/cmab/__init__.py new file mode 100644 index 00000000..2a6fc86c --- /dev/null +++ b/optimizely/cmab/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2025, Optimizely +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/optimizely/cmab/cmab_service.py b/optimizely/cmab/cmab_service.py index 418280b8..a7c4b69b 100644 --- a/optimizely/cmab/cmab_service.py +++ b/optimizely/cmab/cmab_service.py @@ -35,6 +35,18 @@ class CmabCacheValue(TypedDict): class DefaultCmabService: + """ + DefaultCmabService handles decisioning for Contextual Multi-Armed Bandit (CMAB) experiments, + including caching and filtering user attributes for efficient decision retrieval. + + Attributes: + cmab_cache: LRUCache for user CMAB decisions. + cmab_client: Client to fetch decisions from the CMAB backend. + logger: Optional logger. + + Methods: + get_decision: Retrieves a CMAB decision with caching and attribute filtering. + """ def __init__(self, cmab_cache: LRUCache[str, CmabCacheValue], cmab_client: DefaultCmabClient, logger: Optional[_logging.Logger] = None): self.cmab_cache = cmab_cache diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index df85464e..d77ba9d9 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -12,7 +12,7 @@ # limitations under the License. from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple, Optional, Sequence +from typing import TYPE_CHECKING, NamedTuple, Optional, Sequence, List, TypedDict from . import bucketer from . import entities @@ -23,28 +23,81 @@ from .helpers import validator from .optimizely_user_context import OptimizelyUserContext, UserAttributes from .user_profile import UserProfile, UserProfileService, UserProfileTracker +from .cmab.cmab_service import DefaultCmabService, CmabDecision +from optimizely.helpers.enums import Errors if TYPE_CHECKING: # prevent circular dependenacy by skipping import at runtime from .project_config import ProjectConfig from .logger import Logger + from .helpers.types import TrafficAllocation + + +class CmabDecisionResult(TypedDict): + """ + TypedDict representing the result of a CMAB (Contextual Multi-Armed Bandit) decision. + + Attributes: + error (bool): Indicates whether an error occurred during the decision process. + result (Optional[CmabDecision]): Resulting CmabDecision object if the decision was successful, otherwise None. + reasons (List[str]): A list of reasons or messages explaining the outcome or any errors encountered. + """ + error: bool + result: Optional[CmabDecision] + reasons: List[str] + + +class VariationResult(TypedDict): + """ + TypedDict representing the result of a variation decision process. + + Attributes: + cmab_uuid (Optional[str]): The unique identifier for the CMAB experiment, if applicable. + error (bool): Indicates whether an error occurred during the decision process. + reasons (List[str]): A list of reasons explaining the outcome or any errors encountered. + variation (Optional[entities.Variation]): The selected variation entity, or None if no variation was assigned. + """ + cmab_uuid: Optional[str] + error: bool + reasons: List[str] + variation: Optional[entities.Variation] + + +class DecisionResult(TypedDict): + """ + A TypedDict representing the result of a decision process. + + Attributes: + decision (Decision): The decision object containing the outcome of the evaluation. + error (bool): Indicates whether an error occurred during the decision process. + reasons (List[str]): A list of reasons explaining the decision or any errors encountered. + """ + decision: Decision + error: bool + reasons: List[str] class Decision(NamedTuple): - """Named tuple containing selected experiment, variation and source. + """Named tuple containing selected experiment, variation, source and cmab_uuid. None if no experiment/variation was selected.""" experiment: Optional[entities.Experiment] variation: Optional[entities.Variation] source: Optional[str] + cmab_uuid: Optional[str] class DecisionService: """ Class encapsulating all decision related capabilities. """ - def __init__(self, logger: Logger, user_profile_service: Optional[UserProfileService]): + def __init__(self, + logger: Logger, + user_profile_service: Optional[UserProfileService], + cmab_service: DefaultCmabService): self.bucketer = bucketer.Bucketer() self.logger = logger self.user_profile_service = user_profile_service + self.cmab_service = cmab_service + self.cmab_uuid = None # Map of user IDs to another map of experiments to variations. # This contains all the forced variations set by the user @@ -76,6 +129,51 @@ def _get_bucketing_id(self, user_id: str, attributes: Optional[UserAttributes]) return user_id, decide_reasons + def _get_decision_for_cmab_experiment( + self, + project_config: ProjectConfig, + experiment: entities.Experiment, + user_context: OptimizelyUserContext, + options: Optional[Sequence[str]] = None + ) -> CmabDecisionResult: + """ + Retrieves a decision for a contextual multi-armed bandit (CMAB) experiment. + + Args: + project_config: Instance of ProjectConfig. + experiment: The experiment object for which the decision is to be made. + user_context: The user context containing user id and attributes. + options: Optional sequence of decide options. + + Returns: + A dictionary containing: + - "error": Boolean indicating if there was an error. + - "result": The CmabDecision result or empty dict if error. + - "reasons": List of strings with reasons or error messages. + """ + try: + options_list = list(options) if options is not None else [] + cmab_decision = self.cmab_service.get_decision( + project_config, user_context, experiment.id, options_list + ) + return { + "error": False, + "result": cmab_decision, + "reasons": [], + } + except Exception as e: + error_message = Errors.CMAB_FETCH_FAILED_DETAILED.format( + experiment.key, + str(e) + ) + if self.logger: + self.logger.error(error_message) + return { + "error": True, + "result": None, + "reasons": [error_message], + } + def set_forced_variation( self, project_config: ProjectConfig, experiment_key: str, user_id: str, variation_key: Optional[str] @@ -250,29 +348,38 @@ def get_variation( user_profile_tracker: Optional[UserProfileTracker], reasons: list[str] = [], options: Optional[Sequence[str]] = None - ) -> tuple[Optional[entities.Variation], list[str]]: - """ Top-level function to help determine variation user should be put in. - - First, check if experiment is running. - Second, check if user is forced in a variation. - Third, check if there is a stored decision for the user and return the corresponding variation. - Fourth, figure out if user is in the experiment by evaluating audience conditions if any. - Fifth, bucket the user and return the variation. + ) -> VariationResult: + """ + Determines the variation a user should be assigned to for a given experiment. + + The decision process is as follows: + 1. Check if the experiment is running. + 2. Check if the user is forced into a variation via the forced variation map. + 3. Check if the user is whitelisted into a variation for the experiment. + 4. If user profile tracking is enabled and not ignored, check for a stored variation. + 5. Evaluate audience conditions to determine if the user qualifies for the experiment. + 6. For CMAB experiments: + a. Check if the user is in the CMAB traffic allocation. + b. If so, fetch the CMAB decision and assign the corresponding variation and cmab_uuid. + 7. For non-CMAB experiments, bucket the user into a variation. + 8. If a variation is assigned, optionally update the user profile. + 9. Return the assigned variation, decision reasons, and cmab_uuid (if applicable). Args: - project_config: Instance of ProjectConfig. - experiment: Experiment for which user variation needs to be determined. - user_context: contains user id and attributes. - user_profile_tracker: tracker for reading and updating user profile of the user. - reasons: Decision reasons. - options: Decide options. + project_config: Instance of ProjectConfig. + experiment: Experiment for which the user's variation needs to be determined. + user_context: Contains user id and attributes. + user_profile_tracker: Tracker for reading and updating the user's profile. + reasons: List of decision reasons. + options: Decide options. Returns: - Variation user should see. None if user is not in experiment or experiment is not running - And an array of log messages representing decision making. + A tuple of: + - The assigned Variation (or None if not assigned). + - A list of log messages representing decision making. + - The cmab_uuid if the experiment is a CMAB experiment, otherwise None. """ user_id = user_context.user_id - if options: ignore_user_profile = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in options else: @@ -286,20 +393,35 @@ def get_variation( message = f'Experiment "{experiment.key}" is not running.' self.logger.info(message) decide_reasons.append(message) - return None, decide_reasons + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': None + } # Check if the user is forced into a variation variation: Optional[entities.Variation] variation, reasons_received = self.get_forced_variation(project_config, experiment.key, user_id) decide_reasons += reasons_received if variation: - return variation, decide_reasons + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': variation + } # Check to see if user is white-listed for a certain variation variation, reasons_received = self.get_whitelisted_variation(project_config, experiment, user_id) decide_reasons += reasons_received if variation: - return variation, decide_reasons + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': variation + } # Check to see if user has a decision available for the given experiment if user_profile_tracker is not None and not ignore_user_profile: @@ -309,11 +431,16 @@ def get_variation( f'"{experiment}" for user "{user_id}" from user profile.' self.logger.info(message) decide_reasons.append(message) - return variation, decide_reasons + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': variation + } else: self.logger.warning('User profile has invalid format.') - # Bucket user and store the new decision + # Check audience conditions audience_conditions = experiment.get_audience_conditions_or_ids() user_meets_audience_conditions, reasons_received = audience_helper.does_user_meet_audience_conditions( project_config, audience_conditions, @@ -325,13 +452,70 @@ def get_variation( message = f'User "{user_id}" does not meet conditions to be in experiment "{experiment.key}".' self.logger.info(message) decide_reasons.append(message) - return None, decide_reasons + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': None + } # Determine bucketing ID to be used bucketing_id, bucketing_id_reasons = self._get_bucketing_id(user_id, user_context.get_user_attributes()) decide_reasons += bucketing_id_reasons - variation, bucket_reasons = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) - decide_reasons += bucket_reasons + cmab_uuid = None + + # Check if this is a CMAB experiment + # If so, handle CMAB-specific traffic allocation and decision logic. + # Otherwise, proceed with standard bucketing logic for non-CMAB experiments. + if experiment.cmab: + CMAB_DUMMY_ENTITY_ID = "$" + # Build the CMAB-specific traffic allocation + cmab_traffic_allocation: list[TrafficAllocation] = [{ + "entityId": CMAB_DUMMY_ENTITY_ID, + "endOfRange": experiment.cmab['trafficAllocation'] + }] + + # Check if user is in CMAB traffic allocation + group = None + if experiment.groupId: + group = project_config.get_group(group_id=experiment.groupId) + bucketed_entity_id, bucket_reasons = self.bucketer.bucket_to_entity_id( + bucketing_id, experiment, cmab_traffic_allocation, group + ) + decide_reasons += bucket_reasons + if bucketed_entity_id != CMAB_DUMMY_ENTITY_ID: + message = f'User "{user_id}" not in CMAB experiment "{experiment.key}" due to traffic allocation.' + self.logger.info(message) + decide_reasons.append(message) + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': None + } + + # User is in CMAB allocation, proceed to CMAB decision + cmab_decision_result = self._get_decision_for_cmab_experiment(project_config, + experiment, + user_context, + options) + decide_reasons += cmab_decision_result.get('reasons', []) + cmab_decision = cmab_decision_result.get('result') + if not cmab_decision or cmab_decision_result['error']: + return { + 'cmab_uuid': None, + 'error': True, + 'reasons': decide_reasons, + 'variation': None + } + variation_id = cmab_decision['variation_id'] + cmab_uuid = cmab_decision['cmab_uuid'] + variation = project_config.get_variation_from_id(experiment_key=experiment.key, variation_id=variation_id) + else: + # Bucket the user + variation, bucket_reasons = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) + decide_reasons += bucket_reasons + if isinstance(variation, entities.Variation): message = f'User "{user_id}" is in variation "{variation.key}" of experiment {experiment.key}.' self.logger.info(message) @@ -342,11 +526,21 @@ def get_variation( user_profile_tracker.update_user_profile(experiment, variation) except: self.logger.exception(f'Unable to save user profile for user "{user_id}".') - return variation, decide_reasons + return { + 'cmab_uuid': cmab_uuid, + 'error': False, + 'reasons': decide_reasons, + 'variation': variation + } message = f'User "{user_id}" is in no variation.' self.logger.info(message) decide_reasons.append(message) - return None, decide_reasons + return { + 'cmab_uuid': None, + 'error': False, + 'reasons': decide_reasons, + 'variation': None + } def get_variation_for_rollout( self, project_config: ProjectConfig, feature: entities.FeatureFlag, user_context: OptimizelyUserContext @@ -370,7 +564,7 @@ def get_variation_for_rollout( attributes = user_context.get_user_attributes() if not feature or not feature.rolloutId: - return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + return Decision(None, None, enums.DecisionSources.ROLLOUT, None), decide_reasons rollout = project_config.get_rollout_from_id(feature.rolloutId) @@ -378,7 +572,7 @@ def get_variation_for_rollout( message = f'There is no rollout of feature {feature.key}.' self.logger.debug(message) decide_reasons.append(message) - return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + return Decision(None, None, enums.DecisionSources.ROLLOUT, None), decide_reasons rollout_rules = project_config.get_rollout_experiments(rollout) @@ -386,7 +580,7 @@ def get_variation_for_rollout( message = f'Rollout {rollout.id} has no experiments.' self.logger.debug(message) decide_reasons.append(message) - return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + return Decision(None, None, enums.DecisionSources.ROLLOUT, None), decide_reasons index = 0 while index < len(rollout_rules): @@ -401,7 +595,7 @@ def get_variation_for_rollout( if forced_decision_variation: return Decision(experiment=rule, variation=forced_decision_variation, - source=enums.DecisionSources.ROLLOUT), decide_reasons + source=enums.DecisionSources.ROLLOUT, cmab_uuid=None), decide_reasons bucketing_id, bucket_reasons = self._get_bucketing_id(user_id, attributes) decide_reasons += bucket_reasons @@ -435,7 +629,7 @@ def get_variation_for_rollout( self.logger.debug(message) decide_reasons.append(message) return Decision(experiment=rule, variation=bucketed_variation, - source=enums.DecisionSources.ROLLOUT), decide_reasons + source=enums.DecisionSources.ROLLOUT, cmab_uuid=None), decide_reasons elif not everyone_else: # skip this logging for EveryoneElse since this has a message not for everyone_else @@ -455,7 +649,7 @@ def get_variation_for_rollout( # the last rule is special for "Everyone Else" index = len(rollout_rules) - 1 if skip_to_everyone_else else index + 1 - return Decision(None, None, enums.DecisionSources.ROLLOUT), decide_reasons + return Decision(None, None, enums.DecisionSources.ROLLOUT, None), decide_reasons def get_variation_for_feature( self, @@ -591,10 +785,14 @@ def get_variations_for_feature_list( if forced_decision_variation: decision_variation = forced_decision_variation + cmab_uuid = None else: - decision_variation, variation_reasons = self.get_variation( + variation_result = self.get_variation( project_config, experiment, user_context, user_profile_tracker, feature_reasons, options ) + cmab_uuid = variation_result['cmab_uuid'] + variation_reasons = variation_result['reasons'] + decision_variation = variation_result['variation'] feature_reasons.extend(variation_reasons) if decision_variation: @@ -602,7 +800,8 @@ def get_variations_for_feature_list( f'User "{user_context.user_id}" ' f'bucketed into experiment "{experiment.key}" of feature "{feature.key}".' ) - decision = Decision(experiment, decision_variation, enums.DecisionSources.FEATURE_TEST) + decision = Decision(experiment, decision_variation, + enums.DecisionSources.FEATURE_TEST, cmab_uuid) decisions.append((decision, feature_reasons)) experiment_decision_found = True # Mark that a decision was found break # Stop after the first successful experiment decision diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index 2d6febab..f45c7a3b 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -128,7 +128,8 @@ class Errors: ODP_INVALID_ACTION: Final = 'ODP action is not valid (cannot be empty).' MISSING_SDK_KEY: Final = 'SDK key not provided/cannot be found in the datafile.' CMAB_FETCH_FAILED: Final = 'CMAB decision fetch failed with status: {}' - INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response' + INVALID_CMAB_FETCH_RESPONSE: Final = 'Invalid CMAB fetch response' + CMAB_FETCH_FAILED_DETAILED: Final = 'Failed to fetch CMAB decision for experiment key "{}" - {}' class ForcedDecisionLogs: diff --git a/optimizely/helpers/event_tag_utils.py b/optimizely/helpers/event_tag_utils.py index 0efbafb7..cb577950 100644 --- a/optimizely/helpers/event_tag_utils.py +++ b/optimizely/helpers/event_tag_utils.py @@ -81,7 +81,7 @@ def get_numeric_value(event_tags: Optional[EventTags], logger: Optional[Logger] """ logger_message_debug = None - numeric_metric_value = None + numeric_metric_value: Optional[float] = None if event_tags is None: return numeric_metric_value @@ -141,4 +141,4 @@ def get_numeric_value(event_tags: Optional[EventTags], logger: Optional[Logger] ' is in an invalid format and will not be sent to results.' ) - return numeric_metric_value # type: ignore[no-any-return] + return numeric_metric_value diff --git a/optimizely/lib/pymmh3.py b/optimizely/lib/pymmh3.py index b37bf944..7a8ca179 100755 --- a/optimizely/lib/pymmh3.py +++ b/optimizely/lib/pymmh3.py @@ -399,7 +399,7 @@ def fmix(h: int) -> int: return h4 << 96 | h3 << 64 | h2 << 32 | h1 - key = bytearray(xencode(key)) + key = bytes(xencode(key)) if x64arch: return hash128_x64(key, seed) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index af442224..02805063 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -44,12 +44,18 @@ from .optimizely_config import OptimizelyConfig, OptimizelyConfigService from .optimizely_user_context import OptimizelyUserContext, UserAttributes from .project_config import ProjectConfig +from .cmab.cmab_client import DefaultCmabClient, CmabRetryConfig +from .cmab.cmab_service import DefaultCmabService, CmabCacheValue if TYPE_CHECKING: # prevent circular dependency by skipping import at runtime from .user_profile import UserProfileService from .helpers.event_tag_utils import EventTags +# Default constants for CMAB cache +DEFAULT_CMAB_CACHE_TIMEOUT = 30 * 60 * 1000 # 30 minutes in milliseconds +DEFAULT_CMAB_CACHE_SIZE = 1000 + class Optimizely: """ Class encapsulating all SDK functionality. """ @@ -69,7 +75,7 @@ def __init__( datafile_access_token: Optional[str] = None, default_decide_options: Optional[list[str]] = None, event_processor_options: Optional[dict[str, Any]] = None, - settings: Optional[OptimizelySdkSettings] = None + settings: Optional[OptimizelySdkSettings] = None, ) -> None: """ Optimizely init method for managing Custom projects. @@ -169,7 +175,19 @@ def __init__( self._setup_odp(self.config_manager.get_sdk_key()) self.event_builder = event_builder.EventBuilder() - self.decision_service = decision_service.DecisionService(self.logger, user_profile_service) + + # Initialize CMAB components + self.cmab_client = DefaultCmabClient( + retry_config=CmabRetryConfig(), + logger=self.logger + ) + self.cmab_cache: LRUCache[str, CmabCacheValue] = LRUCache(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT) + self.cmab_service = DefaultCmabService( + cmab_cache=self.cmab_cache, + cmab_client=self.cmab_client, + logger=self.logger + ) + self.decision_service = decision_service.DecisionService(self.logger, user_profile_service, self.cmab_service) self.user_profile_service = user_profile_service def _validate_instantiation_options(self) -> None: @@ -634,10 +652,9 @@ def get_variation( user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False) user_profile_tracker = user_profile.UserProfileTracker(user_id, self.user_profile_service, self.logger) user_profile_tracker.load_user_profile() - variation, _ = self.decision_service.get_variation(project_config, - experiment, - user_context, - user_profile_tracker) + variation_result = self.decision_service.get_variation(project_config, experiment, + user_context, user_profile_tracker) + variation = variation_result['variation'] user_profile_tracker.save_user_profile() if variation: variation_key = variation.key @@ -1338,7 +1355,7 @@ def _decide_for_keys( decision_reasons_dict[key] += decision_reasons if variation: - decision = Decision(None, variation, enums.DecisionSources.FEATURE_TEST) + decision = Decision(None, variation, enums.DecisionSources.FEATURE_TEST, None) flag_decisions[key] = decision else: flags_without_forced_decision.append(feature_flag) diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 6c5862a5..06b77f1e 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -457,9 +457,10 @@ def test_get_variation__experiment_not_running(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: - variation, _ = self.decision_service.get_variation( + variation_result = self.decision_service.get_variation( self.project_config, experiment, user, None ) + variation = variation_result['variation'] self.assertIsNone( variation ) @@ -500,7 +501,7 @@ def test_get_variation__bucketing_id_provided(self): "optimizely.bucketer.Bucketer.bucket", return_value=[self.project_config.get_variation_from_id("211127", "211129"), []], ) as mock_bucket: - variation, _ = self.decision_service.get_variation( + _ = self.decision_service.get_variation( self.project_config, experiment, user, @@ -535,9 +536,9 @@ def test_get_variation__user_whitelisted_for_variation(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: - variation, _ = self.decision_service.get_variation( + variation = self.decision_service.get_variation( self.project_config, experiment, user, user_profile_tracker - ) + )['variation'] self.assertEqual( entities.Variation("111128", "control"), variation, @@ -573,9 +574,9 @@ def test_get_variation__user_has_stored_decision(self): ) as mock_audience_check, mock.patch( "optimizely.bucketer.Bucketer.bucket" ) as mock_bucket: - variation, _ = self.decision_service.get_variation( + variation = self.decision_service.get_variation( self.project_config, experiment, user, user_profile_tracker - ) + )['variation'] self.assertEqual( entities.Variation("111128", "control"), variation, @@ -619,9 +620,9 @@ def test_get_variation__user_bucketed_for_new_experiment__user_profile_tracker_a "optimizely.bucketer.Bucketer.bucket", return_value=[entities.Variation("111129", "variation"), []], ) as mock_bucket: - variation, _ = self.decision_service.get_variation( + variation = self.decision_service.get_variation( self.project_config, experiment, user, user_profile_tracker - ) + )['variation'] self.assertEqual( entities.Variation("111129", "variation"), variation, @@ -669,9 +670,9 @@ def test_get_variation__user_does_not_meet_audience_conditions(self): ) as mock_bucket, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: - variation, _ = self.decision_service.get_variation( + variation = self.decision_service.get_variation( self.project_config, experiment, user, user_profile_tracker - ) + )['variation'] self.assertIsNone( variation ) @@ -719,14 +720,14 @@ def test_get_variation__ignore_user_profile_when_specified(self): ) as mock_lookup, mock.patch( "optimizely.user_profile.UserProfileService.save" ) as mock_save: - variation, _ = self.decision_service.get_variation( + variation = self.decision_service.get_variation( self.project_config, experiment, user, user_profile_tracker, [], options=['IGNORE_USER_PROFILE_SERVICE'], - ) + )['variation'] self.assertEqual( entities.Variation("111129", "variation"), variation, @@ -750,6 +751,367 @@ def test_get_variation__ignore_user_profile_when_specified(self): self.assertEqual(0, mock_lookup.call_count) self.assertEqual(0, mock_save.call_count) + def test_get_variation_cmab_experiment_user_in_traffic_allocation(self): + """Test get_variation with CMAB experiment where user is in traffic allocation.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {}, + [ + entities.Variation('111151', 'variation_1'), + entities.Variation('111152', 'variation_2') + ], + [ + {'entityId': '111151', 'endOfRange': 5000}, + {'entityId': '111152', 'endOfRange': 10000} + ], + cmab={'trafficAllocation': 5000} + ) + + cmab_decision = { + 'variation_id': '111151', + 'cmab_uuid': 'test-cmab-uuid-123' + } + + with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id', return_value=['$', []]), \ + mock.patch('optimizely.decision_service.DecisionService._get_decision_for_cmab_experiment', + return_value={'error': False, 'result': cmab_decision, 'reasons': []}), \ + mock.patch.object(self.project_config, 'get_variation_from_id', + return_value=entities.Variation('111151', 'variation_1')), \ + mock.patch.object(self.decision_service, + 'logger') as mock_logger: + + # Call get_variation with the CMAB experiment + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + None + ) + cmab_uuid = variation_result['cmab_uuid'] + variation = variation_result['variation'] + error = variation_result['error'] + reasons = variation_result['reasons'] + + # Verify the variation and cmab_uuid + self.assertEqual(entities.Variation('111151', 'variation_1'), variation) + self.assertEqual('test-cmab-uuid-123', cmab_uuid) + self.assertStrictFalse(error) + self.assertIn('User "test_user" is in variation "variation_1" of experiment cmab_experiment.', reasons) + + # Verify logger was called + mock_logger.info.assert_any_call('User "test_user" is in variation ' + '"variation_1" of experiment cmab_experiment.') + + def test_get_variation_cmab_experiment_user_not_in_traffic_allocation(self): + """Test get_variation with CMAB experiment where user is not in traffic allocation.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {}, + [entities.Variation('111151', 'variation_1')], + [{'entityId': '111151', 'endOfRange': 10000}], + cmab={'trafficAllocation': 5000} + ) + + with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id', return_value=['not_in_allocation', []]), \ + mock.patch('optimizely.decision_service.DecisionService._get_decision_for_cmab_experiment' + ) as mock_cmab_decision, \ + mock.patch.object(self.decision_service, + 'logger') as mock_logger: + + # Call get_variation with the CMAB experiment + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + None + ) + variation = variation_result['variation'] + cmab_uuid = variation_result['cmab_uuid'] + error = variation_result['error'] + reasons = variation_result['reasons'] + + # Verify we get no variation and CMAB service wasn't called + self.assertIsNone(variation) + self.assertIsNone(cmab_uuid) + self.assertStrictFalse(error) + self.assertIn('User "test_user" not in CMAB experiment "cmab_experiment" due to traffic allocation.', + reasons) + mock_cmab_decision.assert_not_called() + + # Verify logger was called + mock_logger.info.assert_any_call('User "test_user" not in CMAB ' + 'experiment "cmab_experiment" due to traffic allocation.') + + def test_get_variation_cmab_experiment_service_error(self): + """Test get_variation with CMAB experiment when the CMAB service returns an error.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {}, + [entities.Variation('111151', 'variation_1')], + [{'entityId': '111151', 'endOfRange': 10000}], + cmab={'trafficAllocation': 5000} + ) + + with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id', return_value=['$', []]), \ + mock.patch('optimizely.decision_service.DecisionService._get_decision_for_cmab_experiment', + return_value={'error': True, 'result': None, 'reasons': ['CMAB service error']}): + + # Call get_variation with the CMAB experiment + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + None + ) + variation = variation_result['variation'] + cmab_uuid = variation_result['cmab_uuid'] + reasons = variation_result['reasons'] + error = variation_result['error'] + + # Verify we get no variation due to CMAB service error + self.assertIsNone(variation) + self.assertIsNone(cmab_uuid) + self.assertIn('CMAB service error', reasons) + self.assertStrictTrue(error) + + def test_get_variation_cmab_experiment_deep_mock_500_error(self): + """Test the full flow of a CMAB experiment with a 500 error from the HTTP request layer.""" + import requests + from optimizely.exceptions import CmabFetchError + from optimizely.helpers.enums import Errors + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {}, + [entities.Variation('111151', 'variation_1')], + [{'entityId': '111151', 'endOfRange': 10000}], + cmab={'trafficAllocation': 5000} + ) + + # Define HTTP error details + http_error = requests.exceptions.HTTPError("500 Server Error") + error_message = Errors.CMAB_FETCH_FAILED.format(http_error) + detailed_error_message = Errors.CMAB_FETCH_FAILED_DETAILED.format(cmab_experiment.key, error_message) + + # Set up mocks for the entire call chain + with mock.patch('optimizely.helpers.experiment.is_experiment_running', return_value=True), \ + mock.patch('optimizely.helpers.audience.does_user_meet_audience_conditions', return_value=[True, []]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id', return_value=['$', []]), \ + mock.patch.object(self.decision_service.cmab_service, 'get_decision', + side_effect=lambda *args, + **kwargs: self.decision_service.cmab_service._fetch_decision(*args, **kwargs)), \ + mock.patch.object(self.decision_service.cmab_service, '_fetch_decision', + side_effect=lambda *args, + **kwargs: self. + decision_service.cmab_service.cmab_client.fetch_decision(*args, **kwargs)), \ + mock.patch.object(self.decision_service.cmab_service.cmab_client, 'fetch_decision', + side_effect=lambda *args, + **kwargs: self.decision_service.cmab_service.cmab_client._do_fetch(*args, **kwargs)), \ + mock.patch.object(self.decision_service.cmab_service.cmab_client, '_do_fetch', + side_effect=CmabFetchError(error_message)), \ + mock.patch.object(self.decision_service, 'logger') as mock_logger: + # Call get_variation with the CMAB experiment + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + None + ) + variation = variation_result['variation'] + cmab_uuid = variation_result['cmab_uuid'] + reasons = variation_result['reasons'] + + # Verify we get no variation due to CMAB service error + self.assertIsNone(variation) + self.assertIsNone(cmab_uuid) + self.assertIn(detailed_error_message, reasons) + + # Verify logger was called with the specific 500 error + mock_logger.error.assert_any_call(detailed_error_message) + + def test_get_variation_cmab_experiment_forced_variation(self): + """Test get_variation with CMAB experiment when user has a forced variation.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {}, + [ + entities.Variation('111151', 'variation_1'), + entities.Variation('111152', 'variation_2') + ], + [ + {'entityId': '111151', 'endOfRange': 5000}, + {'entityId': '111152', 'endOfRange': 10000} + ], + cmab={'trafficAllocation': 5000} + ) + + forced_variation = entities.Variation('111152', 'variation_2') + + with mock.patch('optimizely.decision_service.DecisionService.get_forced_variation', + return_value=[forced_variation, ['User is forced into variation']]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id') as mock_bucket, \ + mock.patch('optimizely.decision_service.DecisionService._get_decision_for_cmab_experiment' + ) as mock_cmab_decision: + + # Call get_variation with the CMAB experiment + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + None + ) + variation = variation_result['variation'] + reasons = variation_result['reasons'] + cmab_uuid = variation_result['cmab_uuid'] + error = variation_result['error'] + + # Verify we get the forced variation + self.assertEqual(forced_variation, variation) + self.assertIsNone(cmab_uuid) + self.assertIn('User is forced into variation', reasons) + self.assertStrictFalse(error) + + # Verify CMAB-specific methods weren't called + mock_bucket.assert_not_called() + mock_cmab_decision.assert_not_called() + + def test_get_variation_cmab_experiment_with_whitelisted_variation(self): + """Test get_variation with CMAB experiment when user has a whitelisted variation.""" + + # Create a user context + user = optimizely_user_context.OptimizelyUserContext( + optimizely_client=None, + logger=None, + user_id="test_user", + user_attributes={} + ) + + # Create a CMAB experiment with forced variations + cmab_experiment = entities.Experiment( + '111150', + 'cmab_experiment', + 'Running', + '111150', + [], # No audience IDs + {'test_user': 'variation_2'}, + [ + entities.Variation('111151', 'variation_1'), + entities.Variation('111152', 'variation_2') + ], + [ + {'entityId': '111151', 'endOfRange': 5000}, + {'entityId': '111152', 'endOfRange': 10000} + ], + cmab={'trafficAllocation': 5000} + ) + + whitelisted_variation = entities.Variation('111152', 'variation_2') + + with mock.patch('optimizely.decision_service.DecisionService.get_forced_variation', + return_value=[None, []]), \ + mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', + return_value=[whitelisted_variation, ['User is whitelisted into variation']]), \ + mock.patch('optimizely.bucketer.Bucketer.bucket_to_entity_id') as mock_bucket, \ + mock.patch('optimizely.decision_service.DecisionService._get_decision_for_cmab_experiment' + ) as mock_cmab_decision: + + # Call get_variation with the CMAB experiment + variation_result = self.decision_service.get_variation( + self.project_config, + cmab_experiment, + user, + None + ) + variation = variation_result['variation'] + cmab_uuid = variation_result['cmab_uuid'] + reasons = variation_result['reasons'] + error = variation_result['error'] + + # Verify we get the whitelisted variation + self.assertEqual(whitelisted_variation, variation) + self.assertIsNone(cmab_uuid) + self.assertIn('User is whitelisted into variation', reasons) + self.assertStrictFalse(error) + + # Verify CMAB-specific methods weren't called + mock_bucket.assert_not_called() + mock_cmab_decision.assert_not_called() + class FeatureFlagDecisionTests(base.BaseTest): def setUp(self): @@ -779,7 +1141,7 @@ def test_get_variation_for_rollout__returns_none_if_no_experiments(self): ) self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), variation_received, ) @@ -810,6 +1172,7 @@ def test_get_variation_for_rollout__returns_decision_if_user_in_rollout(self): self.project_config.get_experiment_from_id("211127"), self.project_config.get_variation_from_id("211127", "211129"), enums.DecisionSources.ROLLOUT, + None ), variation_received, ) @@ -852,6 +1215,7 @@ def test_get_variation_for_rollout__calls_bucket_with_bucketing_id(self): self.project_config.get_experiment_from_id("211127"), self.project_config.get_variation_from_id("211127", "211129"), enums.DecisionSources.ROLLOUT, + None ), variation_received, ) @@ -892,7 +1256,7 @@ def test_get_variation_for_rollout__skips_to_everyone_else_rule(self): ) self.assertEqual( decision_service.Decision( - everyone_else_exp, variation_to_mock, enums.DecisionSources.ROLLOUT + everyone_else_exp, variation_to_mock, enums.DecisionSources.ROLLOUT, None ), variation_received, ) @@ -946,7 +1310,7 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self): self.project_config, feature, user ) self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), variation_received, ) @@ -1013,7 +1377,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( ) decision_patch = mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=[expected_variation, []], + return_value={'variation': expected_variation, 'cmab_uuid': None, 'reasons': [], 'error': False}, ) with decision_patch as mock_decision, self.mock_decision_logger: variation_received, _ = self.decision_service.get_variation_for_feature( @@ -1024,6 +1388,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1104,6 +1469,7 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_ expected_experiment, expected_variation, enums.DecisionSources.ROLLOUT, + None ), decision, ) @@ -1143,7 +1509,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) ) with mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=(expected_variation, []), + return_value={'variation': expected_variation, 'cmab_uuid': None, 'reasons': [], 'error': False}, ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( self.project_config, feature, user, options=None @@ -1153,6 +1519,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self) expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1177,13 +1544,13 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self with mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=[None, []], + return_value={'variation': None, 'cmab_uuid': None, 'reasons': [], 'error': False}, ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( self.project_config, feature, user ) self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), variation_received, ) @@ -1209,13 +1576,13 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no feature = self.project_config.get_feature_from_key("test_feature_in_group") with mock.patch( "optimizely.decision_service.DecisionService.get_variation", - return_value=[None, []], + return_value={'variation': None, 'cmab_uuid': None, 'reasons': [], 'error': False}, ) as mock_decision: variation_received, _ = self.decision_service.get_variation_for_feature( self.project_config, feature, user, False ) self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), variation_received, ) @@ -1249,6 +1616,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1283,6 +1651,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1317,6 +1686,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_mutex_group expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1346,6 +1716,7 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group None, None, enums.DecisionSources.ROLLOUT, + None ), variation_received, ) @@ -1380,6 +1751,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_ expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1412,6 +1784,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_ expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1445,6 +1818,7 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment_ expected_experiment, expected_variation, enums.DecisionSources.FEATURE_TEST, + None ), variation_received, ) @@ -1473,6 +1847,7 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_experiment_ None, None, enums.DecisionSources.ROLLOUT, + None ), variation_received, ) @@ -1507,6 +1882,7 @@ def test_get_variation_for_feature__returns_variation_for_rollout_in_mutex_group expected_experiment, expected_variation, enums.DecisionSources.ROLLOUT, + None ), variation_received, ) @@ -1538,18 +1914,12 @@ def test_get_variation_for_feature_returns_rollout_in_experiment_bucket_range_25 variation_received, _ = self.decision_service.get_variation_for_feature( self.project_config, feature, user ) - print(f"variation received is: {variation_received}") - x = decision_service.Decision( - expected_experiment, - expected_variation, - enums.DecisionSources.ROLLOUT, - ) - print(f"need to be:{x}") self.assertEqual( decision_service.Decision( expected_experiment, expected_variation, enums.DecisionSources.ROLLOUT, + None ), variation_received, ) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 1f4293cd..d198d3b2 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -319,10 +319,15 @@ def test_invalid_json_raises_schema_validation_off(self): def test_activate(self): """ Test that activate calls process with right params and returns expected variation. """ - + variation_result = { + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129'), + 'cmab_uuid': None, + 'reasons': [], + 'error': False + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ) as mock_decision, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -402,9 +407,15 @@ def on_activate(experiment, user_id, attributes, variation, event): notification_id = self.optimizely.notification_center.add_notification_listener( enums.NotificationTypes.ACTIVATE, on_activate ) + variation_result = { + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129'), + 'reasons': [], + 'cmab_uuid': None, + 'error': False + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) @@ -462,11 +473,15 @@ def on_activate(event_key, user_id, attributes, event_tags, event): pass self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) - variation = (self.project_config.get_variation_from_id('test_experiment', '111129'), []) - + variation_result = { + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129'), + 'cmab_uuid': None, + 'reasons': [], + 'error': False + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=variation, + return_value=variation_result, ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: @@ -483,7 +498,7 @@ def on_activate(event_key, user_id, attributes, event_tags, event): 'ab-test', 'test_user', {}, - {'experiment_key': 'test_experiment', 'variation_key': variation[0].key}, + {'experiment_key': 'test_experiment', 'variation_key': variation_result['variation'].key}, ), mock.call( enums.NotificationTypes.ACTIVATE, @@ -503,11 +518,15 @@ def on_activate(event_key, user_id, attributes, event_tags, event): pass self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) - variation = (self.project_config.get_variation_from_id('test_experiment', '111129'), []) - + variation_result = { + 'cmab_uuid': None, + 'reasons': [], + 'error': False, + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129') + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=variation, + return_value=variation_result, ), mock.patch('optimizely.event.event_processor.BatchEventProcessor.process') as mock_process, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: @@ -526,7 +545,7 @@ def on_activate(event_key, user_id, attributes, event_tags, event): 'ab-test', 'test_user', {'test_attribute': 'test_value'}, - {'experiment_key': 'test_experiment', 'variation_key': variation[0].key}, + {'experiment_key': 'test_experiment', 'variation_key': variation_result['variation'].key}, ), mock.call( enums.NotificationTypes.ACTIVATE, @@ -552,9 +571,14 @@ def on_activate(event_key, user_id, attributes, event_tags, event): def test_decision_listener__user_not_in_experiment(self): """ Test that activate calls broadcast decision with variation_key 'None' \ when user not in experiment. """ - + variation_result = { + 'variation': None, + 'error': False, + 'cmab_uuid': None, + 'reasons': [] + } with mock.patch('optimizely.decision_service.DecisionService.get_variation', - return_value=(None, []), ), mock.patch( + return_value=variation_result), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' @@ -671,7 +695,8 @@ def on_activate(experiment, user_id, attributes, variation, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=( - decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST), []), + decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST, None), []), ) as mock_decision, mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): self.assertTrue(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) @@ -699,7 +724,7 @@ def on_activate(experiment, user_id, attributes, variation, event): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process: @@ -715,10 +740,15 @@ def on_activate(experiment, user_id, attributes, variation, event): def test_activate__with_attributes__audience_match(self): """ Test that activate calls process with right params and returns expected variation when attributes are provided and audience conditions are met. """ - + variation_result = { + 'cmab_uuid': None, + 'reasons': [], + 'error': False, + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129') + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -1060,10 +1090,15 @@ def test_activate__with_attributes__audience_match__forced_bucketing(self): def test_activate__with_attributes__audience_match__bucketing_id_provided(self): """ Test that activate calls process with right params and returns expected variation when attributes (including bucketing ID) are provided and audience conditions are met. """ - + variation_result = { + 'cmab_uuid': None, + 'error': False, + 'reasons': [], + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129') + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ) as mock_get_variation, mock.patch('time.time', return_value=42), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ), mock.patch( @@ -1799,10 +1834,15 @@ def test_track__invalid_user_id(self): def test_get_variation(self): """ Test that get_variation returns valid variation and broadcasts decision with proper parameters. """ - + variation_result = { + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129'), + 'reasons': [], + 'error': False, + 'cmab_uuid': None + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: variation = self.optimizely.get_variation('test_experiment', 'test_user') self.assertEqual( @@ -1821,10 +1861,15 @@ def test_get_variation(self): def test_get_variation_lookup_and_save_is_called(self): """ Test that lookup is called, get_variation returns valid variation and then save is called""" - + variation_result = { + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129'), + 'cmab_uuid': None, + 'reasons': [], + 'error': False + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast, mock.patch( @@ -1854,10 +1899,15 @@ def test_get_variation_with_experiment_in_feature(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) project_config = opt_obj.config_manager.get_config() - + variation_result = { + 'error': False, + 'reasons': [], + 'variation': project_config.get_variation_from_id('test_experiment', '111129'), + 'cmab_uuid': None + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: variation = opt_obj.get_variation('test_experiment', 'test_user') self.assertEqual('variation', variation) @@ -1874,9 +1924,14 @@ def test_get_variation_with_experiment_in_feature(self): def test_get_variation__returns_none(self): """ Test that get_variation returns no variation and broadcasts decision with proper parameters. """ - + variation_result = { + 'variation': None, + 'reasons': [], + 'cmab_uuid': None, + 'error': False + } with mock.patch('optimizely.decision_service.DecisionService.get_variation', - return_value=(None, []), ), mock.patch( + return_value=variation_result, ), mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast: self.assertEqual( @@ -2035,7 +2090,7 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2135,7 +2190,7 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2235,7 +2290,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2285,7 +2340,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2387,7 +2442,7 @@ def test_is_feature_enabled__returns_false_for_feature_rollout_if_feature_disabl with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2429,7 +2484,7 @@ def test_is_feature_enabled__returns_false_when_user_is_not_bucketed_into_any_va feature = project_config.get_feature_from_key('test_feature_in_experiment') with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2473,7 +2528,7 @@ def test_is_feature_enabled__returns_false_when_variation_is_nil(self, ): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ) as mock_decision, mock.patch( 'optimizely.event.event_processor.BatchEventProcessor.process' ) as mock_process, mock.patch( @@ -2580,14 +2635,16 @@ def side_effect(*args, **kwargs): response = None if feature.key == 'test_feature_in_experiment': response = decision_service.Decision(mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST) + enums.DecisionSources.FEATURE_TEST, None) elif feature.key == 'test_feature_in_rollout': - response = decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT) + response = decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT, None) elif feature.key == 'test_feature_in_experiment_and_rollout': response = decision_service.Decision( - mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST, ) + mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST, None) else: - response = decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.ROLLOUT) + response = decision_service.Decision(mock_experiment, mock_variation_2, + enums.DecisionSources.ROLLOUT, None) return (response, []) @@ -2714,7 +2771,7 @@ def test_get_feature_variable_boolean(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2752,7 +2809,7 @@ def test_get_feature_variable_double(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2790,7 +2847,7 @@ def test_get_feature_variable_integer(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2828,7 +2885,7 @@ def test_get_feature_variable_string(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2867,7 +2924,7 @@ def test_get_feature_variable_json(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2914,7 +2971,7 @@ def test_get_all_feature_variables(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -2971,7 +3028,7 @@ def test_get_feature_variable(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3000,7 +3057,7 @@ def test_get_feature_variable(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3031,7 +3088,7 @@ def test_get_feature_variable(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3062,7 +3119,7 @@ def test_get_feature_variable(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3094,7 +3151,7 @@ def test_get_feature_variable(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3134,7 +3191,7 @@ def test_get_feature_variable_boolean_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3176,7 +3233,7 @@ def test_get_feature_variable_double_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3218,7 +3275,7 @@ def test_get_feature_variable_integer_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3260,7 +3317,7 @@ def test_get_feature_variable_string_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3302,7 +3359,7 @@ def test_get_feature_variable_json_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3344,7 +3401,7 @@ def test_get_all_feature_variables_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3402,7 +3459,7 @@ def test_get_feature_variable_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3435,7 +3492,7 @@ def test_get_feature_variable_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3468,7 +3525,7 @@ def test_get_feature_variable_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3501,7 +3558,7 @@ def test_get_feature_variable_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3535,7 +3592,7 @@ def test_get_feature_variable_for_feature_in_rollout(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3579,7 +3636,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertTrue( opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') @@ -3589,7 +3646,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), @@ -3599,7 +3656,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), @@ -3609,7 +3666,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), @@ -3619,7 +3676,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( {"test": 12}, opt_obj.get_feature_variable_json('test_feature_in_experiment', 'object', 'test_user'), @@ -3629,14 +3686,14 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), @@ -3645,7 +3702,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), @@ -3654,7 +3711,7 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ): self.assertEqual( 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), @@ -3669,7 +3726,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Boolean with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3703,7 +3760,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Double with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3737,7 +3794,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Integer with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3772,7 +3829,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # String with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3806,7 +3863,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # JSON with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3840,7 +3897,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): # Non-typed with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3871,7 +3928,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3904,7 +3961,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -3937,7 +3994,7 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), []), + return_value=(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( 'optimizely.notification_center.NotificationCenter.send_notifications' ) as mock_broadcast_decision: @@ -4250,7 +4307,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertTrue( opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') @@ -4265,7 +4322,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), @@ -4280,7 +4337,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), @@ -4295,7 +4352,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), @@ -4310,7 +4367,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( {"test": 12}, opt_obj.get_feature_variable_json('test_feature_in_experiment', 'object', 'test_user'), @@ -4325,7 +4382,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) @@ -4337,7 +4394,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), @@ -4351,7 +4408,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), @@ -4365,7 +4422,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), @@ -4387,7 +4444,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertFalse(opt_obj.get_feature_variable_boolean('test_feature_in_rollout', 'is_running', 'test_user')) @@ -4400,7 +4457,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 99.99, opt_obj.get_feature_variable_double('test_feature_in_rollout', 'price', 'test_user'), @@ -4415,7 +4472,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable_integer('test_feature_in_rollout', 'count', 'test_user'), @@ -4430,7 +4487,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'Hello', opt_obj.get_feature_variable_string('test_feature_in_rollout', 'message', 'test_user'), @@ -4444,7 +4501,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( {"field": 1}, opt_obj.get_feature_variable_json('test_feature_in_rollout', 'object', 'test_user'), @@ -4458,7 +4515,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertFalse(opt_obj.get_feature_variable('test_feature_in_rollout', 'is_running', 'test_user')) @@ -4470,7 +4527,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 99.99, opt_obj.get_feature_variable('test_feature_in_rollout', 'price', 'test_user'), @@ -4484,7 +4541,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 999, opt_obj.get_feature_variable('test_feature_in_rollout', 'count', 'test_user'), @@ -4498,7 +4555,7 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.ROLLOUT), []), + mock_variation, enums.DecisionSources.ROLLOUT, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual( 'Hello', opt_obj.get_feature_variable('test_feature_in_rollout', 'message', 'test_user'), @@ -4517,7 +4574,7 @@ def test_get_feature_variable__returns_none_if_type_mismatch(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch.object(opt_obj, 'logger') as mock_client_logger: # "is_working" is boolean variable and we are using double method on it. self.assertIsNone( @@ -4538,7 +4595,7 @@ def test_get_feature_variable__returns_none_if_unable_to_cast(self): with mock.patch( 'optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=(decision_service.Decision(mock_experiment, - mock_variation, enums.DecisionSources.FEATURE_TEST), []), + mock_variation, enums.DecisionSources.FEATURE_TEST, None), []), ), mock.patch( 'optimizely.project_config.ProjectConfig.get_typecast_value', side_effect=ValueError(), ), mock.patch.object( @@ -4806,10 +4863,15 @@ def test_activate(self): variation_key = 'variation' experiment_key = 'test_experiment' user_id = 'test_user' - + variation_result = { + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129'), + 'reasons': [], + 'cmab_uuid': None, + 'error': False + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result, ), mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch.object( @@ -4947,10 +5009,15 @@ def test_activate__empty_user_id(self): variation_key = 'variation' experiment_key = 'test_experiment' user_id = '' - + variation_result = { + 'cmab_uuid': None, + 'reasons': [], + 'error': False, + 'variation': self.project_config.get_variation_from_id('test_experiment', '111129') + } with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + return_value=variation_result ), mock.patch('time.time', return_value=42), mock.patch( 'optimizely.event.event_processor.ForwardingEventProcessor.process' ), mock.patch.object( diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 6705e414..c238ad12 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -234,7 +234,8 @@ def test_decide__feature_test(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ) @@ -319,7 +320,8 @@ def test_decide__feature_test__send_flag_decision_false(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ) @@ -508,7 +510,8 @@ def test_decide_feature_null_variation(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.ROLLOUT + enums.DecisionSources.ROLLOUT, + None ), [] ) @@ -593,7 +596,8 @@ def test_decide_feature_null_variation__send_flag_decision_false(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.ROLLOUT + enums.DecisionSources.ROLLOUT, + None ), [] ) @@ -664,7 +668,8 @@ def test_decide__option__disable_decision_event(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ) @@ -738,7 +743,8 @@ def test_decide__default_option__disable_decision_event(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ) @@ -809,7 +815,8 @@ def test_decide__option__exclude_variables(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ) @@ -915,7 +922,8 @@ def test_decide__option__enabled_flags_only(self): decision_service.Decision( expected_experiment, expected_var, - enums.DecisionSources.ROLLOUT + enums.DecisionSources.ROLLOUT, + None ), [] ) @@ -1004,7 +1012,8 @@ def test_decide__default_options__with__options(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ) @@ -1423,7 +1432,8 @@ def test_decide_experiment(self): decision_service.Decision( mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST + enums.DecisionSources.FEATURE_TEST, + None ), [] ),