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: support for send_flag_decisions #300

Merged
merged 13 commits into from
Oct 8, 2020
4 changes: 2 additions & 2 deletions optimizely/event/event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def _create_visitor(cls, event, logger):
"""

if isinstance(event, user_event.ImpressionEvent):
decision = payload.Decision(event.experiment.layerId, event.experiment.id, event.variation.id,)

metadata = payload.Metadata(event.flag_key, event.rule_key, event.rule_type, event.variation.key)
decision = payload.Decision(event.experiment.layerId, event.experiment.id, event.variation.id, metadata)
snapshot_event = payload.SnapshotEvent(
event.experiment.layerId, event.uuid, cls.ACTIVATE_EVENT_KEY, event.timestamp,
)
Expand Down
13 changes: 12 additions & 1 deletion optimizely/event/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,21 @@ def get_event_params(self):
class Decision(object):
""" Class respresenting Decision. """

def __init__(self, campaign_id, experiment_id, variation_id):
def __init__(self, campaign_id, experiment_id, variation_id, metadata):
self.campaign_id = campaign_id
self.experiment_id = experiment_id
self.variation_id = variation_id
self.metadata = metadata


class Metadata(object):
""" Class respresenting Metadata. """

def __init__(self, flag_key, rule_key, rule_type, variation_key):
self.flag_key = flag_key
self.rule_key = rule_key
self.rule_type = rule_type
self.variation_key = variation_key


class Snapshot(object):
Expand Down
6 changes: 5 additions & 1 deletion optimizely/event/user_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ class ImpressionEvent(UserEvent):
""" Class representing Impression Event. """

def __init__(
self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None,
self, event_context, user_id, experiment, visitor_attributes, variation, flag_key, rule_key, rule_type,
bot_filtering=None,
):
super(ImpressionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering)
self.experiment = experiment
self.variation = variation
self.flag_key = flag_key
self.rule_key = rule_key
self.rule_type = rule_type


class ConversionEvent(UserEvent):
Expand Down
18 changes: 14 additions & 4 deletions optimizely/event/user_event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,25 @@

from . import event_factory
from . import user_event
from optimizely.helpers import enums


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,
cls, project_config, activated_experiment, variation_id, flag_key, rule_key, rule_type, 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.
flag_key: key for a feature flag.
rule_key: key for an experiment.
rule_type: type for the source.
user_id: ID for user.
attributes: Dict representing user attributes and values which need to be recorded.

Expand All @@ -36,12 +40,15 @@ def create_impression_event(
- activated_experiment is None.
"""

if not activated_experiment:
if not activated_experiment and rule_type is not enums.DecisionSources.ROLLOUT:
return None

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

if variation_id and experiment_key:
variation = project_config.get_variation_from_id(experiment_key, variation_id)
event_context = user_event.EventContext(
project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip,
)
Expand All @@ -52,6 +59,9 @@ def create_impression_event(
activated_experiment,
event_factory.EventFactory.build_attribute_list(user_attributes, project_config),
variation,
flag_key,
rule_key,
rule_type,
project_config.get_bot_filtering_value(),
)

Expand Down
1 change: 1 addition & 0 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class DecisionNotificationTypes(object):


class DecisionSources(object):
EXPERIMENT = 'experiment'
FEATURE_TEST = 'feature-test'
ROLLOUT = 'rollout'

Expand Down
23 changes: 18 additions & 5 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,23 @@ def _validate_user_inputs(self, attributes=None, event_tags=None):

return True

def _send_impression_event(self, project_config, experiment, variation, user_id, attributes):
def _send_impression_event(self, project_config, experiment, variation, flag_key, rule_key, rule_type, user_id,
attributes):
""" Helper method to send impression event.

Args:
project_config: Instance of ProjectConfig.
experiment: Experiment for which impression event is being sent.
variation: Variation picked for user for the given experiment.
flag_key: key for a feature flag.
rule_key: key for an experiment.
rule_type: type for the source.
user_id: ID for user.
attributes: Dict representing user attributes and values which need to be recorded.
"""

variation_id = variation.id if variation is not None else None
user_event = user_event_factory.UserEventFactory.create_impression_event(
project_config, experiment, variation.id, user_id, attributes
project_config, experiment, variation_id, flag_key, rule_key, rule_type, user_id, attributes
)

self.event_processor.process(user_event)
Expand Down Expand Up @@ -422,7 +426,8 @@ def activate(self, experiment_key, user_id, attributes=None):

# Create and dispatch impression event
self.logger.info('Activating user "%s" in experiment "%s".' % (user_id, experiment.key))
self._send_impression_event(project_config, experiment, variation, user_id, attributes)
self._send_impression_event(project_config, experiment, variation, '', experiment.key,
enums.DecisionSources.EXPERIMENT, user_id, attributes)

return variation.key

Expand Down Expand Up @@ -573,6 +578,13 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
source_info = {}
decision = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes)
is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST
is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT

if is_source_rollout and project_config.get_send_flag_decisions_value():
self._send_impression_event(
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key if
decision.experiment else '', decision.source, user_id, attributes
)

if decision.variation:
if decision.variation.featureEnabled is True:
Expand All @@ -584,7 +596,8 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
'variation_key': decision.variation.key,
}
self._send_impression_event(
project_config, decision.experiment, decision.variation, user_id, attributes,
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key,
decision.source, user_id, attributes
)

if feature_enabled:
Expand Down
10 changes: 10 additions & 0 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def __init__(self, datafile, logger, error_handler):
self.feature_flags = config.get('featureFlags', [])
self.rollouts = config.get('rollouts', [])
self.anonymize_ip = config.get('anonymizeIP', False)
self.send_flag_decisions = config.get('sendFlagDecisions', False)
self.bot_filtering = config.get('botFiltering', None)

# Utility maps for quick lookup
Expand Down Expand Up @@ -514,6 +515,15 @@ def get_anonymize_ip_value(self):

return self.anonymize_ip

def get_send_flag_decisions_value(self):
""" Gets the Send Flag Decisions value.

Returns:
A boolean value that indicates if we should send flag decisions.
"""

return self.send_flag_decisions

def get_bot_filtering_value(self):
""" Gets the bot filtering value.

Expand Down
1 change: 1 addition & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def setUp(self, config_dict='config_dict'):
'projectId': '111111',
'version': '4',
'botFiltering': True,
'sendFlagDecisions': True,
'events': [{'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}],
'experiments': [
{
Expand Down
14 changes: 14 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,20 @@ def test_get_bot_filtering(self):
self.config_dict_with_features['botFiltering'], project_config.get_bot_filtering_value(),
)

def test_get_send_flag_decisions(self):
""" Test that send_flag_decisions is retrieved correctly when using get_send_flag_decisions_value. """

# Assert send_flag_decisions is None when not provided in data file
self.assertTrue('sendFlagDecisions' not in self.config_dict)
self.assertFalse(self.project_config.get_send_flag_decisions_value())

# Assert send_flag_decisions is retrieved as provided in the data file
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
project_config = opt_obj.config_manager.get_config()
self.assertEqual(
self.config_dict_with_features['sendFlagDecisions'], project_config.get_send_flag_decisions_value(),
)

def test_get_experiment_from_key__valid_key(self):
""" Test that experiment is retrieved correctly for valid experiment key. """

Expand Down
67 changes: 60 additions & 7 deletions tests/test_event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ def test_create_impression_event(self):
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation'}}
],
'events': [
{
Expand Down Expand Up @@ -102,6 +106,9 @@ def test_create_impression_event(self):
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'rule_key',
'experiment',
'test_user',
None,
)
Expand All @@ -128,7 +135,12 @@ def test_create_impression_event__with_attributes(self):
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation'},
}
],
'events': [
{
Expand Down Expand Up @@ -156,6 +168,9 @@ def test_create_impression_event__with_attributes(self):
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'rule_key',
'experiment',
'test_user',
{'test_attribute': 'test_value'},
)
Expand All @@ -180,7 +195,12 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self):
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation'}
}
],
'events': [
{
Expand Down Expand Up @@ -208,6 +228,9 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self):
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'rule_key',
'experiment',
'test_user',
{'do_you_know_me': 'test_value'},
)
Expand Down Expand Up @@ -235,7 +258,11 @@ def test_create_impression_event_calls_is_attribute_valid(self):
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'flag_type': 'experiment',
'variation_key': 'variation'},
}
],
'events': [
{
Expand Down Expand Up @@ -280,6 +307,8 @@ def side_effect(*args, **kwargs):
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'experiment',
'test_user',
attributes,
)
Expand Down Expand Up @@ -317,7 +346,12 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation'},
}
],
'events': [
{
Expand Down Expand Up @@ -347,6 +381,9 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'rule_key',
'experiment',
'test_user',
{'$opt_user_agent': 'Edge'},
)
Expand Down Expand Up @@ -379,7 +416,12 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation'},
}
],
'events': [
{
Expand Down Expand Up @@ -409,6 +451,9 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'rule_key',
'experiment',
'test_user',
None,
)
Expand Down Expand Up @@ -447,7 +492,12 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': 'flag_key',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation'},
}
],
'events': [
{
Expand Down Expand Up @@ -477,6 +527,9 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'flag_key',
'rule_key',
'experiment',
'test_user',
{'$opt_user_agent': 'Chrome'},
)
Expand Down
Loading