Skip to content

Commit 1c821b7

Browse files
committed
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.
1 parent 5870305 commit 1c821b7

22 files changed

Lines changed: 768 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
## Unreleased
44

5-
### Dependencies
5+
### Features
66

7-
- 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))
7+
- Auto-instrument SQLiteDriver for Room users ([#1285](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1285))
8+
- Gated on `sentry-android-sqlite` >= 8.44.0 and the existing `tracingInstrumentation` `DATABASE` feature
9+
- 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)
810

911
### Fixes
1012

1113
- Resolve the sentry-cli path as a task input instead of memoizing it in a static field, fixing stale-path build failures when switching branches with the configuration cache enabled ([#1264](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1264))
1214
- This fixed the issue where sentry-cli could not be found (`A problem occurred starting process 'command ../sentry-cliXXX.exe'`)
1315
- 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))
1416

17+
### Dependencies
18+
19+
- 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))
20+
1521
## 6.10.0
1622

1723
### Features

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ sentrySpringBootJakarta = { group = "io.sentry", name = "sentry-spring-boot-star
6363
mockitoKotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version = "5.4.0" }
6464
arscLib = { group = "io.github.reandroid", name = "ARSCLib", version = "1.1.4" }
6565
zip4j = { group = "net.lingala.zip4j", name = "zip4j", version = "2.11.5" }
66+
# Versions must match RoomDatabase$Builder bytecode fixtures (see SQLiteDriverBytecodeTestUtil).
67+
roomRuntimeAndroid = { group = "androidx.room", name = "room-runtime-android", version = "2.7.0" }
68+
room3RuntimeAndroid = { group = "androidx.room3", name = "room3-runtime-android", version = "3.0.0-alpha06" }
6669

6770
# samples
6871
sample-androidx-recyclerView = "androidx.recyclerview:recyclerview:1.2.0"

plugin-build/build.gradle.kts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,17 @@ dependencies {
5555
testImplementation(libs.asmCommons)
5656

5757
// we need these dependencies for tests, because the bytecode verifier also analyzes superclasses
58+
testImplementationAar(libs.roomRuntimeAndroid)
59+
testImplementationAar(libs.room3RuntimeAndroid)
60+
testImplementationAar(libs.sentryAndroid)
61+
testImplementationAar(libs.sentryAndroidOkhttp)
5862
testImplementationAar(libs.sqlite)
5963
testImplementationAar(libs.sqliteFramework)
60-
testRuntimeOnly(files(androidSdkPath))
61-
testImplementationAar(libs.sentryAndroid)
64+
65+
testImplementation(libs.sample.coroutines.core)
6266
testImplementation(libs.sentryOkhttp)
63-
testImplementationAar(libs.sentryAndroidOkhttp)
67+
68+
testRuntimeOnly(files(androidSdkPath))
6469

6570
// Needed to read contents from APK/Source Bundles
6671
testImplementation(libs.arscLib)

plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/TracingInstrumentationExtension.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,19 @@ open class TracingInstrumentationExtension @Inject constructor(objects: ObjectFa
7575

7676
enum class InstrumentationFeature(val integrationName: String) {
7777
/**
78-
* When enabled the SDK will create spans for any CRUD operation performed by
79-
* 'androidx.sqlite.db.SupportSQLiteOpenHelper' and 'androidx.room'. This feature uses bytecode
80-
* manipulation.
78+
* When enabled the SDK will create spans for database operations at two levels:
79+
*
80+
* **SQL execution** (`db.sql.query` spans): wraps the low-level driver so each individual SQL
81+
* statement produces a span. Two mutually exclusive paths:
82+
* - `androidx.sqlite.db.SupportSQLiteOpenHelper` via any `SupportSQLiteOpenHelper.Factory`
83+
* (open-helper path)
84+
* - `androidx.sqlite.SQLiteDriver` via `RoomDatabase.Builder.setDriver` (driver path)
85+
*
86+
* **DAO method** (`db.sql.room` spans): wraps each public method on Room's generated `@Dao`
87+
* `_Impl` classes, measuring the full DAO call end-to-end (transaction management, query
88+
* execution, and cursor processing). Pre-Room 2.7 only.
89+
*
90+
* This feature uses bytecode manipulation.
8191
*/
8292
DATABASE("DatabaseInstrumentation"),
8393

plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SpanAddingClassVisitorFactory.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.sentry.android.gradle.instrumentation.androidx.compose.ComposeNavigati
1010
import io.sentry.android.gradle.instrumentation.androidx.room.AndroidXRoomDao
1111
import io.sentry.android.gradle.instrumentation.androidx.sqlite.AndroidXSQLiteOpenHelper
1212
import io.sentry.android.gradle.instrumentation.androidx.sqlite.database.AndroidXSQLiteDatabase
13+
import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.AndroidXSQLiteDriver
1314
import io.sentry.android.gradle.instrumentation.androidx.sqlite.statement.AndroidXSQLiteStatement
1415
import io.sentry.android.gradle.instrumentation.appstart.Application
1516
import io.sentry.android.gradle.instrumentation.appstart.ContentProvider
@@ -90,10 +91,12 @@ abstract class SpanAddingClassVisitorFactory :
9091
ChainedInstrumentable(
9192
listOfNotNull(
9293
AndroidXSQLiteOpenHelper().takeIf { sentryModulesService.isNewDatabaseInstrEnabled() },
94+
AndroidXSQLiteDriver().takeIf { sentryModulesService.isSQLiteDriverInstrEnabled() },
9395
AndroidXSQLiteDatabase().takeIf { sentryModulesService.isOldDatabaseInstrEnabled() },
9496
AndroidXSQLiteStatement(androidXSqliteFrameWorkVersion).takeIf {
9597
sentryModulesService.isOldDatabaseInstrEnabled()
9698
},
99+
// Note that DAO spans no longer work on Room 2.7+ or Room 3.0+ due to Room API changes.
97100
AndroidXRoomDao().takeIf {
98101
sentryModulesService.isNewDatabaseInstrEnabled() ||
99102
sentryModulesService.isOldDatabaseInstrEnabled()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver
2+
3+
import com.android.build.api.instrumentation.ClassContext
4+
import io.sentry.android.gradle.instrumentation.ClassInstrumentable
5+
import io.sentry.android.gradle.instrumentation.CommonClassVisitor
6+
import io.sentry.android.gradle.instrumentation.MethodContext
7+
import io.sentry.android.gradle.instrumentation.MethodInstrumentable
8+
import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory
9+
import io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor.SetDriverMethodVisitor
10+
import org.objectweb.asm.ClassVisitor
11+
import org.objectweb.asm.MethodVisitor
12+
13+
/**
14+
* Auto-instruments `SQLiteDriver` for all Room users by wrapping the driver parameter in
15+
* `RoomDatabase.Builder.setDriver(SQLiteDriver)`.
16+
*
17+
* Note: As of this writing, SQLDelight doesn't support `SQLiteDriver`
18+
* ([link](https://github.com/sqldelight/sqldelight/issues/6072)), and developers who use the driver
19+
* directly are expected to wrap it themselves.
20+
*
21+
* The SDK protects against duplicate wrappings, allowing the visitor to wrap the driver
22+
* unconditionally.
23+
*/
24+
class AndroidXSQLiteDriver : ClassInstrumentable {
25+
26+
override fun getVisitor(
27+
instrumentableContext: ClassContext,
28+
apiVersion: Int,
29+
originalVisitor: ClassVisitor,
30+
parameters: SpanAddingClassVisitorFactory.SpanAddingParameters,
31+
): ClassVisitor {
32+
val currentClassName = instrumentableContext.currentClassData.className
33+
34+
return CommonClassVisitor(
35+
apiVersion = apiVersion,
36+
classVisitor = originalVisitor,
37+
className = currentClassName.substringAfterLast('.'),
38+
methodInstrumentables = listOf(SetDriverMethodInstrumentable()),
39+
parameters = parameters,
40+
)
41+
}
42+
43+
// Instrument RoomDatabase.Builder.setDriver() in room-runtime and room3 directly.
44+
override fun isInstrumentable(data: ClassContext): Boolean =
45+
data.currentClassData.className in TARGET_CLASSES
46+
47+
companion object {
48+
49+
/** Currently covers Room 2 and Room 3 packages. Update as needed. */
50+
internal val TARGET_CLASSES =
51+
setOf("androidx.room.RoomDatabase\$Builder", "androidx.room3.RoomDatabase\$Builder")
52+
}
53+
}
54+
55+
class SetDriverMethodInstrumentable : MethodInstrumentable {
56+
57+
override val fqName: String
58+
get() = SET_DRIVER
59+
60+
override fun getVisitor(
61+
instrumentableContext: MethodContext,
62+
apiVersion: Int,
63+
originalVisitor: MethodVisitor,
64+
parameters: SpanAddingClassVisitorFactory.SpanAddingParameters,
65+
): MethodVisitor = SetDriverMethodVisitor(apiVersion, originalVisitor, instrumentableContext)
66+
67+
override fun isInstrumentable(data: MethodContext): Boolean =
68+
data.name == SET_DRIVER && data.descriptor?.startsWith(SET_DRIVER_DESCRIPTOR_PREFIX) == true
69+
70+
companion object {
71+
internal const val SET_DRIVER = "setDriver"
72+
internal const val SET_DRIVER_DESCRIPTOR_PREFIX = "(Landroidx/sqlite/SQLiteDriver;)"
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.sentry.android.gradle.instrumentation.androidx.sqlite.driver.visitor
2+
3+
import io.sentry.android.gradle.instrumentation.MethodContext
4+
import org.objectweb.asm.MethodVisitor
5+
import org.objectweb.asm.Type
6+
import org.objectweb.asm.commons.AdviceAdapter
7+
import org.objectweb.asm.commons.Method
8+
9+
class SetDriverMethodVisitor(
10+
apiVersion: Int,
11+
originalVisitor: MethodVisitor,
12+
instrumentableContext: MethodContext,
13+
) :
14+
AdviceAdapter(
15+
apiVersion,
16+
originalVisitor,
17+
instrumentableContext.access,
18+
instrumentableContext.name,
19+
instrumentableContext.descriptor,
20+
) {
21+
22+
override fun onMethodEnter() {
23+
loadArg(0)
24+
invokeStatic(Type.getType(SENTRY_SQLITE_DRIVER_TYPE), Method(CREATE, SENTRY_CREATE_DESCRIPTOR))
25+
storeArg(0)
26+
}
27+
28+
companion object {
29+
internal const val CREATE = "create"
30+
internal const val SENTRY_CREATE_DESCRIPTOR =
31+
"(Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;"
32+
internal const val SENTRY_SQLITE_DRIVER_TYPE = "Lio/sentry/sqlite/SentrySQLiteDriver;"
33+
}
34+
}

plugin-build/src/main/kotlin/io/sentry/android/gradle/services/SentryModulesService.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ abstract class SentryModulesService :
7272
sentryModules.isAtLeast(SentryModules.SENTRY_ANDROID_SQLITE, SentryVersions.VERSION_SQLITE) &&
7373
parameters.features.get().contains(InstrumentationFeature.DATABASE)
7474

75+
/**
76+
* Returns true when the owning app uses a version of sentry-android-sqlite that contains
77+
* `SentrySQLiteDriver` and the DATABASE feature is enabled.
78+
*
79+
* Room version is not gated here: Room < 2.7 has no matching `setDriver` method, so
80+
* instrumentation is a no-op. Method descriptor matching is the safety boundary.
81+
*/
82+
fun isSQLiteDriverInstrEnabled(): Boolean =
83+
sentryModules.isAtLeast(
84+
SentryModules.SENTRY_ANDROID_SQLITE,
85+
SentryVersions.VERSION_SQLITE_DRIVER,
86+
) && parameters.features.get().contains(InstrumentationFeature.DATABASE)
87+
7588
fun isOldDatabaseInstrEnabled(): Boolean =
7689
!isNewDatabaseInstrEnabled() &&
7790
sentryModules.isAtLeast(

plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal object SentryVersions {
3333
internal val VERSION_LOGCAT = SemVer(6, 17, 0)
3434
internal val VERSION_APP_START = SemVer(7, 1, 0)
3535
internal val VERSION_SQLITE = SemVer(6, 21, 0)
36+
internal val VERSION_SQLITE_DRIVER = SemVer(8, 44, 0)
3637
internal val VERSION_ANDROID_OKHTTP_LISTENER = SemVer(6, 20, 0)
3738
internal val VERSION_OKHTTP = SemVer(7, 0, 0)
3839
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package androidx.sqlite
2+
3+
interface SQLiteDriver

0 commit comments

Comments
 (0)