Skip to content

Commit

Permalink
🖊️ JS Sync impl
Browse files Browse the repository at this point in the history
  • Loading branch information
oakbani committed Jun 1, 2018
1 parent 6b48a7b commit ca14331
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 161 deletions.
2 changes: 1 addition & 1 deletion optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _get_bucketing_id(user_id, attributes):
"""

attributes = attributes or {}
return attributes.get(enums.ReservedAttributes.BUCKETING_ID, user_id)
return attributes.get(enums.ControlAttributes.BUCKETING_ID, user_id)

def get_forced_variation(self, experiment, user_id):
""" Determine if a user is forced into a variation for the given experiment and return that variation.
Expand Down
35 changes: 12 additions & 23 deletions optimizely/event_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from . import version
from .helpers import event_tag_utils
from .helpers.enums import ReservedAttributes
from .helpers.enums import ControlAttributes


class Event(object):
Expand Down Expand Up @@ -186,34 +186,23 @@ def _get_attributes(self, attributes):
attribute_value = attributes.get(attribute_key)
# Omit falsy attribute values
if attribute_value:
# Check for reserved attributes
reserved_attrs = [ReservedAttributes.BUCKETING_ID, ReservedAttributes.USER_AGENT]
if attribute_key in reserved_attrs:
attribute_id = self.config.get_attribute_id(attribute_key)
if attribute_id:
params.append({
'entity_id': attribute_key,
'entity_id': attribute_id,
'key': attribute_key,
'type': self.EventParams.CUSTOM,
'value': attribute_value
'value': attribute_value,
})

else:
attribute = self.config.get_attribute(attribute_key)
if attribute:
params.append({
'entity_id': attribute.id,
'key': attribute_key,
'type': self.EventParams.CUSTOM,
'value': attribute_value,
})

# Append Bot Filtering Attribute
attribute_key = ReservedAttributes.BOT_FILTERING
params.append({
'entity_id': attribute_key,
'key': attribute_key,
'type': self.EventParams.CUSTOM,
'value': self._get_bot_filtering(),
})
if isinstance(self._get_bot_filtering(), bool):
params.append({
'entity_id': ControlAttributes.BOT_FILTERING,
'key': ControlAttributes.BOT_FILTERING,
'type': self.EventParams.CUSTOM,
'value': self._get_bot_filtering(),
})

return params

Expand Down
2 changes: 1 addition & 1 deletion optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class NotificationTypes(object):
TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"


class ReservedAttributes(object):
class ControlAttributes(object):
BOT_FILTERING = '$opt_bot_filtering'
BUCKETING_ID = '$opt_bucketing_id'
USER_AGENT = '$opt_user_agent'
21 changes: 16 additions & 5 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
SUPPORTED_VERSIONS = [V2_CONFIG_VERSION]
UNSUPPORTED_VERSIONS = [V1_CONFIG_VERSION]

RESERVED_ATTRIBUTE_PREFIX = '$opt_'


class ProjectConfig(object):
""" Representation of the Optimizely project config. """
Expand Down Expand Up @@ -56,7 +58,7 @@ def __init__(self, datafile, logger, error_handler):
self.feature_flags = config.get('featureFlags', [])
self.rollouts = config.get('rollouts', [])
self.anonymize_ip = config.get('anonymizeIP', False)
self.bot_filtering = config.get('botFiltering', False)
self.bot_filtering = config.get('botFiltering', None)

# Utility maps for quick lookup
self.group_id_map = self._generate_key_map(self.groups, 'id', entities.Group)
Expand Down Expand Up @@ -364,20 +366,29 @@ def get_event(self, event_key):
self.error_handler.handle_error(exceptions.InvalidEventException(enums.Errors.INVALID_EVENT_KEY_ERROR))
return None

def get_attribute(self, attribute_key):
""" Get attribute for the provided attribute key.
def get_attribute_id(self, attribute_key):
""" Get attribute ID for the provided attribute key.
Args:
attribute_key: Attribute key for which attribute is to be fetched.
Returns:
Attribute corresponding to the provided attribute key.
Attribute ID corresponding to the provided attribute key.
"""

attribute = self.attribute_key_map.get(attribute_key)
has_reserved_prefix = attribute_key.startswith(RESERVED_ATTRIBUTE_PREFIX)

if attribute:
return attribute
if has_reserved_prefix:
self.logger.log(enums.LogLevels.WARNING,
'Attribute %s unexpectedly has reserved prefix %s; using attribute ID instead of reserved attribute name.'
% (attribute_key, RESERVED_ATTRIBUTE_PREFIX))

return attribute.id

if has_reserved_prefix and attribute_key != enums.ControlAttributes.BOT_FILTERING:
return attribute_key

self.logger.log(enums.LogLevels.ERROR, 'Attribute "%s" is not in datafile.' % attribute_key)
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_ERROR))
Expand Down
2 changes: 1 addition & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def setUp(self):
'accountId': '12001',
'projectId': '111111',
'version': '4',
'botFiltering': False,
'botFiltering': True,
'events': [{
'key': 'test_event',
'experimentIds': ['111127'],
Expand Down
54 changes: 41 additions & 13 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,15 +684,15 @@ def test_get_project_id(self):
def test_get_bot_filtering(self):
""" Test that bot filtering is retrieved correctly when using get_bot_filtering_value. """

# Assert bot filtering is False when not provided in data file
# Assert bot filtering is None when not provided in data file
self.assertTrue('botFiltering' not in self.config_dict)
self.assertEqual(False, self.project_config.get_bot_filtering_value())
self.assertIsNone(self.project_config.get_bot_filtering_value())

# Assert bot filtering is retrieved as provided in the data file
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
project_config = opt_obj.config
self.assertEqual(self.config_dict_with_features['botFiltering'],
self.project_config.get_bot_filtering_value()
project_config.get_bot_filtering_value()
)

def test_get_experiment_from_key__valid_key(self):
Expand Down Expand Up @@ -803,16 +803,32 @@ def test_get_event__invalid_key(self):

self.assertIsNone(self.project_config.get_event('invalid_key'))

def test_get_attribute__valid_key(self):
""" Test that attribute is retrieved correctly for valid attribute key. """
def test_get_attribute_id__valid_key(self):
""" Test that attribute ID is retrieved correctly for valid attribute key. """

self.assertEqual(entities.Attribute('111094', 'test_attribute'),
self.project_config.get_attribute('test_attribute'))
self.assertEqual('111094',
self.project_config.get_attribute_id('test_attribute'))

def test_get_attribute__invalid_key(self):
def test_get_attribute_id__invalid_key(self):
""" Test that None is returned when provided attribute key is invalid. """

self.assertIsNone(self.project_config.get_attribute('invalid_key'))
self.assertIsNone(self.project_config.get_attribute_id('invalid_key'))

def test_get_attribute_id__reserved_key(self):
""" Test that Attribute Key is returned as ID when provided attribute key is invalid. """
self.assertEqual('$opt_user_agent',
self.project_config.get_attribute_id('$opt_user_agent'))

def test_get_attribute_id__unknown_key_with_opt_prefix(self):
""" Test that Attribute Key is returned as ID when provided attribute key is not
present in the datafile but has $opt prefix. """
self.assertEqual('$opt_interesting',
self.project_config.get_attribute_id('$opt_interesting'))

def test_get_attribute_id__key_is_bot_filtering_enum(self):
""" Test that None is returned when provided attribute key is
equal to '$opt_bot_filtering'. """
self.assertIsNone(self.project_config.get_attribute_id('$opt_bot_filtering'))

def test_get_group__valid_id(self):
""" Test that group is retrieved correctly for valid group ID. """
Expand Down Expand Up @@ -1152,14 +1168,26 @@ def test_get_event__invalid_key(self):

mock_logging.assert_called_once_with(enums.LogLevels.ERROR, 'Event "invalid_key" is not in datafile.')

def test_get_attribute__invalid_key(self):
def test_get_attribute_id__invalid_key(self):
""" Test that message is logged when provided attribute key is invalid. """

with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
self.project_config.get_attribute('invalid_key')
self.project_config.get_attribute_id('invalid_key')

mock_logging.assert_called_once_with(enums.LogLevels.ERROR, 'Attribute "invalid_key" is not in datafile.')

def test_get_attribute_id__key_with_opt_prefix_but_not_a_control_attribute(self):
""" Test that message is logged when provided attribute key has $opt_ in prefix and
key is not one of the control attributes. """
self.project_config.attribute_key_map['$opt_abc'] = entities.Attribute('007', '$opt_abc')

with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
self.project_config.get_attribute_id('$opt_abc')

mock_logging.assert_called_once_with(enums.LogLevels.WARNING,
("Attribute $opt_abc unexpectedly has reserved prefix $opt_; "
"using attribute ID instead of reserved attribute name."))

def test_get_group__invalid_id(self):
""" Test that message is logged when provided group ID is invalid. """

Expand Down Expand Up @@ -1226,12 +1254,12 @@ def test_get_event__invalid_key(self):
enums.Errors.INVALID_EVENT_KEY_ERROR,
self.project_config.get_event, 'invalid_key')

def test_get_attribute__invalid_key(self):
def test_get_attribute_id__invalid_key(self):
""" Test that exception is raised when provided attribute key is invalid. """

self.assertRaisesRegexp(exceptions.InvalidAttributeException,
enums.Errors.INVALID_ATTRIBUTE_ERROR,
self.project_config.get_attribute, 'invalid_key')
self.project_config.get_attribute_id, 'invalid_key')

def test_get_group__invalid_id(self):
""" Test that exception is raised when provided group ID is invalid. """
Expand Down
Loading

0 comments on commit ca14331

Please sign in to comment.