Skip to content

feat(EventFactory): EventFactory and UserEventFactory #193

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions optimizely/event/entity/conversion_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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)
self.event_context = event_context
self.event = event
self.user_id = user_id
Expand Down
4 changes: 2 additions & 2 deletions optimizely/event/entity/decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


class Decision(object):
def __init__(self, compaign_id, experiment_id, variation_id):
self.campaign_id = compaign_id
def __init__(self, campaign_id, experiment_id, variation_id):
self.campaign_id = campaign_id
self.experiment_id = experiment_id
self.variation_id = variation_id
2 changes: 1 addition & 1 deletion optimizely/event/entity/event_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# 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 version
from ... import version

SDK_VERSION = 'python-sdk'

Expand Down
1 change: 1 addition & 0 deletions optimizely/event/entity/impression_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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)
self.event_context = event_context
self.user_id = user_id
self.experiment = experiment
Expand Down
169 changes: 169 additions & 0 deletions optimizely/event/event_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# 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
from more_itertools.more import always_iterable

from .entity.conversion_event import ConversionEvent
from .entity.decision import Decision
from .entity.event_batch import EventBatch
from .entity.impression_event import ImpressionEvent
from .entity.snapshot import Snapshot
from .entity.snapshot_event import SnapshotEvent
from .entity.visitor import Visitor
from .log_event import LogEvent
from ..helpers import event_tag_utils
from ..helpers import enums
from ..helpers import 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.
"""

visitors = []

for user_event in always_iterable(user_events):
visitors.append(cls._create_visitor(user_event, logger))
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([x for x in visitors if x is not None]) == 0:
return None

event_batch.visitors = visitors

event_batch_json = json.dumps(event_batch.__dict__, default=lambda o: o.__dict__)

return LogEvent(cls.EVENT_ENDPOINT, event_batch_json, cls.HTTP_VERB, cls.HTTP_HEADERS)

@classmethod
def _create_visitor(cls, user_event, logger):
if not user_event:
return None

if isinstance(user_event, ImpressionEvent):
decision = Decision(
user_event.experiment.layerId if hasattr(user_event, 'experiment') else None,
user_event.experiment.id if hasattr(user_event, 'experiment') else None,
user_event.variation.id if hasattr(user_event, 'variation') else None
)

snapshot_event = SnapshotEvent(
user_event.experiment.layerId if hasattr(user_event, 'experiment') else None,
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 if hasattr(user_event, 'event') else None,
user_event.uuid,
user_event.event.key if hasattr(user_event, 'event') else None,
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:
# include log message for invalid event type
return

@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.
project_config: Instance of ProjectConfig.

Returns:
List consisting of valid attributes for the user. Empty otherwise.
"""

if project_config is None:
return None

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({
'entity_id': attribute_id,
'key': attribute_key,
'type': CUSTOM_ATTRIBUTE_FEATURE_TYPE,
'value': attribute_value
})

# Append Bot Filtering Attribute
bot_filtering_value = project_config.get_bot_filtering_value()
if isinstance(bot_filtering_value, bool):
attributes_list.append({
'entity_id': enums.ControlAttributes.BOT_FILTERING,
'key': enums.ControlAttributes.BOT_FILTERING,
'type': CUSTOM_ATTRIBUTE_FEATURE_TYPE,
'value': 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 the Optimizely logging endpoint. """

def __init__(self, url, params, http_verb=None, headers=None):
self.url = url
self.params = params
self.http_verb = http_verb or 'GET'
self.headers = headers
85 changes: 85 additions & 0 deletions optimizely/event/user_event_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 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 .entity.impression_event import ImpressionEvent
from .entity.conversion_event import ConversionEvent
from .entity.event_context import EventContext
from .event_factory import EventFactory


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.
"""

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

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

return ImpressionEvent(
event_context,
user_id,
activated_experiment,
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 = EventContext(
project_config.account_id,
project_config.project_id,
project_config.revision,
project_config.anonymize_ip
)

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