From 97dcc38b9fcc1fd04995465fecc0b37f9bfc41c2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 26 May 2026 10:39:57 -0700 Subject: [PATCH 1/5] [AI-FSSDK] [FSSDK-12670] Block ODP identify event for single identifier Skip sending ODP identify event when fewer than 2 valid (non-null, non-empty) identifiers exist. Changed ODPEventManager.identifyUser to accept a Map of identifiers and filter out null/empty values before checking count. Updated Optimizely.identifyUser to construct identifier map from userId string and pass to ODPEventManager. Updated all related tests to use Map-based API. --- .../java/com/optimizely/ab/Optimizely.java | 9 +- .../optimizely/ab/odp/ODPEventManager.java | 28 +++--- .../com/optimizely/ab/OptimizelyTest.java | 5 +- .../ab/OptimizelyUserContextTest.java | 11 ++- .../ab/odp/ODPEventManagerTest.java | 88 +++++++++++++------ .../com/optimizely/ab/odp/ODPManagerTest.java | 9 +- 6 files changed, 98 insertions(+), 52 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index f69b018c8..d2db01c90 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -57,6 +57,7 @@ import com.optimizely.ab.odp.ODPManager; import com.optimizely.ab.odp.ODPSegmentManager; import com.optimizely.ab.odp.ODPSegmentOption; +import com.optimizely.ab.odp.ODPUserKey; import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; @@ -1794,7 +1795,13 @@ public void identifyUser(@Nonnull String userId) { } ODPManager odpManager = getODPManager(); if (odpManager != null) { - odpManager.getEventManager().identifyUser(userId); + Map identifiers = new HashMap<>(); + if (ODPManager.isVuid(userId)) { + identifiers.put(ODPUserKey.VUID.getKeyString(), userId); + } else { + identifiers.put(ODPUserKey.FS_USER_ID.getKeyString(), userId); + } + odpManager.getEventManager().identifyUser(identifiers); } } diff --git a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java index 43727b501..7fda57b07 100644 --- a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java +++ b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java @@ -111,23 +111,21 @@ public void updateSettings(ODPConfig newConfig) { } } - public void identifyUser(String userId) { - identifyUser(null, userId); - } - - public void identifyUser(@Nullable String vuid, @Nullable String userId) { - Map identifiers = new HashMap<>(); - if (vuid != null) { - identifiers.put(ODPUserKey.VUID.getKeyString(), vuid); - } - if (userId != null) { - if (ODPManager.isVuid(userId)) { - identifiers.put(ODPUserKey.VUID.getKeyString(), userId); - } else { - identifiers.put(ODPUserKey.FS_USER_ID.getKeyString(), userId); + public void identifyUser(@Nonnull Map identifiers) { + // Filter out identifiers with null or empty values + Map validIdentifiers = new HashMap<>(); + for (Map.Entry entry : identifiers.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + validIdentifiers.put(entry.getKey(), entry.getValue()); } } - ODPEvent event = new ODPEvent("fullstack", "identified", identifiers, null); + + if (validIdentifiers.size() < 2) { + logger.debug("ODP identify event is not dispatched (only one identifier provided)."); + return; + } + + ODPEvent event = new ODPEvent("fullstack", "identified", validIdentifiers, null); sendEvent(event); } diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index db707a7dc..15c3eea5f 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -5084,7 +5084,10 @@ public void identifyUser() { .build(); optimizely.identifyUser("the-user"); - Mockito.verify(mockODPEventManager, times(1)).identifyUser("the-user"); + ArgumentCaptor identifiersCaptor = ArgumentCaptor.forClass(Map.class); + Mockito.verify(mockODPEventManager, times(1)).identifyUser(identifiersCaptor.capture()); + Map capturedIdentifiers = identifiersCaptor.getValue(); + assertEquals("the-user", capturedIdentifiers.get("fs_user_id")); } @Test diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 0a1829297..2e479d2ea 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -1970,7 +1970,7 @@ public void identifyUserErrorWhenConfigIsInvalid() { .build(); optimizely.createUserContext("test-user"); - verify(mockODPEventManager, never()).identifyUser("test-user"); + verify(mockODPEventManager, never()).identifyUser(any(Map.class)); Mockito.reset(mockODPEventManager); logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing identifyUser call."); @@ -1992,13 +1992,16 @@ public void identifyUser() { .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - verify(mockODPEventManager).identifyUser("test-user"); + ArgumentCaptor identifiersCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockODPEventManager).identifyUser(identifiersCaptor.capture()); + Map capturedIdentifiers = identifiersCaptor.getValue(); + assertEquals("test-user", capturedIdentifiers.get("fs_user_id")); Mockito.reset(mockODPEventManager); OptimizelyUserContext userContextClone = userContext.copy(); - // identifyUser should not be called the new userContext is created through copy - verify(mockODPEventManager, never()).identifyUser("test-user"); + // identifyUser should not be called when the new userContext is created through copy + verify(mockODPEventManager, never()).identifyUser(any(Map.class)); assertNotSame(userContextClone, userContext); } diff --git a/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java b/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java index 0ade4652f..8f6a6aee5 100644 --- a/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java @@ -203,7 +203,10 @@ public void prepareCorrectPayloadForIdentifyUser() throws InterruptedException { eventManager.updateSettings(odpConfig); eventManager.start(); for (int i = 0; i < 2; i++) { - eventManager.identifyUser("the-vuid-" + i, "the-fs-user-id-" + i); + Map identifiers = new HashMap<>(); + identifiers.put("vuid", "the-vuid-" + i); + identifiers.put("fs_user_id", "the-fs-user-id-" + i); + eventManager.identifyUser(identifiers); } Thread.sleep(1500); @@ -290,61 +293,88 @@ public void preparePayloadForIdentifyUserWithVariationsOfFsUserId() throws Inter } @Test - public void identifyUserWithVuidAndUserId() throws InterruptedException { + public void identifyUserWithMultipleIdentifiers() throws InterruptedException { ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager)); ArgumentCaptor captor = ArgumentCaptor.forClass(ODPEvent.class); - eventManager.identifyUser("vuid_123", "test-user"); + Map identifiers = new HashMap<>(); + identifiers.put("vuid", "vuid_123"); + identifiers.put("fs_user_id", "test-user"); + eventManager.identifyUser(identifiers); verify(eventManager, times(1)).sendEvent(captor.capture()); ODPEvent event = captor.getValue(); - Map identifiers = event.getIdentifiers(); - assertEquals(identifiers.size(), 2); - assertEquals(identifiers.get("vuid"), "vuid_123"); - assertEquals(identifiers.get("fs_user_id"), "test-user"); + Map eventIdentifiers = event.getIdentifiers(); + assertEquals(eventIdentifiers.size(), 2); + assertEquals(eventIdentifiers.get("vuid"), "vuid_123"); + assertEquals(eventIdentifiers.get("fs_user_id"), "test-user"); } @Test - public void identifyUserWithVuidOnly() throws InterruptedException { + public void identifyUserSkippedWithSingleIdentifier() throws InterruptedException { ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager)); - ArgumentCaptor captor = ArgumentCaptor.forClass(ODPEvent.class); - eventManager.identifyUser("vuid_123", null); - verify(eventManager, times(1)).sendEvent(captor.capture()); + Map identifiers = new HashMap<>(); + identifiers.put("fs_user_id", "test-user"); + eventManager.identifyUser(identifiers); + verify(eventManager, never()).sendEvent(any(ODPEvent.class)); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); + } - ODPEvent event = captor.getValue(); - Map identifiers = event.getIdentifiers(); - assertEquals(identifiers.size(), 1); - assertEquals(identifiers.get("vuid"), "vuid_123"); + @Test + public void identifyUserSkippedWithEmptyValues() throws InterruptedException { + ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager)); + + // Two keys but one has empty value - only 1 valid identifier + Map identifiers = new HashMap<>(); + identifiers.put("fs_user_id", "test-user"); + identifiers.put("email", ""); + eventManager.identifyUser(identifiers); + verify(eventManager, never()).sendEvent(any(ODPEvent.class)); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); } @Test - public void identifyUserWithUserIdOnly() throws InterruptedException { + public void identifyUserSkippedWithNullValues() throws InterruptedException { ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager)); - ArgumentCaptor captor = ArgumentCaptor.forClass(ODPEvent.class); - eventManager.identifyUser(null, "test-user"); - verify(eventManager, times(1)).sendEvent(captor.capture()); + // Two keys but one has null value - only 1 valid identifier + Map identifiers = new HashMap<>(); + identifiers.put("fs_user_id", "test-user"); + identifiers.put("vuid", null); + eventManager.identifyUser(identifiers); + verify(eventManager, never()).sendEvent(any(ODPEvent.class)); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); + } - ODPEvent event = captor.getValue(); - Map identifiers = event.getIdentifiers(); - assertEquals(identifiers.size(), 1); - assertEquals(identifiers.get("fs_user_id"), "test-user"); + @Test + public void identifyUserSkippedWithEmptyMap() throws InterruptedException { + ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager)); + + Map identifiers = new HashMap<>(); + eventManager.identifyUser(identifiers); + verify(eventManager, never()).sendEvent(any(ODPEvent.class)); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); } @Test - public void identifyUserWithVuidAsUserId() throws InterruptedException { + public void identifyUserSendsWithThreeIdentifiers() throws InterruptedException { ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager)); ArgumentCaptor captor = ArgumentCaptor.forClass(ODPEvent.class); - eventManager.identifyUser(null, "vuid_123"); + Map identifiers = new HashMap<>(); + identifiers.put("vuid", "vuid_123"); + identifiers.put("fs_user_id", "test-user"); + identifiers.put("email", "test@example.com"); + eventManager.identifyUser(identifiers); verify(eventManager, times(1)).sendEvent(captor.capture()); ODPEvent event = captor.getValue(); - Map identifiers = event.getIdentifiers(); - assertEquals(identifiers.size(), 1); - // SDK will convert userId to vuid when userId has a valid vuid format. - assertEquals(identifiers.get("vuid"), "vuid_123"); + Map eventIdentifiers = event.getIdentifiers(); + assertEquals(3, eventIdentifiers.size()); + assertEquals("vuid_123", eventIdentifiers.get("vuid")); + assertEquals("test-user", eventIdentifiers.get("fs_user_id")); + assertEquals("test@example.com", eventIdentifiers.get("email")); } @Test diff --git a/core-api/src/test/java/com/optimizely/ab/odp/ODPManagerTest.java b/core-api/src/test/java/com/optimizely/ab/odp/ODPManagerTest.java index 1e1f59f29..74cf5792f 100644 --- a/core-api/src/test/java/com/optimizely/ab/odp/ODPManagerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/odp/ODPManagerTest.java @@ -23,8 +23,10 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; @@ -75,13 +77,16 @@ public void shouldUseNewSettingsInEventManagerWhenODPConfigIsUpdated() throws In ODPManager odpManager = ODPManager.builder().withApiManager(mockApiManager).build(); odpManager.updateSettings("test-host", "test-key", new HashSet<>(Arrays.asList("segment1", "segment2"))); - odpManager.getEventManager().identifyUser("vuid", "fsuid"); + Map identifiers = new HashMap<>(); + identifiers.put("vuid", "vuid_value"); + identifiers.put("fs_user_id", "fsuid"); + odpManager.getEventManager().identifyUser(identifiers); Thread.sleep(2000); verify(mockApiManager, times(1)) .sendEvents(eq("test-key"), eq("test-host/v3/events"), any()); odpManager.updateSettings("test-host-updated", "test-key-updated", new HashSet<>(Arrays.asList("segment1"))); - odpManager.getEventManager().identifyUser("vuid", "fsuid"); + odpManager.getEventManager().identifyUser(identifiers); Thread.sleep(1200); verify(mockApiManager, times(1)) .sendEvents(eq("test-key-updated"), eq("test-host-updated/v3/events"), any()); From c50684d5f2123e3017001bf57c24c663f4fd6d7f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 26 May 2026 10:56:50 -0700 Subject: [PATCH 2/5] [FSSDK-12670] Restore deprecated identifyUser overloads for backward compatibility Keep old identifyUser(String) and identifyUser(String, String) signatures as @Deprecated wrappers that delegate to the new identifyUser(Map) method. Co-Authored-By: Claude Opus 4.6 --- .../optimizely/ab/odp/ODPEventManager.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java index 7fda57b07..a3ed14a62 100644 --- a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java +++ b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java @@ -111,8 +111,34 @@ public void updateSettings(ODPConfig newConfig) { } } + /** + * @deprecated Use {@link #identifyUser(Map)} instead. + */ + @Deprecated + public void identifyUser(String userId) { + identifyUser(null, userId); + } + + /** + * @deprecated Use {@link #identifyUser(Map)} instead. + */ + @Deprecated + public void identifyUser(@Nullable String vuid, @Nullable String userId) { + Map identifiers = new HashMap<>(); + if (vuid != null) { + identifiers.put(ODPUserKey.VUID.getKeyString(), vuid); + } + if (userId != null) { + if (ODPManager.isVuid(userId)) { + identifiers.put(ODPUserKey.VUID.getKeyString(), userId); + } else { + identifiers.put(ODPUserKey.FS_USER_ID.getKeyString(), userId); + } + } + identifyUser(identifiers); + } + public void identifyUser(@Nonnull Map identifiers) { - // Filter out identifiers with null or empty values Map validIdentifiers = new HashMap<>(); for (Map.Entry entry : identifiers.entrySet()) { if (entry.getValue() != null && !entry.getValue().isEmpty()) { From af82dcf34d7dc03db69c23ab7bcc32dc6a495b63 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 09:39:08 -0700 Subject: [PATCH 3/5] [FSSDK-12670] Address review feedback: fix log message and null guard --- .../com/optimizely/ab/odp/ODPEventManager.java | 7 ++++++- .../com/optimizely/ab/odp/ODPEventManagerTest.java | 14 +++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java index a3ed14a62..6169eaf81 100644 --- a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java +++ b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java @@ -139,6 +139,11 @@ public void identifyUser(@Nullable String vuid, @Nullable String userId) { } public void identifyUser(@Nonnull Map identifiers) { + if (identifiers == null) { + logger.debug("ODP identify event is not dispatched (fewer than 2 valid identifiers)."); + return; + } + Map validIdentifiers = new HashMap<>(); for (Map.Entry entry : identifiers.entrySet()) { if (entry.getValue() != null && !entry.getValue().isEmpty()) { @@ -147,7 +152,7 @@ public void identifyUser(@Nonnull Map identifiers) { } if (validIdentifiers.size() < 2) { - logger.debug("ODP identify event is not dispatched (only one identifier provided)."); + logger.debug("ODP identify event is not dispatched (fewer than 2 valid identifiers)."); return; } diff --git a/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java b/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java index 8f6a6aee5..6c1a6fd9b 100644 --- a/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java @@ -305,9 +305,9 @@ public void identifyUserWithMultipleIdentifiers() throws InterruptedException { ODPEvent event = captor.getValue(); Map eventIdentifiers = event.getIdentifiers(); - assertEquals(eventIdentifiers.size(), 2); - assertEquals(eventIdentifiers.get("vuid"), "vuid_123"); - assertEquals(eventIdentifiers.get("fs_user_id"), "test-user"); + assertEquals(2, eventIdentifiers.size()); + assertEquals("vuid_123", eventIdentifiers.get("vuid")); + assertEquals("test-user", eventIdentifiers.get("fs_user_id")); } @Test @@ -318,7 +318,7 @@ public void identifyUserSkippedWithSingleIdentifier() throws InterruptedExceptio identifiers.put("fs_user_id", "test-user"); eventManager.identifyUser(identifiers); verify(eventManager, never()).sendEvent(any(ODPEvent.class)); - logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (fewer than 2 valid identifiers)."); } @Test @@ -331,7 +331,7 @@ public void identifyUserSkippedWithEmptyValues() throws InterruptedException { identifiers.put("email", ""); eventManager.identifyUser(identifiers); verify(eventManager, never()).sendEvent(any(ODPEvent.class)); - logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (fewer than 2 valid identifiers)."); } @Test @@ -344,7 +344,7 @@ public void identifyUserSkippedWithNullValues() throws InterruptedException { identifiers.put("vuid", null); eventManager.identifyUser(identifiers); verify(eventManager, never()).sendEvent(any(ODPEvent.class)); - logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (fewer than 2 valid identifiers)."); } @Test @@ -354,7 +354,7 @@ public void identifyUserSkippedWithEmptyMap() throws InterruptedException { Map identifiers = new HashMap<>(); eventManager.identifyUser(identifiers); verify(eventManager, never()).sendEvent(any(ODPEvent.class)); - logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (only one identifier provided)."); + logbackVerifier.expectMessage(Level.DEBUG, "ODP identify event is not dispatched (fewer than 2 valid identifiers)."); } @Test From 3158e18e47653ece67c5e3f5de880d31c44b6b5e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 10:00:36 -0700 Subject: [PATCH 4/5] [FSSDK-12670] Retrigger CI From 436cbe2e89215e1d0c00025fcf0e5a0233130c68 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 27 May 2026 13:49:02 -0700 Subject: [PATCH 5/5] [FSSDK-12670] Add comment explaining the < 2 identifiers guard Co-Authored-By: Claude Opus 4.6 --- .../src/main/java/com/optimizely/ab/odp/ODPEventManager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java index 6169eaf81..79e250219 100644 --- a/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java +++ b/core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java @@ -151,6 +151,8 @@ public void identifyUser(@Nonnull Map identifiers) { } } + // An identify event requires at least 2 identifiers to link (e.g., vuid + fs_user_id). + // A single identifier has no cross-reference value and would generate unnecessary traffic. if (validIdentifiers.size() < 2) { logger.debug("ODP identify event is not dispatched (fewer than 2 valid identifiers)."); return;