Skip to content

Commit 3eb755f

Browse files
- updated function bucket_to_entity_id
- test_optimizely.py fixed to expect new Decision objects
1 parent 91d53b6 commit 3eb755f

File tree

3 files changed

+133
-116
lines changed

3 files changed

+133
-116
lines changed

optimizely/bucketer.py

Lines changed: 38 additions & 26 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
31+
from .entities import Experiment, Variation, Group
3232
from .helpers.types import TrafficAllocation
3333

3434

@@ -170,49 +170,61 @@ def bucket_to_entity_id(
170170
bucketing_id: str,
171171
experiment: Experiment,
172172
traffic_allocations: list,
173-
parent_id: Optional[str] = None
173+
group: Optional[Group] = None
174174
) -> tuple[Optional[str], list[str]]:
175175
"""
176176
Buckets the user and returns the entity ID (for CMAB experiments).
177177
Args:
178178
bucketing_id: The bucketing ID string for the user.
179179
experiment: The experiment object (for group/groupPolicy logic if needed).
180180
traffic_allocations: List of traffic allocation dicts (should have 'entity_id' and 'end_of_range' keys).
181-
parent_id: (optional) Used for mutex group support; if not supplied, experiment.id is used.
181+
group: (optional) Group object for mutex group support.
182182
183183
Returns:
184184
Tuple of (entity_id or None, list of decide reasons).
185185
"""
186186
decide_reasons = []
187187

188-
# If experiment is in a mutually exclusive group with random policy, check group bucketing first
189188
group_id = getattr(experiment, 'groupId', None)
190-
group_policy = getattr(experiment, 'groupPolicy', None)
191-
if group_id and group_policy == 'random':
192-
bucketing_key = f"{bucketing_id}{group_id}"
193-
bucket_number = self._generate_bucket_value(bucketing_key)
194-
# Group traffic allocation would need to be passed in or found here
195-
# For now, skipping group-level allocation (you can extend this for mutex groups)
196-
decide_reasons.append(f'Checked mutex group allocation for group "{group_id}".')
197-
198-
# Main bucketing for experiment or CMAB dummy entity
199-
parent_id = parent_id or experiment.id
200-
bucketing_key = f"{bucketing_id}{parent_id}"
201-
bucket_number = self._generate_bucket_value(bucketing_key)
202-
decide_reasons.append(
203-
f'Assigned bucket {bucket_number} to bucketing ID "{bucketing_id}" for parent "{parent_id}".'
204-
)
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.get("endOfRange", 0)
197+
entity_id = allocation.get("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}".')
205219

206220
for allocation in traffic_allocations:
207-
end_of_range = allocation.get("end_of_range") or allocation.get("endOfRange")
208-
entity_id = allocation.get("entity_id") or allocation.get("entityId")
209-
if end_of_range is not None and bucket_number < end_of_range:
221+
end_of_range = allocation.get("end_of_range", 0)
222+
entity_id = allocation.get("entity_id")
223+
if bucket_val < end_of_range:
210224
decide_reasons.append(
211-
f'User with bucketing ID "{bucketing_id}" bucketed into entity "{entity_id}".'
225+
f'User bucketed into entity id "{entity_id}".'
212226
)
213227
return entity_id, decide_reasons
214228

215-
decide_reasons.append(
216-
f'User with bucketing ID "{bucketing_id}" not bucketed into any entity.'
217-
)
229+
decide_reasons.append('User not bucketed into any entity id.')
218230
return None, decide_reasons

optimizely/decision_service.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class CmabDecisionResult(TypedDict):
3939

4040

4141
class Decision(NamedTuple):
42-
"""Named tuple containing selected experiment, variation and source.
42+
"""Named tuple containing selected experiment, variation, source and cmab_uuid.
4343
None if no experiment/variation was selected."""
4444
experiment: Optional[entities.Experiment]
4545
variation: Optional[entities.Variation]
@@ -397,8 +397,11 @@ def get_variation(
397397
}]
398398

399399
# Check if user is in CMAB traffic allocation
400+
group = None
401+
if experiment.groupId:
402+
group = project_config.get_group(group_id=experiment.groupId)
400403
bucketed_entity_id, bucket_reasons = self.bucketer.bucket_to_entity_id(
401-
bucketing_id, experiment, cmab_traffic_allocation
404+
bucketing_id, experiment, cmab_traffic_allocation, group
402405
)
403406
decide_reasons += bucket_reasons
404407
if bucketed_entity_id != CMAB_DUMMY_ENTITY_ID:

0 commit comments

Comments
 (0)