25
25
Decision = namedtuple ('Decision' , 'experiment variation source' )
26
26
DECISION_SOURCE_EXPERIMENT = 'experiment'
27
27
DECISION_SOURCE_ROLLOUT = 'rollout'
28
+ RESERVED_BUCKETING_ID_ATTRIBUTE = '$opt_bucketing_id'
28
29
29
30
30
31
class DecisionService (object ):
@@ -36,6 +37,21 @@ def __init__(self, config, user_profile_service):
36
37
self .config = config
37
38
self .logger = config .logger
38
39
40
+ @staticmethod
41
+ def _get_bucketing_id (user_id , attributes ):
42
+ """ Helper method to determine bucketing ID for the user.
43
+
44
+ Args:
45
+ user_id: ID for user.
46
+ attributes: Dict representing user attributes. May consist of bucketing ID to be used.
47
+
48
+ Returns:
49
+ String representing bucketing ID for the user. Fallback to user's ID if not provided.
50
+ """
51
+
52
+ attributes = attributes or {}
53
+ return attributes .get (RESERVED_BUCKETING_ID_ATTRIBUTE , user_id )
54
+
39
55
def get_forced_variation (self , experiment , user_id ):
40
56
""" Determine if a user is forced into a variation for the given experiment and return that variation.
41
57
@@ -145,7 +161,9 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
145
161
)
146
162
return None
147
163
148
- variation = self .bucketer .bucket (experiment , user_id )
164
+ # Determine bucketing ID to be used
165
+ bucketing_id = self ._get_bucketing_id (user_id , attributes )
166
+ variation = self .bucketer .bucket (experiment , user_id , bucketing_id )
149
167
150
168
if variation :
151
169
# Store this new decision and return the variation for the user
@@ -188,7 +206,9 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
188
206
continue
189
207
190
208
self .logger .log (enums .LogLevels .DEBUG , 'User "%s" meets conditions for targeting rule %s.' % (user_id , idx + 1 ))
191
- variation = self .bucketer .bucket (experiment , user_id )
209
+ # Determine bucketing ID to be used
210
+ bucketing_id = self ._get_bucketing_id (user_id , attributes )
211
+ variation = self .bucketer .bucket (experiment , user_id , bucketing_id )
192
212
if variation :
193
213
self .logger .log (enums .LogLevels .DEBUG ,
194
214
'User "%s" is in variation %s of experiment %s.' % (user_id , variation .key , experiment .key ))
@@ -205,14 +225,42 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
205
225
if audience_helper .is_user_in_experiment (self .config ,
206
226
self .config .get_experiment_from_key (rollout .experiments [- 1 ].get ('key' )),
207
227
attributes ):
208
- variation = self .bucketer .bucket (everyone_else_experiment , user_id )
228
+ # Determine bucketing ID to be used
229
+ bucketing_id = self ._get_bucketing_id (user_id , attributes )
230
+ variation = self .bucketer .bucket (everyone_else_experiment , user_id , bucketing_id )
209
231
if variation :
210
232
self .logger .log (enums .LogLevels .DEBUG ,
211
233
'User "%s" meets conditions for targeting rule "Everyone Else".' % user_id )
212
234
return Decision (everyone_else_experiment , variation , DECISION_SOURCE_ROLLOUT )
213
235
214
236
return Decision (None , None , DECISION_SOURCE_ROLLOUT )
215
237
238
+ def get_experiment_in_group (self , group , bucketing_id ):
239
+ """ Determine which experiment in the group the user is bucketed into.
240
+
241
+ Args:
242
+ group: The group to bucket the user into.
243
+ bucketing_id: ID to be used for bucketing the user.
244
+
245
+ Returns:
246
+ Experiment if the user is bucketed into an experiment in the specified group. None otherwise.
247
+ """
248
+
249
+ experiment_id = self .bucketer .find_bucket (bucketing_id , group .id , group .trafficAllocation )
250
+ if experiment_id :
251
+ experiment = self .config .get_experiment_from_id (experiment_id )
252
+ if experiment :
253
+ self .logger .log (enums .LogLevels .INFO ,
254
+ 'User with bucketing ID "%s" is in experiment %s of group %s.' %
255
+ (bucketing_id , experiment .key , group .id ))
256
+ return experiment
257
+
258
+ self .logger .log (enums .LogLevels .INFO ,
259
+ 'User with bucketing ID "%s" is not in any experiments of group %s.' %
260
+ (bucketing_id , group .id ))
261
+
262
+ return None
263
+
216
264
def get_variation_for_feature (self , feature , user_id , attributes = None ):
217
265
""" Returns the experiment/variation the user is bucketed in for the given feature.
218
266
@@ -227,12 +275,13 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
227
275
228
276
experiment = None
229
277
variation = None
278
+ bucketing_id = self ._get_bucketing_id (user_id , attributes )
230
279
231
280
# First check if the feature is in a mutex group
232
281
if feature .groupId :
233
282
group = self .config .get_group (feature .groupId )
234
283
if group :
235
- experiment = self .get_experiment_in_group (group , user_id )
284
+ experiment = self .get_experiment_in_group (group , bucketing_id )
236
285
if experiment and experiment .id in feature .experimentIds :
237
286
variation = self .get_variation (experiment , user_id , attributes )
238
287
@@ -259,29 +308,3 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
259
308
return self .get_variation_for_rollout (rollout , user_id , attributes )
260
309
261
310
return Decision (experiment , variation , DECISION_SOURCE_EXPERIMENT )
262
-
263
- def get_experiment_in_group (self , group , user_id ):
264
- """ Determine which experiment in the group the user is bucketed into.
265
-
266
- Args:
267
- group: The group to bucket the user into.
268
- user_id: ID of the user.
269
-
270
- Returns:
271
- Experiment if the user is bucketed into an experiment in the specified group. None otherwise.
272
- """
273
-
274
- experiment_id = self .bucketer .find_bucket (user_id , group .id , group .trafficAllocation )
275
- if experiment_id :
276
- experiment = self .config .get_experiment_from_id (experiment_id )
277
- if experiment :
278
- self .logger .log (enums .LogLevels .INFO ,
279
- 'User "%s" is in experiment %s of group %s.' %
280
- (user_id , experiment .key , group .id ))
281
- return experiment
282
-
283
- self .logger .log (enums .LogLevels .INFO ,
284
- 'User "%s" is not in any experiments of group %s.' %
285
- (user_id , group .id ))
286
-
287
- return None
0 commit comments