Skip to content

Commit

Permalink
feat: support for send_flag_decisions (#300)
Browse files Browse the repository at this point in the history
* feat: support for send_flag_decisions
  • Loading branch information
pawels-optimizely committed Oct 8, 2020
1 parent 2b1d68b commit 4b05245
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 43 deletions.
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

0 comments on commit 4b05245

Please sign in to comment.