diff --git a/optimizely/event/event_factory.py b/optimizely/event/event_factory.py index 8a4bb0cf..66f50442 100644 --- a/optimizely/event/event_factory.py +++ b/optimizely/event/event_factory.py @@ -42,7 +42,10 @@ class EventFactory: to record the events via the Optimizely Events API ("https://developers.optimizely.com/x/events/api/index.html") """ - EVENT_ENDPOINT: Final = 'https://logx.optimizely.com/v1/events' + EVENT_ENDPOINTS: Final = { + 'US': 'https://logx.optimizely.com/v1/events', + 'EU': 'https://eu.logx.optimizely.com/v1/events' + } HTTP_VERB: Final = 'POST' HTTP_HEADERS: Final = {'Content-Type': 'application/json'} ACTIVATE_EVENT_KEY: Final = 'campaign_activated' @@ -83,6 +86,7 @@ def create_log_event( return None user_context = first_event.event_context + region_value = user_context.region.value if hasattr(user_context.region, 'value') else user_context.region event_batch = payload.EventBatch( user_context.account_id, user_context.project_id, @@ -90,6 +94,7 @@ def create_log_event( user_context.client_name, user_context.client_version, user_context.anonymize_ip, + region_value, True, ) @@ -97,7 +102,10 @@ def create_log_event( event_params = event_batch.get_event_params() - return log_event.LogEvent(cls.EVENT_ENDPOINT, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS) + region_str = user_context.region.value if hasattr(user_context.region, 'value') else str(user_context.region) + endpoint = cls.EVENT_ENDPOINTS.get(region_str, cls.EVENT_ENDPOINTS['US']) + + return log_event.LogEvent(endpoint, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS) @classmethod def _create_visitor(cls, event: Optional[user_event.UserEvent], logger: Logger) -> Optional[payload.Visitor]: diff --git a/optimizely/event/payload.py b/optimizely/event/payload.py index ac6f35e4..36917230 100644 --- a/optimizely/event/payload.py +++ b/optimizely/event/payload.py @@ -34,6 +34,7 @@ def __init__( anonymize_ip: bool, enrich_decisions: bool = True, visitors: Optional[list[Visitor]] = None, + region: str = 'US' ): self.account_id = account_id self.project_id = project_id @@ -43,6 +44,7 @@ def __init__( self.anonymize_ip = anonymize_ip self.enrich_decisions = enrich_decisions self.visitors = visitors or [] + self.region = region def __eq__(self, other: object) -> bool: batch_obj = self.get_event_params() diff --git a/optimizely/event/user_event.py b/optimizely/event/user_event.py index 9cdb623a..243234d3 100644 --- a/optimizely/event/user_event.py +++ b/optimizely/event/user_event.py @@ -17,6 +17,7 @@ from sys import version_info from optimizely import version +from optimizely.project_config import Region if version_info < (3, 8): @@ -97,10 +98,11 @@ def __init__( class EventContext: """ Class respresenting User Event Context. """ - def __init__(self, account_id: str, project_id: str, revision: str, anonymize_ip: bool): + def __init__(self, account_id: str, project_id: str, revision: str, anonymize_ip: bool, region: Region): self.account_id = account_id self.project_id = project_id self.revision = revision self.client_name = CLIENT_NAME self.client_version = version.__version__ self.anonymize_ip = anonymize_ip + self.region = region or 'US' diff --git a/optimizely/event/user_event_factory.py b/optimizely/event/user_event_factory.py index ef07d06b..a15d8f57 100644 --- a/optimizely/event/user_event_factory.py +++ b/optimizely/event/user_event_factory.py @@ -76,7 +76,11 @@ def create_impression_event( variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) event_context = user_event.EventContext( - project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip, + project_config.account_id, + project_config.project_id, + project_config.revision, + project_config.anonymize_ip, + project_config.region ) return user_event.ImpressionEvent( @@ -115,7 +119,11 @@ def create_conversion_event( """ event_context = user_event.EventContext( - project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip, + project_config.account_id, + project_config.project_id, + project_config.revision, + project_config.anonymize_ip, + project_config.region ) return user_event.ConversionEvent( diff --git a/optimizely/event_builder.py b/optimizely/event_builder.py index ecabf14c..eb80bdd0 100644 --- a/optimizely/event_builder.py +++ b/optimizely/event_builder.py @@ -54,7 +54,10 @@ class EventBuilder: """ Class which encapsulates methods to build events for tracking impressions and conversions using the new V3 event API (batch). """ - EVENTS_URL: Final = 'https://logx.optimizely.com/v1/events' + EVENTS_URLS: Final = { + 'US': 'https://logx.optimizely.com/v1/events', + 'EU': 'https://eu.logx.optimizely.com/v1/events' + } HTTP_VERB: Final = 'POST' HTTP_HEADERS: Final = {'Content-Type': 'application/json'} @@ -246,7 +249,8 @@ def _get_required_params_for_conversion( def create_impression_event( self, project_config: ProjectConfig, experiment: Experiment, - variation_id: str, user_id: str, attributes: UserAttributes + variation_id: str, user_id: str, attributes: UserAttributes, + region: str = 'US' ) -> Event: """ Create impression Event to be sent to the logging endpoint. @@ -266,11 +270,17 @@ def create_impression_event( params[self.EventParams.USERS][0][self.EventParams.SNAPSHOTS].append(impression_params) - return Event(self.EVENTS_URL, params, http_verb=self.HTTP_VERB, headers=self.HTTP_HEADERS) + params['region'] = project_config.region.value + + region = project_config.region or 'US' + events_url = self.EVENTS_URLS.get(str(region), self.EVENTS_URLS['US']) + + return Event(events_url, params, http_verb=self.HTTP_VERB, headers=self.HTTP_HEADERS) def create_conversion_event( self, project_config: ProjectConfig, event_key: str, - user_id: str, attributes: UserAttributes, event_tags: event_tag_utils.EventTags + user_id: str, attributes: UserAttributes, event_tags: event_tag_utils.EventTags, + region: str = 'US' ) -> Event: """ Create conversion Event to be sent to the logging endpoint. @@ -289,4 +299,10 @@ def create_conversion_event( conversion_params = self._get_required_params_for_conversion(project_config, event_key, event_tags) params[self.EventParams.USERS][0][self.EventParams.SNAPSHOTS].append(conversion_params) - return Event(self.EVENTS_URL, params, http_verb=self.HTTP_VERB, headers=self.HTTP_HEADERS) + + params['region'] = project_config.region.value + + region = project_config.region or 'US' + events_url = self.EVENTS_URLS.get(str(region), self.EVENTS_URLS['US']) + + return Event(events_url, params, http_verb=self.HTTP_VERB, headers=self.HTTP_HEADERS) diff --git a/optimizely/project_config.py b/optimizely/project_config.py index f774ff8a..bfbf7ca5 100644 --- a/optimizely/project_config.py +++ b/optimizely/project_config.py @@ -14,6 +14,7 @@ import json from typing import TYPE_CHECKING, Optional, Type, TypeVar, cast, Any, Iterable, List from sys import version_info +from enum import Enum from . import entities from . import exceptions @@ -42,6 +43,11 @@ EntityClass = TypeVar('EntityClass') +class Region(str, Enum): + US = 'US' + EU = 'EU' + + class ProjectConfig: """ Representation of the Optimizely project config. """ @@ -85,6 +91,13 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any): self.host_for_odp: Optional[str] = None self.all_segments: list[str] = [] + region_value = config.get('region') + self.region: Region + if region_value == Region.EU.value: + self.region = Region.EU + else: + self.region = Region.US + # Utility maps for quick lookup self.group_id_map: dict[str, entities.Group] = self._generate_key_map(self.groups, 'id', entities.Group) self.experiment_id_map: dict[str, entities.Experiment] = self._generate_key_map( diff --git a/tests/base.py b/tests/base.py index 875a26e6..63344d4a 100644 --- a/tests/base.py +++ b/tests/base.py @@ -57,6 +57,7 @@ def fake_server_response(self, status_code: Optional[int] = None, def setUp(self, config_dict='config_dict'): self.config_dict = { + 'region': 'US', 'revision': '42', 'sdkKey': 'basic-test', 'version': '2', @@ -150,6 +151,7 @@ def setUp(self, config_dict='config_dict'): # datafile version 4 self.config_dict_with_features = { + 'region': 'US', 'revision': '1', 'sdkKey': 'features-test', 'accountId': '12001', @@ -553,6 +555,7 @@ def setUp(self, config_dict='config_dict'): } self.config_dict_with_multiple_experiments = { + 'region': 'US', 'revision': '42', 'sdkKey': 'multiple-experiments', 'version': '2', @@ -686,6 +689,7 @@ def setUp(self, config_dict='config_dict'): 'accountId': '10367498574', 'events': [{'experimentIds': ['10420810910'], 'id': '10404198134', 'key': 'winning'}], 'revision': '1337', + 'region': 'US', } self.config_dict_with_typed_audiences = { @@ -1078,6 +1082,7 @@ def setUp(self, config_dict='config_dict'): ], 'revision': '3', 'sdkKey': 'typed-audiences', + 'region': 'US', } self.config_dict_with_audience_segments = { @@ -1274,7 +1279,8 @@ def setUp(self, config_dict='config_dict'): } ], 'revision': '101', - 'sdkKey': 'segments-test' + 'sdkKey': 'segments-test', + 'region': 'US', } config = getattr(self, config_dict) diff --git a/tests/test_config.py b/tests/test_config.py index 9ec5c761..2d7fff5f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -21,7 +21,7 @@ from optimizely import logger from optimizely import optimizely from optimizely.helpers import enums -from optimizely.project_config import ProjectConfig +from optimizely.project_config import ProjectConfig, Region from . import base @@ -154,6 +154,28 @@ def test_init(self): self.assertEqual(expected_variation_key_map, self.project_config.variation_key_map) self.assertEqual(expected_variation_id_map, self.project_config.variation_id_map) + def test_region_when_no_region(self): + """ Test that region defaults to 'US' when not specified in the config. """ + config_dict = copy.deepcopy(self.config_dict_with_multiple_experiments) + opt_obj = optimizely.Optimizely(json.dumps(config_dict)) + project_config = opt_obj.config_manager.get_config() + self.assertEqual(project_config.region, Region.US) + + def test_region_when_specified_in_datafile(self): + """ Test that region is set to 'US' when specified in the config. """ + config_dict_us = copy.deepcopy(self.config_dict_with_multiple_experiments) + config_dict_us['region'] = 'US' + opt_obj_us = optimizely.Optimizely(json.dumps(config_dict_us)) + project_config_us = opt_obj_us.config_manager.get_config() + self.assertEqual(project_config_us.region, Region.US) + + """ Test that region is set to 'EU' when specified in the config. """ + config_dict_eu = copy.deepcopy(self.config_dict_with_multiple_experiments) + config_dict_eu['region'] = 'EU' + opt_obj_eu = optimizely.Optimizely(json.dumps(config_dict_eu)) + project_config_eu = opt_obj_eu.config_manager.get_config() + self.assertEqual(project_config_eu.region, Region.EU) + def test_cmab_field_population(self): """ Test that the cmab field is populated correctly in experiments.""" diff --git a/tests/test_event_builder.py b/tests/test_event_builder.py index fb4d7a0d..593d084c 100644 --- a/tests/test_event_builder.py +++ b/tests/test_event_builder.py @@ -86,6 +86,7 @@ def test_create_impression_event(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -100,7 +101,7 @@ def test_create_impression_event(self): ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -141,6 +142,7 @@ def test_create_impression_event__with_attributes(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -155,7 +157,7 @@ def test_create_impression_event__with_attributes(self): ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -194,6 +196,7 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -205,10 +208,11 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self): '111129', 'test_user', {'do_you_know_me': 'test_value'}, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -250,6 +254,7 @@ def test_create_impression_event_calls_is_attribute_valid(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } def side_effect(*args, **kwargs): @@ -276,11 +281,12 @@ def side_effect(*args, **kwargs): '111129', 'test_user', attributes, + 'US', ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -328,6 +334,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled( 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -341,11 +348,12 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled( '111129', 'test_user', {'$opt_user_agent': 'Edge'}, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -392,6 +400,7 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -405,11 +414,12 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en '111129', 'test_user', None, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -462,6 +472,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -475,11 +486,12 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled '111129', 'test_user', {'$opt_user_agent': 'Chrome'}, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -515,17 +527,18 @@ def test_create_conversion_event(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ): event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', None, None + self.project_config, 'test_event', 'test_user', None, None, 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -563,17 +576,18 @@ def test_create_conversion_event__with_attributes(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' ): event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'test_attribute': 'test_value'}, None, + self.project_config, 'test_event', 'test_user', {'test_attribute': 'test_value'}, None, 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -618,6 +632,7 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled( 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -626,12 +641,12 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled( 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, ): event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Edge'}, None, + self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Edge'}, None, 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -681,6 +696,7 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -689,12 +705,12 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False, ): event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Chrome'}, None, + self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Chrome'}, None, 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -735,6 +751,7 @@ def test_create_conversion_event__with_event_tags(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -746,10 +763,11 @@ def test_create_conversion_event__with_event_tags(self): 'test_user', {'test_attribute': 'test_value'}, {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -788,6 +806,7 @@ def test_create_conversion_event__with_invalid_event_tags(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -799,10 +818,11 @@ def test_create_conversion_event__with_invalid_event_tags(self): 'test_user', {'test_attribute': 'test_value'}, {'revenue': '4200', 'value': True, 'non-revenue': 'abc'}, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, @@ -843,6 +863,7 @@ def test_create_conversion_event__when_event_is_used_in_multiple_experiments(sel 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -854,10 +875,11 @@ def test_create_conversion_event__when_event_is_used_in_multiple_experiments(sel 'test_user', {'test_attribute': 'test_value'}, {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + 'US' ) self._validate_event_object( event_obj, - event_builder.EventBuilder.EVENTS_URL, + event_builder.EventBuilder.EVENTS_URLS.get('US'), expected_params, event_builder.EventBuilder.HTTP_VERB, event_builder.EventBuilder.HTTP_HEADERS, diff --git a/tests/test_event_factory.py b/tests/test_event_factory.py index adbebd35..fd73582a 100644 --- a/tests/test_event_factory.py +++ b/tests/test_event_factory.py @@ -98,6 +98,7 @@ def test_create_impression_event(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -118,7 +119,11 @@ def test_create_impression_event(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_impression_event__with_attributes(self): @@ -162,6 +167,7 @@ def test_create_impression_event__with_attributes(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -182,7 +188,11 @@ def test_create_impression_event__with_attributes(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_impression_event_when_attribute_is_not_in_datafile(self): @@ -224,6 +234,7 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -244,7 +255,11 @@ def test_create_impression_event_when_attribute_is_not_in_datafile(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_impression_event_calls_is_attribute_valid(self): @@ -287,6 +302,7 @@ def test_create_impression_event_calls_is_attribute_valid(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } def side_effect(*args, **kwargs): @@ -296,39 +312,39 @@ def side_effect(*args, **kwargs): return False - attributes = { - 'test_attribute': 'test_value', - 'boolean_key': True, - 'integer_key': 0, - 'double_key': 5.5, - } - - with mock.patch('time.time', return_value=42.123), mock.patch( - 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' - ), mock.patch( - 'optimizely.helpers.validator.is_attribute_valid', side_effect=side_effect, - ): - - event_obj = UserEventFactory.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', - '', - 'experiment', - 'test_user', - attributes, - ) - - 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, + attributes = { + 'test_attribute': 'test_value', + 'boolean_key': True, + 'integer_key': 0, + 'double_key': 5.5, + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.helpers.validator.is_attribute_valid', side_effect=side_effect, + ): + + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + '', + 'experiment', + 'test_user', + attributes, ) + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, + ) + def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(self,): """ Test that create_impression_event creates Event object with right params when user agent attribute is provided and @@ -377,6 +393,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled( 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -399,7 +416,11 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled( 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_enabled(self,): @@ -449,6 +470,7 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -471,7 +493,11 @@ def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_en 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled(self,): @@ -527,6 +553,7 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -549,7 +576,11 @@ def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event(self): @@ -582,6 +613,7 @@ def test_create_conversion_event(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -594,7 +626,11 @@ def test_create_conversion_event(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event__with_attributes(self): @@ -629,6 +665,7 @@ def test_create_conversion_event__with_attributes(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -641,7 +678,11 @@ def test_create_conversion_event__with_attributes(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled(self,): @@ -683,6 +724,7 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled( 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -697,7 +739,11 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled( 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled(self,): @@ -744,6 +790,7 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -758,7 +805,11 @@ def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event__with_event_tags(self): @@ -796,6 +847,7 @@ def test_create_conversion_event__with_event_tags(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -812,7 +864,11 @@ def test_create_conversion_event__with_event_tags(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event__with_invalid_event_tags(self): @@ -848,6 +904,7 @@ def test_create_conversion_event__with_invalid_event_tags(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -864,7 +921,11 @@ def test_create_conversion_event__with_invalid_event_tags(self): 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) def test_create_conversion_event__when_event_is_used_in_multiple_experiments(self): @@ -902,6 +963,7 @@ def test_create_conversion_event__when_event_is_used_in_multiple_experiments(sel 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } with mock.patch('time.time', return_value=42.123), mock.patch( @@ -918,5 +980,9 @@ def test_create_conversion_event__when_event_is_used_in_multiple_experiments(sel 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, + log_event, + EventFactory.EVENT_ENDPOINTS.get('US'), + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, ) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 1f4293cd..2c9caa87 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -365,6 +365,7 @@ def test_activate(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -385,6 +386,76 @@ def test_activate(self): {'Content-Type': 'application/json'}, ) + def test_activate_with_eu_hosting(self): + """ Test that activate calls process with right params and returns expected variation. """ + """ Test EU hosting for activate method. """ + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation', + return_value=(self.project_config.get_variation_from_id('test_experiment', '111129'), []), + ) as mock_decision, mock.patch('time.time', return_value=42), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.event.event_processor.BatchEventProcessor.process' + ) as mock_process: + self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) + + 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': 'test_experiment', + 'rule_type': 'experiment', + 'variation_key': 'variation', + 'enabled': True}, + } + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + 'region': 'EU', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + user_context = mock_decision.call_args[0][2] + user_profile_tracker = mock_decision.call_args[0][3] + + mock_decision.assert_called_once_with( + self.project_config, self.project_config.get_experiment_from_key('test_experiment'), + user_context, user_profile_tracker + ) + self.assertEqual(1, mock_process.call_count) + + self._validate_event_object( + log_event.__dict__, + 'https://eu.logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + def test_add_activate_remove_clear_listener(self): callbackhit = [False] """ Test adding a listener activate passes correctly and gets called""" @@ -764,6 +835,7 @@ def test_activate__with_attributes__audience_match(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -848,6 +920,7 @@ def test_activate__with_attributes_of_different_types(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1044,6 +1117,7 @@ def test_activate__with_attributes__audience_match__forced_bucketing(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1120,6 +1194,7 @@ def test_activate__with_attributes__audience_match__bucketing_id_provided(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1288,6 +1363,7 @@ def test_track__with_attributes(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1424,6 +1500,7 @@ def test_track__with_attributes__bucketing_id_provided(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1504,6 +1581,7 @@ def test_track__with_event_tags(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1560,6 +1638,7 @@ def test_track__with_event_tags_revenue(self): 'account_id': '12001', 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1648,6 +1727,7 @@ def test_track__with_event_tags__forced_bucketing(self): 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -1703,6 +1783,7 @@ def test_track__with_invalid_event_tags(self): 'account_id': '12001', 'anonymize_ip': False, 'revision': '42', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -2103,6 +2184,7 @@ def test_is_feature_enabled__returns_true_for_feature_experiment_if_feature_enab 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '1', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -2204,6 +2286,7 @@ def test_is_feature_enabled__returns_false_for_feature_experiment_if_feature_dis 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '1', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) @@ -2356,6 +2439,7 @@ def test_is_feature_enabled__returns_true_for_feature_rollout_if_feature_enabled 'enrich_decisions': True, 'anonymize_ip': False, 'revision': '1', + 'region': 'US', } log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) diff --git a/tests/test_user_event_factory.py b/tests/test_user_event_factory.py index 009ef05d..4e4d17d1 100644 --- a/tests/test_user_event_factory.py +++ b/tests/test_user_event_factory.py @@ -41,6 +41,21 @@ def test_impression_event(self): self.assertEqual(experiment, impression_event.experiment) self.assertEqual(variation, impression_event.variation) self.assertEqual(user_id, impression_event.user_id) + self.assertEqual(self.project_config.region, impression_event.event_context.region) + + def test_impression_event_with_region_eu(self): + project_config = self.project_config + experiment = self.project_config.get_experiment_from_key('test_experiment') + user_id = 'test_user' + + project_config.region = 'EU' + + impression_event = UserEventFactory.create_impression_event( + project_config, experiment, '111128', '', 'rule_key', 'rule_type', True, user_id, None + ) + + self.assertEqual(self.project_config.region, impression_event.event_context.region) + self.assertEqual('EU', impression_event.event_context.region) def test_impression_event__with_attributes(self): project_config = self.project_config @@ -66,6 +81,7 @@ def test_impression_event__with_attributes(self): self.assertEqual(experiment, impression_event.experiment) self.assertEqual(variation, impression_event.variation) self.assertEqual(user_id, impression_event.user_id) + self.assertEqual(self.project_config.region, impression_event.event_context.region) self.assertEqual( [x.__dict__ for x in expected_attrs], [x.__dict__ for x in impression_event.visitor_attributes], ) @@ -91,6 +107,7 @@ def test_conversion_event(self): self.assertEqual(self.project_config.bot_filtering, conversion_event.bot_filtering) self.assertEqual(self.project_config.get_event(event_key), conversion_event.event) self.assertEqual(user_id, conversion_event.user_id) + self.assertEqual(self.project_config.region, conversion_event.event_context.region) self.assertEqual( [x.__dict__ for x in expected_attrs], [x.__dict__ for x in conversion_event.visitor_attributes], ) @@ -117,6 +134,7 @@ def test_conversion_event__with_event_tags(self): self.assertEqual(self.project_config.bot_filtering, conversion_event.bot_filtering) self.assertEqual(self.project_config.get_event(event_key), conversion_event.event) self.assertEqual(user_id, conversion_event.user_id) + self.assertEqual(self.project_config.region, conversion_event.event_context.region) self.assertEqual( [x.__dict__ for x in expected_attrs], [x.__dict__ for x in conversion_event.visitor_attributes], )