Skip to content

Commit e265be4

Browse files
mauerbacalda-optimizely
authored andcommitted
Introduce v3 batch event endpoint to Python SDK (#61)
1 parent 11269a8 commit e265be4

File tree

3 files changed

+683
-0
lines changed

3 files changed

+683
-0
lines changed

optimizely/event_builder.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# limitations under the License.
1313

1414
import time
15+
import uuid
1516
from abc import abstractmethod
1617
from abc import abstractproperty
1718

@@ -275,3 +276,208 @@ def create_conversion_event(self, event_key, user_id, attributes, event_tags, de
275276
self.params,
276277
http_verb=self.HTTP_VERB,
277278
headers=self.HTTP_HEADERS)
279+
280+
281+
class EventBuilderV3(BaseEventBuilder):
282+
""" Class which encapsulates methods to build events for tracking
283+
impressions and conversions using the new V3 event API (batch). """
284+
285+
EVENTS_URL = 'https://logx.optimizely.com/v1/events'
286+
HTTP_VERB = 'POST'
287+
HTTP_HEADERS = {'Content-Type': 'application/json'}
288+
289+
class EventParams(object):
290+
ACCOUNT_ID = 'account_id'
291+
PROJECT_ID = 'project_id'
292+
EXPERIMENT_ID = 'experiment_id'
293+
CAMPAIGN_ID = 'campaign_id'
294+
VARIATION_ID = 'variation_id'
295+
END_USER_ID = 'visitor_id'
296+
EVENTS = 'events'
297+
EVENT_ID = 'entity_id'
298+
ATTRIBUTES = 'attributes'
299+
DECISIONS = 'decisions'
300+
REVISION = 'revision'
301+
TIME = 'timestamp'
302+
KEY = 'key'
303+
TAGS = 'tags'
304+
UUID = 'uuid'
305+
USERS = 'visitors'
306+
SNAPSHOTS = 'snapshots'
307+
SOURCE_SDK_TYPE = 'client_name'
308+
SOURCE_SDK_VERSION = 'client_version'
309+
CUSTOM = 'custom'
310+
311+
def _add_attributes(self, attributes):
312+
""" Add attribute(s) information to the event.
313+
314+
Args:
315+
attributes: Dict representing user attributes and values which need to be recorded.
316+
"""
317+
318+
if not attributes:
319+
return
320+
321+
visitor = self.params[self.EventParams.USERS][0]
322+
visitor[self.EventParams.ATTRIBUTES] = []
323+
324+
for attribute_key in attributes.keys():
325+
attribute_value = attributes.get(attribute_key)
326+
# Omit falsy attribute values
327+
if attribute_value:
328+
attribute = self.config.get_attribute(attribute_key)
329+
if attribute:
330+
visitor[self.EventParams.ATTRIBUTES].append({
331+
self.EventParams.EVENT_ID: attribute.id,
332+
'key': attribute_key,
333+
'type': self.EventParams.CUSTOM,
334+
'value': attribute_value,
335+
})
336+
337+
def _add_source(self):
338+
""" Add source information to the event. """
339+
340+
self.params[self.EventParams.SOURCE_SDK_TYPE] = 'python-sdk'
341+
self.params[self.EventParams.SOURCE_SDK_VERSION] = version.__version__
342+
343+
def _add_revision(self):
344+
""" Add datafile revision information to the event. """
345+
self.params[self.EventParams.REVISION] = self.config.get_revision()
346+
347+
def _add_time(self):
348+
""" Add time information to the event. """
349+
350+
self.params[self.EventParams.TIME] = int(round(time.time() * 1000))
351+
352+
def _add_visitor(self, user_id):
353+
""" Add user to the event """
354+
355+
self.params[self.EventParams.USERS] = []
356+
# Add a single visitor
357+
visitor = {}
358+
visitor[self.EventParams.END_USER_ID] = user_id
359+
visitor[self.EventParams.SNAPSHOTS] = []
360+
self.params[self.EventParams.USERS].append(visitor)
361+
362+
def _add_common_params(self, user_id, attributes):
363+
""" Add params which are used same in both conversion and impression events.
364+
365+
Args:
366+
user_id: ID for user.
367+
attributes: Dict representing user attributes and values which need to be recorded.
368+
"""
369+
self._add_project_id()
370+
self._add_account_id()
371+
self._add_visitor(user_id)
372+
self._add_attributes(attributes)
373+
self._add_source()
374+
self._add_revision()
375+
376+
def _add_required_params_for_impression(self, experiment, variation_id):
377+
""" Add parameters that are required for the impression event to register.
378+
379+
Args:
380+
experiment: Experiment for which impression needs to be recorded.
381+
variation_id: ID for variation which would be presented to user.
382+
"""
383+
snapshot = {}
384+
385+
snapshot[self.EventParams.DECISIONS] = [{
386+
self.EventParams.EXPERIMENT_ID: experiment.id,
387+
self.EventParams.VARIATION_ID: variation_id,
388+
self.EventParams.CAMPAIGN_ID: experiment.layerId
389+
}]
390+
391+
snapshot[self.EventParams.EVENTS] = [{
392+
self.EventParams.EVENT_ID: experiment.layerId,
393+
self.EventParams.TIME: int(round(time.time() * 1000)),
394+
self.EventParams.KEY: 'campaign_activated',
395+
self.EventParams.UUID: str(uuid.uuid4())
396+
}]
397+
398+
visitor = self.params[self.EventParams.USERS][0]
399+
visitor[self.EventParams.SNAPSHOTS].append(snapshot)
400+
401+
def _add_required_params_for_conversion(self, event_key, event_tags, decisions):
402+
""" Add parameters that are required for the conversion event to register.
403+
404+
Args:
405+
event_key: Key representing the event which needs to be recorded.
406+
event_tags: Dict representing metadata associated with the event.
407+
decisions: List of tuples representing valid experiments IDs and variation IDs.
408+
"""
409+
410+
visitor = self.params[self.EventParams.USERS][0]
411+
412+
for experiment_id, variation_id in decisions:
413+
snapshot = {}
414+
experiment = self.config.get_experiment_from_id(experiment_id)
415+
416+
if variation_id:
417+
snapshot[self.EventParams.DECISIONS] = [{
418+
self.EventParams.EXPERIMENT_ID: experiment_id,
419+
self.EventParams.VARIATION_ID: variation_id,
420+
self.EventParams.CAMPAIGN_ID: experiment.layerId
421+
}]
422+
423+
event_dict = {
424+
self.EventParams.EVENT_ID: self.config.get_event(event_key).id,
425+
self.EventParams.TIME: int(round(time.time() * 1000)),
426+
self.EventParams.KEY: event_key,
427+
self.EventParams.UUID: str(uuid.uuid4())
428+
}
429+
430+
if event_tags:
431+
event_value = event_tag_utils.get_revenue_value(event_tags)
432+
if event_value is not None:
433+
event_dict['revenue'] = event_value
434+
435+
if len(event_tags) > 0:
436+
event_dict[self.EventParams.TAGS] = event_tags
437+
438+
snapshot[self.EventParams.EVENTS] = [event_dict]
439+
visitor[self.EventParams.SNAPSHOTS].append(snapshot)
440+
441+
def create_impression_event(self, experiment, variation_id, user_id, attributes):
442+
""" Create impression Event to be sent to the logging endpoint.
443+
444+
Args:
445+
experiment: Experiment for which impression needs to be recorded.
446+
variation_id: ID for variation which would be presented to user.
447+
user_id: ID for user.
448+
attributes: Dict representing user attributes and values which need to be recorded.
449+
450+
Returns:
451+
Event object encapsulating the impression event.
452+
"""
453+
454+
self.params = {}
455+
self._add_common_params(user_id, attributes)
456+
self._add_required_params_for_impression(experiment, variation_id)
457+
458+
return Event(self.EVENTS_URL,
459+
self.params,
460+
http_verb=self.HTTP_VERB,
461+
headers=self.HTTP_HEADERS)
462+
463+
def create_conversion_event(self, event_key, user_id, attributes, event_tags, decisions):
464+
""" Create conversion Event to be sent to the logging endpoint.
465+
466+
Args:
467+
event_key: Key representing the event which needs to be recorded.
468+
user_id: ID for user.
469+
attributes: Dict representing user attributes and values.
470+
event_tags: Dict representing metadata associated with the event.
471+
decisions: List of tuples representing experiments IDs and variation IDs.
472+
473+
Returns:
474+
Event object encapsulating the conversion event.
475+
"""
476+
477+
self.params = {}
478+
self._add_common_params(user_id, attributes)
479+
self._add_required_params_for_conversion(event_key, event_tags, decisions)
480+
return Event(self.EVENTS_URL,
481+
self.params,
482+
http_verb=self.HTTP_VERB,
483+
headers=self.HTTP_HEADERS)

0 commit comments

Comments
 (0)