Skip to content

[FSSDK-11184] Update: Send CMAB uuid in impression events #458

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

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
88b4f1e
update: integrate CMAB components into OptimizelyFactory
FarhanAnjum-opti Jun 4, 2025
2563c7b
update: add cmab_service parameter to Optimizely constructor for CMAB…
FarhanAnjum-opti Jun 4, 2025
fac8946
update: add docstring to DefaultCmabService class for improved docume…
FarhanAnjum-opti Jun 4, 2025
f74bc8c
update: implement CMAB support in bucketer and decision service, reve…
FarhanAnjum-opti Jun 13, 2025
6d1f73d
linting fix
FarhanAnjum-opti Jun 13, 2025
91d53b6
update: add cmab_uuid handling in DecisionService and related tests
FarhanAnjum-opti Jun 16, 2025
3eb755f
- updated function bucket_to_entity_id
FarhanAnjum-opti Jun 16, 2025
a5e4993
update: add None parameter to Decision constructor in user context tests
FarhanAnjum-opti Jun 16, 2025
c1cd97a
update: enhance CMAB decision handling and add related tests
FarhanAnjum-opti Jun 16, 2025
fd7c723
update: fix logger message formatting in CMAB experiment tests
FarhanAnjum-opti Jun 16, 2025
ec19c3b
mypy fix
FarhanAnjum-opti Jun 16, 2025
029262d
update: refine traffic allocation type hints and key naming in bucket…
FarhanAnjum-opti Jun 16, 2025
180fdee
update: remove unused import of cast in bucketer.py
FarhanAnjum-opti Jun 16, 2025
cd5ba39
update: fix return type for numeric_metric_value in get_numeric_value…
FarhanAnjum-opti Jun 16, 2025
92a3258
update: specify type hint for numeric_metric_value in get_numeric_val…
FarhanAnjum-opti Jun 16, 2025
fe100cb
update: fix logger reference in DefaultCmabClient initialization and …
FarhanAnjum-opti Jun 17, 2025
60a4ada
update: enhance error logging for CMAB fetch failures with detailed m…
FarhanAnjum-opti Jun 20, 2025
265d82b
update: enhance decision result handling by introducing VariationResu…
FarhanAnjum-opti Jun 20, 2025
6ca1102
update: refactor get_variation return structure and change tests acco…
FarhanAnjum-opti Jun 20, 2025
c2b3d96
-Error propagated to optimizely.py
FarhanAnjum-opti Jun 23, 2025
0e25622
update: add cmab_uuid parameter to impression events
FarhanAnjum-opti Jun 27, 2025
088f4af
update: add None parameter to impression events in decision tests
FarhanAnjum-opti Jun 27, 2025
b901c5f
update: modify get_variation to return VariationResult and adjust rel…
FarhanAnjum-opti Jun 27, 2025
fa77cca
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jun 27, 2025
d2fc631
update: unit test fixes
FarhanAnjum-opti Jun 27, 2025
752a030
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jun 27, 2025
cbf2c2c
update: include CMAB UUID in activation and add corresponding tests
FarhanAnjum-opti Jun 27, 2025
fdcdfbf
update: add tests for get_variation with and without CMAB UUID
FarhanAnjum-opti Jun 27, 2025
b9a8555
Revert "update: unit test fixes"
FarhanAnjum-opti Jun 30, 2025
a129854
Revert "update: modify get_variation to return VariationResult and ad…
FarhanAnjum-opti Jun 30, 2025
9d63477
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jun 30, 2025
1f7e2a9
update: make cmab_uuid parameter optional in _send_impression_event m…
FarhanAnjum-opti Jun 30, 2025
73a2802
chore: trigger CI by turning on python flag
FarhanAnjum-opti Jun 30, 2025
a6d9771
update: new class method to handle optimizely error decisions
FarhanAnjum-opti Jun 30, 2025
4743376
fix unit test
FarhanAnjum-opti Jun 30, 2025
6d79053
fix: update error logging format for CMAB fetch failures
FarhanAnjum-opti Jun 30, 2025
3c903c7
chore: trigger CI
FarhanAnjum-opti Jul 1, 2025
c637878
update: enhance decision service to handle error states and improve b…
FarhanAnjum-opti Jul 3, 2025
62bbf64
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jul 3, 2025
0bc4fbd
update: remove debug print statement from Optimizely class
FarhanAnjum-opti Jul 3, 2025
8668565
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jul 3, 2025
fcdad1f
update: enhance bucketing logic to support CMAB traffic allocations
FarhanAnjum-opti Jul 3, 2025
dd4790d
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jul 3, 2025
aca7df4
update: improve error logging for CMAB decision fetch failures
FarhanAnjum-opti Jul 3, 2025
c14b768
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jul 3, 2025
72955a0
update: improve logging and error handling in bucketer and decision s…
FarhanAnjum-opti Jul 7, 2025
265ea3c
Merge branch 'farhan-anjum/FSSDK-11175-add-decision-service-methods-f…
FarhanAnjum-opti Jul 7, 2025
b88c08b
Merge branch 'master' into farhan-anjum/FSSDK-11184-update-impression…
FarhanAnjum-opti Jul 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion optimizely/event/event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ def _create_visitor(cls, event: Optional[user_event.UserEvent], logger: Logger)
experiment_layerId = event.experiment.layerId
experiment_id = event.experiment.id

metadata = payload.Metadata(event.flag_key, event.rule_key, event.rule_type, variation_key, event.enabled)
metadata = payload.Metadata(event.flag_key, event.rule_key,
event.rule_type, variation_key,
event.enabled, event.cmab_uuid)
decision = payload.Decision(experiment_layerId, experiment_id, variation_id, metadata)
snapshot_event = payload.SnapshotEvent(
experiment_layerId, event.uuid, cls.ACTIVATE_EVENT_KEY, event.timestamp,
Expand Down
5 changes: 4 additions & 1 deletion optimizely/event/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ def __init__(self, campaign_id: str, experiment_id: str, variation_id: str, meta
class Metadata:
""" Class respresenting Metadata. """

def __init__(self, flag_key: str, rule_key: str, rule_type: str, variation_key: str, enabled: bool):
def __init__(self, flag_key: str, rule_key: str, rule_type: str,
variation_key: str, enabled: bool, cmab_uuid: Optional[str] = None):
self.flag_key = flag_key
self.rule_key = rule_key
self.rule_type = rule_type
self.variation_key = variation_key
self.enabled = enabled
if cmab_uuid:
self.cmab_uuid = cmab_uuid


class Snapshot:
Expand Down
4 changes: 3 additions & 1 deletion optimizely/event/user_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ def __init__(
rule_key: str,
rule_type: str,
enabled: bool,
bot_filtering: Optional[bool] = None
bot_filtering: Optional[bool] = None,
cmab_uuid: Optional[str] = None
):
super().__init__(event_context, user_id, visitor_attributes, bot_filtering)
self.experiment = experiment
Expand All @@ -79,6 +80,7 @@ def __init__(
self.rule_key = rule_key
self.rule_type = rule_type
self.enabled = enabled
self.cmab_uuid = cmab_uuid


class ConversionEvent(UserEvent):
Expand Down
4 changes: 3 additions & 1 deletion optimizely/event/user_event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ def create_impression_event(
rule_type: str,
enabled: bool,
user_id: str,
user_attributes: Optional[UserAttributes]
user_attributes: Optional[UserAttributes],
cmab_uuid: Optional[str]
) -> Optional[user_event.ImpressionEvent]:
""" Create impression Event to be sent to the logging endpoint.

Expand Down Expand Up @@ -90,6 +91,7 @@ def create_impression_event(
rule_type,
enabled,
project_config.get_bot_filtering_value(),
cmab_uuid,
)

@classmethod
Expand Down
17 changes: 12 additions & 5 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from typing import TYPE_CHECKING, Any, Optional


from . import decision_service
from . import entities
from . import event_builder
Expand Down Expand Up @@ -260,7 +261,7 @@ def _validate_user_inputs(
def _send_impression_event(
self, project_config: project_config.ProjectConfig, experiment: Optional[entities.Experiment],
variation: Optional[entities.Variation], flag_key: str, rule_key: str, rule_type: str,
enabled: bool, user_id: str, attributes: Optional[UserAttributes]
enabled: bool, user_id: str, attributes: Optional[UserAttributes], cmab_uuid: Optional[str] = None
) -> None:
""" Helper method to send impression event.

Expand All @@ -280,7 +281,9 @@ def _send_impression_event(

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, flag_key, rule_key, rule_type, enabled, user_id, attributes
project_config, experiment, variation_id,
flag_key, rule_key, rule_type,
enabled, user_id, attributes, cmab_uuid
)

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

decision = self.decision_service.get_variation_for_feature(project_config, feature, user_context)['decision']
cmab_uuid = decision.cmab_uuid

is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST
is_source_rollout = decision.source == enums.DecisionSources.ROLLOUT

Expand All @@ -729,7 +734,7 @@ def is_feature_enabled(self, feature_key: str, user_id: str, attributes: Optiona
if (is_source_rollout or not decision.variation) 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 '', str(decision.source), feature_enabled, user_id, attributes
decision.experiment else '', str(decision.source), feature_enabled, user_id, attributes, cmab_uuid
)

# Send event if Decision came from an experiment.
Expand All @@ -740,7 +745,7 @@ def is_feature_enabled(self, feature_key: str, user_id: str, attributes: Optiona
}
self._send_impression_event(
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key,
str(decision.source), feature_enabled, user_id, attributes
str(decision.source), feature_enabled, user_id, attributes, cmab_uuid
)

if feature_enabled:
Expand Down Expand Up @@ -1193,7 +1198,9 @@ def _create_optimizely_decision(
flag_decision.variation,
flag_key, rule_key or '',
str(decision_source), feature_enabled,
user_id, attributes)
user_id, attributes,
flag_decision.cmab_uuid
)

decision_event_dispatched = True

Expand Down
139 changes: 139 additions & 0 deletions tests/test_event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def test_create_impression_event(self):
False,
'test_user',
None,
None
)

log_event = EventFactory.create_log_event(event_obj, self.logger)
Expand Down Expand Up @@ -177,6 +178,7 @@ def test_create_impression_event__with_attributes(self):
True,
'test_user',
{'test_attribute': 'test_value'},
None
)

log_event = EventFactory.create_log_event(event_obj, self.logger)
Expand Down Expand Up @@ -239,6 +241,7 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self):
True,
'test_user',
{'do_you_know_me': 'test_value'},
None
)

log_event = EventFactory.create_log_event(event_obj, self.logger)
Expand Down Expand Up @@ -394,6 +397,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(
False,
'test_user',
{'$opt_user_agent': 'Edge'},
None
)

log_event = EventFactory.create_log_event(event_obj, self.logger)
Expand Down Expand Up @@ -466,6 +470,7 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en
False,
'test_user',
None,
None
)

log_event = EventFactory.create_log_event(event_obj, self.logger)
Expand Down Expand Up @@ -544,6 +549,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled
True,
'test_user',
{'$opt_user_agent': 'Chrome'},
None
)

log_event = EventFactory.create_log_event(event_obj, self.logger)
Expand Down Expand Up @@ -920,3 +926,136 @@ def test_create_conversion_event__when_event_is_used_in_multiple_experiments(sel
self._validate_event_object(
log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS,
)

def test_create_impression_event_with_cmab_uuid(self):
""" Test that create_impression_event creates LogEvent object with CMAB UUID in metadata. """

expected_params = {
'account_id': '12001',
'project_id': '111001',
'visitors': [
{
'visitor_id': 'test_user',
'attributes': [],
'snapshots': [
{
'decisions': [
{'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {'flag_key': '',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation',
'enabled': False,
'cmab_uuid': 'test-cmab-uuid-123'
}
}
],
'events': [
{
'timestamp': 42123,
'entity_id': '111182',
'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
'key': 'campaign_activated',
}
],
}
],
}
],
'client_name': 'python-sdk',
'client_version': version.__version__,
'enrich_decisions': True,
'anonymize_ip': False,
'revision': '42',
}

with mock.patch('time.time', return_value=42.123), mock.patch(
'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'
):
event_obj = UserEventFactory.create_impression_event(
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'',
'rule_key',
'experiment',
False,
'test_user',
None,
'test-cmab-uuid-123' # cmab_uuid parameter
)

log_event = EventFactory.create_log_event(event_obj, self.logger)

self._validate_event_object(
log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS,
)

def test_create_impression_event_without_cmab_uuid(self):
""" Test that create_impression_event creates LogEvent object without CMAB UUID when not provided. """

expected_params = {
'account_id': '12001',
'project_id': '111001',
'visitors': [
{
'visitor_id': 'test_user',
'attributes': [],
'snapshots': [
{
'decisions': [
{
'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182',
'metadata': {
'flag_key': '',
'rule_key': 'rule_key',
'rule_type': 'experiment',
'variation_key': 'variation',
'enabled': False
}
}
],
'events': [
{
'timestamp': 42123,
'entity_id': '111182',
'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
'key': 'campaign_activated',
}
],
}
],
}
],
'client_name': 'python-sdk',
'client_version': version.__version__,
'enrich_decisions': True,
'anonymize_ip': False,
'revision': '42',
}

with mock.patch('time.time', return_value=42.123), mock.patch(
'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'
):
event_obj = UserEventFactory.create_impression_event(
self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
'111129',
'',
'rule_key',
'experiment',
False,
'test_user',
None,
None # No cmab_uuid
)

log_event = EventFactory.create_log_event(event_obj, self.logger)

# Verify no cmab_uuid in metadata
metadata = log_event.params['visitors'][0]['snapshots'][0]['decisions'][0]['metadata']
self.assertNotIn('cmab_uuid', metadata)

self._validate_event_object(
log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS,
)
51 changes: 51 additions & 0 deletions tests/test_optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -4890,6 +4890,57 @@ def test_odp_events_not_sent_with_legacy_apis(self):

client.close()

def test_get_variation_with_cmab_uuid(self):
""" Test that get_variation works correctly with CMAB UUID. """
expected_cmab_uuid = "get-variation-cmab-uuid"
variation_result = {
'variation': self.project_config.get_variation_from_id('test_experiment', '111129'),
'cmab_uuid': expected_cmab_uuid,
'reasons': [],
'error': False
}

with mock.patch(
'optimizely.decision_service.DecisionService.get_variation',
return_value=variation_result,
), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast:
variation = self.optimizely.get_variation('test_experiment', 'test_user')
self.assertEqual('variation', variation)

# Verify decision notification is sent with correct parameters
mock_broadcast.assert_any_call(
enums.NotificationTypes.DECISION,
'ab-test',
'test_user',
{},
{'experiment_key': 'test_experiment', 'variation_key': 'variation'},
)

def test_get_variation_without_cmab_uuid(self):
""" Test that get_variation works correctly when CMAB UUID is None. """
variation_result = {
'variation': self.project_config.get_variation_from_id('test_experiment', '111129'),
'cmab_uuid': None,
'reasons': [],
'error': False
}

with mock.patch(
'optimizely.decision_service.DecisionService.get_variation',
return_value=variation_result,
), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast:
variation = self.optimizely.get_variation('test_experiment', 'test_user')
self.assertEqual('variation', variation)

# Verify decision notification is sent correctly
mock_broadcast.assert_any_call(
enums.NotificationTypes.DECISION,
'ab-test',
'test_user',
{},
{'experiment_key': 'test_experiment', 'variation_key': 'variation'},
)


class OptimizelyWithExceptionTest(base.BaseTest):
def setUp(self):
Expand Down
Loading