diff --git a/features/dd-sdk-android-profiling/api/apiSurface b/features/dd-sdk-android-profiling/api/apiSurface index 54fd4226cf..2c27c31db1 100644 --- a/features/dd-sdk-android-profiling/api/apiSurface +++ b/features/dd-sdk-android-profiling/api/apiSurface @@ -14,6 +14,7 @@ data class com.datadog.android.profiling.ProfilingConfiguration fun setApplicationLaunchSampleRate(Float): Builder fun setContinuousSampleRate(Float): Builder fun useCustomEndpoint(String): Builder + fun setAnrTriggerEnabled(Boolean): Builder fun build(): ProfilingConfiguration companion object val DEFAULT: ProfilingConfiguration diff --git a/features/dd-sdk-android-profiling/api/dd-sdk-android-profiling.api b/features/dd-sdk-android-profiling/api/dd-sdk-android-profiling.api index 2745e13131..29c15f5b47 100644 --- a/features/dd-sdk-android-profiling/api/dd-sdk-android-profiling.api +++ b/features/dd-sdk-android-profiling/api/dd-sdk-android-profiling.api @@ -23,8 +23,8 @@ public final class com/datadog/android/profiling/Profiling { public final class com/datadog/android/profiling/ProfilingConfiguration { public static final field Companion Lcom/datadog/android/profiling/ProfilingConfiguration$Companion; - public final fun copy (Ljava/lang/String;FF)Lcom/datadog/android/profiling/ProfilingConfiguration; - public static synthetic fun copy$default (Lcom/datadog/android/profiling/ProfilingConfiguration;Ljava/lang/String;FFILjava/lang/Object;)Lcom/datadog/android/profiling/ProfilingConfiguration; + public final fun copy (Ljava/lang/String;FFZ)Lcom/datadog/android/profiling/ProfilingConfiguration; + public static synthetic fun copy$default (Lcom/datadog/android/profiling/ProfilingConfiguration;Ljava/lang/String;FFZILjava/lang/Object;)Lcom/datadog/android/profiling/ProfilingConfiguration; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -33,6 +33,7 @@ public final class com/datadog/android/profiling/ProfilingConfiguration { public final class com/datadog/android/profiling/ProfilingConfiguration$Builder { public fun ()V public final fun build ()Lcom/datadog/android/profiling/ProfilingConfiguration; + public final fun setAnrTriggerEnabled (Z)Lcom/datadog/android/profiling/ProfilingConfiguration$Builder; public final fun setApplicationLaunchSampleRate (F)Lcom/datadog/android/profiling/ProfilingConfiguration$Builder; public final fun setContinuousSampleRate (F)Lcom/datadog/android/profiling/ProfilingConfiguration$Builder; public final fun useCustomEndpoint (Ljava/lang/String;)Lcom/datadog/android/profiling/ProfilingConfiguration$Builder; diff --git a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/ProfilingConfiguration.kt b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/ProfilingConfiguration.kt index 54186d2c4b..bda54e7b7c 100644 --- a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/ProfilingConfiguration.kt +++ b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/ProfilingConfiguration.kt @@ -6,6 +6,7 @@ package com.datadog.android.profiling +import android.os.ProfilingTrigger import androidx.annotation.FloatRange /** @@ -15,7 +16,8 @@ import androidx.annotation.FloatRange data class ProfilingConfiguration internal constructor( internal val customEndpointUrl: String?, internal val applicationLaunchSampleRate: Float, - internal val continuousSampleRate: Float + internal val continuousSampleRate: Float, + internal val anrTriggerEnabled: Boolean = DEFAULT_ANR_TRIGGER_ENABLED ) { /** @@ -26,6 +28,7 @@ data class ProfilingConfiguration internal constructor( private var customEndpointUrl: String? = null private var applicationLaunchSampleRate: Float = DEFAULT_APPLICATION_LAUNCH_SAMPLE_RATE private var continuousSampleRate: Float = DEFAULT_CONTINUOUS_SAMPLE_RATE + private var anrTriggerEnabled: Boolean = DEFAULT_ANR_TRIGGER_ENABLED /** * Sets the sampling rate for Application Launch profiling. It will be applied on the next application launch. @@ -65,6 +68,19 @@ data class ProfilingConfiguration internal constructor( return this } + /** + * Enables or disables the ANR triggered profiling. + * + * When enabled, the SDK registers [ProfilingTrigger.TRIGGER_TYPE_ANR] so that a + * profile is captured automatically when an ANR occurs. + * + * @param enabled `true` to enable ANR-triggered profiling (default), `false` to disable it. + */ + fun setAnrTriggerEnabled(enabled: Boolean): Builder { + this.anrTriggerEnabled = enabled + return this + } + /** * Builds a [ProfilingConfiguration] based on the current state of this Builder. */ @@ -72,7 +88,8 @@ data class ProfilingConfiguration internal constructor( return ProfilingConfiguration( customEndpointUrl = customEndpointUrl, applicationLaunchSampleRate = applicationLaunchSampleRate, - continuousSampleRate = continuousSampleRate + continuousSampleRate = continuousSampleRate, + anrTriggerEnabled = anrTriggerEnabled ) } } @@ -86,6 +103,12 @@ data class ProfilingConfiguration internal constructor( */ internal const val DEFAULT_CONTINUOUS_SAMPLE_RATE: Float = 15f + /** + * ANR-triggered profiling is enabled by default to preserve the existing behavior, + * making this an opt-out capability. + */ + internal const val DEFAULT_ANR_TRIGGER_ENABLED: Boolean = true + /** * A default configuration for the Profiling feature. */ diff --git a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/NoOpProfiler.kt b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/NoOpProfiler.kt index c51cd03ce3..11798ce1a6 100644 --- a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/NoOpProfiler.kt +++ b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/NoOpProfiler.kt @@ -39,6 +39,8 @@ internal class NoOpProfiler : Profiler { override fun unregisterProfilingCallback(appContext: Context) = Unit + override fun setAnrTriggerEnabled(enabled: Boolean) = Unit + override fun setExtendLaunchSession(extend: Boolean) = Unit override fun resolveProfilingPackageVersionCode(appContext: Context) = Unit diff --git a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/Profiler.kt b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/Profiler.kt index 31db835ad7..1241522d71 100644 --- a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/Profiler.kt +++ b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/Profiler.kt @@ -34,6 +34,8 @@ internal interface Profiler { fun unregisterProfilingCallback(appContext: Context) + fun setAnrTriggerEnabled(enabled: Boolean) + /** * Controls whether an app launch profiling session should extend past the 10-second * TTID threshold. Set to `true` when continuous profiling is enabled for the session diff --git a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/ProfilingFeature.kt b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/ProfilingFeature.kt index 1ba15be0c6..b2ad385fc7 100644 --- a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/ProfilingFeature.kt +++ b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/ProfilingFeature.kt @@ -95,6 +95,7 @@ internal class ProfilingFeature( this.timeProvider.delegate = sdkCore.timeProvider resolveProfilingPackageVersionCode(appContext) this.internalLogger = sdkCore.internalLogger + setAnrTriggerEnabled(configuration.anrTriggerEnabled) registerProfilingCallback(appContext, this@ProfilingFeature) } ProfilingStorage.setSampleRate(appContext, configuration.applicationLaunchSampleRate) diff --git a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/perfetto/PerfettoProfiler.kt b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/perfetto/PerfettoProfiler.kt index 20417056ad..f0adcd2104 100644 --- a/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/perfetto/PerfettoProfiler.kt +++ b/features/dd-sdk-android-profiling/src/main/java/com/datadog/android/profiling/internal/perfetto/PerfettoProfiler.kt @@ -96,6 +96,9 @@ internal class PerfettoProfiler( profilingTelemetry.internalLogger = value } + @Volatile + internal var anrTriggerEnabled: Boolean = true + internal val anrListener = AnrListener { event -> callback?.onAnrDetected(event) } @@ -253,7 +256,7 @@ internal class PerfettoProfiler( ) { synchronized(this) { this.callback = callback - if (buildSdkVersionProvider.isAtLeastBaklava) { + if (buildSdkVersionProvider.isAtLeastBaklava && anrTriggerEnabled) { anrTriggerRegistrar.register(appContext, anrListener) } } @@ -262,7 +265,7 @@ internal class PerfettoProfiler( override fun unregisterProfilingCallback(appContext: Context) { synchronized(this) { callback = null - if (buildSdkVersionProvider.isAtLeastBaklava) { + if (buildSdkVersionProvider.isAtLeastBaklava && anrTriggerEnabled) { anrTriggerRegistrar.unregister(appContext) } } @@ -272,6 +275,10 @@ internal class PerfettoProfiler( this.extendLaunchSession = extend } + override fun setAnrTriggerEnabled(enabled: Boolean) { + this.anrTriggerEnabled = enabled + } + override fun resolveProfilingPackageVersionCode(appContext: Context) { profilingPackageVersionCode(appContext) } diff --git a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/ProfilingFeatureTest.kt b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/ProfilingFeatureTest.kt index 31ce7bee63..f04b907707 100644 --- a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/ProfilingFeatureTest.kt +++ b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/ProfilingFeatureTest.kt @@ -477,6 +477,32 @@ internal class ProfilingFeatureTest { verify(mockSdkCore).setContextUpdateReceiver(testedFeature) } + @Test + fun `M propagate ANR trigger enabled flag W onInitialize()`() { + // Given + val config = fakeConfiguration.copy(anrTriggerEnabled = false) + testedFeature = ProfilingFeature(mockSdkCore, config, mockProfiler) + + // When + testedFeature.onInitialize(mockContext) + + // Then + verify(mockProfiler).setAnrTriggerEnabled(false) + } + + @Test + fun `M propagate ANR trigger enabled flag W onInitialize {enabled}`() { + // Given + val config = fakeConfiguration.copy(anrTriggerEnabled = true) + testedFeature = ProfilingFeature(mockSdkCore, config, mockProfiler) + + // When + testedFeature.onInitialize(mockContext) + + // Then + verify(mockProfiler).setAnrTriggerEnabled(true) + } + @Test fun `M ignore context update W onContextUpdate {non-RUM feature}`( @StringForgery fakeOtherFeatureName: String, diff --git a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/forge/ProfilingConfigurationForgeryFactory.kt b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/forge/ProfilingConfigurationForgeryFactory.kt index af54300807..de5bf6197e 100644 --- a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/forge/ProfilingConfigurationForgeryFactory.kt +++ b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/forge/ProfilingConfigurationForgeryFactory.kt @@ -20,7 +20,8 @@ class ProfilingConfigurationForgeryFactory : continuousSampleRate = forge.aFloat(min = 0f, max = 100f), customEndpointUrl = forge.aNullable { aStringMatching("http(s?)://[a-z]+\\.com/\\w+") - } + }, + anrTriggerEnabled = forge.aBool() ) } } diff --git a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/PerfettoProfilerTest.kt b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/PerfettoProfilerTest.kt index cc2e2ccdff..68b3b9ff34 100644 --- a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/PerfettoProfilerTest.kt +++ b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/PerfettoProfilerTest.kt @@ -1083,6 +1083,32 @@ class PerfettoProfilerTest { verify(mockAnrRegistrar, never()).register(any(), any()) } + @Test + fun `M not delegate to registrar W registerProfilingCallback {ANR trigger disabled}`() { + // Given + // Drop interactions recorded by the set-up call (which used the default enabled state). + reset(mockAnrRegistrar) + testedProfiler.setAnrTriggerEnabled(false) + + // When + testedProfiler.registerProfilingCallback(mockContext, mockProfilerCallback) + + // Then + verify(mockAnrRegistrar, never()).register(any(), any()) + } + + @Test + fun `M not delegate to registrar W unregisterProfilingCallback {ANR trigger disabled}`() { + // Given + testedProfiler.setAnrTriggerEnabled(false) + + // When + testedProfiler.unregisterProfilingCallback(mockContext) + + // Then + verify(mockAnrRegistrar, never()).unregister(any()) + } + @Test fun `M not delegate to registrar W unregisterProfilingCallback {SDK below BAKLAVA}`() { // Given diff --git a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationBuilderTest.kt b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationBuilderTest.kt index d850e9049b..eb48819e98 100644 --- a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationBuilderTest.kt +++ b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationBuilderTest.kt @@ -44,6 +44,7 @@ internal class ProfilingConfigurationBuilderTest { // Then assertThat(configuration.customEndpointUrl).isNull() assertThat(configuration.continuousSampleRate).isEqualTo(DEFAULT_CONTINUOUS_SAMPLE_RATE) + assertThat(configuration.anrTriggerEnabled).isTrue() } @Test @@ -71,4 +72,26 @@ internal class ProfilingConfigurationBuilderTest { // Then assertThat(configuration.customEndpointUrl).isEqualTo(endpoint) } + + @Test + fun `M build config with ANR trigger disabled W setAnrTriggerEnabled(false) and build()`() { + // When + val configuration = testedBuilder + .setAnrTriggerEnabled(false) + .build() + + // Then + assertThat(configuration.anrTriggerEnabled).isFalse() + } + + @Test + fun `M build config with ANR trigger enabled W setAnrTriggerEnabled(true) and build()`() { + // When + val configuration = testedBuilder + .setAnrTriggerEnabled(true) + .build() + + // Then + assertThat(configuration.anrTriggerEnabled).isTrue() + } } diff --git a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationTest.kt b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationTest.kt index 96cdd5a10e..84d6921d08 100644 --- a/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationTest.kt +++ b/features/dd-sdk-android-profiling/src/test/kotlin/com/datadog/android/profiling/internal/ProfilingConfigurationTest.kt @@ -30,6 +30,7 @@ internal class ProfilingConfigurationTest { // Then assertThat(config.customEndpointUrl).isNull() assertThat(config.applicationLaunchSampleRate).isEqualTo(15f) + assertThat(config.anrTriggerEnabled).isTrue() } @Test @@ -78,4 +79,18 @@ internal class ProfilingConfigurationTest { assertThat(modified.customEndpointUrl).isEqualTo(endpoint) assertThat(modified.applicationLaunchSampleRate).isEqualTo(sampleRate) } + + @Test + fun `M preserve ANR trigger flag W data class copy()`() { + // Given + val original = ProfilingConfiguration.Builder() + .setAnrTriggerEnabled(false) + .build() + + // When + val copied = original.copy(continuousSampleRate = 50f) + + // Then + assertThat(copied.anrTriggerEnabled).isFalse() + } }