Skip to content

Commit 6794260

Browse files
msohailhussainaliabbasrizvi
authored andcommitted
feat(ep-factory): Implemented Event Factory and User Event Factory (#194)
1 parent 239b687 commit 6794260

File tree

6 files changed

+1245
-2
lines changed

6 files changed

+1245
-2
lines changed

optimizely/event/event_factory.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright 2019 Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
from optimizely.helpers import enums
15+
from optimizely.helpers import event_tag_utils
16+
from optimizely.helpers import validator
17+
from . import log_event
18+
from . import payload
19+
from . import user_event
20+
21+
CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'
22+
23+
24+
class EventFactory(object):
25+
""" EventFactory builds LogEvent object from a given UserEvent.
26+
This class serves to separate concerns between events in the SDK and the API used
27+
to record the events via the Optimizely Events API ("https://developers.optimizely.com/x/events/api/index.html")
28+
"""
29+
30+
EVENT_ENDPOINT = 'https://logx.optimizely.com/v1/events'
31+
HTTP_VERB = 'POST'
32+
HTTP_HEADERS = {'Content-Type': 'application/json'}
33+
ACTIVATE_EVENT_KEY = 'campaign_activated'
34+
35+
@classmethod
36+
def create_log_event(cls, user_events, logger):
37+
""" Create LogEvent instance.
38+
39+
Args:
40+
user_events: A single UserEvent instance or a list of UserEvent instances.
41+
logger: Provides a logger instance.
42+
43+
Returns:
44+
LogEvent instance.
45+
"""
46+
47+
if not isinstance(user_events, list):
48+
user_events = [user_events]
49+
50+
visitors = []
51+
52+
for event in user_events:
53+
visitor = cls._create_visitor(event, logger)
54+
55+
if visitor:
56+
visitors.append(visitor)
57+
58+
user_context = event.event_context
59+
60+
event_batch = payload.EventBatch(
61+
user_context.account_id,
62+
user_context.project_id,
63+
user_context.revision,
64+
user_context.client_name,
65+
user_context.client_version,
66+
user_context.anonymize_ip,
67+
True
68+
)
69+
70+
if len(visitors) == 0:
71+
return None
72+
73+
event_batch.visitors = visitors
74+
75+
event_params = event_batch.get_event_params()
76+
77+
return log_event.LogEvent(cls.EVENT_ENDPOINT, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS)
78+
79+
@classmethod
80+
def _create_visitor(cls, event, logger):
81+
""" Helper method to create Visitor instance for event_batch.
82+
83+
Args:
84+
event: Instance of UserEvent.
85+
logger: Provides a logger instance.
86+
87+
Returns:
88+
Instance of Visitor. None if:
89+
- event is invalid.
90+
"""
91+
92+
if isinstance(event, user_event.ImpressionEvent):
93+
decision = payload.Decision(
94+
event.experiment.layerId,
95+
event.experiment.id,
96+
event.variation.id,
97+
)
98+
99+
snapshot_event = payload.SnapshotEvent(
100+
event.experiment.layerId,
101+
event.uuid,
102+
cls.ACTIVATE_EVENT_KEY,
103+
event.timestamp
104+
)
105+
106+
snapshot = payload.Snapshot([snapshot_event], [decision])
107+
108+
visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id)
109+
110+
return visitor
111+
112+
elif isinstance(event, user_event.ConversionEvent):
113+
revenue = event_tag_utils.get_revenue_value(event.event_tags)
114+
value = event_tag_utils.get_numeric_value(event.event_tags, logger)
115+
116+
snapshot_event = payload.SnapshotEvent(
117+
event.event.id,
118+
event.uuid,
119+
event.event.key,
120+
event.timestamp,
121+
revenue,
122+
value,
123+
event.event_tags
124+
)
125+
126+
snapshot = payload.Snapshot([snapshot_event])
127+
128+
visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id)
129+
130+
return visitor
131+
132+
else:
133+
logger.error('Invalid user event.')
134+
return None
135+
136+
@staticmethod
137+
def build_attribute_list(attributes, project_config):
138+
""" Create Vistor Attribute List.
139+
140+
Args:
141+
attributes: Dict representing user attributes and values which need to be recorded or None.
142+
project_config: Instance of ProjectConfig.
143+
144+
Returns:
145+
List consisting of valid attributes for the user. Empty otherwise.
146+
"""
147+
148+
attributes_list = []
149+
150+
if project_config is None:
151+
return attributes_list
152+
153+
if isinstance(attributes, dict):
154+
for attribute_key in attributes.keys():
155+
attribute_value = attributes.get(attribute_key)
156+
# Omit attribute values that are not supported by the log endpoint.
157+
if validator.is_attribute_valid(attribute_key, attribute_value):
158+
attribute_id = project_config.get_attribute_id(attribute_key)
159+
if attribute_id:
160+
attributes_list.append(
161+
payload.VisitorAttribute(
162+
attribute_id,
163+
attribute_key,
164+
CUSTOM_ATTRIBUTE_FEATURE_TYPE,
165+
attribute_value)
166+
)
167+
168+
# Append Bot Filtering Attribute
169+
bot_filtering_value = project_config.get_bot_filtering_value()
170+
if isinstance(bot_filtering_value, bool):
171+
attributes_list.append(
172+
payload.VisitorAttribute(
173+
enums.ControlAttributes.BOT_FILTERING,
174+
enums.ControlAttributes.BOT_FILTERING,
175+
CUSTOM_ATTRIBUTE_FEATURE_TYPE,
176+
bot_filtering_value)
177+
)
178+
179+
return attributes_list

optimizely/event/log_event.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2019 Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
15+
class LogEvent(object):
16+
""" Representation of an event which can be sent to Optimizely events API. """
17+
18+
def __init__(self, url, params, http_verb=None, headers=None):
19+
self.url = url
20+
self.params = params
21+
self.http_verb = http_verb or 'POST'
22+
self.headers = headers

optimizely/event/payload.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ def __init__(self, account_id, project_id, revision, client_name, client_version
2929
self.visitors = visitors or []
3030

3131
def __eq__(self, other):
32-
batch_obj = json.loads(json.dumps(self.__dict__, default=lambda o: o.__dict__),
33-
object_pairs_hook=self._dict_clean)
32+
batch_obj = self.get_event_params()
3433
return batch_obj == other
3534

3635
def _dict_clean(self, obj):
@@ -44,6 +43,14 @@ def _dict_clean(self, obj):
4443
result[k] = v
4544
return result
4645

46+
def get_event_params(self):
47+
""" Method to return valid params for LogEvent payload. """
48+
49+
return json.loads(
50+
json.dumps(self.__dict__, default=lambda o: o.__dict__),
51+
object_pairs_hook=self._dict_clean
52+
)
53+
4754

4855
class Decision(object):
4956
""" Class respresenting Decision. """
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright 2019 Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
from . import event_factory
15+
from . import user_event
16+
17+
18+
class UserEventFactory(object):
19+
""" UserEventFactory builds impression and conversion events from a given UserEvent. """
20+
21+
@classmethod
22+
def create_impression_event(cls, project_config, activated_experiment, variation_id, user_id, user_attributes):
23+
""" Create impression Event to be sent to the logging endpoint.
24+
25+
Args:
26+
project_config: Instance of ProjectConfig.
27+
experiment: Experiment for which impression needs to be recorded.
28+
variation_id: ID for variation which would be presented to user.
29+
user_id: ID for user.
30+
attributes: Dict representing user attributes and values which need to be recorded.
31+
32+
Returns:
33+
Event object encapsulating the impression event. None if:
34+
- activated_experiment is None.
35+
"""
36+
37+
if not activated_experiment:
38+
return None
39+
40+
experiment_key = activated_experiment.key
41+
variation = project_config.get_variation_from_id(experiment_key, variation_id)
42+
43+
event_context = user_event.EventContext(
44+
project_config.account_id,
45+
project_config.project_id,
46+
project_config.revision,
47+
project_config.anonymize_ip
48+
)
49+
50+
return user_event.ImpressionEvent(
51+
event_context,
52+
user_id,
53+
activated_experiment,
54+
event_factory.EventFactory.build_attribute_list(user_attributes, project_config),
55+
variation,
56+
project_config.get_bot_filtering_value()
57+
)
58+
59+
@classmethod
60+
def create_conversion_event(cls, project_config, event_key, user_id, user_attributes, event_tags):
61+
""" Create conversion Event to be sent to the logging endpoint.
62+
63+
Args:
64+
project_config: Instance of ProjectConfig.
65+
event_key: Key representing the event which needs to be recorded.
66+
user_id: ID for user.
67+
attributes: Dict representing user attributes and values.
68+
event_tags: Dict representing metadata associated with the event.
69+
70+
Returns:
71+
Event object encapsulating the conversion event.
72+
"""
73+
74+
event_context = user_event.EventContext(
75+
project_config.account_id,
76+
project_config.project_id,
77+
project_config.revision,
78+
project_config.anonymize_ip
79+
)
80+
81+
return user_event.ConversionEvent(
82+
event_context,
83+
project_config.get_event(event_key),
84+
user_id,
85+
event_factory.EventFactory.build_attribute_list(user_attributes, project_config),
86+
event_tags,
87+
project_config.get_bot_filtering_value()
88+
)

0 commit comments

Comments
 (0)