Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.sentry.android.sqlite

import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement
import io.sentry.android.sqlite.SQLiteSpanManager
import io.sentry.IScopes
import io.sentry.ScopesAdapter


/**
* Automatically adds a Sentry span to the current scope for each database query executed.
*
* Usage - wrap this around your current [SQLiteDriver]:
* ```
* val driver = SentrySQLiteDriver.create(AndroidSQLiteDriver())
* ```
*
* If you use Room you can wrap the default [AndroidSQLiteDriver]:
* ```
* val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName")
* .setDriver(SentrySQLiteDriver.create(AndroidSQLiteDriver()))
* ...
* .build()
* ```
*/
public class SentrySQLiteDriver internal constructor(
private val delegate: SQLiteDriver,
private val scopes: IScopes = ScopesAdapter.getInstance(),
) : SQLiteDriver {
override fun open(fileName: String): SQLiteConnection {
val sqliteSpanManager = SQLiteSpanManager(
scopes,
// SQLiteDriver.open docs say:
// >> To open an in-memory database use the special name :memory: as the fileName.
// SQLiteSpanManager expects null for an in-memory databaseName, so replace ":memory:" with null.
databaseName = fileName.takeIf { it != ":memory:" }
)
val connection = delegate.open(fileName)
return SentrySQLiteConnection(connection, sqliteSpanManager)
}

public companion object {
/**
* @param delegate The [SQLiteDriver] instance to delegate calls to.
*/
@JvmStatic
public fun create(delegate: SQLiteDriver): SQLiteDriver {
if (delegate is SentrySQLiteDriver) {
return delegate
} else {
return SentrySQLiteDriver(delegate)
}
}
}
}

internal class SentrySQLiteConnection(
private val delegate: SQLiteConnection,
private val sqliteSpanManager: SQLiteSpanManager,
) : SQLiteConnection by delegate {
override fun prepare(sql: String): SQLiteStatement {
val statement = delegate.prepare(sql)
return SentrySQLiteStatement(statement, sqliteSpanManager, sql)
}
}

internal class SentrySQLiteStatement(
private val delegate: SQLiteStatement,
private val sqliteSpanManager: SQLiteSpanManager,
private val sql: String,
) : SQLiteStatement by delegate {
// We have to start the span only the first time, regardless of how many times its methods get
// called.
private var isSpanStarted = false

override fun step(): Boolean {
if (isSpanStarted) {
return delegate.step()
} else {
isSpanStarted = true
return sqliteSpanManager.performSql(sql) { delegate.step() }
}
}

override fun reset() {
isSpanStarted = false
delegate.reset()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.sentry.android.sqlite

import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteStatement
import io.sentry.IScopes
import io.sentry.SentryOptions
import io.sentry.SentryTracer
import io.sentry.SpanDataConvention
import io.sentry.TransactionContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

class SentrySQLiteDriverTest {
private class Fixture {
private val scopes = mock<IScopes>()
val mockDriver = mock<SQLiteDriver>()
val mockConnection = mock<SQLiteConnection>()
val mockStatement = mock<SQLiteStatement>()
lateinit var sentryTracer: SentryTracer
lateinit var options: SentryOptions

fun getSut(): SentrySQLiteDriver {
options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" }
whenever(scopes.options).thenReturn(options)
sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes)
whenever(scopes.span).thenReturn(sentryTracer)

return SentrySQLiteDriver(mockDriver, scopes)
}
}

private val fixture = Fixture()

@Test
fun `opening connection and running query logs to Sentry`() {
val driver = fixture.getSut()
val sql = "SELECT * FROM users"

whenever(fixture.mockDriver.open("test.db")).thenReturn(fixture.mockConnection)
whenever(fixture.mockConnection.prepare(sql)).thenReturn(fixture.mockStatement)
whenever(fixture.mockStatement.step()).thenReturn(true)

// Open connection, prepare statement, and execute
val connection = driver.open("test.db")
val statement = connection.prepare(sql)
statement.step()

// Verify span was created
assertEquals(1, fixture.sentryTracer.children.size)
val span = fixture.sentryTracer.children.first()
assertEquals("db.sql.query", span.operation)
assertEquals(sql, span.description)
}

@Test
fun `on-disk database sets db name in span`() {
val driver = fixture.getSut()
val databaseName = "myapp.db"
val sql = "INSERT INTO users VALUES (1, 'test')"

whenever(fixture.mockDriver.open(databaseName)).thenReturn(fixture.mockConnection)
whenever(fixture.mockConnection.prepare(sql)).thenReturn(fixture.mockStatement)
whenever(fixture.mockStatement.step()).thenReturn(true)

// Open connection, prepare statement, and execute
val connection = driver.open(databaseName)
val statement = connection.prepare(sql)
statement.step()

// Verify span was created with correct database name
assertEquals(1, fixture.sentryTracer.children.size)
val span = fixture.sentryTracer.children.first()
assertEquals("db.sql.query", span.operation)
assertEquals(sql, span.description)
assertEquals("sqlite", span.getData(SpanDataConvention.DB_SYSTEM_KEY))
assertEquals(databaseName, span.getData(SpanDataConvention.DB_NAME_KEY))
}

@Test
fun `in-memory database sets db system to in-memory`() {
val driver = fixture.getSut()
val sql = "CREATE TABLE temp (id INT)"

whenever(fixture.mockDriver.open(":memory:")).thenReturn(fixture.mockConnection)
whenever(fixture.mockConnection.prepare(sql)).thenReturn(fixture.mockStatement)
whenever(fixture.mockStatement.step()).thenReturn(true)

// Open in-memory connection, prepare statement, and execute
val connection = driver.open(":memory:")
val statement = connection.prepare(sql)
statement.step()

// Verify span was created with correct database system
assertEquals(1, fixture.sentryTracer.children.size)
val span = fixture.sentryTracer.children.first()
assertEquals("db.sql.query", span.operation)
assertEquals(sql, span.description)
assertEquals("in-memory", span.getData(SpanDataConvention.DB_SYSTEM_KEY))
// DB_NAME_KEY should not be set for in-memory databases
assertNotNull(span.getData(SpanDataConvention.DB_SYSTEM_KEY))
assertEquals(null, span.getData(SpanDataConvention.DB_NAME_KEY))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package io.sentry.android.sqlite

import androidx.sqlite.SQLiteStatement
import io.sentry.IScopes
import io.sentry.ISpan
import io.sentry.SentryOptions
import io.sentry.SentryTracer
import io.sentry.SpanStatus
import io.sentry.TransactionContext
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class SentrySQLiteStatementTest {
private class Fixture {
private val scopes = mock<IScopes>()
private val spanManager = SQLiteSpanManager(scopes)
val mockStatement = mock<SQLiteStatement>()
lateinit var sentryTracer: SentryTracer
lateinit var options: SentryOptions

fun getSut(sql: String, isSpanActive: Boolean = true): SentrySQLiteStatement {
options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" }
whenever(scopes.options).thenReturn(options)
sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes)

if (isSpanActive) {
whenever(scopes.span).thenReturn(sentryTracer)
}
return SentrySQLiteStatement(mockStatement, spanManager, sql)
}
}

private val fixture = Fixture()

@Test
fun `all calls are propagated to the delegate`() {
val sql = "SELECT * FROM users"
val statement = fixture.getSut(sql)

whenever(fixture.mockStatement.step()).thenReturn(true)

inOrder(fixture.mockStatement) {
statement.step()
verify(fixture.mockStatement).step()

statement.reset()
verify(fixture.mockStatement).reset()
}
}

@Test
fun `step creates a span if a span is running`() {
val sql = "SELECT * FROM users"
val sut = fixture.getSut(sql)
whenever(fixture.mockStatement.step()).thenReturn(true)
assertEquals(0, fixture.sentryTracer.children.size)
sut.step()
val span = fixture.sentryTracer.children.firstOrNull()
assertSqlSpanCreated(sql, span)
}

@Test
fun `step does not create a span if no span is running`() {
val sql = "SELECT * FROM users"
val sut = fixture.getSut(sql, isSpanActive = false)
whenever(fixture.mockStatement.step()).thenReturn(true)
sut.step()
assertEquals(0, fixture.sentryTracer.children.size)
}

@Test
fun `multiple step calls only create one span`() {
val sql = "SELECT * FROM users"
val sut = fixture.getSut(sql)
whenever(fixture.mockStatement.step()).thenReturn(true, true, false)
assertEquals(0, fixture.sentryTracer.children.size)

// First step creates a span
sut.step()
assertEquals(1, fixture.sentryTracer.children.size)

// Second step doesn't create a new span
sut.step()
assertEquals(1, fixture.sentryTracer.children.size)

// Third step still doesn't create a new span
sut.step()
assertEquals(1, fixture.sentryTracer.children.size)

val span = fixture.sentryTracer.children.firstOrNull()
assertSqlSpanCreated(sql, span)
}

@Test
fun `reset allows step to create a new span`() {
val sql = "SELECT * FROM users"
val sut = fixture.getSut(sql)
whenever(fixture.mockStatement.step()).thenReturn(true)
assertEquals(0, fixture.sentryTracer.children.size)

// First step creates a span
sut.step()
assertEquals(1, fixture.sentryTracer.children.size)

// Reset the statement
sut.reset()

// Next step creates a new span
sut.step()
assertEquals(2, fixture.sentryTracer.children.size)

// Verify both spans were created correctly
fixture.sentryTracer.children.forEach { span ->
assertSqlSpanCreated(sql, span)
}
}

@Test
fun `step returns delegate result`() {
val sql = "SELECT * FROM users"
val sut = fixture.getSut(sql)
whenever(fixture.mockStatement.step()).thenReturn(true, false)

val result1 = sut.step()
assertTrue(result1)

sut.reset()

val result2 = sut.step()
assertFalse(result2)
}

private fun assertSqlSpanCreated(sql: String, span: ISpan?) {
assertNotNull(span)
assertEquals("db.sql.query", span.operation)
assertEquals(sql, span.description)
assertEquals(SpanStatus.OK, span.status)
assertTrue(span.isFinished)
}
}