10000 [FSSDK-9076] fix: send odp event empty values by mnoman09 · Pull Request #513 · optimizely/java-sdk · GitHub
[go: up one dir, main page]

Skip to content

[FSSDK-9076] fix: send odp event empty values #513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,11 @@ public ODPManager getODPManager() {
*/
public void sendODPEvent(@Nullable String type, @Nonnull String action, @Nullable Map<String, String> identifiers, @Nullable Map<String, Object> data) {
if (odpManager != null) {
if (action == null || action.trim().isEmpty()) {
logger.error("ODP action is not valid (cannot be empty).");
return;
}

ODPEvent event = new ODPEvent(type, action, identifiers, data);
odpManager.getEventManager().sendEvent(event);
} else {
Expand Down
2 changes: 1 addition & 1 deletion core-api/src/main/java/com/optimizely/ab/odp/ODPEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ODPEvent {
@Nonnull private Map<String, Object> data;

public ODPEvent(@Nullable String type, @Nonnull String action, @Nullable Map<String, String> identifiers, @Nullable Map<String, Object> data) {
this.type = type == null ? EVENT_TYPE_FULLSTACK : type;
this.type = type == null || type.trim().isEmpty() ? EVENT_TYPE_FULLSTACK : type;
this.action = action;
this.identifiers = identifiers != null ? identifiers : Collections.emptyMap();
this.data = data != null ? data : Collections.emptyMap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public class ODPEventManager {
private static final int DEFAULT_FLUSH_INTERVAL = 1000;
private static final int MAX_RETRIES = 3;
private static final String EVENT_URL_PATH = "/v3/events";
private static final List<String> FS_USER_ID_MATCHES = new ArrayList<>(Arrays.asList(
ODPUserKey.FS_USER_ID.getKeyString(),
ODPUserKey.FS_USER_ID_ALIAS.getKeyString()
));
private static final Object SHUTDOWN_SIGNAL = new Object();

private final int queueSize;
Expand Down Expand Up @@ -121,7 +125,7 @@ public void identifyUser(@Nullable String vuid, @Nullable String userId) {

public void sendEvent(ODPEvent event) {
event.setData(augmentCommonData(event.getData()));
event.setIdentifiers(augmentCommonIdentifiers(event.getIdentifiers()));
event.setIdentifiers(convertCriticalIdentifiers(augmentCommonIdentifiers(event.getIdentifiers())));

if (!event.isIdentifiersValid()) {
logger.error("ODP event send failed (event identifiers must have at least one key-value pair)");
Expand All @@ -133,6 +137,7 @@ public void sendEvent(ODPEvent event) {
return;
}


processEvent(event);
}

Expand All @@ -158,6 +163,27 @@ protected Map<String, String> augmentCommonIdentifiers(Map<String, String> sourc
Map<String, String> identifiers = new HashMap<>();
identifiers.putAll(userCommonIdentifiers);
identifiers.putAll(sourceIdentifiers);

return identifiers;
}

private static Map<String, String> convertCriticalIdentifiers(Map<String, String> identifiers) {

if (identifiers.containsKey(ODPUserKey.FS_USER_ID.getKeyString())) {
return identifiers;
}

List<Map.Entry<String, String>> identifiersList = new ArrayList<>(identifiers.entrySet());

for (Map.Entry<String, String> kvp : identifiersList) {

if (FS_USER_ID_MATCHES.contains(kvp.getKey().toLowerCase())) {
identifiers.remove(kvp.getKey());
identifiers.put(ODPUserKey.FS_USER_ID.getKeyString(), kvp.getValue());
break;
}
}

return identifiers;
}

Expand Down
6 changes: 4 additions & 2 deletions core-api/src/main/java/com/optimizely/ab/odp/ODPUserKey.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
*
* Copyright 2022, Optimizely
* Copyright 2022-2023, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,7 +20,9 @@ public enum ODPUserKey {

VUID("vuid"),

FS_USER_ID("fs_user_id");
FS_USER_ID("fs_user_id"),

FS_USER_ID_ALIAS("fs-user-id");

private final String keyString;

Expand Down
122 changes: 122 additions & 0 deletions core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4762,6 +4762,128 @@ public void sendODPEvent() {
assertEquals(data, eventArgument.getValue().getData());
}

@Test
@SuppressFBWarnings(value = "NP_NONNULL_PARAM_VIOLATION", justification = "Testing nullness contract violation")
public void sendODPEventErrorNullAction() {
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfi D7AE g);
ODPEventManager mockODPEventManager = mock(ODPEventManager.class);
ODPManager mockODPManager = mock(ODPManager.class);

Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager);
Optimizely optimizely = Optimizely.builder()
.withConfigManager(mockProjectConfigManager)
.withODPManager(mockODPManager)
.build();

verify(mockODPEventManager).start();

Map<String, String> identifiers = new HashMap<>();
identifiers.put("id1", "value1");
identifiers.put("id2", "value2");

Map<String, Object> data = new HashMap<>();
data.put("sdk", "java");
data.put("revision", 52);

optimizely.sendODPEvent("fullstack", null, identifiers, data);
logbackVerifier.expectMessage(Level.ERROR, "ODP action is not valid (cannot be empty).");
}

@Test
public void sendODPEventErrorEmptyAction() {
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfig);
ODPEventManager mockODPEventManager = mock(ODPEventManager.class);
ODPManager mockODPManager = mock(ODPManager.class);

Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager);
Optimizely optimizely = Optimizely.builder()
.withConfigManager(mockProjectConfigManager)
.withODPManager(mockODPManager)
.build();

verify(mockODPEventManager).start();

Map<String, String> identifiers = new HashMap<>();
identifiers.put("id1", "value1");
identifiers.put("id2", "value2");

Map<String, Object> data = new HashMap<>();
data.put("sdk", "java");
data.put("revision", 52);

optimizely.sendODPEvent("fullstack", "", identifiers, data);
logbackVerifier.expectMessage(Level.ERROR, "ODP action is not valid (cannot be empty).");
}

@Test
@SuppressFBWarnings(value = "NP_NONNULL_PARAM_VIOLATION", justification = "Testing nullness contract violation")
public void sendODPEventNullType() {
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfig);
ODPEventManager mockODPEventManager = mock(ODPEventManager.class);
ODPManager mockODPManager = mock(ODPManager.class);

Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager);
Optimizely optimizely = Optimizely.builder()
.withConfigManager(mockProjectConfigManager)
.withODPManager(mockODPManager)
.build();

verify(mockODPEventManager).start();

Map<String, String> identifiers = new HashMap<>();
identifiers.put("id1", "value1");
identifiers.put("id2", "value2");

Map<String, Object> data = new HashMap<>();
data.put("sdk", "java");
data.put("revision", 52);

optimizely.sendODPEvent(null, "identify", identifiers, data);
ArgumentCaptor<ODPEvent> eventArgument = ArgumentCaptor.forClass(ODPEvent.class);
verify(mockODPEventManager).sendEvent(eventArgument.capture());

assertEquals("fullstack", eventArgument.getValue().getType());
assertEquals("identify", eventArgument.getValue().getAction());
assertEquals(identifiers, eventArgument.getValue().getIdentifiers());
assertEquals(data, eventArgument.getValue().getData());
}

@Test
public void sendODPEventEmptyType() {
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(validProjectConfig);
ODPEventManager mockODPEventManager = mock(ODPEventManager.class);
ODPManager mockODPManager = mock(ODPManager.class);

Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager);
Optimizely optimizely = Optimizely.builder()
.withConfigManager(mockProjectConfigManager)
.withODPManager(mockODPManager)
.build();

verify(mockODPEventManager).start();

Map<String, String> identifiers = new HashMap<>();
identifiers.put("id1", "value1");
identifiers.put("id2", "value2");

Map<String, Object> data = new HashMap<>();
data.put("sdk", "java");
data.put("revision", 52);

optimizely.sendODPEvent("", "identify", identifiers, data);
ArgumentCaptor<ODPEvent> eventArgument = ArgumentCaptor.forClass(ODPEvent.class);
verify(mockODPEventManager).sendEvent(eventArgument.capture());

assertEquals("fullstack", eventArgument.getValue().getType());
assertEquals("identify", eventArgument.getValue().getAction());
assertEquals(identifiers, eventArgument.getValue().getIdentifiers());
assertEquals(data, eventArgument.getValue().getData());
}

@Test
public void sendODPEventError() {
ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,73 @@ public void prepareCorrectPayloadForIdentifyUser() throws InterruptedException {
}
}

@Test
public void preparePayloadForIdentifyUserWithVariationsOfFsUserId() throws InterruptedException {
Mockito.reset(mockApiManager);
Mockito.when(mockApiManager.sendEvents(any(), any(), any())).thenReturn(202);
int flushInterval = 1;
ODPConfig odpConfig = new ODPConfig("key", "http://www.odp-host.com", null);
ODPEventManager eventManager = new ODPEventManager(mockApiManager, null, flushInterval);
eventManager.updateSettings(odpConfig);
eventManager.start();
ODPEvent event1 = new ODPEvent("fullstack",
"identified",
new HashMap<String, String>() {{
put("fs-user-id", "123");
}}, null);

ODPEvent event2 = new ODPEvent("fullstack",
"identified",
new HashMap<String, String>() {{
put("FS-user-ID", "123");
}}, null);

ODPEvent event3 = new ODPEvent("fullstack",
"identified",
new HashMap<String, String>() {{
put("FS_USER_ID", "123");
put("fs.user.id", "456");
}}, null);

ODPEvent event4 = new ODPEvent("fullstack",
"identified",
new HashMap<String, String>() {{
put("fs_user_id", "123");
put("fsuserid", "456");
}}, null);
List<Map<String, String>> expectedIdentifiers = new ArrayList<Map<String, String>>() {{
add(new HashMap<String, String>() {{
put("fs_user_id", "123");
}});
add(new HashMap<String, String>() {{
put("fs_user_id", "123");
}});
add(new HashMap<String, String>() {{
put("fs_user_id", "123");
put("fs.user.id", "456");
}});
add(new HashMap<String, String>() {{
put("fs_user_id", "123");
put("fsuserid", "456");
}});
}};
eventManager.sendEvent(event1);
eventManager.sendEvent(event2);
eventManager.sendEvent(event3);
eventManager.sendEvent(event4);

Thread.sleep(1500);
Mockito.verify(mockApiManager, times(1)).sendEvents(eq("key"), eq("http://www.odp-host.com/v3/events"), payloadCaptor.capture());

String payload = payloadCaptor.getValue();
JSONArray events = new JSONArray(payload);
assertEquals(4, events.length());
for (int i = 0; i < events.length(); i++) {
JSONObject event = events.getJSONObject(i);
assertEquals(event.getJSONObject("identifiers").toMap(), expectedIdentifiers.get(i));
}
}

@Test
public void identifyUserWithVuidAndUserId() throws InterruptedException {
ODPEventManager eventManager = spy(new ODPEventManager(mockApiManager));
Expand Down
0