Skip to content

Commit 8e92fda

Browse files
Merge branch 'master' into esra/FSSDK-11458_eu_hosting
2 parents f83865f + 1e8b9ba commit 8e92fda

File tree

9 files changed

+368
-17
lines changed

9 files changed

+368
-17
lines changed

optimizely/event/event_factory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ def _create_visitor(cls, event: Optional[user_event.UserEvent], logger: Logger)
137137
experiment_layerId = event.experiment.layerId
138138
experiment_id = event.experiment.id
139139

140-
metadata = payload.Metadata(event.flag_key, event.rule_key, event.rule_type, variation_key, event.enabled)
140+
metadata = payload.Metadata(event.flag_key, event.rule_key,
141+
event.rule_type, variation_key,
142+
event.enabled, event.cmab_uuid)
141143
decision = payload.Decision(experiment_layerId, experiment_id, variation_id, metadata)
142144
snapshot_event = payload.SnapshotEvent(
143145
experiment_layerId, event.uuid, cls.ACTIVATE_EVENT_KEY, event.timestamp,

optimizely/event/payload.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,15 @@ def __init__(self, campaign_id: str, experiment_id: str, variation_id: str, meta
8181
class Metadata:
8282
""" Class respresenting Metadata. """
8383

84-
def __init__(self, flag_key: str, rule_key: str, rule_type: str, variation_key: str, enabled: bool):
84+
def __init__(self, flag_key: str, rule_key: str, rule_type: str,
85+
variation_key: str, enabled: bool, cmab_uuid: Optional[str] = None):
8586
self.flag_key = flag_key
8687
self.rule_key = rule_key
8788
self.rule_type = rule_type
8889
self.variation_key = variation_key
8990
self.enabled = enabled
91+
if cmab_uuid:
92+
self.cmab_uuid = cmab_uuid
9093

9194

9295
class Snapshot:

optimizely/event/user_event.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def __init__(
7171
rule_key: str,
7272
rule_type: str,
7373
enabled: bool,
74-
bot_filtering: Optional[bool] = None
74+
bot_filtering: Optional[bool] = None,
75+
cmab_uuid: Optional[str] = None
7576
):
7677
super().__init__(event_context, user_id, visitor_attributes, bot_filtering)
7778
self.experiment = experiment
@@ -80,6 +81,7 @@ def __init__(
8081
self.rule_key = rule_key
8182
self.rule_type = rule_type
8283
self.enabled = enabled
84+
self.cmab_uuid = cmab_uuid
8385

8486

8587
class ConversionEvent(UserEvent):

optimizely/event/user_event_factory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def create_impression_event(
4040
rule_type: str,
4141
enabled: bool,
4242
user_id: str,
43-
user_attributes: Optional[UserAttributes]
43+
user_attributes: Optional[UserAttributes],
44+
cmab_uuid: Optional[str]
4445
) -> Optional[user_event.ImpressionEvent]:
4546
""" Create impression Event to be sent to the logging endpoint.
4647
@@ -94,6 +95,7 @@ def create_impression_event(
9495
rule_type,
9596
enabled,
9697
project_config.get_bot_filtering_value(),
98+
cmab_uuid,
9799
)
98100

99101
@classmethod

optimizely/optimizely.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from typing import TYPE_CHECKING, Any, Optional
1717

18+
1819
from . import decision_service
1920
from . import entities
2021
from . import event_builder
@@ -260,7 +261,7 @@ def _validate_user_inputs(
260261
def _send_impression_event(
261262
self, project_config: project_config.ProjectConfig, experiment: Optional[entities.Experiment],
262263
variation: Optional[entities.Variation], flag_key: str, rule_key: str, rule_type: str,
263-
enabled: bool, user_id: str, attributes: Optional[UserAttributes]
264+
enabled: bool, user_id: str, attributes: Optional[UserAttributes], cmab_uuid: Optional[str] = None
264265
) -> None:
265266
""" Helper method to send impression event.
266267
@@ -280,7 +281,9 @@ def _send_impression_event(
280281

281282
variation_id = variation.id if variation is not None else None
282283
user_event = user_event_factory.UserEventFactory.create_impression_event(
283-
project_config, experiment, variation_id, flag_key, rule_key, rule_type, enabled, user_id, attributes
284+
project_config, experiment, variation_id,
285+
flag_key, rule_key, rule_type,
286+
enabled, user_id, attributes, cmab_uuid
284287
)
285288

286289
if user_event is None:
@@ -719,6 +722,8 @@ def is_feature_enabled(self, feature_key: str, user_id: str, attributes: Optiona
719722
user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False)
720723

721724
decision = self.decision_service.get_variation_for_feature(project_config, feature, user_context)['decision']
725+
cmab_uuid = decision.cmab_uuid
726+
722727
is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST
723728
is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT
724729

@@ -729,7 +734,7 @@ def is_feature_enabled(self, feature_key: str, user_id: str, attributes: Optiona
729734
if (is_source_rollout or not decision.variation) and project_config.get_send_flag_decisions_value():
730735
self._send_impression_event(
731736
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key if
732-
decision.experiment else '', str(decision.source), feature_enabled, user_id, attributes
737+
decision.experiment else '', str(decision.source), feature_enabled, user_id, attributes, cmab_uuid
733738
)
734739

735740
# Send event if Decision came from an experiment.
@@ -740,7 +745,7 @@ def is_feature_enabled(self, feature_key: str, user_id: str, attributes: Optiona
740745
}
741746
self._send_impression_event(
742747
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key,
743-
str(decision.source), feature_enabled, user_id, attributes
748+
str(decision.source), feature_enabled, user_id, attributes, cmab_uuid
744749
)
745750

746751
if feature_enabled:
@@ -1193,7 +1198,9 @@ def _create_optimizely_decision(
11931198
flag_decision.variation,
11941199
flag_key, rule_key or '',
11951200
str(decision_source), feature_enabled,
1196-
user_id, attributes)
1201+
user_id, attributes,
1202+
flag_decision.cmab_uuid
1203+
)
11971204

11981205
decision_event_dispatched = True
11991206

tests/test_event_factory.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def test_create_impression_event(self):
113113
False,
114114
'test_user',
115115
None,
116+
None
116117
)
117118

118119
log_event = EventFactory.create_log_event(event_obj, self.logger)
@@ -247,6 +248,7 @@ def test_create_impression_event__with_attributes(self):
247248
True,
248249
'test_user',
249250
{'test_attribute': 'test_value'},
251+
None
250252
)
251253

252254
log_event = EventFactory.create_log_event(event_obj, self.logger)
@@ -313,6 +315,7 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self):
313315
True,
314316
'test_user',
315317
{'do_you_know_me': 'test_value'},
318+
None
316319
)
317320

318321
log_event = EventFactory.create_log_event(event_obj, self.logger)
@@ -472,6 +475,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(
472475
False,
473476
'test_user',
474477
{'$opt_user_agent': 'Edge'},
478+
None
475479
)
476480

477481
log_event = EventFactory.create_log_event(event_obj, self.logger)
@@ -548,6 +552,7 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en
548552
False,
549553
'test_user',
550554
None,
555+
None
551556
)
552557

553558
log_event = EventFactory.create_log_event(event_obj, self.logger)
@@ -630,6 +635,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled
630635
True,
631636
'test_user',
632637
{'$opt_user_agent': 'Chrome'},
638+
None
633639
)
634640

635641
log_event = EventFactory.create_log_event(event_obj, self.logger)
@@ -1089,3 +1095,136 @@ def test_create_conversion_event__when_event_is_used_in_multiple_experiments(sel
10891095
EventFactory.HTTP_VERB,
10901096
EventFactory.HTTP_HEADERS,
10911097
)
1098+
1099+
def test_create_impression_event_with_cmab_uuid(self):
1100+
""" Test that create_impression_event creates LogEvent object with CMAB UUID in metadata. """
1101+
1102+
expected_params = {
1103+
'account_id': '12001',
1104+
'project_id': '111001',
1105+
'visitors': [
1106+
{
1107+
'visitor_id': 'test_user',
1108+
'attributes': [],
1109+
'snapshots': [
1110+
{
1111+
'decisions': [
1112+
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
1113+
'metadata': {'flag_key': '',
1114+
'rule_key': 'rule_key',
1115+
'rule_type': 'experiment',
1116+
'variation_key': 'variation',
1117+
'enabled': False,
1118+
'cmab_uuid': 'test-cmab-uuid-123'
1119+
}
1120+
}
1121+
],
1122+
'events': [
1123+
{
1124+
'timestamp': 42123,
1125+
'entity_id': '111182',
1126+
'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
1127+
'key': 'campaign_activated',
1128+
}
1129+
],
1130+
}
1131+
],
1132+
}
1133+
],
1134+
'client_name': 'python-sdk',
1135+
'client_version': version.__version__,
1136+
'enrich_decisions': True,
1137+
'anonymize_ip': False,
1138+
'revision': '42',
1139+
}
1140+
1141+
with mock.patch('time.time', return_value=42.123), mock.patch(
1142+
'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'
1143+
):
1144+
event_obj = UserEventFactory.create_impression_event(
1145+
self.project_config,
1146+
self.project_config.get_experiment_from_key('test_experiment'),
1147+
'111129',
1148+
'',
1149+
'rule_key',
1150+
'experiment',
1151+
False,
1152+
'test_user',
1153+
None,
1154+
'test-cmab-uuid-123' # cmab_uuid parameter
1155+
)
1156+
1157+
log_event = EventFactory.create_log_event(event_obj, self.logger)
1158+
1159+
self._validate_event_object(
1160+
log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS,
1161+
)
1162+
1163+
def test_create_impression_event_without_cmab_uuid(self):
1164+
""" Test that create_impression_event creates LogEvent object without CMAB UUID when not provided. """
1165+
1166+
expected_params = {
1167+
'account_id': '12001',
1168+
'project_id': '111001',
1169+
'visitors': [
1170+
{
1171+
'visitor_id': 'test_user',
1172+
'attributes': [],
1173+
'snapshots': [
1174+
{
1175+
'decisions': [
1176+
{
1177+
'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
1178+
'metadata': {
1179+
'flag_key': '',
1180+
'rule_key': 'rule_key',
1181+
'rule_type': 'experiment',
1182+
'variation_key': 'variation',
1183+
'enabled': False
1184+
}
1185+
}
1186+
],
1187+
'events': [
1188+
{
1189+
'timestamp': 42123,
1190+
'entity_id': '111182',
1191+
'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
1192+
'key': 'campaign_activated',
1193+
}
1194+
],
1195+
}
1196+
],
1197+
}
1198+
],
1199+
'client_name': 'python-sdk',
1200+
'client_version': version.__version__,
1201+
'enrich_decisions': True,
1202+
'anonymize_ip': False,
1203+
'revision': '42',
1204+
}
1205+
1206+
with mock.patch('time.time', return_value=42.123), mock.patch(
1207+
'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'
1208+
):
1209+
event_obj = UserEventFactory.create_impression_event(
1210+
self.project_config,
1211+
self.project_config.get_experiment_from_key('test_experiment'),
1212+
'111129',
1213+
'',
1214+
'rule_key',
1215+
'experiment',
1216+
False,
1217+
'test_user',
1218+
None,
1219+
None # No cmab_uuid
1220+
)
1221+
1222+
log_event = EventFactory.create_log_event(event_obj, self.logger)
1223+
1224+
# Verify no cmab_uuid in metadata
1225+
metadata = log_event.params['visitors'][0]['snapshots'][0]['decisions'][0]['metadata']
1226+
self.assertNotIn('cmab_uuid', metadata)
1227+
1228+
self._validate_event_object(
1229+
log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS,
1230+
)

tests/test_optimizely.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5750,3 +5750,77 @@ def test_decide_returns_error_decision_when_decision_service_fails(self):
57505750
self.assertIsNone(decision.rule_key)
57515751
self.assertEqual(decision.flag_key, 'test_feature_in_experiment')
57525752
self.assertIn('CMAB service failed to fetch decision', decision.reasons)
5753+
5754+
def test_decide_includes_cmab_uuid_in_dispatched_event(self):
5755+
"""Test that decide dispatches event with correct CMAB UUID."""
5756+
import copy
5757+
from typing import List
5758+
config_dict = copy.deepcopy(self.config_dict_with_features)
5759+
config_dict['experiments'][0]['cmab'] = {'attributeIds': ['808797688', '808797689'], 'trafficAllocation': 4000}
5760+
config_dict['experiments'][0]['trafficAllocation'] = []
5761+
5762+
class TestEventDispatcher:
5763+
"""Custom event dispatcher for testing that captures dispatched events."""
5764+
5765+
def __init__(self):
5766+
self.dispatched_events: List[event_builder.Event] = []
5767+
5768+
def dispatch_event(self, event: event_builder.Event) -> None:
5769+
"""Capture the event instead of actually dispatching it."""
5770+
self.dispatched_events.append(event)
5771+
5772+
test_dispatcher = TestEventDispatcher()
5773+
5774+
opt_obj = optimizely.Optimizely(json.dumps(config_dict), event_dispatcher=test_dispatcher)
5775+
user_context = opt_obj.create_user_context('test_user')
5776+
project_config = opt_obj.config_manager.get_config()
5777+
5778+
# Mock decision service to return a CMAB result
5779+
expected_cmab_uuid = 'uuid-cmab'
5780+
mock_experiment = project_config.get_experiment_from_key('test_experiment')
5781+
mock_variation = project_config.get_variation_from_id('test_experiment', '111129')
5782+
5783+
# Create decision with CMAB UUID
5784+
decision_with_cmab = decision_service.Decision(
5785+
mock_experiment,
5786+
mock_variation,
5787+
enums.DecisionSources.FEATURE_TEST,
5788+
expected_cmab_uuid
5789+
)
5790+
5791+
# Mock the decision service method that's actually called by decide
5792+
with mock.patch.object(
5793+
opt_obj.decision_service, 'get_variations_for_feature_list',
5794+
return_value=[{
5795+
'decision': decision_with_cmab,
5796+
'reasons': [],
5797+
'error': False
5798+
}]
5799+
):
5800+
# Call decide
5801+
decision = user_context.decide('test_feature_in_experiment')
5802+
5803+
# Verify the decision contains the expected information
5804+
self.assertTrue(decision.enabled)
5805+
self.assertEqual(decision.variation_key, 'variation')
5806+
self.assertEqual(decision.rule_key, 'test_experiment')
5807+
self.assertEqual(decision.flag_key, 'test_feature_in_experiment')
5808+
5809+
# Verify an event was dispatched
5810+
time.sleep(0.1)
5811+
self.assertEqual(len(test_dispatcher.dispatched_events), 1)
5812+
5813+
dispatched_event = test_dispatcher.dispatched_events[0]
5814+
5815+
# Verify the structure exists before accessing
5816+
self.assertIn('visitors', dispatched_event.params)
5817+
self.assertTrue(len(dispatched_event.params['visitors']) > 0)
5818+
self.assertIn('snapshots', dispatched_event.params['visitors'][0])
5819+
self.assertTrue(len(dispatched_event.params['visitors'][0]['snapshots']) > 0)
5820+
self.assertIn('decisions', dispatched_event.params['visitors'][0]['snapshots'][0])
5821+
self.assertTrue(len(dispatched_event.params['visitors'][0]['snapshots'][0]['decisions']) > 0)
5822+
5823+
# Get the metadata and assert CMAB UUID
5824+
metadata = dispatched_event.params['visitors'][0]['snapshots'][0]['decisions'][0]['metadata']
5825+
self.assertIn('cmab_uuid', metadata)
5826+
self.assertEqual(metadata['cmab_uuid'], expected_cmab_uuid)

0 commit comments

Comments
 (0)