@@ -50,7 +50,7 @@ def __init__(self, datafile, event_dispatcher=None, logger=None, error_handler=N
5050 self .error_handler = error_handler or noop_error_handler
5151
5252 try :
53- self ._validate_inputs (datafile , skip_json_validation )
53+ self ._validate_instantiation_options (datafile , skip_json_validation )
5454 except exceptions .InvalidInputException as error :
5555 self .is_valid = False
5656 self .logger = SimpleLogger ()
@@ -75,15 +75,15 @@ def __init__(self, datafile, event_dispatcher=None, logger=None, error_handler=N
7575 self .bucketer = bucketer .Bucketer (self .config )
7676 self .event_builder = event_builder .EventBuilder (self .config , self .bucketer )
7777
78- def _validate_inputs (self , datafile , skip_json_validation ):
79- """ Helper method to validate all input parameters.
78+ def _validate_instantiation_options (self , datafile , skip_json_validation ):
79+ """ Helper method to validate all instantiation parameters.
8080
8181 Args:
8282 datafile: JSON string representing the project.
8383 skip_json_validation: Boolean representing whether JSON schema validation needs to be skipped or not.
8484
8585 Raises:
86- Exception if provided input is invalid .
86+ Exception if provided instantiation options are valid .
8787 """
8888
8989 if not skip_json_validation and not validator .is_datafile_valid (datafile ):
@@ -98,39 +98,74 @@ def _validate_inputs(self, datafile, skip_json_validation):
9898 if not validator .is_error_handler_valid (self .error_handler ):
9999 raise exceptions .InvalidInputException (enums .Errors .INVALID_INPUT_ERROR .format ('error_handler' ))
100100
101- def _validate_preconditions (self , experiment , user_id , attributes ):
101+ def _validate_preconditions (self , experiment , attributes = None , event_tags = None ):
102102 """ Helper method to validate all pre-conditions before we go ahead to bucket user.
103103
104104 Args:
105105 experiment: Object representing the experiment.
106- user_id: ID for user.
107106 attributes: Dict representing user attributes.
108107
109108 Returns:
110109 Boolean depending upon whether all conditions are met or not.
111110 """
112-
113- if attributes and not validator .are_attributes_valid (attributes ):
114- self .logger .log (enums .LogLevels .ERROR , 'Provided attributes are in an invalid format.' )
115- self .error_handler .handle_error (exceptions .InvalidAttributeException (enums .Errors .INVALID_ATTRIBUTE_FORMAT ))
111+ if not self ._validate_user_inputs (attributes , event_tags ):
116112 return False
117113
118114 if not experiment_helper .is_experiment_running (experiment ):
119115 self .logger .log (enums .LogLevels .INFO , 'Experiment "%s" is not running.' % experiment .key )
120116 return False
121117
122- if experiment_helper .is_user_in_forced_variation (experiment .forcedVariations , user_id ):
123- return True
118+ return True
124119
125- if not audience_helper .is_user_in_experiment (self .config , experiment , attributes ):
126- self .logger .log (
127- enums .LogLevels .INFO ,
128- 'User "%s" does not meet conditions to be in experiment "%s".' % (user_id , experiment .key )
129- )
120+ def _validate_user_inputs (self , attributes = None , event_tags = None ):
121+ """ Helper method to validate user inputs.
122+
123+ Args:
124+ attributes: Dict representing user attributes.
125+ event_tags: Dict representing metadata associated with an event.
126+
127+ Returns:
128+ Boolean True if inputs are valid. False otherwise.
129+
130+ """
131+
132+ if attributes and not validator .are_attributes_valid (attributes ):
133+ self .logger .log (enums .LogLevels .ERROR , 'Provided attributes are in an invalid format.' )
134+ self .error_handler .handle_error (exceptions .InvalidAttributeException (enums .Errors .INVALID_ATTRIBUTE_FORMAT ))
135+ return False
136+
137+ if event_tags and not validator .are_event_tags_valid (event_tags ):
138+ self .logger .log (enums .LogLevels .ERROR , 'Provided event tags are in an invalid format.' )
139+ self .error_handler .handle_error (exceptions .InvalidEventTagException (enums .Errors .INVALID_EVENT_TAG_FORMAT ))
130140 return False
131141
132142 return True
133143
144+ def _get_valid_experiments_for_event (self , event , user_id , attributes ):
145+ """ Helper method to determine which experiments we should track for the given event.
146+
147+ Args:
148+ event: The event which needs to be recorded.
149+ user_id: ID for user.
150+ attributes: Dict representing user attributes.
151+
152+ Returns:
153+ List of tuples representing valid experiment IDs and variation IDs into which the user is bucketed.
154+ """
155+ valid_experiments = []
156+ for experiment_id in event .experimentIds :
157+ experiment = self .config .get_experiment_from_id (experiment_id )
158+ variation_key = self .get_variation (experiment .key , user_id , attributes )
159+
160+ if not variation_key :
161+ self .logger .log (enums .LogLevels .INFO , 'Not tracking user "%s" for experiment "%s".' % (user_id , experiment .key ))
162+ continue
163+
164+ variation = self .config .get_variation_from_key (experiment .key , variation_key )
165+ valid_experiments .append ((experiment_id , variation .id ))
166+
167+ return valid_experiments
168+
134169 def activate (self , experiment_key , user_id , attributes = None ):
135170 """ Buckets visitor and sends impression event to Optimizely.
136171
@@ -148,22 +183,15 @@ def activate(self, experiment_key, user_id, attributes=None):
148183 self .logger .log (enums .LogLevels .ERROR , enums .Errors .INVALID_DATAFILE .format ('activate' ))
149184 return None
150185
151- experiment = self .config .get_experiment_from_key (experiment_key )
152- if not experiment :
153- self .logger .log (enums .LogLevels .INFO , 'Not activating user "%s".' % user_id )
154- return None
155-
156- if not self ._validate_preconditions (experiment , user_id , attributes ):
157- self .logger .log (enums .LogLevels .INFO , 'Not activating user "%s".' % user_id )
158- return None
159-
160- variation = self .bucketer .bucket (experiment , user_id )
186+ variation_key = self .get_variation (experiment_key , user_id , attributes )
161187
162- if not variation :
188+ if not variation_key :
163189 self .logger .log (enums .LogLevels .INFO , 'Not activating user "%s".' % user_id )
164190 return None
165191
166192 # Create and dispatch impression event
193+ experiment = self .config .get_experiment_from_key (experiment_key )
194+ variation = self .config .get_variation_from_key (experiment_key , variation_key )
167195 impression_event = self .event_builder .create_impression_event (experiment , variation .id , user_id , attributes )
168196 self .logger .log (enums .LogLevels .INFO , 'Activating user "%s" in experiment "%s".' % (user_id , experiment .key ))
169197 self .logger .log (enums .LogLevels .DEBUG ,
@@ -191,11 +219,6 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
191219 self .logger .log (enums .LogLevels .ERROR , enums .Errors .INVALID_DATAFILE .format ('track' ))
192220 return
193221
194- if attributes and not validator .are_attributes_valid (attributes ):
195- self .logger .log (enums .LogLevels .ERROR , 'Provided attributes are in an invalid format.' )
196- self .error_handler .handle_error (exceptions .InvalidAttributeException (enums .Errors .INVALID_ATTRIBUTE_FORMAT ))
197- return
198-
199222 if event_tags :
200223 if isinstance (event_tags , numbers .Number ):
201224 event_tags = {
@@ -204,24 +227,16 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
204227 self .logger .log (enums .LogLevels .WARNING ,
205228 'Event value is deprecated in track call. Use event tags to pass in revenue value instead.' )
206229
207- if not validator .are_event_tags_valid (event_tags ):
208- self .logger .log (enums .LogLevels .ERROR , 'Provided event tags are in an invalid format.' )
209- self .error_handler .handle_error (exceptions .InvalidEventTagException (enums .Errors .INVALID_EVENT_TAG_FORMAT ))
210- return
230+ if not self ._validate_user_inputs (attributes , event_tags ):
231+ return
211232
212233 event = self .config .get_event (event_key )
213234 if not event :
214235 self .logger .log (enums .LogLevels .INFO , 'Not tracking user "%s" for event "%s".' % (user_id , event_key ))
215236 return
216237
217238 # Filter out experiments that are not running or that do not include the user in audience conditions
218- valid_experiments = []
219- for experiment_id in event .experimentIds :
220- experiment = self .config .get_experiment_from_id (experiment_id )
221- if not self ._validate_preconditions (experiment , user_id , attributes ):
222- self .logger .log (enums .LogLevels .INFO , 'Not tracking user "%s" for experiment "%s".' % (user_id , experiment .key ))
223- continue
224- valid_experiments .append (experiment )
239+ valid_experiments = self ._get_valid_experiments_for_event (event , user_id , attributes )
225240
226241 # Create and dispatch conversion event if there are valid experiments
227242 if valid_experiments :
@@ -259,10 +274,23 @@ def get_variation(self, experiment_key, user_id, attributes=None):
259274
260275 experiment = self .config .get_experiment_from_key (experiment_key )
261276 if not experiment :
277+ self .logger .log (enums .LogLevels .INFO , 'Experiment key "%s" is invalid. Not activating user "%s".' % (experiment_key , user_id ))
262278 return None
263279
264- if not self ._validate_preconditions (experiment , user_id , attributes ):
280+ if not self ._validate_preconditions (experiment , attributes ):
265281 return None
282+
283+ forcedVariation = self .bucketer .get_forced_variation (experiment , user_id )
284+ if forcedVariation :
285+ return forcedVariation .key
286+
287+ if not audience_helper .is_user_in_experiment (self .config , experiment , attributes ):
288+ self .logger .log (
289+ enums .LogLevels .INFO ,
290+ 'User "%s" does not meet conditions to be in experiment "%s".' % (user_id , experiment .key )
291+ )
292+ return None
293+
266294 variation = self .bucketer .bucket (experiment , user_id )
267295
268296 if variation :
0 commit comments