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