diff --git a/.gitignore b/.gitignore
index eee2dba424..0ce2c01344 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,6 @@
# Sentry Config File
sentry.properties
/opencode.json
+# JVM crash and replay logs
+hs_err_pid*.log
+replay_pid*.log
diff --git a/app/build.gradle b/app/build.gradle
index 773cd86c77..281cfb9b66 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -59,17 +59,20 @@ android {
buildTypes {
debug {
applicationIdSuffix = '.debug'
+ buildConfigField "String", "SENTRY_DSN", '""'
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
+ buildConfigField "String", "SENTRY_DSN", "\"${System.getenv('SENTRY_DSN') ?: ''}\""
}
nightly {
initWith release
applicationIdSuffix = '.nightly'
signingConfig signingConfigs.release
+ buildConfigField "String", "SENTRY_DSN", "\"${System.getenv('SENTRY_DSN') ?: ''}\""
}
}
buildFeatures {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d2dcbbf308..814c8a6568 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1689,7 +1689,7 @@
-
+
diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt
index 800bf9a553..e990e4e5a1 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/BaseApp.kt
@@ -71,6 +71,10 @@ open class BaseApp : Application(), Configuration.Provider {
super.onCreate()
PlatformRegistry.applicationContext = this // TODO replace with OkHttp.initialize
AppCompatDelegate.setDefaultNightMode(settings.theme)
+ // Initialize Sentry only if user has opted in
+ if (settings.isCrashAnalyticsEnabled) {
+ initializeSentry()
+ }
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
@@ -101,4 +105,21 @@ open class BaseApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(it)
}
}
+
+ private fun initializeSentry() {
+ try {
+ io.sentry.android.core.SentryAndroid.init(this) { options ->
+ // DSN is read from BuildConfig which gets it from SENTRY_DSN environment variable
+ // Only set if DSN is provided (non-empty)
+ if (BuildConfig.SENTRY_DSN.isNotEmpty()) {
+ options.dsn = BuildConfig.SENTRY_DSN
+ options.isEnableAutoSessionTracking = true
+ options.environment = if (BuildConfig.DEBUG) "debug" else "production"
+ }
+ }
+ } catch (e: Exception) {
+ // Log error but don't crash if Sentry initialization fails
+ e.printStackTrace()
+ }
+ }
}
diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt
index af6aa2ec37..ec676d8c8e 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt
@@ -580,6 +580,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isStatsEnabled: Boolean
get() = prefs.getBoolean(KEY_STATS_ENABLED, false)
+ var isCrashAnalyticsEnabled: Boolean
+ get() = prefs.getBoolean(KEY_CRASH_ANALYTICS_ENABLED, true)
+ set(value) = prefs.edit { putBoolean(KEY_CRASH_ANALYTICS_ENABLED, value) }
+
val isAutoLocalChaptersCleanupEnabled: Boolean
get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false)
@@ -817,6 +821,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_DISCORD_RPC = "discord_rpc"
const val KEY_DISCORD_RPC_SKIP_NSFW = "discord_rpc_skip_nsfw"
const val KEY_DISCORD_TOKEN = "discord_token"
+ const val KEY_CRASH_ANALYTICS_ENABLED = "crash_analytics_enabled"
// keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version"
diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt
index 5ceeab3fb5..475b909230 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/MainActivity.kt
@@ -79,7 +79,6 @@ import io.github.landwarderer.futon.search.ui.suggestion.SearchSuggestionViewMod
import io.github.landwarderer.futon.search.ui.suggestion.adapter.SearchSuggestionAdapter
import javax.inject.Inject
import com.google.android.material.R as materialR
-import io.sentry.Sentry
@AndroidEntryPoint
class MainActivity : BaseActivity(), AppBarOwner, BottomNavOwner,
diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt
index bfffce87c3..b3c544be74 100644
--- a/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt
+++ b/app/src/main/kotlin/io/github/landwarderer/futon/main/ui/welcome/WelcomeSheet.kt
@@ -16,6 +16,8 @@ import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import io.github.landwarderer.futon.R
+import io.github.landwarderer.futon.core.prefs.AppSettings
+import javax.inject.Inject
import io.github.landwarderer.futon.core.model.titleResId
import io.github.landwarderer.futon.core.nav.router
import io.github.landwarderer.futon.core.ui.sheet.BaseAdaptiveSheet
@@ -33,6 +35,9 @@ import java.util.Locale
class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipClickListener, View.OnClickListener,
ActivityResultCallback {
+ @Inject
+ lateinit var settings: AppSettings
+
private val viewModel by viewModels()
private val backupSelectCall = registerForActivityResult(
@@ -52,6 +57,10 @@ class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipC
binding.chipBackup.setOnClickListener(this)
binding.chipSync.setOnClickListener(this)
binding.chipDirectories.setOnClickListener(this)
+ binding.switchCrashReporting.isChecked = settings.isCrashAnalyticsEnabled
+ binding.switchCrashReporting.setOnCheckedChangeListener { _, isChecked ->
+ settings.isCrashAnalyticsEnabled = isChecked
+ }
viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged)
viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged)
@@ -125,4 +134,5 @@ class WelcomeSheet : BaseAdaptiveSheet(), ChipsView.OnChipC
},
)
}
+
}
diff --git a/app/src/main/res/layout/sheet_welcome.xml b/app/src/main/res/layout/sheet_welcome.xml
index 282979dd03..07a8a0509c 100644
--- a/app/src/main/res/layout/sheet_welcome.xml
+++ b/app/src/main/res/layout/sheet_welcome.xml
@@ -76,6 +76,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
16dp
8dp
+ 24dp
6dp
8dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bec5eb19d2..61055e288b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -906,4 +906,6 @@
Default directory for downloading manga
This directory with all data will be deleted if you uninstall the application
%1$s available
+ Crash Reporting
+ Send anonymous crash logs to help improve the app. Takes effect after restart.
diff --git a/app/src/main/res/xml/pref_services.xml b/app/src/main/res/xml/pref_services.xml
index f78877bb10..692b65fc79 100644
--- a/app/src/main/res/xml/pref_services.xml
+++ b/app/src/main/res/xml/pref_services.xml
@@ -77,4 +77,11 @@
app:allowDividerAbove="true"
app:icon="@drawable/ic_discord" />
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d44796dc0d..ed4a160a84 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,7 +18,7 @@ desugar = "2.1.5"
diskLruCache = "1.5"
documentfile = "1.1.0"
fragment = "1.8.9"
-gradle = "8.13.0"
+gradle = "8.13.2"
guava = "33.4.8-android"
hilt = "1.3.0"
json = "20250517"