Skip to content

Commit 84b2bbd

Browse files
authored
Refactor android sqlite instrumentation (SDK side) (#2722)
* added new module sentry-android-sqlite * added SQLiteSpanManager, SentrySupportSQLiteStatement, SentrySupportSQLiteOpenHelper and SentrySupportSQLiteDatabase wrappers
1 parent b2152fd commit 84b2bbd

File tree

17 files changed

+928
-0
lines changed

17 files changed

+928
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ distributions/
1919
*.vscode/
2020
sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories
2121
sentry-samples/sentry-samples-spring-boot-jakarta/spy.log
22+
spy.log

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Features
66

7+
- Introduce new `sentry-android-sqlite` integration ([#2722](https://github.com/getsentry/sentry-java/pull/2722))
8+
- This integration replaces the old `androidx.sqlite` database instrumentation in the Sentry Android Gradle plugin
9+
- A new capability to manually instrument your `androidx.sqlite` databases.
10+
- You can wrap your custom `SupportSQLiteOpenHelper` instance into `SentrySupportSQLiteOpenHelper(myHelper)` if you're not using the Sentry Android Gradle plugin and still benefit from performance auto-instrumentation.
711
- Add SentryWrapper for Callable and Supplier Interface ([#2720](https://github.com/getsentry/sentry-java/pull/2720))
812
- Load sentry-debug-meta.properties ([#2734](https://github.com/getsentry/sentry-java/pull/2734))
913
- This enables source context for Java

buildSrc/src/main/java/Config.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ object Config {
5757
val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
5858
val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
5959
val androidxCore = "androidx.core:core:1.3.2"
60+
val androidxSqlite = "androidx.sqlite:sqlite:2.3.1"
6061
val androidxRecylerView = "androidx.recyclerview:recyclerview:1.2.1"
6162

6263
val slf4jApi = "org.slf4j:slf4j-api:1.7.30"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
public final class io/sentry/android/sqlite/BuildConfig {
2+
public static final field BUILD_TYPE Ljava/lang/String;
3+
public static final field DEBUG Z
4+
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
5+
public static final field VERSION_NAME Ljava/lang/String;
6+
public fun <init> ()V
7+
}
8+
9+
public final class io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper : androidx/sqlite/db/SupportSQLiteOpenHelper {
10+
public static final field Companion Lio/sentry/android/sqlite/SentrySupportSQLiteOpenHelper$Companion;
11+
public synthetic fun <init> (Landroidx/sqlite/db/SupportSQLiteOpenHelper;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
12+
public fun close ()V
13+
public static final fun create (Landroidx/sqlite/db/SupportSQLiteOpenHelper;)Landroidx/sqlite/db/SupportSQLiteOpenHelper;
14+
public fun getDatabaseName ()Ljava/lang/String;
15+
public fun getReadableDatabase ()Landroidx/sqlite/db/SupportSQLiteDatabase;
16+
public fun getWritableDatabase ()Landroidx/sqlite/db/SupportSQLiteDatabase;
17+
public fun setWriteAheadLoggingEnabled (Z)V
18+
}
19+
20+
public final class io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper$Companion {
21+
public final fun create (Landroidx/sqlite/db/SupportSQLiteOpenHelper;)Landroidx/sqlite/db/SupportSQLiteOpenHelper;
22+
}
23+
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import io.gitlab.arturbosch.detekt.Detekt
2+
import org.jetbrains.kotlin.config.KotlinCompilerVersion
3+
4+
plugins {
5+
id("com.android.library")
6+
kotlin("android")
7+
jacoco
8+
id(Config.QualityPlugins.gradleVersions)
9+
id(Config.QualityPlugins.detektPlugin)
10+
}
11+
12+
android {
13+
compileSdk = Config.Android.compileSdkVersion
14+
namespace = "io.sentry.android.sqlite"
15+
16+
defaultConfig {
17+
targetSdk = Config.Android.targetSdkVersion
18+
minSdk = Config.Android.minSdkVersion
19+
20+
// for AGP 4.1
21+
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
22+
}
23+
24+
buildTypes {
25+
getByName("debug")
26+
getByName("release") {
27+
consumerProguardFiles("proguard-rules.pro")
28+
}
29+
}
30+
31+
kotlinOptions {
32+
jvmTarget = JavaVersion.VERSION_1_8.toString()
33+
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
34+
}
35+
36+
testOptions {
37+
animationsDisabled = true
38+
unitTests.apply {
39+
isReturnDefaultValues = true
40+
isIncludeAndroidResources = true
41+
}
42+
}
43+
44+
lint {
45+
warningsAsErrors = true
46+
checkDependencies = true
47+
48+
// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
49+
checkReleaseBuilds = false
50+
}
51+
52+
variantFilter {
53+
if (Config.Android.shouldSkipDebugVariant(buildType.name)) {
54+
ignore = true
55+
}
56+
}
57+
}
58+
59+
tasks.withType<Test> {
60+
configure<JacocoTaskExtension> {
61+
isIncludeNoLocationClasses = false
62+
}
63+
}
64+
65+
kotlin {
66+
explicitApi()
67+
}
68+
69+
dependencies {
70+
api(projects.sentry)
71+
72+
compileOnly(Config.Libs.androidxSqlite)
73+
74+
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
75+
76+
// tests
77+
testImplementation(Config.Libs.androidxSqlite)
78+
testImplementation(Config.TestLibs.kotlinTestJunit)
79+
testImplementation(Config.TestLibs.androidxJunit)
80+
testImplementation(Config.TestLibs.mockitoKotlin)
81+
testImplementation(Config.TestLibs.mockitoInline)
82+
}
83+
84+
tasks.withType<Detekt> {
85+
// Target version of the generated JVM bytecode. It is used for type resolution.
86+
jvmTarget = JavaVersion.VERSION_1_8.toString()
87+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
##---------------Begin: proguard configuration for SQLite ----------
2+
3+
# To ensure that stack traces is unambiguous
4+
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
5+
-keepattributes LineNumberTable,SourceFile
6+
7+
##---------------End: proguard configuration for SQLite ----------
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.sentry.android.sqlite
2+
3+
import android.database.SQLException
4+
import io.sentry.HubAdapter
5+
import io.sentry.IHub
6+
import io.sentry.SentryIntegrationPackageStorage
7+
import io.sentry.SpanStatus
8+
9+
internal class SQLiteSpanManager(
10+
private val hub: IHub = HubAdapter.getInstance()
11+
) {
12+
13+
init {
14+
SentryIntegrationPackageStorage.getInstance().addIntegration("SQLite")
15+
}
16+
17+
/**
18+
* Performs a sql operation, creates a span and handles exceptions in case of occurrence.
19+
*
20+
* @param sql The sql query
21+
* @param operation The sql operation to execute.
22+
* In case of an error the surrounding span will have its status set to INTERNAL_ERROR
23+
*/
24+
@Suppress("TooGenericExceptionCaught")
25+
@Throws(SQLException::class)
26+
fun <T> performSql(sql: String, operation: () -> T): T {
27+
val span = hub.span?.startChild("db.sql.query", sql)
28+
return try {
29+
val result = operation()
30+
span?.status = SpanStatus.OK
31+
result
32+
} catch (e: Throwable) {
33+
span?.status = SpanStatus.INTERNAL_ERROR
34+
span?.throwable = e
35+
throw e
36+
} finally {
37+
span?.finish()
38+
}
39+
}
40+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.sentry.android.sqlite
2+
3+
import android.annotation.SuppressLint
4+
import android.database.Cursor
5+
import android.database.SQLException
6+
import android.os.Build
7+
import android.os.CancellationSignal
8+
import androidx.annotation.RequiresApi
9+
import androidx.sqlite.db.SupportSQLiteDatabase
10+
import androidx.sqlite.db.SupportSQLiteQuery
11+
import androidx.sqlite.db.SupportSQLiteStatement
12+
13+
/**
14+
* The Sentry's [SentrySupportSQLiteDatabase], it will automatically add a span
15+
* out of the active span bound to the scope for each database query.
16+
* It's a wrapper around [SupportSQLiteDatabase], and it's created automatically
17+
* by the [SentrySupportSQLiteOpenHelper].
18+
*
19+
* @param delegate The [SupportSQLiteDatabase] instance to delegate calls to.
20+
* @param sqLiteSpanManager The [SQLiteSpanManager] responsible for the creation of the spans.
21+
*/
22+
internal class SentrySupportSQLiteDatabase(
23+
private val delegate: SupportSQLiteDatabase,
24+
private val sqLiteSpanManager: SQLiteSpanManager
25+
) : SupportSQLiteDatabase by delegate {
26+
27+
/**
28+
* Compiles the given SQL statement. It will return Sentry's wrapper around SupportSQLiteStatement.
29+
*
30+
* @param sql The sql query.
31+
* @return Compiled statement.
32+
*/
33+
override fun compileStatement(sql: String): SupportSQLiteStatement {
34+
return SentrySupportSQLiteStatement(delegate.compileStatement(sql), sqLiteSpanManager, sql)
35+
}
36+
37+
@Suppress("AcronymName") // To keep consistency with framework method name.
38+
override fun execPerConnectionSQL(
39+
sql: String,
40+
@SuppressLint("ArrayReturn") bindArgs: Array<out Any?>?
41+
) {
42+
sqLiteSpanManager.performSql(sql) {
43+
delegate.execPerConnectionSQL(sql, bindArgs)
44+
}
45+
}
46+
47+
override fun query(query: String): Cursor {
48+
return sqLiteSpanManager.performSql(query) {
49+
delegate.query(query)
50+
}
51+
}
52+
53+
override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
54+
return sqLiteSpanManager.performSql(query) {
55+
delegate.query(query, bindArgs)
56+
}
57+
}
58+
59+
override fun query(query: SupportSQLiteQuery): Cursor {
60+
return sqLiteSpanManager.performSql(query.sql) {
61+
delegate.query(query)
62+
}
63+
}
64+
65+
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
66+
override fun query(
67+
query: SupportSQLiteQuery,
68+
cancellationSignal: CancellationSignal?
69+
): Cursor {
70+
return sqLiteSpanManager.performSql(query.sql) {
71+
delegate.query(query, cancellationSignal)
72+
}
73+
}
74+
75+
@Throws(SQLException::class)
76+
override fun execSQL(sql: String) {
77+
sqLiteSpanManager.performSql(sql) {
78+
delegate.execSQL(sql)
79+
}
80+
}
81+
82+
@Throws(SQLException::class)
83+
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
84+
sqLiteSpanManager.performSql(sql) {
85+
delegate.execSQL(sql, bindArgs)
86+
}
87+
}
88+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.sentry.android.sqlite
2+
3+
import androidx.sqlite.db.SupportSQLiteDatabase
4+
import androidx.sqlite.db.SupportSQLiteOpenHelper
5+
6+
/**
7+
* The Sentry's [SentrySupportSQLiteOpenHelper], it will automatically add a span
8+
* out of the active span bound to the scope for each database query.
9+
* It's a wrapper around an instance of [SupportSQLiteOpenHelper].
10+
*
11+
* You can wrap your custom [SupportSQLiteOpenHelper] instance with `SentrySupportSQLiteOpenHelper(myHelper)`.
12+
* If you're using the Sentry Android Gradle plugin, this will be applied automatically.
13+
*
14+
* Usage - wrap your custom [SupportSQLiteOpenHelper] instance in [SentrySupportSQLiteOpenHelper]
15+
*
16+
* ```
17+
* val openHelper = SentrySupportSQLiteOpenHelper.create(myOpenHelper)
18+
* ```
19+
*
20+
* If you use Room you can wrap the default [FrameworkSQLiteOpenHelperFactory]:
21+
*
22+
* ```
23+
* val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName")
24+
* .openHelperFactory { configuration ->
25+
* SentrySupportSQLiteOpenHelper.create(FrameworkSQLiteOpenHelperFactory().create(configuration))
26+
* }
27+
* ...
28+
* .build()
29+
* ```
30+
*
31+
* @param delegate The [SupportSQLiteOpenHelper] instance to delegate calls to.
32+
*/
33+
class SentrySupportSQLiteOpenHelper private constructor(
34+
private val delegate: SupportSQLiteOpenHelper
35+
) : SupportSQLiteOpenHelper by delegate {
36+
37+
private val sqLiteSpanManager = SQLiteSpanManager()
38+
39+
private val sentryWritableDatabase: SupportSQLiteDatabase by lazy {
40+
SentrySupportSQLiteDatabase(delegate.writableDatabase, sqLiteSpanManager)
41+
}
42+
43+
private val sentryReadableDatabase: SupportSQLiteDatabase by lazy {
44+
SentrySupportSQLiteDatabase(delegate.readableDatabase, sqLiteSpanManager)
45+
}
46+
47+
override val writableDatabase: SupportSQLiteDatabase
48+
get() = sentryWritableDatabase
49+
50+
override val readableDatabase: SupportSQLiteDatabase
51+
get() = sentryReadableDatabase
52+
53+
companion object {
54+
55+
// @JvmStatic is needed to let this method be accessed by our gradle plugin
56+
@JvmStatic
57+
fun create(delegate: SupportSQLiteOpenHelper): SupportSQLiteOpenHelper {
58+
return if (delegate is SentrySupportSQLiteOpenHelper) {
59+
delegate
60+
} else {
61+
SentrySupportSQLiteOpenHelper(delegate)
62+
}
63+
}
64+
}
65+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.sentry.android.sqlite
2+
3+
import androidx.sqlite.db.SupportSQLiteStatement
4+
5+
/**
6+
* The Sentry's [SentrySupportSQLiteStatement], it will automatically add a span
7+
* out of the active span bound to the scope when it is executed.
8+
* It's a wrapper around an instance of [SupportSQLiteStatement], and it's created automatically
9+
* by [SentrySupportSQLiteDatabase.compileStatement].
10+
*
11+
* @param delegate The [SupportSQLiteStatement] instance to delegate calls to.
12+
* @param sqLiteSpanManager The [SQLiteSpanManager] responsible for the creation of the spans.
13+
* @param sql The query string.
14+
*/
15+
internal class SentrySupportSQLiteStatement(
16+
private val delegate: SupportSQLiteStatement,
17+
private val sqLiteSpanManager: SQLiteSpanManager,
18+
private val sql: String
19+
) : SupportSQLiteStatement by delegate {
20+
21+
override fun execute() {
22+
return sqLiteSpanManager.performSql(sql) {
23+
delegate.execute()
24+
}
25+
}
26+
27+
override fun executeUpdateDelete(): Int {
28+
return sqLiteSpanManager.performSql(sql) {
29+
delegate.executeUpdateDelete()
30+
}
31+
}
32+
33+
override fun executeInsert(): Long {
34+
return sqLiteSpanManager.performSql(sql) {
35+
delegate.executeInsert()
36+
}
37+
}
38+
39+
override fun simpleQueryForLong(): Long {
40+
return sqLiteSpanManager.performSql(sql) {
41+
delegate.simpleQueryForLong()
42+
}
43+
}
44+
45+
override fun simpleQueryForString(): String? {
46+
return sqLiteSpanManager.performSql(sql) {
47+
delegate.simpleQueryForString()
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)