From 2633d21f5a60a12fed2076135e334ff77facdd14 Mon Sep 17 00:00:00 2001 From: Timur Valeev Date: Tue, 18 Nov 2025 13:28:00 +0000 Subject: [PATCH] RUM-10907: Supporting process uptime for telemetry debug attribute --- dd-sdk-android-core/api/apiSurface | 1 + .../api/dd-sdk-android-core.api | 1 + .../datadog/android/api/context/TimeInfo.kt | 6 +- .../datadog/android/core/InternalSdkCore.kt | 9 +++ .../android/core/internal/CoreFeature.kt | 3 + .../core/internal/DatadogContextProvider.kt | 15 +---- .../android/core/internal/DatadogCore.kt | 16 ++--- .../core/internal/NoOpContextProvider.kt | 7 +-- .../core/internal/NoOpInternalSdkCore.kt | 9 +-- .../internal/time/AppStartTimeProvider.kt | 6 ++ .../time/DefaultAppStartTimeProvider.kt | 3 + .../core/internal/time/KronosTimeProvider.kt | 4 -- .../core/internal/time/TimeProviderExt.kt | 21 +++++++ .../datadog/android/core/DatadogCoreTest.kt | 15 +++++ .../time/DefaultAppStartTimeProviderTest.kt | 50 ++++++++++++++-- dd-sdk-android-internal/api/apiSurface | 1 - .../api/dd-sdk-android-internal.api | 4 ++ .../internal/time/DefaultTimeProvider.kt | 1 - .../android/internal/time/TimeProvider.kt | 2 +- detekt_custom_safe_calls.yml | 2 + .../internal/TelemetryEventHandler.kt | 17 +++++- .../assertj/TelemetryDebugEventAssert.kt | 60 +++++++------------ .../internal/TelemetryEventHandlerTest.kt | 21 ++++++- 23 files changed, 186 insertions(+), 88 deletions(-) create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/TimeProviderExt.kt diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 3754148b4d..1011c845e8 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -201,6 +201,7 @@ interface com.datadog.android.core.InternalSdkCore : com.datadog.android.api.fea val lastViewEvent: com.google.gson.JsonObject? val lastFatalAnrSent: Long? val appStartTimeNs: Long + val appUptimeNs: Long fun writeLastViewEvent(ByteArray) fun deleteLastViewEvent() fun writeLastFatalAnrSent(Long) 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 7735430a37..036ae6acc5 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -594,6 +594,7 @@ public abstract interface class com/datadog/android/core/InternalSdkCore : com/d public abstract fun deleteLastViewEvent ()V public abstract fun getAllFeatures ()Ljava/util/List; public abstract fun getAppStartTimeNs ()J + public abstract fun getAppUptimeNs ()J public abstract fun getDatadogContext (Ljava/util/Set;)Lcom/datadog/android/api/context/DatadogContext; public abstract fun getFirstPartyHostResolver ()Lcom/datadog/android/core/internal/net/FirstPartyHostHeaderTypeResolver; public abstract fun getLastFatalAnrSent ()Ljava/lang/Long; diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt index 7728d46b33..832857ea7b 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/context/TimeInfo.kt @@ -20,4 +20,8 @@ data class TimeInfo( val serverTimeNs: Long, val serverTimeOffsetNs: Long, val serverTimeOffsetMs: Long -) +) { + internal companion object { + internal val EMPTY = TimeInfo(0, 0, 0, 0) + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt index 3924e20cbd..824e889630 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/InternalSdkCore.kt @@ -78,6 +78,15 @@ interface InternalSdkCore : FeatureSdkCore { @InternalApi val appStartTimeNs: Long + /** + * Provide the time since the application started in nanoseconds from device boot, or our best guess + * if the actual start time is not available. Note: since the implementation may rely on [System.nanoTime], + * this property can only be used to measure elapsed time and is not related to any other notion of system + * or wall-clock time. The value is the time since VM start. + */ + @InternalApi + val appUptimeNs: Long + /** * Writes current RUM view event to the dedicated file for the needs of NDK crash reporting. * 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 06aa2b35dd..3baa1d54ee 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 @@ -209,6 +209,9 @@ internal class CoreFeature( internal val appStartTimeNs: Long get() = appStartTimeProvider.appStartTimeNs + internal val appUptimeNs: Long + get() = appStartTimeProvider.appUptimeNs + // lazy here on purpose: we need to read it only once, even if it is used in different features @get:WorkerThread internal val lastViewEvent: JsonObject? by lazy { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt index f60a0b5bd8..bcf1286f78 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogContextProvider.kt @@ -10,8 +10,7 @@ import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.DeviceInfo import com.datadog.android.api.context.LocaleInfo import com.datadog.android.api.context.ProcessInfo -import com.datadog.android.api.context.TimeInfo -import java.util.concurrent.TimeUnit +import com.datadog.android.core.internal.time.composeTimeInfo internal class DatadogContextProvider( private val coreFeature: CoreFeature, @@ -30,17 +29,7 @@ internal class DatadogContextProvider( variant = coreFeature.variant, sdkVersion = coreFeature.sdkVersion, source = coreFeature.sourceName, - time = with(coreFeature.timeProvider) { - val deviceTimeMs = getDeviceTimestamp() - val serverTimeMs = getServerTimestamp() - TimeInfo( - deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(deviceTimeMs), - serverTimeNs = TimeUnit.MILLISECONDS.toNanos(serverTimeMs), - serverTimeOffsetNs = TimeUnit.MILLISECONDS - .toNanos(serverTimeMs - deviceTimeMs), - serverTimeOffsetMs = serverTimeMs - deviceTimeMs - ) - }, + time = coreFeature.timeProvider.composeTimeInfo(), processInfo = ProcessInfo( isMainProcess = coreFeature.isMainProcess ), diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt index a6bc76c13f..eb6c9a01b4 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt @@ -34,6 +34,7 @@ import com.datadog.android.core.internal.logger.SdkInternalLogger import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.system.BuildSdkVersionProvider import com.datadog.android.core.internal.time.DefaultAppStartTimeProvider +import com.datadog.android.core.internal.time.composeTimeInfo import com.datadog.android.core.internal.utils.executeSafe import com.datadog.android.core.internal.utils.getSafe import com.datadog.android.core.internal.utils.scheduleSafe @@ -100,17 +101,7 @@ internal class DatadogCore( /** @inheritDoc */ override val time: TimeInfo get() { - return with(coreFeature.timeProvider) { - val deviceTimeMs = getDeviceTimestamp() - val serverTimeMs = getServerTimestamp() - TimeInfo( - deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(deviceTimeMs), - serverTimeNs = TimeUnit.MILLISECONDS.toNanos(serverTimeMs), - serverTimeOffsetNs = TimeUnit.MILLISECONDS - .toNanos(serverTimeMs - deviceTimeMs), - serverTimeOffsetMs = serverTimeMs - deviceTimeMs - ) - } + return coreFeature.timeProvider.composeTimeInfo() } /** @inheritDoc */ @@ -387,6 +378,9 @@ internal class DatadogCore( override val appStartTimeNs: Long get() = coreFeature.appStartTimeNs + override val appUptimeNs: Long + get() = coreFeature.appUptimeNs + @WorkerThread override fun writeLastViewEvent(data: ByteArray) { // we need to write it only if we are going to read ApplicationExitInfo (available on diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt index baa2fad01e..93e3849e47 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpContextProvider.kt @@ -28,12 +28,7 @@ internal class NoOpContextProvider : ContextProvider { variant = "", source = "", sdkVersion = "", - time = TimeInfo( - deviceTimeNs = 0L, - serverTimeNs = 0L, - serverTimeOffsetMs = 0L, - serverTimeOffsetNs = 0L - ), + time = TimeInfo.EMPTY, processInfo = ProcessInfo(isMainProcess = true), networkInfo = NetworkInfo( connectivity = NetworkInfo.Connectivity.NETWORK_OTHER, diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt index 2775bb0bfd..39445c207c 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/NoOpInternalSdkCore.kt @@ -41,16 +41,15 @@ import java.util.concurrent.TimeUnit /** * A no-op implementation of [SdkCore]. */ +@Suppress("TooManyFunctions") internal object NoOpInternalSdkCore : InternalSdkCore { override val name: String = "no-op" override val time: TimeInfo = with(System.currentTimeMillis()) { - TimeInfo( + TimeInfo.EMPTY.copy( deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(this), - serverTimeNs = TimeUnit.MILLISECONDS.toNanos(this), - serverTimeOffsetNs = 0L, - serverTimeOffsetMs = 0L + serverTimeNs = TimeUnit.MILLISECONDS.toNanos(this) ) } @@ -78,6 +77,8 @@ internal object NoOpInternalSdkCore : InternalSdkCore { get() = null override val appStartTimeNs: Long get() = 0 + override val appUptimeNs: Long + get() = 0 // endregion diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt index 3826d781b7..c7802aa971 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/AppStartTimeProvider.kt @@ -12,4 +12,10 @@ internal interface AppStartTimeProvider { * if the actual start time is not available. */ val appStartTimeNs: Long + + /** + * Provide the time since the application started in nanoseconds from device boot, or our best guess + * if the actual start time is not available. + */ + val appUptimeNs: Long } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt index 1209ec2130..b470b7026d 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProvider.kt @@ -28,4 +28,7 @@ internal class DefaultAppStartTimeProvider( else -> DdRumContentProvider.createTimeNs } } + + override val appUptimeNs: Long + get() = System.nanoTime() - appStartTimeNs } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt index 809c47706c..1ff11ae1e4 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/KronosTimeProvider.kt @@ -14,10 +14,6 @@ internal class KronosTimeProvider( private val clock: Clock ) : TimeProvider { - override fun getDeviceTimestamp(): Long { - return System.currentTimeMillis() - } - override fun getServerTimestamp(): Long { return clock.getCurrentTimeMs() } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/TimeProviderExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/TimeProviderExt.kt new file mode 100644 index 0000000000..35efbfc8c9 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/time/TimeProviderExt.kt @@ -0,0 +1,21 @@ +/* + * 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 + +import com.datadog.android.api.context.TimeInfo +import com.datadog.android.internal.time.TimeProvider +import java.util.concurrent.TimeUnit + +internal fun TimeProvider.composeTimeInfo(): TimeInfo { + val deviceTimeMs = getDeviceTimestamp() + val serverTimeMs = getServerTimestamp() + return TimeInfo( + deviceTimeNs = TimeUnit.MILLISECONDS.toNanos(deviceTimeMs), + serverTimeNs = TimeUnit.MILLISECONDS.toNanos(serverTimeMs), + serverTimeOffsetNs = TimeUnit.MILLISECONDS.toNanos(serverTimeMs - deviceTimeMs), + serverTimeOffsetMs = serverTimeMs - deviceTimeMs + ) +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt index 8912f4e350..3d3e66789e 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt @@ -1124,6 +1124,21 @@ internal class DatadogCoreTest { assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) } + @Test + fun `M provide app uptime W appUptimeNs()`( + @LongForgery(min = 0L) fakeAppUptimeNs: Long + ) { + // Given + testedCore.coreFeature = mock() + whenever(testedCore.coreFeature.appUptimeNs) doReturn fakeAppUptimeNs + + // When + val appStartTimeNs = testedCore.appUptimeNs + + // Then + assertThat(appStartTimeNs).isEqualTo(fakeAppUptimeNs) + } + @Test fun `M return tracking consent W trackingConsent()`( @Forgery fakeTrackingConsent: TrackingConsent diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt index bb01164101..38c9725aba 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/time/DefaultAppStartTimeProviderTest.kt @@ -36,10 +36,10 @@ class DefaultAppStartTimeProviderTest { whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion val diffMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() val startTimeNs = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(diffMs) + val testedProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) // WHEN - val timeProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) - val providedStartTime = timeProvider.appStartTimeNs + val providedStartTime = testedProvider.appStartTimeNs // THEN assertThat(providedStartTime) @@ -54,12 +54,54 @@ class DefaultAppStartTimeProviderTest { val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion val startTimeNs = DdRumContentProvider.createTimeNs + val testedProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) // WHEN - val timeProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) - val providedStartTime = timeProvider.appStartTimeNs + val providedStartTime = testedProvider.appStartTimeNs // THEN assertThat(providedStartTime).isEqualTo(startTimeNs) } + + @Test + fun `M return app uptime W appUptimeNs`( + @IntForgery(min = Build.VERSION_CODES.M) apiVersion: Int + ) { + // GIVEN + val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock { + on { version } doReturn apiVersion + } + val testedProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) + + // WHEN + val beforeNs = System.nanoTime() + val appStartTimeNs = testedProvider.appStartTimeNs + val uptime = testedProvider.appUptimeNs + val afterNs = System.nanoTime() + + // THEN + val expectedUptime = beforeNs - appStartTimeNs + assertThat(uptime) + .isGreaterThan(0) + .isCloseTo(expectedUptime, Offset.offset(TimeUnit.MILLISECONDS.toNanos(100))) + .isLessThanOrEqualTo(afterNs - appStartTimeNs) + } + + @Test + fun `M return increasing uptime W appUptimeNs called multiple times`( + @IntForgery(min = Build.VERSION_CODES.M) apiVersion: Int + ) { + // GIVEN + val mockBuildSdkVersionProvider: BuildSdkVersionProvider = mock() + whenever(mockBuildSdkVersionProvider.version) doReturn apiVersion + val testedProvider = DefaultAppStartTimeProvider(mockBuildSdkVersionProvider) + + // WHEN + val uptime1 = testedProvider.appUptimeNs + Thread.sleep(10) + val uptime2 = testedProvider.appUptimeNs + + // THEN + assertThat(uptime2).isGreaterThan(uptime1) + } } diff --git a/dd-sdk-android-internal/api/apiSurface b/dd-sdk-android-internal/api/apiSurface index 6777c73e6f..435d0bb31a 100644 --- a/dd-sdk-android-internal/api/apiSurface +++ b/dd-sdk-android-internal/api/apiSurface @@ -92,7 +92,6 @@ class com.datadog.android.internal.thread.NamedRunnable : NamedExecutionUnit, Ru class com.datadog.android.internal.thread.NamedCallable : NamedExecutionUnit, java.util.concurrent.Callable constructor(String, java.util.concurrent.Callable) class com.datadog.android.internal.time.DefaultTimeProvider : TimeProvider - override fun getDeviceTimestamp(): Long override fun getServerTimestamp(): Long override fun getServerOffsetNanos(): Long override fun getServerOffsetMillis(): Long diff --git a/dd-sdk-android-internal/api/dd-sdk-android-internal.api b/dd-sdk-android-internal/api/dd-sdk-android-internal.api index 74321d9102..22ef25cfc9 100644 --- a/dd-sdk-android-internal/api/dd-sdk-android-internal.api +++ b/dd-sdk-android-internal/api/dd-sdk-android-internal.api @@ -268,6 +268,10 @@ public abstract interface class com/datadog/android/internal/time/TimeProvider { public abstract fun getServerTimestamp ()J } +public final class com/datadog/android/internal/time/TimeProvider$DefaultImpls { + public static fun getDeviceTimestamp (Lcom/datadog/android/internal/time/TimeProvider;)J +} + public final class com/datadog/android/internal/utils/ByteArrayExtKt { public static final fun toHexString ([B)Ljava/lang/String; } diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt index b60ff61293..4bc9cab863 100644 --- a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/DefaultTimeProvider.kt @@ -11,7 +11,6 @@ package com.datadog.android.internal.time * The offsets are always 0. */ class DefaultTimeProvider : TimeProvider { - override fun getDeviceTimestamp(): Long = System.currentTimeMillis() override fun getServerTimestamp(): Long = System.currentTimeMillis() diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt index 030f550d52..f6f47e909c 100644 --- a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt +++ b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/time/TimeProvider.kt @@ -16,7 +16,7 @@ interface TimeProvider { /** * Returns the current device timestamp in milliseconds. */ - fun getDeviceTimestamp(): Long + fun getDeviceTimestamp(): Long = System.currentTimeMillis() /** * Returns the current server timestamp in milliseconds. diff --git a/detekt_custom_safe_calls.yml b/detekt_custom_safe_calls.yml index 68d9b12dcf..4b16f02654 100644 --- a/detekt_custom_safe_calls.yml +++ b/detekt_custom_safe_calls.yml @@ -56,9 +56,11 @@ datadog: - "android.os.Looper.getMainLooper()" - "android.os.Looper.setMessageLogging(android.util.Printer?)" - "android.os.Process.getStartElapsedRealtime()" + - "android.os.Process.getStartUptimeMillis()" - "android.os.Process.myPid()" - "android.os.SystemClock.elapsedRealtime()" - "android.os.SystemClock.elapsedRealtimeNanos()" + - "android.os.SystemClock.uptimeMillis()" - "android.os.StrictMode.allowThreadDiskReads()" - "android.os.StrictMode.allowThreadDiskWrites()" - "android.os.StrictMode.setThreadPolicy(android.os.StrictMode.ThreadPolicy?)" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt index fb8c1a1d29..400c778a14 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt @@ -38,6 +38,7 @@ import com.datadog.android.telemetry.model.TelemetryErrorEvent import com.datadog.android.telemetry.model.TelemetryUsageEvent import com.datadog.android.telemetry.model.TelemetryUsageEvent.ActionType import java.util.Locale +import java.util.concurrent.TimeUnit import com.datadog.android.telemetry.model.TelemetryConfigurationEvent.ViewTrackingStrategy as VTS @Suppress("TooManyFunctions") @@ -202,6 +203,7 @@ internal class TelemetryEventHandler( val resolvedAdditionalProperties = additionalProperties.orEmpty() .toMutableMap() .cleanUpInternalAttributes() + .addDiagnosticsAttributes() return TelemetryDebugEvent( dd = TelemetryDebugEvent.Dd(), @@ -248,6 +250,7 @@ internal class TelemetryEventHandler( val resolvedAdditionalProperties = additionalProperties.orEmpty() .toMutableMap() .cleanUpInternalAttributes() + .addDiagnosticsAttributes() return TelemetryErrorEvent( dd = TelemetryErrorEvent.Dd(), @@ -527,10 +530,17 @@ internal class TelemetryEventHandler( private fun Map.getFloat(key: LocalAttribute.Key) = get(key.toString()) as? Float - private fun Map.cleanUpInternalAttributes() = toMutableMap().apply { + private fun MutableMap.cleanUpInternalAttributes() = toMutableMap().apply { LocalAttribute.Key.values().forEach { key -> remove(key.toString()) } } + private fun MutableMap.addDiagnosticsAttributes() = apply { + put( + DIAGNOSTICS_PROCESS_UPTIME, + TimeUnit.NANOSECONDS.toMillis(sdkCore.appUptimeNs) + ) + } + // endregion internal enum class TracerApi { @@ -557,5 +567,10 @@ internal class TelemetryEventHandler( internal const val OKHTTP_INTERCEPTOR_SAMPLE_RATE = "okhttp_interceptor_sample_rate" internal const val OKHTTP_INTERCEPTOR_HEADER_TYPES = "okhttp_interceptor_header_types" + + // A name of the telemetry attribute set for all ERROR and DEBUG telemetry events (including metrics). + // The value of this attribute represents the number of milliseconds elapsed from the process start + // to the moment the telemetry event was recorded. + internal const val DIAGNOSTICS_PROCESS_UPTIME = "process_uptime" } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryDebugEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryDebugEventAssert.kt index 1cacacf1bd..36770157be 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryDebugEventAssert.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryDebugEventAssert.kt @@ -16,157 +16,141 @@ internal class TelemetryDebugEventAssert(actual: TelemetryDebugEvent) : TelemetryDebugEventAssert::class.java ) { - fun hasDate(expected: Long): TelemetryDebugEventAssert { + fun hasDate(expected: Long) = apply { assertThat(actual.date) .overridingErrorMessage( "Expected event data to have date $expected but was ${actual.date}" ) .isEqualTo(expected) - return this } - fun hasSource(expected: TelemetryDebugEvent.Source): TelemetryDebugEventAssert { + fun hasSource(expected: TelemetryDebugEvent.Source) = apply { assertThat(actual.source) .overridingErrorMessage( "Expected event data to have source $expected but was ${actual.source}" ) .isEqualTo(expected) - return this } - fun hasService(expected: String): TelemetryDebugEventAssert { + fun hasService(expected: String) = apply { assertThat(actual.service) .overridingErrorMessage( "Expected event data to have service $expected but was ${actual.service}" ) .isEqualTo(expected) - return this } - fun hasVersion(expected: String): TelemetryDebugEventAssert { + fun hasVersion(expected: String) = apply { assertThat(actual.version) .overridingErrorMessage( "Expected event data to have version $expected but was ${actual.version}" ) .isEqualTo(expected) - return this } - fun hasApplicationId(expected: String?): TelemetryDebugEventAssert { + fun hasApplicationId(expected: String?) = apply { assertThat(actual.application?.id) .overridingErrorMessage( "Expected event data to have" + " application.id $expected but was ${actual.application?.id}" ) .isEqualTo(expected) - return this } - fun hasSessionId(expected: String?): TelemetryDebugEventAssert { + fun hasSessionId(expected: String?) = apply { assertThat(actual.session?.id) .overridingErrorMessage( "Expected event data to have session.id $expected but was ${actual.session?.id}" ) .isEqualTo(expected) - return this } - fun hasViewId(expected: String?): TelemetryDebugEventAssert { + fun hasViewId(expected: String?) = apply { assertThat(actual.view?.id) .overridingErrorMessage( "Expected event data to have view.id $expected but was ${actual.view?.id}" ) .isEqualTo(expected) - return this } - fun hasActionId(expected: String?): TelemetryDebugEventAssert { + fun hasActionId(expected: String?) = apply { assertThat(actual.action?.id) .overridingErrorMessage( "Expected event data to have action.id $expected but was ${actual.action?.id}" ) .isEqualTo(expected) - return this } - fun hasMessage(expected: String): TelemetryDebugEventAssert { + fun hasMessage(expected: String) = apply { assertThat(actual.telemetry.message) .overridingErrorMessage( "Expected event data to have telemetry.message $expected" + " but was ${actual.telemetry.message}" ) .isEqualTo(expected) - return this } - fun hasAdditionalProperties(additionalProperties: Map): TelemetryDebugEventAssert { + fun hasAdditionalProperties(additionalProperties: Map) = apply { assertThat(actual.telemetry.additionalProperties) .overridingErrorMessage( "Expected event data to have telemetry.additionalProperties $additionalProperties" + " but was ${actual.telemetry.additionalProperties}" ) .isEqualTo(additionalProperties) - return this } - fun hasDeviceArchitecture(expected: String?): TelemetryDebugEventAssert { + fun hasDeviceArchitecture(expected: String?) = apply { assertThat(actual.telemetry.device?.architecture) .overridingErrorMessage( "Expected event data to have telemetry.device architecture $expected" + - " but was ${actual.telemetry.message}" + " but was ${actual.telemetry.device?.architecture}" ) .isEqualTo(expected) - return this } - fun hasDeviceModel(expected: String?): TelemetryDebugEventAssert { + fun hasDeviceModel(expected: String?) = apply { assertThat(actual.telemetry.device?.model) .overridingErrorMessage( "Expected event data to have telemetry.device model $expected" + - " but was ${actual.telemetry.message}" + " but was ${actual.telemetry.device?.model}" ) .isEqualTo(expected) - return this } - fun hasDeviceBrand(expected: String?): TelemetryDebugEventAssert { + fun hasDeviceBrand(expected: String?) = apply { assertThat(actual.telemetry.device?.brand) .overridingErrorMessage( "Expected event data to have telemetry.device brand $expected" + - " but was ${actual.telemetry.message}" + " but was ${actual.telemetry.device?.brand}" ) .isEqualTo(expected) - return this } - fun hasOsBuild(expected: String?): TelemetryDebugEventAssert { + fun hasOsBuild(expected: String?) = apply { assertThat(actual.telemetry.os?.build) .overridingErrorMessage( "Expected event data to have telemetry.os build $expected" + - " but was ${actual.telemetry.message}" + " but was ${actual.telemetry.os?.build}" ) .isEqualTo(expected) - return this } - fun hasOsName(expected: String?): TelemetryDebugEventAssert { + fun hasOsName(expected: String?) = apply { assertThat(actual.telemetry.os?.name) .overridingErrorMessage( "Expected event data to have telemetry.os name $expected" + - " but was ${actual.telemetry.message}" + " but was ${actual.telemetry.os?.name}" ) .isEqualTo(expected) - return this } - fun hasOsVersion(expected: String?): TelemetryDebugEventAssert { + fun hasOsVersion(expected: String?) = apply { assertThat(actual.telemetry.os?.version) .overridingErrorMessage( "Expected event data to have telemetry.os version $expected" + - " but was ${actual.telemetry.message}" + " but was ${actual.telemetry.os?.version}" ) .isEqualTo(expected) - return this } companion object { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt index e4101e0423..0874d15f8d 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt @@ -39,6 +39,7 @@ import com.datadog.android.telemetry.assertj.TelemetryConfigurationEventAssert.C import com.datadog.android.telemetry.assertj.TelemetryDebugEventAssert.Companion.assertThat import com.datadog.android.telemetry.assertj.TelemetryErrorEventAssert.Companion.assertThat import com.datadog.android.telemetry.assertj.TelemetryUsageEventAssert.Companion.assertThat +import com.datadog.android.telemetry.internal.TelemetryEventHandler.Companion.DIAGNOSTICS_PROCESS_UPTIME import com.datadog.android.telemetry.internal.TelemetryEventHandler.Companion.OKHTTP_INTERCEPTOR_HEADER_TYPES import com.datadog.android.telemetry.internal.TelemetryEventHandler.Companion.OKHTTP_INTERCEPTOR_SAMPLE_RATE import com.datadog.android.telemetry.model.TelemetryConfigurationEvent @@ -51,6 +52,7 @@ import com.datadog.tools.unit.forge.aThrowable import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -83,6 +85,7 @@ import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import org.mockito.stubbing.Answer import java.util.Locale +import java.util.concurrent.TimeUnit import kotlin.reflect.jvm.jvmName import com.datadog.android.telemetry.model.TelemetryConfigurationEvent.ViewTrackingStrategy as VTS @@ -135,6 +138,9 @@ internal class TelemetryEventHandlerTest { @StringForgery lateinit var fakeDeviceArchitecture: String + @LongForgery(min = 0L) + private var fakeAppUptimeNs: Long = 0L + @StringForgery lateinit var fakeDeviceBrand: String @@ -211,6 +217,7 @@ internal class TelemetryEventHandlerTest { callback.invoke(fakeDatadogContext, mockEventWriteScope) } whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.appUptimeNs) doReturn fakeAppUptimeNs testedTelemetryHandler = TelemetryEventHandler( mockSdkCore, @@ -1468,7 +1475,7 @@ internal class TelemetryEventHandlerTest { .hasSessionId(rumContext.sessionId) .hasViewId(rumContext.viewId) .hasActionId(rumContext.actionId) - .hasAdditionalProperties(internalDebugEvent.additionalProperties ?: emptyMap()) + .hasAdditionalProperties(internalDebugEvent.additionalProperties?.withDiagnosticAttributes() ?: emptyMap()) .hasDeviceArchitecture(fakeDeviceArchitecture) .hasDeviceBrand(fakeDeviceBrand) .hasDeviceModel(fakeDeviceModel) @@ -1493,7 +1500,10 @@ internal class TelemetryEventHandlerTest { .hasSessionId(rumContext.sessionId) .hasViewId(rumContext.viewId) .hasActionId(rumContext.actionId) - .hasAdditionalProperties(internalMetricEvent.additionalProperties ?: emptyMap()) + .hasAdditionalProperties( + internalMetricEvent.additionalProperties?.withDiagnosticAttributes() + ?: emptyMap() + ) .hasDeviceArchitecture(fakeDeviceArchitecture) .hasDeviceBrand(fakeDeviceBrand) .hasDeviceModel(fakeDeviceModel) @@ -1528,7 +1538,7 @@ internal class TelemetryEventHandlerTest { .hasOsBuild(fakeOsBuildId) .hasOsName(fakeOsName) .hasOsVersion(fakeOsVersion) - .hasAdditionalProperties(internalErrorEvent.additionalProperties ?: emptyMap()) + .hasAdditionalProperties(internalErrorEvent.additionalProperties?.withDiagnosticAttributes() ?: emptyMap()) } private fun assertErrorEventMatchesInternalEvent( @@ -1617,6 +1627,11 @@ internal class TelemetryEventHandlerTest { ) } + private fun Map.withDiagnosticAttributes(): Map { + val processUptime = TimeUnit.NANOSECONDS.toMillis(fakeAppUptimeNs) + return this + mapOf(DIAGNOSTICS_PROCESS_UPTIME to processUptime) + } + // endregion companion object {