Skip to content

Commit

Permalink
Merge 47ffc9a into 9577cab
Browse files Browse the repository at this point in the history
  • Loading branch information
alda-optimizely committed Oct 9, 2017
2 parents 9577cab + 47ffc9a commit bb8d1f1
Show file tree
Hide file tree
Showing 23 changed files with 1,465 additions and 571 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -3,6 +3,7 @@ python:
- "2.7"
- "3.4"
install: "pip install -r requirements/core.txt;pip install -r requirements/test.txt"
before_script: "pep8"
script: "nosetests --with-coverage --cover-package=optimizely"
after_success:
- coveralls
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,8 @@
# 1.3.0
- Forced bucketing.
- Numeric metrics.
- Updated event builder to support new endpoint.

# 1.2.1
- Removed older feature flag parsing.

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -21,7 +21,7 @@ We welcome contributions and feedback! All contributors must sign our [Contribut

## Style

We enforce PEP-8 rules with a few minor deviations.
We enforce PEP-8 rules with a few minor [deviations](https://github.com/optimizely/python-sdk/blob/master/tox.ini).

## License

Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -74,7 +74,7 @@ nosetests tests.test_event:EventTest
To run a single test you can use the following command:

```
nosetests tests.<file_name_without_extension>:ClassName:test_name
nosetests tests.<file_name_without_extension>:ClassName.test_name
```

For example, to run `test_event.EventTest.test_dispatch`, the command would be:
Expand Down
5 changes: 5 additions & 0 deletions optimizely/decision_service.py
Expand Up @@ -99,6 +99,11 @@ def get_variation(self, experiment, user_id, attributes):
self.logger.log(enums.LogLevels.INFO, 'Experiment "%s" is not running.' % experiment.key)
return None

# Check if the user is forced into a variation
variation = self.config.get_forced_variation(experiment.key, user_id)
if variation:
return variation

# Check to see if user is white-listed for a certain variation
variation = self.get_forced_variation(experiment, user_id)
if variation:
Expand Down
1 change: 1 addition & 0 deletions optimizely/entities.py
Expand Up @@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.


class BaseEntity(object):

def __eq__(self, other):
Expand Down
1 change: 1 addition & 0 deletions optimizely/error_handler.py
Expand Up @@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.


class BaseErrorHandler(object):
""" Class encapsulating exception handling functionality.
Override with your own exception handler providing handle_error method. """
Expand Down
170 changes: 96 additions & 74 deletions optimizely/event_builder.py
Expand Up @@ -12,6 +12,7 @@
# limitations under the License.

import time
import uuid
from abc import abstractmethod
from abc import abstractproperty

Expand Down Expand Up @@ -97,34 +98,32 @@ def _add_common_params(self, user_id, attributes):

class EventBuilder(BaseEventBuilder):
""" Class which encapsulates methods to build events for tracking
impressions and conversions using the new endpoints. """
impressions and conversions using the new V3 event API (batch). """

IMPRESSION_ENDPOINT = 'https://logx.optimizely.com/log/decision'
CONVERSION_ENDPOINT = 'https://logx.optimizely.com/log/event'
EVENTS_URL = 'https://logx.optimizely.com/v1/events'
HTTP_VERB = 'POST'
HTTP_HEADERS = {'Content-Type': 'application/json'}

class EventParams(object):
ACCOUNT_ID = 'accountId'
PROJECT_ID = 'projectId'
LAYER_ID = 'layerId'
EXPERIMENT_ID = 'experimentId'
VARIATION_ID = 'variationId'
END_USER_ID = 'visitorId'
EVENT_ID = 'eventEntityId'
EVENT_NAME = 'eventName'
EVENT_METRICS = 'eventMetrics'
EVENT_FEATURES = 'eventFeatures'
USER_FEATURES = 'userFeatures'
DECISION = 'decision'
LAYER_STATES = 'layerStates'
REVISION = 'revision'
ACCOUNT_ID = 'account_id'
PROJECT_ID = 'project_id'
EXPERIMENT_ID = 'experiment_id'
CAMPAIGN_ID = 'campaign_id'
VARIATION_ID = 'variation_id'
END_USER_ID = 'visitor_id'
EVENTS = 'events'
EVENT_ID = 'entity_id'
ATTRIBUTES = 'attributes'
DECISIONS = 'decisions'
TIME = 'timestamp'
SOURCE_SDK_TYPE = 'clientEngine'
SOURCE_SDK_VERSION = 'clientVersion'
ACTION_TRIGGERED = 'actionTriggered'
IS_GLOBAL_HOLDBACK = 'isGlobalHoldback'
IS_LAYER_HOLDBACK = 'isLayerHoldback'
KEY = 'key'
TAGS = 'tags'
UUID = 'uuid'
USERS = 'visitors'
SNAPSHOTS = 'snapshots'
SOURCE_SDK_TYPE = 'client_name'
SOURCE_SDK_VERSION = 'client_version'
CUSTOM = 'custom'

def _add_attributes(self, attributes):
""" Add attribute(s) information to the event.
Expand All @@ -133,7 +132,9 @@ def _add_attributes(self, attributes):
attributes: Dict representing user attributes and values which need to be recorded.
"""

self.params[self.EventParams.USER_FEATURES] = []
visitor = self.params[self.EventParams.USERS][0]
visitor[self.EventParams.ATTRIBUTES] = []

if not attributes:
return

Expand All @@ -143,12 +144,11 @@ def _add_attributes(self, attributes):
if attribute_value:
attribute = self.config.get_attribute(attribute_key)
if attribute:
self.params[self.EventParams.USER_FEATURES].append({
'id': attribute.id,
'name': attribute_key,
'type': 'custom',
visitor[self.EventParams.ATTRIBUTES].append({
self.EventParams.EVENT_ID: attribute.id,
'key': attribute_key,
'type': self.EventParams.CUSTOM,
'value': attribute_value,
'shouldIndex': True
})

def _add_source(self):
Expand All @@ -157,30 +157,58 @@ def _add_source(self):
self.params[self.EventParams.SOURCE_SDK_TYPE] = 'python-sdk'
self.params[self.EventParams.SOURCE_SDK_VERSION] = version.__version__

def _add_revision(self):
""" Add datafile revision information to the event. """
self.params[self.EventParams.REVISION] = self.config.get_revision()

def _add_time(self):
""" Add time information to the event. """

self.params[self.EventParams.TIME] = int(round(time.time() * 1000))

def _add_visitor(self, user_id):
""" Add user to the event """

self.params[self.EventParams.USERS] = []
# Add a single visitor
visitor = {}
visitor[self.EventParams.END_USER_ID] = user_id
visitor[self.EventParams.SNAPSHOTS] = []
self.params[self.EventParams.USERS].append(visitor)

def _add_common_params(self, user_id, attributes):
""" Add params which are used same in both conversion and impression events.
Args:
user_id: ID for user.
attributes: Dict representing user attributes and values which need to be recorded.
"""
self._add_project_id()
self._add_account_id()
self._add_visitor(user_id)
self._add_attributes(attributes)
self._add_source()

def _add_required_params_for_impression(self, experiment, variation_id):
""" Add parameters that are required for the impression event to register.
Args:
experiment: Experiment for which impression needs to be recorded.
variation_id: ID for variation which would be presented to user.
"""
snapshot = {}

self.params[self.EventParams.IS_GLOBAL_HOLDBACK] = False
self.params[self.EventParams.LAYER_ID] = experiment.layerId
self.params[self.EventParams.DECISION] = {
snapshot[self.EventParams.DECISIONS] = [{
self.EventParams.EXPERIMENT_ID: experiment.id,
self.EventParams.VARIATION_ID: variation_id,
self.EventParams.IS_LAYER_HOLDBACK: False
}
self.EventParams.CAMPAIGN_ID: experiment.layerId
}]

snapshot[self.EventParams.EVENTS] = [{
self.EventParams.EVENT_ID: experiment.layerId,
self.EventParams.TIME: int(round(time.time() * 1000)),
self.EventParams.KEY: 'campaign_activated',
self.EventParams.UUID: str(uuid.uuid4())
}]

visitor = self.params[self.EventParams.USERS][0]
visitor[self.EventParams.SNAPSHOTS].append(snapshot)

def _add_required_params_for_conversion(self, event_key, event_tags, decisions):
""" Add parameters that are required for the conversion event to register.
Expand All @@ -191,47 +219,40 @@ def _add_required_params_for_conversion(self, event_key, event_tags, decisions):
decisions: List of tuples representing valid experiments IDs and variation IDs.
"""

self.params[self.EventParams.IS_GLOBAL_HOLDBACK] = False
self.params[self.EventParams.EVENT_FEATURES] = []
self.params[self.EventParams.EVENT_METRICS] = []

if event_tags:
event_value = event_tag_utils.get_revenue_value(event_tags)
if event_value is not None:
self.params[self.EventParams.EVENT_METRICS] = [{
'name': event_tag_utils.EVENT_VALUE_METRIC,
'value': event_value
}]

for event_tag_id in event_tags.keys():
event_tag_value = event_tags.get(event_tag_id)
if event_tag_value is None:
continue

event_feature = {
'name': event_tag_id,
'type': 'custom',
'value': event_tag_value,
'shouldIndex': False,
}
self.params[self.EventParams.EVENT_FEATURES].append(event_feature)
visitor = self.params[self.EventParams.USERS][0]

self.params[self.EventParams.LAYER_STATES] = []
for experiment_id, variation_id in decisions:
snapshot = {}
experiment = self.config.get_experiment_from_id(experiment_id)
self.params[self.EventParams.LAYER_STATES].append({
self.EventParams.LAYER_ID: experiment.layerId,
self.EventParams.REVISION: self.config.get_revision(),
self.EventParams.ACTION_TRIGGERED: True,
self.EventParams.DECISION: {
self.EventParams.EXPERIMENT_ID: experiment.id,

if variation_id:
snapshot[self.EventParams.DECISIONS] = [{
self.EventParams.EXPERIMENT_ID: experiment_id,
self.EventParams.VARIATION_ID: variation_id,
self.EventParams.IS_LAYER_HOLDBACK: False
self.EventParams.CAMPAIGN_ID: experiment.layerId
}]

event_dict = {
self.EventParams.EVENT_ID: self.config.get_event(event_key).id,
self.EventParams.TIME: int(round(time.time() * 1000)),
self.EventParams.KEY: event_key,
self.EventParams.UUID: str(uuid.uuid4())
}
})

self.params[self.EventParams.EVENT_ID] = self.config.get_event(event_key).id
self.params[self.EventParams.EVENT_NAME] = event_key
if event_tags:
revenue_value = event_tag_utils.get_revenue_value(event_tags)
if revenue_value is not None:
event_dict[event_tag_utils.REVENUE_METRIC_TYPE] = revenue_value

numeric_value = event_tag_utils.get_numeric_value(event_tags, self.config.logger)
if numeric_value is not None:
event_dict[event_tag_utils.NUMERIC_METRIC_TYPE] = numeric_value

if len(event_tags) > 0:
event_dict[self.EventParams.TAGS] = event_tags

snapshot[self.EventParams.EVENTS] = [event_dict]
visitor[self.EventParams.SNAPSHOTS].append(snapshot)

def create_impression_event(self, experiment, variation_id, user_id, attributes):
""" Create impression Event to be sent to the logging endpoint.
Expand All @@ -249,7 +270,8 @@ def create_impression_event(self, experiment, variation_id, user_id, attributes)
self.params = {}
self._add_common_params(user_id, attributes)
self._add_required_params_for_impression(experiment, variation_id)
return Event(self.IMPRESSION_ENDPOINT,

return Event(self.EVENTS_URL,
self.params,
http_verb=self.HTTP_VERB,
headers=self.HTTP_HEADERS)
Expand All @@ -271,7 +293,7 @@ def create_conversion_event(self, event_key, user_id, attributes, event_tags, de
self.params = {}
self._add_common_params(user_id, attributes)
self._add_required_params_for_conversion(event_key, event_tags, decisions)
return Event(self.CONVERSION_ENDPOINT,
return Event(self.EVENTS_URL,
self.params,
http_verb=self.HTTP_VERB,
headers=self.HTTP_HEADERS)
1 change: 1 addition & 0 deletions optimizely/exceptions.py
Expand Up @@ -11,6 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.


class InvalidAttributeException(Exception):
""" Raised when provided attribute is invalid. """
pass
Expand Down

0 comments on commit bb8d1f1

Please sign in to comment.