From 6a910bdd233d32bc23e4102a1d612d401894f34b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Jul 2025 20:43:12 +0000 Subject: [PATCH 1/4] Add support for userId in DevCycleUser with priority and type checks Co-authored-by: jonathan --- .../sdk/server/common/model/DevCycleUser.java | 13 ++- .../server/common/model/DevCycleUserTest.java | 106 ++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java b/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java index 955417a4..76132d3e 100755 --- a/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java @@ -139,15 +139,21 @@ static void setCustomValue(Map customData, String key, Value val * * @param ctx A context to load a targeting key and user data from * @return An initialized DevCycleUser with data from the context - * @throws TargetingKeyMissingError if the targeting key or user_id attribute is not set + * @throws TargetingKeyMissingError if the targeting key, user_id, or userId attribute is not set */ public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) { String userId = ""; + String userIdSource = null; // Track which field was used for user ID if (ctx != null && ctx.getTargetingKey() != null && !ctx.getTargetingKey().isEmpty()) { userId = ctx.getTargetingKey(); - } else if (ctx != null && ctx.getValue("user_id") != null) { + userIdSource = "targetingKey"; + } else if (ctx != null && ctx.getValue("user_id") != null && ctx.getValue("user_id").isString()) { userId = ctx.getValue("user_id").asString(); + userIdSource = "user_id"; + } else if (ctx != null && ctx.getValue("userId") != null && ctx.getValue("userId").isString()) { + userId = ctx.getValue("userId").asString(); + userIdSource = "userId"; } if (userId == null || userId.isEmpty()) { @@ -160,7 +166,8 @@ public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) { Map privateCustomData = new LinkedHashMap<>(); for (String key : ctx.keySet()) { - if (key.equals("user_id") || key.equals("targetingKey")) { + if (key.equals("user_id") || key.equals("targetingKey") || + (key.equals("userId") && "userId".equals(userIdSource))) { continue; } diff --git a/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java b/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java index 99a4d550..acd8d54d 100644 --- a/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java +++ b/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java @@ -70,6 +70,112 @@ public void testCreateUserOnlyUserId() { Assert.assertEquals(user.getUserId(), "user-4567"); } + @Test + public void testCreateUserWithUserId() { + Map apiAttrs = new LinkedHashMap(); + apiAttrs.put("userId", new Value("test-userId-123")); + + // ensure fallback to userId when target key and user_id are null + EvaluationContext ctx = new MutableContext(null, apiAttrs); + DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "test-userId-123"); + + // ensure fallback to userId when target key and user_id are empty + ctx = new MutableContext("", apiAttrs); + user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "test-userId-123"); + } + + @Test + public void testUserIdPriorityOrder() { + Map apiAttrs = new LinkedHashMap(); + apiAttrs.put("user_id", new Value("user_id_value")); + apiAttrs.put("userId", new Value("userId_value")); + + // Test priority: targetingKey > user_id > userId + // When all three are present, targetingKey should win + EvaluationContext ctx = new MutableContext("targetingKey_value", apiAttrs); + DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "targetingKey_value"); + + // When targetingKey is null, user_id should win over userId + ctx = new MutableContext(null, apiAttrs); + user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "user_id_value"); + + // When targetingKey is empty, user_id should win over userId + ctx = new MutableContext("", apiAttrs); + user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "user_id_value"); + + // When only userId is present, it should be used + Map userIdOnlyAttrs = new LinkedHashMap(); + userIdOnlyAttrs.put("userId", new Value("userId_only_value")); + ctx = new MutableContext(null, userIdOnlyAttrs); + user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "userId_only_value"); + } + + @Test + public void testUserIdExcludedFromCustomData() { + Map apiAttrs = new LinkedHashMap(); + apiAttrs.put("userId", new Value("test-userId-123")); + apiAttrs.put("customField", new Value("customValue")); + + // When userId is used as the user ID, it should be excluded from custom data + EvaluationContext ctx = new MutableContext(null, apiAttrs); + DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx); + + Assert.assertEquals(user.getUserId(), "test-userId-123"); + Assert.assertNotNull(user.getCustomData()); + Assert.assertEquals(user.getCustomData().size(), 1); + Assert.assertEquals(user.getCustomData().get("customField"), "customValue"); + Assert.assertFalse(user.getCustomData().containsKey("userId")); + } + + @Test + public void testUserIdInCustomDataWhenNotUsedAsUserId() { + Map apiAttrs = new LinkedHashMap(); + apiAttrs.put("user_id", new Value("user_id_value")); + apiAttrs.put("userId", new Value("userId_value")); + apiAttrs.put("customField", new Value("customValue")); + + // When user_id takes precedence, userId should be included in custom data + EvaluationContext ctx = new MutableContext(null, apiAttrs); + DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx); + + Assert.assertEquals(user.getUserId(), "user_id_value"); + Assert.assertNotNull(user.getCustomData()); + Assert.assertEquals(user.getCustomData().size(), 2); + Assert.assertEquals(user.getCustomData().get("customField"), "customValue"); + Assert.assertEquals(user.getCustomData().get("userId"), "userId_value"); + Assert.assertFalse(user.getCustomData().containsKey("user_id")); + } + + @Test + public void testInvalidUserIdTypes() { + Map apiAttrs = new LinkedHashMap(); + + // Test with non-string userId value - should be ignored + apiAttrs.put("userId", new Value(123)); + EvaluationContext ctx = new MutableContext(null, apiAttrs); + + try { + DevCycleUser.fromEvaluationContext(ctx); + Assert.fail("Expected TargetingKeyMissingError"); + } catch (TargetingKeyMissingError e) { + // expected + } + + // Test with non-string user_id value but valid userId string - should use userId + apiAttrs.put("user_id", new Value(456)); + apiAttrs.put("userId", new Value("valid-userId")); + ctx = new MutableContext(null, apiAttrs); + + DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx); + Assert.assertEquals(user.getUserId(), "valid-userId"); + } + @Test public void testCreateUserWithAttributes() { Map apiAttrs = new LinkedHashMap(); From ccde45dd5a780c3ac83e4e26adec4d1af8eb6bc2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Jul 2025 14:46:07 +0000 Subject: [PATCH 2/4] Remove user ID source tracking and exclude all user ID fields from custom data Co-authored-by: jonathan --- .../devcycle/sdk/server/common/model/DevCycleUser.java | 7 +------ .../sdk/server/common/model/DevCycleUserTest.java | 9 +++++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java b/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java index 76132d3e..1b421127 100755 --- a/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java @@ -143,17 +143,13 @@ static void setCustomValue(Map customData, String key, Value val */ public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) { String userId = ""; - String userIdSource = null; // Track which field was used for user ID if (ctx != null && ctx.getTargetingKey() != null && !ctx.getTargetingKey().isEmpty()) { userId = ctx.getTargetingKey(); - userIdSource = "targetingKey"; } else if (ctx != null && ctx.getValue("user_id") != null && ctx.getValue("user_id").isString()) { userId = ctx.getValue("user_id").asString(); - userIdSource = "user_id"; } else if (ctx != null && ctx.getValue("userId") != null && ctx.getValue("userId").isString()) { userId = ctx.getValue("userId").asString(); - userIdSource = "userId"; } if (userId == null || userId.isEmpty()) { @@ -166,8 +162,7 @@ public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) { Map privateCustomData = new LinkedHashMap<>(); for (String key : ctx.keySet()) { - if (key.equals("user_id") || key.equals("targetingKey") || - (key.equals("userId") && "userId".equals(userIdSource))) { + if (key.equals("user_id") || key.equals("targetingKey") || key.equals("userId")) { continue; } diff --git a/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java b/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java index acd8d54d..1b57312e 100644 --- a/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java +++ b/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java @@ -134,22 +134,23 @@ public void testUserIdExcludedFromCustomData() { } @Test - public void testUserIdInCustomDataWhenNotUsedAsUserId() { + public void testAllUserIdFieldsExcludedFromCustomData() { Map apiAttrs = new LinkedHashMap(); apiAttrs.put("user_id", new Value("user_id_value")); apiAttrs.put("userId", new Value("userId_value")); apiAttrs.put("customField", new Value("customValue")); - // When user_id takes precedence, userId should be included in custom data + // All user ID fields should be excluded from custom data regardless of which is used EvaluationContext ctx = new MutableContext(null, apiAttrs); DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx); Assert.assertEquals(user.getUserId(), "user_id_value"); Assert.assertNotNull(user.getCustomData()); - Assert.assertEquals(user.getCustomData().size(), 2); + Assert.assertEquals(user.getCustomData().size(), 1); Assert.assertEquals(user.getCustomData().get("customField"), "customValue"); - Assert.assertEquals(user.getCustomData().get("userId"), "userId_value"); + Assert.assertFalse(user.getCustomData().containsKey("userId")); Assert.assertFalse(user.getCustomData().containsKey("user_id")); + Assert.assertFalse(user.getCustomData().containsKey("targetingKey")); } @Test From 8a0e624223d345af931a9bc9ffdb5b4479710359 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Jul 2025 14:47:15 +0000 Subject: [PATCH 3/4] Rename DevCycleUserTest methods to clarify evaluation context origin Co-authored-by: jonathan --- .../sdk/server/common/model/DevCycleUserTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java b/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java index 1b57312e..1aa68ee1 100644 --- a/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java +++ b/src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java @@ -71,7 +71,7 @@ public void testCreateUserOnlyUserId() { } @Test - public void testCreateUserWithUserId() { + public void testFromEvaluationContextWithUserId() { Map apiAttrs = new LinkedHashMap(); apiAttrs.put("userId", new Value("test-userId-123")); @@ -87,7 +87,7 @@ public void testCreateUserWithUserId() { } @Test - public void testUserIdPriorityOrder() { + public void testFromEvaluationContextUserIdPriorityOrder() { Map apiAttrs = new LinkedHashMap(); apiAttrs.put("user_id", new Value("user_id_value")); apiAttrs.put("userId", new Value("userId_value")); @@ -117,7 +117,7 @@ public void testUserIdPriorityOrder() { } @Test - public void testUserIdExcludedFromCustomData() { + public void testFromEvaluationContextUserIdExcludedFromCustomData() { Map apiAttrs = new LinkedHashMap(); apiAttrs.put("userId", new Value("test-userId-123")); apiAttrs.put("customField", new Value("customValue")); @@ -134,7 +134,7 @@ public void testUserIdExcludedFromCustomData() { } @Test - public void testAllUserIdFieldsExcludedFromCustomData() { + public void testFromEvaluationContextAllUserIdFieldsExcludedFromCustomData() { Map apiAttrs = new LinkedHashMap(); apiAttrs.put("user_id", new Value("user_id_value")); apiAttrs.put("userId", new Value("userId_value")); @@ -154,7 +154,7 @@ public void testAllUserIdFieldsExcludedFromCustomData() { } @Test - public void testInvalidUserIdTypes() { + public void testFromEvaluationContextInvalidUserIdTypes() { Map apiAttrs = new LinkedHashMap(); // Test with non-string userId value - should be ignored From 79d35efc8f048a0017f492bbcf51aeb330f3b071 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 22 Jul 2025 11:04:52 -0400 Subject: [PATCH 4/4] Update src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/devcycle/sdk/server/common/model/DevCycleUser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java b/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java index 1b421127..24206a66 100755 --- a/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java +++ b/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java @@ -139,7 +139,7 @@ static void setCustomValue(Map customData, String key, Value val * * @param ctx A context to load a targeting key and user data from * @return An initialized DevCycleUser with data from the context - * @throws TargetingKeyMissingError if the targeting key, user_id, or userId attribute is not set + * @throws TargetingKeyMissingError if none of the targeting key, user_id, or userId attributes are set or valid */ public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) { String userId = "";