Skip to content

Commit 70e1d44

Browse files
Send event on feature access (#75)
1 parent 2f1625c commit 70e1d44

File tree

4 files changed

+122
-48
lines changed

4 files changed

+122
-48
lines changed

optimizely/decision_service.py

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

1414
import sys
15+
from collections import namedtuple
1516

1617
from . import bucketer
1718
from .helpers import audience as audience_helper
@@ -21,6 +22,9 @@
2122
from .user_profile import UserProfile
2223

2324

25+
Decision = namedtuple('Decision', 'experiment variation')
26+
27+
2428
class DecisionService(object):
2529
""" Class encapsulating all decision related capabilities. """
2630

@@ -86,7 +90,7 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
8690
Fifth, bucket the user and return the variation.
8791
8892
Args:
89-
experiment_key: Experiment for which user variation needs to be determined.
93+
experiment: Experiment for which user variation needs to be determined.
9094
user_id: ID for user.
9195
attributes: Dict representing user attributes.
9296
ignore_user_profile: True to ignore the user profile lookup. Defaults to False.
@@ -156,7 +160,7 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
156160
return None
157161

158162
def get_variation_for_rollout(self, rollout, user_id, attributes=None):
159-
""" Determine which variation the user is in for a given rollout.
163+
""" Determine which experiment/variation the user is in for a given rollout.
160164
Returns the variation of the first experiment the user qualifies for.
161165
162166
Args:
@@ -165,7 +169,7 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
165169
attributes: Dict representing user attributes.
166170
167171
Returns:
168-
Variation the user should see. None if the user is not in any of the rollout's targeting rules.
172+
Decision namedtuple consisting of experiment and variation for the user.
169173
"""
170174

171175
# Go through each experiment in order and try to get the variation for the user
@@ -186,7 +190,7 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
186190
if variation:
187191
self.logger.log(enums.LogLevels.DEBUG,
188192
'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key))
189-
return variation
193+
return Decision(experiment, variation)
190194
else:
191195
# Evaluate no further rules
192196
self.logger.log(enums.LogLevels.DEBUG,
@@ -203,22 +207,23 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
203207
if variation:
204208
self.logger.log(enums.LogLevels.DEBUG,
205209
'User "%s" meets conditions for targeting rule "Everyone Else".' % user_id)
206-
return variation
210+
return Decision(everyone_else_experiment, variation)
207211

208-
return None
212+
return Decision(None, None)
209213

210214
def get_variation_for_feature(self, feature, user_id, attributes=None):
211-
""" Returns the variation the user is bucketed in for the given feature.
215+
""" Returns the experiment/variation the user is bucketed in for the given feature.
212216
213217
Args:
214218
feature: Feature for which we are determining if it is enabled or not for the given user.
215219
user_id: ID for user.
216220
attributes: Dict representing user attributes.
217221
218222
Returns:
219-
Variation that the user is bucketed in. None if the user is not in any variation.
223+
Decision namedtuple consisting of experiment and variation for the user.
220224
"""
221225

226+
experiment = None
222227
variation = None
223228

224229
# First check if the feature is in a mutex group
@@ -249,9 +254,9 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
249254
# Next check if user is part of a rollout
250255
if not variation and feature.rolloutId:
251256
rollout = self.config.get_rollout_from_id(feature.rolloutId)
252-
variation = self.get_variation_for_rollout(rollout, user_id, attributes)
257+
return self.get_variation_for_rollout(rollout, user_id, attributes)
253258

254-
return variation
259+
return Decision(experiment, variation)
255260

256261
def get_experiment_in_group(self, group, user_id):
257262
""" Determine which experiment in the group the user is bucketed into.

optimizely/optimizely.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,31 @@ def _get_decisions(self, event, user_id, attributes):
152152

153153
return decisions
154154

155+
def _send_impression_event(self, experiment, variation, user_id, attributes):
156+
""" Helper method to send impression event.
157+
158+
Args:
159+
experiment: Experiment for which impression event is being sent.
160+
variation: Variation picked for user for the given experiment.
161+
user_id: ID for user.
162+
attributes: Dict representing user attributes and values which need to be recorded.
163+
"""
164+
165+
impression_event = self.event_builder.create_impression_event(experiment,
166+
variation.id,
167+
user_id,
168+
attributes)
169+
170+
self.logger.log(enums.LogLevels.DEBUG,
171+
'Dispatching impression event to URL %s with params %s.' % (impression_event.url,
172+
impression_event.params))
173+
174+
try:
175+
self.event_dispatcher.dispatch_event(impression_event)
176+
except:
177+
error = sys.exc_info()[1]
178+
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch impression event. Error: %s' % str(error))
179+
155180
def activate(self, experiment_key, user_id, attributes=None):
156181
""" Buckets visitor and sends impression event to Optimizely.
157182
@@ -175,19 +200,12 @@ def activate(self, experiment_key, user_id, attributes=None):
175200
self.logger.log(enums.LogLevels.INFO, 'Not activating user "%s".' % user_id)
176201
return None
177202

178-
# Create and dispatch impression event
179203
experiment = self.config.get_experiment_from_key(experiment_key)
180204
variation = self.config.get_variation_from_key(experiment_key, variation_key)
181-
impression_event = self.event_builder.create_impression_event(experiment, variation.id, user_id, attributes)
205+
206+
# Create and dispatch impression event
182207
self.logger.log(enums.LogLevels.INFO, 'Activating user "%s" in experiment "%s".' % (user_id, experiment.key))
183-
self.logger.log(enums.LogLevels.DEBUG,
184-
'Dispatching impression event to URL %s with params %s.' % (impression_event.url,
185-
impression_event.params))
186-
try:
187-
self.event_dispatcher.dispatch_event(impression_event)
188-
except:
189-
error = sys.exc_info()[1]
190-
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch impression event. Error: %s' % str(error))
208+
self._send_impression_event(experiment, variation, user_id, attributes)
191209

192210
return variation.key
193211

@@ -297,9 +315,13 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
297315
if not feature:
298316
return False
299317

300-
variation = self.decision_service.get_variation_for_feature(feature, user_id, attributes)
301-
if variation:
318+
decision = self.decision_service.get_variation_for_feature(feature, user_id, attributes)
319+
if decision.variation:
302320
self.logger.log(enums.LogLevels.INFO, 'Feature "%s" is enabled for user "%s".' % (feature_key, user_id))
321+
self._send_impression_event(decision.experiment,
322+
decision.variation,
323+
user_id,
324+
attributes)
303325
return True
304326

305327
self.logger.log(enums.LogLevels.INFO, 'Feature "%s" is not enabled for user "%s".' % (feature_key, user_id))

tests/test_decision_service.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import json
1515
import mock
1616

17+
from optimizely import decision_service
1718
from optimizely import entities
1819
from optimizely import optimizely
1920
from optimizely import user_profile
@@ -343,7 +344,8 @@ def test_get_variation_for_rollout__returns_none_if_no_experiments(self, mock_lo
343344
""" Test that get_variation_for_rollout returns None if there are no experiments (targeting rules). """
344345

345346
no_experiment_rollout = self.project_config.get_rollout_from_id('201111')
346-
self.assertIsNone(self.decision_service.get_variation_for_rollout(no_experiment_rollout, 'test_user'))
347+
self.assertEqual(decision_service.Decision(None, None),
348+
self.decision_service.get_variation_for_rollout(no_experiment_rollout, 'test_user'))
347349

348350
# Assert no log messages were generated
349351
self.assertEqual(0, mock_logging.call_count)
@@ -356,7 +358,8 @@ def test_get_variation_for_rollout__skips_to_everyone_else_rule(self, mock_loggi
356358

357359
with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check,\
358360
mock.patch('optimizely.bucketer.Bucketer.bucket', return_value=None):
359-
self.assertIsNone(self.decision_service.get_variation_for_rollout(rollout, 'test_user'))
361+
self.assertEqual(decision_service.Decision(None, None),
362+
self.decision_service.get_variation_for_rollout(rollout, 'test_user'))
360363

361364
# Check that after first experiment, it skips to the last experiment to check
362365
self.assertEqual(
@@ -378,7 +381,8 @@ def test_get_variation_for_rollout__returns_none_for_user_not_in_rollout(self, m
378381
rollout = self.project_config.get_rollout_from_id('211111')
379382

380383
with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=False) as mock_audience_check:
381-
self.assertIsNone(self.decision_service.get_variation_for_rollout(rollout, 'test_user'))
384+
self.assertEqual(decision_service.Decision(None, None),
385+
self.decision_service.get_variation_for_rollout(rollout, 'test_user'))
382386

383387
# Check that all experiments in rollout layer were checked
384388
self.assertEqual(
@@ -399,11 +403,12 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_experiment(
399403

400404
feature = self.project_config.get_feature_from_key('test_feature_in_experiment')
401405

406+
expected_experiment = self.project_config.get_experiment_from_key('test_experiment')
402407
expected_variation = self.project_config.get_variation_from_id('test_experiment', '111129')
403-
with mock.patch(
404-
'optimizely.decision_service.DecisionService.get_variation',
405-
return_value=expected_variation) as mock_decision:
406-
self.assertEqual(expected_variation, self.decision_service.get_variation_for_feature(feature, 'user1'))
408+
with mock.patch('optimizely.decision_service.DecisionService.get_variation',
409+
return_value=expected_variation) as mock_decision:
410+
self.assertEqual(decision_service.Decision(expected_experiment, expected_variation),
411+
self.decision_service.get_variation_for_feature(feature, 'user1'))
407412

408413
mock_decision.assert_called_once_with(
409414
self.project_config.get_experiment_from_key('test_experiment'), 'user1', None
@@ -436,12 +441,14 @@ def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_
436441

437442
feature = self.project_config.get_feature_from_key('test_feature_in_experiment_and_rollout')
438443

444+
expected_experiment = self.project_config.get_experiment_from_key('211127')
439445
expected_variation = self.project_config.get_variation_from_id('211127', '211129')
440446
with mock.patch(
441447
'optimizely.helpers.audience.is_user_in_experiment',
442448
side_effect=[False, True]) as mock_audience_check, \
443449
mock.patch('optimizely.bucketer.Bucketer.bucket', return_value=expected_variation):
444-
self.assertEqual(expected_variation, self.decision_service.get_variation_for_feature(feature, 'user1'))
450+
self.assertEqual(decision_service.Decision(expected_experiment, expected_variation),
451+
self.decision_service.get_variation_for_feature(feature, 'user1'))
445452

446453
self.assertEqual(2, mock_audience_check.call_count)
447454
mock_audience_check.assert_any_call(self.project_config,
@@ -453,21 +460,20 @@ def test_get_variation_for_feature__returns_variation_for_feature_in_group(self,
453460
""" Test that get_variation_for_feature returns the variation of
454461
the experiment the user is bucketed in the feature's group. """
455462

456-
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
457-
project_config = opt_obj.config
458-
decision_service = opt_obj.decision_service
459-
feature = project_config.get_feature_from_key('test_feature_in_group')
463+
feature = self.project_config.get_feature_from_key('test_feature_in_group')
460464

461-
expected_variation = project_config.get_variation_from_id('group_exp_1', '28901')
465+
expected_experiment = self.project_config.get_experiment_from_key('group_exp_1')
466+
expected_variation = self.project_config.get_variation_from_id('group_exp_1', '28901')
462467
with mock.patch(
463468
'optimizely.decision_service.DecisionService.get_experiment_in_group',
464-
return_value=project_config.get_experiment_from_key('group_exp_1')) as mock_get_experiment_in_group, \
469+
return_value=self.project_config.get_experiment_from_key('group_exp_1')) as mock_get_experiment_in_group, \
465470
mock.patch('optimizely.decision_service.DecisionService.get_variation',
466471
return_value=expected_variation) as mock_decision:
467-
self.assertEqual(expected_variation, decision_service.get_variation_for_feature(feature, 'user1'))
472+
self.assertEqual(decision_service.Decision(expected_experiment, expected_variation),
473+
self.decision_service.get_variation_for_feature(feature, 'user1'))
468474

469-
mock_get_experiment_in_group.assert_called_once_with(project_config.get_group('19228'), 'user1')
470-
mock_decision.assert_called_once_with(project_config.get_experiment_from_key('group_exp_1'), 'user1', None)
475+
mock_get_experiment_in_group.assert_called_once_with(self.project_config.get_group('19228'), 'user1')
476+
mock_decision.assert_called_once_with(self.project_config.get_experiment_from_key('group_exp_1'), 'user1', None)
471477

472478
def test_get_variation_for_feature__returns_none_for_user_not_in_group(self, _):
473479
""" Test that get_variation_for_feature returns None for
@@ -478,7 +484,8 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_group(self, _):
478484
with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group',
479485
return_value=None) as mock_get_experiment_in_group, \
480486
mock.patch('optimizely.decision_service.DecisionService.get_variation') as mock_decision:
481-
self.assertIsNone(self.decision_service.get_variation_for_feature(feature, 'user1'))
487+
self.assertEqual(decision_service.Decision(None, None),
488+
self.decision_service.get_variation_for_feature(feature, 'user1'))
482489

483490
mock_get_experiment_in_group.assert_called_once_with(self.project_config.get_group('19228'), 'user1')
484491
self.assertFalse(mock_decision.called)
@@ -487,9 +494,11 @@ def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self
487494
""" Test that get_variation_for_feature returns None for user not in the associated experiment. """
488495

489496
feature = self.project_config.get_feature_from_key('test_feature_in_experiment')
497+
expected_experiment = self.project_config.get_experiment_from_key('test_experiment')
490498

491499
with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None) as mock_decision:
492-
self.assertIsNone(self.decision_service.get_variation_for_feature(feature, 'user1'))
500+
self.assertEqual(decision_service.Decision(expected_experiment, None),
501+
self.decision_service.get_variation_for_feature(feature, 'user1'))
493502

494503
mock_decision.assert_called_once_with(
495504
self.project_config.get_experiment_from_key('test_experiment'), 'user1', None
@@ -500,10 +509,12 @@ def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_no
500509
not targeting a feature, then None is returned. """
501510

502511
feature = self.project_config.get_feature_from_key('test_feature_in_group')
512+
expected_experiment = self.project_config.get_experiment_from_key('group_exp_2')
503513

504514
with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group',
505515
return_value=self.project_config.get_experiment_from_key('group_exp_2')) as mock_decision:
506-
self.assertIsNone(self.decision_service.get_variation_for_feature(feature, 'user_1'))
516+
self.assertEqual(decision_service.Decision(expected_experiment, None),
517+
self.decision_service.get_variation_for_feature(feature, 'user_1'))
507518

508519
mock_decision.assert_called_once_with(self.project_config.get_group('19228'), 'user_1')
509520

0 commit comments

Comments
 (0)