Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
af48d48
Flags Client State enum
typotter Nov 24, 2025
a0cc0cc
Add flags-openfeature module to local-ci
typotter Nov 24, 2025
1d10ded
Client State observer and reg/unreg
typotter Nov 24, 2025
fe1b172
use DD core subscription, listeners, and implement new subscription m…
typotter Nov 24, 2025
8035650
Flags State channel w/named state methods, listener paradigm and leve…
typotter Nov 24, 2025
4d1923c
tests
typotter Nov 25, 2025
856631b
api surface
typotter Nov 25, 2025
c9f9cbc
trim comments
typotter Nov 25, 2025
a44a582
detekt safe
typotter Nov 25, 2025
2ffdd58
api
typotter Nov 25, 2025
d6d6538
refactor into FlagsStateManager
typotter Nov 27, 2025
4478372
tidy listener api on FlagsClient
typotter Nov 27, 2025
a913a5c
hasFlags and tests
typotter Nov 27, 2025
4a54161
state listeners
typotter Nov 27, 2025
949a693
emit current state
typotter Nov 27, 2025
be53823
fix race
typotter Nov 27, 2025
d2b729d
separate executors, abstract state interaction out of FlagsClient
typotter Nov 27, 2025
6d79a8b
lint imports
typotter Nov 27, 2025
f341b57
safe calls and sync on state
typotter Nov 27, 2025
1cb6beb
lint
typotter Nov 27, 2025
9332ebf
not using atomic reference for flags client state
typotter Nov 27, 2025
81a95bd
comments and noop client is always ready
typotter Nov 27, 2025
acda00f
api import coroutines
typotter Nov 27, 2025
5a288d8
add transitive deps
typotter Nov 27, 2025
7fdd225
remove coroutines and Flow
typotter Nov 28, 2025
740d7e7
no need to sync
typotter Nov 28, 2025
952b5b8
Fix comments, tests, safe calls
typotter Dec 1, 2025
c8b1143
apply suggestion
typotter Dec 1, 2025
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
2 changes: 2 additions & 0 deletions detekt_custom_safe_calls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ datadog:
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.SdkCore?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.feature.FeatureEventReceiver?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.internal.repository.DefaultFlagsRepository.FlagsState?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.FlagsClientState?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.ProviderContext?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.rum.internal.domain.RumContext?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.collections.Map?)"
Expand Down Expand Up @@ -1064,6 +1065,7 @@ datadog:
- "kotlin.IllegalArgumentException(kotlin.String?)"
- "kotlin.IllegalStateException(kotlin.String?)"
- "kotlin.Throwable.constructor()"
- "kotlin.Throwable.constructor(kotlin.String?)"
- "kotlin.Throwable.fillInStackTrace()"
- "kotlin.Throwable.stackTraceToString()"
- "kotlin.UnsupportedOperationException()"
Expand Down
14 changes: 14 additions & 0 deletions features/dd-sdk-android-flags/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface com.datadog.android.flags.FlagsClient
fun resolveIntValue(String, Int): Int
fun resolveStructureValue(String, org.json.JSONObject): org.json.JSONObject
fun <T: Any> resolve(String, T): com.datadog.android.flags.model.ResolutionDetails<T>
val state: StateObservable
class Builder
constructor(String = DEFAULT_CLIENT_NAME, com.datadog.android.api.SdkCore = Datadog.getInstance())
fun build(): FlagsClient
Expand All @@ -22,13 +23,26 @@ data class com.datadog.android.flags.FlagsConfiguration
fun gracefulModeEnabled(Boolean): Builder
fun build(): FlagsConfiguration
companion object
interface com.datadog.android.flags.FlagsStateListener
fun onStateChanged(com.datadog.android.flags.model.FlagsClientState)
interface com.datadog.android.flags.StateObservable
fun getCurrentState(): com.datadog.android.flags.model.FlagsClientState
fun addListener(FlagsStateListener)
fun removeListener(FlagsStateListener)
enum com.datadog.android.flags.model.ErrorCode
- PROVIDER_NOT_READY
- FLAG_NOT_FOUND
- PARSE_ERROR
- TYPE_MISMATCH
data class com.datadog.android.flags.model.EvaluationContext
constructor(String, Map<String, String> = emptyMap())
sealed class com.datadog.android.flags.model.FlagsClientState
object NotReady : FlagsClientState
object Ready : FlagsClientState
object Reconciling : FlagsClientState
object Stale : FlagsClientState
data class Error : FlagsClientState
constructor(Throwable? = null)
data class com.datadog.android.flags.model.ResolutionDetails<T: Any>
constructor(T, String? = null, ResolutionReason? = null, ErrorCode? = null, String? = null, Map<String, Any> = emptyMap())
enum com.datadog.android.flags.model.ResolutionReason
Expand Down
43 changes: 43 additions & 0 deletions features/dd-sdk-android-flags/api/dd-sdk-android-flags.api
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public abstract interface class com/datadog/android/flags/FlagsClient {
public static fun get ()Lcom/datadog/android/flags/FlagsClient;
public static fun get (Ljava/lang/String;)Lcom/datadog/android/flags/FlagsClient;
public static fun get (Ljava/lang/String;Lcom/datadog/android/api/SdkCore;)Lcom/datadog/android/flags/FlagsClient;
public abstract fun getState ()Lcom/datadog/android/flags/StateObservable;
public abstract fun resolve (Ljava/lang/String;Ljava/lang/Object;)Lcom/datadog/android/flags/model/ResolutionDetails;
public abstract fun resolveBooleanValue (Ljava/lang/String;Z)Z
public abstract fun resolveDoubleValue (Ljava/lang/String;D)D
Expand Down Expand Up @@ -55,6 +56,16 @@ public final class com/datadog/android/flags/FlagsConfiguration$Builder {
public final class com/datadog/android/flags/FlagsConfiguration$Companion {
}

public abstract interface class com/datadog/android/flags/FlagsStateListener {
public abstract fun onStateChanged (Lcom/datadog/android/flags/model/FlagsClientState;)V
}

public abstract interface class com/datadog/android/flags/StateObservable {
public abstract fun addListener (Lcom/datadog/android/flags/FlagsStateListener;)V
public abstract fun getCurrentState ()Lcom/datadog/android/flags/model/FlagsClientState;
public abstract fun removeListener (Lcom/datadog/android/flags/FlagsStateListener;)V
}

public final class com/datadog/android/flags/model/ErrorCode : java/lang/Enum {
public static final field FLAG_NOT_FOUND Lcom/datadog/android/flags/model/ErrorCode;
public static final field PARSE_ERROR Lcom/datadog/android/flags/model/ErrorCode;
Expand Down Expand Up @@ -170,6 +181,38 @@ public final class com/datadog/android/flags/model/ExposureEvent$Subject$Compani
public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/flags/model/ExposureEvent$Subject;
}

public abstract class com/datadog/android/flags/model/FlagsClientState {
}

public final class com/datadog/android/flags/model/FlagsClientState$Error : com/datadog/android/flags/model/FlagsClientState {
public fun <init> ()V
public fun <init> (Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Throwable;
public final fun copy (Ljava/lang/Throwable;)Lcom/datadog/android/flags/model/FlagsClientState$Error;
public static synthetic fun copy$default (Lcom/datadog/android/flags/model/FlagsClientState$Error;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/datadog/android/flags/model/FlagsClientState$Error;
public fun equals (Ljava/lang/Object;)Z
public final fun getError ()Ljava/lang/Throwable;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/datadog/android/flags/model/FlagsClientState$NotReady : com/datadog/android/flags/model/FlagsClientState {
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$NotReady;
}

public final class com/datadog/android/flags/model/FlagsClientState$Ready : com/datadog/android/flags/model/FlagsClientState {
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$Ready;
}

public final class com/datadog/android/flags/model/FlagsClientState$Reconciling : com/datadog/android/flags/model/FlagsClientState {
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$Reconciling;
}

public final class com/datadog/android/flags/model/FlagsClientState$Stale : com/datadog/android/flags/model/FlagsClientState {
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$Stale;
}

public final class com/datadog/android/flags/model/ResolutionDetails {
public fun <init> (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/String;Lcom/datadog/android/flags/model/ResolutionReason;Lcom/datadog/android/flags/model/ErrorCode;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.datadog.android.core.InternalSdkCore
import com.datadog.android.flags.internal.DatadogFlagsClient
import com.datadog.android.flags.internal.DefaultRumEvaluationLogger
import com.datadog.android.flags.internal.FlagsFeature
import com.datadog.android.flags.internal.FlagsStateManager
import com.datadog.android.flags.internal.LogWithPolicy
import com.datadog.android.flags.internal.NoOpFlagsClient
import com.datadog.android.flags.internal.NoOpRumEvaluationLogger
Expand All @@ -28,6 +29,7 @@ import com.datadog.android.flags.internal.repository.NoOpFlagsRepository
import com.datadog.android.flags.internal.repository.net.PrecomputeMapper
import com.datadog.android.flags.model.EvaluationContext
import com.datadog.android.flags.model.ResolutionDetails
import com.datadog.android.internal.utils.DDCoreSubscription
import org.json.JSONObject

/**
Expand Down Expand Up @@ -148,6 +150,24 @@ interface FlagsClient {
*/
fun <T : Any> resolve(flagKey: String, defaultValue: T): ResolutionDetails<T>

/**
* Observable interface for tracking client state changes.
*
* Provides three ways to observe state:
* - Synchronous: [StateObservable.getCurrentState] for immediate queries (Java-friendly)
* - Callback: [StateObservable.addListener] for traditional observers (Java-friendly)
*
* Example:
* ```kotlin
* // Synchronous
* val current = client.state.getCurrentState()
*
* // Callback
* client.state.addListener(listener)
* ```
*/
val state: StateObservable

/**
* Builder for creating [FlagsClient] instances with custom configuration.
*
Expand Down Expand Up @@ -338,7 +358,8 @@ interface FlagsClient {

// region Internal

internal const val FLAGS_CLIENT_EXECUTOR_NAME = "flags-client-executor"
internal const val FLAGS_NETWORK_EXECUTOR_NAME = "flags-network"
internal const val FLAGS_STATE_NOTIFICATION_EXECUTOR_NAME = "flags-state-notifications"

@Suppress("LongMethod")
internal fun createInternal(
Expand All @@ -347,8 +368,11 @@ interface FlagsClient {
flagsFeature: FlagsFeature,
name: String
): FlagsClient {
val executorService = featureSdkCore.createSingleThreadExecutorService(
executorContext = FLAGS_CLIENT_EXECUTOR_NAME
val networkExecutorService = featureSdkCore.createSingleThreadExecutorService(
executorContext = FLAGS_NETWORK_EXECUTOR_NAME
)
val stateNotificationExecutorService = featureSdkCore.createSingleThreadExecutorService(
executorContext = FLAGS_STATE_NOTIFICATION_EXECUTOR_NAME
)

val datadogContext = (featureSdkCore as InternalSdkCore).getDatadogContext()
Expand Down Expand Up @@ -400,12 +424,19 @@ interface FlagsClient {

val precomputeMapper = PrecomputeMapper(featureSdkCore.internalLogger)

val flagStateManager = FlagsStateManager(
DDCoreSubscription.create(),
stateNotificationExecutorService,
featureSdkCore.internalLogger
)

val evaluationsManager = EvaluationsManager(
executorService = executorService,
executorService = networkExecutorService,
internalLogger = featureSdkCore.internalLogger,
flagsRepository = flagsRepository,
assignmentsReader = assignmentsDownloader,
precomputeMapper = precomputeMapper
precomputeMapper = precomputeMapper,
flagStateManager = flagStateManager
)

val rumEvaluationLogger = createRumEvaluationLogger(featureSdkCore)
Expand All @@ -416,7 +447,8 @@ interface FlagsClient {
flagsRepository = flagsRepository,
flagsConfiguration = configuration,
rumEvaluationLogger = rumEvaluationLogger,
processor = flagsFeature.processor
processor = flagsFeature.processor,
flagStateManager = flagStateManager
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.flags

import com.datadog.android.flags.model.FlagsClientState

/**
* Listener interface for receiving state change notifications from a [FlagsClient].
*
* Implementations of this interface can be registered with a [FlagsClient] to receive
* callbacks whenever the client's state changes.
*/
interface FlagsStateListener {
/**
* Called when the state of the [FlagsClient] changes.
*
* @param newState The new state of the client. If the state is [FlagsClientState.Error],
* the error details are contained within the state object itself.
*/
fun onStateChanged(newState: FlagsClientState)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.flags

import com.datadog.android.flags.model.FlagsClientState

/**
* Observable interface for tracking [FlagsClient] state changes.
*
* This interface provides two ways to observe state:
* 1. **Synchronous getter**: [getCurrentState] for immediate state queries
* 2. **Callback pattern**: [addListener]/[removeListener] for reactive observers
*
* ## Usage Examples
*
* ```kotlin
* // Synchronous getter
* val current = client.state.getCurrentState()
* if (current is FlagsClientState.Ready) {
* // Proceed
* }
*
* // Callback pattern
* client.state.addListener(object : FlagsStateListener {
* override fun onStateChanged(newState: FlagsClientState) {
* // Handle state change
* }
* })
* ```
*/
interface StateObservable {
/**
* Returns the current state synchronously.
*
* This method is safe to call from any thread.
*
* @return The current [FlagsClientState].
*/
fun getCurrentState(): FlagsClientState

/**
* Registers a listener to receive state change notifications.
*
* The listener will immediately receive the current state upon registration,
* then be notified of all future state changes.
*
* @param listener The [FlagsStateListener] to register.
*/
fun addListener(listener: FlagsStateListener)

/**
* Unregisters a previously registered state listener.
*
* @param listener The [FlagsStateListener] to unregister.
*/
fun removeListener(listener: FlagsStateListener)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.datadog.android.api.InternalLogger
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.flags.FlagsClient
import com.datadog.android.flags.FlagsConfiguration
import com.datadog.android.flags.StateObservable
import com.datadog.android.flags.internal.evaluation.EvaluationsManager
import com.datadog.android.flags.internal.model.PrecomputedFlag
import com.datadog.android.flags.internal.repository.FlagsRepository
Expand All @@ -35,6 +36,7 @@ import org.json.JSONObject
* @param flagsConfiguration configuration for the flags feature
* @param rumEvaluationLogger responsible for sending flag evaluations to RUM.
* @param processor responsible for writing exposure batches to be sent to flags backend.
* @param flagStateManager channel for managing state change listeners
*/
@Suppress("TooManyFunctions") // All functions are necessary for flag evaluation lifecycle
internal class DatadogFlagsClient(
Expand All @@ -43,9 +45,12 @@ internal class DatadogFlagsClient(
private val flagsRepository: FlagsRepository,
private val flagsConfiguration: FlagsConfiguration,
private val rumEvaluationLogger: RumEvaluationLogger,
private val processor: EventsProcessor
private val processor: EventsProcessor,
private val flagStateManager: FlagsStateManager
) : FlagsClient {

override val state: StateObservable = flagStateManager

// region FlagsClient

/**
Expand Down
Loading
Loading