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..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 @@ -111,10 +111,18 @@ 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) { @@ -127,7 +135,30 @@ public void identifyUser(@Nullable String vuid, @Nullable String userId) { identifiers.put(ODPUserKey.FS_USER_ID.getKeyString(), userId); } } - ODPEvent event = new ODPEvent("fullstack", "identified", identifiers, null); + identifyUser(identifiers); + } + + 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()) { + validIdentifiers.put(entry.getKey(), entry.getValue()); + } + } + + // 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; + } + + 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..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 @@ -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(2, eventIdentifiers.size()); + assertEquals("vuid_123", eventIdentifiers.get("vuid")); + assertEquals("test-user", eventIdentifiers.get("fs_user_id")); } @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 (fewer than 2 valid identifiers)."); + } - 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 (fewer than 2 valid identifiers)."); } @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 (fewer than 2 valid identifiers)."); + } - 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 (fewer than 2 valid identifiers)."); } @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());