-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(ep-factory): Implemented Event Factory and User Event Factory #194
Changes from 25 commits
232f0a0
2320939
fbb5a68
c162465
633b095
8a5f531
35fcd82
d7201cb
c829fca
c3c9d46
a2eeb85
a045291
23ab6ce
6474e98
1184568
592a306
c4a412a
2a3374c
ce44c11
d4f2c66
80b0963
071460a
900d96d
8cd8547
e628e0f
f851754
65a752b
8998580
c3d5b07
b5d461b
5ef24de
1d5aeca
f97d430
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# Copyright 2019, Optimizely | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# Copyright 2019 Optimizely | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from .user_event import ConversionEvent, ImpressionEvent | ||
from .payload import Decision, EventBatch, Snapshot, SnapshotEvent, Visitor, VisitorAttribute | ||
from .log_event import LogEvent | ||
from optimizely.helpers import enums, event_tag_utils, validator | ||
|
||
CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' | ||
|
||
|
||
class EventFactory(object): | ||
""" EventFactory builds LogEvent object from a given UserEvent. | ||
This class serves to separate concerns between events in the SDK and the API used | ||
to record the events via the Optimizely Events API ("https://developers.optimizely.com/x/events/api/index.html") | ||
""" | ||
|
||
EVENT_ENDPOINT = 'https://logx.optimizely.com/v1/events' | ||
HTTP_VERB = 'POST' | ||
HTTP_HEADERS = {'Content-Type': 'application/json'} | ||
ACTIVATE_EVENT_KEY = 'campaign_activated' | ||
|
||
@classmethod | ||
def create_log_event(cls, user_events, logger): | ||
""" Create LogEvent instance. | ||
|
||
Args: | ||
user_events: An array of UserEvent instances. | ||
logger: Provides a logger instance. | ||
|
||
Returns: | ||
LogEvent instance. | ||
""" | ||
|
||
if not isinstance(user_events, list): | ||
mjc1283 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
user_events = [user_events] | ||
|
||
visitors = [] | ||
|
||
for user_event in user_events: | ||
visitor = cls._create_visitor(user_event, logger) | ||
|
||
if visitor: | ||
visitors.append(visitor) | ||
|
||
user_context = user_event.event_context | ||
|
||
event_batch = EventBatch( | ||
user_context.account_id, | ||
user_context.project_id, | ||
user_context.revision, | ||
user_context.client_name, | ||
user_context.client_version, | ||
user_context.anonymize_ip, | ||
True | ||
) | ||
|
||
if len(visitors) == 0: | ||
return None | ||
|
||
event_batch.visitors = visitors | ||
|
||
event_params = event_batch.get_event_params() | ||
|
||
return LogEvent(cls.EVENT_ENDPOINT, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS) | ||
|
||
@classmethod | ||
def _create_visitor(cls, user_event, logger): | ||
mjc1283 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Helper method to create Visitor instance for event_batch. """ | ||
|
||
if isinstance(user_event, ImpressionEvent): | ||
decision = Decision( | ||
user_event.experiment.layerId, | ||
user_event.experiment.id, | ||
user_event.variation.id, | ||
) | ||
|
||
snapshot_event = SnapshotEvent( | ||
user_event.experiment.layerId, | ||
user_event.uuid, | ||
cls.ACTIVATE_EVENT_KEY, | ||
user_event.timestamp | ||
) | ||
|
||
snapshot = Snapshot([snapshot_event], [decision]) | ||
|
||
visitor = Visitor([snapshot], user_event.visitor_attributes, user_event.user_id) | ||
|
||
return visitor | ||
|
||
elif isinstance(user_event, ConversionEvent): | ||
revenue = event_tag_utils.get_revenue_value(user_event.event_tags) | ||
value = event_tag_utils.get_numeric_value(user_event.event_tags, logger) | ||
|
||
snapshot_event = SnapshotEvent( | ||
user_event.event.id, | ||
user_event.uuid, | ||
user_event.event.key, | ||
user_event.timestamp, | ||
revenue, | ||
value, | ||
user_event.event_tags | ||
) | ||
|
||
snapshot = Snapshot([snapshot_event]) | ||
|
||
visitor = Visitor([snapshot], user_event.visitor_attributes, user_event.user_id) | ||
|
||
return visitor | ||
|
||
else: | ||
logger.error('Invalid user event.') | ||
return None | ||
|
||
@staticmethod | ||
def build_attribute_list(attributes, project_config): | ||
""" Create Vistor Attribute List. | ||
|
||
Args: | ||
attributes: Dict representing user attributes and values which need to be recorded or None. | ||
project_config: Instance of ProjectConfig. | ||
|
||
Returns: | ||
List consisting of valid attributes for the user. Empty otherwise. | ||
""" | ||
|
||
if project_config is None: | ||
return None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's return an empty list in this case, instead of |
||
|
||
attributes_list = [] | ||
|
||
if isinstance(attributes, dict): | ||
mjc1283 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for attribute_key in attributes.keys(): | ||
attribute_value = attributes.get(attribute_key) | ||
# Omit attribute values that are not supported by the log endpoint. | ||
if validator.is_attribute_valid(attribute_key, attribute_value): | ||
attribute_id = project_config.get_attribute_id(attribute_key) | ||
if attribute_id: | ||
attributes_list.append( | ||
VisitorAttribute( | ||
attribute_id, | ||
attribute_key, | ||
CUSTOM_ATTRIBUTE_FEATURE_TYPE, | ||
attribute_value) | ||
) | ||
|
||
# Append Bot Filtering Attribute | ||
bot_filtering_value = project_config.get_bot_filtering_value() | ||
if isinstance(bot_filtering_value, bool): | ||
attributes_list.append( | ||
VisitorAttribute( | ||
enums.ControlAttributes.BOT_FILTERING, | ||
enums.ControlAttributes.BOT_FILTERING, | ||
CUSTOM_ATTRIBUTE_FEATURE_TYPE, | ||
bot_filtering_value) | ||
) | ||
|
||
return attributes_list |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Copyright 2019 Optimizely | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
|
||
class LogEvent(object): | ||
""" Representation of an event which can be sent to the Optimizely logging endpoint. """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. |
||
|
||
def __init__(self, url, params, http_verb=None, headers=None): | ||
self.url = url | ||
self.params = params | ||
self.http_verb = http_verb or 'GET' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. Let's start defaulting to |
||
self.headers = headers |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# Copyright 2019 Optimizely | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import json | ||
|
||
|
||
class EventBatch(object): | ||
""" Class respresenting Event Batch. """ | ||
|
||
def __init__(self, account_id, project_id, revision, client_name, client_version, | ||
anonymize_ip, enrich_decisions=True, visitors=None): | ||
self.account_id = account_id | ||
self.project_id = project_id | ||
self.revision = revision | ||
self.client_name = client_name | ||
self.client_version = client_version | ||
self.anonymize_ip = anonymize_ip | ||
self.enrich_decisions = enrich_decisions | ||
self.visitors = visitors or [] | ||
|
||
def __eq__(self, other): | ||
batch_obj = self.get_event_params() | ||
return batch_obj == other | ||
|
||
def _dict_clean(self, obj): | ||
""" Helper method to remove keys from dictionary with None values. """ | ||
|
||
result = {} | ||
for k, v in obj: | ||
if v is None and k in ['revenue', 'value', 'tags', 'decisions']: | ||
continue | ||
else: | ||
result[k] = v | ||
return result | ||
|
||
def get_event_params(self): | ||
""" Method to return valid params for LogEvent payload. """ | ||
|
||
return json.loads( | ||
json.dumps(self.__dict__, default=lambda o: o.__dict__), | ||
object_pairs_hook=self._dict_clean | ||
) | ||
|
||
|
||
class Decision(object): | ||
""" Class respresenting Decision. """ | ||
|
||
def __init__(self, campaign_id, experiment_id, variation_id): | ||
self.campaign_id = campaign_id | ||
self.experiment_id = experiment_id | ||
self.variation_id = variation_id | ||
|
||
|
||
class Snapshot(object): | ||
""" Class representing Snapshot. """ | ||
|
||
def __init__(self, events, decisions=None): | ||
self.events = events | ||
self.decisions = decisions | ||
|
||
|
||
class SnapshotEvent(object): | ||
""" Class representing Snapshot Event. """ | ||
|
||
def __init__(self, entity_id, uuid, key, timestamp, revenue=None, value=None, tags=None): | ||
self.entity_id = entity_id | ||
self.uuid = uuid | ||
self.key = key | ||
self.timestamp = timestamp | ||
self.revenue = revenue | ||
self.value = value | ||
self.tags = tags | ||
|
||
|
||
class Visitor(object): | ||
""" Class representing Visitor. """ | ||
|
||
def __init__(self, snapshots, attributes, visitor_id): | ||
self.snapshots = snapshots | ||
self.attributes = attributes | ||
self.visitor_id = visitor_id | ||
|
||
|
||
class VisitorAttribute(object): | ||
""" Class representing Visitor Attribute. """ | ||
|
||
def __init__(self, entity_id, key, attribute_type, value): | ||
self.entity_id = entity_id | ||
self.key = key | ||
self.type = attribute_type | ||
self.value = value |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Copyright 2019 Optimizely | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import time | ||
import uuid | ||
|
||
from optimizely import version | ||
|
||
CLIENT_NAME = 'python-sdk' | ||
|
||
|
||
class UserEvent(object): | ||
""" Class respresenting User Event. """ | ||
|
||
def __init__(self, event_context, user_id, visitor_attributes, bot_filtering=None): | ||
self.event_context = event_context | ||
self.user_id = user_id | ||
self.visitor_attributes = visitor_attributes | ||
self.bot_filtering = bot_filtering | ||
self.uuid = self._get_uuid() | ||
self.timestamp = self._get_time() | ||
|
||
def _get_time(self): | ||
return int(round(time.time() * 1000)) | ||
|
||
def _get_uuid(self): | ||
return str(uuid.uuid4()) | ||
|
||
|
||
class ImpressionEvent(UserEvent): | ||
""" Class representing Impression Event. """ | ||
|
||
def __init__(self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None): | ||
super(ImpressionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering) | ||
self.experiment = experiment | ||
self.variation = variation | ||
|
||
|
||
class ConversionEvent(UserEvent): | ||
""" Class representing Conversion Event. """ | ||
|
||
def __init__(self, event_context, event, user_id, visitor_attributes, event_tags, bot_filtering=None): | ||
super(ConversionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering) | ||
self.event = event | ||
self.event_tags = event_tags | ||
|
||
|
||
class EventContext(object): | ||
""" Class respresenting User Event Context. """ | ||
|
||
def __init__(self, account_id, project_id, revision, anonymize_ip): | ||
self.account_id = account_id | ||
self.project_id = project_id | ||
self.revision = revision | ||
self.client_name = CLIENT_NAME | ||
self.client_version = version.__version__ | ||
self.anonymize_ip = anonymize_ip |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should have mentioned earlier, but import order here is incorrect
It should be external packages (alphabetic), internal packages (alphabetic), files from same package (alphabetic)