Skip to content

Commit ad953e3

Browse files
fix: support user_id / userId in OF Context (#261)
* fix: support user_id / userId in OF Context * chore: update user_id priority
1 parent 7e53d7b commit ad953e3

File tree

2 files changed

+97
-8
lines changed

2 files changed

+97
-8
lines changed

android-client-sdk/src/main/java/com/devcycle/sdk/android/openfeature/DevCycleContextMapper.kt

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,27 @@ object DevCycleContextMapper {
1111
if (context == null) return null
1212

1313
val builder = DevCycleUser.builder()
14-
var hasTargetingKey = false
14+
var userId = ""
1515
var hasStandardAttributes = false
1616
var isAnonymousExplicitlySet = false
1717

18-
// Map targeting key to user ID if available
19-
context.getTargetingKey()?.let { targetingKey ->
20-
if (targetingKey.isNotBlank()) {
21-
builder.withUserId(targetingKey)
22-
hasTargetingKey = true
18+
context.getTargetingKey()?.takeIf { it.isNotEmpty() }?.let {
19+
userId = it
20+
} ?: run {
21+
context.getValue("user_id")?.takeIf { it is Value.String }?.asString()?.let {
22+
userId = it
23+
} ?: run {
24+
context.getValue("userId")?.takeIf { it is Value.String }?.asString()?.let {
25+
userId = it
26+
}
2327
}
2428
}
2529

30+
if (!userId.isEmpty()) {
31+
builder.withUserId(userId)
32+
}
33+
34+
2635
// Map standard attributes
2736
context.getValue("email")?.let { email ->
2837
if (email is Value.String) {
@@ -68,6 +77,9 @@ object DevCycleContextMapper {
6877
// Use direct asMap method call instead of reflection
6978
context.asMap().forEach { (key, value) ->
7079
when (key) {
80+
"userId", "user_id" -> {
81+
// Skip these as they are always processed for user ID
82+
}
7183
"email" -> {
7284
// Only skip if it was successfully processed as a string above
7385
if (value !is Value.String) {
@@ -142,10 +154,10 @@ object DevCycleContextMapper {
142154
}
143155

144156
// Only return a user if we have meaningful data
145-
return if (hasTargetingKey || hasStandardAttributes || customData.isNotEmpty() || privateCustomData.isNotEmpty()) {
157+
return if (!userId.isEmpty() || hasStandardAttributes || customData.isNotEmpty() || privateCustomData.isNotEmpty()) {
146158
// If user has a targeting key, they should be considered identified (not anonymous)
147159
// unless explicitly set to anonymous via a boolean value
148-
if (hasTargetingKey && !isAnonymousExplicitlySet) {
160+
if (!userId.isEmpty() && !isAnonymousExplicitlySet) {
149161
builder.withIsAnonymous(false)
150162
}
151163

android-client-sdk/src/test/java/com/devcycle/sdk/android/openfeature/DevCycleContextMapperTest.kt

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,83 @@ class DevCycleContextMapperTest {
2929
assertEquals("user-123", jsonMap["userId"])
3030
}
3131

32+
@Test
33+
fun `maps userId attribute to user ID when no targeting key`() {
34+
val context = ImmutableContext(
35+
attributes = mutableMapOf(
36+
"userId" to Value.String("user-from-userId")
37+
)
38+
)
39+
40+
val result = DevCycleContextMapper.evaluationContextToDevCycleUser(context)
41+
42+
assertNotNull(result)
43+
val jsonMap = convertToJsonMap(result!!)
44+
assertEquals("user-from-userId", jsonMap["userId"])
45+
assertEquals(false, jsonMap["isAnonymous"]) // User should be identified
46+
}
47+
48+
@Test
49+
fun `maps user_id attribute to user ID when no targeting key or userId`() {
50+
val context = ImmutableContext(
51+
attributes = mutableMapOf(
52+
"user_id" to Value.String("user-from-user_id")
53+
)
54+
)
55+
56+
val result = DevCycleContextMapper.evaluationContextToDevCycleUser(context)
57+
58+
assertNotNull(result)
59+
val jsonMap = convertToJsonMap(result!!)
60+
assertEquals("user-from-user_id", jsonMap["userId"])
61+
assertEquals(false, jsonMap["isAnonymous"]) // User should be identified
62+
}
63+
64+
@Test
65+
fun `prioritizes targeting key over userId and user_id attributes`() {
66+
val context = ImmutableContext(
67+
targetingKey = "targeting-key-user",
68+
attributes = mutableMapOf(
69+
"userId" to Value.String("userId-attribute"),
70+
"user_id" to Value.String("user_id-attribute")
71+
)
72+
)
73+
74+
val result = DevCycleContextMapper.evaluationContextToDevCycleUser(context)
75+
76+
assertNotNull(result)
77+
val jsonMap = convertToJsonMap(result!!)
78+
assertEquals("targeting-key-user", jsonMap["userId"])
79+
80+
// userId and user_id should not appear in custom data when targeting key is used
81+
val customData = jsonMap["customData"] as? Map<*, *>
82+
assertNull(customData?.get("userId"))
83+
assertNull(customData?.get("user_id"))
84+
}
85+
86+
@Test
87+
fun `prioritizes user_id over userId attribute when no targeting key`() {
88+
val context = ImmutableContext(
89+
attributes = mutableMapOf(
90+
"userId" to Value.String("userId-attribute"),
91+
"user_id" to Value.String("user_id-attribute")
92+
)
93+
)
94+
95+
val result = DevCycleContextMapper.evaluationContextToDevCycleUser(context)
96+
97+
assertNotNull(result)
98+
val jsonMap = convertToJsonMap(result!!)
99+
// Should use user_id value as it has higher priority than userId (matching Java SDK)
100+
assertEquals("user_id-attribute", jsonMap["userId"])
101+
assertEquals(false, jsonMap["isAnonymous"]) // User should be identified
102+
103+
// Neither should appear in custom data when used for user ID
104+
val customData = jsonMap["customData"] as? Map<*, *>
105+
assertNull(customData?.get("userId"))
106+
assertNull(customData?.get("user_id"))
107+
}
108+
32109
@Test
33110
fun `maps standard attributes correctly`() {
34111
val context = ImmutableContext(

0 commit comments

Comments
 (0)