-
-
Notifications
You must be signed in to change notification settings - Fork 464
Support for androidx.sqlite's SQLiteDriver #5002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
angusholder
wants to merge
4
commits into
getsentry:main
Choose a base branch
from
angusholder:SQLiteDriver
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+344
−0
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySQLiteDriver.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } | ||
| } | ||
107 changes: 107 additions & 0 deletions
107
sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySQLiteDriverTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) | ||
| } | ||
| } |
147 changes: 147 additions & 0 deletions
147
sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySQLiteStatementTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.