diff --git a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs index cc606667..5f5cf296 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs @@ -203,11 +203,15 @@ public void ShouldLogWhenOdpNotIntegratedAndIdentifyUserCalled() eventManager.UpdateSettings(mockOdpConfig. Object); // auto-start after update; Logs 1x here - eventManager.IdentifyUser(FS_USER_ID); // Logs 1x here too + eventManager.IdentifyUser(FS_USER_ID); // Blocked by single identifier check _mockLogger.Verify( l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), - Times.Exactly(3)); // during Start() and SendEvent() + Times.Exactly(2)); // during Start() and UpdateSettings() + _mockLogger.Verify( + l => l.Log(LogLevel.DEBUG, + "ODP identify event is not dispatched (fewer than 2 valid identifiers)."), + Times.Once); // IdentifyUser blocked before reaching SendEvent } [Test] @@ -606,14 +610,12 @@ public void ShouldFlushAllScheduledEventsBeforeStopping() } [Test] - public void ShouldPrepareCorrectPayloadForIdentifyUser() + public void ShouldNotSendIdentifyEventWithSingleIdentifier() { const string USER_ID = "test_fs_user_id"; - var cde = new CountdownEvent(1); var eventsCollector = new List>(); _mockApiManager.Setup(api => api.SendEvents(It.IsAny(), It.IsAny(), - Capture.In(eventsCollector))). - Callback(() => cde.Signal()); + Capture.In(eventsCollector))); var eventManager = new OdpEventManager.Builder(). WithOdpEventApiManager(_mockApiManager.Object). WithLogger(_mockLogger.Object). @@ -623,20 +625,16 @@ public void ShouldPrepareCorrectPayloadForIdentifyUser() eventManager.UpdateSettings(_odpConfig); eventManager.IdentifyUser(USER_ID); - cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); - var eventsSentToApi = eventsCollector.FirstOrDefault(); - var actualEvent = eventsSentToApi?.FirstOrDefault(); - Assert.IsNotNull(actualEvent); - Assert.AreEqual(Constants.ODP_EVENT_TYPE, actualEvent.Type); - Assert.AreEqual("identified", actualEvent.Action); - Assert.AreEqual(USER_ID, actualEvent.Identifiers[Constants.FS_USER_ID]); - var eventData = actualEvent.Data; - Assert.AreEqual(Guid.NewGuid().ToString().Length, - eventData["idempotence_id"].ToString().Length); - Assert.AreEqual("sdk", eventData["data_source_type"]); - Assert.AreEqual("csharp-sdk", eventData["data_source"]); - Assert.IsNotNull(eventData["data_source_version"]); + // Allow time for any async processing + Thread.Sleep(500); + + // No events should be sent since only one identifier (fs_user_id) was provided + Assert.IsEmpty(eventsCollector); + _mockLogger.Verify( + l => l.Log(LogLevel.DEBUG, + "ODP identify event is not dispatched (fewer than 2 valid identifiers)."), + Times.Once); } [Test] diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index b880daa9..7856170d 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -378,7 +378,21 @@ public void IdentifyUser(string userId) { Constants.FS_USER_ID, userId }, }; - var odpEvent = new OdpEvent(Constants.ODP_EVENT_TYPE, "identified", identifiers); + // Filter out null/empty identifier values and require 2+ valid identifiers + var validIdentifiers = identifiers + .Where(kvp => !string.IsNullOrEmpty(kvp.Value)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // Identify requires 2+ identifiers to link (e.g., vuid + fs_user_id). + // A single identifier has no cross-reference value and generates unnecessary traffic. + if (validIdentifiers.Count < 2) + { + _logger.Log(LogLevel.DEBUG, + "ODP identify event is not dispatched (fewer than 2 valid identifiers)."); + return; + } + + var odpEvent = new OdpEvent(Constants.ODP_EVENT_TYPE, "identified", validIdentifiers); SendEvent(odpEvent); }