Skip to content

Commit 81f5be9

Browse files
[FSSDK-11175] Update: Implement Decision Service methods to handle CMAB (#457)
* update: integrate CMAB components into OptimizelyFactory * update: add cmab_service parameter to Optimizely constructor for CMAB support * update: add docstring to DefaultCmabService class for improved documentation * update: implement CMAB support in bucketer and decision service, revert OptimizelyFactory * linting fix * update: add cmab_uuid handling in DecisionService and related tests * - updated function bucket_to_entity_id - test_optimizely.py fixed to expect new Decision objects * update: add None parameter to Decision constructor in user context tests * update: enhance CMAB decision handling and add related tests * update: fix logger message formatting in CMAB experiment tests * mypy fix * update: refine traffic allocation type hints and key naming in bucketer and decision service * update: remove unused import of cast in bucketer.py * update: fix return type for numeric_metric_value in get_numeric_value and ensure key is of bytes type in hash128 * update: specify type hint for numeric_metric_value in get_numeric_value function * update: fix logger reference in DefaultCmabClient initialization and add __init__.py for cmab module * update: enhance error logging for CMAB fetch failures with detailed messages and add a test for handling 500 errors * update: enhance decision result handling by introducing VariationResult and updating get_variation return type to include detailed error information * update: refactor get_variation return structure and change tests accordingly * -Error propagated to optimizely.py -test cases changed to handle return type dicts of DecisionResult and VariationResult * update: modify get_variation to return VariationResult and adjust related logic for improved variation handling * update: unit test fixes * Revert "update: unit test fixes" This reverts commit d2fc631. * Revert "update: modify get_variation to return VariationResult and adjust related logic for improved variation handling" This reverts commit b901c5f. * update: enhance decision service to handle error states and improve bucketing logic * update: remove debug print statement from Optimizely class * update: enhance bucketing logic to support CMAB traffic allocations * update: improve error logging for CMAB decision fetch failures * update: improve logging and error handling in bucketer and decision service
1 parent 82ec019 commit 81f5be9

13 files changed

+1255
-433
lines changed

optimizely/bucketer.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,34 @@ def bucket(
119119
and array of log messages representing decision making.
120120
*/.
121121
"""
122+
variation_id, decide_reasons = self.bucket_to_entity_id(project_config, experiment, user_id, bucketing_id)
123+
if variation_id:
124+
variation = project_config.get_variation_from_id_by_experiment_id(experiment.id, variation_id)
125+
return variation, decide_reasons
126+
127+
else:
128+
message = 'Bucketed into an empty traffic range. Returning nil.'
129+
project_config.logger.info(message)
130+
decide_reasons.append(message)
131+
132+
return None, decide_reasons
133+
134+
def bucket_to_entity_id(
135+
self, project_config: ProjectConfig,
136+
experiment: Experiment, user_id: str, bucketing_id: str
137+
) -> tuple[Optional[str], list[str]]:
138+
"""
139+
For a given experiment and bucketing ID determines variation ID to be shown to user.
140+
141+
Args:
142+
project_config: Instance of ProjectConfig.
143+
experiment: The experiment object (used for group/groupPolicy logic if needed).
144+
user_id: The user ID string.
145+
bucketing_id: The bucketing ID string for the user.
146+
147+
Returns:
148+
Tuple of (entity_id or None, list of decide reasons).
149+
"""
122150
decide_reasons: list[str] = []
123151
if not experiment:
124152
return None, decide_reasons
@@ -151,16 +179,16 @@ def bucket(
151179
project_config.logger.info(message)
152180
decide_reasons.append(message)
153181

182+
traffic_allocations: list[TrafficAllocation] = experiment.trafficAllocation
183+
if experiment.cmab:
184+
traffic_allocations = [
185+
{
186+
"entityId": "$",
187+
"endOfRange": experiment.cmab['trafficAllocation']
188+
}
189+
]
154190
# Bucket user if not in white-list and in group (if any)
155191
variation_id = self.find_bucket(project_config, bucketing_id,
156-
experiment.id, experiment.trafficAllocation)
157-
if variation_id:
158-
variation = project_config.get_variation_from_id_by_experiment_id(experiment.id, variation_id)
159-
return variation, decide_reasons
192+
experiment.id, traffic_allocations)
160193

161-
else:
162-
message = 'Bucketed into an empty traffic range. Returning nil.'
163-
project_config.logger.info(message)
164-
decide_reasons.append(message)
165-
166-
return None, decide_reasons
194+
return variation_id, decide_reasons

optimizely/cmab/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright 2025, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.

optimizely/cmab/cmab_service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ class CmabCacheValue(TypedDict):
3535

3636

3737
class DefaultCmabService:
38+
"""
39+
DefaultCmabService handles decisioning for Contextual Multi-Armed Bandit (CMAB) experiments,
40+
including caching and filtering user attributes for efficient decision retrieval.
41+
42+
Attributes:
43+
cmab_cache: LRUCache for user CMAB decisions.
44+
cmab_client: Client to fetch decisions from the CMAB backend.
45+
logger: Optional logger.
46+
47+
Methods:
48+
get_decision: Retrieves a CMAB decision with caching and attribute filtering.
49+
"""
3850
def __init__(self, cmab_cache: LRUCache[str, CmabCacheValue],
3951
cmab_client: DefaultCmabClient, logger: Optional[_logging.Logger] = None):
4052
self.cmab_cache = cmab_cache

optimizely/decision/optimizely_decision.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,23 @@ def as_json(self) -> dict[str, Any]:
4848
'user_context': self.user_context.as_json() if self.user_context else None,
4949
'reasons': self.reasons
5050
}
51+
52+
@classmethod
53+
def new_error_decision(cls, key: str, user: OptimizelyUserContext, reasons: list[str]) -> OptimizelyDecision:
54+
"""Create a new OptimizelyDecision representing an error state.
55+
Args:
56+
key: The flag key
57+
user: The user context
58+
reasons: List of reasons explaining the error
59+
Returns:
60+
OptimizelyDecision with error state values
61+
"""
62+
return cls(
63+
variation_key=None,
64+
enabled=False,
65+
variables={},
66+
rule_key=None,
67+
flag_key=key,
68+
user_context=user,
69+
reasons=reasons if reasons else []
70+
)

0 commit comments

Comments
 (0)