Skip to content

Commit bd9768e

Browse files
authored
Merge pull request #3025 from DataDog/typo/FFL-1442-android-sdk-of-wrapper-implement-observe-method
feat: State change notification for flags client
2 parents ac0c5d5 + c8b1143 commit bd9768e

File tree

19 files changed

+795
-30
lines changed

19 files changed

+795
-30
lines changed

detekt_custom_safe_calls.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ datadog:
449449
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.SdkCore?)"
450450
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.feature.FeatureEventReceiver?)"
451451
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.internal.repository.DefaultFlagsRepository.FlagsState?)"
452+
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.FlagsClientState?)"
452453
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.ProviderContext?)"
453454
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.rum.internal.domain.RumContext?)"
454455
- "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.collections.Map?)"
@@ -1064,6 +1065,7 @@ datadog:
10641065
- "kotlin.IllegalArgumentException(kotlin.String?)"
10651066
- "kotlin.IllegalStateException(kotlin.String?)"
10661067
- "kotlin.Throwable.constructor()"
1068+
- "kotlin.Throwable.constructor(kotlin.String?)"
10671069
- "kotlin.Throwable.fillInStackTrace()"
10681070
- "kotlin.Throwable.stackTraceToString()"
10691071
- "kotlin.UnsupportedOperationException()"

features/dd-sdk-android-flags/api/apiSurface

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface com.datadog.android.flags.FlagsClient
88
fun resolveIntValue(String, Int): Int
99
fun resolveStructureValue(String, org.json.JSONObject): org.json.JSONObject
1010
fun <T: Any> resolve(String, T): com.datadog.android.flags.model.ResolutionDetails<T>
11+
val state: StateObservable
1112
class Builder
1213
constructor(String = DEFAULT_CLIENT_NAME, com.datadog.android.api.SdkCore = Datadog.getInstance())
1314
fun build(): FlagsClient
@@ -22,13 +23,26 @@ data class com.datadog.android.flags.FlagsConfiguration
2223
fun gracefulModeEnabled(Boolean): Builder
2324
fun build(): FlagsConfiguration
2425
companion object
26+
interface com.datadog.android.flags.FlagsStateListener
27+
fun onStateChanged(com.datadog.android.flags.model.FlagsClientState)
28+
interface com.datadog.android.flags.StateObservable
29+
fun getCurrentState(): com.datadog.android.flags.model.FlagsClientState
30+
fun addListener(FlagsStateListener)
31+
fun removeListener(FlagsStateListener)
2532
enum com.datadog.android.flags.model.ErrorCode
2633
- PROVIDER_NOT_READY
2734
- FLAG_NOT_FOUND
2835
- PARSE_ERROR
2936
- TYPE_MISMATCH
3037
data class com.datadog.android.flags.model.EvaluationContext
3138
constructor(String, Map<String, String> = emptyMap())
39+
sealed class com.datadog.android.flags.model.FlagsClientState
40+
object NotReady : FlagsClientState
41+
object Ready : FlagsClientState
42+
object Reconciling : FlagsClientState
43+
object Stale : FlagsClientState
44+
data class Error : FlagsClientState
45+
constructor(Throwable? = null)
3246
data class com.datadog.android.flags.model.ResolutionDetails<T: Any>
3347
constructor(T, String? = null, ResolutionReason? = null, ErrorCode? = null, String? = null, Map<String, Any> = emptyMap())
3448
enum com.datadog.android.flags.model.ResolutionReason

features/dd-sdk-android-flags/api/dd-sdk-android-flags.api

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public abstract interface class com/datadog/android/flags/FlagsClient {
1111
public static fun get ()Lcom/datadog/android/flags/FlagsClient;
1212
public static fun get (Ljava/lang/String;)Lcom/datadog/android/flags/FlagsClient;
1313
public static fun get (Ljava/lang/String;Lcom/datadog/android/api/SdkCore;)Lcom/datadog/android/flags/FlagsClient;
14+
public abstract fun getState ()Lcom/datadog/android/flags/StateObservable;
1415
public abstract fun resolve (Ljava/lang/String;Ljava/lang/Object;)Lcom/datadog/android/flags/model/ResolutionDetails;
1516
public abstract fun resolveBooleanValue (Ljava/lang/String;Z)Z
1617
public abstract fun resolveDoubleValue (Ljava/lang/String;D)D
@@ -55,6 +56,16 @@ public final class com/datadog/android/flags/FlagsConfiguration$Builder {
5556
public final class com/datadog/android/flags/FlagsConfiguration$Companion {
5657
}
5758

59+
public abstract interface class com/datadog/android/flags/FlagsStateListener {
60+
public abstract fun onStateChanged (Lcom/datadog/android/flags/model/FlagsClientState;)V
61+
}
62+
63+
public abstract interface class com/datadog/android/flags/StateObservable {
64+
public abstract fun addListener (Lcom/datadog/android/flags/FlagsStateListener;)V
65+
public abstract fun getCurrentState ()Lcom/datadog/android/flags/model/FlagsClientState;
66+
public abstract fun removeListener (Lcom/datadog/android/flags/FlagsStateListener;)V
67+
}
68+
5869
public final class com/datadog/android/flags/model/ErrorCode : java/lang/Enum {
5970
public static final field FLAG_NOT_FOUND Lcom/datadog/android/flags/model/ErrorCode;
6071
public static final field PARSE_ERROR Lcom/datadog/android/flags/model/ErrorCode;
@@ -170,6 +181,38 @@ public final class com/datadog/android/flags/model/ExposureEvent$Subject$Compani
170181
public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/flags/model/ExposureEvent$Subject;
171182
}
172183

184+
public abstract class com/datadog/android/flags/model/FlagsClientState {
185+
}
186+
187+
public final class com/datadog/android/flags/model/FlagsClientState$Error : com/datadog/android/flags/model/FlagsClientState {
188+
public fun <init> ()V
189+
public fun <init> (Ljava/lang/Throwable;)V
190+
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
191+
public final fun component1 ()Ljava/lang/Throwable;
192+
public final fun copy (Ljava/lang/Throwable;)Lcom/datadog/android/flags/model/FlagsClientState$Error;
193+
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;
194+
public fun equals (Ljava/lang/Object;)Z
195+
public final fun getError ()Ljava/lang/Throwable;
196+
public fun hashCode ()I
197+
public fun toString ()Ljava/lang/String;
198+
}
199+
200+
public final class com/datadog/android/flags/model/FlagsClientState$NotReady : com/datadog/android/flags/model/FlagsClientState {
201+
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$NotReady;
202+
}
203+
204+
public final class com/datadog/android/flags/model/FlagsClientState$Ready : com/datadog/android/flags/model/FlagsClientState {
205+
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$Ready;
206+
}
207+
208+
public final class com/datadog/android/flags/model/FlagsClientState$Reconciling : com/datadog/android/flags/model/FlagsClientState {
209+
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$Reconciling;
210+
}
211+
212+
public final class com/datadog/android/flags/model/FlagsClientState$Stale : com/datadog/android/flags/model/FlagsClientState {
213+
public static final field INSTANCE Lcom/datadog/android/flags/model/FlagsClientState$Stale;
214+
}
215+
173216
public final class com/datadog/android/flags/model/ResolutionDetails {
174217
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
175218
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

features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/FlagsClient.kt

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.datadog.android.core.InternalSdkCore
1616
import com.datadog.android.flags.internal.DatadogFlagsClient
1717
import com.datadog.android.flags.internal.DefaultRumEvaluationLogger
1818
import com.datadog.android.flags.internal.FlagsFeature
19+
import com.datadog.android.flags.internal.FlagsStateManager
1920
import com.datadog.android.flags.internal.LogWithPolicy
2021
import com.datadog.android.flags.internal.NoOpFlagsClient
2122
import com.datadog.android.flags.internal.NoOpRumEvaluationLogger
@@ -28,6 +29,7 @@ import com.datadog.android.flags.internal.repository.NoOpFlagsRepository
2829
import com.datadog.android.flags.internal.repository.net.PrecomputeMapper
2930
import com.datadog.android.flags.model.EvaluationContext
3031
import com.datadog.android.flags.model.ResolutionDetails
32+
import com.datadog.android.internal.utils.DDCoreSubscription
3133
import org.json.JSONObject
3234

3335
/**
@@ -148,6 +150,24 @@ interface FlagsClient {
148150
*/
149151
fun <T : Any> resolve(flagKey: String, defaultValue: T): ResolutionDetails<T>
150152

153+
/**
154+
* Observable interface for tracking client state changes.
155+
*
156+
* Provides three ways to observe state:
157+
* - Synchronous: [StateObservable.getCurrentState] for immediate queries (Java-friendly)
158+
* - Callback: [StateObservable.addListener] for traditional observers (Java-friendly)
159+
*
160+
* Example:
161+
* ```kotlin
162+
* // Synchronous
163+
* val current = client.state.getCurrentState()
164+
*
165+
* // Callback
166+
* client.state.addListener(listener)
167+
* ```
168+
*/
169+
val state: StateObservable
170+
151171
/**
152172
* Builder for creating [FlagsClient] instances with custom configuration.
153173
*
@@ -338,7 +358,8 @@ interface FlagsClient {
338358

339359
// region Internal
340360

341-
internal const val FLAGS_CLIENT_EXECUTOR_NAME = "flags-client-executor"
361+
internal const val FLAGS_NETWORK_EXECUTOR_NAME = "flags-network"
362+
internal const val FLAGS_STATE_NOTIFICATION_EXECUTOR_NAME = "flags-state-notifications"
342363

343364
@Suppress("LongMethod")
344365
internal fun createInternal(
@@ -347,8 +368,11 @@ interface FlagsClient {
347368
flagsFeature: FlagsFeature,
348369
name: String
349370
): FlagsClient {
350-
val executorService = featureSdkCore.createSingleThreadExecutorService(
351-
executorContext = FLAGS_CLIENT_EXECUTOR_NAME
371+
val networkExecutorService = featureSdkCore.createSingleThreadExecutorService(
372+
executorContext = FLAGS_NETWORK_EXECUTOR_NAME
373+
)
374+
val stateNotificationExecutorService = featureSdkCore.createSingleThreadExecutorService(
375+
executorContext = FLAGS_STATE_NOTIFICATION_EXECUTOR_NAME
352376
)
353377

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

401425
val precomputeMapper = PrecomputeMapper(featureSdkCore.internalLogger)
402426

427+
val flagStateManager = FlagsStateManager(
428+
DDCoreSubscription.create(),
429+
stateNotificationExecutorService,
430+
featureSdkCore.internalLogger
431+
)
432+
403433
val evaluationsManager = EvaluationsManager(
404-
executorService = executorService,
434+
executorService = networkExecutorService,
405435
internalLogger = featureSdkCore.internalLogger,
406436
flagsRepository = flagsRepository,
407437
assignmentsReader = assignmentsDownloader,
408-
precomputeMapper = precomputeMapper
438+
precomputeMapper = precomputeMapper,
439+
flagStateManager = flagStateManager
409440
)
410441

411442
val rumEvaluationLogger = createRumEvaluationLogger(featureSdkCore)
@@ -416,7 +447,8 @@ interface FlagsClient {
416447
flagsRepository = flagsRepository,
417448
flagsConfiguration = configuration,
418449
rumEvaluationLogger = rumEvaluationLogger,
419-
processor = flagsFeature.processor
450+
processor = flagsFeature.processor,
451+
flagStateManager = flagStateManager
420452
)
421453
}
422454
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.flags
8+
9+
import com.datadog.android.flags.model.FlagsClientState
10+
11+
/**
12+
* Listener interface for receiving state change notifications from a [FlagsClient].
13+
*
14+
* Implementations of this interface can be registered with a [FlagsClient] to receive
15+
* callbacks whenever the client's state changes.
16+
*/
17+
interface FlagsStateListener {
18+
/**
19+
* Called when the state of the [FlagsClient] changes.
20+
*
21+
* @param newState The new state of the client. If the state is [FlagsClientState.Error],
22+
* the error details are contained within the state object itself.
23+
*/
24+
fun onStateChanged(newState: FlagsClientState)
25+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.flags
8+
9+
import com.datadog.android.flags.model.FlagsClientState
10+
11+
/**
12+
* Observable interface for tracking [FlagsClient] state changes.
13+
*
14+
* This interface provides two ways to observe state:
15+
* 1. **Synchronous getter**: [getCurrentState] for immediate state queries
16+
* 2. **Callback pattern**: [addListener]/[removeListener] for reactive observers
17+
*
18+
* ## Usage Examples
19+
*
20+
* ```kotlin
21+
* // Synchronous getter
22+
* val current = client.state.getCurrentState()
23+
* if (current is FlagsClientState.Ready) {
24+
* // Proceed
25+
* }
26+
*
27+
* // Callback pattern
28+
* client.state.addListener(object : FlagsStateListener {
29+
* override fun onStateChanged(newState: FlagsClientState) {
30+
* // Handle state change
31+
* }
32+
* })
33+
* ```
34+
*/
35+
interface StateObservable {
36+
/**
37+
* Returns the current state synchronously.
38+
*
39+
* This method is safe to call from any thread.
40+
*
41+
* @return The current [FlagsClientState].
42+
*/
43+
fun getCurrentState(): FlagsClientState
44+
45+
/**
46+
* Registers a listener to receive state change notifications.
47+
*
48+
* The listener will immediately receive the current state upon registration,
49+
* then be notified of all future state changes.
50+
*
51+
* @param listener The [FlagsStateListener] to register.
52+
*/
53+
fun addListener(listener: FlagsStateListener)
54+
55+
/**
56+
* Unregisters a previously registered state listener.
57+
*
58+
* @param listener The [FlagsStateListener] to unregister.
59+
*/
60+
fun removeListener(listener: FlagsStateListener)
61+
}

features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/DatadogFlagsClient.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.datadog.android.api.InternalLogger
1010
import com.datadog.android.api.feature.FeatureSdkCore
1111
import com.datadog.android.flags.FlagsClient
1212
import com.datadog.android.flags.FlagsConfiguration
13+
import com.datadog.android.flags.StateObservable
1314
import com.datadog.android.flags.internal.evaluation.EvaluationsManager
1415
import com.datadog.android.flags.internal.model.PrecomputedFlag
1516
import com.datadog.android.flags.internal.repository.FlagsRepository
@@ -35,6 +36,7 @@ import org.json.JSONObject
3536
* @param flagsConfiguration configuration for the flags feature
3637
* @param rumEvaluationLogger responsible for sending flag evaluations to RUM.
3738
* @param processor responsible for writing exposure batches to be sent to flags backend.
39+
* @param flagStateManager channel for managing state change listeners
3840
*/
3941
@Suppress("TooManyFunctions") // All functions are necessary for flag evaluation lifecycle
4042
internal class DatadogFlagsClient(
@@ -43,9 +45,12 @@ internal class DatadogFlagsClient(
4345
private val flagsRepository: FlagsRepository,
4446
private val flagsConfiguration: FlagsConfiguration,
4547
private val rumEvaluationLogger: RumEvaluationLogger,
46-
private val processor: EventsProcessor
48+
private val processor: EventsProcessor,
49+
private val flagStateManager: FlagsStateManager
4750
) : FlagsClient {
4851

52+
override val state: StateObservable = flagStateManager
53+
4954
// region FlagsClient
5055

5156
/**

0 commit comments

Comments
 (0)