Skip to content

Commit

Permalink
add confidence and context apis
Browse files Browse the repository at this point in the history
  • Loading branch information
vahidlazio committed Mar 11, 2024
1 parent 1159eee commit 760ff6f
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.spotify.confidence.Confidence
import com.spotify.confidence.ConfidenceFeatureProvider
import com.spotify.confidence.ConfidenceValue
import com.spotify.confidence.EventSender
import com.spotify.confidence.EventValue
import com.spotify.confidence.EventsScope
import com.spotify.confidence.InitialisationStrategy
import com.spotify.confidence.eventSender
import com.spotify.confidence.openFeatureProvider
import dev.openfeature.sdk.Client
import dev.openfeature.sdk.EvaluationContext
import dev.openfeature.sdk.FlagEvaluationDetails
import dev.openfeature.sdk.ImmutableContext
import dev.openfeature.sdk.OpenFeatureAPI
import dev.openfeature.sdk.Value
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.UUID

Expand Down Expand Up @@ -48,30 +50,30 @@ class MainVm(app: Application) : AndroidViewModel(app) {
}

client = OpenFeatureAPI.getClient()

val mutableMap = mutableMapOf<String, ConfidenceValue>()
mutableMap["screen"] = ConfidenceValue.String("value")
mutableMap["hello"] = ConfidenceValue.Boolean(false)
mutableMap["NN"] = ConfidenceValue.Double(20.0)
mutableMap["my_struct"] = ConfidenceValue.Struct(mapOf("x" to ConfidenceValue.Double(2.0)))

val confidence = Confidence(clientSecret)

viewModelScope.launch {
OpenFeatureAPI.setEvaluationContext(ctx)
val provider = ConfidenceFeatureProvider.create(
val provider = confidence.openFeatureProvider(
app.applicationContext,
clientSecret,
initialisationStrategy = strategy
)
OpenFeatureAPI.setProviderAndWait(provider, Dispatchers.IO)

val fields = {
val mutableMap = mutableMapOf<String, EventValue>()
mutableMap["screen"] = EventValue.String("value")
mutableMap["hello"] = EventValue.Boolean(false)
mutableMap["NN"] = EventValue.Double(20.0)
mutableMap["my_struct"] = EventValue.Struct(mapOf("x" to EventValue.Double(2.0)))
mutableMap
}
eventSender = confidence.eventSender(app.applicationContext)

val scope = EventsScope(
fields = fields
)

eventSender = provider.eventSender(app.applicationContext)
.withScope(scope)
launch {
delay(5000)
val ctx: EvaluationContext = ImmutableContext(targetingKey = "X")
OpenFeatureAPI.setEvaluationContext(ctx)
}

eventSender.emit("eventDefinitions/navigate")

Expand All @@ -94,7 +96,7 @@ class MainVm(app: Application) : AndroidViewModel(app) {
}.toComposeColor()
_message.postValue(messageValue)
_color.postValue(colorFlag)
eventSender.emit("eventDefinitions/navigate", mapOf("screen" to EventValue.String("Hello")))
eventSender.emit("eventDefinitions/navigate", mapOf("screen" to ConfidenceValue.String("Hello")))
}

fun updateContext() {
Expand Down
119 changes: 93 additions & 26 deletions Provider/src/main/java/com/spotify/confidence/ConfidenceExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,48 +1,115 @@
package com.spotify.confidence

import android.content.Context
import dev.openfeature.sdk.OpenFeatureAPI
import com.spotify.confidence.client.ResolveResponse
import dev.openfeature.sdk.EvaluationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

val confidenceFlushPolicy = object : FlushPolicy {
private var size = 0
override fun reset() {
size = 0
interface ConfidenceContextProvider {
fun confidenceContext(): ConfidenceValue.Struct
}

typealias ConfidenceFieldsType = Map<String, ConfidenceValue>

interface ConfidenceAPI : ConfidenceContextProvider {
fun fork(context: ConfidenceContext): ConfidenceAPI
fun putContext(context: ConfidenceContext)
}

interface ConfidenceContext {
val name: String
val value: ConfidenceValue
}

class PageContext(private val page: String) : ConfidenceContext {
override val value: ConfidenceValue
get() = ConfidenceValue.String(page)
override val name: String
get() = "page"
}

class CommonContext : ConfidenceContextProvider {
override fun confidenceContext(): ConfidenceValue.Struct = ConfidenceValue.Struct(mapOf())
}

fun EvaluationContext.toConfidenceContext() = object : ConfidenceContext {
override val name: String = "open_feature"
override val value: ConfidenceValue
get() = ConfidenceValue.Struct(
asMap()
.map { it.key to ConfidenceValue.String(it.value.toString()) }
.toMap() + ("targeting_key" to ConfidenceValue.String(getTargetingKey()))
)
}

class Confidence(
val clientSecret: String,
private val root: ConfidenceContextProvider = CommonContext()
) : ConfidenceAPI {
private var contextMap: ConfidenceValue.Struct = ConfidenceValue.Struct(mapOf())
override fun putContext(context: ConfidenceContext) {
val map = contextMap.value.toMutableMap()
map[context.name] = context.value
contextMap = ConfidenceValue.Struct(map)
}

override fun hit(event: Event) {
size++
override fun confidenceContext(): ConfidenceValue.Struct {
return ConfidenceValue.Struct(root.confidenceContext().value + contextMap.value)
}

override fun shouldFlush(): Boolean {
return size > 4
override fun fork(context: ConfidenceContext) = Confidence(clientSecret, this).also {
it.putContext(context)
}
}

fun ConfidenceFeatureProvider.eventSender(
internal fun ConfidenceAPI.resolve(flags: List<String>): ResolveResponse {
TODO()
}

fun Confidence.openFeatureProvider(
context: Context,
initialisationStrategy: InitialisationStrategy
): ConfidenceFeatureProvider = ConfidenceFeatureProvider.create(
context,
confidenceAPI = this,
clientSecret = clientSecret,
initialisationStrategy = initialisationStrategy
)

fun Confidence.eventSender(
context: Context,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): EventSender = EventSenderImpl.create(
clientSecret = this.clientSecret(),
clientSecret = clientSecret,
dispatcher = dispatcher,
flushPolicies = listOf(confidenceFlushPolicy),
scope = EventsScope(
fields = {
OpenFeatureAPI.getEvaluationContext()?.let { evalContext ->
val map = mutableMapOf<String, EventValue>()
map["targeting_key"] = EventValue.String(evalContext.getTargetingKey())
evalContext.asMap().forEach {
map[it.key] = EventValue.String(Json.encodeToString(it.value))
}
map
} ?: mapOf()
}
),
context = context
context = context,
confidenceContext = this
).onLowMemory { files ->
val sortedFiles = files.sortedBy { it.lastModified() }
sortedFiles.take(10).forEach { it.delete() }
}

val confidenceFlushPolicy = object : FlushPolicy {
private var size = 0
override fun reset() {
size = 0
}

override fun hit(event: Event) {
size++
}

override fun shouldFlush(): Boolean {
return size > 4
}
}

sealed class ConfidenceValue {
data class String(val value: kotlin.String) : ConfidenceValue()
data class Double(val value: kotlin.Double) : ConfidenceValue()
data class Boolean(val value: kotlin.Boolean) : ConfidenceValue()
data class Int(val value: kotlin.Int) : ConfidenceValue()
data class Struct(val value: Map<kotlin.String, ConfidenceValue>) : ConfidenceValue()
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ class ConfidenceFeatureProvider private constructor(
private val cache: ProviderCache,
private val storage: DiskStorage,
private val initialisationStrategy: InitialisationStrategy,
private val client: ConfidenceClient,
private val flagApplier: FlagApplier,
private val eventHandler: EventHandler,
private val confidenceAPI: Confidence,
dispatcher: CoroutineDispatcher
) : FeatureProvider {
private val job = SupervisorJob()
Expand All @@ -70,10 +70,6 @@ class ConfidenceFeatureProvider private constructor(
}
}

fun clientSecret(): String {
return client.clientSecret()
}

private fun internalInitialize(
initialContext: EvaluationContext,
strategy: InitialisationStrategy
Expand All @@ -86,7 +82,8 @@ class ConfidenceFeatureProvider private constructor(
}

coroutineScope.launch(networkExceptionHandler) {
val resolveResponse = client.resolve(listOf(), initialContext)
confidenceAPI.putContext(initialContext.toConfidenceContext())
val resolveResponse = confidenceAPI.resolve(listOf())
if (resolveResponse is ResolveResponse.Resolved) {
val (flags, resolveToken) = resolveResponse.flags

Expand Down Expand Up @@ -266,8 +263,10 @@ class ConfidenceFeatureProvider private constructor(
storage: DiskStorage? = null,
flagApplier: FlagApplier? = null,
eventHandler: EventHandler = EventHandler(Dispatchers.IO),
dispatcher: CoroutineDispatcher = Dispatchers.IO
dispatcher: CoroutineDispatcher = Dispatchers.IO,
confidenceAPI: Confidence? = null
): ConfidenceFeatureProvider {
val confidence = confidenceAPI ?: Confidence(clientSecret)
val configuredClient = client ?: ConfidenceRemoteClient(
clientSecret = clientSecret,
sdkMetadata = SdkMetadata(SDK_ID, BuildConfig.SDK_VERSION),
Expand All @@ -287,9 +286,9 @@ class ConfidenceFeatureProvider private constructor(
cache = cache ?: InMemoryCache(),
storage = diskStorage,
initialisationStrategy = initialisationStrategy,
client = configuredClient,
flagApplier = flagApplierWithRetries,
eventHandler,
confidence,
dispatcher
)
}
Expand Down Expand Up @@ -333,4 +332,8 @@ class ConfidenceFeatureProvider private constructor(
sealed interface InitialisationStrategy {
object FetchAndActivate : InitialisationStrategy
object ActivateAndFetchAsync : InitialisationStrategy
}

private fun ConfidenceContext.toEvaluationContext(): EvaluationContext {
TODO("Not yet implemented")
}
59 changes: 16 additions & 43 deletions Provider/src/main/java/com/spotify/confidence/EventSender.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import java.io.File

interface EventSender {
fun emit(definition: String, payload: EventPayloadType = mapOf())
fun withScope(scope: EventsScope): EventSender
fun emit(
definition: String,
payload: ConfidenceFieldsType = mapOf()
)
fun onLowMemory(body: (List<File>) -> Unit): EventSender
fun stop()
}
Expand All @@ -22,40 +23,17 @@ interface FlushPolicy {
fun shouldFlush(): Boolean
}

sealed class EventValue {
data class String(val value: kotlin.String) : EventValue()
data class Double(val value: kotlin.Double) : EventValue()
data class Boolean(val value: kotlin.Boolean) : EventValue()
data class Int(val value: kotlin.Int) : EventValue()
data class Struct(val value: Map<kotlin.String, EventValue>) : EventValue()
}

typealias EventPayloadType = Map<String, EventValue>

data class EventsScope(
val fields: () -> EventPayloadType = { mapOf() }
)

class EventSenderImpl internal constructor(
private val eventSenderEngine: EventSenderEngine,
private val dispatcher: CoroutineDispatcher,
private val scope: EventsScope = EventsScope()
private val confidenceContext: ConfidenceContextProvider,
dispatcher: CoroutineDispatcher
) : EventSender {
private val coroutineScope = CoroutineScope(dispatcher)
override fun emit(definition: String, payload: EventPayloadType) {
val scope = scope.fields()
eventSenderEngine.emit(definition, payload + scope)
}

override fun withScope(scope: EventsScope): EventSender {
val combinedFields = {
scope.fields() + this.scope.fields()
}
return EventSenderImpl(
eventSenderEngine,
dispatcher,
EventsScope(fields = combinedFields)
)
override fun emit(
definition: String,
payload: ConfidenceFieldsType
) {
eventSenderEngine.emit(definition, payload, confidenceContext.confidenceContext())
}

override fun onLowMemory(body: (List<File>) -> Unit): EventSender {
Expand All @@ -71,28 +49,23 @@ class EventSenderImpl internal constructor(

override fun stop() {
eventSenderEngine.stop()
instance = null
}

companion object {
private var instance: EventSender? = null
fun create(
context: Context,
clientSecret: String,
scope: EventsScope,
flushPolicies: List<FlushPolicy> = listOf(),
confidenceContext: ConfidenceContextProvider,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): EventSender = instance ?: run {
val engine = EventSenderEngine(
EventStorageImpl(context),
): EventSender {
val engine = EventSenderEngine.instance(
context,
clientSecret,
uploader = EventSenderUploaderImpl(OkHttpClient(), dispatcher),
flushPolicies = flushPolicies,
dispatcher = dispatcher
)
EventSenderImpl(engine, dispatcher, scope).also {
instance = it
}
return EventSenderImpl(engine, confidenceContext, dispatcher)
}
}
}
Loading

0 comments on commit 760ff6f

Please sign in to comment.