Skip to content

Commit fed07a5

Browse files
committed
[FFL-2929] Add composable assignment request transport
Add opt-in assignment request timeout and retry controls plus an OkHttp Call.Factory transport seam. Preserve upgrade behavior with zero SDK timeout and zero retries by default. Environment: Datadog workspace
1 parent f8159e9 commit fed07a5

16 files changed

Lines changed: 1302 additions & 75 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
3+
* [IMPROVEMENT] Add an assignment-only HTTP call factory plus optional per-attempt timeout and retry-count configuration for Flags assignment requests. Timeouts and retries are opt-in; the default remains one request with no SDK-added timeout.
4+
15
# 3.13.1 / 2026-08-27
26

37
* [BUGFIX] Make `view` required again in RUM events. See [#3783](https://github.com/DataDog/dd-sdk-android/pull/3783)
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
Dependencies List
22

33
androidx.annotation:annotation-jvm:1.9.1 : 59 Kb
4+
com.squareup.okhttp3:okhttp:4.12.0 : 771 Kb
5+
com.squareup.okio:okio-jvm:3.6.0 : 351 Kb
46
dev.openfeature:kotlin-sdk-android:0.6.2 : 118 Kb
5-
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20 : 963 b
6-
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 : 969 b
7+
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10 : 959 b
8+
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10 : 965 b
79
org.jetbrains.kotlin:kotlin-stdlib:2.1.21 : 1683 Kb
810
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 : 1514 Kb
911
org.jetbrains:annotations:23.0.0 : 28 Kb
1012

11-
Total transitive dependencies size : 3 Mb
13+
Total transitive dependencies size : 4 Mb
1214

features/dd-sdk-android-flags/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,40 @@ val flagsConfig = FlagsConfiguration.Builder()
9393
.build()
9494
```
9595

96+
#### Configure assignment request limits
97+
98+
Precomputed assignment requests make one attempt and have no SDK-added timeout by default. Set an explicit timeout to
99+
include both receiving the response and downloading its body. A zero timeout keeps the SDK timeout disabled and preserves
100+
any timeout already configured on the HTTP client. The retry count accepts values from zero to ten; zero disables retries.
101+
102+
```kotlin
103+
val flagsConfig = FlagsConfiguration.Builder()
104+
.assignmentRequestTimeout(2_000)
105+
.assignmentRequestRetryCount(2)
106+
.build()
107+
```
108+
109+
#### Customize the assignment request transport
110+
111+
Supply an OkHttp `Call.Factory` to customize only precomputed assignment requests. The SDK still constructs the
112+
request URL, method, body, and authentication headers; the factory must preserve them. Exposure and evaluation
113+
uploads continue to use the SDK transport. Timeout and retry policies compose independently on top of the supplied
114+
factory.
115+
116+
```kotlin
117+
val customOkHttpClient = OkHttpClient.Builder()
118+
// Add assignment-specific proxy, TLS, or interceptors here.
119+
.build()
120+
121+
val flagsConfig = FlagsConfiguration.Builder()
122+
.assignmentRequestCallFactory(customOkHttpClient)
123+
.assignmentRequestTimeout(2_000)
124+
.assignmentRequestRetryCount(2)
125+
.build()
126+
```
127+
128+
The SDK does not own or shut down a supplied call factory or its resources.
129+
96130
## Use the Feature Flags SDK
97131

98132
### Create a Flags client

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ interface com.datadog.android.flags.FlagsClient
1919
companion object
2020
fun get(String = DEFAULT_CLIENT_NAME, com.datadog.android.api.SdkCore = Datadog.getInstance()): FlagsClient
2121
data class com.datadog.android.flags.FlagsConfiguration
22+
fun copy(Boolean = this.trackExposures, Boolean = this.trackEvaluations, String? = this.customExposureEndpoint, String? = this.customEvaluationEndpoint, String? = this.customFlagEndpoint, Long = this.evaluationFlushIntervalMs, Boolean = this.rumIntegrationEnabled, Boolean = this.gracefulModeEnabled): FlagsConfiguration
2223
class Builder
2324
fun trackExposures(Boolean): Builder
2425
fun trackEvaluations(Boolean): Builder
2526
fun useCustomExposureEndpoint(String): Builder
2627
fun useCustomEvaluationEndpoint(String): Builder
2728
fun evaluationFlushInterval(Long): Builder
2829
fun useCustomFlagEndpoint(String): Builder
30+
fun assignmentRequestCallFactory(okhttp3.Call.Factory): Builder
31+
fun assignmentRequestTimeout(Long): Builder
32+
fun assignmentRequestRetryCount(Int): Builder
2933
fun rumIntegrationEnabled(Boolean): Builder
3034
fun gracefulModeEnabled(Boolean): Builder
3135
fun build(): FlagsConfiguration

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,19 @@ public final class com/datadog/android/flags/FlagsClient$DefaultImpls {
4646

4747
public final class com/datadog/android/flags/FlagsConfiguration {
4848
public final fun copy (ZZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;JZZ)Lcom/datadog/android/flags/FlagsConfiguration;
49+
public final fun copy (ZZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;JZZLokhttp3/Call$Factory;JI)Lcom/datadog/android/flags/FlagsConfiguration;
4950
public static synthetic fun copy$default (Lcom/datadog/android/flags/FlagsConfiguration;ZZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;JZZILjava/lang/Object;)Lcom/datadog/android/flags/FlagsConfiguration;
51+
public static synthetic fun copy$default (Lcom/datadog/android/flags/FlagsConfiguration;ZZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;JZZLokhttp3/Call$Factory;JIILjava/lang/Object;)Lcom/datadog/android/flags/FlagsConfiguration;
5052
public fun equals (Ljava/lang/Object;)Z
5153
public fun hashCode ()I
5254
public fun toString ()Ljava/lang/String;
5355
}
5456

5557
public final class com/datadog/android/flags/FlagsConfiguration$Builder {
5658
public fun <init> ()V
59+
public final fun assignmentRequestCallFactory (Lokhttp3/Call$Factory;)Lcom/datadog/android/flags/FlagsConfiguration$Builder;
60+
public final fun assignmentRequestRetryCount (I)Lcom/datadog/android/flags/FlagsConfiguration$Builder;
61+
public final fun assignmentRequestTimeout (J)Lcom/datadog/android/flags/FlagsConfiguration$Builder;
5762
public final fun build ()Lcom/datadog/android/flags/FlagsConfiguration;
5863
public final fun evaluationFlushInterval (J)Lcom/datadog/android/flags/FlagsConfiguration$Builder;
5964
public final fun gracefulModeEnabled (Z)Lcom/datadog/android/flags/FlagsConfiguration$Builder;

features/dd-sdk-android-flags/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ dependencies {
5757

5858
implementation(libs.gson)
5959
implementation(libs.kotlin)
60-
implementation(libs.okHttp)
60+
// Public API: FlagsConfiguration.Builder.assignmentRequestCallFactory(Call.Factory)
61+
api(libs.okHttp)
6162
implementation(libs.androidXAnnotation)
6263
implementation(libs.androidXCollection)
6364

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.datadog.android.flags.model.EvaluationContext
3131
import com.datadog.android.flags.model.FlagsClientState
3232
import com.datadog.android.flags.model.ResolutionDetails
3333
import com.datadog.android.internal.utils.DDCoreStateHolder
34+
import okhttp3.Call
3435
import org.json.JSONObject
3536

3637
/**
@@ -408,11 +409,14 @@ interface FlagsClient {
408409
NoOpFlagsRepository()
409410
}
410411

411-
val callFactory = featureSdkCore.createOkHttpCallFactory()
412+
val callFactory = resolveAssignmentRequestCallFactory(configuration, featureSdkCore)
412413
val assignmentsDownloader = PrecomputedAssignmentsDownloader(
413414
internalLogger = featureSdkCore.internalLogger,
414415
callFactory = callFactory,
415-
requestFactory = flagsFeature.precomputedRequestFactory
416+
requestFactory = flagsFeature.precomputedRequestFactory,
417+
requestTimeoutMs = configuration.assignmentRequestTimeoutMs,
418+
requestRetryCount = configuration.assignmentRequestRetryCount,
419+
timeProvider = featureSdkCore.timeProvider
416420
)
417421

418422
val precomputeMapper = PrecomputeMapper(featureSdkCore.internalLogger)
@@ -448,6 +452,12 @@ interface FlagsClient {
448452
)
449453
}
450454

455+
internal fun resolveAssignmentRequestCallFactory(
456+
configuration: FlagsConfiguration,
457+
featureSdkCore: FeatureSdkCore
458+
): Call.Factory = configuration.assignmentRequestCallFactory
459+
?: featureSdkCore.createOkHttpCallFactory()
460+
451461
private fun createRumEvaluationLogger(featureSdkCore: FeatureSdkCore): RumEvaluationLogger {
452462
val rumFeatureScope = featureSdkCore.getFeature(RUM_FEATURE_NAME)
453463

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

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
package com.datadog.android.flags
88

9+
import okhttp3.Call
10+
11+
private const val DEFAULT_ASSIGNMENT_REQUEST_TIMEOUT_MS = 0L
12+
private const val DEFAULT_ASSIGNMENT_REQUEST_RETRY_COUNT = 0
13+
private const val MAX_ASSIGNMENT_REQUEST_RETRY_COUNT = 10
14+
915
/**
1016
* Describes configuration to be used for the Flags feature.
1117
*/
@@ -18,17 +24,54 @@ data class FlagsConfiguration internal constructor(
1824
internal val customFlagEndpoint: String?,
1925
internal val evaluationFlushIntervalMs: Long,
2026
internal val rumIntegrationEnabled: Boolean,
21-
internal val gracefulModeEnabled: Boolean
27+
internal val gracefulModeEnabled: Boolean,
28+
internal val assignmentRequestCallFactory: Call.Factory?,
29+
internal val assignmentRequestTimeoutMs: Long,
30+
internal val assignmentRequestRetryCount: Int
2231
) {
32+
/**
33+
* Copies this configuration while preserving assignment request transport policies.
34+
*
35+
* This overload retains the public JVM signature generated before assignment request settings were added to the
36+
* primary constructor. Keep its parameter list stable for binary compatibility.
37+
*/
38+
@Suppress("LongParameterList")
39+
fun copy(
40+
trackExposures: Boolean = this.trackExposures,
41+
trackEvaluations: Boolean = this.trackEvaluations,
42+
customExposureEndpoint: String? = this.customExposureEndpoint,
43+
customEvaluationEndpoint: String? = this.customEvaluationEndpoint,
44+
customFlagEndpoint: String? = this.customFlagEndpoint,
45+
evaluationFlushIntervalMs: Long = this.evaluationFlushIntervalMs,
46+
rumIntegrationEnabled: Boolean = this.rumIntegrationEnabled,
47+
gracefulModeEnabled: Boolean = this.gracefulModeEnabled
48+
): FlagsConfiguration = FlagsConfiguration(
49+
trackExposures = trackExposures,
50+
trackEvaluations = trackEvaluations,
51+
customExposureEndpoint = customExposureEndpoint,
52+
customEvaluationEndpoint = customEvaluationEndpoint,
53+
customFlagEndpoint = customFlagEndpoint,
54+
evaluationFlushIntervalMs = evaluationFlushIntervalMs,
55+
rumIntegrationEnabled = rumIntegrationEnabled,
56+
gracefulModeEnabled = gracefulModeEnabled,
57+
assignmentRequestCallFactory = assignmentRequestCallFactory,
58+
assignmentRequestTimeoutMs = assignmentRequestTimeoutMs,
59+
assignmentRequestRetryCount = assignmentRequestRetryCount
60+
)
61+
2362
/**
2463
* A Builder class for a [FlagsConfiguration].
2564
*/
65+
@Suppress("TooManyFunctions")
2666
class Builder {
2767
private var trackExposures: Boolean = true
2868
private var trackEvaluations: Boolean = true
2969
private var customExposureEndpoint: String? = null
3070
private var customEvaluationEndpoint: String? = null
3171
private var customFlagEndpoint: String? = null
72+
private var assignmentRequestCallFactory: Call.Factory? = null
73+
private var assignmentRequestTimeoutMs: Long = DEFAULT_ASSIGNMENT_REQUEST_TIMEOUT_MS
74+
private var assignmentRequestRetryCount: Int = DEFAULT_ASSIGNMENT_REQUEST_RETRY_COUNT
3275
private var evaluationFlushIntervalMs: Long = DEFAULT_EVALUATION_FLUSH_INTERVAL_MS
3376
private var rumIntegrationEnabled: Boolean = true
3477
private var gracefulModeEnabled: Boolean = true
@@ -116,6 +159,57 @@ data class FlagsConfiguration internal constructor(
116159
return this
117160
}
118161

162+
/**
163+
* Sets the HTTP call factory used only for precomputed assignment requests.
164+
*
165+
* The SDK constructs each request, including its URL, method, body, and authentication headers, before passing
166+
* it to [Call.Factory.newCall]. The factory must preserve those request properties. Exposure and evaluation
167+
* uploads continue to use the SDK's own HTTP transport.
168+
*
169+
* The SDK does not take ownership of the factory or its resources. The configured assignment timeout and retry
170+
* policies are applied on top of calls created by this factory.
171+
*
172+
* @param callFactory Factory used to create precomputed assignment calls.
173+
* @return this [Builder] instance for method chaining.
174+
*/
175+
fun assignmentRequestCallFactory(callFactory: Call.Factory): Builder {
176+
assignmentRequestCallFactory = callFactory
177+
return this
178+
}
179+
180+
/**
181+
* Sets the timeout for each precomputed assignment request.
182+
* The timeout includes downloading the response body. A value of zero disables the SDK timeout and preserves
183+
* any timeout already configured on the HTTP client.
184+
*
185+
* @param timeoutMs The timeout for each request, in milliseconds.
186+
* @return this [Builder] instance for method chaining.
187+
* @throws IllegalArgumentException if [timeoutMs] is negative.
188+
*/
189+
fun assignmentRequestTimeout(timeoutMs: Long): Builder {
190+
@Suppress("UnsafeThirdPartyFunctionCall") // Intentional public argument validation.
191+
require(timeoutMs >= 0) { "timeoutMs must be greater than or equal to 0" }
192+
assignmentRequestTimeoutMs = timeoutMs
193+
return this
194+
}
195+
196+
/**
197+
* Sets the number of retries after a transient precomputed assignment request failure.
198+
* The default is zero (no retries). The retry count must be between zero and ten, inclusive.
199+
*
200+
* @param retryCount The number of retries after the first attempt.
201+
* @return this [Builder] instance for method chaining.
202+
* @throws IllegalArgumentException if [retryCount] is outside the supported range.
203+
*/
204+
fun assignmentRequestRetryCount(retryCount: Int): Builder {
205+
@Suppress("UnsafeThirdPartyFunctionCall") // Intentional public argument validation.
206+
require(retryCount in 0..MAX_ASSIGNMENT_REQUEST_RETRY_COUNT) {
207+
"retryCount must be between 0 and $MAX_ASSIGNMENT_REQUEST_RETRY_COUNT"
208+
}
209+
assignmentRequestRetryCount = retryCount
210+
return this
211+
}
212+
119213
/**
120214
* Sets whether RUM evaluation logging is enabled.
121215
* This adds the result of evaluating a feature flag to the view.
@@ -160,7 +254,10 @@ data class FlagsConfiguration internal constructor(
160254
customFlagEndpoint = customFlagEndpoint,
161255
evaluationFlushIntervalMs = evaluationFlushIntervalMs,
162256
rumIntegrationEnabled = rumIntegrationEnabled,
163-
gracefulModeEnabled = gracefulModeEnabled
257+
gracefulModeEnabled = gracefulModeEnabled,
258+
assignmentRequestCallFactory = assignmentRequestCallFactory,
259+
assignmentRequestTimeoutMs = assignmentRequestTimeoutMs,
260+
assignmentRequestRetryCount = assignmentRequestRetryCount
164261
)
165262

166263
internal companion object {

0 commit comments

Comments
 (0)