Skip to content

Commit 9b80008

Browse files
Merge pull request #3056 from DataDog/typo/FFL-1579-sdk-android-expose-callback-for-set-evaluation-context-method-call
[FFL-1579] Add callback support to setEvaluationContext method Co-authored-by: typotter <[email protected]>
2 parents eebab6b + 1088bcd commit 9b80008

File tree

11 files changed

+224
-10
lines changed

11 files changed

+224
-10
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
interface com.datadog.android.flags.EvaluationContextCallback
2+
fun onSuccess()
3+
fun onFailure(Throwable)
14
object com.datadog.android.flags.Flags
25
fun enable(FlagsConfiguration = FlagsConfiguration.default, com.datadog.android.api.SdkCore = Datadog.getInstance())
36
interface com.datadog.android.flags.FlagsClient
4-
fun setEvaluationContext(com.datadog.android.flags.model.EvaluationContext)
7+
fun setEvaluationContext(com.datadog.android.flags.model.EvaluationContext, EvaluationContextCallback? = null)
58
fun resolveBooleanValue(String, Boolean): Boolean
69
fun resolveStringValue(String, String): String
710
fun resolveDoubleValue(String, Double): Double

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
public abstract interface class com/datadog/android/flags/EvaluationContextCallback {
2+
public abstract fun onFailure (Ljava/lang/Throwable;)V
3+
public abstract fun onSuccess ()V
4+
}
5+
16
public final class com/datadog/android/flags/Flags {
27
public static final field INSTANCE Lcom/datadog/android/flags/Flags;
38
public static final fun enable ()V
@@ -18,7 +23,7 @@ public abstract interface class com/datadog/android/flags/FlagsClient {
1823
public abstract fun resolveIntValue (Ljava/lang/String;I)I
1924
public abstract fun resolveStringValue (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
2025
public abstract fun resolveStructureValue (Ljava/lang/String;Lorg/json/JSONObject;)Lorg/json/JSONObject;
21-
public abstract fun setEvaluationContext (Lcom/datadog/android/flags/model/EvaluationContext;)V
26+
public abstract fun setEvaluationContext (Lcom/datadog/android/flags/model/EvaluationContext;Lcom/datadog/android/flags/EvaluationContextCallback;)V
2227
}
2328

2429
public final class com/datadog/android/flags/FlagsClient$Builder {
@@ -34,6 +39,10 @@ public final class com/datadog/android/flags/FlagsClient$Companion {
3439
public static synthetic fun get$default (Lcom/datadog/android/flags/FlagsClient$Companion;Ljava/lang/String;Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)Lcom/datadog/android/flags/FlagsClient;
3540
}
3641

42+
public final class com/datadog/android/flags/FlagsClient$DefaultImpls {
43+
public static synthetic fun setEvaluationContext$default (Lcom/datadog/android/flags/FlagsClient;Lcom/datadog/android/flags/model/EvaluationContext;Lcom/datadog/android/flags/EvaluationContextCallback;ILjava/lang/Object;)V
44+
}
45+
3746
public final class com/datadog/android/flags/FlagsConfiguration {
3847
public static final field Companion Lcom/datadog/android/flags/FlagsConfiguration$Companion;
3948
public final fun copy (ZLjava/lang/String;Ljava/lang/String;ZZ)Lcom/datadog/android/flags/FlagsConfiguration;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
/**
10+
* Callback interface for asynchronous evaluation context update operations.
11+
*
12+
* This callback is invoked on a background thread after the [FlagsClient.setEvaluationContext]
13+
* operation completes. The callback is guaranteed to be invoked after the corresponding
14+
* [FlagsClientState] transition.
15+
*/
16+
interface EvaluationContextCallback {
17+
/**
18+
* Invoked when the evaluation context update completes successfully.
19+
*
20+
* This method is called on a background executor thread after the state transitions
21+
* to [FlagsClientState.Ready]. The new flag evaluations are now available for
22+
* subsequent flag resolution calls.
23+
*/
24+
fun onSuccess()
25+
26+
/**
27+
* Invoked when the evaluation context update fails.
28+
*
29+
* This method is called on a background executor thread after the state transitions
30+
* to either [FlagsClientState.Stale] (network failed but cached flags available) or
31+
* [FlagsClientState.Error] (network failed with no cached flags).
32+
*
33+
* @param error A [Throwable] containing details about the failure, typically including
34+
* a message explaining the network request failure.
35+
*/
36+
fun onFailure(error: Throwable)
37+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,13 @@ interface FlagsClient {
6767
* The context is used to determine which flag values to return based on user targeting
6868
* rules. This method triggers a background fetch of updated flag evaluations.
6969
*
70+
* This method returns immediately without blocking. The actual context update and flag
71+
* fetching happen asynchronously on a background thread.
72+
*
7073
* @param context The [EvaluationContext] containing targeting key and attributes.
74+
* @param callback Optional callback to notify when the operation completes or fails.
7175
*/
72-
fun setEvaluationContext(context: EvaluationContext)
76+
fun setEvaluationContext(context: EvaluationContext, callback: EvaluationContextCallback? = null)
7377

7478
/**
7579
* Resolves a boolean flag value.

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package com.datadog.android.flags.internal
88

99
import com.datadog.android.api.InternalLogger
1010
import com.datadog.android.api.feature.FeatureSdkCore
11+
import com.datadog.android.flags.EvaluationContextCallback
1112
import com.datadog.android.flags.FlagsClient
1213
import com.datadog.android.flags.FlagsConfiguration
1314
import com.datadog.android.flags.StateObservable
@@ -66,9 +67,11 @@ internal class DatadogFlagsClient(
6667
*
6768
* @param context The evaluation context containing targeting key and attributes.
6869
* Must contain a valid targeting key; invalid contexts are logged and ignored.
70+
* @param callback Optional callback to notify when the operation completes or fails.
71+
* Invoked on a background executor thread after the state transition.
6972
*/
70-
override fun setEvaluationContext(context: EvaluationContext) {
71-
evaluationsManager.updateEvaluationsForContext(context)
73+
override fun setEvaluationContext(context: EvaluationContext, callback: EvaluationContextCallback?) {
74+
evaluationsManager.updateEvaluationsForContext(context, callback)
7275
}
7376

7477
/**

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package com.datadog.android.flags.internal
88

99
import com.datadog.android.api.InternalLogger
10+
import com.datadog.android.flags.EvaluationContextCallback
1011
import com.datadog.android.flags.FlagsClient
1112
import com.datadog.android.flags.FlagsStateListener
1213
import com.datadog.android.flags.StateObservable
@@ -43,10 +44,17 @@ internal class NoOpFlagsClient(
4344

4445
/**
4546
* No-op implementation that ignores context updates and logs a warning.
47+
*
48+
* Consistent with the graceful degradation pattern, this method succeeds silently
49+
* (invoking the success callback) while logging the no-op behavior. The actual error
50+
* state is communicated via [state.getCurrentState] which returns [FlagsClientState.Error].
51+
*
4652
* @param context Ignored evaluation context.
53+
* @param callback Optional callback invoked immediately with success.
4754
*/
48-
override fun setEvaluationContext(context: EvaluationContext) {
55+
override fun setEvaluationContext(context: EvaluationContext, callback: EvaluationContextCallback?) {
4956
logOperation("setEvaluationContext", InternalLogger.Level.WARN)
57+
callback?.onSuccess()
5058
}
5159

5260
/**

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ package com.datadog.android.flags.internal.evaluation
88

99
import com.datadog.android.api.InternalLogger
1010
import com.datadog.android.core.internal.utils.executeSafe
11+
import com.datadog.android.flags.EvaluationContextCallback
1112
import com.datadog.android.flags.internal.FlagsStateManager
13+
import com.datadog.android.flags.internal.net.NetworkRequestFailedException
1214
import com.datadog.android.flags.internal.net.PrecomputedAssignmentsReader
1315
import com.datadog.android.flags.internal.repository.FlagsRepository
1416
import com.datadog.android.flags.internal.repository.net.PrecomputeMapper
@@ -50,8 +52,9 @@ internal class EvaluationsManager(
5052
*
5153
* @param context The evaluation context to process. Must be non-null and contain
5254
* a valid targeting key.
55+
* @param callback Optional callback invoked when the context is set and the flags have been fetched successfully or not.
5356
*/
54-
fun updateEvaluationsForContext(context: EvaluationContext) {
57+
fun updateEvaluationsForContext(context: EvaluationContext, callback: EvaluationContextCallback? = null) {
5558
flagStateManager.updateState(FlagsClientState.Reconciling)
5659

5760
executorService.executeSafe(
@@ -76,18 +79,21 @@ internal class EvaluationsManager(
7679
)
7780

7881
flagStateManager.updateState(FlagsClientState.Ready)
82+
callback?.onSuccess()
7983
} else {
8084
internalLogger.log(
8185
InternalLogger.Level.WARN,
8286
InternalLogger.Target.USER,
8387
{ NETWORK_REQUEST_FAILED_MESSAGE }
8488
)
8589

90+
val throwable = NetworkRequestFailedException(NETWORK_REQUEST_FAILED_MESSAGE)
8691
if (hadFlags) {
8792
flagStateManager.updateState(FlagsClientState.Stale)
8893
} else {
89-
flagStateManager.updateState(FlagsClientState.Error(Throwable(NETWORK_REQUEST_FAILED_MESSAGE)))
94+
flagStateManager.updateState(FlagsClientState.Error(throwable))
9095
}
96+
callback?.onFailure(throwable)
9197
}
9298
}
9399
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.internal.net
8+
9+
/**
10+
* Exception thrown when a network request for flag evaluations fails.
11+
*
12+
* This exception is used internally to communicate network failures when updating evaluation
13+
* contexts. It provides a clear signal that the failure was due to network connectivity issues
14+
* rather than application logic errors.
15+
*
16+
* @param message A descriptive message about the network failure
17+
*/
18+
internal class NetworkRequestFailedException(message: String) : RuntimeException(message)

features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/NoOpFlagsClientTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ internal class NoOpFlagsClientTest {
9595
)
9696
}
9797

98+
@Test
99+
fun `M invoke onSuccess W setEvaluationContext() { with callback }`(forge: Forge) {
100+
// Given
101+
val fakeContext = EvaluationContext(forge.anAlphabeticalString(), emptyMap())
102+
val mockCallback = mock<EvaluationContextCallback>()
103+
104+
// When
105+
testedClient.setEvaluationContext(fakeContext, callback = mockCallback)
106+
107+
// Then
108+
// NoOp client follows graceful degradation pattern - succeeds silently
109+
verify(mockCallback).onSuccess()
110+
}
111+
98112
// endregion
99113

100114
// region resolveBooleanValue()

features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/DatadogFlagsClientTest.kt

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.datadog.android.flags.internal
99
import com.datadog.android.api.InternalLogger
1010
import com.datadog.android.api.feature.Feature.Companion.RUM_FEATURE_NAME
1111
import com.datadog.android.api.feature.FeatureSdkCore
12+
import com.datadog.android.flags.EvaluationContextCallback
1213
import com.datadog.android.flags.FlagsConfiguration
1314
import com.datadog.android.flags.FlagsStateListener
1415
import com.datadog.android.flags.internal.evaluation.EvaluationsManager
@@ -34,6 +35,7 @@ import org.mockito.Mockito.mock
3435
import org.mockito.junit.jupiter.MockitoExtension
3536
import org.mockito.junit.jupiter.MockitoSettings
3637
import org.mockito.kotlin.any
38+
import org.mockito.kotlin.anyOrNull
3739
import org.mockito.kotlin.argumentCaptor
3840
import org.mockito.kotlin.doReturn
3941
import org.mockito.kotlin.eq
@@ -871,7 +873,10 @@ internal class DatadogFlagsClientTest {
871873

872874
// Then
873875
val contextCaptor = argumentCaptor<EvaluationContext>()
874-
verify(mockEvaluationsManager).updateEvaluationsForContext(contextCaptor.capture())
876+
verify(mockEvaluationsManager).updateEvaluationsForContext(
877+
contextCaptor.capture(),
878+
anyOrNull()
879+
)
875880

876881
val capturedContext = contextCaptor.firstValue
877882
assertThat(capturedContext.targetingKey).isEqualTo(fakeTargetingKey)
@@ -894,7 +899,10 @@ internal class DatadogFlagsClientTest {
894899
// Then
895900
// Verify that blank targeting keys are allowed and processed
896901
val contextCaptor = argumentCaptor<EvaluationContext>()
897-
verify(mockEvaluationsManager).updateEvaluationsForContext(contextCaptor.capture())
902+
verify(mockEvaluationsManager).updateEvaluationsForContext(
903+
contextCaptor.capture(),
904+
anyOrNull()
905+
)
898906

899907
val capturedContext = contextCaptor.firstValue
900908
assertThat(capturedContext.targetingKey).isEmpty()
@@ -919,6 +927,37 @@ internal class DatadogFlagsClientTest {
919927
verify(mockEvaluationsManager).updateEvaluationsForContext(fakeContext)
920928
}
921929

930+
@Test
931+
fun `M delegate callback W setEvaluationContext() { with callback }`(forge: Forge) {
932+
// Given
933+
val fakeContext = EvaluationContext(forge.anAlphabeticalString(), emptyMap())
934+
val mockCallback = mock<EvaluationContextCallback>()
935+
936+
// When
937+
testedClient.setEvaluationContext(fakeContext, mockCallback)
938+
939+
// Then
940+
verify(mockEvaluationsManager).updateEvaluationsForContext(
941+
fakeContext,
942+
mockCallback
943+
)
944+
}
945+
946+
@Test
947+
fun `M delegate with null callback W setEvaluationContext() { without callback }`(forge: Forge) {
948+
// Given
949+
val fakeContext = EvaluationContext(forge.anAlphabeticalString(), emptyMap())
950+
951+
// When
952+
testedClient.setEvaluationContext(fakeContext)
953+
954+
// Then
955+
verify(mockEvaluationsManager).updateEvaluationsForContext(
956+
fakeContext,
957+
null
958+
)
959+
}
960+
922961
// endregion
923962

924963
// region exposure event logging configuration

0 commit comments

Comments
 (0)