Skip to content

Commit

Permalink
Add Evaluation Callback Option (#195)
Browse files Browse the repository at this point in the history
* Add Evaluation Callback Option

* fix lint

* fix more lint

* One more lint fix

* fix tests

* update test

* update

* update

* update
  • Loading branch information
sroyal-statsig authored Mar 7, 2024
1 parent 1f87006 commit 0b25b31
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 69 deletions.
14 changes: 14 additions & 0 deletions src/main/java/com/statsig/androidsdk/BaseConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.statsig.androidsdk

open class BaseConfig(
private val name: String,
private val details: EvaluationDetails,
) {
open fun getName(): String {
return this.name
}

open fun getEvaluationDetails(): EvaluationDetails {
return this.details
}
}
10 changes: 1 addition & 9 deletions src/main/java/com/statsig/androidsdk/DynamicConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class DynamicConfig(
private val isExperimentActive: Boolean = false,
private val isDeviceBased: Boolean = false,
private val allocatedExperimentName: String? = null,
) {
) : BaseConfig(name, details) {
internal constructor(
configName: String,
apiDynamicConfig: APIDynamicConfig,
Expand Down Expand Up @@ -175,10 +175,6 @@ class DynamicConfig(
return this.jsonValue
}

fun getName(): String {
return this.name
}

fun getIsUserInExperiment(): Boolean {
return this.isUserInExperiment
}
Expand All @@ -187,10 +183,6 @@ class DynamicConfig(
return this.isExperimentActive
}

fun getEvaluationDetails(): EvaluationDetails {
return this.details
}

fun getRuleID(): String {
return this.rule
}
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/com/statsig/androidsdk/FeatureGate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.statsig.androidsdk

/**
* A helper class for interfacing with Feature Gate defined in the Statsig console
*/
class FeatureGate(
private val name: String,
private val details: EvaluationDetails,
private val value: Boolean,
private val rule: String = "",
private val groupName: String? = null,
private val secondaryExposures: Array<Map<String, String>> = arrayOf(),
) : BaseConfig(name, details) {
internal constructor(
gateName: String,
apiFeatureGate: APIFeatureGate,
evalDetails: EvaluationDetails,
) : this(
gateName,
evalDetails,
apiFeatureGate.value,
apiFeatureGate.ruleID,
apiFeatureGate.groupName,
apiFeatureGate.secondaryExposures,
)

fun getValue(): Boolean {
return this.value
}

fun getRuleID(): String {
return this.rule
}

fun getGroupName(): String? {
return this.groupName
}

fun getSecondaryExposures(): Array<Map<String, String>> {
return this.secondaryExposures
}
}
21 changes: 0 additions & 21 deletions src/main/java/com/statsig/androidsdk/InitializeResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,3 @@ internal data class APIDynamicConfig(
@SerializedName("allocated_experiment_name") val allocatedExperimentName: String? = null,
@SerializedName("explicit_parameters") val explicitParameters: Array<String> = arrayOf(),
)

internal data class FeatureGate(
val name: String,
val details: EvaluationDetails,
val value: Boolean = false,
val ruleID: String = "",
val groupName: String? = null,
val secondaryExposures: Array<Map<String, String>> = arrayOf(),
) {
constructor(
apiFeatureGate: APIFeatureGate,
evalDetails: EvaluationDetails,
) : this(
apiFeatureGate.name,
evalDetails,
apiFeatureGate.value,
apiFeatureGate.ruleID,
apiFeatureGate.groupName,
apiFeatureGate.secondaryExposures,
)
}
10 changes: 1 addition & 9 deletions src/main/java/com/statsig/androidsdk/Layer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Layer internal constructor(
private val isDeviceBased: Boolean = false,
private val allocatedExperimentName: String? = null,
private val explicitParameters: Set<String>? = null,
) {
) : BaseConfig(name, details) {
internal constructor(
client: StatsigClient?,
layerName: String,
Expand Down Expand Up @@ -142,10 +142,6 @@ class Layer internal constructor(
}
}

fun getName(): String {
return this.name
}

fun getIsUserInExperiment(): Boolean {
return this.isUserInExperiment
}
Expand All @@ -154,10 +150,6 @@ class Layer internal constructor(
return this.isExperimentActive
}

fun getEvaluationDetails(): EvaluationDetails {
return this.details
}

fun getRuleID(): String {
return this.rule
}
Expand Down
45 changes: 26 additions & 19 deletions src/main/java/com/statsig/androidsdk/StatsigClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,11 @@ class StatsigClient() : LifecycleEventListener {
var result = false
errorBoundary.capture({
val gate = store.checkGate(gateName)
result = gate.value
options.evaluationCallback?.invoke(gate)
result = gate.getValue()
logExposure(gateName, gate)
}, tag = functionName, configName = gateName)
options.evaluationCallback?.invoke(FeatureGate(gateName, EvaluationDetails(EvaluationReason.Uninitialized), false, ""))
return result
}

Expand All @@ -175,8 +177,10 @@ class StatsigClient() : LifecycleEventListener {
errorBoundary.capture({
this.logger.addNonExposedCheck(gateName)
val gate = store.checkGate(gateName)
result = gate.value
options.evaluationCallback?.invoke(gate)
result = gate.getValue()
}, tag = functionName, configName = gateName)
options.evaluationCallback?.invoke(FeatureGate(gateName, EvaluationDetails(EvaluationReason.Uninitialized), false, ""))
return result
}

Expand All @@ -190,12 +194,13 @@ class StatsigClient() : LifecycleEventListener {
fun getConfig(configName: String): DynamicConfig {
val functionName = "getConfig"
enforceInitialized(functionName)
var result: DynamicConfig? = null
var result: DynamicConfig = DynamicConfig.getUninitialized(configName)
errorBoundary.capture({
result = store.getConfig(configName)
logExposure(configName, result!!)
logExposure(configName, result)
}, tag = functionName, configName = configName)
return result ?: DynamicConfig.getUninitialized(configName)
options.evaluationCallback?.invoke(result)
return result
}

/**
Expand All @@ -208,12 +213,13 @@ class StatsigClient() : LifecycleEventListener {
fun getConfigWithExposureLoggingDisabled(configName: String): DynamicConfig {
val functionName = "getConfigWithExposureLoggingDisabled"
enforceInitialized(functionName)
var result: DynamicConfig? = null
var result: DynamicConfig = DynamicConfig.getUninitialized(configName)
errorBoundary.capture({
this.logger.addNonExposedCheck(configName)
result = store.getConfig(configName)
}, tag = functionName, configName = configName)
return result ?: DynamicConfig.getUninitialized(configName)
options.evaluationCallback?.invoke(result)
return result
}

/**
Expand All @@ -227,14 +233,14 @@ class StatsigClient() : LifecycleEventListener {
fun getExperiment(experimentName: String, keepDeviceValue: Boolean = false): DynamicConfig {
val functionName = "getExperiment"
enforceInitialized(functionName)
var res: DynamicConfig? = null
var res: DynamicConfig = DynamicConfig.getUninitialized(experimentName)
errorBoundary.capture({
res = store.getExperiment(experimentName, keepDeviceValue)
updateStickyValues()
logExposure(experimentName, res!!)
logExposure(experimentName, res)
}, tag = functionName, configName = experimentName)

return res ?: DynamicConfig.getUninitialized(experimentName)
options.evaluationCallback?.invoke(res)
return res
}

/**
Expand All @@ -251,13 +257,14 @@ class StatsigClient() : LifecycleEventListener {
): DynamicConfig {
val functionName = "getExperimentWithExposureLoggingDisabled"
enforceInitialized(functionName)
var exp: DynamicConfig? = null
var exp: DynamicConfig = DynamicConfig.getUninitialized(experimentName)
errorBoundary.capture({
this.logger.addNonExposedCheck(experimentName)
exp = store.getExperiment(experimentName, keepDeviceValue)
updateStickyValues()
}, configName = experimentName, tag = functionName)
return exp ?: DynamicConfig.getUninitialized(experimentName)
options.evaluationCallback?.invoke(exp)
return exp
}

/**
Expand All @@ -271,13 +278,13 @@ class StatsigClient() : LifecycleEventListener {
fun getLayer(layerName: String, keepDeviceValue: Boolean = false): Layer {
val functionName = "getLayer"
enforceInitialized(functionName)
var layer: Layer? = null
var layer: Layer = Layer.getUninitialized(layerName)
errorBoundary.capture({
layer = store.getLayer(this, layerName, keepDeviceValue)
updateStickyValues()
}, tag = functionName, configName = layerName)

return layer ?: Layer.getUninitialized(layerName)
options.evaluationCallback?.invoke(layer)
return layer
}

/**
Expand All @@ -294,14 +301,14 @@ class StatsigClient() : LifecycleEventListener {
): Layer {
val functionName = "getLayerWithExposureLoggingDisabled"
enforceInitialized(functionName)
var layer: Layer? = null
var layer: Layer = Layer.getUninitialized(layerName)
errorBoundary.capture({
this.logger.addNonExposedCheck(layerName)
layer = store.getLayer(null, layerName, keepDeviceValue)
updateStickyValues()
}, tag = functionName, configName = layerName)

return layer ?: Layer.getUninitialized(layerName)
options.evaluationCallback?.invoke(layer)
return layer
}

/**
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/com/statsig/androidsdk/StatsigLogger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal class StatsigLogger(
}

fun logExposure(name: String, gate: FeatureGate, user: StatsigUser, isManual: Boolean) {
val dedupeKey = name + gate.value + gate.ruleID + gate.details.reason.toString()
val dedupeKey = name + gate.getValue() + gate.getRuleID() + gate.getEvaluationDetails().reason.toString()
if (!shouldLogExposure(dedupeKey)) {
return
}
Expand All @@ -90,15 +90,15 @@ internal class StatsigLogger(

val metadata = mutableMapOf(
"gate" to name,
"gateValue" to gate.value.toString(),
"ruleID" to gate.ruleID,
"reason" to gate.details.reason.toString(),
"time" to gate.details.time.toString(),
"gateValue" to gate.getValue().toString(),
"ruleID" to gate.getRuleID(),
"reason" to gate.getEvaluationDetails().reason.toString(),
"time" to gate.getEvaluationDetails().time.toString(),
)
addManualFlag(metadata, isManual)

event.metadata = metadata
event.secondaryExposures = gate.secondaryExposures
event.secondaryExposures = gate.getSecondaryExposures()
log(event)
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/statsig/androidsdk/StatsigOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class StatsigOptions(
* Note: This requires special authorization from Statsig
*/
@SerializedName("disableHashing") var disableHashing: Boolean? = false,

var evaluationCallback: ((BaseConfig) -> Unit)? = null,
) {

private var environment: MutableMap<String, String>? = null
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/statsig/androidsdk/Store.kt
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ internal class Store(private val statsigScope: CoroutineScope, private val share

val hashName = Hashing.getHashedString(gateName, currentCache.values.hashUsed)
val gate = currentCache.values.featureGates?.get(hashName)
?: return FeatureGate(gateName, getEvaluationDetails(false))
return FeatureGate(gate, getEvaluationDetails(true))
?: return FeatureGate(gateName, getEvaluationDetails(false), false)
return FeatureGate(gateName, gate, getEvaluationDetails(true))
}

fun getConfig(configName: String): DynamicConfig {
Expand Down
Loading

0 comments on commit 0b25b31

Please sign in to comment.