2525Decision = namedtuple ('Decision' , 'experiment variation source' )
2626DECISION_SOURCE_EXPERIMENT = 'experiment'
2727DECISION_SOURCE_ROLLOUT = 'rollout'
28+ RESERVED_BUCKETING_ID_ATTRIBUTE = '$opt_bucketing_id'
2829
2930
3031class DecisionService (object ):
@@ -36,6 +37,21 @@ def __init__(self, config, user_profile_service):
3637 self .config = config
3738 self .logger = config .logger
3839
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+
3955 def get_forced_variation (self , experiment , user_id ):
4056 """ Determine if a user is forced into a variation for the given experiment and return that variation.
4157
@@ -145,7 +161,9 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
145161 )
146162 return None
147163
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 )
149167
150168 if variation :
151169 # 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):
188206 continue
189207
190208 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 )
192212 if variation :
193213 self .logger .log (enums .LogLevels .DEBUG ,
194214 '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):
205225 if audience_helper .is_user_in_experiment (self .config ,
206226 self .config .get_experiment_from_key (rollout .experiments [- 1 ].get ('key' )),
207227 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 )
209231 if variation :
210232 self .logger .log (enums .LogLevels .DEBUG ,
211233 'User "%s" meets conditions for targeting rule "Everyone Else".' % user_id )
212234 return Decision (everyone_else_experiment , variation , DECISION_SOURCE_ROLLOUT )
213235
214236 return Decision (None , None , DECISION_SOURCE_ROLLOUT )
215237
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+
216264 def get_variation_for_feature (self , feature , user_id , attributes = None ):
217265 """ Returns the experiment/variation the user is bucketed in for the given feature.
218266
@@ -227,12 +275,13 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
227275
228276 experiment = None
229277 variation = None
278+ bucketing_id = self ._get_bucketing_id (user_id , attributes )
230279
231280 # First check if the feature is in a mutex group
232281 if feature .groupId :
233282 group = self .config .get_group (feature .groupId )
234283 if group :
235- experiment = self .get_experiment_in_group (group , user_id )
284+ experiment = self .get_experiment_in_group (group , bucketing_id )
236285 if experiment and experiment .id in feature .experimentIds :
237286 variation = self .get_variation (experiment , user_id , attributes )
238287
@@ -259,29 +308,3 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
259308 return self .get_variation_for_rollout (rollout , user_id , attributes )
260309
261310 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