Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 21 additions & 2 deletions optimizely/odp/odp_event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,28 @@ def dispatch(self, event: OdpEvent) -> None:
except Full:
self.logger.warning(Errors.ODP_EVENT_FAILED.format("Queue is full"))

def identify_user(self, user_id: str) -> None:
def identify_user(self, identifiers: dict[str, str]) -> None:
"""Send an identify event to ODP if there are multiple valid identifiers.

An identify event is only useful when there are two or more identifiers
to link together in the ODP user profile. Sending a single identifier
provides no linking value and wastes a network call.

Args:
identifiers: A dictionary of identifier key-value pairs
(e.g. {"fs_user_id": "abc", "email": "a@b.com"}).
"""
# Filter out identifiers with None or empty string values.
valid_identifiers = {k: v for k, v in identifiers.items() if v}

if len(valid_identifiers) < 2:
self.logger.debug(
'ODP identify event is not dispatched (only one identifier provided).'
)
return

self.send_event(OdpManagerConfig.EVENT_TYPE, 'identified',
{OdpManagerConfig.KEY_FOR_USER_ID: user_id}, {})
valid_identifiers, {})

def update_config(self) -> None:
"""Adds update config signal to event_queue."""
Expand Down
4 changes: 2 additions & 2 deletions optimizely/odp/odp_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ def fetch_qualified_segments(self, user_id: str, options: list[str]) -> Optional

return self.segment_manager.fetch_qualified_segments(user_key, user_value, options)

def identify_user(self, user_id: str) -> None:
def identify_user(self, identifiers: dict[str, str]) -> None:
if not self.enabled or not self.event_manager:
self.logger.debug('ODP identify event is not dispatched (ODP disabled).')
return
if self.odp_config.odp_state() == OdpConfigState.NOT_INTEGRATED:
self.logger.debug('ODP identify event is not dispatched (ODP not integrated).')
return

self.event_manager.identify_user(user_id)
self.event_manager.identify_user(identifiers)

def send_event(self, type: str, action: str, identifiers: dict[str, str], data: dict[str, Any]) -> None:
"""
Expand Down
4 changes: 2 additions & 2 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -1541,7 +1541,7 @@ def _update_odp_config_on_datafile_update(self) -> None:
config.all_segments
)

def _identify_user(self, user_id: str) -> None:
def _identify_user(self, identifiers: dict[str, str]) -> None:
if not self.is_valid:
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('identify_user'))
return
Expand All @@ -1551,7 +1551,7 @@ def _identify_user(self, user_id: str) -> None:
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('identify_user'))
return

self.odp_manager.identify_user(user_id)
self.odp_manager.identify_user(identifiers)

def _fetch_qualified_segments(self, user_id: str, options: Optional[list[str]] = None) -> Optional[list[str]]:
if not self.is_valid:
Expand Down
4 changes: 3 additions & 1 deletion optimizely/optimizely_user_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import TYPE_CHECKING, Any, Callable, Optional, NewType, Dict

from optimizely.decision import optimizely_decision
from optimizely.helpers.enums import OdpManagerConfig

if TYPE_CHECKING:
# prevent circular dependency by skipping import at runtime
Expand Down Expand Up @@ -73,7 +74,8 @@ def __init__(
] = {}

if self.client and identify:
self.client._identify_user(user_id)
identifiers = {OdpManagerConfig.KEY_FOR_USER_ID: user_id}
self.client._identify_user(identifiers)

class OptimizelyDecisionContext:
""" Using class with attributes here instead of namedtuple because
Expand Down
60 changes: 53 additions & 7 deletions tests/test_odp_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_configurations_disable_odp(self):
mock_logger.reset_mock()

# these call should be dropped gracefully with None
manager.identify_user('user1')
manager.identify_user({'fs_user_id': 'user1'})

manager.send_event('t1', 'a1', {}, {})
mock_logger.error.assert_called_once_with('ODP is not enabled.')
Expand Down Expand Up @@ -126,10 +126,11 @@ def test_identify_user_datafile_not_ready(self):

manager = OdpManager(False, OptimizelySegmentsCache, event_manager=event_manager, logger=mock_logger)

identifiers = {'fs_user_id': 'user1', 'email': 'test@example.com'}
with mock.patch.object(event_manager, 'identify_user') as mock_identify_user:
manager.identify_user('user1')
manager.identify_user(identifiers)

mock_identify_user.assert_called_once_with('user1')
mock_identify_user.assert_called_once_with(identifiers)
mock_logger.error.assert_not_called()

def test_identify_user_odp_integrated(self):
Expand All @@ -139,13 +140,14 @@ def test_identify_user_odp_integrated(self):
manager = OdpManager(False, LRUCache(10, 20), event_manager=event_manager, logger=mock_logger)
manager.update_odp_config('key1', 'host1', [])

identifiers = {'fs_user_id': 'user1', 'email': 'test@example.com'}
with mock.patch.object(event_manager, 'dispatch') as mock_dispatch_event:
manager.identify_user('user1')
manager.identify_user(identifiers)

mock_dispatch_event.assert_called_once_with({
'type': 'fullstack',
'action': 'identified',
'identifiers': {'fs_user_id': 'user1'},
'identifiers': {'fs_user_id': 'user1', 'email': 'test@example.com'},
'data': {
'idempotence_id': mock.ANY,
'data_source_type': 'sdk',
Expand All @@ -161,8 +163,9 @@ def test_identify_user_odp_not_integrated(self):
manager = OdpManager(False, CustomCache(), event_manager=event_manager, logger=mock_logger)
manager.update_odp_config(None, None, [])

identifiers = {'fs_user_id': 'user1', 'email': 'test@example.com'}
with mock.patch.object(event_manager, 'dispatch') as mock_dispatch_event:
manager.identify_user('user1')
manager.identify_user(identifiers)

mock_dispatch_event.assert_not_called()
mock_logger.error.assert_not_called()
Expand All @@ -175,13 +178,56 @@ def test_identify_user_odp_disabled(self):
manager = OdpManager(False, OptimizelySegmentsCache, event_manager=event_manager, logger=mock_logger)
manager.enabled = False

identifiers = {'fs_user_id': 'user1', 'email': 'test@example.com'}
with mock.patch.object(event_manager, 'identify_user') as mock_identify_user:
manager.identify_user('user1')
manager.identify_user(identifiers)

mock_identify_user.assert_not_called()
mock_logger.error.assert_not_called()
mock_logger.debug.assert_called_with('ODP identify event is not dispatched (ODP disabled).')

def test_identify_user_single_identifier_skipped(self):
"""Identify event should not be sent when only one identifier is provided."""
mock_logger = mock.MagicMock()
event_manager = OdpEventManager(mock_logger, OdpEventApiManager())

manager = OdpManager(False, LRUCache(10, 20), event_manager=event_manager, logger=mock_logger)
manager.update_odp_config('key1', 'host1', [])

with mock.patch.object(event_manager, 'dispatch') as mock_dispatch_event:
manager.identify_user({'fs_user_id': 'user1'})

mock_dispatch_event.assert_not_called()

def test_identify_user_empty_values_not_counted(self):
"""Identifiers with empty or None values should not count toward the minimum."""
mock_logger = mock.MagicMock()
event_manager = OdpEventManager(mock_logger, OdpEventApiManager())

manager = OdpManager(False, LRUCache(10, 20), event_manager=event_manager, logger=mock_logger)
manager.update_odp_config('key1', 'host1', [])

with mock.patch.object(event_manager, 'dispatch') as mock_dispatch_event:
manager.identify_user({'fs_user_id': 'user1', 'email': '', 'vuid': None})

mock_dispatch_event.assert_not_called()

def test_identify_user_multiple_identifiers_sent(self):
"""Identify event should be sent when two or more valid identifiers are provided."""
mock_logger = mock.MagicMock()
event_manager = OdpEventManager(mock_logger, OdpEventApiManager())

manager = OdpManager(False, LRUCache(10, 20), event_manager=event_manager, logger=mock_logger)
manager.update_odp_config('key1', 'host1', [])

identifiers = {'fs_user_id': 'user1', 'vuid': 'vuid123', 'email': 'a@b.com'}
with mock.patch.object(event_manager, 'dispatch') as mock_dispatch_event:
manager.identify_user(identifiers)

mock_dispatch_event.assert_called_once()
event = mock_dispatch_event.call_args[0][0]
self.assertEqual(event.identifiers, identifiers)

def test_send_event_datafile_not_ready(self):
mock_logger = mock.MagicMock()
event_manager = OdpEventManager(mock_logger, OdpEventApiManager())
Expand Down
2 changes: 1 addition & 1 deletion tests/test_optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -5343,7 +5343,7 @@ def test_send_identify_event__when_called_with_odp_enabled(self):
with mock.patch.object(client, '_identify_user') as identify:
client.create_user_context('user-id')

identify.assert_called_once_with('user-id')
identify.assert_called_once_with({'fs_user_id': 'user-id'})
mock_logger.error.assert_not_called()
client.close()

Expand Down
4 changes: 2 additions & 2 deletions tests/test_user_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2094,7 +2094,7 @@ def test_send_identify_event_when_user_context_created(self):
with mock.patch.object(client, '_identify_user') as identify:
OptimizelyUserContext(client, mock_logger, 'user-id')

identify.assert_called_once_with('user-id')
identify.assert_called_once_with({'fs_user_id': 'user-id'})
mock_logger.error.assert_not_called()
client.close()

Expand All @@ -2104,7 +2104,7 @@ def test_identify_is_skipped_with_decisions(self):
with mock.patch.object(client, '_identify_user') as identify:
user_context = OptimizelyUserContext(client, mock_logger, 'user-id')

identify.assert_called_once_with('user-id')
identify.assert_called_once_with({'fs_user_id': 'user-id'})
mock_logger.error.assert_not_called()

with mock.patch.object(client, '_identify_user') as identify:
Expand Down
Loading