Skip to content
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

feat(ep-factory): Implemented Event Factory and User Event Factory #194

Merged
merged 33 commits into from Sep 20, 2019

Conversation

msohailhussain
Copy link
Contributor

Summary

  • UserEventFactory decouples ProjectConfig & provide the object to represent impression and conversion event.
  • EventFactory represents EventBatch after grouping imp/conv objects.

Test plan

  • Converted EventBuilder unit tests to EventFactory to make sure, all scenarios are covered to send payload to Server.

@coveralls
Copy link

coveralls commented Jul 24, 2019

Coverage Status

Coverage increased (+1.8%) to 98.264% when pulling f97d430 on sohail/pr-193 into 239b687 on master.

@mnoman09 mnoman09 changed the base branch from master to sohail/pr-189 August 1, 2019 07:41
mnoman09 and others added 6 commits August 1, 2019 00:41
@msohailhussain msohailhussain requested a review from a team as a code owner August 20, 2019 09:49
@zashraf1985
Copy link
Contributor

@mikeng13... This is ready from our side, It would be great if we can get a review on this.

Copy link

@mjc1283 mjc1283 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments about how invalid data might be handled. The handling of None seems a little haphazard to me.

If the intent is for the event factory to be robust against missing data and return potentially invalid LogEvents, as opposed to crashing or returning Nones, I'd like that to be more obvious. We could have comments explaining what's going on, or validation/cleaning steps performed explicitly in their own areas, instead of mixed throughout the other logic.

But in the first place, why would we create a LogEvent with some missing field, instead of logging error messages and returning None? It seems simpler to only create valid LogEvents.

LogEvent instance.
"""

def _dict_clean(obj):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think _dict_clean should be a static method, not declared inside create_log_event.

optimizely/event/event_factory.py Show resolved Hide resolved
optimizely/event/event_factory.py Outdated Show resolved Hide resolved

@classmethod
def _create_visitor(cls, user_event, logger):
if not user_event:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this check necssary? In what circumstances is user_event falsy?

visitors = []

for user_event in user_events:
visitors.append(cls._create_visitor(user_event, logger))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cls._create_visitor could return None. We have to filter out None values from the return value before appending it to visitors.

return visitor

else:
# include log message for invalid event type
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to log an error message here.


if isinstance(user_event, ImpressionEvent):
decision = Decision(
user_event.experiment.layerId if user_event.experiment else None,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If user_event.experiment is None or user_event.variation is None, that seems like an error or invalid data, right? Shouldn't all ImpressionEvents have experiment and variation?

optimizely/event/event_factory.py Show resolved Hide resolved
""" Create Vistor Attribute List.

Args:
attributes: Dict representing user attributes and values which need to be recorded.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If attributes might not be a dict, let's mention what else it could be.

@mariamjamal94
Copy link

I left some comments about how invalid data might be handled. The handling of None seems a little haphazard to me.

If the intent is for the event factory to be robust against missing data and return potentially invalid LogEvents, as opposed to crashing or returning Nones, I'd like that to be more obvious. We could have comments explaining what's going on, or validation/cleaning steps performed explicitly in their own areas, instead of mixed throughout the other logic.

But in the first place, why would we create a LogEvent with some missing field, instead of logging error messages and returning None? It seems simpler to only create valid LogEvents.

LogEvent is not missing a field here. EventBatch, which is sent as params property of LogEvent can have some None values. Server does not expect any keys with null values in the payload. Hence we simply need to remove those keys which don't have values. I initially did this cleanup here in the EventFactory but now I have moved it inside get_event_params method inside EventBatch class.

optimizely/event/event_factory.py Show resolved Hide resolved
optimizely/event/event_factory.py Outdated Show resolved Hide resolved
Event object encapsulating the impression event.
"""

experiment_key = activated_experiment.key if activated_experiment else None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API requires experiment id to be sent in a decision. So I don't think we can create a valid impression event if activated_experiment is None.

Wouldn't it be better to return None from create_impression_event in this case? Otherwise we're creating an ImpressionEvent that won't be counted by the API.

Copy link

@mjc1283 mjc1283 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left one more comment about the return value of build_attribute_list. After that, this LGTM. Thanks for addressing all my concerns.

"""

if project_config is None:
return None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's return an empty list in this case, instead of None.



class LogEvent(object):
""" Representation of an event which can be sent to the Optimizely logging endpoint. """
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Optimizely events API.

@oakbani oakbani changed the base branch from sohail/pr-189 to master September 12, 2019 13:07
Copy link
Contributor

@aliabbasrizvi aliabbasrizvi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, but I missed reviewing the import order. Please address and then it will be good to merge.


from .user_event import ConversionEvent, ImpressionEvent
from .payload import Decision, EventBatch, Snapshot, SnapshotEvent, Visitor, VisitorAttribute
from .log_event import LogEvent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have mentioned earlier, but import order here is incorrect

It should be external packages (alphabetic), internal packages (alphabetic), files from same package (alphabetic)

def __init__(self, url, params, http_verb=None, headers=None):
self.url = url
self.params = params
self.http_verb = http_verb or 'GET'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Let's start defaulting to POST. It used to be GET long ago.

# See the License for the specific language governing permissions and
# limitations under the License.

from .user_event import EventContext, ConversionEvent, ImpressionEvent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not import user_event rather than import classes like this? Also, follow alphabetic order.

tests/test_event_factory.py Show resolved Hide resolved
import unittest
import uuid

from . import base
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue here as well as base belongs to same package and should be imported last.

from optimizely.helpers import validator
from . import user_event
from . import payload
from . import log_event
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. Last 3 imports are not alphabetic.

@aliabbasrizvi aliabbasrizvi merged commit 6794260 into master Sep 20, 2019
@oakbani oakbani deleted the sohail/pr-193 branch September 24, 2019 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants