From 1b5a1c54004d277f8a618195d18422794631323e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 1 Jun 2026 15:26:35 -0700 Subject: [PATCH] [FSSDK-12721] Skip ODP identify event for single-identifier users Change IdentifyUser call chain to accept Dictionary identifiers instead of single userId string. Only send ODP identify event when 2+ valid (non-null, non-empty) identifiers exist. Filter out null/empty values before counting. Add tests for single identifier (skipped), multiple identifiers (sent), and null/empty value filtering. Update existing tests to use dict-based identifier propagation. --- .../OdpTests/OdpEventManagerTests.cs | 14 ++- .../OdpTests/OdpManagerTest.cs | 85 +++++++++++++++++-- OptimizelySDK.Tests/OptimizelyTest.cs | 5 +- OptimizelySDK/Odp/IOdpEventManager.cs | 7 +- OptimizelySDK/Odp/IOdpManager.cs | 6 +- OptimizelySDK/Odp/OdpEventManager.cs | 11 +-- OptimizelySDK/Odp/OdpManager.cs | 26 +++++- OptimizelySDK/Optimizely.cs | 6 +- OptimizelySDK/OptimizelyUserContext.cs | 5 +- 9 files changed, 132 insertions(+), 33 deletions(-) diff --git a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs index cc606667..b25fd118 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs @@ -203,7 +203,11 @@ 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(new Dictionary + { + { FS_USER_ID, "test-user" }, + { "email", "user@example.com" }, + }); // Logs 1x here too _mockLogger.Verify( l => l.Log(LogLevel.WARN, Constants.ODP_NOT_INTEGRATED_MESSAGE), @@ -622,7 +626,12 @@ public void ShouldPrepareCorrectPayloadForIdentifyUser() Build(); eventManager.UpdateSettings(_odpConfig); - eventManager.IdentifyUser(USER_ID); + var identifiers = new Dictionary + { + { Constants.FS_USER_ID, USER_ID }, + { "email", "user@example.com" }, + }; + eventManager.IdentifyUser(identifiers); cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS); var eventsSentToApi = eventsCollector.FirstOrDefault(); @@ -631,6 +640,7 @@ public void ShouldPrepareCorrectPayloadForIdentifyUser() Assert.AreEqual(Constants.ODP_EVENT_TYPE, actualEvent.Type); Assert.AreEqual("identified", actualEvent.Action); Assert.AreEqual(USER_ID, actualEvent.Identifiers[Constants.FS_USER_ID]); + Assert.AreEqual("user@example.com", actualEvent.Identifiers["email"]); var eventData = actualEvent.Data; Assert.AreEqual(Guid.NewGuid().ToString().Length, eventData["idempotence_id"].ToString().Length); diff --git a/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs b/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs index 6b2bc9ac..52764503 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpManagerTest.cs @@ -283,9 +283,36 @@ public void ShouldGetSegmentManager() } [Test] - public void ShouldIdentifyUserWhenOdpIsIntegrated() + public void ShouldIdentifyUserWhenMultipleIdentifiers() { - _mockOdpEventManager.Setup(e => e.IdentifyUser(It.IsAny())); + _mockOdpEventManager.Setup(e => + e.IdentifyUser(It.IsAny>())); + _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); + var manager = new OdpManager.Builder(). + WithEventManager(_mockOdpEventManager.Object). + WithLogger(_mockLogger.Object). + Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); + + var identifiers = new Dictionary + { + { Constants.FS_USER_ID, VALID_FS_USER_ID }, + { "email", "user@example.com" }, + }; + manager.IdentifyUser(identifiers); + manager.Dispose(); + + _mockLogger.Verify( + l => l.Log(It.IsAny(), It.IsAny()), Times.Never); + _mockOdpEventManager.Verify( + e => e.IdentifyUser(It.IsAny>()), Times.Once); + } + + [Test] + public void ShouldNotIdentifyUserWhenSingleIdentifier() + { + _mockOdpEventManager.Setup(e => + e.IdentifyUser(It.IsAny>())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). @@ -293,17 +320,25 @@ public void ShouldIdentifyUserWhenOdpIsIntegrated() Build(); manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); - manager.IdentifyUser(VALID_FS_USER_ID); + var identifiers = new Dictionary + { + { Constants.FS_USER_ID, VALID_FS_USER_ID }, + }; + manager.IdentifyUser(identifiers); manager.Dispose(); - _mockLogger.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Never); - _mockOdpEventManager.Verify(e => e.IdentifyUser(It.IsAny()), Times.Once); + _mockLogger.Verify(l => + l.Log(LogLevel.DEBUG, + "ODP identify event not dispatched (fewer than 2 valid identifiers).")); + _mockOdpEventManager.Verify( + e => e.IdentifyUser(It.IsAny>()), Times.Never); } [Test] public void ShouldNotIdentifyUserWhenOdpDisabled() { - _mockOdpEventManager.Setup(e => e.IdentifyUser(It.IsAny())); + _mockOdpEventManager.Setup(e => + e.IdentifyUser(It.IsAny>())); _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); var manager = new OdpManager.Builder(). WithEventManager(_mockOdpEventManager.Object). @@ -311,12 +346,46 @@ public void ShouldNotIdentifyUserWhenOdpDisabled() Build(false); manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); - manager.IdentifyUser(VALID_FS_USER_ID); + var identifiers = new Dictionary + { + { Constants.FS_USER_ID, VALID_FS_USER_ID }, + { "email", "user@example.com" }, + }; + manager.IdentifyUser(identifiers); manager.Dispose(); _mockLogger.Verify(l => l.Log(LogLevel.DEBUG, "ODP identify event not dispatched (ODP disabled).")); - _mockOdpEventManager.Verify(e => e.IdentifyUser(It.IsAny()), Times.Never); + _mockOdpEventManager.Verify( + e => e.IdentifyUser(It.IsAny>()), Times.Never); + } + + [Test] + public void ShouldNotIdentifyUserWhenNullEmptyValues() + { + _mockOdpEventManager.Setup(e => + e.IdentifyUser(It.IsAny>())); + _mockOdpEventManager.Setup(e => e.IsStarted).Returns(true); + var manager = new OdpManager.Builder(). + WithEventManager(_mockOdpEventManager.Object). + WithLogger(_mockLogger.Object). + Build(); + manager.UpdateSettings(API_KEY, API_HOST, _emptySegmentsToCheck); + + // Two identifiers but one is empty - should not send + var identifiers = new Dictionary + { + { Constants.FS_USER_ID, VALID_FS_USER_ID }, + { "email", "" }, + }; + manager.IdentifyUser(identifiers); + manager.Dispose(); + + _mockLogger.Verify(l => + l.Log(LogLevel.DEBUG, + "ODP identify event not dispatched (fewer than 2 valid identifiers).")); + _mockOdpEventManager.Verify( + e => e.IdentifyUser(It.IsAny>()), Times.Never); } [Test] diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 26052021..1b5e1f7d 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -6232,7 +6232,10 @@ public void TestFetchQualifiedSegmentsInvalidOptimizelyObject() public void TestIdentifyUserInvalidOptimizelyObject() { var optly = new Optimizely("Random datafile", null, LoggerMock.Object); - optly.IdentifyUser("some_user"); + optly.IdentifyUser(new Dictionary + { + { "fs_user_id", "some_user" }, + }); LoggerMock.Verify( l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IdentifyUser'."), Times.Once); diff --git a/OptimizelySDK/Odp/IOdpEventManager.cs b/OptimizelySDK/Odp/IOdpEventManager.cs index b9e1465d..b48d8d6e 100644 --- a/OptimizelySDK/Odp/IOdpEventManager.cs +++ b/OptimizelySDK/Odp/IOdpEventManager.cs @@ -15,6 +15,7 @@ */ using System; +using System.Collections.Generic; using OptimizelySDK.Odp.Entity; namespace OptimizelySDK.Odp @@ -52,10 +53,10 @@ public interface IOdpEventManager void Dispose(); /// - /// Associate a full-stack userid with an established VUID + /// Send an identify event to ODP with the given identifiers /// - /// Full-stack User ID - void IdentifyUser(string userId); + /// Dictionary of identifier key-value pairs + void IdentifyUser(Dictionary identifiers); /// /// Update ODP configuration settings diff --git a/OptimizelySDK/Odp/IOdpManager.cs b/OptimizelySDK/Odp/IOdpManager.cs index ac112f1d..4e65e740 100644 --- a/OptimizelySDK/Odp/IOdpManager.cs +++ b/OptimizelySDK/Odp/IOdpManager.cs @@ -41,10 +41,10 @@ public interface IOdpManager string[] FetchQualifiedSegments(string userId, List options); /// - /// Send identification event to ODP for a given full-stack User ID + /// Send identification event to ODP when 2+ valid identifiers exist /// - /// User ID to send - void IdentifyUser(string userId); + /// Dictionary of identifier key-value pairs + void IdentifyUser(Dictionary identifiers); /// /// Add event to queue for sending to ODP diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index b880daa9..3b65801c 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -368,16 +368,11 @@ public void Dispose() } /// - /// Associate a full-stack userid with an established VUID + /// Send an identify event to ODP with the given identifiers /// - /// Full-stack User ID - public void IdentifyUser(string userId) + /// Dictionary of identifier key-value pairs + public void IdentifyUser(Dictionary identifiers) { - var identifiers = new Dictionary - { - { Constants.FS_USER_ID, userId }, - }; - var odpEvent = new OdpEvent(Constants.ODP_EVENT_TYPE, "identified", identifiers); SendEvent(odpEvent); } diff --git a/OptimizelySDK/Odp/OdpManager.cs b/OptimizelySDK/Odp/OdpManager.cs index 63738d03..b6dcf360 100644 --- a/OptimizelySDK/Odp/OdpManager.cs +++ b/OptimizelySDK/Odp/OdpManager.cs @@ -97,10 +97,10 @@ public string[] FetchQualifiedSegments(string userId, List opt } /// - /// Send identification event to ODP for a given full-stack User ID + /// Send identification event to ODP when 2+ valid identifiers exist /// - /// User ID to send - public void IdentifyUser(string userId) + /// Dictionary of identifier key-value pairs + public void IdentifyUser(Dictionary identifiers) { if (EventManagerOrConfigNotReady()) { @@ -108,7 +108,25 @@ public void IdentifyUser(string userId) return; } - EventManager.IdentifyUser(userId); + // Filter out null and empty identifier values + var validIdentifiers = new Dictionary(); + foreach (var kvp in identifiers) + { + if (!string.IsNullOrEmpty(kvp.Value)) + { + validIdentifiers[kvp.Key] = kvp.Value; + } + } + + // Only send identify event when 2+ valid identifiers exist + if (validIdentifiers.Count < 2) + { + _logger.Log(LogLevel.DEBUG, + "ODP identify event not dispatched (fewer than 2 valid identifiers)."); + return; + } + + EventManager.IdentifyUser(validIdentifiers); } /// diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index ed4f7469..40efdcba 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -1499,8 +1499,8 @@ List segmentOptions /// /// Send identification event to Optimizely Data Platform /// - /// FS User ID to send - internal void IdentifyUser(string userId) + /// Dictionary of identifier key-value pairs + internal void IdentifyUser(Dictionary identifiers) { var config = ProjectConfigManager?.GetConfig(); @@ -1510,7 +1510,7 @@ internal void IdentifyUser(string userId) return; } - OdpManager?.IdentifyUser(userId); + OdpManager?.IdentifyUser(identifiers); } /// diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index f7b37dac..e1134bd7 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -91,7 +91,10 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, #if USE_ODP if (shouldIdentifyUser) { - optimizely.IdentifyUser(UserId); + optimizely.IdentifyUser(new Dictionary + { + { Odp.Constants.FS_USER_ID, UserId }, + }); } #endif }