Skip to content
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

Merged
merged 33 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
232f0a0
feat: Add event_batch datamodels.
Jul 19, 2019
2320939
fix: unit test fixes.
Jul 22, 2019
fbb5a68
Merge branch 'master' into epmodels
mnoman09 Jul 22, 2019
c162465
feat: add event_factory and user_event_factory for event processing.
Jul 24, 2019
633b095
Removed third party dependency
msohailhussain Jul 26, 2019
8a5f531
Merge branch 'master' into sohail/pr-189
mnoman09 Aug 1, 2019
35fcd82
Merge remote-tracking branch 'origin/sohail/pr-189' into sohail/pr-193
mnoman09 Aug 1, 2019
d7201cb
Removed HasAttr
mnoman09 Aug 1, 2019
c829fca
Merge branch 'master' into sohail/pr-189
mnoman09 Aug 16, 2019
c3c9d46
Addressed feedback. Restructured classes.
msohailhussain Aug 20, 2019
a2eeb85
Merge branch 'sohail/pr-189' into sohail/pr-193
mnoman09 Aug 20, 2019
a045291
update: structural changes.
Aug 20, 2019
23ab6ce
fix: addressed more feedback
Aug 21, 2019
6474e98
update: removed print statements.
mnoman09 Aug 21, 2019
1184568
merged PR 189
mnoman09 Aug 21, 2019
592a306
fix: use VisitorAttribute class to create visitor attributes list.
Aug 21, 2019
c4a412a
fix: fix log_event payload.
Aug 22, 2019
2a3374c
removed commented code
mnoman09 Aug 22, 2019
ce44c11
fix linter error.
mnoman09 Aug 22, 2019
d4f2c66
Merge branch 'master' into sohail/pr-189
rashidsp Aug 28, 2019
80b0963
fix: addressed minor feedback.
Aug 28, 2019
071460a
fix: addressed minor feedback.
Aug 28, 2019
900d96d
Merge branch 'sohail/pr-189' of github.com:optimizely/python-sdk into…
Aug 28, 2019
8cd8547
Merge branch 'sohail/pr-189' into sohail/pr-193
Aug 28, 2019
e628e0f
fix: addressed review comments.
mariamjamal94 Sep 2, 2019
f851754
fix: addresses review comments.
mariamjamal94 Sep 4, 2019
65a752b
fix: addresses review comment.
mariamjamal94 Sep 5, 2019
8998580
fix: updated docstring for LogEvent.
mariamjamal94 Sep 6, 2019
c3d5b07
Merge branch 'master' into sohail/pr-193
mariamjamal94 Sep 12, 2019
b5d461b
Merge branch 'master' into sohail/pr-193
aliabbasrizvi Sep 16, 2019
5ef24de
fix: address import order issues.
mariamjamal94 Sep 18, 2019
1d5aeca
Merge branch 'master' into sohail/pr-193
oakbani Sep 20, 2019
f97d430
nit addressed
msohailhussain Sep 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
179 changes: 179 additions & 0 deletions optimizely/event/event_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# 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 optimizely.helpers import enums
from optimizely.helpers import event_tag_utils
from optimizely.helpers import validator
from . import user_event
from . import payload
from . import log_event
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Last 3 imports are not alphabetic.


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: A single UserEvent instance or a list 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 event in user_events:
visitor = cls._create_visitor(event, logger)

if visitor:
visitors.append(visitor)

user_context = event.event_context

event_batch = payload.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 log_event.LogEvent(cls.EVENT_ENDPOINT, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS)

@classmethod
def _create_visitor(cls, event, logger):
""" Helper method to create Visitor instance for event_batch.

Args:
event: Instance of UserEvent.
logger: Provides a logger instance.

Returns:
Instance of Visitor. None if:
- event is invalid.
"""

if isinstance(event, user_event.ImpressionEvent):
decision = payload.Decision(
event.experiment.layerId,
event.experiment.id,
event.variation.id,
)

snapshot_event = payload.SnapshotEvent(
event.experiment.layerId,
event.uuid,
cls.ACTIVATE_EVENT_KEY,
event.timestamp
)

snapshot = payload.Snapshot([snapshot_event], [decision])

visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id)

return visitor

elif isinstance(event, user_event.ConversionEvent):
revenue = event_tag_utils.get_revenue_value(event.event_tags)
value = event_tag_utils.get_numeric_value(event.event_tags, logger)

snapshot_event = payload.SnapshotEvent(
event.event.id,
event.uuid,
event.event.key,
event.timestamp,
revenue,
value,
event.event_tags
)

snapshot = payload.Snapshot([snapshot_event])

visitor = payload.Visitor([snapshot], event.visitor_attributes, 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.
"""

attributes_list = []

if project_config is None:
return 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(
payload.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(
payload.VisitorAttribute(
enums.ControlAttributes.BOT_FILTERING,
enums.ControlAttributes.BOT_FILTERING,
CUSTOM_ATTRIBUTE_FEATURE_TYPE,
bot_filtering_value)
)

return attributes_list
22 changes: 22 additions & 0 deletions optimizely/event/log_event.py
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 Optimizely events API. """

def __init__(self, url, params, http_verb=None, headers=None):
self.url = url
self.params = params
self.http_verb = http_verb or 'POST'
self.headers = headers
11 changes: 9 additions & 2 deletions optimizely/event/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ def __init__(self, account_id, project_id, revision, client_name, client_version
self.visitors = visitors or []

def __eq__(self, other):
batch_obj = json.loads(json.dumps(self.__dict__, default=lambda o: o.__dict__),
object_pairs_hook=self._dict_clean)
batch_obj = self.get_event_params()
return batch_obj == other

def _dict_clean(self, obj):
Expand All @@ -44,6 +43,14 @@ def _dict_clean(self, obj):
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. """
Expand Down
88 changes: 88 additions & 0 deletions optimizely/event/user_event_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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 . import event_factory
from . import user_event


class UserEventFactory(object):
""" UserEventFactory builds impression and conversion events from a given UserEvent. """

@classmethod
def create_impression_event(cls, project_config, activated_experiment, variation_id, user_id, user_attributes):
""" Create impression Event to be sent to the logging endpoint.

Args:
project_config: Instance of ProjectConfig.
experiment: Experiment for which impression needs to be recorded.
variation_id: ID for variation which would be presented to user.
user_id: ID for user.
attributes: Dict representing user attributes and values which need to be recorded.

Returns:
Event object encapsulating the impression event. None if:
- activated_experiment is None.
"""

if not activated_experiment:
return None

experiment_key = activated_experiment.key
variation = project_config.get_variation_from_id(experiment_key, variation_id)

event_context = user_event.EventContext(
project_config.account_id,
project_config.project_id,
project_config.revision,
project_config.anonymize_ip
)

return user_event.ImpressionEvent(
event_context,
user_id,
activated_experiment,
event_factory.EventFactory.build_attribute_list(user_attributes, project_config),
variation,
project_config.get_bot_filtering_value()
)

@classmethod
def create_conversion_event(cls, project_config, event_key, user_id, user_attributes, event_tags):
""" Create conversion Event to be sent to the logging endpoint.

Args:
project_config: Instance of ProjectConfig.
event_key: Key representing the event which needs to be recorded.
user_id: ID for user.
attributes: Dict representing user attributes and values.
event_tags: Dict representing metadata associated with the event.

Returns:
Event object encapsulating the conversion event.
"""

event_context = user_event.EventContext(
project_config.account_id,
project_config.project_id,
project_config.revision,
project_config.anonymize_ip
)

return user_event.ConversionEvent(
event_context,
project_config.get_event(event_key),
user_id,
event_factory.EventFactory.build_attribute_list(user_attributes, project_config),
event_tags,
project_config.get_bot_filtering_value()
)