Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ coroutineScope.launch(Dispatchers.IO) {
## 🌟 Features

| Status | Features | Description |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|--------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
| ❌ | [Logging](#logging) | Integrate with popular logging packages. |
| ❌ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
| ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>

Expand Down Expand Up @@ -117,6 +118,16 @@ If the hook you're looking for hasn't been created yet, see the [develop a hook]

Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.

### Tracking

The [tracking API](https://openfeature.dev/specification/sections/tracking/) allows you to use
OpenFeature abstractions to associate user actions with feature flag evaluations.
This is essential for robust experimentation powered by feature flags. Note that, unlike methods
that handle feature flag evaluations, calling `track(...)` may throw an `IllegalArgumentException`
if an empty string is passed as the `trackingEventName`.

Tracking is optionally implemented by Providers.


```kotlin
// add a hook globally, to run on all evaluations
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/java/dev/openfeature/sdk/Client.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.openfeature.sdk

interface Client : Features {
interface Client : Features, Tracking {
val metadata: ClientMetadata
val hooks: List<Hook<*>>

Expand Down
12 changes: 12 additions & 0 deletions android/src/main/java/dev/openfeature/sdk/FeatureProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,16 @@ interface FeatureProvider : EventObserver, ProviderStatus {
fun getIntegerEvaluation(key: String, defaultValue: Int, context: EvaluationContext?): ProviderEvaluation<Int>
fun getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?): ProviderEvaluation<Double>
fun getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?): ProviderEvaluation<Value>

/**
* Feature provider implementations can opt in for to support Tracking by implementing this method.
*
* Performs tracking of a particular action or application state.
*
* @param trackingEventName Event name to track
* @param context Evaluation context used in flag evaluation (Optional)
* @param details Data pertinent to a particular tracking event (Optional)
*/
fun track(trackingEventName: String, context: EvaluationContext?, details: TrackingEventDetails?) {
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.openfeature.sdk

class ImmutableStructure(private val attributes: Map<String, Value> = mapOf()) : Structure {
constructor(vararg pairs: Pair<String, Value>) : this(pairs.toMap())

override fun keySet(): Set<String> {
return attributes.keys
}
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/java/dev/openfeature/sdk/NoOpProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import dev.openfeature.sdk.events.OpenFeatureEvents
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

class NoOpProvider(override val hooks: List<Hook<*>> = listOf()) : FeatureProvider {
open class NoOpProvider(override val hooks: List<Hook<*>> = listOf()) : FeatureProvider {
override val metadata: ProviderMetadata = NoOpProviderMetadata("No-op provider")
override fun initialize(initialContext: EvaluationContext?) {
// no-op
Expand Down
12 changes: 12 additions & 0 deletions android/src/main/java/dev/openfeature/sdk/OpenFeatureClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ class OpenFeatureClient(
return evaluateFlag(OBJECT, key, defaultValue, options)
}

override fun track(trackingEventName: String, details: TrackingEventDetails?) {
validateTrackingEventName(trackingEventName)
openFeatureAPI.getProvider()
.track(trackingEventName, openFeatureAPI.getEvaluationContext(), details)
}

private fun <T> evaluateFlag(
flagValueType: FlagValueType,
key: String,
Expand Down Expand Up @@ -257,4 +263,10 @@ class OpenFeatureClient(
}

data class Metadata(override val name: String?) : ClientMetadata
}

private fun validateTrackingEventName(name: String) {
if (name.isEmpty()) {
throw IllegalArgumentException("trackingEventName cannot be empty")
}
}
15 changes: 15 additions & 0 deletions android/src/main/java/dev/openfeature/sdk/Tracking.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.openfeature.sdk

/**
* Interface for Tracking events.
*/
interface Tracking {
/**
* Performs tracking of a particular action or application state.
*
* @param trackingEventName Event name to track
* @param details Data pertinent to a particular tracking event
* @throws IllegalArgumentException if {@code trackingEventName} is null
*/
fun track(trackingEventName: String, details: TrackingEventDetails? = null)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.openfeature.sdk

data class TrackingEventDetails(
val `value`: Number? = null,
val structure: Structure = ImmutableStructure()
) : Structure by structure
90 changes: 90 additions & 0 deletions android/src/test/java/dev/openfeature/sdk/TrackingProviderTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package dev.openfeature.sdk

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test

class TrackingProviderTests {

private lateinit var inMemoryTrackingProvider: InMemoryTrackingProvider

@Before
fun setup() {
inMemoryTrackingProvider = InMemoryTrackingProvider()
}

@Test(expected = IllegalArgumentException::class)
fun throwsOnEmptyName() {
OpenFeatureAPI.setProvider(inMemoryTrackingProvider)
OpenFeatureAPI.getClient().track("")
assertEquals(0, inMemoryTrackingProvider.trackings.size)
}

@Test
fun sendWithoutDetailsAppendsContext() {
OpenFeatureAPI.setProvider(inMemoryTrackingProvider)
val evaluationContext = ImmutableContext(
"targetingKey",
mapOf("integer" to Value.Integer(33))
)
OpenFeatureAPI.setEvaluationContext(
evaluationContext
)
OpenFeatureAPI.getClient().track("MyEventName")

val trackedEventCall = inMemoryTrackingProvider.trackings[0]
assertEquals("MyEventName", trackedEventCall.first)
val trackedEventDetails = trackedEventCall.third
assertNull(trackedEventDetails)
val trackedContext = trackedEventCall.second
assertEquals(evaluationContext, trackedContext)
}

@Test
fun trackEventWithDetails() {
OpenFeatureAPI.setProvider(inMemoryTrackingProvider)
val evaluationContext = ImmutableContext(
"targetingKey",
mapOf("integer" to Value.Integer(33))
)
OpenFeatureAPI.setEvaluationContext(
evaluationContext
)
OpenFeatureAPI.getClient().track(
"Checkout",
TrackingEventDetails(
499.99,
ImmutableStructure(
"numberOfItems" to Value.Integer(4),
"timeInCheckout" to Value.String("PT3M20S")
)
)
)

val trackedEventCall = inMemoryTrackingProvider.trackings[0]
assertEquals("Checkout", trackedEventCall.first)
val trackedEventDetails = trackedEventCall.third
assertNotNull(trackedEventDetails!!.value)
assertEquals(499.99, trackedEventDetails.value)
assertEquals(2, trackedEventDetails.structure.asMap().size)
assertEquals(Value.Integer(4), trackedEventDetails.structure.getValue("numberOfItems"))
assertEquals(Value.String("PT3M20S"), trackedEventDetails.structure.getValue("timeInCheckout"))

val trackedContext = trackedEventCall.second
assertEquals(evaluationContext, trackedContext)
}

private class InMemoryTrackingProvider : NoOpProvider() {
val trackings = mutableListOf<Triple<String, EvaluationContext?, TrackingEventDetails?>>()

override fun track(
trackingEventName: String,
context: EvaluationContext?,
details: TrackingEventDetails?
) {
trackings.add(Triple(trackingEventName, context, details))
}
}
}
Loading