Skip to content

Commit 62bbf64

Browse files
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-for-cmab' into farhan-anjum/FSSDK-11184-update-impression-event-for-cmab
2 parents 3c903c7 + c637878 commit 62bbf64

File tree

6 files changed

+132
-191
lines changed

6 files changed

+132
-191
lines changed

optimizely/bucketer.py

Lines changed: 30 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
if TYPE_CHECKING:
2929
# prevent circular dependenacy by skipping import at runtime
3030
from .project_config import ProjectConfig
31-
from .entities import Experiment, Variation, Group
31+
from .entities import Experiment, Variation
3232
from .helpers.types import TrafficAllocation
3333

3434

@@ -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+
elif not decide_reasons:
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
@@ -154,77 +182,5 @@ def bucket(
154182
# Bucket user if not in white-list and in group (if any)
155183
variation_id = self.find_bucket(project_config, bucketing_id,
156184
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
160-
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
167-
168-
def bucket_to_entity_id(
169-
self,
170-
bucketing_id: str,
171-
experiment: Experiment,
172-
traffic_allocations: list[TrafficAllocation],
173-
group: Optional[Group] = None
174-
) -> tuple[Optional[str], list[str]]:
175-
"""
176-
Buckets the user and returns the entity ID (for CMAB experiments).
177-
Args:
178-
bucketing_id: The bucketing ID string for the user.
179-
experiment: The experiment object (for group/groupPolicy logic if needed).
180-
traffic_allocations: List of traffic allocation dicts (should have 'entity_id' and 'end_of_range' keys).
181-
group: (optional) Group object for mutex group support.
182185

183-
Returns:
184-
Tuple of (entity_id or None, list of decide reasons).
185-
"""
186-
decide_reasons = []
187-
188-
group_id = getattr(experiment, 'groupId', None)
189-
if group_id and group and getattr(group, 'policy', None) == 'random':
190-
bucket_key = bucketing_id + group_id
191-
bucket_val = self._generate_bucket_value(bucket_key)
192-
decide_reasons.append(f'Generated group bucket value {bucket_val} for key "{bucket_key}".')
193-
194-
matched = False
195-
for allocation in group.trafficAllocation:
196-
end_of_range = allocation['endOfRange']
197-
entity_id = allocation['entityId']
198-
if bucket_val < end_of_range:
199-
matched = True
200-
if entity_id != experiment.id:
201-
decide_reasons.append(
202-
f'User not bucketed into experiment "{experiment.id}" (got "{entity_id}").'
203-
)
204-
return None, decide_reasons
205-
decide_reasons.append(
206-
f'User is bucketed into experiment "{experiment.id}" within group "{group_id}".'
207-
)
208-
break
209-
if not matched:
210-
decide_reasons.append(
211-
f'User not bucketed into any experiment in group "{group_id}".'
212-
)
213-
return None, decide_reasons
214-
215-
# Main experiment bucketing
216-
bucket_key = bucketing_id + experiment.id
217-
bucket_val = self._generate_bucket_value(bucket_key)
218-
decide_reasons.append(f'Generated experiment bucket value {bucket_val} for key "{bucket_key}".')
219-
220-
for allocation in traffic_allocations:
221-
end_of_range = allocation['endOfRange']
222-
entity_id = allocation['entityId']
223-
if bucket_val < end_of_range:
224-
decide_reasons.append(
225-
f'User bucketed into entity id "{entity_id}".'
226-
)
227-
return entity_id, decide_reasons
228-
229-
decide_reasons.append('User not bucketed into any entity id.')
230-
return None, decide_reasons
186+
return variation_id, decide_reasons

optimizely/decision/optimizely_decision.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,10 @@ def as_json(self) -> dict[str, Any]:
5252
@classmethod
5353
def new_error_decision(cls, key: str, user: OptimizelyUserContext, reasons: list[str]) -> OptimizelyDecision:
5454
"""Create a new OptimizelyDecision representing an error state.
55-
5655
Args:
5756
key: The flag key
5857
user: The user context
5958
reasons: List of reasons explaining the error
60-
6159
Returns:
6260
OptimizelyDecision with error state values
6361
"""

optimizely/decision_service.py

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
# prevent circular dependenacy by skipping import at runtime
3131
from .project_config import ProjectConfig
3232
from .logger import Logger
33-
from .helpers.types import TrafficAllocation
3433

3534

3635
class CmabDecisionResult(TypedDict):
@@ -134,6 +133,7 @@ def _get_decision_for_cmab_experiment(
134133
project_config: ProjectConfig,
135134
experiment: entities.Experiment,
136135
user_context: OptimizelyUserContext,
136+
bucketing_id: str,
137137
options: Optional[Sequence[str]] = None
138138
) -> CmabDecisionResult:
139139
"""
@@ -143,14 +143,36 @@ def _get_decision_for_cmab_experiment(
143143
project_config: Instance of ProjectConfig.
144144
experiment: The experiment object for which the decision is to be made.
145145
user_context: The user context containing user id and attributes.
146+
bucketing_id: The bucketing ID to use for traffic allocation.
146147
options: Optional sequence of decide options.
147148
148149
Returns:
149150
A dictionary containing:
150151
- "error": Boolean indicating if there was an error.
151-
- "result": The CmabDecision result or empty dict if error.
152+
- "result": The CmabDecision result or None if error.
152153
- "reasons": List of strings with reasons or error messages.
153154
"""
155+
decide_reasons: list[str] = []
156+
user_id = user_context.user_id
157+
158+
# Check if user is in CMAB traffic allocation
159+
bucketed_entity_id, bucket_reasons = self.bucketer.bucket_to_entity_id(
160+
project_config, experiment, user_id, bucketing_id
161+
)
162+
decide_reasons.extend(bucket_reasons)
163+
164+
if not bucketed_entity_id:
165+
message = f'User "{user_context.user_id}" not in CMAB experiment ' \
166+
f'"{experiment.key}" due to traffic allocation.'
167+
self.logger.info(message)
168+
decide_reasons.append(message)
169+
return {
170+
"error": False,
171+
"result": None,
172+
"reasons": decide_reasons,
173+
}
174+
175+
# User is in CMAB allocation, proceed to CMAB decision
154176
try:
155177
options_list = list(options) if options is not None else []
156178
cmab_decision = self.cmab_service.get_decision(
@@ -159,7 +181,7 @@ def _get_decision_for_cmab_experiment(
159181
return {
160182
"error": False,
161183
"result": cmab_decision,
162-
"reasons": [],
184+
"reasons": decide_reasons,
163185
}
164186
except Exception as e:
165187
error_message = Errors.CMAB_FETCH_FAILED_DETAILED.format(experiment.key)
@@ -465,49 +487,25 @@ def get_variation(
465487
# If so, handle CMAB-specific traffic allocation and decision logic.
466488
# Otherwise, proceed with standard bucketing logic for non-CMAB experiments.
467489
if experiment.cmab:
468-
CMAB_DUMMY_ENTITY_ID = "$"
469-
# Build the CMAB-specific traffic allocation
470-
cmab_traffic_allocation: list[TrafficAllocation] = [{
471-
"entityId": CMAB_DUMMY_ENTITY_ID,
472-
"endOfRange": experiment.cmab['trafficAllocation']
473-
}]
474-
475-
# Check if user is in CMAB traffic allocation
476-
group = None
477-
if experiment.groupId:
478-
group = project_config.get_group(group_id=experiment.groupId)
479-
bucketed_entity_id, bucket_reasons = self.bucketer.bucket_to_entity_id(
480-
bucketing_id, experiment, cmab_traffic_allocation, group
481-
)
482-
decide_reasons += bucket_reasons
483-
if bucketed_entity_id != CMAB_DUMMY_ENTITY_ID:
484-
message = f'User "{user_id}" not in CMAB experiment "{experiment.key}" due to traffic allocation.'
485-
self.logger.info(message)
486-
decide_reasons.append(message)
487-
return {
488-
'cmab_uuid': None,
489-
'error': False,
490-
'reasons': decide_reasons,
491-
'variation': None
492-
}
493-
494-
# User is in CMAB allocation, proceed to CMAB decision
490+
experiment.cmab
495491
cmab_decision_result = self._get_decision_for_cmab_experiment(project_config,
496492
experiment,
497493
user_context,
494+
bucketing_id,
498495
options)
499496
decide_reasons += cmab_decision_result.get('reasons', [])
500497
cmab_decision = cmab_decision_result.get('result')
501-
if not cmab_decision or cmab_decision_result['error']:
498+
if cmab_decision_result['error']:
502499
return {
503500
'cmab_uuid': None,
504501
'error': True,
505502
'reasons': decide_reasons,
506503
'variation': None
507504
}
508-
variation_id = cmab_decision['variation_id']
509-
cmab_uuid = cmab_decision['cmab_uuid']
510-
variation = project_config.get_variation_from_id(experiment_key=experiment.key, variation_id=variation_id)
505+
variation_id = cmab_decision['variation_id'] if cmab_decision else None
506+
cmab_uuid = cmab_decision['cmab_uuid'] if cmab_decision else None
507+
variation = project_config.get_variation_from_id(experiment_key=experiment.key,
508+
variation_id=variation_id) if variation_id else None
511509
else:
512510
# Bucket the user
513511
variation, bucket_reasons = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id)

optimizely/optimizely.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@ def _decide_for_keys(
13761376
user_context,
13771377
merged_decide_options
13781378
)
1379-
1379+
print("here")
13801380
for i in range(0, len(flags_without_forced_decision)):
13811381
decision = decision_list[i]['decision']
13821382
reasons = decision_list[i]['reasons']

0 commit comments

Comments
 (0)