Skip to content

Commit f76eb4b

Browse files
authored
Introduce event tags (#35)
1 parent 7d43053 commit f76eb4b

File tree

10 files changed

+493
-30
lines changed

10 files changed

+493
-30
lines changed

optimizely/event_builder.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016-2017, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -19,6 +19,7 @@
1919
from . import project_config
2020
from . import version
2121
from .helpers import enums
22+
from .helpers import event_tag_utils
2223

2324

2425
class Event(object):
@@ -225,19 +226,22 @@ def create_impression_event(self, experiment, variation_id, user_id, attributes)
225226
return Event(self.OFFLINE_API_PATH.format(project_id=self.params[self.EventParams.PROJECT_ID]),
226227
self.params)
227228

228-
def create_conversion_event(self, event_key, user_id, attributes, event_value, valid_experiments):
229+
def create_conversion_event(self, event_key, user_id, attributes, event_tags, valid_experiments):
229230
""" Create conversion Event to be sent to the logging endpoint.
230231
231232
Args:
232233
event_key: Event key representing the event which needs to be recorded.
233234
user_id: ID for user.
234-
event_value: Value associated with the event. Can be used to represent revenue in cents.
235+
attributes: Dict representing user attributes and values.
236+
event_tags: Dict representing metadata associated with the event.
235237
valid_experiments: List of tuples representing valid experiments for the event.
236238
237239
Returns:
238240
Event object encapsulating the conversion event.
239241
"""
240242

243+
event_value = event_tag_utils.get_revenue_value(event_tags)
244+
241245
self.params = {}
242246
self._add_common_params(user_id, attributes)
243247
self._add_conversion_goal(event_key, event_value)
@@ -254,7 +258,6 @@ class EventBuilderV2(BaseEventBuilder):
254258
CONVERSION_ENDPOINT = 'https://logx.optimizely.com/log/event'
255259
HTTP_VERB = 'POST'
256260
HTTP_HEADERS = {'Content-Type': 'application/json'}
257-
EVENT_VALUE_METRIC = 'revenue'
258261

259262
class EventParams(object):
260263
ACCOUNT_ID = 'accountId'
@@ -329,25 +332,40 @@ def _add_required_params_for_impression(self, experiment, variation_id):
329332
self.EventParams.IS_LAYER_HOLDBACK: False
330333
}
331334

332-
def _add_required_params_for_conversion(self, event_key, user_id, event_value, valid_experiments):
335+
def _add_required_params_for_conversion(self, event_key, user_id, event_tags, valid_experiments):
333336
""" Add parameters that are required for the conversion event to register.
334337
335338
Args:
336339
event_key: Key representing the event which needs to be recorded.
337340
user_id: ID for user.
338-
event_value: Value associated with the event. Can be used to represent revenue in cents.
341+
event_tags: Dict representing metadata associated with the event.
339342
valid_experiments: List of tuples representing valid experiments for the event.
340343
"""
341344

342345
self.params[self.EventParams.IS_GLOBAL_HOLDBACK] = False
343346
self.params[self.EventParams.EVENT_FEATURES] = []
344347
self.params[self.EventParams.EVENT_METRICS] = []
345348

346-
if event_value:
347-
self.params[self.EventParams.EVENT_METRICS] = [{
348-
'name': self.EVENT_VALUE_METRIC,
349-
'value': event_value
350-
}]
349+
if event_tags:
350+
event_value = event_tag_utils.get_revenue_value(event_tags)
351+
if event_value is not None:
352+
self.params[self.EventParams.EVENT_METRICS] = [{
353+
'name': event_tag_utils.EVENT_VALUE_METRIC,
354+
'value': event_value
355+
}]
356+
357+
for event_tag_id in event_tags.keys():
358+
event_tag_value = event_tags.get(event_tag_id)
359+
if event_tag_value is None:
360+
continue
361+
362+
event_feature = {
363+
'id': event_tag_id,
364+
'type': 'custom',
365+
'value': event_tag_value,
366+
'shouldIndex': False,
367+
}
368+
self.params[self.EventParams.EVENT_FEATURES].append(event_feature)
351369

352370
self.params[self.EventParams.LAYER_STATES] = []
353371
for experiment in valid_experiments:
@@ -387,23 +405,23 @@ def create_impression_event(self, experiment, variation_id, user_id, attributes)
387405
http_verb=self.HTTP_VERB,
388406
headers=self.HTTP_HEADERS)
389407

390-
def create_conversion_event(self, event_key, user_id, attributes, event_value, valid_experiments):
408+
def create_conversion_event(self, event_key, user_id, attributes, event_tags, valid_experiments):
391409
""" Create conversion Event to be sent to the logging endpoint.
392410
393411
Args:
394412
event_key: Key representing the event which needs to be recorded.
395413
user_id: ID for user.
396-
event_value: Value associated with the event. Can be used to represent revenue in cents.
397-
valid_experiments: List of tuples representing valid experiments for the event.
398414
attributes: Dict representing user attributes and values.
415+
event_tags: Dict representing metadata associated with the event.
416+
valid_experiments: List of tuples representing valid experiments for the event.
399417
400418
Returns:
401419
Event object encapsulating the conversion event.
402420
"""
403421

404422
self.params = {}
405423
self._add_common_params(user_id, attributes)
406-
self._add_required_params_for_conversion(event_key, user_id, event_value, valid_experiments)
424+
self._add_required_params_for_conversion(event_key, user_id, event_tags, valid_experiments)
407425
return Event(self.CONVERSION_ENDPOINT,
408426
self.params,
409427
http_verb=self.HTTP_VERB,

optimizely/exceptions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016-2017, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -21,6 +21,11 @@ class InvalidAudienceException(Exception):
2121
pass
2222

2323

24+
class InvalidEventTagException(Exception):
25+
""" Raised when provided event tag is invalid. """
26+
pass
27+
28+
2429
class InvalidExperimentException(Exception):
2530
""" Raised when provided experiment key is invalid. """
2631
pass

optimizely/helpers/enums.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016-2017, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -32,6 +32,7 @@ class Errors(object):
3232
INVALID_INPUT_ERROR = 'Provided "{}" is in an invalid format.'
3333
INVALID_ATTRIBUTE_ERROR = 'Provided attribute is not in datafile.'
3434
INVALID_ATTRIBUTE_FORMAT = 'Attributes provided are in an invalid format.'
35+
INVALID_EVENT_TAG_FORMAT = 'Event tags provided are in an invalid format.'
3536
INVALID_AUDIENCE_ERROR = 'Provided audience is not in datafile.'
3637
INVALID_EXPERIMENT_KEY_ERROR = 'Provided experiment is not in datafile.'
3738
INVALID_EVENT_KEY_ERROR = 'Provided event is not in datafile.'

optimizely/helpers/event_tag_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2017, 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+
import numbers
15+
16+
EVENT_VALUE_METRIC = 'revenue'
17+
18+
19+
def get_revenue_value(event_tags):
20+
if event_tags is None:
21+
return None
22+
23+
if not isinstance(event_tags, dict):
24+
return None
25+
26+
if EVENT_VALUE_METRIC not in event_tags:
27+
return None
28+
29+
raw_value = event_tags[EVENT_VALUE_METRIC]
30+
31+
if not isinstance(raw_value, numbers.Integral):
32+
return None
33+
34+
return raw_value

optimizely/helpers/validator.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016-2017, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -116,3 +116,16 @@ def are_attributes_valid(attributes):
116116
"""
117117

118118
return type(attributes) is dict
119+
120+
121+
def are_event_tags_valid(event_tags):
122+
""" Determine if event tags provided are dict or not.
123+
124+
Args:
125+
event_tags: Event tags which need to be validated.
126+
127+
Returns:
128+
Boolean depending upon whether event_tags are in valid format or not.
129+
"""
130+
131+
return type(event_tags) is dict

optimizely/optimizely.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016-2017, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -11,6 +11,7 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313

14+
import numbers
1415
import sys
1516

1617
from . import bucketer
@@ -176,14 +177,14 @@ def activate(self, experiment_key, user_id, attributes=None):
176177

177178
return variation.key
178179

179-
def track(self, event_key, user_id, attributes=None, event_value=None):
180+
def track(self, event_key, user_id, attributes=None, event_tags=None):
180181
""" Send conversion event to Optimizely.
181182
182183
Args:
183184
event_key: Event key representing the event which needs to be recorded.
184185
user_id: ID for user.
185186
attributes: Dict representing visitor attributes and values which need to be recorded.
186-
event_value: Value associated with the event. Can be used to represent revenue in cents.
187+
event_tags: Dict representing metadata associated with the event.
187188
"""
188189

189190
if not self.is_valid:
@@ -195,6 +196,19 @@ def track(self, event_key, user_id, attributes=None, event_value=None):
195196
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT))
196197
return
197198

199+
if event_tags:
200+
if isinstance(event_tags, numbers.Number):
201+
event_tags = {
202+
'revenue': event_tags
203+
}
204+
self.logger.log(enums.LogLevels.WARNING,
205+
'Event value is deprecated in track call. Use event tags to pass in revenue value instead.')
206+
207+
if not validator.are_event_tags_valid(event_tags):
208+
self.logger.log(enums.LogLevels.ERROR, 'Provided event tags are in an invalid format.')
209+
self.error_handler.handle_error(exceptions.InvalidEventTagException(enums.Errors.INVALID_EVENT_TAG_FORMAT))
210+
return
211+
198212
event = self.config.get_event(event_key)
199213
if not event:
200214
self.logger.log(enums.LogLevels.INFO, 'Not tracking user "%s" for event "%s".' % (user_id, event_key))
@@ -211,7 +225,7 @@ def track(self, event_key, user_id, attributes=None, event_value=None):
211225

212226
# Create and dispatch conversion event if there are valid experiments
213227
if valid_experiments:
214-
conversion_event = self.event_builder.create_conversion_event(event_key, user_id, attributes, event_value,
228+
conversion_event = self.event_builder.create_conversion_event(event_key, user_id, attributes, event_tags,
215229
valid_experiments)
216230
self.logger.log(enums.LogLevels.INFO, 'Tracking event "%s" for user "%s".' % (event_key, user_id))
217231
self.logger.log(enums.LogLevels.DEBUG,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright 2017, 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+
import unittest
15+
16+
from optimizely.helpers import event_tag_utils
17+
18+
class EventTagUtilsTest(unittest.TestCase):
19+
20+
def test_get_revenue_value__invalid_args(self):
21+
""" Test that revenue value is not returned for invalid arguments. """
22+
self.assertIsNone(event_tag_utils.get_revenue_value(None))
23+
self.assertIsNone(event_tag_utils.get_revenue_value(0.5))
24+
self.assertIsNone(event_tag_utils.get_revenue_value(65536))
25+
self.assertIsNone(event_tag_utils.get_revenue_value(9223372036854775807))
26+
self.assertIsNone(event_tag_utils.get_revenue_value('9223372036854775807'))
27+
self.assertIsNone(event_tag_utils.get_revenue_value(True))
28+
self.assertIsNone(event_tag_utils.get_revenue_value(False))
29+
30+
def test_get_revenue_value__no_revenue_tag(self):
31+
""" Test that revenue value is not returned when there's no revenue event tag. """
32+
self.assertIsNone(event_tag_utils.get_revenue_value([]))
33+
self.assertIsNone(event_tag_utils.get_revenue_value({}))
34+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': 42}))
35+
36+
def test_get_revenue_value__invalid_revenue_tag(self):
37+
""" Test that revenue value is not returned when revenue event tag has invalid data type. """
38+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': None}))
39+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': 0.5}))
40+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': '65536'}))
41+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': True}))
42+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': False}))
43+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': [1, 2, 3]}))
44+
self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': {'a', 'b', 'c'}}))
45+
46+
def test_get_revenue_value__revenue_tag(self):
47+
""" Test that correct revenue value is returned. """
48+
self.assertEqual(0, event_tag_utils.get_revenue_value({'revenue': 0}))
49+
self.assertEqual(65536, event_tag_utils.get_revenue_value({'revenue': 65536}))
50+
self.assertEqual(9223372036854775807, event_tag_utils.get_revenue_value({'revenue': 9223372036854775807}))

tests/helpers_tests/test_validator.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016-2017, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -89,6 +89,18 @@ def test_are_attributes_valid__returns_false(self):
8989
self.assertFalse(validator.are_attributes_valid(['key', 'value']))
9090
self.assertFalse(validator.are_attributes_valid(42))
9191

92+
def test_are_event_tags_valid__returns_true(self):
93+
""" Test that valid event tags returns True. """
94+
95+
self.assertTrue(validator.are_event_tags_valid({'key': 'value', 'revenue': 0}))
96+
97+
def test_are_event_tags_valid__returns_false(self):
98+
""" Test that invalid event tags returns False. """
99+
100+
self.assertFalse(validator.are_event_tags_valid('key:value'))
101+
self.assertFalse(validator.are_event_tags_valid(['key', 'value']))
102+
self.assertFalse(validator.are_event_tags_valid(42))
103+
92104

93105
class DatafileV2ValidationTests(base.BaseTestV2):
94106

0 commit comments

Comments
 (0)