Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<String>)` 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.
Expand Down
1 change: 1 addition & 0 deletions dd-sdk-android-core/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>): Builder
companion object
class com.datadog.android.core.configuration.HostsSanitizer
fun sanitizeHosts(List<String>, String): List<String>
Expand Down
1 change: 1 addition & 0 deletions dd-sdk-android-core/api/dd-sdk-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>
)

// region Builder
Expand Down Expand Up @@ -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<String>): Builder {
coreConfig = coreConfig.copy(ntpHosts = ntpHosts.toList())
return this
}

internal fun allowClearTextHttp(): Builder {
coreConfig = coreConfig.copy(
needsClearTextHttp = true
Expand All @@ -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<String> = 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,
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -199,6 +198,7 @@ internal class CoreFeature(
@Volatile
internal var appBuildId: String? = null
internal var customUploadSchedulerStrategy: UploadSchedulerStrategy? = null
internal var ntpHosts: List<String> = Configuration.DEFAULT_NTP_HOSTS

internal lateinit var uploadExecutorService: ScheduledThreadPoolExecutor
internal lateinit var persistenceExecutorService: FlushableExecutorService
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -583,6 +580,7 @@ internal class CoreFeature(
site = configuration.site
backpressureStrategy = configuration.backpressureStrategy
customUploadSchedulerStrategy = configuration.uploadSchedulerStrategy
ntpHosts = configuration.ntpHosts
}

private fun setupInfoProviders(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
)
}
}
Loading