|
28 | 28 | if TYPE_CHECKING:
|
29 | 29 | # prevent circular dependenacy by skipping import at runtime
|
30 | 30 | from .project_config import ProjectConfig
|
31 |
| - from .entities import Experiment, Variation |
| 31 | + from .entities import Experiment, Variation, Group |
32 | 32 | from .helpers.types import TrafficAllocation
|
33 | 33 |
|
34 | 34 |
|
@@ -170,49 +170,61 @@ def bucket_to_entity_id(
|
170 | 170 | bucketing_id: str,
|
171 | 171 | experiment: Experiment,
|
172 | 172 | traffic_allocations: list,
|
173 |
| - parent_id: Optional[str] = None |
| 173 | + group: Optional[Group] = None |
174 | 174 | ) -> tuple[Optional[str], list[str]]:
|
175 | 175 | """
|
176 | 176 | Buckets the user and returns the entity ID (for CMAB experiments).
|
177 | 177 | Args:
|
178 | 178 | bucketing_id: The bucketing ID string for the user.
|
179 | 179 | experiment: The experiment object (for group/groupPolicy logic if needed).
|
180 | 180 | 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. |
182 | 182 |
|
183 | 183 | Returns:
|
184 | 184 | Tuple of (entity_id or None, list of decide reasons).
|
185 | 185 | """
|
186 | 186 | decide_reasons = []
|
187 | 187 |
|
188 |
| - # If experiment is in a mutually exclusive group with random policy, check group bucketing first |
189 | 188 | 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}".') |
205 | 219 |
|
206 | 220 | 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: |
210 | 224 | 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}".' |
212 | 226 | )
|
213 | 227 | return entity_id, decide_reasons
|
214 | 228 |
|
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.') |
218 | 230 | return None, decide_reasons
|
0 commit comments