Skip to content

Commit 5ae931e

Browse files
committed
chore(android-sqlite): Skip wrapping SupportSQLiteDriver bridge to avoid duplicate spans
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 a16a9da commit 5ae931e

4 files changed

Lines changed: 52 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Automatically generates spans for all SQLite statements
99
- To use it, pass your `SQLiteDriver` to `SentrySQLiteDriver.create(...)`
1010
- You'll need `androidx.sqlite:sqlite` (2.5.0+) on your app's classpath (Room usually provides it for you). androidx.sqlite 2.6.0+ requires minSdk 23.
11+
- The Room 2.7+ `androidx.sqlite.driver.SupportSQLiteDriver` bridge adapter is recognized and skipped by `SentrySQLiteDriver.create(...)` so apps that wrap both the open helper and the bridge driver do not emit duplicate spans. Spans come from the open helper layer in that configuration.
1112
- See https://docs.sentry.io/platforms/android/integrations/room-and-sqlite/ for more details, including info about migrating from `SentrySupportSQLiteOpenHelper`
1213

1314
## 8.43.1

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ import io.sentry.SentryLevel
2121
* .build()
2222
* ```
2323
*
24-
* **Warning:** Do not use [SentrySQLiteDriver] together with
25-
* [SentrySupportSQLiteOpenHelper][io.sentry.android.sqlite.SentrySupportSQLiteOpenHelper] on the
26-
* same database file. Both wrappers instrument at different layers, so combining them will produce
27-
* duplicate spans for every SQL statement.
24+
* Note: In order to avoid duplicate spans, wrapping no-ops in the case of the
25+
* `androidx.sqlite.driver.SupportSQLiteDriver`. Wrap the open helper passed to its constructor via
26+
* `SentrySupportSQLiteOpenHelper` instead.
2827
*
2928
* @param delegate The [SQLiteDriver] instance to delegate calls to.
3029
*/
@@ -68,8 +67,22 @@ public class SentrySQLiteDriver private constructor(private val delegate: SQLite
6867

6968
public companion object {
7069

70+
/**
71+
* Fully-qualified class name of the bridge adapter often used with Room 2.7+. It implements the
72+
* `SQLiteDriver` interface and its constructor consumes a `SupportSQLiteOpenHelper`. (Users of
73+
* the Sentry Android Gradle Plugin will have the `SupportSQLiteOpenHelper` wrapped for them
74+
* automatically.) We deliberately avoid wrapping the adapter to prevent duplicate spans.
75+
*/
76+
private const val SUPPORT_SQLITE_DRIVER_FQN = "androidx.sqlite.driver.SupportSQLiteDriver"
77+
7178
@JvmStatic
7279
public fun create(delegate: SQLiteDriver): SQLiteDriver =
73-
delegate as? SentrySQLiteDriver ?: SentrySQLiteDriver(delegate)
80+
// String rather than an `is` check for SupportSQLiteDriver to avoid a compile-time dependency
81+
// on androidx.sqlite:sqlite-framework.
82+
if (delegate is SentrySQLiteDriver || delegate.javaClass.name == SUPPORT_SQLITE_DRIVER_FQN) {
83+
delegate
84+
} else {
85+
SentrySQLiteDriver(delegate)
86+
}
7487
}
7588
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
* The production check is `delegate.javaClass.name ==
12+
* "androidx.sqlite.driver.SupportSQLiteDriver"`, so any class with this exact fully-qualified name
13+
* exercises the branch.
14+
*/
15+
internal class SupportSQLiteDriver : SQLiteDriver {
16+
17+
override val hasConnectionPool: Boolean = false
18+
19+
override fun open(fileName: String): SQLiteConnection {
20+
throw UnsupportedOperationException("Test stub; not for runtime use")
21+
}
22+
}

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)

0 commit comments

Comments
 (0)