Skip to content

Commit d9ecf8f

Browse files
committed
feat: userId and customId specific feature gate overrides
1 parent 2469701 commit d9ecf8f

File tree

4 files changed

+94
-3
lines changed

4 files changed

+94
-3
lines changed

src/main/kotlin/com/statsig/sdk/Evaluator.kt

+27-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ internal class Evaluator(
3838
}
3939
}
4040
private val persistentStore: UserPersistentStorageHandler
41-
private var gateOverrides: MutableMap<String, Boolean> = HashMap()
41+
private var gateOverrides: MutableMap<String, MutableMap<String, Boolean>> = HashMap()
4242
private var configOverrides: MutableMap<String, Map<String, Any>> = HashMap()
4343
private var layerOverrides: MutableMap<String, Map<String, Any>> = HashMap()
4444
private var hashLookupTable: MutableMap<String, ULong> = HashMap()
@@ -148,7 +148,17 @@ internal class Evaluator(
148148
}
149149

150150
fun overrideGate(gateName: String, gateValue: Boolean) {
151-
gateOverrides[gateName] = gateValue
151+
if (gateOverrides[gateName] == null) {
152+
gateOverrides[gateName] = HashMap()
153+
}
154+
gateOverrides[gateName]?.set("", gateValue)
155+
}
156+
157+
fun overrideGate(gateName: String, gateValue: Boolean, userId: String) {
158+
if (gateOverrides[gateName] == null) {
159+
gateOverrides[gateName] = HashMap()
160+
}
161+
gateOverrides[gateName]?.set(userId, gateValue)
152162
}
153163

154164
fun overrideConfig(configName: String, configValue: Map<String, Any>) {
@@ -171,6 +181,10 @@ internal class Evaluator(
171181
gateOverrides.remove(gateName)
172182
}
173183

184+
fun removeGateOverride(gateName: String, userId: String) {
185+
gateOverrides[gateName]?.remove(userId)
186+
}
187+
174188
fun getConfig(ctx: EvaluationContext, dynamicConfigName: String) {
175189
if (configOverrides.containsKey(dynamicConfigName)) {
176190
ctx.evaluation.jsonValue = configOverrides[dynamicConfigName] ?: mapOf<String, Any>()
@@ -308,7 +322,17 @@ internal class Evaluator(
308322
@JvmOverloads
309323
fun checkGate(ctx: EvaluationContext, gateName: String) {
310324
if (gateOverrides.containsKey(gateName)) {
311-
val value = gateOverrides[gateName] ?: false
325+
val userIds = mutableListOf<String>()
326+
ctx.user.userID?.let { userIds.add(it) }
327+
ctx.user.customIDs?.let { customIdMap -> userIds.addAll(customIdMap.values ) }
328+
userIds.add("")
329+
330+
val value: Boolean = userIds
331+
.stream()
332+
.map { gateOverrides[gateName]?.get(it) }
333+
.filter { it != null }
334+
.findFirst().orElse(false) ?: false
335+
312336
ctx.evaluation.booleanValue = value
313337
ctx.evaluation.jsonValue = value
314338
ctx.evaluation.evaluationDetails = createEvaluationDetails(EvaluationReason.LOCAL_OVERRIDE)

src/main/kotlin/com/statsig/sdk/Statsig.kt

+28
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,21 @@ class Statsig {
300300
statsigServer.overrideGate(gateName, gateValue)
301301
}
302302

303+
/**
304+
* Sets a value to be returned for the given gate instead of the actual evaluated value.
305+
*
306+
* @param gateName The name of the gate to be overridden
307+
* @param gateValue The value that will be returned
308+
* @param userId The user ID to override the gate for
309+
*/
310+
@JvmStatic
311+
fun overrideGate(gateName: String, gateValue: Boolean, userId: String) {
312+
if (!checkInitialized()) {
313+
return
314+
}
315+
statsigServer.overrideGate(gateName, gateValue, userId)
316+
}
317+
303318
/**
304319
* Removes the given gate override
305320
*
@@ -312,6 +327,19 @@ class Statsig {
312327
}
313328
}
314329

330+
/**
331+
* Removes the given gate override for the given user ID
332+
*
333+
* @param gateName
334+
* @param userId
335+
*/
336+
@JvmStatic
337+
fun removeGateOverride(gateName: String, userId: String) {
338+
if (checkInitialized()) {
339+
statsigServer.removeGateOverride(gateName, userId)
340+
}
341+
}
342+
315343
/**
316344
* Sets a value to be returned for the given dynamic config/experiment instead of the actual evaluated value.
317345
*

src/main/kotlin/com/statsig/sdk/StatsigServer.kt

+24
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ sealed class StatsigServer {
8585

8686
abstract fun overrideGate(gateName: String, gateValue: Boolean)
8787

88+
abstract fun overrideGate(gateName: String, gateValue: Boolean, userId: String)
89+
8890
abstract fun removeGateOverride(gateName: String)
8991

92+
abstract fun removeGateOverride(gateName: String, userId: String)
93+
9094
abstract fun overrideConfig(configName: String, configValue: Map<String, Any>)
9195

9296
abstract fun removeConfigOverride(configName: String)
@@ -943,6 +947,16 @@ private class StatsigServerImpl() :
943947
}, { return@captureSync })
944948
}
945949

950+
override fun overrideGate(gateName: String, gateValue: Boolean, userId: String) {
951+
if (!isSDKInitialized()) {
952+
return
953+
}
954+
errorBoundary.captureSync("overrideGate", {
955+
isSDKInitialized()
956+
evaluator.overrideGate(gateName, gateValue, userId)
957+
}, { return@captureSync })
958+
}
959+
946960
override fun removeGateOverride(gateName: String) {
947961
if (!isSDKInitialized()) {
948962
return
@@ -953,6 +967,16 @@ private class StatsigServerImpl() :
953967
}, { return@captureSync })
954968
}
955969

970+
override fun removeGateOverride(gateName: String, userId: String) {
971+
if (!isSDKInitialized()) {
972+
return
973+
}
974+
errorBoundary.captureSync("removeGateOverride", {
975+
isSDKInitialized()
976+
evaluator.removeGateOverride(gateName, userId)
977+
}, { return@captureSync })
978+
}
979+
956980
override fun overrideConfig(configName: String, configValue: Map<String, Any>) {
957981
if (!isSDKInitialized()) {
958982
return

src/test/java/com/statsig/sdk/LocalOverridesTest.kt

+15
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ class LocalOverridesTest {
3939
assertFalse(Statsig.checkGate(user, "override_me"))
4040
}
4141

42+
@Test
43+
fun testGateOverridesWithUserId() = runBlocking {
44+
users.forEach { user -> testGateOverridesWithUserIdHelper(user) }
45+
}
46+
47+
private fun testGateOverridesWithUserIdHelper(user: StatsigUser) = runBlocking {
48+
assertFalse(Statsig.checkGate(user, "override_me"))
49+
50+
Statsig.overrideGate("override_me", true, user.userID ?: user.customIDs?.get("customID") ?: "")
51+
assertTrue(Statsig.checkGate(user, "override_me"))
52+
53+
Statsig.removeGateOverride("override_me")
54+
assertFalse(Statsig.checkGate(user, "override_me"))
55+
}
56+
4257
@Test
4358
fun testConfigOverrides() = runBlocking {
4459
users.forEach { user -> testConfigOverridesHelper(user) }

0 commit comments

Comments
 (0)