Skip to content

Commit

Permalink
feat(ep-factory): Implemented Event Factory and User Event Factory (#194
Browse files Browse the repository at this point in the history
)
  • Loading branch information
msohailhussain authored and aliabbasrizvi committed Sep 20, 2019
1 parent 239b687 commit 6794260
Show file tree
Hide file tree
Showing 6 changed files with 1,245 additions and 2 deletions.
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 log_event
from . import payload
from . import user_event

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):
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):
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()
)

0 comments on commit 6794260

Please sign in to comment.