diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fea1f153..4b990b193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Support ProGuard mapping tasks when R8 is enabled with the AGP app `optimization.enable` DSL ([#1376](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1376)) +### Performance + +- Eliminate reflection for known optional Sentry SDK class-availability checks, reducing SDK initialization time by about 1% in an absent-heavy startup benchmark ([#1375](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1375)) + - This optimization is enabled by default. If it causes problems, disable it with `sentry.runtimeOptimizations.enabled = false`. + ### Dependencies - Bump Android SDK from v8.51.0 to v8.52.0 ([#1378](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1378)) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt index a97384a42..b4af24b0c 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt @@ -19,7 +19,9 @@ import io.sentry.android.gradle.SentryTasksProvider.getAssembleTaskProvider import io.sentry.android.gradle.SentryTasksProvider.getBundleTask import io.sentry.android.gradle.SentryTasksProvider.getMappingFileProvider import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.instrumentation.SentrySdkOptimizationClassVisitorFactory import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory +import io.sentry.android.gradle.instrumentation.resolveClassAvailability import io.sentry.android.gradle.services.SentryModulesService import io.sentry.android.gradle.snapshot.GenerateSnapshotTestsTask import io.sentry.android.gradle.sourcecontext.OutputPaths @@ -177,35 +179,48 @@ fun ApplicationAndroidComponentsExtension.configure( } } - if (extension.tracingInstrumentation.enabled.get()) { - /** - * We detect sentry-android SDK version using configurations.incoming.afterResolve. This is - * guaranteed to be executed BEFORE any of the build tasks/transforms are started. - * - * After detecting the sdk state, we use Gradle's shared build service to persist the state - * between builds and also during a single build, because transforms are run in parallel. - */ - val sentryModulesService = + val runtimeOptimizationsEnabled = extension.runtimeOptimizations.enabled.get() + val tracingInstrumentationEnabled = extension.tracingInstrumentation.enabled.get() + // Both visitor factories need the resolved dependency graph. + val modulesService = + if (runtimeOptimizationsEnabled || tracingInstrumentationEnabled) { SentryModulesService.register( - project, - extension.tracingInstrumentation.features, - extension.tracingInstrumentation.logcat.enabled, - extension.includeSourceContext, - extension.dexguardEnabled, - extension.tracingInstrumentation.appStart.enabled, + project, + extension.tracingInstrumentation.features, + extension.tracingInstrumentation.logcat.enabled, + extension.includeSourceContext, + extension.dexguardEnabled, + extension.tracingInstrumentation.appStart.enabled, + ) + .also { + // Keep the service alive after configuration so instrumentation can read it. + buildEvents.onTaskCompletion(it) + } + } else { + null + } + + val modules = + modulesService?.let { + project.collectModules("${variant.name}RuntimeClasspath", variant.name, it) + } + + if (runtimeOptimizationsEnabled) { + variant.instrumentation.transformClassesWith( + SentrySdkOptimizationClassVisitorFactory::class.java, + InstrumentationScope.ALL, + ) { params -> + params.classAvailability.setDisallowChanges( + checkNotNull(modules).map(::resolveClassAvailability).orElse(emptyMap()) ) - /** - * We have to register SentryModulesService as a build event listener, so it will not be - * discarded after the configuration phase (where we store the collected dependencies), and - * will be passed down to the InstrumentationFactory - */ - buildEvents.onTaskCompletion(sentryModulesService) - - project.collectModules( - "${variant.name}RuntimeClasspath", - variant.name, - sentryModulesService, + } + variant.instrumentation.setAsmFramesComputationMode( + FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS ) + } + + if (tracingInstrumentationEnabled) { + val tracingModulesService = checkNotNull(modulesService) variant.configureInstrumentation( SpanAddingClassVisitorFactory::class.java, @@ -219,7 +234,7 @@ fun ApplicationAndroidComponentsExtension.configure( params.debug.setDisallowChanges(extension.tracingInstrumentation.debug.get()) params.logcatMinLevel.setDisallowChanges(extension.tracingInstrumentation.logcat.minLevel) - params.sentryModulesService.setDisallowChanges(sentryModulesService) + params.sentryModulesService.setDisallowChanges(tracingModulesService) params.features.setDisallowChanges(extension.tracingInstrumentation.features) params.logcatEnabled.setDisallowChanges(extension.tracingInstrumentation.logcat.enabled) params.appStartEnabled.setDisallowChanges( @@ -235,7 +250,7 @@ fun ApplicationAndroidComponentsExtension.configure( project, extension, sentryTelemetryProvider, - sentryModulesService, + tracingModulesService, variant.name, ) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/RuntimeOptimizationsExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/RuntimeOptimizationsExtension.kt new file mode 100644 index 000000000..e22fef4f3 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/RuntimeOptimizationsExtension.kt @@ -0,0 +1,12 @@ +package io.sentry.android.gradle.extensions + +import javax.inject.Inject +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +open class RuntimeOptimizationsExtension @Inject constructor(objects: ObjectFactory) { + /** + * Enables runtime optimizations of the Sentry SDK at the cost of build time. Defaults to true. + */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt index 7cd7daf87..813882981 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt @@ -97,6 +97,14 @@ abstract class SentryPluginExtension @Inject constructor(objects: ObjectFactory) tracingInstrumentationAction.execute(tracingInstrumentation) } + val runtimeOptimizations: RuntimeOptimizationsExtension = + objects.newInstance(RuntimeOptimizationsExtension::class.java) + + /** Configure runtime optimizations of the Sentry SDK. Default configuration is enabled. */ + fun runtimeOptimizations(runtimeOptimizationsAction: Action) { + runtimeOptimizationsAction.execute(runtimeOptimizations) + } + val autoInstallation: AutoInstallExtension = objects.newInstance(AutoInstallExtension::class.java) /** Configure the auto installation feature. */ diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt new file mode 100644 index 000000000..2103416e0 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt @@ -0,0 +1,142 @@ +package io.sentry.android.gradle.instrumentation + +import io.sentry.android.gradle.SentryPlugin +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Injects build-time knowledge of optional classes into `LoadClass`. + * + * `classAvailability` maps class names probed during SDK initialization to whether their owning + * dependency is present. Before instrumentation the field is uninitialized: + * ```java + * static Map classAvailability; + * ``` + * + * After instrumentation the generated static initializer is equivalent to: + * ```java + * classAvailability = new HashMap<>(); + * classAvailability.put("timber.log.Timber", true); + * ``` + * + * Known entries avoid reflection; omitted class names still fall back to it. SDK versions without + * the field are left unchanged. + */ +internal class LoadClassClassVisitor( + apiVersion: Int, + nextClassVisitor: ClassVisitor, + private val classAvailability: Map, +) : ClassVisitor(apiVersion, nextClassVisitor) { + private var hasAvailabilityField = false + private var hasStaticInitializer = false + + override fun visitField( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + value: Any?, + ): FieldVisitor? { + if (name == AVAILABILITY_FIELD && descriptor == MAP_DESCRIPTOR) { + hasAvailabilityField = true + } + return super.visitField(access, name, descriptor, signature, value) + } + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): MethodVisitor { + val visitor = super.visitMethod(access, name, descriptor, signature, exceptions) + if (name != STATIC_INITIALIZER || descriptor != VOID_METHOD_DESCRIPTOR) { + return visitor + } + + hasStaticInitializer = true + return object : MethodVisitor(api, visitor) { + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.RETURN && hasAvailabilityField) { + injectClassAvailability(this) + } + super.visitInsn(opcode) + } + } + } + + override fun visitEnd() { + if (!hasAvailabilityField) { + SentryPlugin.logger.info( + "Sentry SDK runtime reflection checks were not optimized because the current SDK version does not support this optimization." + ) + } + if (hasAvailabilityField && !hasStaticInitializer) { + val visitor = + super.visitMethod( + Opcodes.ACC_STATIC, + STATIC_INITIALIZER, + VOID_METHOD_DESCRIPTOR, + null, + null, + ) + visitor.visitCode() + injectClassAvailability(visitor) + visitor.visitInsn(Opcodes.RETURN) + // Triggers ASM frame/max computation; arguments are ignored in compute mode. + visitor.visitMaxs(0, 0) + visitor.visitEnd() + } + super.visitEnd() + } + + private fun injectClassAvailability(visitor: MethodVisitor) { + visitor.visitTypeInsn(Opcodes.NEW, HASH_MAP_NAME) + visitor.visitInsn(Opcodes.DUP) + visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, HASH_MAP_NAME, "", "()V", false) + visitor.visitFieldInsn( + Opcodes.PUTSTATIC, + LOAD_CLASS_INTERNAL_NAME, + AVAILABILITY_FIELD, + MAP_DESCRIPTOR, + ) + + classAvailability.forEach { (className, available) -> + visitor.visitFieldInsn( + Opcodes.GETSTATIC, + LOAD_CLASS_INTERNAL_NAME, + AVAILABILITY_FIELD, + MAP_DESCRIPTOR, + ) + visitor.visitLdcInsn(className) + visitor.visitInsn(if (available) Opcodes.ICONST_1 else Opcodes.ICONST_0) + visitor.visitMethodInsn( + Opcodes.INVOKESTATIC, + "java/lang/Boolean", + "valueOf", + "(Z)Ljava/lang/Boolean;", + false, + ) + visitor.visitMethodInsn( + Opcodes.INVOKEINTERFACE, + "java/util/Map", + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + true, + ) + visitor.visitInsn(Opcodes.POP) + } + } + + private companion object { + const val LOAD_CLASS_INTERNAL_NAME = "io/sentry/util/LoadClass" + const val AVAILABILITY_FIELD = "classAvailability" + const val MAP_DESCRIPTOR = "Ljava/util/Map;" + const val HASH_MAP_NAME = "java/util/HashMap" + const val STATIC_INITIALIZER = "" + const val VOID_METHOD_DESCRIPTOR = "()V" + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt new file mode 100644 index 000000000..5f5cb497c --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt @@ -0,0 +1,74 @@ +package io.sentry.android.gradle.instrumentation + +import com.android.build.api.instrumentation.AsmClassVisitorFactory +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData +import com.android.build.api.instrumentation.InstrumentationParameters +import io.sentry.android.gradle.util.SentryModules +import org.gradle.api.artifacts.ModuleIdentifier +import org.gradle.api.internal.artifacts.DefaultModuleIdentifier +import org.gradle.api.provider.MapProperty +import org.gradle.api.tasks.Input +import org.objectweb.asm.ClassVisitor + +abstract class SentrySdkOptimizationClassVisitorFactory : + AsmClassVisitorFactory { + + interface SdkOptimizationParameters : InstrumentationParameters { + @get:Input val classAvailability: MapProperty + } + + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor, + ): ClassVisitor { + return LoadClassClassVisitor( + instrumentationContext.apiVersion.get(), + nextClassVisitor, + parameters.get().classAvailability.get(), + ) + } + + // Empty availability means the runtime classpath is unknown. Skip this transformation so + // LoadClass falls back to reflection. + override fun isInstrumentable(classData: ClassData): Boolean = + classData.className == LOAD_CLASS_NAME && parameters.get().classAvailability.get().isNotEmpty() + + internal companion object { + const val LOAD_CLASS_NAME = "io.sentry.util.LoadClass" + + val CLASS_MODULES: Map> = + sortedMapOf( + "androidx.compose.ui.node.Owner" to + setOf(module("androidx.compose.ui", "ui"), module("androidx.compose.ui", "ui-android")), + "androidx.core.view.ScrollingView" to setOf(module("androidx.core", "core")), + "androidx.fragment.app.FragmentManager\$FragmentLifecycleCallbacks" to + setOf(module("androidx.fragment", "fragment")), + "androidx.lifecycle.Lifecycle" to + setOf( + module("androidx.lifecycle", "lifecycle-common"), + module("androidx.lifecycle", "lifecycle-common-jvm"), + ), + "io.sentry.android.distribution.DistributionIntegration" to + setOf(SentryModules.SENTRY_ANDROID_DISTRIBUTION), + "io.sentry.android.fragment.FragmentLifecycleIntegration" to + setOf(SentryModules.SENTRY_ANDROID_FRAGMENT), + "io.sentry.android.replay.ReplayIntegration" to setOf(SentryModules.SENTRY_ANDROID_REPLAY), + "io.sentry.android.timber.SentryTimberIntegration" to + setOf(SentryModules.SENTRY_ANDROID_TIMBER), + "io.sentry.compose.gestures.ComposeGestureTargetLocator" to + setOf(SentryModules.SENTRY_ANDROID_COMPOSE, module("io.sentry", "sentry-compose")), + "io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter" to + setOf(SentryModules.SENTRY_ANDROID_COMPOSE, module("io.sentry", "sentry-compose")), + "timber.log.Timber" to setOf(module("com.jakewharton.timber", "timber")), + ) + + private fun module(group: String, name: String): ModuleIdentifier = + DefaultModuleIdentifier.newId(group, name) + } +} + +internal fun resolveClassAvailability(modules: Set): Map = + SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.mapValues { (_, owners) -> + owners.any { it in modules } + } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryModulesCollector.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryModulesCollector.kt index ba8424625..a0d161c7e 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryModulesCollector.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/SentryModulesCollector.kt @@ -12,7 +12,7 @@ fun Project.collectModules( configurationName: String, variantName: String, sentryModulesService: Provider, -) { +): Provider> { val configProvider = try { configurations.named(configurationName) @@ -20,7 +20,7 @@ fun Project.collectModules( logger.warn { "Unable to find configuration $configurationName for variant $variantName." } sentryModulesService.get().sentryModules = emptyMap() sentryModulesService.get().externalModules = emptyMap() - return + return provider> { null } } configProvider.configure { configuration -> @@ -40,6 +40,12 @@ fun Project.collectModules( sentryModulesService.get().externalModules = externalModules } } + + return configProvider.map { configuration -> + configuration.incoming.resolutionResult.allComponents + .mapNotNull { it.moduleVersion?.module } + .toSet() + } } private fun Set.versionMap( diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt index c9919b17e..19f41a239 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt @@ -60,6 +60,8 @@ internal object SentryModules { DefaultModuleIdentifier.newId("io.sentry", "sentry-android-navigation") internal val SENTRY_ANDROID_TIMBER = DefaultModuleIdentifier.newId("io.sentry", "sentry-android-timber") + internal val SENTRY_ANDROID_REPLAY = + DefaultModuleIdentifier.newId("io.sentry", "sentry-android-replay") internal val SENTRY_ANDROID_DISTRIBUTION = DefaultModuleIdentifier.newId("io.sentry", "sentry-android-distribution") internal val SENTRY_OKHTTP = DefaultModuleIdentifier.newId("io.sentry", "sentry-okhttp") diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt new file mode 100644 index 000000000..723192d31 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt @@ -0,0 +1,141 @@ +package io.sentry.android.gradle.instrumentation + +import com.google.common.truth.Truth.assertThat +import io.sentry.android.gradle.util.SentryModules +import org.gradle.api.internal.artifacts.DefaultModuleIdentifier +import org.junit.Test +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes + +class LoadClassClassVisitorTest { + @Test + fun `resolves every known class from the module graph`() { + val modules = SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.values.flatten().toSet() + + assertThat(resolveClassAvailability(modules).values).doesNotContain(false) + } + + @Test + fun `marks every known class as unavailable when its module is absent`() { + val availability = resolveClassAvailability(emptySet()) + + assertThat(availability).hasSize(SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.size) + assertThat(availability.values).doesNotContain(true) + } + + @Test + fun `supports platform-specific module coordinates`() { + val modules = + setOf( + DefaultModuleIdentifier.newId("androidx.compose.ui", "ui-android"), + DefaultModuleIdentifier.newId("androidx.lifecycle", "lifecycle-common-jvm"), + SentryModules.SENTRY_ANDROID_REPLAY, + ) + + assertThat(resolveClassAvailability(modules)) + .containsAtLeast( + "androidx.compose.ui.node.Owner", + true, + "androidx.lifecycle.Lifecycle", + true, + "io.sentry.android.replay.ReplayIntegration", + true, + ) + } + + @Test + fun `injects availability map when static initializer is missing`() { + val availability = mapOf("present.Class" to true, "missing.Class" to false) + + val clazz = load(transformClass(hasStaticInitializer = false, availability = availability)) + + assertThat(readAvailability(clazz)).containsExactlyEntriesIn(availability) + } + + @Test + fun `injects availability map and preserves existing static initializer`() { + val availability = mapOf("present.Class" to true) + + val clazz = load(transformClass(hasStaticInitializer = true, availability = availability)) + + assertThat(clazz.getDeclaredField("marker").getInt(null)).isEqualTo(7) + assertThat(readAvailability(clazz)).containsExactlyEntriesIn(availability) + } + + @Test + fun `does not modify SDK versions without the availability field`() { + val clazz = load(transformClass(hasAvailabilityField = false)) + + assertThat(clazz.declaredFields.map { it.name }).doesNotContain("classAvailability") + } + + private fun transformClass( + hasAvailabilityField: Boolean = true, + hasStaticInitializer: Boolean = false, + availability: Map = emptyMap(), + ): ByteArray { + val original = ClassWriter(0) + original.visit( + Opcodes.V1_8, + Opcodes.ACC_PUBLIC, + LOAD_CLASS_INTERNAL_NAME, + null, + "java/lang/Object", + null, + ) + if (hasAvailabilityField) { + original + .visitField( + Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC, + "classAvailability", + "Ljava/util/Map;", + null, + null, + ) + .visitEnd() + } + if (hasStaticInitializer) { + original + .visitField(Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, "marker", "I", null, null) + .visitEnd() + original.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null).apply { + visitCode() + visitIntInsn(Opcodes.BIPUSH, 7) + visitFieldInsn(Opcodes.PUTSTATIC, LOAD_CLASS_INTERNAL_NAME, "marker", "I") + visitInsn(Opcodes.RETURN) + visitMaxs(1, 0) + visitEnd() + } + } + original.visitEnd() + + val reader = ClassReader(original.toByteArray()) + val writer = ClassWriter(reader, ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + reader.accept(LoadClassClassVisitor(Opcodes.ASM9, writer, availability), 0) + return writer.toByteArray() + } + + private fun load(bytes: ByteArray): Class<*> = + object : ClassLoader(javaClass.classLoader) { + fun define(): Class<*> = + defineClass( + SentrySdkOptimizationClassVisitorFactory.LOAD_CLASS_NAME, + bytes, + 0, + bytes.size, + ) + } + .define() + + @Suppress("UNCHECKED_CAST") + private fun readAvailability(clazz: Class<*>): Map = + clazz.getDeclaredField("classAvailability").run { + isAccessible = true + get(null) as Map + } + + private companion object { + const val LOAD_CLASS_INTERNAL_NAME = "io/sentry/util/LoadClass" + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt index 4417328be..98e1b8e1d 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt @@ -9,11 +9,12 @@ class SentryPluginCheckAndroidSdkTest : BaseSentryPluginTest(BuildConfig.AgpVersion, GradleVersion.current().version) { @Test - fun `when tracingInstrumentation is disabled does not check sentry-android sdk state`() { + fun `when instrumentation is disabled does not check sentry-android sdk state`() { appBuildFile.appendText( // language=Groovy """ sentry.tracingInstrumentation.enabled = false + sentry.runtimeOptimizations.enabled = false ${captureSdkState()} """ diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt index a5198b6c4..1bc29f880 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt @@ -490,13 +490,28 @@ class SentryPluginTest : @Test fun `skips tracing instrumentation if tracingInstrumentation is disabled`() { - applyTracingInstrumentation(false, appStart = false, logcat = false) + applyTracingInstrumentation( + tracingInstrumentation = false, + runtimeOptimizations = false, + appStart = false, + logcat = false, + ) val build = runner.appendArguments(":app:assembleRelease", "--dry-run").build() assertFalse(":app:transformReleaseClassesWithAsm" in build.output) } + @Test + fun `registers runtime optimizations independently from tracing instrumentation`() { + applyTracingInstrumentation(false, appStart = false, logcat = false) + + val build = runner.appendArguments(":app:assembleRelease", "--info").build() + + assertTrue(":app:transformReleaseClassesWithAsm" in build.output) + assertTrue("Detected Sentry modules" in build.output) + } + @Test fun `register tracing instrumentation if tracingInstrumentation is enabled`() { applyTracingInstrumentation() @@ -1216,6 +1231,7 @@ class SentryPluginTest : private fun applyTracingInstrumentation( tracingInstrumentation: Boolean = true, + runtimeOptimizations: Boolean = true, features: Set = emptySet(), logcat: Boolean = false, appStart: Boolean = false, @@ -1236,6 +1252,9 @@ class SentryPluginTest : sentry { autoUploadProguardMapping = false + runtimeOptimizations { + enabled = $runtimeOptimizations + } tracingInstrumentation { forceInstrumentDependencies = $forceInstrumentDependencies enabled = $tracingInstrumentation diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt index 71481c042..2ace513b5 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesCollectorTest.kt @@ -1,5 +1,6 @@ package io.sentry.android.gradle.util +import com.google.common.truth.Truth.assertThat import io.sentry.android.gradle.extensions.InstrumentationFeature import io.sentry.android.gradle.instrumentation.fakes.CapturingTestLogger import io.sentry.android.gradle.services.SentryModulesService @@ -84,14 +85,16 @@ class SentryModulesCollectorTest { private val fixture = Fixture() @Test - fun `configuration cannot be found - logs a warning and the modules list is empty`() { + fun `configuration cannot be found - logs a warning and does not provide modules`() { val project = fixture.getSut(testProjectDir.root) - project.collectModules( - "releaseRuntimeClasspath", - "release", - fixture.sentryModulesServiceProvider, - ) + val modules = + project.collectModules( + "releaseRuntimeClasspath", + "release", + fixture.sentryModulesServiceProvider, + ) assertTrue { fixture.getSentryModules().isEmpty() } + assertThat(modules.isPresent).isFalse() assertTrue { fixture.logger.capturedMessage == "[sentry] Unable to find configuration releaseRuntimeClasspath for variant release." @@ -189,13 +192,15 @@ class SentryModulesCollectorTest { } val project = fixture.getSut(testProjectDir.root, dependencies = setOf(sqliteDep)) - project.collectModules( - fixture.configurationName, - fixture.variantName, - fixture.sentryModulesServiceProvider, - ) + val modules = + project.collectModules( + fixture.configurationName, + fixture.variantName, + fixture.sentryModulesServiceProvider, + ) assertTrue { fixture.getExternalModules()[moduleIdentifier]!! == SemVer.parse(version) } + assertThat(modules.get()).containsExactly(moduleIdentifier) } @Test