Skip to content

Commit 1169809

Browse files
Better exception handling in the SDK (#27)
1 parent f6b1a76 commit 1169809

File tree

8 files changed

+196
-58
lines changed

8 files changed

+196
-58
lines changed

optimizely/event_builder.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from abc import abstractmethod
33
from abc import abstractproperty
44

5+
from . import exceptions
56
from . import project_config
67
from . import version
78
from .helpers import enums
@@ -416,4 +417,4 @@ def get_event_builder(config, bucketer):
416417
if config_version == project_config.V2_CONFIG_VERSION:
417418
return EventBuilderV2(config, bucketer)
418419

419-
raise Exception(enums.Errors.UNSUPPORTED_CONFIG_VERSION)
420+
raise exceptions.InvalidInputException(enums.Errors.UNSUPPORTED_DATAFILE_VERSION)

optimizely/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ class InvalidGroupException(Exception):
2323
pass
2424

2525

26+
class InvalidInputException(Exception):
27+
""" Raised when provided datafile, event dispatcher, logger or error handler is invalid. """
28+
pass
29+
30+
2631
class InvalidVariationException(Exception):
2732
""" Raised when provided variation is invalid. """
2833
pass

optimizely/helpers/enums.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Errors(object):
2222
INVALID_AUDIENCE_ERROR = 'Provided audience is not in datafile.'
2323
INVALID_EXPERIMENT_KEY_ERROR = 'Provided experiment is not in datafile.'
2424
INVALID_EVENT_KEY_ERROR = 'Provided event is not in datafile.'
25+
INVALID_DATAFILE = 'Datafile has invalid format. Failing "{}".'
2526
INVALID_GROUP_ID_ERROR = 'Provided group is not in datafile.'
2627
INVALID_VARIATION_ERROR = 'Provided variation is not in datafile.'
27-
UNSUPPORTED_CONFIG_VERSION = 'Datafile provided has unsupported version.'
28+
UNSUPPORTED_DATAFILE_VERSION = 'Provided datafile has unsupported version.'

optimizely/helpers/validator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ def is_datafile_valid(datafile):
1717

1818
try:
1919
datafile_json = json.loads(datafile)
20+
datafile_version = datafile_json.get('version')
2021
except:
2122
return False
2223

23-
datafile_version = datafile_json.get('version')
2424
json_schema = None
2525

2626
if datafile_version == project_config.V1_CONFIG_VERSION:

optimizely/optimizely.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import logging
2+
import sys
3+
14
from . import bucketer
25
from . import event_builder
36
from . import exceptions
@@ -27,14 +30,33 @@ def __init__(self, datafile, event_dispatcher=None, logger=None, error_handler=N
2730
By default JSON schema validation will be performed.
2831
"""
2932

33+
self.is_valid = True
3034
self.event_dispatcher = event_dispatcher or default_event_dispatcher
3135
self.logger = logger or noop_logger
3236
self.error_handler = error_handler or noop_error_handler
33-
self._validate_inputs(datafile, skip_json_validation)
3437

35-
self.config = project_config.ProjectConfig(datafile, self.logger, self.error_handler)
38+
try:
39+
self._validate_inputs(datafile, skip_json_validation)
40+
except exceptions.InvalidInputException as error:
41+
self.is_valid = False
42+
logging.error(str(error))
43+
return
44+
45+
try:
46+
self.config = project_config.ProjectConfig(datafile, self.logger, self.error_handler)
47+
except:
48+
self.is_valid = False
49+
self.config = None
50+
logging.error(enums.Errors.INVALID_INPUT_ERROR.format('datafile'))
51+
return
52+
3653
self.bucketer = bucketer.Bucketer(self.config)
37-
self.event_builder = event_builder.get_event_builder(self.config, self.bucketer)
54+
55+
try:
56+
self.event_builder = event_builder.get_event_builder(self.config, self.bucketer)
57+
except:
58+
self.is_valid = False
59+
logging.error(enums.Errors.UNSUPPORTED_DATAFILE_VERSION)
3860

3961
def _validate_inputs(self, datafile, skip_json_validation):
4062
""" Helper method to validate all input parameters.
@@ -48,16 +70,16 @@ def _validate_inputs(self, datafile, skip_json_validation):
4870
"""
4971

5072
if not skip_json_validation and not validator.is_datafile_valid(datafile):
51-
raise Exception(enums.Errors.INVALID_INPUT_ERROR.format('datafile'))
73+
raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT_ERROR.format('datafile'))
5274

5375
if not validator.is_event_dispatcher_valid(self.event_dispatcher):
54-
raise Exception(enums.Errors.INVALID_INPUT_ERROR.format('event_dispatcher'))
76+
raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT_ERROR.format('event_dispatcher'))
5577

5678
if not validator.is_logger_valid(self.logger):
57-
raise Exception(enums.Errors.INVALID_INPUT_ERROR.format('logger'))
79+
raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT_ERROR.format('logger'))
5880

5981
if not validator.is_error_handler_valid(self.error_handler):
60-
raise Exception(enums.Errors.INVALID_INPUT_ERROR.format('error_handler'))
82+
raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT_ERROR.format('error_handler'))
6183

6284
def _validate_preconditions(self, experiment, user_id, attributes):
6385
""" Helper method to validate all pre-conditions before we go ahead to bucket user.
@@ -105,6 +127,10 @@ def activate(self, experiment_key, user_id, attributes=None):
105127
None if user is not in experiment or if experiment is not Running.
106128
"""
107129

130+
if not self.is_valid:
131+
logging.error(enums.Errors.INVALID_DATAFILE.format('activate'))
132+
return None
133+
108134
experiment = self.config.get_experiment_from_key(experiment_key)
109135
if not experiment:
110136
self.logger.log(enums.LogLevels.INFO, 'Not activating user "%s".' % user_id)
@@ -126,7 +152,11 @@ def activate(self, experiment_key, user_id, attributes=None):
126152
self.logger.log(enums.LogLevels.DEBUG,
127153
'Dispatching impression event to URL %s with params %s.' % (impression_event.url,
128154
impression_event.params))
129-
self.event_dispatcher.dispatch_event(impression_event)
155+
try:
156+
self.event_dispatcher.dispatch_event(impression_event)
157+
except:
158+
error = sys.exc_info()[1]
159+
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch impression event. Error: %s' % str(error))
130160

131161
return variation.key
132162

@@ -140,13 +170,17 @@ def track(self, event_key, user_id, attributes=None, event_value=None):
140170
event_value: Value associated with the event. Can be used to represent revenue in cents.
141171
"""
142172

173+
if not self.is_valid:
174+
logging.error(enums.Errors.INVALID_DATAFILE.format('track'))
175+
return
176+
143177
if attributes and not validator.are_attributes_valid(attributes):
144178
self.logger.log(enums.LogLevels.ERROR, 'Provided attributes are in an invalid format.')
145179
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT))
146180
return
147181

148182
event = self.config.get_event(event_key)
149-
if not event.experimentIds:
183+
if not event:
150184
self.logger.log(enums.LogLevels.INFO, 'Not tracking user "%s" for event "%s".' % (user_id, event_key))
151185
return
152186

@@ -167,7 +201,14 @@ def track(self, event_key, user_id, attributes=None, event_value=None):
167201
self.logger.log(enums.LogLevels.DEBUG,
168202
'Dispatching conversion event to URL %s with params %s.' % (conversion_event.url,
169203
conversion_event.params))
170-
self.event_dispatcher.dispatch_event(conversion_event)
204+
try:
205+
self.event_dispatcher.dispatch_event(conversion_event)
206+
except:
207+
error = sys.exc_info()[1]
208+
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch conversion event. Error: %s' % str(error))
209+
210+
else:
211+
self.logger.log(enums.LogLevels.INFO, 'There are no valid experiments for event "%s" to track.' % event_key)
171212

172213
def get_variation(self, experiment_key, user_id, attributes=None):
173214
""" Gets variation where user will be bucketed.
@@ -182,6 +223,10 @@ def get_variation(self, experiment_key, user_id, attributes=None):
182223
None if user is not in experiment or if experiment is not Running.
183224
"""
184225

226+
if not self.is_valid:
227+
logging.error(enums.Errors.INVALID_DATAFILE.format('get_variation'))
228+
return None
229+
185230
experiment = self.config.get_experiment_from_key(experiment_key)
186231
if not experiment:
187232
return None
@@ -194,4 +239,3 @@ def get_variation(self, experiment_key, user_id, attributes=None):
194239
return variation.key
195240

196241
return None
197-

optimizely/project_config.py

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ def __init__(self, datafile, logger, error_handler):
3737
self.audiences = config.get('audiences', [])
3838

3939
# Utility maps for quick lookup
40-
self.group_id_map = self._generate_key_map_entity(self.groups, 'id', entities.Group)
41-
self.experiment_key_map = self._generate_key_map_entity(self.experiments, 'key', entities.Experiment)
42-
self.event_key_map = self._generate_key_map_entity(self.events, 'key', entities.Event)
43-
self.attribute_key_map = self._generate_key_map_entity(self.attributes, 'key', entities.Attribute)
44-
self.audience_id_map = self._generate_key_map_entity(self.audiences, 'id', entities.Audience)
40+
self.group_id_map = self._generate_key_map(self.groups, 'id', entities.Group)
41+
self.experiment_key_map = self._generate_key_map(self.experiments, 'key', entities.Experiment)
42+
self.event_key_map = self._generate_key_map(self.events, 'key', entities.Event)
43+
self.attribute_key_map = self._generate_key_map(self.attributes, 'key', entities.Attribute)
44+
self.audience_id_map = self._generate_key_map(self.audiences, 'id', entities.Audience)
4545
self.audience_id_map = self._deserialize_audience(self.audience_id_map)
4646
for group in self.group_id_map.values():
47-
experiments_in_group_key_map = self._generate_key_map_entity(group.experiments, 'key', entities.Experiment)
47+
experiments_in_group_key_map = self._generate_key_map(group.experiments, 'key', entities.Experiment)
4848
for experiment in experiments_in_group_key_map.values():
4949
experiment.__dict__.update({
5050
'groupId': group.id,
@@ -57,33 +57,15 @@ def __init__(self, datafile, logger, error_handler):
5757
self.variation_id_map = {}
5858
for experiment in self.experiment_key_map.values():
5959
self.experiment_id_map[experiment.id] = experiment
60-
self.variation_key_map[experiment.key] = self._generate_key_map_entity(
60+
self.variation_key_map[experiment.key] = self._generate_key_map(
6161
experiment.variations, 'key', entities.Variation
6262
)
6363
self.variation_id_map[experiment.key] = {}
6464
for variation in self.variation_key_map.get(experiment.key).values():
6565
self.variation_id_map[experiment.key][variation.id] = variation
6666

6767
@staticmethod
68-
def _generate_key_map(list, key):
69-
""" Helper method to generate map from key to dict in list of dicts.
70-
71-
Args:
72-
list: List consisting of dict.
73-
key: Key in each dict which will be key in the map.
74-
75-
Returns:
76-
Map mapping key to dict.
77-
"""
78-
79-
key_map = {}
80-
for obj in list:
81-
key_map[obj[key]] = obj
82-
83-
return key_map
84-
85-
@staticmethod
86-
def _generate_key_map_entity(list, key, entity_class):
68+
def _generate_key_map(list, key, entity_class):
8769
""" Helper method to generate map from key to entity object for given list of dicts.
8870
8971
Args:

tests/test_event_builder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import unittest
33

44
from optimizely import event_builder
5+
from optimizely import exceptions
56
from optimizely import version
7+
from optimizely.helpers import enums
68
from . import base
79

810

@@ -183,7 +185,7 @@ def test_get_event_builder__invalid_datafile_version(self):
183185
""" Test that get_event_builder raises exception for unsupported datafile version. """
184186

185187
with mock.patch('optimizely.project_config.ProjectConfig.get_version', return_value='unsupported_version'):
186-
self.assertRaisesRegexp(Exception, 'Datafile provided has unsupported version.',
188+
self.assertRaisesRegexp(exceptions.InvalidInputException, enums.Errors.UNSUPPORTED_DATAFILE_VERSION,
187189
event_builder.get_event_builder, self.optimizely.config, self.optimizely.bucketer)
188190

189191
def test_create_impression_event(self):

0 commit comments

Comments
 (0)