Skip to content

Commit 703e7ae

Browse files
authored
Merge 02edd2a into ca6b6d8
2 parents ca6b6d8 + 02edd2a commit 703e7ae

15 files changed

Lines changed: 1076 additions & 27 deletions

CHANGELOG.md

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

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add `SentrySQLiteDriver` to `sentry-android-sqlite` for instrumenting AndroidX's `SQLiteDriver` ([#5466](https://github.com/getsentry/sentry-java/pull/5466))
8+
- Automatically generates spans for all SQLite statements
9+
- To use it, pass your `SQLiteDriver` to `SentrySQLiteDriver.create(...)`
10+
- See https://docs.sentry.io/platforms/android/integrations/room-and-sqlite/ for more details, including info about migrating from `SentrySupportSQLiteOpenHelper`
11+
312
## 8.43.0
413

514
### 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 usage and migration guidance, as well as how to avoid duplicate spans when using Room's `SupportSQLiteDriver` adapter.
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: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@ 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.SQLiteSpanHelper
11+
import io.sentry.sqlite.dbMetadataFromDatabaseName
1512

1613
internal class SQLiteSpanManager(
1714
private val scopes: IScopes = ScopesAdapter.getInstance(),
18-
private val databaseName: String? = null,
15+
databaseName: String? = null,
1916
) {
20-
private val stackTraceFactory = SentryStackTraceFactory(scopes.options)
17+
18+
private val spanHelper = SQLiteSpanHelper(scopes, dbMetadataFromDatabaseName(databaseName))
2119

2220
init {
2321
SentryIntegrationPackageStorage.getInstance().addIntegration("SQLite")
@@ -45,33 +43,18 @@ internal class SQLiteSpanManager(
4543
if (result is CrossProcessCursor) {
4644
return SentryCrossProcessCursor(result, this, sql) as T
4745
}
48-
span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)
49-
span?.spanContext?.origin = TRACE_ORIGIN
46+
span = spanHelper.startSpan(sql, startTimestamp)
5047
span?.status = SpanStatus.OK
5148
result
5249
} catch (e: Throwable) {
53-
span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)
54-
span?.spanContext?.origin = TRACE_ORIGIN
50+
span = spanHelper.startSpan(sql, startTimestamp)
5551
span?.status = SpanStatus.INTERNAL_ERROR
5652
span?.throwable = e
5753
throw e
5854
} 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()
55+
span?.let {
56+
spanHelper.applyDataToSpan(it)
57+
it.finish()
7558
}
7659
}
7760
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.sentry.sqlite
2+
3+
/**
4+
* Value associated with [DB_SYSTEM_KEY][io.sentry.SpanDataConvention.DB_SYSTEM_KEY] for in-memory
5+
* databases.
6+
*/
7+
internal const val DB_SYSTEM_IN_MEMORY = "in-memory"
8+
9+
/**
10+
* Value associated with [DB_SYSTEM_KEY][io.sentry.SpanDataConvention.DB_SYSTEM_KEY] for SQLite
11+
* databases.
12+
*/
13+
internal const val DB_SYSTEM_SQLITE = "sqlite"
14+
15+
/**
16+
* Sentinel file name that [SQLiteDriver.open][androidx.sqlite.SQLiteDriver.open] interprets as an
17+
* in-memory database:
18+
* https://developer.android.com/reference/androidx/sqlite/driver/AndroidSQLiteDriver.
19+
*/
20+
private const val IN_MEMORY_DB_FILENAME = ":memory:"
21+
22+
/** Path separators matching [File.separatorChar][java.io.File.separatorChar]. */
23+
private val FILE_NAME_PATH_SEPARATORS = charArrayOf('/', '\\')
24+
25+
internal data class DbMetadata(val name: String?, val system: String)
26+
27+
/**
28+
* Resolves metadata from the [fileName] argument to
29+
* [SQLiteDriver.open][androidx.sqlite.SQLiteDriver.open].
30+
*/
31+
internal fun dbMetadataFromFileName(fileName: String): DbMetadata {
32+
if (fileName == IN_MEMORY_DB_FILENAME) {
33+
return DbMetadata(name = null, system = DB_SYSTEM_IN_MEMORY)
34+
}
35+
36+
val trimmed = fileName.trimEnd('/', '\\')
37+
if (trimmed.isEmpty()) {
38+
return DbMetadata(name = null, system = DB_SYSTEM_SQLITE)
39+
}
40+
41+
val index = trimmed.lastIndexOfAny(FILE_NAME_PATH_SEPARATORS)
42+
val basename = if (index >= 0) trimmed.substring(index + 1) else trimmed
43+
return DbMetadata(name = basename.ifEmpty { null }, system = DB_SYSTEM_SQLITE)
44+
}
45+
46+
/**
47+
* Resolves metadata from
48+
* [SupportSQLiteOpenHelper.databaseName][androidx.sqlite.db.SupportSQLiteOpenHelper.databaseName].
49+
*/
50+
internal fun dbMetadataFromDatabaseName(databaseName: String?): DbMetadata =
51+
if (databaseName == null) {
52+
DbMetadata(name = null, system = DB_SYSTEM_IN_MEMORY)
53+
} else {
54+
DbMetadata(name = databaseName, system = DB_SYSTEM_SQLITE)
55+
}
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+
dbMetadata.name?.let { span.setData(SpanDataConvention.DB_NAME_KEY, it) }
31+
span.setData(SpanDataConvention.DB_SYSTEM_KEY, dbMetadata.system)
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.SentryIntegrationPackageStorage
7+
import io.sentry.SentryLevel
8+
9+
/**
10+
* Wraps a [SQLiteDriver] and automatically adds 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+
* [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.
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+
SentryIntegrationPackageStorage.getInstance().addIntegration("SQLiteDriver")
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+
}

0 commit comments

Comments
 (0)