Skip to content

Commit 4b05245

Browse files
feat: support for send_flag_decisions (#300)
* feat: support for send_flag_decisions
1 parent 2b1d68b commit 4b05245

14 files changed

+300
-43
lines changed

optimizely/event/event_factory.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ def _create_visitor(cls, event, logger):
8989
"""
9090

9191
if isinstance(event, user_event.ImpressionEvent):
92-
decision = payload.Decision(event.experiment.layerId, event.experiment.id, event.variation.id,)
93-
92+
metadata = payload.Metadata(event.flag_key, event.rule_key, event.rule_type, event.variation.key)
93+
decision = payload.Decision(event.experiment.layerId, event.experiment.id, event.variation.id, metadata)
9494
snapshot_event = payload.SnapshotEvent(
9595
event.experiment.layerId, event.uuid, cls.ACTIVATE_EVENT_KEY, event.timestamp,
9696
)

optimizely/event/payload.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,21 @@ def get_event_params(self):
6161
class Decision(object):
6262
""" Class respresenting Decision. """
6363

64-
def __init__(self, campaign_id, experiment_id, variation_id):
64+
def __init__(self, campaign_id, experiment_id, variation_id, metadata):
6565
self.campaign_id = campaign_id
6666
self.experiment_id = experiment_id
6767
self.variation_id = variation_id
68+
self.metadata = metadata
69+
70+
71+
class Metadata(object):
72+
""" Class respresenting Metadata. """
73+
74+
def __init__(self, flag_key, rule_key, rule_type, variation_key):
75+
self.flag_key = flag_key
76+
self.rule_key = rule_key
77+
self.rule_type = rule_type
78+
self.variation_key = variation_key
6879

6980

7081
class Snapshot(object):

optimizely/event/user_event.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ class ImpressionEvent(UserEvent):
4141
""" Class representing Impression Event. """
4242

4343
def __init__(
44-
self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None,
44+
self, event_context, user_id, experiment, visitor_attributes, variation, flag_key, rule_key, rule_type,
45+
bot_filtering=None,
4546
):
4647
super(ImpressionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering)
4748
self.experiment = experiment
4849
self.variation = variation
50+
self.flag_key = flag_key
51+
self.rule_key = rule_key
52+
self.rule_type = rule_type
4953

5054

5155
class ConversionEvent(UserEvent):

optimizely/event/user_event_factory.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,25 @@
1313

1414
from . import event_factory
1515
from . import user_event
16+
from optimizely.helpers import enums
1617

1718

1819
class UserEventFactory(object):
1920
""" UserEventFactory builds impression and conversion events from a given UserEvent. """
2021

2122
@classmethod
2223
def create_impression_event(
23-
cls, project_config, activated_experiment, variation_id, user_id, user_attributes,
24+
cls, project_config, activated_experiment, variation_id, flag_key, rule_key, rule_type, user_id, user_attributes
2425
):
2526
""" Create impression Event to be sent to the logging endpoint.
2627
2728
Args:
2829
project_config: Instance of ProjectConfig.
2930
experiment: Experiment for which impression needs to be recorded.
3031
variation_id: ID for variation which would be presented to user.
32+
flag_key: key for a feature flag.
33+
rule_key: key for an experiment.
34+
rule_type: type for the source.
3135
user_id: ID for user.
3236
attributes: Dict representing user attributes and values which need to be recorded.
3337
@@ -36,12 +40,15 @@ def create_impression_event(
3640
- activated_experiment is None.
3741
"""
3842

39-
if not activated_experiment:
43+
if not activated_experiment and rule_type is not enums.DecisionSources.ROLLOUT:
4044
return None
4145

42-
experiment_key = activated_experiment.key
43-
variation = project_config.get_variation_from_id(experiment_key, variation_id)
46+
variation, experiment_key = None, None
47+
if activated_experiment:
48+
experiment_key = activated_experiment.key
4449

50+
if variation_id and experiment_key:
51+
variation = project_config.get_variation_from_id(experiment_key, variation_id)
4552
event_context = user_event.EventContext(
4653
project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip,
4754
)
@@ -52,6 +59,9 @@ def create_impression_event(
5259
activated_experiment,
5360
event_factory.EventFactory.build_attribute_list(user_attributes, project_config),
5461
variation,
62+
flag_key,
63+
rule_key,
64+
rule_type,
5565
project_config.get_bot_filtering_value(),
5666
)
5767

optimizely/helpers/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class DecisionNotificationTypes(object):
8989

9090

9191
class DecisionSources(object):
92+
EXPERIMENT = 'experiment'
9293
FEATURE_TEST = 'feature-test'
9394
ROLLOUT = 'rollout'
9495

optimizely/optimizely.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,23 @@ def _validate_user_inputs(self, attributes=None, event_tags=None):
160160

161161
return True
162162

163-
def _send_impression_event(self, project_config, experiment, variation, user_id, attributes):
163+
def _send_impression_event(self, project_config, experiment, variation, flag_key, rule_key, rule_type, user_id,
164+
attributes):
164165
""" Helper method to send impression event.
165166
166167
Args:
167168
project_config: Instance of ProjectConfig.
168169
experiment: Experiment for which impression event is being sent.
169170
variation: Variation picked for user for the given experiment.
171+
flag_key: key for a feature flag.
172+
rule_key: key for an experiment.
173+
rule_type: type for the source.
170174
user_id: ID for user.
171175
attributes: Dict representing user attributes and values which need to be recorded.
172176
"""
173-
177+
variation_id = variation.id if variation is not None else None
174178
user_event = user_event_factory.UserEventFactory.create_impression_event(
175-
project_config, experiment, variation.id, user_id, attributes
179+
project_config, experiment, variation_id, flag_key, rule_key, rule_type, user_id, attributes
176180
)
177181

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

423427
# Create and dispatch impression event
424428
self.logger.info('Activating user "%s" in experiment "%s".' % (user_id, experiment.key))
425-
self._send_impression_event(project_config, experiment, variation, user_id, attributes)
429+
self._send_impression_event(project_config, experiment, variation, '', experiment.key,
430+
enums.DecisionSources.EXPERIMENT, user_id, attributes)
426431

427432
return variation.key
428433

@@ -573,6 +578,13 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
573578
source_info = {}
574579
decision = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes)
575580
is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST
581+
is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT
582+
583+
if is_source_rollout and project_config.get_send_flag_decisions_value():
584+
self._send_impression_event(
585+
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key if
586+
decision.experiment else '', decision.source, user_id, attributes
587+
)
576588

577589
if decision.variation:
578590
if decision.variation.featureEnabled is True:
@@ -584,7 +596,8 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
584596
'variation_key': decision.variation.key,
585597
}
586598
self._send_impression_event(
587-
project_config, decision.experiment, decision.variation, user_id, attributes,
599+
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key,
600+
decision.source, user_id, attributes
588601
)
589602

590603
if feature_enabled:

optimizely/project_config.py

+10
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __init__(self, datafile, logger, error_handler):
6161
self.feature_flags = config.get('featureFlags', [])
6262
self.rollouts = config.get('rollouts', [])
6363
self.anonymize_ip = config.get('anonymizeIP', False)
64+
self.send_flag_decisions = config.get('sendFlagDecisions', False)
6465
self.bot_filtering = config.get('botFiltering', None)
6566

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

515516
return self.anonymize_ip
516517

518+
def get_send_flag_decisions_value(self):
519+
""" Gets the Send Flag Decisions value.
520+
521+
Returns:
522+
A boolean value that indicates if we should send flag decisions.
523+
"""
524+
525+
return self.send_flag_decisions
526+
517527
def get_bot_filtering_value(self):
518528
""" Gets the bot filtering value.
519529

tests/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def setUp(self, config_dict='config_dict'):
129129
'projectId': '111111',
130130
'version': '4',
131131
'botFiltering': True,
132+
'sendFlagDecisions': True,
132133
'events': [{'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}],
133134
'experiments': [
134135
{

tests/test_config.py

+14
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,20 @@ def test_get_bot_filtering(self):
613613
self.config_dict_with_features['botFiltering'], project_config.get_bot_filtering_value(),
614614
)
615615

616+
def test_get_send_flag_decisions(self):
617+
""" Test that send_flag_decisions is retrieved correctly when using get_send_flag_decisions_value. """
618+
619+
# Assert send_flag_decisions is None when not provided in data file
620+
self.assertTrue('sendFlagDecisions' not in self.config_dict)
621+
self.assertFalse(self.project_config.get_send_flag_decisions_value())
622+
623+
# Assert send_flag_decisions is retrieved as provided in the data file
624+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
625+
project_config = opt_obj.config_manager.get_config()
626+
self.assertEqual(
627+
self.config_dict_with_features['sendFlagDecisions'], project_config.get_send_flag_decisions_value(),
628+
)
629+
616630
def test_get_experiment_from_key__valid_key(self):
617631
""" Test that experiment is retrieved correctly for valid experiment key. """
618632

tests/test_event_factory.py

+60-7
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ def test_create_impression_event(self):
7474
'snapshots': [
7575
{
7676
'decisions': [
77-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
77+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
78+
'metadata': {'flag_key': 'flag_key',
79+
'rule_key': 'rule_key',
80+
'rule_type': 'experiment',
81+
'variation_key': 'variation'}}
7882
],
7983
'events': [
8084
{
@@ -102,6 +106,9 @@ def test_create_impression_event(self):
102106
self.project_config,
103107
self.project_config.get_experiment_from_key('test_experiment'),
104108
'111129',
109+
'flag_key',
110+
'rule_key',
111+
'experiment',
105112
'test_user',
106113
None,
107114
)
@@ -128,7 +135,12 @@ def test_create_impression_event__with_attributes(self):
128135
'snapshots': [
129136
{
130137
'decisions': [
131-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
138+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
139+
'metadata': {'flag_key': 'flag_key',
140+
'rule_key': 'rule_key',
141+
'rule_type': 'experiment',
142+
'variation_key': 'variation'},
143+
}
132144
],
133145
'events': [
134146
{
@@ -156,6 +168,9 @@ def test_create_impression_event__with_attributes(self):
156168
self.project_config,
157169
self.project_config.get_experiment_from_key('test_experiment'),
158170
'111129',
171+
'flag_key',
172+
'rule_key',
173+
'experiment',
159174
'test_user',
160175
{'test_attribute': 'test_value'},
161176
)
@@ -180,7 +195,12 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self):
180195
'snapshots': [
181196
{
182197
'decisions': [
183-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
198+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
199+
'metadata': {'flag_key': 'flag_key',
200+
'rule_key': 'rule_key',
201+
'rule_type': 'experiment',
202+
'variation_key': 'variation'}
203+
}
184204
],
185205
'events': [
186206
{
@@ -208,6 +228,9 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self):
208228
self.project_config,
209229
self.project_config.get_experiment_from_key('test_experiment'),
210230
'111129',
231+
'flag_key',
232+
'rule_key',
233+
'experiment',
211234
'test_user',
212235
{'do_you_know_me': 'test_value'},
213236
)
@@ -235,7 +258,11 @@ def test_create_impression_event_calls_is_attribute_valid(self):
235258
'snapshots': [
236259
{
237260
'decisions': [
238-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
261+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
262+
'metadata': {'flag_key': 'flag_key',
263+
'flag_type': 'experiment',
264+
'variation_key': 'variation'},
265+
}
239266
],
240267
'events': [
241268
{
@@ -280,6 +307,8 @@ def side_effect(*args, **kwargs):
280307
self.project_config,
281308
self.project_config.get_experiment_from_key('test_experiment'),
282309
'111129',
310+
'flag_key',
311+
'experiment',
283312
'test_user',
284313
attributes,
285314
)
@@ -317,7 +346,12 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(
317346
'snapshots': [
318347
{
319348
'decisions': [
320-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
349+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
350+
'metadata': {'flag_key': 'flag_key',
351+
'rule_key': 'rule_key',
352+
'rule_type': 'experiment',
353+
'variation_key': 'variation'},
354+
}
321355
],
322356
'events': [
323357
{
@@ -347,6 +381,9 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(
347381
self.project_config,
348382
self.project_config.get_experiment_from_key('test_experiment'),
349383
'111129',
384+
'flag_key',
385+
'rule_key',
386+
'experiment',
350387
'test_user',
351388
{'$opt_user_agent': 'Edge'},
352389
)
@@ -379,7 +416,12 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en
379416
'snapshots': [
380417
{
381418
'decisions': [
382-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
419+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
420+
'metadata': {'flag_key': 'flag_key',
421+
'rule_key': 'rule_key',
422+
'rule_type': 'experiment',
423+
'variation_key': 'variation'},
424+
}
383425
],
384426
'events': [
385427
{
@@ -409,6 +451,9 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en
409451
self.project_config,
410452
self.project_config.get_experiment_from_key('test_experiment'),
411453
'111129',
454+
'flag_key',
455+
'rule_key',
456+
'experiment',
412457
'test_user',
413458
None,
414459
)
@@ -447,7 +492,12 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled
447492
'snapshots': [
448493
{
449494
'decisions': [
450-
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'}
495+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
496+
'metadata': {'flag_key': 'flag_key',
497+
'rule_key': 'rule_key',
498+
'rule_type': 'experiment',
499+
'variation_key': 'variation'},
500+
}
451501
],
452502
'events': [
453503
{
@@ -477,6 +527,9 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled
477527
self.project_config,
478528
self.project_config.get_experiment_from_key('test_experiment'),
479529
'111129',
530+
'flag_key',
531+
'rule_key',
532+
'experiment',
480533
'test_user',
481534
{'$opt_user_agent': 'Chrome'},
482535
)

0 commit comments

Comments
 (0)