diff --git a/CHANGELOG.md b/CHANGELOG.md index feecee85..a3b59efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Populate events with dependencies metadata ([#396](https://github.com/getsentry/sentry-android-gradle-plugin/pull/396)) +- Add auto-instrumentation for compose navigation ([#392](https://github.com/getsentry/sentry-android-gradle-plugin/pull/392)) ### Dependencies diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 070da917..4121e28a 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -27,7 +27,7 @@ object LibsVersion { const val JUNIT = "4.13.2" const val ASM = "7.0" // compatibility matrix -> https://developer.android.com/reference/tools/gradle-api/7.1/com/android/build/api/instrumentation/InstrumentationContext#apiversion const val SQLITE = "2.1.0" - const val SENTRY = "5.5.0" + const val SENTRY = "6.6.0" } object Libs { @@ -62,6 +62,12 @@ object Samples { const val recyclerView = "androidx.recyclerview:recyclerview:1.2.0" const val lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" const val appcompat = "androidx.appcompat:appcompat:1.2.0" + + const val composeRuntime = "androidx.compose.runtime:runtime:1.1.1" + const val composeNavigation = "androidx.navigation:navigation-compose:2.5.2" + const val composeActivity = "androidx.activity:activity-compose:1.4.0" + const val composeFoundation = "androidx.compose.foundation:foundation:1.2.1" + const val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.2.1" } object Coroutines { diff --git a/examples/android-instrumentation-sample/build.gradle.kts b/examples/android-instrumentation-sample/build.gradle.kts index 9a767546..4ddea1e2 100644 --- a/examples/android-instrumentation-sample/build.gradle.kts +++ b/examples/android-instrumentation-sample/build.gradle.kts @@ -42,6 +42,14 @@ android { jvmTarget = JavaVersion.VERSION_1_8.toString() } namespace = "io.sentry.samples.instrumentation" + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.1.1" + } } // useful, when we want to modify room-generated classes, and then compile them into .class files @@ -55,6 +63,12 @@ dependencies { implementation(Samples.AndroidX.lifecycle) implementation(Samples.AndroidX.appcompat) + implementation(Samples.AndroidX.composeRuntime) + implementation(Samples.AndroidX.composeActivity) + implementation(Samples.AndroidX.composeFoundation) + implementation(Samples.AndroidX.composeFoundationLayout) + implementation(Samples.AndroidX.composeNavigation) + implementation(Samples.Coroutines.core) implementation(Samples.Coroutines.android) diff --git a/examples/android-instrumentation-sample/src/main/AndroidManifest.xml b/examples/android-instrumentation-sample/src/main/AndroidManifest.xml index e849c17b..aac329eb 100644 --- a/examples/android-instrumentation-sample/src/main/AndroidManifest.xml +++ b/examples/android-instrumentation-sample/src/main/AndroidManifest.xml @@ -12,6 +12,11 @@ + + diff --git a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/ComposeActivity.kt b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/ComposeActivity.kt new file mode 100644 index 00000000..c61b7942 --- /dev/null +++ b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/ComposeActivity.kt @@ -0,0 +1,82 @@ +package io.sentry.samples.instrumentation.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController + +class ComposeActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = Destination.Home.route + ) { + val pillShape = RoundedCornerShape(50) + + composable(Destination.Home.route) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + BasicText( + modifier = Modifier + .border(2.dp, Color.Gray, pillShape) + .clip(pillShape) + .clickable { + navController.navigate(Destination.Details.route) + } + .padding(24.dp), + text = "Home. Tap to go to Details." + ) + } + } + composable(Destination.Details.route) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + BasicText( + modifier = Modifier + .border(2.dp, Color.Gray, pillShape) + .clip(pillShape) + .clickable { + navController.popBackStack() + } + .padding(24.dp), + text = "Details. Tap or press back to return." + ) + } + } + } + } + } + + sealed class Destination( + val route: String + ) { + object Home : Destination("home") + object Details : Destination("details") + } +} diff --git a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt index 3819b171..b98716fb 100644 --- a/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt +++ b/examples/android-instrumentation-sample/src/main/java/io/sentry/samples/instrumentation/ui/MainActivity.kt @@ -53,6 +53,10 @@ class MainActivity : ComponentActivity() { startActivity(Intent(this, EditActivity::class.java)) return@setOnMenuItemClickListener true } + if (it.itemId == R.id.action_compose) { + startActivity(Intent(this, ComposeActivity::class.java)) + return@setOnMenuItemClickListener true + } return@setOnMenuItemClickListener false } } diff --git a/examples/android-instrumentation-sample/src/main/res/menu/main.xml b/examples/android-instrumentation-sample/src/main/res/menu/main.xml index a3a9ce6d..7b7628c0 100644 --- a/examples/android-instrumentation-sample/src/main/res/menu/main.xml +++ b/examples/android-instrumentation-sample/src/main/res/menu/main.xml @@ -9,4 +9,11 @@ android:title="Add" app:showAsAction="ifRoom" tools:ignore="HardcodedText" /> + + diff --git a/plugin-build/build.gradle.kts b/plugin-build/build.gradle.kts index 85c6d6ad..121fe015 100644 --- a/plugin-build/build.gradle.kts +++ b/plugin-build/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { testImplementationAar(Libs.SQLITE) testImplementationAar(Libs.SQLITE_FRAMEWORK) testRuntimeOnly(files(androidSdkPath)) - testRuntimeOnly(Libs.SENTRY_ANDROID) + testImplementationAar(Libs.SENTRY_ANDROID) testRuntimeOnly( files( @@ -75,14 +75,13 @@ tasks.withType().configureEach { } tasks.withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() classpath += files(sourceSets["main"].groovy.classesDirectory) kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xjvm-default=enable") languageVersion = "1.4" + apiVersion = "1.4" } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt index 06e6101d..b89d626a 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AbstractInstallStrategy.kt @@ -11,36 +11,59 @@ abstract class AbstractInstallStrategy : ComponentMetadataRule { protected lateinit var logger: Logger - protected abstract val moduleId: String + protected abstract val sentryModuleId: String protected abstract val shouldInstallModule: Boolean - protected open val minSupportedVersion: SemVer = SemVer(0, 0, 0) + protected open val minSupportedThirdPartyVersion: SemVer = SemVer(0, 0, 0) + + protected open val minSupportedSentryVersion: SemVer = SemVer(0, 0, 0) override fun execute(context: ComponentMetadataContext) { val autoInstallState = AutoInstallState.getInstance() if (!shouldInstallModule) { logger.info { - "$moduleId won't be installed because it was already installed directly" + "$sentryModuleId won't be installed because it was already installed directly" } return } - val semVer = SemVer.parse(context.details.id.version) - if (semVer < minSupportedVersion) { + val thirdPartySemVersion = SemVer.parse(context.details.id.version) + if (thirdPartySemVersion < minSupportedThirdPartyVersion) { logger.warn { - "$moduleId won't be installed because the current version is " + - "lower than the minimum supported version ($minSupportedVersion)" + "$sentryModuleId won't be installed because the current version is " + + "lower than the minimum supported version ($minSupportedThirdPartyVersion)" } return } + if (minSupportedSentryVersion.major > 0) { + try { + val sentrySemVersion = SemVer.parse(autoInstallState.sentryVersion) + if (sentrySemVersion < minSupportedSentryVersion) { + logger.warn { + "$sentryModuleId won't be installed because the current version is " + + "lower than the minimum supported sentry version " + + "($autoInstallState.sentryVersion)" + } + return + } + } catch (ex: IllegalArgumentException) { + logger.warn { + "$sentryModuleId won't be installed because the provided " + + "sentry version($autoInstallState.sentryVersion) could not be processed " + + "as a semantic version." + } + return + } + } + context.details.allVariants { metadata -> metadata.withDependencies { dependencies -> val sentryVersion = autoInstallState.sentryVersion - dependencies.add("$SENTRY_GROUP:$moduleId:$sentryVersion") + dependencies.add("$SENTRY_GROUP:$sentryModuleId:$sentryVersion") logger.info { - "$moduleId was successfully installed with version: $sentryVersion" + "$sentryModuleId was successfully installed with version: $sentryVersion" } } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt index 0119a959..20ab810f 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstall.kt @@ -1,5 +1,7 @@ package io.sentry.android.gradle.autoinstall +import io.sentry.android.gradle.autoinstall.compose.ComposeInstallStrategy +import io.sentry.android.gradle.autoinstall.compose.ComposeInstallStrategy.Registrar.SENTRY_COMPOSE_ID import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy.Registrar.SENTRY_FRAGMENT_ID import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy @@ -18,7 +20,8 @@ private const val SENTRY_ANDROID_CORE_ID = "sentry-android-core" private val strategies = listOf( OkHttpInstallStrategy.Registrar, TimberInstallStrategy.Registrar, - FragmentInstallStrategy.Registrar + FragmentInstallStrategy.Registrar, + ComposeInstallStrategy.Registrar ) fun Project.installDependencies(extension: SentryPluginExtension) { @@ -34,6 +37,7 @@ fun Project.installDependencies(extension: SentryPluginExtension) { installOkHttp = !dependencies.isModuleAvailable(SENTRY_OKHTTP_ID) installTimber = !dependencies.isModuleAvailable(SENTRY_TIMBER_ID) installFragment = !dependencies.isModuleAvailable(SENTRY_FRAGMENT_ID) + installCompose = !dependencies.isModuleAvailable(SENTRY_COMPOSE_ID) } } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt index 123fbb88..d561498b 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/AutoInstallState.kt @@ -22,11 +22,16 @@ class AutoInstallState private constructor() : Serializable { @set:Synchronized var installTimber: Boolean = false + @get:Synchronized + @set:Synchronized + var installCompose: Boolean = false + override fun toString(): String { return "AutoInstallState(sentryVersion='$sentryVersion', " + "installOkHttp=$installOkHttp, " + "installFragment=$installFragment, " + - "installTimber=$installTimber)" + "installTimber=$installTimber," + + "installCompose=$installCompose)" } override fun equals(other: Any?): Boolean { @@ -39,6 +44,7 @@ class AutoInstallState private constructor() : Serializable { if (installOkHttp != other.installOkHttp) return false if (installFragment != other.installFragment) return false if (installTimber != other.installTimber) return false + if (installCompose != other.installCompose) return false return true } @@ -48,6 +54,7 @@ class AutoInstallState private constructor() : Serializable { result = 31 * result + installOkHttp.hashCode() result = 31 * result + installFragment.hashCode() result = 31 * result + installTimber.hashCode() + result = 31 * result + installCompose.hashCode() return result } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/compose/ComposeInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/compose/ComposeInstallStrategy.kt new file mode 100644 index 00000000..dd634326 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/compose/ComposeInstallStrategy.kt @@ -0,0 +1,43 @@ +package io.sentry.android.gradle.autoinstall.compose + +import io.sentry.android.gradle.SentryPlugin +import io.sentry.android.gradle.autoinstall.AbstractInstallStrategy +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar +import io.sentry.android.gradle.util.SemVer +import javax.inject.Inject +import org.gradle.api.artifacts.dsl.ComponentMetadataHandler +import org.slf4j.Logger + +abstract class ComposeInstallStrategy : AbstractInstallStrategy { + + constructor(logger: Logger) : super() { + this.logger = logger + } + + @Suppress("unused") // used by Gradle + @Inject // inject is needed to avoid Gradle error + constructor() : this(SentryPlugin.logger) + + override val sentryModuleId: String get() = SENTRY_COMPOSE_ID + + override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installCompose + + override val minSupportedSentryVersion: SemVer + get() = SemVer(6, 7, 0) + + override val minSupportedThirdPartyVersion: SemVer + get() = SemVer(1, 0, 0) + + companion object Registrar : InstallStrategyRegistrar { + + internal const val SENTRY_COMPOSE_ID = "sentry-compose-android" + + override fun register(component: ComponentMetadataHandler) { + component.withModule( + "androidx.compose.runtime:runtime", + ComposeInstallStrategy::class.java + ) {} + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt index 122d4c82..3db2c84a 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/fragment/FragmentInstallStrategy.kt @@ -4,6 +4,7 @@ import io.sentry.android.gradle.SentryPlugin import io.sentry.android.gradle.autoinstall.AbstractInstallStrategy import io.sentry.android.gradle.autoinstall.AutoInstallState import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar +import io.sentry.android.gradle.util.SemVer import javax.inject.Inject import org.gradle.api.artifacts.dsl.ComponentMetadataHandler import org.slf4j.Logger @@ -19,10 +20,12 @@ abstract class FragmentInstallStrategy : AbstractInstallStrategy { @Inject // inject is needed to avoid Gradle error constructor() : this(SentryPlugin.logger) - override val moduleId: String get() = SENTRY_FRAGMENT_ID + override val sentryModuleId: String get() = SENTRY_FRAGMENT_ID override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installFragment + override val minSupportedSentryVersion: SemVer get() = SemVer(5, 1, 0) + companion object Registrar : InstallStrategyRegistrar { private const val FRAGMENT_GROUP = "androidx.fragment" private const val FRAGMENT_ID = "fragment" diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt index 87c98be3..5960e70d 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/okhttp/OkHttpInstallStrategy.kt @@ -20,11 +20,13 @@ abstract class OkHttpInstallStrategy : AbstractInstallStrategy { @Inject // inject is needed to avoid Gradle error constructor() : this(SentryPlugin.logger) - override val moduleId: String get() = SENTRY_OKHTTP_ID + override val sentryModuleId: String get() = SENTRY_OKHTTP_ID override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installOkHttp - override val minSupportedVersion: SemVer get() = MIN_SUPPORTED_VERSION + override val minSupportedThirdPartyVersion: SemVer get() = MIN_SUPPORTED_VERSION + + override val minSupportedSentryVersion: SemVer get() = SemVer(4, 4, 0) companion object Registrar : InstallStrategyRegistrar { private const val OKHTTP_GROUP = "com.squareup.okhttp3" diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt index 12ea60a5..8b4a3ebb 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/autoinstall/timber/TimberInstallStrategy.kt @@ -20,11 +20,13 @@ abstract class TimberInstallStrategy : AbstractInstallStrategy { @Inject // inject is needed to avoid Gradle error constructor() : this(SentryPlugin.logger) - override val moduleId: String get() = SENTRY_TIMBER_ID + override val sentryModuleId: String get() = SENTRY_TIMBER_ID override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installTimber - override val minSupportedVersion: SemVer get() = MIN_SUPPORTED_VERSION + override val minSupportedThirdPartyVersion: SemVer get() = MIN_SUPPORTED_VERSION + + override val minSupportedSentryVersion: SemVer get() = SemVer(3, 0, 0) companion object Registrar : InstallStrategyRegistrar { private const val TIMBER_GROUP = "com.jakewharton.timber" diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt index 7a0986db..6e782090 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt @@ -42,7 +42,8 @@ open class TracingInstrumentationExtension @Inject constructor(objects: ObjectFa setOf( InstrumentationFeature.DATABASE, InstrumentationFeature.FILE_IO, - InstrumentationFeature.OKHTTP + InstrumentationFeature.OKHTTP, + InstrumentationFeature.COMPOSE, ) ) } @@ -68,5 +69,13 @@ enum class InstrumentationFeature { * This feature uses bytecode manipulation and attaches SentryOkHttpInterceptor to all OkHttp * clients in the project. */ - OKHTTP + OKHTTP, + + /** + * When enabled the SDK will create breadcrumbs when navigating + * using [androidx.navigation.NavController]. + * This feature uses bytecode manipulation and adds an OnDestinationChangedListener to all + * navigation controllers used in Jetpack Compose. + */ + COMPOSE } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/ChainedInstrumentable.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/ChainedInstrumentable.kt index 926a19b6..1ff8d868 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/ChainedInstrumentable.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/ChainedInstrumentable.kt @@ -40,4 +40,9 @@ class ChainedInstrumentable( override fun isInstrumentable(data: ClassContext): Boolean = instrumentables.any { it.isInstrumentable(data) } + + override fun toString(): String { + return "ChainedInstrumentable(instrumentables=" + + "${instrumentables.joinToString(", ") { it.javaClass.simpleName }})" + } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt index 08dd1760..eafe3a0d 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt @@ -6,6 +6,7 @@ import com.android.build.api.instrumentation.ClassData import com.android.build.api.instrumentation.InstrumentationParameters import io.sentry.android.gradle.SentryPlugin import io.sentry.android.gradle.extensions.InstrumentationFeature +import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigation import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement @@ -19,9 +20,7 @@ import io.sentry.android.gradle.services.SentryModulesService import io.sentry.android.gradle.util.SemVer import io.sentry.android.gradle.util.SentryModules import io.sentry.android.gradle.util.SentryVersions -import io.sentry.android.gradle.util.debug import io.sentry.android.gradle.util.info -import io.sentry.android.gradle.util.warn import java.io.File import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty @@ -58,15 +57,15 @@ abstract class SpanAddingClassVisitorFactory : val tmpDir: Property @get:Internal - var _instrumentables: List? + var _instrumentable: ClassInstrumentable? } - private val instrumentables: List + private val instrumentable: ClassInstrumentable get() { - val memoized = parameters.get()._instrumentables + val memoized = parameters.get()._instrumentable if (memoized != null) { - SentryPlugin.logger.debug { - "Memoized: ${memoized.joinToString { it::class.java.simpleName }}" + SentryPlugin.logger.info { + "Instrumentable: $memoized [Memoized]" } return memoized } @@ -78,26 +77,41 @@ abstract class SpanAddingClassVisitorFactory : * version to [SentryVersions] if it involves runtime classes * from the sentry-android SDK. */ - val instrumentables = listOfNotNull( - AndroidXSQLiteDatabase().takeIf { - isDatabaseInstrEnabled(sentryModules, parameters.get()) - }, - AndroidXSQLiteStatement().takeIf { - isDatabaseInstrEnabled(sentryModules, parameters.get()) - }, - AndroidXRoomDao().takeIf { - isDatabaseInstrEnabled(sentryModules, parameters.get()) - }, - OkHttp().takeIf { isOkHttpInstrEnabled(sentryModules, parameters.get()) }, - ChainedInstrumentable( - listOf(WrappingInstrumentable(), RemappingInstrumentable()) - ).takeIf { isFileIOInstrEnabled(sentryModules, parameters.get()) } + val instrumentable = ChainedInstrumentable( + listOfNotNull( + AndroidXSQLiteDatabase().takeIf { + isDatabaseInstrEnabled(sentryModules, parameters.get()) + }, + AndroidXSQLiteStatement().takeIf { + isDatabaseInstrEnabled(sentryModules, parameters.get()) + }, + AndroidXRoomDao().takeIf { + isDatabaseInstrEnabled(sentryModules, parameters.get()) + }, + OkHttp().takeIf { isOkHttpInstrEnabled(sentryModules, parameters.get()) }, + WrappingInstrumentable().takeIf { + isFileIOInstrEnabled( + sentryModules, + parameters.get() + ) + }, + RemappingInstrumentable().takeIf { + isFileIOInstrEnabled( + sentryModules, + parameters.get() + ) + }, + ComposeNavigation().takeIf { + isComposeInstrEnabled(sentryModules, parameters.get()) + }, + ) ) + SentryPlugin.logger.info { - "Instrumentables: ${instrumentables.joinToString { it::class.java.simpleName }}" + "Instrumentable: $instrumentable" } - parameters.get()._instrumentables = ArrayList(instrumentables) - return instrumentables + parameters.get()._instrumentable = instrumentable + return instrumentable } private fun isDatabaseInstrEnabled( @@ -126,6 +140,15 @@ abstract class SpanAddingClassVisitorFactory : SentryVersions.VERSION_OKHTTP ) && parameters.features.get().contains(InstrumentationFeature.OKHTTP) + private fun isComposeInstrEnabled( + sentryModules: Map, + parameters: SpanAddingParameters + ): Boolean = + sentryModules.isAtLeast( + SentryModules.SENTRY_ANDROID_COMPOSE, + SentryVersions.VERSION_COMPOSE + ) && parameters.features.get().contains(InstrumentationFeature.COMPOSE) + private fun Map.isAtLeast(module: String, minVersion: SemVer): Boolean = getOrDefault(module, SentryVersions.VERSION_DEFAULT) >= minVersion @@ -144,23 +167,14 @@ abstract class SpanAddingClassVisitorFactory : return nextClassVisitor } - return instrumentables.find { it.isInstrumentable(classContext) } - ?.getVisitor( - classContext, - instrumentationContext.apiVersion.get(), - nextClassVisitor, - parameters = parameters.get() - ) - ?: nextClassVisitor.also { - SentryPlugin.logger.warn { - """ - $className is not supported for instrumentation. - This is likely a bug, please file an issue at https://github.com/getsentry/sentry-android-gradle-plugin/issues - """.trimIndent() - } - } + return instrumentable.getVisitor( + classContext, + instrumentationContext.apiVersion.get(), + nextClassVisitor, + parameters = parameters.get() + ) } override fun isInstrumentable(classData: ClassData): Boolean = - instrumentables.any { it.isInstrumentable(classData.toClassContext()) } + instrumentable.isInstrumentable(classData.toClassContext()) } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/compose/ComposeNavigation.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/compose/ComposeNavigation.kt new file mode 100644 index 00000000..3302d123 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/compose/ComposeNavigation.kt @@ -0,0 +1,54 @@ +package io.sentry.android.gradle.instrumentation.androidx.compose + +import com.android.build.api.instrumentation.ClassContext +import io.sentry.android.gradle.instrumentation.ClassInstrumentable +import io.sentry.android.gradle.instrumentation.CommonClassVisitor +import io.sentry.android.gradle.instrumentation.MethodContext +import io.sentry.android.gradle.instrumentation.MethodInstrumentable +import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory +import io.sentry.android.gradle.instrumentation.androidx.compose.visitor.RememberNavControllerMethodVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +open class ComposeNavigation : ClassInstrumentable { + + companion object { + private const val NAV_HOST_CONTROLLER_CLASSNAME = + "androidx.navigation.compose.NavHostControllerKt" + } + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): ClassVisitor { + return CommonClassVisitor( + apiVersion, + originalVisitor, + NAV_HOST_CONTROLLER_CLASSNAME, + listOf(object : MethodInstrumentable { + + override val fqName: String get() = "rememberNavController" + + override fun getVisitor( + instrumentableContext: MethodContext, + apiVersion: Int, + originalVisitor: MethodVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters + ): MethodVisitor { + return RememberNavControllerMethodVisitor( + apiVersion, + originalVisitor, + instrumentableContext + ) + } + }), + parameters + ) + } + + override fun isInstrumentable(data: ClassContext): Boolean { + return data.currentClassData.className == NAV_HOST_CONTROLLER_CLASSNAME + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/compose/visitor/RememberNavControllerMethodVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/compose/visitor/RememberNavControllerMethodVisitor.kt new file mode 100644 index 00000000..701d5a23 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/compose/visitor/RememberNavControllerMethodVisitor.kt @@ -0,0 +1,45 @@ +package io.sentry.android.gradle.instrumentation.androidx.compose.visitor + +import io.sentry.android.gradle.instrumentation.MethodContext +import io.sentry.android.gradle.instrumentation.wrap.Replacement +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Type +import org.objectweb.asm.commons.AdviceAdapter +import org.objectweb.asm.commons.Method + +class RememberNavControllerMethodVisitor( + apiVersion: Int, + originalVisitor: MethodVisitor, + instrumentableContext: MethodContext +) : AdviceAdapter( + apiVersion, + originalVisitor, + instrumentableContext.access, + instrumentableContext.name, + instrumentableContext.descriptor +) { + /* ktlint-disable max-line-length */ + private val replacement = Replacement( + "Lio/sentry/compose/SentryNavigationIntegrationKt;", + "withSentryObservableEffect", + "(Landroidx/navigation/NavHostController;Landroidx/compose/runtime/Composer;I)Landroidx/navigation/NavHostController;" + ) + /* ktlint-enable max-line-length */ + + override fun onMethodExit(opcode: Int) { + // NavHostController is the return value; + // thus it's already on top of stack + + // Composer $composer + loadArg(1) + + // int $changed + loadArg(2) + + invokeStatic( + Type.getType(replacement.owner), + Method(replacement.name, replacement.descriptor) + ) + super.onMethodExit(opcode) + } +} 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 1992fd7e..fd297f7e 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 @@ -21,11 +21,13 @@ internal object SentryVersions { internal val VERSION_PERFORMANCE = SemVer(4, 0, 0) internal val VERSION_OKHTTP = SemVer(5, 0, 0) internal val VERSION_FILE_IO = SemVer(5, 5, 0) + internal val VERSION_COMPOSE = SemVer(6, 7, 0) } internal object SentryModules { internal const val SENTRY_ANDROID_CORE = "sentry-android-core" internal const val SENTRY_ANDROID_OKHTTP = "sentry-android-okhttp" + internal const val SENTRY_ANDROID_COMPOSE = "sentry-compose-android" } /** diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt index 382be5ed..0a1fd8be 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginAutoInstallTest.kt @@ -25,11 +25,7 @@ class SentryPluginAutoInstallTest( """.trimIndent() ) - val result = runner - .appendArguments("app:dependencies") - .appendArguments("--configuration") - .appendArguments("debugRuntimeClasspath") - .build() + val result = runListDependenciesTask() assertTrue { "io.sentry:sentry-android:$SENTRY_SDK_VERSION" in result.output } @@ -56,16 +52,16 @@ class SentryPluginAutoInstallTest( """.trimIndent() ) - val result = runner - .appendArguments("app:dependencies") - .appendArguments("--configuration") - .appendArguments("debugRuntimeClasspath") - .build() + val result = runListDependenciesTask() assertFalse { "io.sentry:sentry-android:5.1.0" in result.output } assertTrue { "io.sentry:sentry-android-timber:5.1.0" in result.output } assertTrue { "io.sentry:sentry-android-fragment:5.1.0" in result.output } assertFalse { "io.sentry:sentry-android-okhttp:5.1.0" in result.output } assertTrue { "io.sentry:sentry-android-okhttp:5.4.0" in result.output } + assertFalse { "io.sentry:sentry-compose-android:5.1.0" in result.output } + + // ensure all dependencies could be resolved + assertFalse { "FAILED" in result.output } } @Test @@ -84,15 +80,14 @@ class SentryPluginAutoInstallTest( """.trimIndent() ) - val result = runner - .appendArguments("app:dependencies") - .appendArguments("--configuration") - .appendArguments("debugRuntimeClasspath") - .build() + val result = runListDependenciesTask() assertFalse { "io.sentry:sentry-android:$SENTRY_SDK_VERSION" in result.output } assertFalse { "io.sentry:sentry-android-timber:$SENTRY_SDK_VERSION" in result.output } assertFalse { "io.sentry:sentry-android-fragment:$SENTRY_SDK_VERSION" in result.output } assertFalse { "io.sentry:sentry-android-okhttp:$SENTRY_SDK_VERSION" in result.output } + + // ensure all dependencies could be resolved + assertFalse { "FAILED" in result.output } } @Test @@ -113,14 +108,63 @@ class SentryPluginAutoInstallTest( """.trimIndent() ) - val result = runner - .appendArguments("app:dependencies") - .appendArguments("--configuration") - .appendArguments("debugRuntimeClasspath") - .build() + val result = runListDependenciesTask() + assertTrue { "io.sentry:sentry-android:5.1.2" in result.output } assertTrue { "io.sentry:sentry-android-timber:5.1.2" in result.output } assertTrue { "io.sentry:sentry-android-okhttp:5.1.2" in result.output } assertTrue { "io.sentry:sentry-android-fragment:5.4.0" in result.output } + + // ensure all dependencies could be resolved + assertFalse { "FAILED" in result.output } } + + @Test + fun `compose is not added for lower sentry versions`() { + appBuildFile.appendText( + // language=Groovy + """ + dependencies { + implementation 'androidx.compose.runtime:runtime:1.1.1' + } + + sentry.autoInstallation.enabled = true + sentry.autoInstallation.sentryVersion = "6.6.0" + sentry.includeProguardMapping = false + """.trimIndent() + ) + + val result = runListDependenciesTask() + + assertFalse { "io.sentry:sentry-compose-android:6.6.0" in result.output } + assertFalse { "FAILED" in result.output } + } + + @Test + fun `compose is added with when sentry version 6_7_0 or above is used`() { + appBuildFile.appendText( + // language=Groovy + """ + dependencies { + implementation 'androidx.compose.runtime:runtime:1.1.1' + } + + sentry.autoInstallation.enabled = true + sentry.autoInstallation.sentryVersion = "6.7.0" + sentry.includeProguardMapping = false + """.trimIndent() + ) + + val result = runListDependenciesTask() + + assertTrue { "io.sentry:sentry-compose-android:6.7.0" in result.output } + // ensure all dependencies could be resolved + assertFalse { "FAILED" in result.output } + } + + private fun runListDependenciesTask() = runner + .appendArguments("app:dependencies") + .appendArguments("--configuration") + .appendArguments("debugRuntimeClasspath") + .build() } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt index f22234e1..e6825290 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/SentryPluginTest.kt @@ -182,12 +182,12 @@ class SentryPluginTest( applyTracingInstrumentation(features = setOf(InstrumentationFeature.DATABASE)) val build = runner - .appendArguments(":app:assembleDebug", "--debug") + .appendArguments(":app:assembleDebug", "--info") .build() assertTrue { - "[sentry] Instrumentables: AndroidXSQLiteDatabase, AndroidXSQLiteStatement," + - " AndroidXRoomDao" in build.output + "[sentry] Instrumentable: ChainedInstrumentable(instrumentables=" + + "AndroidXSQLiteDatabase, AndroidXSQLiteStatement, AndroidXRoomDao)" in build.output } } @@ -196,27 +196,73 @@ class SentryPluginTest( applyTracingInstrumentation(features = setOf(InstrumentationFeature.FILE_IO)) val build = runner - .appendArguments(":app:assembleDebug", "--debug") + .appendArguments(":app:assembleDebug", "--info") .build() assertTrue { - "[sentry] Instrumentables: ChainedInstrumentable" in build.output + "[sentry] Instrumentable: ChainedInstrumentable(instrumentables=" + + "WrappingInstrumentable, RemappingInstrumentable)" in build.output } } @Test - fun `applies all instrumentables when all features enabled`() { + fun `applies only Compose instrumentable when only Compose feature enabled`() { applyTracingInstrumentation( - features = setOf(InstrumentationFeature.DATABASE, InstrumentationFeature.FILE_IO) + features = setOf(InstrumentationFeature.COMPOSE), + dependencies = setOf( + "androidx.compose.runtime:runtime:1.1.0", + "io.sentry:sentry-compose-android:6.7.0" + ) ) val build = runner - .appendArguments(":app:assembleDebug", "--debug") + .appendArguments(":app:assembleDebug", "--info") + .build() + assertTrue { + "[sentry] Instrumentable: ChainedInstrumentable(instrumentables=" + + "ComposeNavigation)" in build.output + } + } + + @Test + fun `does not apply Compose instrumentable when app does not depend on compose (runtime)`() { + applyTracingInstrumentation( + features = setOf(InstrumentationFeature.COMPOSE) + ) + + val build = runner + .appendArguments(":app:assembleDebug", "--info") + .build() + assertTrue { + "[sentry] Instrumentable: ChainedInstrumentable(instrumentables=)" in build.output + } + } + + @Test + fun `applies all instrumentables when all features are enabled`() { + applyTracingInstrumentation( + features = setOf( + InstrumentationFeature.DATABASE, + InstrumentationFeature.FILE_IO, + InstrumentationFeature.OKHTTP, + InstrumentationFeature.COMPOSE + ), + dependencies = setOf( + "com.squareup.okhttp3:okhttp:3.14.9", + "io.sentry:sentry-android-okhttp:6.6.0", + "androidx.compose.runtime:runtime:1.1.0", + "io.sentry:sentry-compose-android:6.7.0" + ) + ) + val build = runner + .appendArguments(":app:assembleDebug", "--info") .build() assertTrue { - "[sentry] Instrumentables: AndroidXSQLiteDatabase, AndroidXSQLiteStatement," + - " AndroidXRoomDao, ChainedInstrumentable" in build.output + "[sentry] Instrumentable: ChainedInstrumentable(instrumentables=" + + "AndroidXSQLiteDatabase, AndroidXSQLiteStatement, AndroidXRoomDao, OkHttp, " + + "WrappingInstrumentable, RemappingInstrumentable, " + + "ComposeNavigation)" in build.output } } @@ -229,7 +275,7 @@ class SentryPluginTest( dependencies { implementation 'com.squareup.okhttp3:okhttp:3.14.9' - implementation 'io.sentry:sentry-android-okhttp:5.5.0' + implementation 'io.sentry:sentry-android-okhttp:6.6.0' } """.trimIndent() ) @@ -401,14 +447,16 @@ class SentryPluginTest( private fun applyTracingInstrumentation( tracingInstrumentation: Boolean = true, - features: Set = setOf(), + features: Set = emptySet(), + dependencies: Set = emptySet(), debug: Boolean = false ) { appBuildFile.appendText( // language=Groovy """ dependencies { - implementation 'io.sentry:sentry-android:5.5.0' + implementation 'io.sentry:sentry-android:6.6.0' + ${dependencies.joinToString("\n") { "implementation '$it'" }} } sentry { @@ -417,12 +465,7 @@ class SentryPluginTest( forceInstrumentDependencies = true enabled = $tracingInstrumentation debug = $debug - features = ${ - features.joinToString( - prefix = "[", - postfix = "]" - ) { "${it::class.java.canonicalName}.${it.name}" } - } + features = [${features.joinToString { "${it::class.java.canonicalName}.${it.name}" }}] } } """.trimIndent() diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/compose/ComposeInstallStrategyTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/compose/ComposeInstallStrategyTest.kt new file mode 100644 index 00000000..333cf965 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/autoinstall/compose/ComposeInstallStrategyTest.kt @@ -0,0 +1,104 @@ +package io.sentry.android.gradle.autoinstall.compose + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.doAnswer +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.android.gradle.autoinstall.AutoInstallState +import io.sentry.android.gradle.instrumentation.fakes.CapturingTestLogger +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.gradle.api.Action +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataDetails +import org.gradle.api.artifacts.DirectDependenciesMetadata +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.artifacts.VariantMetadata +import org.junit.Test +import org.slf4j.Logger + +class ComposeInstallStrategyTest { + class Fixture { + val logger = CapturingTestLogger() + val dependencies = mock() + val metadataDetails = mock() + val metadataContext = mock { + whenever(it.details).thenReturn(metadataDetails) + val metadata = mock() + doAnswer { + (it.arguments[0] as Action).execute(dependencies) + }.whenever(metadata).withDependencies(any>()) + + doAnswer { + // trigger the callback registered in tests + (it.arguments[0] as Action).execute(metadata) + }.whenever(metadataDetails).allVariants(any>()) + } + + fun getSut( + installCompose: Boolean = true, + composeVersion: String = "1.0.0" + ): ComposeInstallStrategy { + val id = mock { + whenever(it.version).doReturn(composeVersion) + } + whenever(metadataDetails.id).thenReturn(id) + + with(AutoInstallState.getInstance()) { + this.installCompose = installCompose + this.sentryVersion = "6.7.0" + } + return ComposeInstallStrategyImpl(logger) + } + } + + private val fixture = Fixture() + + @Test + fun `when sentry-compose-android is a direct dependency logs a message and does nothing`() { + val sut = fixture.getSut(installCompose = false) + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-compose-android won't be installed because it was already " + + "installed directly" + } + verify(fixture.metadataContext, never()).details + } + + @Test + fun `when sentry version is unsupported logs a message and does nothing`() { + val sut = fixture.getSut(composeVersion = "0.9.0") + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-compose-android won't be installed because the current " + + "version is lower than the minimum supported version (1.0.0)" + } + verify(fixture.metadataDetails, never()).allVariants(any()) + } + + @Test + fun `installs sentry-android-compose with info message`() { + val sut = fixture.getSut() + sut.execute(fixture.metadataContext) + + assertTrue { + fixture.logger.capturedMessage == + "[sentry] sentry-compose-android was successfully installed with version: 6.7.0" + } + verify(fixture.dependencies).add( + check { + assertEquals("io.sentry:sentry-compose-android:6.7.0", it) + } + ) + } + + private class ComposeInstallStrategyImpl(logger: Logger) : ComposeInstallStrategy(logger) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt index e477a15f..7f4ff493 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/VisitorTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.gradle.instrumentation import com.android.build.api.instrumentation.ClassContext +import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigation import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement @@ -137,7 +138,8 @@ class VisitorTest( null ), arrayOf("okhttp/v3", "RealCall", OkHttp(), null), - arrayOf("okhttp/v4", "RealCall", OkHttp(), null) + arrayOf("okhttp/v4", "RealCall", OkHttp(), null), + arrayOf("androidxCompose", "NavHostControllerKt", ComposeNavigation(), null) ) private fun roomDaoTestParameters(suffix: String = "") = arrayOf( diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt index ae17eee0..c9f23a86 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/fakes/TestSpanAddingParameters.kt @@ -33,5 +33,5 @@ class TestSpanAddingParameters( override val tmpDir: Property get() = DefaultProperty(PropertyHost.NO_OP, File::class.java).convention(inMemoryDir) - override var _instrumentables: List? = listOf() + override var _instrumentable: ClassInstrumentable? = null } diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/androidxCompose/NavHostControllerKt.class b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxCompose/NavHostControllerKt.class new file mode 100644 index 00000000..9444122b Binary files /dev/null and b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxCompose/NavHostControllerKt.class differ