Skip to content

Commit 7c1a728

Browse files
authored
chore(android-sqlite): Skip wrapping SupportSQLiteDriver bridge to avoid duplicate spans (#5514)
SentrySQLiteDriver.create() now recognizes the Room 2.7+ androidx.sqlite.driver.SupportSQLiteDriver bridge adapter and returns it unwrapped. That lets us protect against the one known vector where using both SentrySQLiteDriver and SentrySupportSQLiteOpenHelper with the same db table is allowed under either the Room or SQLDelight APIs: ```kotlin // AVOID — this configuration produces duplicate spans for every SQL statement. // Step 1: Developer wraps their open helper with Sentry, either manually or // via the Sentry Android Gradle Plugin. val sentryWrappedHelper: SupportSQLiteOpenHelper = SentrySupportSQLiteOpenHelper.create( FrameworkSQLiteOpenHelperFactory().create(configuration) ) // Step 2: Developer builds the compat driver around that wrapped helper. val driver: SQLiteDriver = SupportSQLiteDriver(sentryWrappedHelper) // Step 3: Developer (wrongly!) wraps the driver with Sentry as well. All // spans will now be duplicated. val sentryWrappedDriver: SQLiteDriver = SentrySQLiteDriver.create(driver) Room.databaseBuilder(context, MyDb::class.java, "mydb") .setDriver(sentryWrappedDriver) .build() ``` This commit lets us avoid step 3 by no-op'ing if a developer tries to pass a SupportSQLiteDriver to SentrySQLiteDriver.create().
1 parent f6192aa commit 7c1a728

9 files changed

Lines changed: 412 additions & 62 deletions

File tree

sentry-android-sqlite/proguard-rules.pro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@
44
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
55
-keepattributes LineNumberTable,SourceFile
66

7+
# SentrySQLiteDriver.create() uses a runtime class-name check to skip wrapping the Room 2.7+
8+
# SupportSQLiteDriver bridge adapter and avoid duplicate spans.
9+
-keepnames class androidx.sqlite.driver.SupportSQLiteDriver
10+
711
##---------------End: proguard configuration for SQLite ----------

sentry-android-sqlite/src/main/java/io/sentry/sqlite/SentrySQLiteDriver.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,6 @@ import org.jetbrains.annotations.ApiStatus
2222
* .build()
2323
* ```
2424
*
25-
* **Warning:** Do not use [SentrySQLiteDriver] together with
26-
* [SentrySupportSQLiteOpenHelper][io.sentry.android.sqlite.SentrySupportSQLiteOpenHelper] on the
27-
* same database file. Both wrappers instrument at different layers and combining them will produce
28-
* duplicate spans.
29-
*
3025
* @param delegate The [SQLiteDriver] instance to delegate calls to.
3126
*/
3227
@ApiStatus.Experimental
@@ -73,11 +68,32 @@ public class SentrySQLiteDriver private constructor(private val delegate: SQLite
7368
public companion object {
7469

7570
/**
76-
* Wraps the provided delegate in a [SentrySQLiteDriver]. Returns the delegate as-is if already
77-
* wrapped.
71+
* Name of the bridge adapter often used with Room 2.7+. It implements the `SQLiteDriver`
72+
* interface and its constructor consumes a `SupportSQLiteOpenHelper`. (Users of the Sentry
73+
* Android Gradle Plugin will have the `SupportSQLiteOpenHelper` wrapped for them
74+
* automatically.) We deliberately avoid wrapping the adapter to prevent duplicate spans.
75+
*
76+
* String (rather than an `is` check) lets us avoid a compile-time dependency on
77+
* androidx.sqlite:sqlite-framework.
78+
*/
79+
private const val SUPPORT_SQLITE_DRIVER_FQN = "androidx.sqlite.driver.SupportSQLiteDriver"
80+
81+
/**
82+
* Wraps the provided delegate in a [SentrySQLiteDriver].
83+
*
84+
* To avoid duplicate spans, returns the delegate as-is if:
85+
* 1. it's already wrapped, or
86+
* 2. it's an `androidx.sqlite.driver.SupportSQLiteDriver`.
87+
*
88+
* In the case of (2), wrap the open helper passed to the `SupportSQLiteDriver` constructor via
89+
* `SentrySupportSQLiteOpenHelper` instead.
7890
*/
7991
@JvmStatic
8092
public fun create(delegate: SQLiteDriver): SQLiteDriver =
81-
delegate as? SentrySQLiteDriver ?: SentrySQLiteDriver(delegate)
93+
if (delegate is SentrySQLiteDriver || delegate.javaClass.name == SUPPORT_SQLITE_DRIVER_FQN) {
94+
delegate
95+
} else {
96+
SentrySQLiteDriver(delegate)
97+
}
8298
}
8399
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package androidx.sqlite.driver
2+
3+
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.SQLiteDriver
5+
6+
/**
7+
* Minimal stub of `androidx.sqlite.driver.SupportSQLiteDriver` (which lives in
8+
* `androidx.sqlite:sqlite-framework`, not on this module's compile/test classpath) for verifying
9+
* behavior of `SentrySQLiteDriver.create(SupportSQLiteDriver)`.
10+
*/
11+
internal class SupportSQLiteDriver : SQLiteDriver {
12+
13+
override val hasConnectionPool: Boolean = false
14+
15+
override fun open(fileName: String): SQLiteConnection {
16+
throw UnsupportedOperationException("Test stub; not for runtime use")
17+
}
18+
}

sentry-android-sqlite/src/test/java/io/sentry/sqlite/SentrySQLiteDriverTest.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.sentry.sqlite
33
import androidx.sqlite.SQLiteConnection
44
import androidx.sqlite.SQLiteDriver
55
import androidx.sqlite.SQLiteStatement
6+
import androidx.sqlite.driver.SupportSQLiteDriver
67
import io.sentry.IScopes
78
import io.sentry.Sentry
89
import io.sentry.SentryIntegrationPackageStorage
@@ -64,6 +65,16 @@ class SentrySQLiteDriverTest {
6465
assertSame(wrapped, doubleWrapped)
6566
}
6667

68+
@Test
69+
fun `create with SupportSQLiteDriver bridge returns same instance without wrapping`() {
70+
val bridge = SupportSQLiteDriver()
71+
72+
val result = SentrySQLiteDriver.create(bridge)
73+
74+
assertSame(bridge, result)
75+
assertFalse(result is SentrySQLiteDriver)
76+
}
77+
6778
@Test
6879
fun `hasConnectionPool forwards delegate value when supported`() {
6980
whenever(fixture.mockDriver.hasConnectionPool).thenReturn(true)

sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/sqlite/DisplayInfo.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ internal val OPENHELPER_ROOM =
8787
.trimIndent(),
8888
)
8989

90+
// Bridge demos run the same SQL as the driver paths; spans come from the open-helper layer.
91+
internal val BRIDGE_DIRECT = DRIVER_DIRECT
92+
93+
internal val BRIDGE_ROOM2 = DRIVER_ROOM2
94+
9095
internal val OPENHELPER_SQLDELIGHT =
9196
DisplayInfo(
9297
sql =

0 commit comments

Comments
 (0)