From ffd62fbf77b713c9dc902868a3dae904bcb6d9ab Mon Sep 17 00:00:00 2001 From: Fiona Date: Mon, 17 Aug 2026 00:38:08 -0700 Subject: [PATCH 1/3] feat(core): let the NTP servers for clock sync be configured The SDK always synchronized its clock against a fixed set of public-internet NTP pool hosts, with no supported way to point it elsewhere or turn it off. A deployment isolated from the public internet cannot reach those hosts, and some environments do not permit contacting them at all. Configuration.Builder.setNtpHosts(List) now chooses the servers, and an empty list skips clock synchronization entirely. Events are then timestamped with the device clock, which is the fallback the SDK already applied whenever synchronization failed, so no other behaviour changes. --- CHANGELOG.md | 6 ++++ dd-sdk-android-core/api/apiSurface | 1 + .../api/dd-sdk-android-core.api | 1 + .../core/configuration/Configuration.kt | 27 +++++++++++++++-- .../android/core/internal/CoreFeature.kt | 18 +++++------- .../configuration/ConfigurationBuilderTest.kt | 29 +++++++++++++++++++ .../android/core/internal/CoreFeatureTest.kt | 22 ++++++++++++++ .../forge/ConfigurationCoreForgeryFactory.kt | 7 ++++- 8 files changed, 98 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd7486dd1..4249a97dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.6.0 / 2026-08-17 + +* [FEATURE] Add `Configuration.Builder.setNtpHosts(List)` so the NTP servers used to synchronize the SDK clock can be chosen at initialization. The SDK previously always synchronized against a fixed set of public-internet NTP pool hosts. A deployment isolated from the public internet cannot reach those hosts, and some environments do not permit contacting them at all, yet there was no supported way to change or disable the behaviour. Pass the NTP servers reachable from the network the app runs on, or an empty list to skip clock synchronization entirely — events are then timestamped with the device clock, which is the same fallback the SDK already applied whenever synchronization failed. + +--- + # 0.5.0 / 2026-07-28 * [IMPROVEMENT] Stop reading SIM carrier info (`TelephonyManager.simCarrierIdName` / `simCarrierId`) in `BroadcastReceiverNetworkInfoProvider`. This call path was already unreachable at runtime (the provider is only used below API 24, while the carrier branch required API 28+), so removing it has no functional impact but eliminates the telephony-API reference from the bytecode that privacy-compliance static scanners flag. diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index dc36e0dcdf..9fa71f2e16 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -261,6 +261,7 @@ data class com.datadog.android.core.configuration.Configuration fun setBackpressureStrategy(BackPressureStrategy): Builder fun setUploadSchedulerStrategy(UploadSchedulerStrategy?): Builder fun setVersion(String): Builder + fun setNtpHosts(List): Builder companion object class com.datadog.android.core.configuration.HostsSanitizer fun sanitizeHosts(List, String): List diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index 3dc79e2111..d3109f93aa 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -717,6 +717,7 @@ public final class com/datadog/android/core/configuration/Configuration$Builder public final fun setEncryption (Lcom/datadog/android/security/Encryption;)Lcom/datadog/android/core/configuration/Configuration$Builder; public final fun setFirstPartyHosts (Ljava/util/List;)Lcom/datadog/android/core/configuration/Configuration$Builder; public final fun setFirstPartyHostsWithHeaderType (Ljava/util/Map;)Lcom/datadog/android/core/configuration/Configuration$Builder; + public final fun setNtpHosts (Ljava/util/List;)Lcom/datadog/android/core/configuration/Configuration$Builder; public final fun setPersistenceStrategyFactory (Lcom/datadog/android/core/persistence/PersistenceStrategy$Factory;)Lcom/datadog/android/core/configuration/Configuration$Builder; public final fun setProxy (Ljava/net/Proxy;Lokhttp3/Authenticator;)Lcom/datadog/android/core/configuration/Configuration$Builder; public final fun setUploadFrequency (Lcom/datadog/android/core/configuration/UploadFrequency;)Lcom/datadog/android/core/configuration/Configuration$Builder; diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt index 6fa6fbd8e2..ccad4b55c5 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt @@ -8,6 +8,7 @@ package com.datadog.android.core.configuration import com.datadog.android.Datadog import com.datadog.android.FlashcatSite +import com.datadog.android.core.internal.time.DatadogNtpEndpoint import com.datadog.android.core.persistence.PersistenceStrategy import com.datadog.android.security.Encryption import com.datadog.android.trace.TracingHeaderType @@ -44,7 +45,8 @@ internal constructor( val batchProcessingLevel: BatchProcessingLevel, val persistenceStrategyFactory: PersistenceStrategy.Factory?, val backpressureStrategy: BackPressureStrategy, - val uploadSchedulerStrategy: UploadSchedulerStrategy? + val uploadSchedulerStrategy: UploadSchedulerStrategy?, + val ntpHosts: List ) // region Builder @@ -285,6 +287,22 @@ internal constructor( return this } + /** + * Sets the NTP servers used to synchronize the SDK clock with server time. + * + * The default servers are reachable over the public internet, which a deployment + * isolated from it cannot use. Point this at NTP servers reachable from the + * network the app runs on, or pass an empty list to skip clock synchronization + * entirely and timestamp events with the device clock. + * + * @param ntpHosts the NTP server host names, or an empty list to disable + * clock synchronization + */ + fun setNtpHosts(ntpHosts: List): Builder { + coreConfig = coreConfig.copy(ntpHosts = ntpHosts.toList()) + return this + } + internal fun allowClearTextHttp(): Builder { coreConfig = coreConfig.copy( needsClearTextHttp = true @@ -311,6 +329,10 @@ internal constructor( BackPressureMitigation.IGNORE_NEWEST ) + // Declared before DEFAULT_CORE_CONFIG: companion properties initialise in + // declaration order, and DEFAULT_CORE_CONFIG reads this one. + internal val DEFAULT_NTP_HOSTS: List = DatadogNtpEndpoint.values().map { it.host } + internal val DEFAULT_CORE_CONFIG = Core( needsClearTextHttp = false, enableDeveloperModeWhenDebuggable = false, @@ -324,7 +346,8 @@ internal constructor( batchProcessingLevel = BatchProcessingLevel.MEDIUM, persistenceStrategyFactory = null, backpressureStrategy = DEFAULT_BACKPRESSURE_STRATEGY, - uploadSchedulerStrategy = null + uploadSchedulerStrategy = null, + ntpHosts = DEFAULT_NTP_HOSTS ) internal const val NETWORK_REQUESTS_TRACKING_FEATURE_NAME = "Network requests" diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index bc381f2e56..a6deb53201 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -62,7 +62,6 @@ import com.datadog.android.core.internal.thread.DatadogThreadFactory import com.datadog.android.core.internal.thread.LoggingScheduledThreadPoolExecutor import com.datadog.android.core.internal.thread.ScheduledExecutorServiceFactory import com.datadog.android.core.internal.time.AppStartTimeProvider -import com.datadog.android.core.internal.time.DatadogNtpEndpoint import com.datadog.android.core.internal.time.KronosTimeProvider import com.datadog.android.core.internal.time.LoggingSyncListener import com.datadog.android.core.internal.user.DatadogUserInfoProvider @@ -199,6 +198,7 @@ internal class CoreFeature( @Volatile internal var appBuildId: String? = null internal var customUploadSchedulerStrategy: UploadSchedulerStrategy? = null + internal var ntpHosts: List = Configuration.DEFAULT_NTP_HOSTS internal lateinit var uploadExecutorService: ScheduledThreadPoolExecutor internal lateinit var persistenceExecutorService: FlushableExecutorService @@ -260,9 +260,11 @@ internal class CoreFeature( readApplicationInformation(appContext, configuration) resolveProcessInfo(appContext) setupExecutors() - persistenceExecutorService.executeSafe("NTP Sync initialization", unboundInternalLogger) { - // Kronos performs I/O operation on startup, it needs to run in background - initializeClockSync(appContext) + if (ntpHosts.isNotEmpty()) { + persistenceExecutorService.executeSafe("NTP Sync initialization", unboundInternalLogger) { + // Kronos performs I/O operation on startup, it needs to run in background + initializeClockSync(appContext) + } } setupOkHttpClient(configuration.coreConfig) firstPartyHostHeaderTypeResolver @@ -469,12 +471,7 @@ internal class CoreFeature( } kronosClock = AndroidClockFactory.createKronosClock( safeContext, - ntpHosts = listOf( - DatadogNtpEndpoint.NTP_0, - DatadogNtpEndpoint.NTP_1, - DatadogNtpEndpoint.NTP_2, - DatadogNtpEndpoint.NTP_3 - ).map { it.host }, + ntpHosts = ntpHosts, cacheExpirationMs = TimeUnit.MINUTES.toMillis(NTP_CACHE_EXPIRATION_MINUTES), minWaitTimeBetweenSyncMs = TimeUnit.MINUTES.toMillis(NTP_DELAY_BETWEEN_SYNCS_MINUTES), syncListener = LoggingSyncListener(internalLogger) @@ -583,6 +580,7 @@ internal class CoreFeature( site = configuration.site backpressureStrategy = configuration.backpressureStrategy customUploadSchedulerStrategy = configuration.uploadSchedulerStrategy + ntpHosts = configuration.ntpHosts } private fun setupInfoProviders( diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt index 873a2d98d2..87b5b58ddf 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/configuration/ConfigurationBuilderTest.kt @@ -311,6 +311,35 @@ internal class ConfigurationBuilderTest { assertThat(config.additionalConfig).isEmpty() } + @Test + fun `M use ntp hosts W setNtpHosts()`(forge: Forge) { + // Given + val ntpHosts = forge.aList(size = forge.anInt(1, 5)) { + aStringMatching("[a-z]+\\.pool\\.ntp\\.org") + } + + // When + val config = testedBuilder + .setNtpHosts(ntpHosts) + .build() + + // Then + assertThat(config.coreConfig).isEqualTo( + Configuration.DEFAULT_CORE_CONFIG.copy(ntpHosts = ntpHosts) + ) + } + + @Test + fun `M disable clock sync W setNtpHosts() { empty list }`() { + // When + val config = testedBuilder + .setNtpHosts(emptyList()) + .build() + + // Then + assertThat(config.coreConfig.ntpHosts).isEmpty() + } + @Test fun `M build with additionalConfig W setAdditionalConfiguration()`(forge: Forge) { // Given diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt index 42d28860b2..892a3e101e 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt @@ -200,6 +200,27 @@ internal class CoreFeatureTest { .isInstanceOf(KronosTimeProvider::class.java) } + @Test + fun `M skip time sync W initialize { no ntp hosts }`() { + // Given + val config = fakeConfig.copy( + coreConfig = fakeConfig.coreConfig.copy(ntpHosts = emptyList()) + ) + + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + config, + fakeConsent + ) + + // Then + assertThat(testedFeature.kronosClock).isNull() + assertThat(testedFeature.timeProvider) + .isInstanceOf(DefaultTimeProvider::class.java) + } + @Test fun `M initialize system info provider W initialize`() { // When @@ -337,6 +358,7 @@ internal class CoreFeatureTest { assertThat(testedFeature.contextRef.get()).isEqualTo(appContext.mockInstance) assertThat(testedFeature.batchSize).isEqualTo(fakeConfig.coreConfig.batchSize) assertThat(testedFeature.uploadFrequency).isEqualTo(fakeConfig.coreConfig.uploadFrequency) + assertThat(testedFeature.ntpHosts).isEqualTo(fakeConfig.coreConfig.ntpHosts) } @Test diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt index 0bdae837be..b16c6e6d87 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/utils/forge/ConfigurationCoreForgeryFactory.kt @@ -60,7 +60,12 @@ internal class ConfigurationCoreForgeryFactory : mock(), forge.aValueFrom(BackPressureMitigation::class.java) ), - uploadSchedulerStrategy = forge.aNullable { mock() } + uploadSchedulerStrategy = forge.aNullable { mock() }, + // Always non-empty: an empty list disables clock sync, which is a distinct + // case covered by its own tests rather than left to chance here. + ntpHosts = forge.aList(size = forge.anInt(1, 5)) { + aStringMatching("[a-z]+\\.pool\\.ntp\\.org") + } ) } } From 29bb024aa3c8879357ad272e940e2f1b8919a7fe Mon Sep 17 00:00:00 2001 From: Fiona Date: Mon, 17 Aug 2026 08:28:27 -0700 Subject: [PATCH 2/3] fix(core): default to NTP servers reachable from mainland China Clock synchronization ran against the 0.datadog.pool.ntp.org through 3.datadog.pool.ntp.org hosts. Those are frequently slow or unreachable from mainland China, where this SDK is predominantly deployed, so synchronization timed out and events were timestamped with the device clock instead. The defaults are now ntp.aliyun.com, ntp1.aliyun.com, time1.cloud.tencent.com and cn.pool.ntp.org: two cloud providers plus the community pool, so no single operator being unreachable stops the clock from synchronizing. Apps that need other servers select them with setNtpHosts. DatadogNtpEndpoint only ever held those four hosts and is replaced by the list itself, since an enum whose values are immediately flattened to strings adds a type without adding meaning. --- CHANGELOG.md | 2 ++ .../core/configuration/Configuration.kt | 10 ++++-- .../core/internal/time/DatadogNtpEndpoint.kt | 33 ------------------- 3 files changed, 10 insertions(+), 35 deletions(-) delete mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4249a97dd4..32e59deff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 0.6.0 / 2026-08-17 +* [CHANGE] Change the default NTP servers from `0.datadog.pool.ntp.org` through `3.datadog.pool.ntp.org` to `ntp.aliyun.com`, `ntp1.aliyun.com`, `time1.cloud.tencent.com` and `cn.pool.ntp.org`. The previous hosts are frequently slow or unreachable from mainland China, where the SDK is predominantly deployed, so clock synchronization would time out and events fell back to the device clock. The new defaults span two cloud providers and the community pool, so no single operator being unreachable stops the clock from synchronizing. Apps that need other servers can select them with `setNtpHosts`. + * [FEATURE] Add `Configuration.Builder.setNtpHosts(List)` so the NTP servers used to synchronize the SDK clock can be chosen at initialization. The SDK previously always synchronized against a fixed set of public-internet NTP pool hosts. A deployment isolated from the public internet cannot reach those hosts, and some environments do not permit contacting them at all, yet there was no supported way to change or disable the behaviour. Pass the NTP servers reachable from the network the app runs on, or an empty list to skip clock synchronization entirely — events are then timestamped with the device clock, which is the same fallback the SDK already applied whenever synchronization failed. --- diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt index ccad4b55c5..1f47df7776 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/configuration/Configuration.kt @@ -8,7 +8,6 @@ package com.datadog.android.core.configuration import com.datadog.android.Datadog import com.datadog.android.FlashcatSite -import com.datadog.android.core.internal.time.DatadogNtpEndpoint import com.datadog.android.core.persistence.PersistenceStrategy import com.datadog.android.security.Encryption import com.datadog.android.trace.TracingHeaderType @@ -331,7 +330,14 @@ internal constructor( // Declared before DEFAULT_CORE_CONFIG: companion properties initialise in // declaration order, and DEFAULT_CORE_CONFIG reads this one. - internal val DEFAULT_NTP_HOSTS: List = DatadogNtpEndpoint.values().map { it.host } + // Two cloud providers plus the community pool, so no single operator being + // unreachable stops the clock from synchronizing. + internal val DEFAULT_NTP_HOSTS: List = listOf( + "ntp.aliyun.com", + "ntp1.aliyun.com", + "time1.cloud.tencent.com", + "cn.pool.ntp.org" + ) internal val DEFAULT_CORE_CONFIG = Core( needsClearTextHttp = false, diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt deleted file mode 100644 index f9102057da..0000000000 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DatadogNtpEndpoint.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.core.internal.time - -/** - * This object contains constant values for all the Datadog NTP Endpoint urls used in the SDK. - */ -internal enum class DatadogNtpEndpoint(val host: String) { - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - NTP_0("0.datadog.pool.ntp.org"), - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - NTP_1("1.datadog.pool.ntp.org"), - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - NTP_2("2.datadog.pool.ntp.org"), - - /** - * Endpoint for the Network Time Protocol time syncing. - */ - NTP_3("3.datadog.pool.ntp.org") -} From 0153cc0a26fe7af52ae6b44d8a85659b8e7495c4 Mon Sep 17 00:00:00 2001 From: Fiona Date: Tue, 18 Aug 2026 01:19:23 -0700 Subject: [PATCH 3/3] docs: state the measured NTP server latencies in the changelog The entry asserted the previous default servers were frequently unreachable. Measuring from a mainland-China host shows three of the four do answer, so the entry now reports what was measured: the new servers answer at stratum 2 within 12-40 ms, the previous ones at stratum 3 within 41-241 ms, and one of the four did not answer. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e59deff9..7ddb051a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # 0.6.0 / 2026-08-17 -* [CHANGE] Change the default NTP servers from `0.datadog.pool.ntp.org` through `3.datadog.pool.ntp.org` to `ntp.aliyun.com`, `ntp1.aliyun.com`, `time1.cloud.tencent.com` and `cn.pool.ntp.org`. The previous hosts are frequently slow or unreachable from mainland China, where the SDK is predominantly deployed, so clock synchronization would time out and events fell back to the device clock. The new defaults span two cloud providers and the community pool, so no single operator being unreachable stops the clock from synchronizing. Apps that need other servers can select them with `setNtpHosts`. +* [CHANGE] Change the default NTP servers from `0.datadog.pool.ntp.org` through `3.datadog.pool.ntp.org` to `ntp.aliyun.com`, `ntp1.aliyun.com`, `time1.cloud.tencent.com` and `cn.pool.ntp.org`. Measured from a mainland-China host, where this SDK is predominantly deployed, the new servers answer at stratum 2 within 12-40 ms, while the previous ones answer at stratum 3 within 41-241 ms and one of the four did not answer at all. The new defaults span two cloud providers and the community pool, so no single operator being unreachable stops the clock from synchronizing. Apps that need other servers can select them with `setNtpHosts`. * [FEATURE] Add `Configuration.Builder.setNtpHosts(List)` so the NTP servers used to synchronize the SDK clock can be chosen at initialization. The SDK previously always synchronized against a fixed set of public-internet NTP pool hosts. A deployment isolated from the public internet cannot reach those hosts, and some environments do not permit contacting them at all, yet there was no supported way to change or disable the behaviour. Pass the NTP servers reachable from the network the app runs on, or an empty list to skip clock synchronization entirely — events are then timestamped with the device clock, which is the same fallback the SDK already applied whenever synchronization failed.