Skip to content

Commit 2a55b58

Browse files
authored
Merge a590992 into adad078
2 parents adad078 + a590992 commit 2a55b58

16 files changed

Lines changed: 1043 additions & 28 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 8.44.0
4+
5+
### Features
6+
7+
- Add `SentrySQLiteDriver` for `SQLiteDriver` instrumentation in `sentry-android-sqlite` ([#5466](https://github.com/getsentry/sentry-java/pull/5466))
8+
- Wrap via `SentrySQLiteDriver.create(AndroidSQLiteDriver())` and `Room.databaseBuilder(...).setDriver(...)`
9+
- See https://docs.sentry.io/platforms/android/integrations/room-and-sqlite/ for info about migrating from `SentrySupportOpenSQLiteHelper`
10+
311
## 8.43.0
412

513
### Features

sentry-android-sqlite/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# sentry-android-sqlite
2+
3+
This module provides automatic SQLite query instrumentation for Android.
4+
5+
Two instrumentation paths are supported, matching the two SQLite APIs offered by AndroidX:
6+
7+
- **`androidx.sqlite.SQLiteDriver`** — used by Room 2.7+ via `Room.databaseBuilder(...).setDriver(...)` and by SQLDelight via its AndroidX SQLite driver.
8+
- **`androidx.sqlite.db.SupportSQLiteOpenHelper`** — used by legacy Room via `Room.databaseBuilder(...).openHelperFactory(...)`, or applied automatically by the Sentry Android Gradle plugin.
9+
10+
Please consult the [Sentry Docs](https://docs.sentry.io/platforms/android/integrations/room-and-sqlite/) for installation, usage, migration guidance, and how to avoid duplicate spans when both paths are present.
11+
12+
## Package layout
13+
14+
This module is organized as two separate packages:
15+
16+
- **`io.sentry.android.sqlite`**: Android-specific code. Classes here depend on `android.database.*` (e.g., `CrossProcessCursor`, `SQLException`) and/or on `androidx.sqlite.db.*`, the Android-only compatibility layer over the platform's SQLite. The `SentrySupportSQLiteOpenHelper` path and its span helper `SQLiteSpanManager` live here.
17+
- **`io.sentry.sqlite`**: Code whose contract depends only on the multiplatform `androidx.sqlite.*` interfaces (e.g., `SQLiteDriver` and `SQLiteConnection`). `SentrySQLiteDriver` and its span helper `SQLiteSpanRecorder` live here.
18+
19+
The split anticipates the possibility of future Kotlin Multiplatform support. The `androidx.sqlite.*` driver interfaces are defined in the library's `commonMain` source set and are reused by Room across Android, JVM, and native targets. Classes in `io.sentry.sqlite` are written against those portable interfaces and are intended to lift cleanly into a KMP `commonMain` source set if/when the `sentry` core gains multiplatform targets. Classes in `io.sentry.android.sqlite` are Android-only by construction and will stay where they are.
20+
21+
Note that the module artifact itself (`sentry-android-sqlite`) is currently an Android-only AAR regardless of package layout.

sentry-android-sqlite/api/sentry-android-sqlite.api

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,14 @@ public final class io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper$Compan
2121
public final fun create (Landroidx/sqlite/db/SupportSQLiteOpenHelper;)Landroidx/sqlite/db/SupportSQLiteOpenHelper;
2222
}
2323

24+
public final class io/sentry/sqlite/SentrySQLiteDriver : androidx/sqlite/SQLiteDriver {
25+
public static final field Companion Lio/sentry/sqlite/SentrySQLiteDriver$Companion;
26+
public synthetic fun <init> (Landroidx/sqlite/SQLiteDriver;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
27+
public static final fun create (Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;
28+
public fun open (Ljava/lang/String;)Landroidx/sqlite/SQLiteConnection;
29+
}
30+
31+
public final class io/sentry/sqlite/SentrySQLiteDriver$Companion {
32+
public final fun create (Landroidx/sqlite/SQLiteDriver;)Landroidx/sqlite/SQLiteDriver;
33+
}
34+

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

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,22 @@ import android.database.CrossProcessCursor
44
import android.database.SQLException
55
import io.sentry.IScopes
66
import io.sentry.ISpan
7-
import io.sentry.Instrumenter
87
import io.sentry.ScopesAdapter
98
import io.sentry.SentryIntegrationPackageStorage
10-
import io.sentry.SentryStackTraceFactory
11-
import io.sentry.SpanDataConvention
129
import io.sentry.SpanStatus
13-
14-
private const val TRACE_ORIGIN = "auto.db.sqlite"
10+
import io.sentry.sqlite.SQLITE_INTEGRATION_NAME
11+
import io.sentry.sqlite.SQLiteSpanHelper
12+
import io.sentry.sqlite.dbMetadataFromDatabaseName
1513

1614
internal class SQLiteSpanManager(
1715
private val scopes: IScopes = ScopesAdapter.getInstance(),
18-
private val databaseName: String? = null,
16+
databaseName: String? = null,
1917
) {
20-
private val stackTraceFactory = SentryStackTraceFactory(scopes.options)
18+
19+
private val spanHelper = SQLiteSpanHelper(scopes, dbMetadataFromDatabaseName(databaseName))
2120

2221
init {
23-
SentryIntegrationPackageStorage.getInstance().addIntegration("SQLite")
22+
SentryIntegrationPackageStorage.getInstance().addIntegration(SQLITE_INTEGRATION_NAME)
2423
}
2524

2625
/**
@@ -45,33 +44,18 @@ internal class SQLiteSpanManager(
4544
if (result is CrossProcessCursor) {
4645
return SentryCrossProcessCursor(result, this, sql) as T
4746
}
48-
span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)
49-
span?.spanContext?.origin = TRACE_ORIGIN
47+
span = spanHelper.startSpan(sql, startTimestamp)
5048
span?.status = SpanStatus.OK
5149
result
5250
} catch (e: Throwable) {
53-
span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)
54-
span?.spanContext?.origin = TRACE_ORIGIN
51+
span = spanHelper.startSpan(sql, startTimestamp)
5552
span?.status = SpanStatus.INTERNAL_ERROR
5653
span?.throwable = e
5754
throw e
5855
} finally {
59-
span?.apply {
60-
val isMainThread: Boolean = scopes.options.threadChecker.isMainThread
61-
setData(SpanDataConvention.BLOCKED_MAIN_THREAD_KEY, isMainThread)
62-
if (isMainThread) {
63-
setData(SpanDataConvention.CALL_STACK_KEY, stackTraceFactory.inAppCallStack)
64-
}
65-
// if db name is null, then it's an in-memory database as per
66-
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt;l=38-42
67-
if (databaseName != null) {
68-
setData(SpanDataConvention.DB_SYSTEM_KEY, "sqlite")
69-
setData(SpanDataConvention.DB_NAME_KEY, databaseName)
70-
} else {
71-
setData(SpanDataConvention.DB_SYSTEM_KEY, "in-memory")
72-
}
73-
74-
finish()
56+
span?.let {
57+
spanHelper.applyDataToSpan(it)
58+
it.finish()
7559
}
7660
}
7761
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.sentry.sqlite
2+
3+
/**
4+
* Sentinel file name that [SQLiteDriver.open][androidx.sqlite.SQLiteDriver.open] interprets as an
5+
* in-memory database:
6+
* https://developer.android.com/reference/androidx/sqlite/driver/AndroidSQLiteDriver.
7+
*/
8+
private const val IN_MEMORY_DB_FILENAME = ":memory:"
9+
10+
/**
11+
* Value associated with [DB_SYSTEM_KEY][io.sentry.SpanDataConvention.DB_SYSTEM_KEY] for in-memory
12+
* databases.
13+
*/
14+
internal const val DB_SYSTEM_IN_MEMORY = "in-memory"
15+
16+
/**
17+
* Value associated with [DB_SYSTEM_KEY][io.sentry.SpanDataConvention.DB_SYSTEM_KEY] for SQLite
18+
* databases.
19+
*/
20+
internal const val DB_SYSTEM_SQLITE = "sqlite"
21+
22+
internal data class DbMetadata(val name: String?, val system: String)
23+
24+
/**
25+
* Resolves metadata from the [fileName] argument to
26+
* [SQLiteDriver.open][androidx.sqlite.SQLiteDriver.open].
27+
*/
28+
internal fun dbMetadataFromFileName(fileName: String): DbMetadata {
29+
if (fileName == IN_MEMORY_DB_FILENAME) {
30+
return DbMetadata(name = null, system = DB_SYSTEM_IN_MEMORY)
31+
}
32+
33+
val trimmed = fileName.trimEnd('/', '\\')
34+
if (trimmed.isEmpty()) {
35+
return DbMetadata(name = null, system = DB_SYSTEM_SQLITE)
36+
}
37+
38+
val index = trimmed.lastIndexOfAny(charArrayOf('/', '\\'))
39+
val basename = if (index >= 0) trimmed.substring(index + 1) else trimmed
40+
return DbMetadata(name = basename.ifEmpty { null }, system = DB_SYSTEM_SQLITE)
41+
}
42+
43+
/**
44+
* Resolves metadata from
45+
* [SupportSQLiteOpenHelper.databaseName][androidx.sqlite.db.SupportSQLiteOpenHelper.databaseName].
46+
*/
47+
internal fun dbMetadataFromDatabaseName(databaseName: String?): DbMetadata =
48+
if (databaseName == null) {
49+
DbMetadata(name = null, system = DB_SYSTEM_IN_MEMORY)
50+
} else {
51+
DbMetadata(name = databaseName, system = DB_SYSTEM_SQLITE)
52+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.sentry.sqlite
2+
3+
import io.sentry.IScopes
4+
import io.sentry.ISpan
5+
import io.sentry.Instrumenter
6+
import io.sentry.SentryDate
7+
import io.sentry.SentryStackTraceFactory
8+
import io.sentry.SpanDataConvention
9+
10+
private const val SQLITE_TRACE_ORIGIN = "auto.db.sqlite"
11+
12+
/** Shared span creation and metadata for SQLite instrumentation. */
13+
internal class SQLiteSpanHelper(private val scopes: IScopes, private val dbMetadata: DbMetadata) {
14+
15+
private val stackTraceFactory = SentryStackTraceFactory(scopes.options)
16+
17+
fun startSpan(sql: String, startTimestamp: SentryDate): ISpan? =
18+
scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)?.apply {
19+
spanContext.origin = SQLITE_TRACE_ORIGIN
20+
}
21+
22+
fun applyDataToSpan(span: ISpan) {
23+
val isMainThread = scopes.options.threadChecker.isMainThread
24+
span.setData(SpanDataConvention.BLOCKED_MAIN_THREAD_KEY, isMainThread)
25+
26+
if (isMainThread) {
27+
span.setData(SpanDataConvention.CALL_STACK_KEY, stackTraceFactory.inAppCallStack)
28+
}
29+
30+
span.setData(SpanDataConvention.DB_SYSTEM_KEY, dbMetadata.system)
31+
span.setData(SpanDataConvention.DB_NAME_KEY, dbMetadata.name)
32+
}
33+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.sentry.sqlite
2+
3+
import io.sentry.IScopes
4+
import io.sentry.ScopesAdapter
5+
import io.sentry.SentryDate
6+
import io.sentry.SentryLevel
7+
import io.sentry.SentryLongDate
8+
import io.sentry.SpanStatus
9+
10+
internal class SQLiteSpanRecorder(
11+
fileName: String,
12+
private val scopes: IScopes = ScopesAdapter.getInstance(),
13+
) {
14+
15+
private val spanHelper = SQLiteSpanHelper(scopes, dbMetadataFromFileName(fileName))
16+
17+
/**
18+
* Returns a start timestamp for a db.sql.query span.
19+
*
20+
* Exposed so callers can capture a wall-clock start before accumulating database time.
21+
* Internalizing the start time in [recordSpan] would shift spans to end-of-work on the trace
22+
* timeline, which is less desirable.
23+
*/
24+
fun startTimestamp(): SentryDate = scopes.options.dateProvider.now()
25+
26+
/** Records a db.sql.query span. */
27+
@Suppress("TooGenericExceptionCaught")
28+
fun recordSpan(
29+
sql: String,
30+
startTimestamp: SentryDate,
31+
durationNanos: Long,
32+
status: SpanStatus,
33+
throwable: Throwable? = null,
34+
) {
35+
try {
36+
val span = spanHelper.startSpan(sql, startTimestamp) ?: return
37+
throwable?.let { span.throwable = it }
38+
spanHelper.applyDataToSpan(span)
39+
val endTimestamp = SentryLongDate(startTimestamp.nanoTimestamp() + durationNanos)
40+
span.finish(status, endTimestamp)
41+
} catch (t: Throwable) {
42+
scopes.options.logger.log(SentryLevel.ERROR, "Failed to record SQLite span.", t)
43+
}
44+
}
45+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.sentry.sqlite
2+
3+
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.SQLiteStatement
5+
6+
internal class SentrySQLiteConnection(
7+
private val delegate: SQLiteConnection,
8+
private val spanRecorder: SQLiteSpanRecorder,
9+
) : SQLiteConnection by delegate {
10+
11+
override fun prepare(sql: String): SQLiteStatement {
12+
val statement = delegate.prepare(sql)
13+
return statement as? SentrySQLiteStatement
14+
?: SentrySQLiteStatement(statement, spanRecorder, sql)
15+
}
16+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.sentry.sqlite
2+
3+
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.SQLiteDriver
5+
import io.sentry.ScopesAdapter
6+
import io.sentry.SentryLevel
7+
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
8+
9+
/**
10+
* Wraps a [SQLiteDriver] and automatically adds Sentry spans for each SQL statement it executes.
11+
*
12+
* Example usage:
13+
* ```
14+
* val driver = SentrySQLiteDriver.create(AndroidSQLiteDriver())
15+
* ```
16+
*
17+
* If you use Room:
18+
* ```
19+
* val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName")
20+
* .setDriver(SentrySQLiteDriver.create(AndroidSQLiteDriver()))
21+
* .build()
22+
* ```
23+
*
24+
* **Warning:** Do not use [SentrySQLiteDriver] together with
25+
* [io.sentry.android.sqlite.SentrySupportSQLiteOpenHelper] on the same database file. Both wrappers
26+
* instrument at different layers, so combining them will produce duplicate spans for every SQL
27+
* statement.
28+
*
29+
* @param delegate The [SQLiteDriver] instance to delegate calls to.
30+
*/
31+
public class SentrySQLiteDriver private constructor(private val delegate: SQLiteDriver) :
32+
SQLiteDriver {
33+
34+
init {
35+
addIntegrationToSdkVersion(SQLITE_INTEGRATION_NAME)
36+
}
37+
38+
@Suppress("TooGenericExceptionCaught")
39+
override fun open(fileName: String): SQLiteConnection {
40+
val connection = delegate.open(fileName)
41+
42+
return try {
43+
val spanRecorder = SQLiteSpanRecorder(fileName)
44+
// create() ensures delegate is unwrapped, so we don't protect against double-wrapping the
45+
// connection.
46+
SentrySQLiteConnection(connection, spanRecorder)
47+
} catch (t: Throwable) {
48+
ScopesAdapter.getInstance()
49+
.options
50+
.logger
51+
.log(
52+
SentryLevel.ERROR,
53+
"Failed to instrument SQLite connection; returning uninstrumented connection.",
54+
t,
55+
)
56+
connection
57+
}
58+
}
59+
60+
public companion object {
61+
62+
@JvmStatic
63+
public fun create(delegate: SQLiteDriver): SQLiteDriver =
64+
delegate as? SentrySQLiteDriver ?: SentrySQLiteDriver(delegate)
65+
}
66+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.sentry.sqlite
2+
3+
internal const val SQLITE_INTEGRATION_NAME: String = "SQLite"

0 commit comments

Comments
 (0)