From 7760c9692cce5df4a0eab78a2aec0dd38f002f30 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Tue, 30 Jun 2026 15:17:18 +0200 Subject: [PATCH] feat(app): crash reporting via Sentry (vendor-neutral, manual init) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production crash/ANR diagnostics — the app had none. Integrated manually (cleaner, reviewable diff than the wizard; no example-error crash injected). - Sentry Android Gradle plugin (io.sentry.android.gradle 4.14.1): auto-installs the SDK and, on release, uploads the R8 mapping so stack traces deobfuscate. The mapping upload reads the auth token from the SENTRY_AUTH_TOKEN env var or a git-ignored sentry.properties — never committed; the task SKIPS cleanly when absent, so CI and contributor builds without the secret still succeed. - Manual SentryAndroid.init() in PinakesApplication.onCreate (earliest point): DSN (a public ingest endpoint, safe in source), environment = debug/production by build type, release = applicationId@versionName+versionCode, isSendDefaultPii = false (no PII), tracesSampleRate = 0.0 (crash reporting only, no perf quota burn). Manifest disables the SDK auto-init ContentProvider so our init stays in control. - .gitignore: sentry.properties (auth token secret). Note: Sentry is NOT a Google service. Release APK grows 4.3 MB -> 9.0 MB (the sentry-android meta-package bundles native NDK crash capture); can be trimmed to sentry-android-core if size matters. Verified: assembleDebug + testDebugUnitTest 37/37 green; assembleRelease builds with R8, and uploadSentryProguardMappingsRelease is SKIPPED (no auth token) as designed. --- .gitignore | 3 +++ app/build.gradle.kts | 23 +++++++++++++++++++ app/src/main/AndroidManifest.xml | 5 ++++ .../com/pinakes/app/PinakesApplication.kt | 15 ++++++++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 2 ++ 6 files changed, 49 insertions(+) diff --git a/.gitignore b/.gitignore index e2c273a..7b0f010 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ build/ *.keystore # Generated i18n resources (regenerated from i18n/*.json at build) app/src/main/res/values*/strings.xml + +# Sentry auth token (mapping upload) — secret, never commit +sentry.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c506b7..c3cb013 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,7 @@ plugins { // kapt is already on the classpath via the Kotlin Gradle plugin — apply it // without a version (declaring one conflicts: "already on the classpath"). id("org.jetbrains.kotlin.kapt") + alias(libs.plugins.sentry) } // --------------------------------------------------------------------------- @@ -243,6 +244,28 @@ androidComponents { } } +// --------------------------------------------------------------------------- +// Sentry — crash reporting + ProGuard/R8 mapping upload (so release stack traces +// deobfuscate). The DSN is a public ingest endpoint and lives in source; the AUTH +// TOKEN (used only to UPLOAD mappings) is a secret and is read from the env or a +// git-ignored sentry.properties — never committed. Mapping upload is skipped (with +// a warning, not a failure) when no auth token is present, so CI/contributor builds +// without the secret still succeed. +// --------------------------------------------------------------------------- +sentry { + org.set("fabiodalez") + projectName.set("android") + authToken.set(System.getenv("SENTRY_AUTH_TOKEN")) + // Upload R8 mapping files so crash stack traces are readable in Sentry. + includeProguardMapping.set(true) + autoUploadProguardMapping.set(!System.getenv("SENTRY_AUTH_TOKEN").isNullOrBlank()) + // We call SentryAndroid.init() ourselves in PinakesApplication, so let the SDK's + // auto-install add the dependency but keep our explicit init in control. + autoInstallation { enabled.set(true) } + // Don't phone home build telemetry from this repo's builds. + telemetry.set(false) +} + dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ec19b12..d0e133a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,11 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Pinakes"> + + + + + options.dsn = + "https://a51e7537271cdf25767251d18d2e4ffa@o4511654498926592.ingest.de.sentry.io/4511654504300624" + options.environment = if (BuildConfig.DEBUG) "debug" else "production" + options.release = "${BuildConfig.APPLICATION_ID}@${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}" + options.isDebug = false + options.isSendDefaultPii = false // no IP / user data attached by default + options.tracesSampleRate = 0.0 // crash reporting only — no performance tracing + } + services = ServiceLocator(this) // Refresh the cached catalog every time the app comes to the foreground, so the diff --git a/build.gradle.kts b/build.gradle.kts index 132ad8d..09375a1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.sentry) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0add25..24c31da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ appcompat = "1.7.0" media3 = "1.4.1" room = "2.6.1" work = "2.9.1" +sentry = "4.14.1" junit = "4.13.2" coroutinesTest = "1.8.1" robolectric = "4.13" @@ -63,3 +64,4 @@ android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +sentry = { id = "io.sentry.android.gradle", version.ref = "sentry" }