Skip to content

Commit 6fc42d0

Browse files
dd-mergequeue[bot]typotterjonathanmos
authored
Merge pull request #2982 from DataDog/merge-from-feature-flagging-feature-branch
Flags Feature Co-authored-by: typotter <[email protected]> Co-authored-by: dd-mergequeue[bot] <121105855+dd-mergequeue[bot]@users.noreply.github.com> Co-authored-by: jonathanmos <[email protected]>
2 parents aa4a68f + 682054e commit 6fc42d0

File tree

85 files changed

+10834
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+10834
-20
lines changed

ci/pipelines/default-pipeline.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,24 @@ publish:release-internal:
526526
- dd-sdk-android-internal/verification-metadata.xml
527527

528528
# region Publish features/*
529+
530+
publish:release-flags:
531+
tags: [ "arch:amd64" ]
532+
only:
533+
- tags
534+
- develop
535+
image: $CI_IMAGE_DOCKER
536+
stage: publish
537+
timeout: 30m
538+
script:
539+
- !reference [.snippets, set-publishing-credentials]
540+
- ./gradlew :features:dd-sdk-android-flags:publishToSonatype closeSonatypeStagingRepository --stacktrace --no-daemon
541+
artifacts:
542+
when: on_success
543+
expire_in: 7 days
544+
paths:
545+
- features/dd-sdk-android-flags/verification-metadata.xml
546+
529547
publish:release-trace-api:
530548
tags: [ "arch:amd64" ]
531549
only:
@@ -1081,6 +1099,7 @@ notify:merge-verification-metadata:
10811099
- publish:release-okhttp
10821100
- publish:release-okhttp-otel
10831101
- publish:release-benchmark
1102+
- publish:release-flags
10841103
script:
10851104
- python3 ./ci/scripts/merge_verification_metadata.py
10861105
artifacts:

dd-sdk-android-core/api/apiSurface

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ interface com.datadog.android.api.feature.Feature
110110
companion object
111111
const val LOGS_FEATURE_NAME: String
112112
const val RUM_FEATURE_NAME: String
113+
const val FLAGS_FEATURE_NAME: String
113114
const val TRACING_FEATURE_NAME: String
114115
const val SESSION_REPLAY_FEATURE_NAME: String
115116
const val SESSION_REPLAY_RESOURCES_FEATURE_NAME: String
@@ -140,6 +141,7 @@ interface com.datadog.android.api.feature.FeatureSdkCore : com.datadog.android.a
140141
fun removeContextUpdateReceiver(FeatureContextUpdateReceiver)
141142
fun createSingleThreadExecutorService(String): java.util.concurrent.ExecutorService
142143
fun createScheduledExecutorService(String): java.util.concurrent.ScheduledExecutorService
144+
fun createOkHttpCallFactory(okhttp3.OkHttpClient.Builder.() -> Unit = {}): okhttp3.Call.Factory
143145
fun setAnonymousId(java.util.UUID?)
144146
interface com.datadog.android.api.feature.StorageBackedFeature : Feature
145147
val requestFactory: com.datadog.android.api.net.RequestFactory

dd-sdk-android-core/api/dd-sdk-android-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ public final class com/datadog/android/api/context/UserInfo {
360360

361361
public abstract interface class com/datadog/android/api/feature/Feature {
362362
public static final field Companion Lcom/datadog/android/api/feature/Feature$Companion;
363+
public static final field FLAGS_FEATURE_NAME Ljava/lang/String;
363364
public static final field LOGS_FEATURE_NAME Ljava/lang/String;
364365
public static final field NDK_CRASH_REPORTS_FEATURE_NAME Ljava/lang/String;
365366
public static final field RUM_FEATURE_NAME Ljava/lang/String;
@@ -372,6 +373,7 @@ public abstract interface class com/datadog/android/api/feature/Feature {
372373
}
373374

374375
public final class com/datadog/android/api/feature/Feature$Companion {
376+
public static final field FLAGS_FEATURE_NAME Ljava/lang/String;
375377
public static final field LOGS_FEATURE_NAME Ljava/lang/String;
376378
public static final field NDK_CRASH_REPORTS_FEATURE_NAME Ljava/lang/String;
377379
public static final field RUM_FEATURE_NAME Ljava/lang/String;
@@ -411,6 +413,7 @@ public final class com/datadog/android/api/feature/FeatureScopeExtKt {
411413
}
412414

413415
public abstract interface class com/datadog/android/api/feature/FeatureSdkCore : com/datadog/android/api/SdkCore {
416+
public abstract fun createOkHttpCallFactory (Lkotlin/jvm/functions/Function1;)Lokhttp3/Call$Factory;
414417
public abstract fun createScheduledExecutorService (Ljava/lang/String;)Ljava/util/concurrent/ScheduledExecutorService;
415418
public abstract fun createSingleThreadExecutorService (Ljava/lang/String;)Ljava/util/concurrent/ExecutorService;
416419
public abstract fun getFeature (Ljava/lang/String;)Lcom/datadog/android/api/feature/FeatureScope;
@@ -426,6 +429,7 @@ public abstract interface class com/datadog/android/api/feature/FeatureSdkCore :
426429
}
427430

428431
public final class com/datadog/android/api/feature/FeatureSdkCore$DefaultImpls {
432+
public static synthetic fun createOkHttpCallFactory$default (Lcom/datadog/android/api/feature/FeatureSdkCore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lokhttp3/Call$Factory;
429433
public static synthetic fun getFeatureContext$default (Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;ZILjava/lang/Object;)Ljava/util/Map;
430434
public static synthetic fun updateFeatureContext$default (Lcom/datadog/android/api/feature/FeatureSdkCore;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
431435
}

dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/Feature.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ interface Feature {
4444
*/
4545
const val RUM_FEATURE_NAME: String = "rum"
4646

47+
/**
48+
* Flags feature name.
49+
*/
50+
const val FLAGS_FEATURE_NAME: String = "flags"
51+
4752
/**
4853
* Tracing feature name.
4954
*/

dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureSdkCore.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package com.datadog.android.api.feature
88

99
import com.datadog.android.api.InternalLogger
1010
import com.datadog.android.api.SdkCore
11+
import okhttp3.Call
12+
import okhttp3.OkHttpClient
1113
import java.util.UUID
1214
import java.util.concurrent.ExecutorService
1315
import java.util.concurrent.ScheduledExecutorService
@@ -115,6 +117,28 @@ interface FeatureSdkCore : SdkCore {
115117
*/
116118
fun createScheduledExecutorService(executorContext: String): ScheduledExecutorService
117119

120+
/**
121+
* Creates an OkHttp [okhttp3.Call.Factory] with custom configuration that shares the SDK's
122+
* underlying thread pool and connection pool.
123+
*
124+
* This method allows features to configure their HTTP client while efficiently
125+
* sharing resources (dispatcher thread pool, connection pool) across the entire SDK.
126+
* Using a shared base client reduces resource consumption and improves performance
127+
* through better connection reuse.
128+
*
129+
* Example:
130+
* ```
131+
* val callFactory = sdkCore.createOkHttpCallFactory {
132+
* callTimeout(45, TimeUnit.SECONDS)
133+
* writeTimeout(30, TimeUnit.SECONDS)
134+
* }
135+
* ```
136+
*
137+
* @param block Configuration block to customize the [OkHttpClient.Builder]
138+
* @return A [Call.Factory] instance configured with shared resources
139+
*/
140+
fun createOkHttpCallFactory(block: OkHttpClient.Builder.() -> Unit = {}): Call.Factory
141+
118142
/**
119143
* Allows the given feature to set the anonymous ID for the SDK.
120144
*

dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,35 @@ internal class CoreFeature(
110110
private val scheduledExecutorServiceFactory: ScheduledExecutorServiceFactory
111111
) {
112112

113+
/**
114+
* Lazy-initialized shared OkHttpClient instance with FIPS 140-2 compliant configuration.
115+
* This base client is used as the foundation for all HTTP clients in the SDK.
116+
* When creating new clients via newBuilder(), the new clients will share the
117+
* dispatcher (thread pool) and connection pool with this base client, reducing
118+
* resource consumption across the SDK.
119+
*
120+
* Configuration includes:
121+
* - FIPS 140-2 compliant TLS 1.2/1.3 with restricted cipher suites for GovCloud support
122+
* - Network timeouts (45 seconds)
123+
* - HTTP/2 and HTTP/1.1 protocol support
124+
* - Rotating DNS resolver for reliability
125+
*/
126+
@Suppress("SpreadOperator", "UnsafeThirdPartyFunctionCall")
127+
private val lazySharedOkHttpClient: OkHttpClient by lazy {
128+
val connectionSpec = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)
129+
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
130+
.cipherSuites(*RESTRICTED_CIPHER_SUITES)
131+
.build()
132+
133+
OkHttpClient.Builder()
134+
.callTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
135+
.writeTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
136+
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
137+
.connectionSpecs(listOf(connectionSpec))
138+
.dns(RotatingDnsResolver()) // NPE cannot happen here
139+
.build()
140+
}
141+
113142
internal class OkHttpCallFactory(factory: () -> OkHttpClient) : Call.Factory {
114143
val okhttpClient by lazy(factory)
115144

@@ -301,6 +330,19 @@ internal class CoreFeature(
301330
return scheduledExecutorServiceFactory.create(internalLogger, executorContext, backpressureStrategy)
302331
}
303332

333+
fun createOkHttpCallFactory(block: OkHttpClient.Builder.() -> Unit): Call.Factory {
334+
return object : Call.Factory {
335+
// Create a new client that shares pools with the base client
336+
private val client = lazySharedOkHttpClient.newBuilder()
337+
.apply(block)
338+
.build()
339+
340+
override fun newCall(request: Request): Call {
341+
return client.newCall(request)
342+
}
343+
}
344+
}
345+
304346
@Throws(UnsupportedOperationException::class, InterruptedException::class)
305347
@Suppress("UnsafeThirdPartyFunctionCall") // Used in Nightly tests only
306348
fun drainAndShutdownExecutors() {
@@ -563,23 +605,18 @@ internal class CoreFeature(
563605
networkInfoProvider.register(appContext)
564606
}
565607

566-
@Suppress("SpreadOperator")
567608
private fun setupOkHttpClient(configuration: Configuration.Core) {
568609
callFactory = OkHttpCallFactory {
569-
val connectionSpec = when {
570-
configuration.needsClearTextHttp -> ConnectionSpec.CLEARTEXT
571-
else -> ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)
572-
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
573-
.cipherSuites(*RESTRICTED_CIPHER_SUITES)
574-
.build()
575-
}
610+
// Use shared base client to inherit FIPS-compliant configuration,
611+
// shared thread pool and connection pool
612+
val builder = lazySharedOkHttpClient.newBuilder()
576613

577-
val builder = OkHttpClient.Builder()
578-
builder.callTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
579-
.writeTimeout(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)
580-
.protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
581-
.connectionSpecs(listOf(connectionSpec))
614+
// Override connection specs for cleartext HTTP if needed
615+
if (configuration.needsClearTextHttp) {
616+
builder.connectionSpecs(listOf(ConnectionSpec.CLEARTEXT))
617+
}
582618

619+
// Add debug or production interceptors
583620
if (BuildConfig.DEBUG) {
584621
@Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here
585622
builder.addNetworkInterceptor(CurlInterceptor())
@@ -588,14 +625,12 @@ internal class CoreFeature(
588625
builder.addInterceptor(GzipRequestInterceptor(internalLogger))
589626
}
590627

628+
// Configure proxy if provided
591629
if (configuration.proxy != null) {
592630
builder.proxy(configuration.proxy)
593631
builder.proxyAuthenticator(configuration.proxyAuth)
594632
}
595633

596-
@Suppress("UnsafeThirdPartyFunctionCall") // NPE cannot happen here
597-
builder.dns(RotatingDnsResolver())
598-
599634
builder.build()
600635
}
601636
}

dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import com.datadog.android.error.internal.CrashReportsFeature
4343
import com.datadog.android.internal.telemetry.InternalTelemetryEvent
4444
import com.datadog.android.privacy.TrackingConsent
4545
import com.google.gson.JsonObject
46+
import okhttp3.Call
47+
import okhttp3.OkHttpClient
4648
import java.io.File
4749
import java.util.Collections
4850
import java.util.Locale
@@ -340,6 +342,11 @@ internal class DatadogCore(
340342
return coreFeature.createScheduledExecutorService(executorContext)
341343
}
342344

345+
/** @inheritDoc */
346+
override fun createOkHttpCallFactory(block: OkHttpClient.Builder.() -> Unit): Call.Factory {
347+
return coreFeature.createOkHttpCallFactory(block)
348+
}
349+
343350
override fun setAnonymousId(anonymousId: UUID?) {
344351
coreFeature.contextExecutorService.executeSafe("DatadogCore.setAnonymousId", internalLogger) {
345352
coreFeature.userInfoProvider.setAnonymousId(anonymousId?.toString())

dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeReso
2121
import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver
2222
import com.datadog.android.privacy.TrackingConsent
2323
import com.google.gson.JsonObject
24+
import okhttp3.Call
25+
import okhttp3.OkHttpClient
26+
import okhttp3.Protocol
27+
import okhttp3.Request
28+
import okhttp3.Response
29+
import okio.Timeout
2430
import java.io.File
2531
import java.util.UUID
2632
import java.util.concurrent.Callable
@@ -140,6 +146,10 @@ internal object NoOpInternalSdkCore : InternalSdkCore {
140146
return NoOpScheduledExecutorService()
141147
}
142148

149+
override fun createOkHttpCallFactory(block: OkHttpClient.Builder.() -> Unit): Call.Factory {
150+
return NoOpCallFactory
151+
}
152+
143153
override fun setAnonymousId(anonymousId: UUID?) = Unit
144154

145155
// endregion
@@ -291,4 +301,47 @@ internal object NoOpInternalSdkCore : InternalSdkCore {
291301
throw ExecutionException("Unsupported", UnsupportedOperationException())
292302
}
293303
}
304+
305+
object NoOpCallFactory : Call.Factory {
306+
override fun newCall(request: Request): Call {
307+
return NoOpCall(request)
308+
}
309+
}
310+
311+
class NoOpCall(private val originalRequest: Request) : Call {
312+
override fun cancel() = Unit
313+
314+
override fun clone(): Call {
315+
return NoOpCall(originalRequest)
316+
}
317+
318+
override fun enqueue(responseCallback: okhttp3.Callback) = Unit
319+
320+
@Suppress("UnsafeThirdPartyFunctionCall") // All required fields are set, won't throw
321+
override fun execute(): Response {
322+
// Response.Builder.build() requires: request, protocol, code, and message
323+
// to be set, otherwise it throws IllegalStateException
324+
return Response.Builder()
325+
.request(originalRequest)
326+
.protocol(Protocol.HTTP_1_1)
327+
.message("OK")
328+
.build()
329+
}
330+
331+
override fun isCanceled(): Boolean {
332+
return false
333+
}
334+
335+
override fun isExecuted(): Boolean {
336+
return false
337+
}
338+
339+
override fun request(): Request {
340+
return originalRequest
341+
}
342+
343+
override fun timeout(): Timeout {
344+
return Timeout.NONE
345+
}
346+
}
294347
}

dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,24 @@ internal class DatadogCoreTest {
14611461
assertThat(isActive).isTrue()
14621462
}
14631463

1464+
@Test
1465+
fun `M delegate to core feature W createOkHttpCallFactory() { }`(
1466+
@LongForgery(min = 0, max = 60) customTimeout: Long,
1467+
@LongForgery(min = 0, max = 60) customWriteTimeout: Long
1468+
) {
1469+
// Given
1470+
testedCore.initialize(fakeConfiguration)
1471+
1472+
// When
1473+
val callFactory = testedCore.createOkHttpCallFactory {
1474+
callTimeout(customTimeout, TimeUnit.SECONDS)
1475+
writeTimeout(customWriteTimeout, TimeUnit.SECONDS)
1476+
}
1477+
1478+
// Then
1479+
assertThat(callFactory).isNotNull()
1480+
}
1481+
14641482
class ErrorRecordingRunnable(
14651483
private val collector: MutableList<Throwable>,
14661484
private val delegate: Runnable
@@ -1477,6 +1495,7 @@ internal class DatadogCoreTest {
14771495
companion object {
14781496

14791497
val msToNs = TimeUnit.MILLISECONDS.toNanos(1)
1498+
val secondsToNs = TimeUnit.SECONDS.toNanos(1)
14801499

14811500
val appContext = ApplicationContextTestConfiguration(Application::class.java)
14821501

0 commit comments

Comments
 (0)