Skip to content
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

Refactor android sqlite instrumentation (SDK side) #2722

Merged
merged 10 commits into from
May 30, 2023
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ distributions/
*.vscode/
sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories
sentry-samples/sentry-samples-spring-boot-jakarta/spy.log
spy.log
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Features

- Introduce new `sentry-android-sqlite` integration ([#2722](https://github.com/getsentry/sentry-java/pull/2722))
- This integration replaces the old `androidx.sqlite` database instrumentation in the Sentry Android Gradle plugin
- A new capability to manually instrument your `androidx.sqlite` databases.
- 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.
- Add SentryWrapper for Callable and Supplier Interface ([#2720](https://github.com/getsentry/sentry-java/pull/2720))

## 6.20.0
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ object Config {
val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
val androidxCore = "androidx.core:core:1.3.2"
val androidxSqlite = "androidx.sqlite:sqlite:2.3.1"
romtsn marked this conversation as resolved.
Show resolved Hide resolved
val androidxRecylerView = "androidx.recyclerview:recyclerview:1.2.1"

val slf4jApi = "org.slf4j:slf4j-api:1.7.30"
Expand Down
23 changes: 23 additions & 0 deletions sentry-android-sqlite/api/sentry-android-sqlite.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public final class io/sentry/android/sqlite/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper : androidx/sqlite/db/SupportSQLiteOpenHelper {
public static final field Companion Lio/sentry/android/sqlite/SentrySupportSQLiteOpenHelper$Companion;
public synthetic fun <init> (Landroidx/sqlite/db/SupportSQLiteOpenHelper;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close ()V
public static final fun create (Landroidx/sqlite/db/SupportSQLiteOpenHelper;)Landroidx/sqlite/db/SupportSQLiteOpenHelper;
public fun getDatabaseName ()Ljava/lang/String;
public fun getReadableDatabase ()Landroidx/sqlite/db/SupportSQLiteDatabase;
public fun getWritableDatabase ()Landroidx/sqlite/db/SupportSQLiteDatabase;
public fun setWriteAheadLoggingEnabled (Z)V
}

public final class io/sentry/android/sqlite/SentrySupportSQLiteOpenHelper$Companion {
public final fun create (Landroidx/sqlite/db/SupportSQLiteOpenHelper;)Landroidx/sqlite/db/SupportSQLiteOpenHelper;
}

87 changes: 87 additions & 0 deletions sentry-android-sqlite/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.kotlin.config.KotlinCompilerVersion

plugins {
id("com.android.library")
kotlin("android")
jacoco
id(Config.QualityPlugins.gradleVersions)
id(Config.QualityPlugins.detektPlugin)
}

android {
compileSdk = Config.Android.compileSdkVersion
namespace = "io.sentry.android.sqlite"

defaultConfig {
targetSdk = Config.Android.targetSdkVersion
minSdk = Config.Android.minSdkVersion

// for AGP 4.1
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
}

buildTypes {
getByName("debug")
getByName("release") {
consumerProguardFiles("proguard-rules.pro")
}
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
}

testOptions {
animationsDisabled = true
unitTests.apply {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}

lint {
warningsAsErrors = true
checkDependencies = true

// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
checkReleaseBuilds = false
}

variantFilter {
if (Config.Android.shouldSkipDebugVariant(buildType.name)) {
ignore = true
}
}
}

tasks.withType<Test> {
configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = false
}
}

kotlin {
explicitApi()
}

dependencies {
api(projects.sentry)

compileOnly(Config.Libs.androidxSqlite)

implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))

// tests
testImplementation(Config.Libs.androidxSqlite)
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.androidxJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockitoInline)
}

tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
7 changes: 7 additions & 0 deletions sentry-android-sqlite/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
##---------------Begin: proguard configuration for SQLite ----------

# To ensure that stack traces is unambiguous
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
-keepattributes LineNumberTable,SourceFile

##---------------End: proguard configuration for SQLite ----------
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.sentry.android.sqlite

import android.database.SQLException
import io.sentry.HubAdapter
import io.sentry.IHub
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SpanStatus

internal class SQLiteSpanManager(
private val hub: IHub = HubAdapter.getInstance()
) {

init {
SentryIntegrationPackageStorage.getInstance().addIntegration("SQLite")
}

/**
* Performs a sql operation, creates a span and handles exceptions in case of occurrence.
*
* @param sql The sql query
* @param operation The sql operation to execute.
* In case of an error the surrounding span will have its status set to INTERNAL_ERROR
*/
@Suppress("TooGenericExceptionCaught")
@Throws(SQLException::class)
fun <T> performSql(sql: String, operation: () -> T): T {
val span = hub.span?.startChild("db.sql.query", sql)
return try {
val result = operation()
span?.status = SpanStatus.OK
result
} catch (e: Throwable) {
span?.status = SpanStatus.INTERNAL_ERROR
span?.throwable = e
throw e
} finally {
span?.finish()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.sentry.android.sqlite

import android.annotation.SuppressLint
import android.database.Cursor
import android.database.SQLException
import android.os.Build
import android.os.CancellationSignal
import androidx.annotation.RequiresApi
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement

/**
* The Sentry's [SentrySupportSQLiteDatabase], it will automatically add a span
* out of the active span bound to the scope for each database query.
* It's a wrapper around [SupportSQLiteDatabase], and it's created automatically
* by the [SentrySupportSQLiteOpenHelper].
*
* @param delegate The [SupportSQLiteDatabase] instance to delegate calls to.
* @param sqLiteSpanManager The [SQLiteSpanManager] responsible for the creation of the spans.
*/
internal class SentrySupportSQLiteDatabase(
private val delegate: SupportSQLiteDatabase,
private val sqLiteSpanManager: SQLiteSpanManager
) : SupportSQLiteDatabase by delegate {

/**
* Compiles the given SQL statement. It will return Sentry's wrapper around SupportSQLiteStatement.
*
* @param sql The sql query.
* @return Compiled statement.
*/
override fun compileStatement(sql: String): SupportSQLiteStatement {
return SentrySupportSQLiteStatement(delegate.compileStatement(sql), sqLiteSpanManager, sql)
}

@Suppress("AcronymName") // To keep consistency with framework method name.
override fun execPerConnectionSQL(
sql: String,
@SuppressLint("ArrayReturn") bindArgs: Array<out Any?>?
) {
sqLiteSpanManager.performSql(sql) {
delegate.execPerConnectionSQL(sql, bindArgs)
}
}

override fun query(query: String): Cursor {
return sqLiteSpanManager.performSql(query) {
delegate.query(query)
}
}

override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
return sqLiteSpanManager.performSql(query) {
delegate.query(query, bindArgs)
}
}

override fun query(query: SupportSQLiteQuery): Cursor {
return sqLiteSpanManager.performSql(query.sql) {
delegate.query(query)
}
}

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun query(
query: SupportSQLiteQuery,
cancellationSignal: CancellationSignal?
): Cursor {
return sqLiteSpanManager.performSql(query.sql) {
delegate.query(query, cancellationSignal)
}
}

@Throws(SQLException::class)
override fun execSQL(sql: String) {
sqLiteSpanManager.performSql(sql) {
delegate.execSQL(sql)
}
}

@Throws(SQLException::class)
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
sqLiteSpanManager.performSql(sql) {
delegate.execSQL(sql, bindArgs)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.sentry.android.sqlite

import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper

/**
* The Sentry's [SentrySupportSQLiteOpenHelper], it will automatically add a span
* out of the active span bound to the scope for each database query.
* It's a wrapper around an instance of [SupportSQLiteOpenHelper].
*
* You can wrap your custom [SupportSQLiteOpenHelper] instance with `SentrySupportSQLiteOpenHelper(myHelper)`.
* If you're using the Sentry Android Gradle plugin, this will be applied automatically.
*
* Usage - wrap your custom [SupportSQLiteOpenHelper] instance in [SentrySupportSQLiteOpenHelper]
*
* ```
* val openHelper = SentrySupportSQLiteOpenHelper.create(myOpenHelper)
* ```
*
* If you use Room you can wrap the default [FrameworkSQLiteOpenHelperFactory]:
*
* ```
* val database = Room.databaseBuilder(context, MyDatabase::class.java, "dbName")
* .openHelperFactory { configuration ->
* SentrySupportSQLiteOpenHelper.create(FrameworkSQLiteOpenHelperFactory().create(configuration))
* }
* ...
* .build()
* ```
*
* @param delegate The [SupportSQLiteOpenHelper] instance to delegate calls to.
*/
class SentrySupportSQLiteOpenHelper private constructor(
private val delegate: SupportSQLiteOpenHelper
) : SupportSQLiteOpenHelper by delegate {

private val sqLiteSpanManager = SQLiteSpanManager()

private val sentryWritableDatabase: SupportSQLiteDatabase by lazy {
SentrySupportSQLiteDatabase(delegate.writableDatabase, sqLiteSpanManager)
}

private val sentryReadableDatabase: SupportSQLiteDatabase by lazy {
SentrySupportSQLiteDatabase(delegate.readableDatabase, sqLiteSpanManager)
}

override val writableDatabase: SupportSQLiteDatabase
romtsn marked this conversation as resolved.
Show resolved Hide resolved
get() = sentryWritableDatabase

override val readableDatabase: SupportSQLiteDatabase
get() = sentryReadableDatabase

companion object {

// @JvmStatic is needed to let this method be accessed by our gradle plugin
@JvmStatic
fun create(delegate: SupportSQLiteOpenHelper): SupportSQLiteOpenHelper {
return if (delegate is SentrySupportSQLiteOpenHelper) {
delegate
} else {
SentrySupportSQLiteOpenHelper(delegate)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.sentry.android.sqlite

import androidx.sqlite.db.SupportSQLiteStatement

/**
* The Sentry's [SentrySupportSQLiteStatement], it will automatically add a span
* out of the active span bound to the scope when it is executed.
* It's a wrapper around an instance of [SupportSQLiteStatement], and it's created automatically
* by [SentrySupportSQLiteDatabase.compileStatement].
*
* @param delegate The [SupportSQLiteStatement] instance to delegate calls to.
* @param sqLiteSpanManager The [SQLiteSpanManager] responsible for the creation of the spans.
* @param sql The query string.
*/
internal class SentrySupportSQLiteStatement(
private val delegate: SupportSQLiteStatement,
private val sqLiteSpanManager: SQLiteSpanManager,
private val sql: String
) : SupportSQLiteStatement by delegate {

override fun execute() {
return sqLiteSpanManager.performSql(sql) {
delegate.execute()
}
}

override fun executeUpdateDelete(): Int {
return sqLiteSpanManager.performSql(sql) {
delegate.executeUpdateDelete()
}
}

override fun executeInsert(): Long {
return sqLiteSpanManager.performSql(sql) {
delegate.executeInsert()
}
}

override fun simpleQueryForLong(): Long {
return sqLiteSpanManager.performSql(sql) {
delegate.simpleQueryForLong()
}
}

override fun simpleQueryForString(): String? {
return sqLiteSpanManager.performSql(sql) {
delegate.simpleQueryForString()
}
}
}
Loading