Skip to content

Commit 37591e7

Browse files
committed
Address Roman's comments
- Merge SQLiteSpanHelper + SQLiteSpanRecorder into a single SQLiteSpanInstrumentation class. - DRY out reference to file name path separators.
1 parent 02edd2a commit 37591e7

12 files changed

Lines changed: 157 additions & 125 deletions

sentry-android-sqlite/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ Please consult the [Sentry Docs](https://docs.sentry.io/platforms/android/integr
1313

1414
This module is organized as two separate packages:
1515

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.
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 `SQLiteSpanManager` wrapper live here.
17+
- **`io.sentry.sqlite`**: Code whose contract depends only on the multiplatform `androidx.sqlite.*` interfaces (e.g., `SQLiteDriver` and `SQLiteConnection`). `SentrySQLiteDriver` and shared span instrumentation via `SQLiteSpanInstrumentation` live here.
1818

1919
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.
2020

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

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ package io.sentry.android.sqlite
33
import android.database.CrossProcessCursor
44
import android.database.SQLException
55
import io.sentry.IScopes
6-
import io.sentry.ISpan
76
import io.sentry.ScopesAdapter
87
import io.sentry.SentryIntegrationPackageStorage
98
import io.sentry.SpanStatus
10-
import io.sentry.sqlite.SQLiteSpanHelper
11-
import io.sentry.sqlite.dbMetadataFromDatabaseName
9+
import io.sentry.sqlite.SQLiteSpanInstrumentation
1210

1311
internal class SQLiteSpanManager(
1412
private val scopes: IScopes = ScopesAdapter.getInstance(),
1513
databaseName: String? = null,
1614
) {
1715

18-
private val spanHelper = SQLiteSpanHelper(scopes, dbMetadataFromDatabaseName(databaseName))
16+
private val spans = SQLiteSpanInstrumentation.fromDatabaseName(databaseName, scopes)
1917

2018
init {
2119
SentryIntegrationPackageStorage.getInstance().addIntegration("SQLite")
@@ -31,8 +29,8 @@ internal class SQLiteSpanManager(
3129
@Suppress("TooGenericExceptionCaught", "UNCHECKED_CAST")
3230
@Throws(SQLException::class)
3331
fun <T> performSql(sql: String, operation: () -> T): T {
34-
val startTimestamp = scopes.getOptions().dateProvider.now()
35-
var span: ISpan? = null
32+
val startTimestamp = spans.startTimestamp()
33+
3634
return try {
3735
val result = operation()
3836
/*
@@ -43,19 +41,11 @@ internal class SQLiteSpanManager(
4341
if (result is CrossProcessCursor) {
4442
return SentryCrossProcessCursor(result, this, sql) as T
4543
}
46-
span = spanHelper.startSpan(sql, startTimestamp)
47-
span?.status = SpanStatus.OK
44+
spans.recordSpan(sql, startTimestamp, SpanStatus.OK)
4845
result
4946
} catch (e: Throwable) {
50-
span = spanHelper.startSpan(sql, startTimestamp)
51-
span?.status = SpanStatus.INTERNAL_ERROR
52-
span?.throwable = e
47+
spans.recordSpan(sql, startTimestamp, SpanStatus.INTERNAL_ERROR, e)
5348
throw e
54-
} finally {
55-
span?.let {
56-
spanHelper.applyDataToSpan(it)
57-
it.finish()
58-
}
5949
}
6050
}
6151
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal fun dbMetadataFromFileName(fileName: String): DbMetadata {
3333
return DbMetadata(name = null, system = DB_SYSTEM_IN_MEMORY)
3434
}
3535

36-
val trimmed = fileName.trimEnd('/', '\\')
36+
val trimmed = fileName.trimEnd { it in FILE_NAME_PATH_SEPARATORS }
3737
if (trimmed.isEmpty()) {
3838
return DbMetadata(name = null, system = DB_SYSTEM_SQLITE)
3939
}

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

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.sentry.sqlite
2+
3+
import io.sentry.IScopes
4+
import io.sentry.Instrumenter
5+
import io.sentry.ScopesAdapter
6+
import io.sentry.SentryDate
7+
import io.sentry.SentryLongDate
8+
import io.sentry.SentryStackTraceFactory
9+
import io.sentry.SpanDataConvention
10+
import io.sentry.SpanStatus
11+
12+
private const val SQLITE_TRACE_ORIGIN = "auto.db.sqlite"
13+
14+
/** Shared span creation and metadata for SQLite instrumentation. */
15+
internal class SQLiteSpanInstrumentation(
16+
private val scopes: IScopes,
17+
private val dbMetadata: DbMetadata,
18+
) {
19+
20+
private val stackTraceFactory = SentryStackTraceFactory(scopes.options)
21+
22+
/**
23+
* Returns a start timestamp for a `db.sql.query` span.
24+
*
25+
* Exposed so callers can capture a wall-clock start before accumulating database time.
26+
* Internalizing the start time in [recordSpan] would shift spans to end-of-work on the trace
27+
* timeline, which is less desirable.
28+
*/
29+
fun startTimestamp(): SentryDate = scopes.options.dateProvider.now()
30+
31+
/** Records a `db.sql.query` span from [startTimestamp] to the moment of invocation. */
32+
fun recordSpan(
33+
sql: String,
34+
startTimestamp: SentryDate,
35+
status: SpanStatus,
36+
throwable: Throwable? = null,
37+
) {
38+
recordSpan(sql, startTimestamp, endTimestamp = null, status, throwable)
39+
}
40+
41+
/** Records a `db.sql.query` span from [startTimestamp] to [startTimestamp] + [durationNanos]. */
42+
fun recordSpan(
43+
sql: String,
44+
startTimestamp: SentryDate,
45+
durationNanos: Long,
46+
status: SpanStatus,
47+
throwable: Throwable? = null,
48+
) {
49+
val endTimestamp = SentryLongDate(startTimestamp.nanoTimestamp() + durationNanos)
50+
recordSpan(sql, startTimestamp, endTimestamp, status, throwable)
51+
}
52+
53+
private fun recordSpan(
54+
sql: String,
55+
startTimestamp: SentryDate,
56+
endTimestamp: SentryDate?,
57+
status: SpanStatus,
58+
throwable: Throwable?,
59+
) {
60+
scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)?.apply {
61+
spanContext.origin = SQLITE_TRACE_ORIGIN
62+
throwable?.let { this.throwable = it }
63+
64+
val isMainThread = scopes.options.threadChecker.isMainThread
65+
setData(SpanDataConvention.BLOCKED_MAIN_THREAD_KEY, isMainThread)
66+
67+
if (isMainThread) {
68+
setData(SpanDataConvention.CALL_STACK_KEY, stackTraceFactory.inAppCallStack)
69+
}
70+
71+
dbMetadata.name?.let { setData(SpanDataConvention.DB_NAME_KEY, it) }
72+
setData(SpanDataConvention.DB_SYSTEM_KEY, dbMetadata.system)
73+
finish(status, endTimestamp)
74+
}
75+
}
76+
77+
companion object {
78+
79+
fun fromDatabaseName(databaseName: String?, scopes: IScopes = ScopesAdapter.getInstance()) =
80+
SQLiteSpanInstrumentation(scopes, dbMetadataFromDatabaseName(databaseName))
81+
82+
fun fromFileName(fileName: String, scopes: IScopes = ScopesAdapter.getInstance()) =
83+
SQLiteSpanInstrumentation(scopes, dbMetadataFromFileName(fileName))
84+
}
85+
}

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

Lines changed: 0 additions & 45 deletions
This file was deleted.

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import androidx.sqlite.SQLiteStatement
55

66
internal class SentrySQLiteConnection(
77
private val delegate: SQLiteConnection,
8-
private val spanRecorder: SQLiteSpanRecorder,
8+
private val spans: SQLiteSpanInstrumentation,
99
) : SQLiteConnection by delegate {
1010

1111
override fun prepare(sql: String): SQLiteStatement {
1212
val statement = delegate.prepare(sql)
13-
return statement as? SentrySQLiteStatement
14-
?: SentrySQLiteStatement(statement, spanRecorder, sql)
13+
return statement as? SentrySQLiteStatement ?: SentrySQLiteStatement(statement, spans, sql)
1514
}
1615
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ public class SentrySQLiteDriver private constructor(private val delegate: SQLite
4040
val connection = delegate.open(fileName)
4141

4242
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)
43+
val spans = SQLiteSpanInstrumentation.fromFileName(fileName)
44+
// create() ensures delegate is unwrapped, so we don't need to protect against double-wrapping
45+
// the connection.
46+
SentrySQLiteConnection(connection, spans)
4747
} catch (t: Throwable) {
4848
ScopesAdapter.getInstance()
4949
.options

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import io.sentry.SpanStatus
1818
*/
1919
internal class SentrySQLiteStatement(
2020
private val delegate: SQLiteStatement,
21-
private val spanRecorder: SQLiteSpanRecorder,
21+
private val spans: SQLiteSpanInstrumentation,
2222
private val sql: String,
2323
private val nanoTimeProvider: () -> Long = { System.nanoTime() },
2424
) : SQLiteStatement by delegate {
@@ -37,7 +37,7 @@ internal class SentrySQLiteStatement(
3737
val beforeNanos = nanoTimeProvider()
3838
return try {
3939
if (firstStepTimestamp == null) {
40-
firstStepTimestamp = spanRecorder.startTimestamp()
40+
firstStepTimestamp = spans.startTimestamp()
4141
}
4242

4343
stepsComplete = !delegate.step()
@@ -76,6 +76,6 @@ internal class SentrySQLiteStatement(
7676
val duration = accumulatedDbNanos
7777
firstStepTimestamp = null
7878
accumulatedDbNanos = 0L
79-
spanRecorder.recordSpan(sql, start, duration, status, throwable)
79+
spans.recordSpan(sql, start, duration, status, throwable)
8080
}
8181
}

sentry-android-sqlite/src/test/java/io/sentry/sqlite/SQLiteSpanRecorderTest.kt renamed to sentry-android-sqlite/src/test/java/io/sentry/sqlite/SQLiteSpanInstrumentationTest.kt

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import kotlin.test.assertTrue
1616
import org.mockito.kotlin.mock
1717
import org.mockito.kotlin.whenever
1818

19-
class SQLiteSpanRecorderTest {
19+
class SQLiteSpanInstrumentationTest {
2020

2121
private class Fixture {
2222

@@ -27,14 +27,14 @@ class SQLiteSpanRecorderTest {
2727
fun getSut(
2828
isTransactionActive: Boolean = true,
2929
fileName: String = ":memory:",
30-
): SQLiteSpanRecorder {
30+
): SQLiteSpanInstrumentation {
3131
options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" }
3232
whenever(scopes.options).thenReturn(options)
3333
sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes)
3434
if (isTransactionActive) {
3535
whenever(scopes.span).thenReturn(sentryTracer)
3636
}
37-
return SQLiteSpanRecorder(fileName, scopes)
37+
return SQLiteSpanInstrumentation.fromFileName(fileName, scopes)
3838
}
3939
}
4040

@@ -147,10 +147,47 @@ class SQLiteSpanRecorderTest {
147147
}
148148

149149
@Test
150-
fun `recordSpan does not throw if span recording fails`() {
150+
fun `recordSpan without a duration finishes the span at the time of invocation`() {
151151
val sut = fixture.getSut()
152-
whenever(fixture.scopes.span).thenThrow(RuntimeException("span unavailable"))
152+
val start = sut.startTimestamp()
153153

154-
sut.recordSpan("SELECT 1", sut.startTimestamp(), 1_000_000, SpanStatus.OK)
154+
sut.recordSpan("SELECT 1", start, SpanStatus.OK)
155+
156+
val span = fixture.sentryTracer.children.first()
157+
assertTrue(span.isFinished)
158+
assertEquals(SpanStatus.OK, span.status)
159+
// Unlike the duration overload, no synthetic end timestamp is supplied; the span finishes at
160+
// "now", i.e. at or after its start.
161+
assertTrue(span.finishDate!!.nanoTimestamp() >= start.nanoTimestamp())
162+
}
163+
164+
@Test
165+
fun `fromFileName sets db name from fileName`() {
166+
val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" }
167+
whenever(fixture.scopes.options).thenReturn(options)
168+
fixture.sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes)
169+
whenever(fixture.scopes.span).thenReturn(fixture.sentryTracer)
170+
171+
val sut = SQLiteSpanInstrumentation.fromFileName("tracks.db", fixture.scopes)
172+
sut.recordSpan("SELECT 1", sut.startTimestamp(), SpanStatus.OK)
173+
174+
val span = fixture.sentryTracer.children.first()
175+
assertEquals("sqlite", span.data[SpanDataConvention.DB_SYSTEM_KEY])
176+
assertEquals("tracks.db", span.data[SpanDataConvention.DB_NAME_KEY])
177+
}
178+
179+
@Test
180+
fun `fromDatabaseName sets db name from databaseName`() {
181+
val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" }
182+
whenever(fixture.scopes.options).thenReturn(options)
183+
fixture.sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes)
184+
whenever(fixture.scopes.span).thenReturn(fixture.sentryTracer)
185+
186+
val sut = SQLiteSpanInstrumentation.fromDatabaseName("tracks.db", fixture.scopes)
187+
sut.recordSpan("SELECT 1", sut.startTimestamp(), SpanStatus.OK)
188+
189+
val span = fixture.sentryTracer.children.first()
190+
assertEquals("sqlite", span.data[SpanDataConvention.DB_SYSTEM_KEY])
191+
assertEquals("tracks.db", span.data[SpanDataConvention.DB_NAME_KEY])
155192
}
156193
}

0 commit comments

Comments
 (0)