diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd7486dd1..7ddb051a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 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`. 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. + +--- + # 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..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 @@ -44,7 +44,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 +286,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 +328,17 @@ internal constructor( BackPressureMitigation.IGNORE_NEWEST ) + // Declared before DEFAULT_CORE_CONFIG: companion properties initialise in + // declaration order, and DEFAULT_CORE_CONFIG reads this one. + // 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, enableDeveloperModeWhenDebuggable = false, @@ -324,7 +352,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/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") -} 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") + } ) } }