Skip to content

Commit 4fda848

Browse files
jonathannorriscursoragentCopilot
authored
feat: Update Java OpenFeature user ID mapping (#176)
* Add support for userId in DevCycleUser with priority and type checks Co-authored-by: jonathan <[email protected]> * Remove user ID source tracking and exclude all user ID fields from custom data Co-authored-by: jonathan <[email protected]> * Rename DevCycleUserTest methods to clarify evaluation context origin Co-authored-by: jonathan <[email protected]> * Update src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent b67c4be commit 4fda848

File tree

2 files changed

+112
-3
lines changed

2 files changed

+112
-3
lines changed

src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,17 @@ static void setCustomValue(Map<String, Object> customData, String key, Value val
139139
*
140140
* @param ctx A context to load a targeting key and user data from
141141
* @return An initialized DevCycleUser with data from the context
142-
* @throws TargetingKeyMissingError if the targeting key or user_id attribute is not set
142+
* @throws TargetingKeyMissingError if none of the targeting key, user_id, or userId attributes are set or valid
143143
*/
144144
public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) {
145145
String userId = "";
146146

147147
if (ctx != null && ctx.getTargetingKey() != null && !ctx.getTargetingKey().isEmpty()) {
148148
userId = ctx.getTargetingKey();
149-
} else if (ctx != null && ctx.getValue("user_id") != null) {
149+
} else if (ctx != null && ctx.getValue("user_id") != null && ctx.getValue("user_id").isString()) {
150150
userId = ctx.getValue("user_id").asString();
151+
} else if (ctx != null && ctx.getValue("userId") != null && ctx.getValue("userId").isString()) {
152+
userId = ctx.getValue("userId").asString();
151153
}
152154

153155
if (userId == null || userId.isEmpty()) {
@@ -160,7 +162,7 @@ public static DevCycleUser fromEvaluationContext(EvaluationContext ctx) {
160162
Map<String, Object> privateCustomData = new LinkedHashMap<>();
161163

162164
for (String key : ctx.keySet()) {
163-
if (key.equals("user_id") || key.equals("targetingKey")) {
165+
if (key.equals("user_id") || key.equals("targetingKey") || key.equals("userId")) {
164166
continue;
165167
}
166168

src/test/java/com/devcycle/sdk/server/common/model/DevCycleUserTest.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,113 @@ public void testCreateUserOnlyUserId() {
7070
Assert.assertEquals(user.getUserId(), "user-4567");
7171
}
7272

73+
@Test
74+
public void testFromEvaluationContextWithUserId() {
75+
Map<String, Value> apiAttrs = new LinkedHashMap();
76+
apiAttrs.put("userId", new Value("test-userId-123"));
77+
78+
// ensure fallback to userId when target key and user_id are null
79+
EvaluationContext ctx = new MutableContext(null, apiAttrs);
80+
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);
81+
Assert.assertEquals(user.getUserId(), "test-userId-123");
82+
83+
// ensure fallback to userId when target key and user_id are empty
84+
ctx = new MutableContext("", apiAttrs);
85+
user = DevCycleUser.fromEvaluationContext(ctx);
86+
Assert.assertEquals(user.getUserId(), "test-userId-123");
87+
}
88+
89+
@Test
90+
public void testFromEvaluationContextUserIdPriorityOrder() {
91+
Map<String, Value> apiAttrs = new LinkedHashMap();
92+
apiAttrs.put("user_id", new Value("user_id_value"));
93+
apiAttrs.put("userId", new Value("userId_value"));
94+
95+
// Test priority: targetingKey > user_id > userId
96+
// When all three are present, targetingKey should win
97+
EvaluationContext ctx = new MutableContext("targetingKey_value", apiAttrs);
98+
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);
99+
Assert.assertEquals(user.getUserId(), "targetingKey_value");
100+
101+
// When targetingKey is null, user_id should win over userId
102+
ctx = new MutableContext(null, apiAttrs);
103+
user = DevCycleUser.fromEvaluationContext(ctx);
104+
Assert.assertEquals(user.getUserId(), "user_id_value");
105+
106+
// When targetingKey is empty, user_id should win over userId
107+
ctx = new MutableContext("", apiAttrs);
108+
user = DevCycleUser.fromEvaluationContext(ctx);
109+
Assert.assertEquals(user.getUserId(), "user_id_value");
110+
111+
// When only userId is present, it should be used
112+
Map<String, Value> userIdOnlyAttrs = new LinkedHashMap();
113+
userIdOnlyAttrs.put("userId", new Value("userId_only_value"));
114+
ctx = new MutableContext(null, userIdOnlyAttrs);
115+
user = DevCycleUser.fromEvaluationContext(ctx);
116+
Assert.assertEquals(user.getUserId(), "userId_only_value");
117+
}
118+
119+
@Test
120+
public void testFromEvaluationContextUserIdExcludedFromCustomData() {
121+
Map<String, Value> apiAttrs = new LinkedHashMap();
122+
apiAttrs.put("userId", new Value("test-userId-123"));
123+
apiAttrs.put("customField", new Value("customValue"));
124+
125+
// When userId is used as the user ID, it should be excluded from custom data
126+
EvaluationContext ctx = new MutableContext(null, apiAttrs);
127+
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);
128+
129+
Assert.assertEquals(user.getUserId(), "test-userId-123");
130+
Assert.assertNotNull(user.getCustomData());
131+
Assert.assertEquals(user.getCustomData().size(), 1);
132+
Assert.assertEquals(user.getCustomData().get("customField"), "customValue");
133+
Assert.assertFalse(user.getCustomData().containsKey("userId"));
134+
}
135+
136+
@Test
137+
public void testFromEvaluationContextAllUserIdFieldsExcludedFromCustomData() {
138+
Map<String, Value> apiAttrs = new LinkedHashMap();
139+
apiAttrs.put("user_id", new Value("user_id_value"));
140+
apiAttrs.put("userId", new Value("userId_value"));
141+
apiAttrs.put("customField", new Value("customValue"));
142+
143+
// All user ID fields should be excluded from custom data regardless of which is used
144+
EvaluationContext ctx = new MutableContext(null, apiAttrs);
145+
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);
146+
147+
Assert.assertEquals(user.getUserId(), "user_id_value");
148+
Assert.assertNotNull(user.getCustomData());
149+
Assert.assertEquals(user.getCustomData().size(), 1);
150+
Assert.assertEquals(user.getCustomData().get("customField"), "customValue");
151+
Assert.assertFalse(user.getCustomData().containsKey("userId"));
152+
Assert.assertFalse(user.getCustomData().containsKey("user_id"));
153+
Assert.assertFalse(user.getCustomData().containsKey("targetingKey"));
154+
}
155+
156+
@Test
157+
public void testFromEvaluationContextInvalidUserIdTypes() {
158+
Map<String, Value> apiAttrs = new LinkedHashMap();
159+
160+
// Test with non-string userId value - should be ignored
161+
apiAttrs.put("userId", new Value(123));
162+
EvaluationContext ctx = new MutableContext(null, apiAttrs);
163+
164+
try {
165+
DevCycleUser.fromEvaluationContext(ctx);
166+
Assert.fail("Expected TargetingKeyMissingError");
167+
} catch (TargetingKeyMissingError e) {
168+
// expected
169+
}
170+
171+
// Test with non-string user_id value but valid userId string - should use userId
172+
apiAttrs.put("user_id", new Value(456));
173+
apiAttrs.put("userId", new Value("valid-userId"));
174+
ctx = new MutableContext(null, apiAttrs);
175+
176+
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);
177+
Assert.assertEquals(user.getUserId(), "valid-userId");
178+
}
179+
73180
@Test
74181
public void testCreateUserWithAttributes() {
75182
Map<String, Value> apiAttrs = new LinkedHashMap();

0 commit comments

Comments
 (0)