From 48097c0f7f2ad1a2710ec72e9d74ae2a44ceb930 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 26 May 2026 11:57:45 -0700 Subject: [PATCH 1/5] [FSSDK-12670] Block ODP identify events with single identifier Add count check in IdentifyUser to filter out empty identifier values and require 2+ valid identifiers before dispatching. Server-side C# SDK only has fs_user_id (no VUID), so identify events will be correctly skipped with a debug log message. --- .../OdpTests/OdpEventManagerTests.cs | 36 +++++++++---------- OptimizelySDK/Odp/OdpEventManager.cs | 14 +++++++- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs index cc606667..7d4569c0 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 (only one identifier provided)."), + 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 (only one identifier provided)."), + Times.Once); } [Test] diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index b880daa9..73109ce9 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -378,7 +378,19 @@ 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); + + if (validIdentifiers.Count < 2) + { + _logger.Log(LogLevel.DEBUG, + "ODP identify event is not dispatched (only one identifier provided)."); + return; + } + + var odpEvent = new OdpEvent(Constants.ODP_EVENT_TYPE, "identified", validIdentifiers); SendEvent(odpEvent); } From 9324adba2bfff93ed772fe8b49502a452a192560 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 09:39:05 -0700 Subject: [PATCH 2/5] [FSSDK-12670] Address review feedback: fix log message --- OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs | 4 ++-- OptimizelySDK/Odp/OdpEventManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs index 7d4569c0..5f5cf296 100644 --- a/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs +++ b/OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs @@ -210,7 +210,7 @@ public void ShouldLogWhenOdpNotIntegratedAndIdentifyUserCalled() Times.Exactly(2)); // during Start() and UpdateSettings() _mockLogger.Verify( l => l.Log(LogLevel.DEBUG, - "ODP identify event is not dispatched (only one identifier provided)."), + "ODP identify event is not dispatched (fewer than 2 valid identifiers)."), Times.Once); // IdentifyUser blocked before reaching SendEvent } @@ -633,7 +633,7 @@ public void ShouldNotSendIdentifyEventWithSingleIdentifier() Assert.IsEmpty(eventsCollector); _mockLogger.Verify( l => l.Log(LogLevel.DEBUG, - "ODP identify event is not dispatched (only one identifier provided)."), + "ODP identify event is not dispatched (fewer than 2 valid identifiers)."), Times.Once); } diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index 73109ce9..cb7d5d68 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -386,7 +386,7 @@ public void IdentifyUser(string userId) if (validIdentifiers.Count < 2) { _logger.Log(LogLevel.DEBUG, - "ODP identify event is not dispatched (only one identifier provided)."); + "ODP identify event is not dispatched (fewer than 2 valid identifiers)."); return; } From be8686a1df3ce75c1883aaba3a7523338a9e6608 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 10:00:17 -0700 Subject: [PATCH 3/5] [FSSDK-12670] Retrigger CI From 8149c82dbb6175316d464bf76ada3cb0c7940a79 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 13:49:09 -0700 Subject: [PATCH 4/5] [FSSDK-12670] Add comment explaining the < 2 identifiers guard Co-Authored-By: Claude Opus 4.6 --- OptimizelySDK/Odp/OdpEventManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OptimizelySDK/Odp/OdpEventManager.cs b/OptimizelySDK/Odp/OdpEventManager.cs index cb7d5d68..7856170d 100644 --- a/OptimizelySDK/Odp/OdpEventManager.cs +++ b/OptimizelySDK/Odp/OdpEventManager.cs @@ -383,6 +383,8 @@ public void IdentifyUser(string userId) .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, From fe0e47bffefe8db33278b110cf5a2daf99853365 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 16:38:14 -0700 Subject: [PATCH 5/5] [FSSDK-12670] retrigger CI