From 1548c9c5981cd256c3a74e9d7e0dc029eea7c240 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 6 Jun 2026 08:48:18 +0200 Subject: [PATCH 1/2] feat(plugin): Auto-wrap SQLiteDriver with SentrySQLiteDriver for Room users (GRADLE-107) Adds a new ASM bytecode method visitor that lets us auto-wrap all occurrences of SQLiteDriver with SentrySQLiteDriver whenever the driver is passed to Room.DatabaseBuilder.setDriver(...). For instance: val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName") .setDriver(AndroidSQLiteDriver()) .build() becomes: val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName") .setDriver(SentrySQLiteDriver.create(AndroidSQLiteDriver())) .build() The wrapping policy is naive in that every SQLiteDriver passed to setDriver() is wrapped. That's deliberate because SentrySQLiteDriver protects against double-wrapping internally, which lets us keep our visitor implementation simple. Preconditions: 1. InstrumentationFeature.DATABASE is enabled 2. The owning app is using a version of sentry-android-sqlite that includes SentrySQLiteDriver Coverage: - Auto-wraps SQLiteDriver for all Room users (sole Room access point is via its Room.DatabaseBuilder.setDriver() method). - SQLDelight users don't need driver auto-wrapping (they still use SupportSQLiteOpenHelper, which we already auto-wrap). - The few developers who use SQLiteDriver directly will need to wrap it manually. --- CHANGELOG.md | 10 +- gradle/libs.versions.toml | 5 +- plugin-build/build.gradle.kts | 11 +- .../TracingInstrumentationExtension.kt | 13 +- .../SpanAddingClassVisitorFactory.kt | 3 + .../sqlite/driver/AndroidXSQLiteDriver.kt | 90 ++++++++++ .../driver/visitor/SetDriverMethodVisitor.kt | 34 ++++ .../gradle/services/SentryModulesService.kt | 13 ++ .../io/sentry/android/gradle/util/Versions.kt | 1 + .../kotlin/androidx/sqlite/SQLiteDriver.kt | 3 + .../gradle/instrumentation/VisitorTest.kt | 15 ++ .../sqlite/driver/AndroidXSQLiteDriverTest.kt | 71 ++++++++ .../driver/SQLiteDriverBytecodeTestUtil.kt | 55 ++++++ .../SetDriverMethodInstrumentableTest.kt | 48 +++++ .../visitor/SetDriverMethodVisitorTest.kt | 84 +++++++++ .../gradle/integration/SentryPluginTest.kt | 139 +++++++++++++++ .../gradle/util/SentryModulesServiceTest.kt | 164 ++++++++++++++++++ .../io/sentry/sqlite/SentrySQLiteDriver.kt | 9 + .../androidxRoom/RoomDatabase$Builder.class | Bin 0 -> 30384 bytes .../androidxRoom/RoomDatabase3$Builder.class | Bin 0 -> 26703 bytes .../androidxSqliteDriver/NoSetDriver.class | Bin 0 -> 269 bytes 21 files changed, 759 insertions(+), 9 deletions(-) create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt create mode 100644 plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt create mode 100644 plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase$Builder.class create mode 100644 plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase3$Builder.class create mode 100644 plugin-build/src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf1f4b2a..e5b6b6f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ ## Unreleased -### Dependencies +### Features -- Bump mockito-kotlin from `com.nhaarman.mockitokotlin2:2.2.0` to `org.mockito.kotlin:5.4.0` ([#1286](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1286)) +- Auto-instrument SQLiteDriver for Room users ([#1285](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1285)) + - Gated on `sentry-android-sqlite` >= 8.44.0 and the existing `tracingInstrumentation` `DATABASE` feature + - For users of the `androidx.sqlite.driver.SupportSQLiteDriver` bridge, auto-instrumentation wraps only the `SupportSQLiteOpenHelper` consumed by the bridge and not the bridge itself (avoids duplicate spans) ### Fixes @@ -12,6 +14,10 @@ - This fixed the issue where sentry-cli could not be found (`A problem occurred starting process 'command ../sentry-cliXXX.exe'`) - Defer the telemetry default-org lookup to execution time so the configuration cache no longer re-runs `sentry-cli` on every build ([#1263](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1263)) +### Dependencies + +- Bump mockito-kotlin from `com.nhaarman.mockitokotlin2:2.2.0` to `org.mockito.kotlin:5.4.0` ([#1286](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1286)) + ## 6.10.0 ### Features diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75831f373..4693b4e53 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,8 +60,11 @@ sentryAndroidOkhttp = { group = "io.sentry", name = "sentry-android-okhttp", ver sentrySpringBootJakarta = { group = "io.sentry", name = "sentry-spring-boot-starter-jakarta", version.ref = "sentry" } # test -mockitoKotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version = "5.4.0" } arscLib = { group = "io.github.reandroid", name = "ARSCLib", version = "1.1.4" } +mockitoKotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version = "5.4.0" } +# Room & Room 3 runtime versions must match RoomDatabase$Builder bytecode fixtures (see SQLiteDriverBytecodeTestUtil) +roomRuntimeAndroid = { group = "androidx.room", name = "room-runtime-android", version = "2.7.0" } +room3RuntimeAndroid = { group = "androidx.room3", name = "room3-runtime-android", version = "3.0.0-alpha06" } zip4j = { group = "net.lingala.zip4j", name = "zip4j", version = "2.11.5" } # samples diff --git a/plugin-build/build.gradle.kts b/plugin-build/build.gradle.kts index 38232b324..117b0007a 100644 --- a/plugin-build/build.gradle.kts +++ b/plugin-build/build.gradle.kts @@ -55,12 +55,17 @@ dependencies { testImplementation(libs.asmCommons) // we need these dependencies for tests, because the bytecode verifier also analyzes superclasses + testImplementationAar(libs.roomRuntimeAndroid) + testImplementationAar(libs.room3RuntimeAndroid) + testImplementationAar(libs.sentryAndroid) + testImplementationAar(libs.sentryAndroidOkhttp) testImplementationAar(libs.sqlite) testImplementationAar(libs.sqliteFramework) - testRuntimeOnly(files(androidSdkPath)) - testImplementationAar(libs.sentryAndroid) + + testImplementation(libs.sample.coroutines.core) testImplementation(libs.sentryOkhttp) - testImplementationAar(libs.sentryAndroidOkhttp) + + testRuntimeOnly(files(androidSdkPath)) // Needed to read contents from APK/Source Bundles testImplementation(libs.arscLib) 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 d51ca26f4..a7ee1171a 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 @@ -75,9 +75,16 @@ open class TracingInstrumentationExtension @Inject constructor(objects: ObjectFa enum class InstrumentationFeature(val integrationName: String) { /** - * When enabled the SDK will create spans for any CRUD operation performed by - * 'androidx.sqlite.db.SupportSQLiteOpenHelper' and 'androidx.room'. This feature uses bytecode - * manipulation. + * When enabled the SDK will create spans for database operations at two levels: + * + * **SQL statement execution** (`db.sql.query` spans): Wraps the low-level db open helper or + * driver so each SQL statement produces one or more spans. + * + * **DAO method execution** (`db.sql.room` spans): Wraps each public method on Room's generated + * `@Dao` `_Impl` classes, measuring the full DAO call end-to-end (transaction management, query + * execution, and cursor processing). Only for Room users and only on versions prior to Room 2.7. + * + * This feature uses bytecode manipulation. */ DATABASE("DatabaseInstrumentation"), 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 fd4699f9c..0a22d2b68 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 @@ -10,6 +10,7 @@ import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigati import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.AndroidXSQLiteOpenHelper import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement import io.sentry.android.gradle.instrumentation.appstart.Application import io.sentry.android.gradle.instrumentation.appstart.ContentProvider @@ -90,10 +91,12 @@ abstract class SpanAddingClassVisitorFactory : ChainedInstrumentable( listOfNotNull( AndroidXSQLiteOpenHelper().takeIf { sentryModulesService.isNewDatabaseInstrEnabled() }, + AndroidXSQLiteDriver().takeIf { sentryModulesService.isSQLiteDriverInstrEnabled() }, AndroidXSQLiteDatabase().takeIf { sentryModulesService.isOldDatabaseInstrEnabled() }, AndroidXSQLiteStatement(androidXSqliteFrameWorkVersion).takeIf { sentryModulesService.isOldDatabaseInstrEnabled() }, + // Note that DAO spans no longer work on Room 2.7+ or Room 3.0+ due to Room API changes. AndroidXRoomDao().takeIf { sentryModulesService.isNewDatabaseInstrEnabled() || sentryModulesService.isOldDatabaseInstrEnabled() diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt new file mode 100644 index 000000000..f207e5ae1 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt @@ -0,0 +1,90 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +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.sqlite.driver.visitor.SetDriverMethodVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Auto-instruments `SQLiteDriver` for all Room users by wrapping any driver passed to + * `RoomDatabase.Builder.setDriver(SQLiteDriver)`. + * + * In other words, this: + * ```kotlin + * val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName") + * .setDriver(AndroidSQLiteDriver()) + * .build() + * ``` + * + * becomes: + * ```kotlin + * val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName") + * .setDriver(SentrySQLiteDriver.create(AndroidSQLiteDriver())) + * .build() + * ``` + * + * `SentrySQLiteDriver` protects against duplicate wrappings, allowing the visitor to wrap + * `SQLiteDriver` unconditionally. + * + * Coverage is limited to Room because SQLDelight + * [doesn't support `SQLiteDriver`](https://github.com/sqldelight/sqldelight/issues/6072) (it uses + * `SupportSQLiteOpenHelper`, which we auto-instrument via + * [AndroidXSQLiteOpenHelper][io.sentry.android.gradle.instrumentation.androidx.sqlite.AndroidXSQLiteOpenHelper]). + * To keep our implementation simple and build times fast, developers who use `SQLiteDriver` + * directly are expected to wrap it themselves. + */ +class AndroidXSQLiteDriver : ClassInstrumentable { + + override fun getVisitor( + instrumentableContext: ClassContext, + apiVersion: Int, + originalVisitor: ClassVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters, + ): ClassVisitor { + val currentClassName = instrumentableContext.currentClassData.className + + return CommonClassVisitor( + apiVersion = apiVersion, + classVisitor = originalVisitor, + className = currentClassName.substringAfterLast('.'), + methodInstrumentables = listOf(SetDriverMethodInstrumentable()), + parameters = parameters, + ) + } + + override fun isInstrumentable(data: ClassContext): Boolean = + data.currentClassData.className in TARGET_CLASSES + + companion object { + + // Currently covers Room 2 and Room 3 packages. Update as needed. + internal val TARGET_CLASSES = + setOf("androidx.room.RoomDatabase\$Builder", "androidx.room3.RoomDatabase\$Builder") + } +} + +class SetDriverMethodInstrumentable : MethodInstrumentable { + + override val fqName: String + get() = SET_DRIVER + + override fun getVisitor( + instrumentableContext: MethodContext, + apiVersion: Int, + originalVisitor: MethodVisitor, + parameters: SpanAddingClassVisitorFactory.SpanAddingParameters, + ): MethodVisitor = SetDriverMethodVisitor(apiVersion, originalVisitor, instrumentableContext) + + override fun isInstrumentable(data: MethodContext): Boolean = + data.name == SET_DRIVER && data.descriptor?.startsWith(SET_DRIVER_DESCRIPTOR_PREFIX) == true + + companion object { + internal const val SET_DRIVER = "setDriver" + internal const val SET_DRIVER_DESCRIPTOR_PREFIX = "(Landroidx/sqlite/SQLiteDriver;)" + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt new file mode 100644 index 000000000..7fe22cbee --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt @@ -0,0 +1,34 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor + +import io.sentry.android.gradle.instrumentation.MethodContext +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Type +import org.objectweb.asm.commons.AdviceAdapter +import org.objectweb.asm.commons.Method + +class SetDriverMethodVisitor( + apiVersion: Int, + originalVisitor: MethodVisitor, + instrumentableContext: MethodContext, +) : + AdviceAdapter( + apiVersion, + originalVisitor, + instrumentableContext.access, + instrumentableContext.name, + instrumentableContext.descriptor, + ) { + + override fun onMethodEnter() { + loadArg(0) + invokeStatic(Type.getType(SENTRY_SQLITE_DRIVER_TYPE), Method(CREATE, SENTRY_CREATE_DESCRIPTOR)) + storeArg(0) + } + + companion object { + internal const val CREATE = "create" + internal const val SENTRY_CREATE_DESCRIPTOR = + "(Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;" + internal const val SENTRY_SQLITE_DRIVER_TYPE = "Lio/sentry/sqlite/SentrySQLiteDriver;" + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt index 29ba71bb3..62bf25c3f 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt @@ -72,6 +72,19 @@ abstract class SentryModulesService : sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_SQLITE, SentryVersions.VERSION_SQLITE) && parameters.features.get().contains(InstrumentationFeature.DATABASE) + /** + * Returns true when the owning app uses a version of sentry-android-sqlite that contains + * `SentrySQLiteDriver` and the DATABASE feature is enabled. + * + * Room version is not gated here: Room < 2.7 has no matching `setDriver` method, so + * instrumentation is a no-op. Method descriptor matching is the safety boundary. + */ + fun isSQLiteDriverInstrEnabled(): Boolean = + sentryModules.isAtLeast( + SentryModules.SENTRY_ANDROID_SQLITE, + SentryVersions.VERSION_SQLITE_DRIVER, + ) && parameters.features.get().contains(InstrumentationFeature.DATABASE) + fun isOldDatabaseInstrEnabled(): Boolean = !isNewDatabaseInstrEnabled() && sentryModules.isAtLeast( 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 8485bde93..d5ec03683 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 @@ -33,6 +33,7 @@ internal object SentryVersions { internal val VERSION_LOGCAT = SemVer(6, 17, 0) internal val VERSION_APP_START = SemVer(7, 1, 0) internal val VERSION_SQLITE = SemVer(6, 21, 0) + internal val VERSION_SQLITE_DRIVER = SemVer(8, 44, 0) internal val VERSION_ANDROID_OKHTTP_LISTENER = SemVer(6, 20, 0) internal val VERSION_OKHTTP = SemVer(7, 0, 0) } diff --git a/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt b/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt new file mode 100644 index 000000000..187e5e6a8 --- /dev/null +++ b/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt @@ -0,0 +1,3 @@ +package androidx.sqlite + +interface SQLiteDriver 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 56ba455bf..819d56cc3 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 @@ -5,6 +5,7 @@ import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigati import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao import io.sentry.android.gradle.instrumentation.androidx.sqlite.AndroidXSQLiteOpenHelper import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement import io.sentry.android.gradle.instrumentation.appstart.Application import io.sentry.android.gradle.instrumentation.appstart.ContentProvider @@ -122,6 +123,20 @@ class VisitorTest( AndroidXSQLiteStatement(SemVer(2, 3, 0)), null, ), + // RoomDatabase$Builder fixtures: see SQLiteDriverBytecodeTestUtil (extracted from published + // AARs). + arrayOf( + "androidxRoom", + "RoomDatabase\$Builder", + AndroidXSQLiteDriver(), + TestClassContext("androidx.room.RoomDatabase\$Builder"), + ), + arrayOf( + "androidxRoom", + "RoomDatabase3\$Builder", + AndroidXSQLiteDriver(), + TestClassContext("androidx.room3.RoomDatabase\$Builder"), + ), roomDaoTestParameters("DeleteAndReturnUnit"), roomDaoTestParameters("InsertAndReturnLong"), roomDaoTestParameters("InsertAndReturnUnit"), diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt new file mode 100644 index 000000000..bcc844eeb --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt @@ -0,0 +1,71 @@ +@file:Suppress("UnstableApiUsage") + +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import io.sentry.android.gradle.instrumentation.ChainedInstrumentable +import io.sentry.android.gradle.instrumentation.fakes.TestClassContext +import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters +import java.io.FileInputStream +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes + +class AndroidXSQLiteDriverTest { + + @get:Rule val tmpDir = TemporaryFolder() + + private val instrumentable = AndroidXSQLiteDriver() + + @Test + fun `isInstrumentable returns true for RoomDatabase Builder classes`() { + assertTrue( + instrumentable.isInstrumentable(TestClassContext("androidx.room.RoomDatabase\$Builder")) + ) + assertTrue( + instrumentable.isInstrumentable(TestClassContext("androidx.room3.RoomDatabase\$Builder")) + ) + } + + @Test + fun `isInstrumentable returns false for unrelated classes`() { + assertFalse(instrumentable.isInstrumentable(TestClassContext("com.example.RoomConfig"))) + assertFalse(instrumentable.isInstrumentable(TestClassContext("io.sentry.Sentry"))) + assertFalse(instrumentable.isInstrumentable(TestClassContext("com.example.FakeSetDriver"))) + } + + @Test + fun `ChainedInstrumentable does not instrument unrelated classes`() { + val className = "com.example.NoSetDriver" + val originalBytes = loadNoSetDriverFixtureBytes() + val instrumentedBytes = instrumentThroughChain(className, originalBytes) + + assertEquals(0, SQLiteDriverBytecodeTestUtil.countWrapCalls(instrumentedBytes)) + } + + private fun instrumentThroughChain(className: String, bytes: ByteArray): ByteArray { + val classReader = ClassReader(bytes) + val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) + val classVisitor = + ChainedInstrumentable(listOf(instrumentable)) + .getVisitor( + TestClassContext(className), + Opcodes.ASM9, + classWriter, + parameters = TestSpanAddingParameters(inMemoryDir = tmpDir.root), + ) + classReader.accept(classVisitor, ClassReader.SKIP_FRAMES) + return classWriter.toByteArray() + } + + private fun loadNoSetDriverFixtureBytes(): ByteArray = + FileInputStream( + "src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class" + ) + .use { it.readBytes() } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt new file mode 100644 index 000000000..918f6d5c1 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SQLiteDriverBytecodeTestUtil.kt @@ -0,0 +1,55 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor.SetDriverMethodVisitor +import java.io.FileInputStream +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode + +internal object SQLiteDriverBytecodeTestUtil { + + private const val FIXTURES_ROOT = "src/test/resources/testFixtures/instrumentation/androidxRoom" + + /** + * Room `Builder` bytecode fixtures extracted from published AARs: + * - `RoomDatabase$Builder.class`: `androidx.room:room-runtime-android:2.7.0` + * - `RoomDatabase3$Builder.class`: `androidx.room3:room3-runtime-android:3.0.0-alpha06` + * + * Extract from Google Maven by unzipping each AAR's `classes.jar` and copying + * `androidx/room/RoomDatabase$Builder.class` (or `androidx/room3/...`). + * + * `VisitorTest` needs matching Room runtime AARs (and coroutines) on the test classpath so ASM + * can resolve types referenced by the real bytecode. + */ + private val CLASS_NAME_TO_FIXTURE = + mapOf( + "androidx.room.RoomDatabase\$Builder" to "RoomDatabase\$Builder", + "androidx.room3.RoomDatabase\$Builder" to "RoomDatabase3\$Builder", + ) + + fun loadRoomBuilderFixture(className: String): ByteArray { + val fixtureName = + CLASS_NAME_TO_FIXTURE[className] ?: error("No committed fixture for class $className") + return FileInputStream("$FIXTURES_ROOT/$fixtureName.class").use { it.readBytes() } + } + + fun isWrapCall(insn: MethodInsnNode): Boolean = + insn.opcode == Opcodes.INVOKESTATIC && + insn.owner == Type.getType(SetDriverMethodVisitor.SENTRY_SQLITE_DRIVER_TYPE).internalName && + insn.name == SetDriverMethodVisitor.CREATE && + insn.desc == SetDriverMethodVisitor.SENTRY_CREATE_DESCRIPTOR + + fun isSetDriverDescriptor(descriptor: String): Boolean = + descriptor.startsWith(SetDriverMethodInstrumentable.SET_DRIVER_DESCRIPTOR_PREFIX) + + fun countWrapCalls(bytes: ByteArray): Int { + val classNode = ClassNode().also { ClassReader(bytes).accept(it, 0) } + return classNode.methods.sumOf(::countWrapCalls) + } + + fun countWrapCalls(method: MethodNode): Int = + method.instructions.filterIsInstance().count(::isWrapCall) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt new file mode 100644 index 000000000..421dabeb0 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/SetDriverMethodInstrumentableTest.kt @@ -0,0 +1,48 @@ +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver + +import io.sentry.android.gradle.instrumentation.MethodContext +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class SetDriverMethodInstrumentableTest { + + private val instrumentable = SetDriverMethodInstrumentable() + + @Test + fun `isInstrumentable returns true for setDriver with SQLiteDriver parameter`() { + assertTrue( + instrumentable.isInstrumentable( + methodContext( + SetDriverMethodInstrumentable.SET_DRIVER, + "${SetDriverMethodInstrumentable.SET_DRIVER_DESCRIPTOR_PREFIX}Landroidx/room/RoomDatabase\$Builder;", + ) + ) + ) + } + + @Test + fun `isInstrumentable returns false for unrelated method names`() { + assertFalse(instrumentable.isInstrumentable(methodContext("build", "()V"))) + } + + @Test + fun `isInstrumentable returns false for setDriver with non-SQLiteDriver descriptor`() { + assertFalse( + instrumentable.isInstrumentable( + methodContext(SetDriverMethodInstrumentable.SET_DRIVER, "(Ljava/lang/Object;)V") + ) + ) + } + + @Test + fun `isInstrumentable returns false when descriptor is null`() { + assertFalse( + instrumentable.isInstrumentable(methodContext(SetDriverMethodInstrumentable.SET_DRIVER, null)) + ) + } + + private fun methodContext(name: String, descriptor: String?) = + MethodContext(Opcodes.ACC_PUBLIC, name, descriptor, null, null) +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt new file mode 100644 index 000000000..10e3d0f58 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt @@ -0,0 +1,84 @@ +@file:Suppress("UnstableApiUsage") + +package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor + +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.SQLiteDriverBytecodeTestUtil +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.SetDriverMethodInstrumentable +import io.sentry.android.gradle.instrumentation.fakes.TestClassContext +import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode + +class SetDriverMethodVisitorTest { + + @get:Rule val tmpDir = TemporaryFolder() + + @Test + fun `wraps the driver parameter at the start of Room 2_x Builder setDriver`() { + assertSetDriverWrappedOnce("androidx.room.RoomDatabase\$Builder") + } + + @Test + fun `wraps the driver parameter at the start of Room 3_x Builder setDriver`() { + assertSetDriverWrappedOnce("androidx.room3.RoomDatabase\$Builder") + } + + private fun assertSetDriverWrappedOnce(className: String) { + val instrumentedBytes = instrument(className) + val setDriverMethod = findSetDriverMethod(instrumentedBytes) + + assertEquals( + 1, + SQLiteDriverBytecodeTestUtil.countWrapCalls(setDriverMethod), + "setDriver should contain exactly one wrap", + ) + assertTrue( + wrapPrecedesOriginalBody(setDriverMethod), + "SentrySQLiteDriver.create() must run before the original setDriver body", + ) + } + + private fun instrument(className: String): ByteArray { + val bytes = SQLiteDriverBytecodeTestUtil.loadRoomBuilderFixture(className) + val classReader = ClassReader(bytes) + val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) + val classVisitor = + AndroidXSQLiteDriver() + .getVisitor( + TestClassContext(className), + Opcodes.ASM9, + classWriter, + parameters = TestSpanAddingParameters(inMemoryDir = tmpDir.root), + ) + classReader.accept(classVisitor, ClassReader.SKIP_FRAMES) + return classWriter.toByteArray() + } + + private fun findSetDriverMethod(bytes: ByteArray): MethodNode { + val classNode = ClassNode().also { ClassReader(bytes).accept(it, 0) } + return classNode.methods.first { + it.name == SetDriverMethodInstrumentable.SET_DRIVER && + SQLiteDriverBytecodeTestUtil.isSetDriverDescriptor(it.desc) + } + } + + private fun wrapPrecedesOriginalBody(method: MethodNode): Boolean { + val realInsns = method.instructions.toArray().filter { it.opcode >= 0 } + val wrapIndex = + realInsns.indexOfFirst { it is MethodInsnNode && SQLiteDriverBytecodeTestUtil.isWrapCall(it) } + assertTrue(wrapIndex >= 0, "setDriver has no SentrySQLiteDriver.create call") + val returnIndex = + realInsns.indexOfFirst { it.opcode == Opcodes.ARETURN || it.opcode == Opcodes.RETURN } + return wrapIndex < returnIndex + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt index 50c8c5b0e..cfbe17c63 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt @@ -16,11 +16,13 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertTrue +import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.TaskOutcome import org.gradle.util.GradleVersion import org.hamcrest.CoreMatchers.`is` import org.junit.Assert.assertThrows import org.junit.Assume.assumeThat +import org.junit.Ignore import org.junit.Test class SentryPluginTest : @@ -529,6 +531,87 @@ class SentryPluginTest : } } + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `applies sqliteDriver instrumentable when sentry gate passes with room2`() { + val build = + buildDatabaseInstrumentation(SQLITE, ROOM2_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain( + build, + "AndroidXSQLiteOpenHelper", + "AndroidXSQLiteDriver", + "AndroidXRoomDao", + ) + } + + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `applies sqliteDriver instrumentable when sentry gate passes with room3`() { + val build = + buildDatabaseInstrumentation(SQLITE, ROOM3_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain( + build, + "AndroidXSQLiteOpenHelper", + "AndroidXSQLiteDriver", + "AndroidXRoomDao", + ) + } + + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate fails without room on classpath`() { + val build = + buildDatabaseInstrumentation(SQLITE_OLD, SENTRY_ANDROID_SQLITE_OPEN_HELPER, minSdk = null) + + assertInstrumentableChain(build, "AndroidXSQLiteOpenHelper", "AndroidXRoomDao") + } + + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate fails with room2`() { + val build = + buildDatabaseInstrumentation(SQLITE, ROOM2_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_OPEN_HELPER) + + assertInstrumentableChain(build, "AndroidXSQLiteOpenHelper", "AndroidXRoomDao") + } + + @Test + fun `does not apply sqliteDriver instrumentable when sentry gate fails with room3`() { + val build = + buildDatabaseInstrumentation(SQLITE, ROOM3_AT_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_OPEN_HELPER) + + assertInstrumentableChain(build, "AndroidXSQLiteOpenHelper", "AndroidXRoomDao") + } + + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `applies sqliteDriver instrumentable when sentry gate passes without room on classpath`() { + val build = buildDatabaseInstrumentation(SQLITE, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain( + build, + "AndroidXSQLiteOpenHelper", + "AndroidXSQLiteDriver", + "AndroidXRoomDao", + ) + } + + @Ignore(SQLITE_DRIVER_IGNORE_REASON) + @Test + fun `applies sqliteDriver instrumentable when sentry gate passes with room below 2_7`() { + val build = + buildDatabaseInstrumentation(SQLITE, ROOM2_BELOW_DRIVER_FLOOR, SENTRY_ANDROID_SQLITE_DRIVER) + + assertInstrumentableChain( + build, + "AndroidXSQLiteOpenHelper", + "AndroidXSQLiteDriver", + "AndroidXRoomDao", + ) + } + + // Database path when sentry-android-sqlite is absent (old AndroidXSQLiteDatabase/Statement). + // Orthogonal to the SQLiteDriver gate matrix above. @Test fun `apply old Database instrumentable when app does not depend on sentry-android-sqlite`() { applyTracingInstrumentation( @@ -1106,6 +1189,7 @@ class SentryPluginTest : excludes: Set = emptySet(), sdkVersion: String = "7.1.0", forceInstrumentDependencies: Boolean = true, + minSdk: Int? = null, ) { appBuildFile.appendText( // language=Groovy @@ -1131,8 +1215,63 @@ class SentryPluginTest : excludes = ["${excludes.joinToString()}"] } } + ${ + minSdk?.let { + """ + android { + defaultConfig { + minSdkVersion $it + } + } + """ + } ?: "" + } """ .trimIndent() ) } + + private fun buildDatabaseInstrumentation( + vararg dependencies: String, + minSdk: Int? = DRIVER_PATH_MIN_SDK, + ): BuildResult { + applyTracingInstrumentation( + features = setOf(InstrumentationFeature.DATABASE), + dependencies = dependencies.toSet(), + appStart = false, + logcat = false, + minSdk = minSdk, + ) + return runner.appendArguments(":app:assembleDebug", "--info").build() + } + + private fun assertInstrumentableChain(build: BuildResult, vararg expected: String) { + assertEquals(expected.toList(), instrumentables(build)) + } + + private fun instrumentables(build: BuildResult): List { + val line = + build.output.lines().first { + it.contains("[sentry] Instrumentable: ChainedInstrumentable(instrumentables=") + } + val prefix = "ChainedInstrumentable(instrumentables=" + val start = line.indexOf(prefix) + prefix.length + val end = line.lastIndexOf(')') + return line.substring(start, end).split(", ").filter { it.isNotEmpty() } + } + + companion object { + private const val SQLITE_DRIVER_IGNORE_REASON = + "Placeholder version VERSION_SQLITE_DRIVER not yet on Maven" + + private const val SQLITE = "androidx.sqlite:sqlite:2.6.2" + private const val SQLITE_OLD = "androidx.sqlite:sqlite:2.0.0" + private const val SENTRY_ANDROID_SQLITE_OPEN_HELPER = "io.sentry:sentry-android-sqlite:6.21.0" + private const val SENTRY_ANDROID_SQLITE_DRIVER = "io.sentry:sentry-android-sqlite:8.44.0" + private const val ROOM2_AT_DRIVER_FLOOR = "androidx.room:room-runtime:2.7.0" + private const val ROOM2_BELOW_DRIVER_FLOOR = "androidx.room:room-runtime:2.6.1" + private const val ROOM3_AT_DRIVER_FLOOR = "androidx.room3:room3-runtime:3.0.0-alpha06" + /** androidx.sqlite 2.6.x and room3-runtime both require minSdk 23 in the test fixture. */ + private const val DRIVER_PATH_MIN_SDK = 23 + } } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt new file mode 100644 index 000000000..74266bdef --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt @@ -0,0 +1,164 @@ +package io.sentry.android.gradle.util + +import io.sentry.android.gradle.extensions.InstrumentationFeature +import io.sentry.android.gradle.services.SentryModulesService +import java.io.File +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.gradle.api.artifacts.ModuleIdentifier +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class SentryModulesServiceTest { + + class Fixture { + + data class Sut(val service: SentryModulesService, val project: org.gradle.api.Project) + + fun getSut( + tmpDir: File, + features: Set = emptySet(), + sentryModules: Map = emptyMap(), + ): Sut { + val fakeProject = ProjectBuilder.builder().withProjectDir(tmpDir).build() + + val featureProvider = fakeProject.provider { features } + val logcatEnabled = fakeProject.provider { true } + val sourceContextEnabled = fakeProject.provider { false } + val dexguardEnabled = fakeProject.provider { false } + val appStartEnabled = fakeProject.provider { false } + + val serviceProvider = + SentryModulesService.register( + fakeProject, + featureProvider, + logcatEnabled, + sourceContextEnabled, + dexguardEnabled, + appStartEnabled, + ) + val service = serviceProvider.get() + service.sentryModules = sentryModules + return Sut(service, fakeProject) + } + } + + @get:Rule val testProjectDir = TemporaryFolder() + + private val fixture = Fixture() + + private fun sqliteDriverSentryModules(): Map = + mapOf(SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE_DRIVER) + + @Test + fun `isSQLiteDriverInstrEnabled is true when sentry-android-sqlite meets threshold and DATABASE is enabled`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = sqliteDriverSentryModules(), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is absent from classpath`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = emptyMap(), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is below VERSION_SQLITE`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = mapOf(SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is one minor below VERSION_SQLITE_DRIVER`() { + val belowThreshold = + SemVer( + SentryVersions.VERSION_SQLITE_DRIVER.major, + SentryVersions.VERSION_SQLITE_DRIVER.minor - 1, + SentryVersions.VERSION_SQLITE_DRIVER.patch, + ) + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = mapOf(SentryModules.SENTRY_ANDROID_SQLITE to belowThreshold), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `isSQLiteDriverInstrEnabled is false when DATABASE is disabled`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = emptySet(), + sentryModules = sqliteDriverSentryModules(), + ) + + assertFalse(service.isSQLiteDriverInstrEnabled()) + } + + @Test + fun `VERSION_SQLITE_DRIVER is greater than or equal to VERSION_SQLITE`() { + // Gating relies on the presence of the open helper whenever the driver is present: both + // instrumentables fire together and we rely on SentrySQLiteDriver.create to dedup the + // SupportSQLiteDriver bridge case. + assertTrue(SentryVersions.VERSION_SQLITE_DRIVER >= SentryVersions.VERSION_SQLITE) + } + + @Test + fun `between VERSION_SQLITE and VERSION_SQLITE_DRIVER only the open-helper path is on`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = + mapOf( + SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE, + SentryModules.SENTRY_ANDROID_CORE to SentryVersions.VERSION_PERFORMANCE, + ), + ) + + assertTrue(service.isNewDatabaseInstrEnabled()) + assertFalse(service.isSQLiteDriverInstrEnabled()) + assertFalse(service.isOldDatabaseInstrEnabled()) + } + + @Test + fun `at VERSION_SQLITE_DRIVER the open-helper path is also on and the old path is off`() { + val (service, _) = + fixture.getSut( + tmpDir = testProjectDir.root, + features = setOf(InstrumentationFeature.DATABASE), + sentryModules = + mapOf( + SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE_DRIVER, + SentryModules.SENTRY_ANDROID_CORE to SentryVersions.VERSION_PERFORMANCE, + ), + ) + + assertTrue(service.isSQLiteDriverInstrEnabled()) + assertTrue(service.isNewDatabaseInstrEnabled()) // superset relationship + assertFalse(service.isOldDatabaseInstrEnabled()) // suppressed by !isNewDatabaseInstrEnabled() + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt b/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt new file mode 100644 index 000000000..7cd08376b --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt @@ -0,0 +1,9 @@ +package io.sentry.sqlite + +import androidx.sqlite.SQLiteDriver + +class SentrySQLiteDriver private constructor(private val delegate: SQLiteDriver) : SQLiteDriver { + companion object { + @JvmStatic fun create(delegate: SQLiteDriver): SQLiteDriver = SentrySQLiteDriver(delegate) + } +} diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase$Builder.class b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase$Builder.class new file mode 100644 index 0000000000000000000000000000000000000000..88e8733f1fb42234ff347606f9b788771abcea9b GIT binary patch literal 30384 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 HviE-gtPZZ2 literal 0 HcmV?d00001 diff --git a/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase3$Builder.class b/plugin-build/src/test/resources/testFixtures/instrumentation/androidxRoom/RoomDatabase3$Builder.class new file mode 100644 index 0000000000000000000000000000000000000000..57cc73de25f9e4c6a06f1beae49d4a2d5969c725 GIT binary patch literal 26703 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 Horm^64Sg@Q literal 0 HcmV?d00001 From 2ff8ac580542559cbe57f8d459b6abb9c805a257 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 10 Jun 2026 23:28:54 +0200 Subject: [PATCH 2/2] Minor doc updates, DRYing, and test refinements --- .../TracingInstrumentationExtension.kt | 2 +- .../sqlite/driver/AndroidXSQLiteDriver.kt | 8 ++-- .../driver/visitor/SetDriverMethodVisitor.kt | 3 +- .../gradle/services/SentryModulesService.kt | 2 +- .../kotlin/androidx/sqlite/SQLiteDriver.kt | 9 +++++ .../sqlite/driver/AndroidXSQLiteDriverTest.kt | 40 +++++++++++++++---- .../visitor/SetDriverMethodVisitorTest.kt | 11 +++-- .../gradle/integration/SentryPluginTest.kt | 15 +++---- .../gradle/util/SentryModulesServiceTest.kt | 30 ++++---------- .../io/sentry/sqlite/SentrySQLiteDriver.kt | 5 +++ 10 files changed, 76 insertions(+), 49 deletions(-) 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 a7ee1171a..2ff69cd0a 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 @@ -82,7 +82,7 @@ enum class InstrumentationFeature(val integrationName: String) { * * **DAO method execution** (`db.sql.room` spans): Wraps each public method on Room's generated * `@Dao` `_Impl` classes, measuring the full DAO call end-to-end (transaction management, query - * execution, and cursor processing). Only for Room users and only on versions prior to Room 2.7. + * execution, and cursor processing). Only for Room users on < Room 2.7. * * This feature uses bytecode manipulation. */ diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt index f207e5ae1..f3872b8a9 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriver.kt @@ -10,6 +10,9 @@ import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor.S import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor +/** JVM type descriptor for `androidx.sqlite.SQLiteDriver`. */ +internal const val SQLITE_DRIVER_TYPE_DESCRIPTOR = "Landroidx/sqlite/SQLiteDriver;" + /** * Auto-instruments `SQLiteDriver` for all Room users by wrapping any driver passed to * `RoomDatabase.Builder.setDriver(SQLiteDriver)`. @@ -70,9 +73,6 @@ class AndroidXSQLiteDriver : ClassInstrumentable { class SetDriverMethodInstrumentable : MethodInstrumentable { - override val fqName: String - get() = SET_DRIVER - override fun getVisitor( instrumentableContext: MethodContext, apiVersion: Int, @@ -85,6 +85,6 @@ class SetDriverMethodInstrumentable : MethodInstrumentable { companion object { internal const val SET_DRIVER = "setDriver" - internal const val SET_DRIVER_DESCRIPTOR_PREFIX = "(Landroidx/sqlite/SQLiteDriver;)" + internal const val SET_DRIVER_DESCRIPTOR_PREFIX = "($SQLITE_DRIVER_TYPE_DESCRIPTOR)" } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt index 7fe22cbee..1dfdf62ff 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitor.kt @@ -1,6 +1,7 @@ package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor import io.sentry.android.gradle.instrumentation.MethodContext +import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.SQLITE_DRIVER_TYPE_DESCRIPTOR import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Type import org.objectweb.asm.commons.AdviceAdapter @@ -28,7 +29,7 @@ class SetDriverMethodVisitor( companion object { internal const val CREATE = "create" internal const val SENTRY_CREATE_DESCRIPTOR = - "(Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;" + "($SQLITE_DRIVER_TYPE_DESCRIPTOR)$SQLITE_DRIVER_TYPE_DESCRIPTOR" internal const val SENTRY_SQLITE_DRIVER_TYPE = "Lio/sentry/sqlite/SentrySQLiteDriver;" } } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt index 62bf25c3f..83101a6fa 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt @@ -77,7 +77,7 @@ abstract class SentryModulesService : * `SentrySQLiteDriver` and the DATABASE feature is enabled. * * Room version is not gated here: Room < 2.7 has no matching `setDriver` method, so - * instrumentation is a no-op. Method descriptor matching is the safety boundary. + * instrumentation is a no-op. */ fun isSQLiteDriverInstrEnabled(): Boolean = sentryModules.isAtLeast( diff --git a/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt b/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt index 187e5e6a8..7e3944dcf 100644 --- a/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt +++ b/plugin-build/src/test/kotlin/androidx/sqlite/SQLiteDriver.kt @@ -1,3 +1,12 @@ package androidx.sqlite +/** + * Minimal stub of `androidx.sqlite.SQLiteDriver` so ASM can resolve the type referenced by the + * instrumented bytecode. + * + * Must not coexist with androidx.sqlite >= 2.5 on the test classpath: that version introduces the + * real `SQLiteDriver`, which would collide with this stub. Safe today because `libs.sqlite` is + * pinned below 2.5 and `testImplementationAar` does not pull transitive dependencies (Room 2.7+ + * depends on androidx.sqlite 2.5+, but `Aar2JarPlugin` sets `isTransitive = false`). + */ interface SQLiteDriver diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt index bcc844eeb..458413a67 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/AndroidXSQLiteDriverTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver import io.sentry.android.gradle.instrumentation.ChainedInstrumentable +import io.sentry.android.gradle.instrumentation.ClassInstrumentable import io.sentry.android.gradle.instrumentation.fakes.TestClassContext import io.sentry.android.gradle.instrumentation.fakes.TestSpanAddingParameters import java.io.FileInputStream @@ -48,21 +49,44 @@ class AndroidXSQLiteDriverTest { assertEquals(0, SQLiteDriverBytecodeTestUtil.countWrapCalls(instrumentedBytes)) } - private fun instrumentThroughChain(className: String, bytes: ByteArray): ByteArray { + @Test + fun `does not wrap when a visited class has no setDriver method`() { + // The Room < 2.7 production path: RoomDatabase$Builder matches the allowlist (so getVisitor + // runs), but the class has no setDriver(SQLiteDriver) and must pass through without a wrap. + val instrumentedBytes = + instrumentDirectly("com.example.NoSetDriver", loadNoSetDriverFixtureBytes()) + + assertEquals(0, SQLiteDriverBytecodeTestUtil.countWrapCalls(instrumentedBytes)) + } + + private fun instrumentThroughChain(className: String, bytes: ByteArray): ByteArray = + instrument(ChainedInstrumentable(listOf(instrumentable)), className, bytes) + + private fun instrumentDirectly(className: String, bytes: ByteArray): ByteArray = + instrument(instrumentable, className, bytes) + + private fun instrument( + classInstrumentable: ClassInstrumentable, + className: String, + bytes: ByteArray, + ): ByteArray { val classReader = ClassReader(bytes) val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) val classVisitor = - ChainedInstrumentable(listOf(instrumentable)) - .getVisitor( - TestClassContext(className), - Opcodes.ASM9, - classWriter, - parameters = TestSpanAddingParameters(inMemoryDir = tmpDir.root), - ) + classInstrumentable.getVisitor( + TestClassContext(className), + Opcodes.ASM9, + classWriter, + parameters = TestSpanAddingParameters(inMemoryDir = tmpDir.root), + ) classReader.accept(classVisitor, ClassReader.SKIP_FRAMES) return classWriter.toByteArray() } + /** + * `NoSetDriver.class`: hand-compiled `public class NoSetDriver { int unrelated(int) }`. Shape is + * irrelevant / any class without `setDriver(SQLiteDriver)` works. + */ private fun loadNoSetDriverFixtureBytes(): ByteArray = FileInputStream( "src/test/resources/testFixtures/instrumentation/androidxSqliteDriver/NoSetDriver.class" diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt index 10e3d0f58..92f728473 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/androidx/sqlite/driver/visitor/SetDriverMethodVisitorTest.kt @@ -16,6 +16,7 @@ import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassWriter import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode @@ -77,8 +78,12 @@ class SetDriverMethodVisitorTest { val wrapIndex = realInsns.indexOfFirst { it is MethodInsnNode && SQLiteDriverBytecodeTestUtil.isWrapCall(it) } assertTrue(wrapIndex >= 0, "setDriver has no SentrySQLiteDriver.create call") - val returnIndex = - realInsns.indexOfFirst { it.opcode == Opcodes.ARETURN || it.opcode == Opcodes.RETURN } - return wrapIndex < returnIndex + val firstOriginalBodyIndex = + realInsns.indexOfFirst { + (it is MethodInsnNode && !SQLiteDriverBytecodeTestUtil.isWrapCall(it)) || + it is FieldInsnNode + } + assertTrue(firstOriginalBodyIndex >= 0, "setDriver fixture has no recognizable original body") + return wrapIndex < firstOriginalBodyIndex } } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt index cfbe17c63..18d5de702 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt @@ -4,6 +4,7 @@ import io.sentry.BuildConfig import io.sentry.android.gradle.extensions.InstrumentationFeature import io.sentry.android.gradle.util.AgpVersions import io.sentry.android.gradle.util.SemVer +import io.sentry.android.gradle.util.SentryVersions import io.sentry.android.gradle.verifyDebugMetaPropertiesNotInApk import io.sentry.android.gradle.verifyDependenciesReportAndroid import io.sentry.android.gradle.verifyIntegrationList @@ -561,8 +562,7 @@ class SentryPluginTest : @Test fun `does not apply sqliteDriver instrumentable when sentry gate fails without room on classpath`() { - val build = - buildDatabaseInstrumentation(SQLITE_OLD, SENTRY_ANDROID_SQLITE_OPEN_HELPER, minSdk = null) + val build = buildDatabaseInstrumentation(SQLITE, SENTRY_ANDROID_SQLITE_OPEN_HELPER) assertInstrumentableChain(build, "AndroidXSQLiteOpenHelper", "AndroidXRoomDao") } @@ -1231,16 +1231,13 @@ class SentryPluginTest : ) } - private fun buildDatabaseInstrumentation( - vararg dependencies: String, - minSdk: Int? = DRIVER_PATH_MIN_SDK, - ): BuildResult { + private fun buildDatabaseInstrumentation(vararg dependencies: String): BuildResult { applyTracingInstrumentation( features = setOf(InstrumentationFeature.DATABASE), dependencies = dependencies.toSet(), appStart = false, logcat = false, - minSdk = minSdk, + minSdk = DRIVER_PATH_MIN_SDK, ) return runner.appendArguments(":app:assembleDebug", "--info").build() } @@ -1265,9 +1262,9 @@ class SentryPluginTest : "Placeholder version VERSION_SQLITE_DRIVER not yet on Maven" private const val SQLITE = "androidx.sqlite:sqlite:2.6.2" - private const val SQLITE_OLD = "androidx.sqlite:sqlite:2.0.0" private const val SENTRY_ANDROID_SQLITE_OPEN_HELPER = "io.sentry:sentry-android-sqlite:6.21.0" - private const val SENTRY_ANDROID_SQLITE_DRIVER = "io.sentry:sentry-android-sqlite:8.44.0" + private val SENTRY_ANDROID_SQLITE_DRIVER = + "io.sentry:sentry-android-sqlite:${SentryVersions.VERSION_SQLITE_DRIVER}" private const val ROOM2_AT_DRIVER_FLOOR = "androidx.room:room-runtime:2.7.0" private const val ROOM2_BELOW_DRIVER_FLOOR = "androidx.room:room-runtime:2.6.1" private const val ROOM3_AT_DRIVER_FLOOR = "androidx.room3:room3-runtime:3.0.0-alpha06" diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt index 74266bdef..6ada4e95d 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/util/SentryModulesServiceTest.kt @@ -15,13 +15,11 @@ class SentryModulesServiceTest { class Fixture { - data class Sut(val service: SentryModulesService, val project: org.gradle.api.Project) - fun getSut( tmpDir: File, features: Set = emptySet(), sentryModules: Map = emptyMap(), - ): Sut { + ): SentryModulesService { val fakeProject = ProjectBuilder.builder().withProjectDir(tmpDir).build() val featureProvider = fakeProject.provider { features } @@ -41,7 +39,7 @@ class SentryModulesServiceTest { ) val service = serviceProvider.get() service.sentryModules = sentryModules - return Sut(service, fakeProject) + return service } } @@ -54,7 +52,7 @@ class SentryModulesServiceTest { @Test fun `isSQLiteDriverInstrEnabled is true when sentry-android-sqlite meets threshold and DATABASE is enabled`() { - val (service, _) = + val service = fixture.getSut( tmpDir = testProjectDir.root, features = setOf(InstrumentationFeature.DATABASE), @@ -66,7 +64,7 @@ class SentryModulesServiceTest { @Test fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is absent from classpath`() { - val (service, _) = + val service = fixture.getSut( tmpDir = testProjectDir.root, features = setOf(InstrumentationFeature.DATABASE), @@ -76,18 +74,6 @@ class SentryModulesServiceTest { assertFalse(service.isSQLiteDriverInstrEnabled()) } - @Test - fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is below VERSION_SQLITE`() { - val (service, _) = - fixture.getSut( - tmpDir = testProjectDir.root, - features = setOf(InstrumentationFeature.DATABASE), - sentryModules = mapOf(SentryModules.SENTRY_ANDROID_SQLITE to SentryVersions.VERSION_SQLITE), - ) - - assertFalse(service.isSQLiteDriverInstrEnabled()) - } - @Test fun `isSQLiteDriverInstrEnabled is false when sentry-android-sqlite is one minor below VERSION_SQLITE_DRIVER`() { val belowThreshold = @@ -96,7 +82,7 @@ class SentryModulesServiceTest { SentryVersions.VERSION_SQLITE_DRIVER.minor - 1, SentryVersions.VERSION_SQLITE_DRIVER.patch, ) - val (service, _) = + val service = fixture.getSut( tmpDir = testProjectDir.root, features = setOf(InstrumentationFeature.DATABASE), @@ -108,7 +94,7 @@ class SentryModulesServiceTest { @Test fun `isSQLiteDriverInstrEnabled is false when DATABASE is disabled`() { - val (service, _) = + val service = fixture.getSut( tmpDir = testProjectDir.root, features = emptySet(), @@ -128,7 +114,7 @@ class SentryModulesServiceTest { @Test fun `between VERSION_SQLITE and VERSION_SQLITE_DRIVER only the open-helper path is on`() { - val (service, _) = + val service = fixture.getSut( tmpDir = testProjectDir.root, features = setOf(InstrumentationFeature.DATABASE), @@ -146,7 +132,7 @@ class SentryModulesServiceTest { @Test fun `at VERSION_SQLITE_DRIVER the open-helper path is also on and the old path is off`() { - val (service, _) = + val service = fixture.getSut( tmpDir = testProjectDir.root, features = setOf(InstrumentationFeature.DATABASE), diff --git a/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt b/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt index 7cd08376b..39140f805 100644 --- a/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt +++ b/plugin-build/src/test/kotlin/io/sentry/sqlite/SentrySQLiteDriver.kt @@ -2,6 +2,11 @@ package io.sentry.sqlite import androidx.sqlite.SQLiteDriver +/** + * Minimal stub of `io.sentry.sqlite.SentrySQLiteDriver` so ASM can resolve the `INVOKESTATIC + * io/sentry/sqlite/SentrySQLiteDriver.create` emitted by [SetDriverMethodVisitor]. The `@JvmStatic + * create(SQLiteDriver)` shape here mirrors the SDK contract the visitor depends on. + */ class SentrySQLiteDriver private constructor(private val delegate: SQLiteDriver) : SQLiteDriver { companion object { @JvmStatic fun create(delegate: SQLiteDriver): SQLiteDriver = SentrySQLiteDriver(delegate)