From ada77e0c4bdbcb26e39452b2e6e5d6cf9f2d91a1 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 17 Jul 2026 12:13:02 -0700 Subject: [PATCH 1/2] ADFA-4751: Rebrand remaining Sentry references to GlitchTip The functional migration (ADFA-4397) kept the io.sentry SDK because GlitchTip is Sentry-protocol-compatible, and re-pointed only the DSN. This is the follow-up branding cleanup it left behind. User-facing / docs / naming: - privacy_disclosure_message onboarding string (English + Indonesian): now names GlitchTip instead of Sentry. - REVIEW.md / SECURITY.md prose -> GlitchTip (kept the Sentry.captureException code example, which is the real SDK API). - Renamed our own identifiers: SentryDiagnosticsContext -> GlitchTipDiagnosticsContext (file + test), SENTRY_ENV_* -> GLITCHTIP_ENV_*, shouldReportToSentry -> shouldReportToGlitchTip, sentryLogAppender -> glitchTipLogAppender. - Backend/product comments and log strings -> GlitchTip. Deliberately kept (would break crash reporting): all io.sentry.* imports and Sentry.* API calls, the manifest io.sentry.* meta-data keys and ${sentryDsn} placeholder, the io.sentry deps / plugin / sentry {} block, and the proguard -keep io.sentry.** rules. Each kept anchor now carries a one-line note that the Sentry SDK is our GlitchTip client. Historical "(Sentry APPDEVFORALL-####)" provenance notes are left as-is. Spotless (ratchet) retabbed a few touched files that were previously space-indented; those whitespace-only hunks bring them to the tab standard. Note: local.properties (gitignored) still uses old sentryDsn* keys pointing at sentry.io while the build now reads GLITCHTIP_DSN -- a per-developer config drift to fix separately, not part of this PR. --- .github/workflows/analyze.yml | 8 +- REVIEW.md | 12 +- SECURITY.md | 2 +- app/build.gradle.kts | 4 +- app/proguard-rules.pro | 2 +- .../app/strictmode/WhitelistEngineTest.kt | 18 ++- app/src/main/AndroidManifest.xml | 7 +- .../app/DeviceProtectedApplicationLoader.kt | 30 ++--- .../itsaky/androidide/app/IDEApplication.kt | 17 +-- .../assets/AssetsInstallationHelper.kt | 119 +++++++++--------- .../fragments/onboarding/GreetingFragment.kt | 2 +- .../handlers/CrashEventSubscriber.kt | 49 ++++---- ...text.kt => GlitchTipDiagnosticsContext.kt} | 76 ++++++----- .../com/itsaky/androidide/ui/SafeLineChart.kt | 48 +++---- .../viewmodel/InstallationViewModel.kt | 71 ++++++----- .../assets/AssetsInstallationHelperTest.kt | 68 +++++----- ....kt => GlitchTipDiagnosticsContextTest.kt} | 12 +- .../events/editor/LayoutEditorEvents.kt | 10 +- gradle/libs.versions.toml | 3 +- .../src/main/res/values-in-rID/strings.xml | 2 +- resources/src/main/res/values/strings.xml | 2 +- 21 files changed, 298 insertions(+), 264 deletions(-) rename app/src/main/java/com/itsaky/androidide/handlers/{SentryDiagnosticsContext.kt => GlitchTipDiagnosticsContext.kt} (80%) rename app/src/test/java/com/itsaky/androidide/handlers/{SentryDiagnosticsContextTest.kt => GlitchTipDiagnosticsContextTest.kt} (91%) diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 0e87601811..e246c78621 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -75,9 +75,9 @@ jobs: - name: Assemble V8 Debug env: - # Build-time secrets consumed by the Gradle build (Sentry plugin, - # Firebase config). Scoped to this step so SonarCloud and other - # third-party actions never see them. + # Build-time secrets consumed by the Gradle build (GlitchTip crash + # reporting via the Sentry gradle plugin, Firebase config). Scoped to + # this step so SonarCloud and other third-party actions never see them. FIREBASE_CONSOLE_URL: ${{ secrets.FIREBASE_CONSOLE_URL }} GLITCHTIP_DSN: ${{ secrets.GLITCHTIP_DSN }} run: | @@ -101,7 +101,7 @@ jobs: env: GRADLE_OPTS: "-Xmx10g -XX:MaxMetaspaceSize=512m" SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - # The Gradle build also drives Sentry/Firebase configuration during + # The Gradle build also drives GlitchTip/Firebase configuration during # the unit-test compile path. FIREBASE_CONSOLE_URL: ${{ secrets.FIREBASE_CONSOLE_URL }} GLITCHTIP_DSN: ${{ secrets.GLITCHTIP_DSN }} diff --git a/REVIEW.md b/REVIEW.md index 0921dc6861..39de210c00 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -16,7 +16,7 @@ A review isn't done because it *looks* fine; it's done when you can **show what | Area | Evidence to show | |---|---| | Ticket / feature completeness | Requirements list from ADFA-####, each mapped to code + test (or flagged missing) | -| §1 Exceptions | Where new failure paths are caught; nothing new can reach the Sentry wrapper | +| §1 Exceptions | Where new failure paths are caught; nothing new can reach the GlitchTip wrapper | | §2 Leaks | LeakCanary result for the touched flows (clean, or the leak addressed) | | §3 Threading/StrictMode | No new main-thread I/O or long compute; StrictMode run clean, no app-code whitelist | | §4 Security | Which untrusted inputs were validated; secrets checked | @@ -31,7 +31,7 @@ Keep it proportional — a two-line change needs a two-line ledger. ## The 60-second checklist - [ ] **Feature complete:** does what the linked ticket asks — requirements implemented, intended flow covered by tests. -- [ ] **Exceptions** are handled locally — nothing unexpected reaches the global Sentry crash handler. +- [ ] **Exceptions** are handled locally — nothing unexpected reaches the global GlitchTip crash handler. - [ ] **No leaks** LeakCanary would catch later: every register/open/subscribe has a matching unregister/close in the right lifecycle callback. - [ ] **No main-thread disk/network I/O** — no new StrictMode violations, and no whitelisting of *our own* code. - [ ] **Security:** untrusted input (zip entries, URLs, file paths, web-server requests) is validated; no secrets in code, logs, or analytics. @@ -46,9 +46,9 @@ Keep it proportional — a two-line change needs a two-line ledger. --- -## 1. Exception handling — stay out of the Sentry crash wrapper +## 1. Exception handling — stay out of the GlitchTip crash wrapper -`IDEApplication` installs a global uncaught-exception handler (`handleUncaughtException`) that reports to **Sentry** and then runs the device/credential-protected loaders' handlers. An exception that escapes your code lands there and is recorded as a **crash**. That handler is a safety net, not a control-flow tool. +`IDEApplication` installs a global uncaught-exception handler (`handleUncaughtException`) that reports to **GlitchTip** and then runs the device/credential-protected loaders' handlers. An exception that escapes your code lands there and is recorded as a **crash**. That handler is a safety net, not a control-flow tool. - **Catch where you can recover.** Wrap I/O, parsing, IPC to the `tooling-api`, git, and plugin calls. Convert failures into a sealed error state (`…UiEffect.ShowError`, `Result`, `BuildState.Failed`) the UI can render. - **Never swallow silently.** A bare `catch (e: Exception) {}` hides bugs. At minimum log it; if it's notable-but-handled, report it explicitly with the established idiom: @@ -89,7 +89,7 @@ This app extracts archives, runs a local web server, stores git credentials and - **Injection / path traversal (Zip Slip):** template/project extraction (`ZipRecipeExecutor`) and any unzip must reject entries that resolve outside the target dir (`canonicalPath.startsWith(targetDir)`). Validate file paths built from user/project input. - **SQL:** use parameterized queries (`rawQuery(sql, args)` with `?` placeholders), never string-concatenated SQL. (Existing `WebServer`/tooltip queries already do this — match them.) -- **Secrets & credential storage:** git tokens, keystore/signing passwords → `EncryptedSharedPreferences` / the Android Keystore, never plaintext files, never committed, **never logged or sent to analytics/Sentry**. Scrub secrets from breadcrumbs and exception messages. +- **Secrets & credential storage:** git tokens, keystore/signing passwords → `EncryptedSharedPreferences` / the Android Keystore, never plaintext files, never committed, **never logged or sent to analytics/GlitchTip**. Scrub secrets from breadcrumbs and exception messages. - **Local web server (`WebServer`):** bind to loopback, scope what it serves, and don't reflect unsanitized input into responses. Treat every request as untrusted. - **Network:** HTTPS only; no disabled TLS/hostname verification; verify git remotes. - **Untrusted code/plugins:** respect the plugin manifest permission model (`plugin.permissions` in `AndroidManifest.xml`); don't widen plugin capabilities or load classes from untrusted sources without the manager's checks. @@ -188,7 +188,7 @@ Hold the change to the patterns in [ARCHITECTURE.md](ARCHITECTURE.md). The key r CoGo is meant to work **without a network** — editing, building, and running an app on-device must not depend on connectivity. Hold new work to that: - **Degrade gracefully offline.** A feature that needs the network must still launch, explain itself, and leave the rest of the app usable when there's no connection — never block a core flow (edit/build/run) on a request. -- **Network calls are non-blocking and failure-tolerant.** Analytics, Sentry, and Gemini calls run off the main thread and must tolerate timeouts/failures silently (no crash, no hang, no lost user action). A dropped analytics event is acceptable; a dropped keystroke is not. +- **Network calls are non-blocking and failure-tolerant.** Analytics, GlitchTip, and Gemini calls run off the main thread and must tolerate timeouts/failures silently (no crash, no hang, no lost user action). A dropped analytics event is acceptable; a dropped keystroke is not. - **No network on the critical path.** Don't introduce a connectivity dependency into startup, the editor, or the build pipeline. - **Verify it offline.** For a change to a network-touching flow, actually exercise it with the network off — `adb shell svc wifi disable && adb shell svc data disable` (re-enable after), or airplane mode — and confirm the core edit/build/run flow still works. Add an explicit offline test case for the path rather than trusting it by inspection. diff --git a/SECURITY.md b/SECURITY.md index 0c86ee17ab..9c56179e61 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -78,7 +78,7 @@ We render content in WebViews (tooltips, markdown preview, APK viewer) and run a ### 6. Android component & data exposure - **Exported components** — `activity`/`service`/`receiver`/`provider` with `android:exported="true"` and no permission is a standard finding. Export only what must be, and protect it with a signature-level permission. Validate all incoming `Intent` extras (intent-redirection / spoofing). - **PendingIntent** — must be `FLAG_IMMUTABLE` unless mutability is genuinely required. -- **Insecure storage** — no `MODE_WORLD_READABLE/WRITEABLE`; don't put sensitive data on external/shared storage; don't log file contents, tokens, or PII (scanners flag `Log`/print of tainted data, and it also leaks into Sentry/analytics). +- **Insecure storage** — no `MODE_WORLD_READABLE/WRITEABLE`; don't put sensitive data on external/shared storage; don't log file contents, tokens, or PII (scanners flag `Log`/print of tainted data, and it also leaks into GlitchTip/analytics). - **Manifest hygiene** — `android:allowBackup` and `android:debuggable` are flagged for sensitive apps; set deliberately. - Request the minimum permissions; over-requesting is flagged. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 03e154225a..ad2f3b64a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ plugins { id("kotlin-parcelize") id("androidx.navigation.safeargs.kotlin") id("com.itsaky.androidide.desugaring") + // Sentry gradle plugin; the SDK it wires up reports to our GlitchTip backend. alias(libs.plugins.sentry) alias(libs.plugins.google.services) } @@ -149,6 +150,7 @@ android { } } +// Sentry gradle plugin config (crash reporting to GlitchTip). sentry { includeProguardMapping = false } @@ -324,7 +326,7 @@ dependencies { implementation(libs.koin.android) implementation(libs.androidx.security.crypto) - // Sentry Android SDK (core + replay for quality configuration) + // Sentry Android SDK (core + replay for quality configuration); our GlitchTip client. implementation(libs.sentry.core) implementation(libs.sentry.android.core) implementation(libs.sentry.logback) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 71c0c2cf9a..fbbe621f83 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -128,7 +128,7 @@ *; } -## Sentry +## GlitchTip crash reporting (via the Sentry SDK; GlitchTip speaks the Sentry protocol) -keepattributes SourceFile,LineNumberTable -keep class io.sentry.** { *; } -dontwarn io.sentry.** diff --git a/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt b/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt index dc93a21754..8f004512d1 100644 --- a/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt +++ b/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt @@ -85,19 +85,17 @@ class WhitelistEngineTest { fun evaluateReturnsAllowForOplusUIFirstDiskReadViolation() { val violatingFrames = listOf( - // Minimal "realistic" prelude (as in Sentry) - stackTraceElement("android.os.StrictMode\$AndroidBlockGuardPolicy", "onReadFromDisk", "StrictMode.java", 1772,), - stackTraceElement("libcore.io.BlockGuardOs", "access", "BlockGuardOs.java", 74,), - stackTraceElement("java.io.UnixFileSystem", "checkAccess", "UnixFileSystem.java", 337,), - + // Minimal "realistic" prelude (as in GlitchTip) + stackTraceElement("android.os.StrictMode\$AndroidBlockGuardPolicy", "onReadFromDisk", "StrictMode.java", 1772), + stackTraceElement("libcore.io.BlockGuardOs", "access", "BlockGuardOs.java", 74), + stackTraceElement("java.io.UnixFileSystem", "checkAccess", "UnixFileSystem.java", 337), // Whitelisted sequence (adjacent, in-order) stackTraceElement("java.io.File", "exists", "File.java", 829), stackTraceElement("com.oplus.uifirst.Utils", "writeProcNode", "Utils.java", 139), - stackTraceElement("com.oplus.uifirst.OplusUIFirstManager", "writeProcNode", "OplusUIFirstManager.java", 382,), - stackTraceElement("com.oplus.uifirst.OplusUIFirstManager", "setBinderThreadUxFlag", "OplusUIFirstManager.java", 877,), - - // Minimal tail (system server / wm — optional but matches Sentry shape) - stackTraceElement("com.android.server.wm.ActivityRecordExtImpl", "hookSetBinderUxFlag", "ActivityRecordExtImpl.java", 3008,), + stackTraceElement("com.oplus.uifirst.OplusUIFirstManager", "writeProcNode", "OplusUIFirstManager.java", 382), + stackTraceElement("com.oplus.uifirst.OplusUIFirstManager", "setBinderThreadUxFlag", "OplusUIFirstManager.java", 877), + // Minimal tail (system server / wm — optional but matches GlitchTip shape) + stackTraceElement("com.android.server.wm.ActivityRecordExtImpl", "hookSetBinderUxFlag", "ActivityRecordExtImpl.java", 3008), ) val violation = createViolation(violatingFrames) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 45a8e39a65..c351166bac 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -128,7 +128,10 @@ android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" android:windowSoftInputMode="adjustResize" /> - + + @@ -151,7 +154,7 @@ - + diff --git a/app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt b/app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt index 928bfde0f7..0af8a83fd9 100644 --- a/app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt +++ b/app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt @@ -1,8 +1,13 @@ package com.itsaky.androidide.app +import android.os.Build +import android.provider.Settings import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext import com.itsaky.androidide.BuildConfig import com.itsaky.androidide.analytics.IAnalyticsManager import com.itsaky.androidide.app.strictmode.StrictModeConfig @@ -13,7 +18,7 @@ import com.itsaky.androidide.events.LspApiEventsIndex import com.itsaky.androidide.events.LspJavaEventsIndex import com.itsaky.androidide.events.ProjectsApiEventsIndex import com.itsaky.androidide.handlers.CrashEventSubscriber -import com.itsaky.androidide.handlers.SentryDiagnosticsContext +import com.itsaky.androidide.handlers.GlitchTipDiagnosticsContext import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE import com.itsaky.androidide.ui.themes.IThemeManager import com.itsaky.androidide.utils.Environment @@ -22,6 +27,8 @@ import com.termux.shared.reflection.ReflectionUtils import io.github.rosemoe.sora.widget.schemes.EditorColorScheme import io.sentry.Sentry import io.sentry.android.core.SentryAndroid +import io.sentry.logback.SentryAppender +import io.sentry.protocol.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -31,13 +38,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.slf4j.LoggerFactory import kotlin.system.exitProcess -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import ch.qos.logback.classic.LoggerContext -import io.sentry.logback.SentryAppender -import io.sentry.protocol.User -import android.os.Build -import android.provider.Settings /** * @author Akash Yadav @@ -74,16 +74,18 @@ internal object DeviceProtectedApplicationLoader : ) runCatching { + // Initialize the Sentry SDK; it reports to our GlitchTip backend + // (GlitchTip is Sentry-protocol-compatible), so the SDK types stay io.sentry. SentryAndroid.init(app) { options -> options.environment = - if (BuildConfig.DEBUG) IDEApplication.SENTRY_ENV_DEV else IDEApplication.SENTRY_ENV_PROD + if (BuildConfig.DEBUG) IDEApplication.GLITCHTIP_ENV_DEV else IDEApplication.GLITCHTIP_ENV_PROD - // Enrich every Sentry event with app-specific diagnostic context. - SentryDiagnosticsContext.install(options) + // Enrich every GlitchTip event with app-specific diagnostic context. + GlitchTipDiagnosticsContext.install(options) } val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext - val sentryLogAppender = + val glitchTipLogAppender = SentryAppender().apply { context = loggerContext setMinimumEventLevel(Level.OFF) @@ -91,7 +93,7 @@ internal object DeviceProtectedApplicationLoader : setMinimumLevel(Level.WARN) start() } - loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(sentryLogAppender) + loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(glitchTipLogAppender) Sentry.setUser( User().apply { @@ -145,7 +147,7 @@ internal object DeviceProtectedApplicationLoader : exception: Throwable, ) { // we can't write logs to files, nor we can show the crash handler - // activity to the user. Just report to Sentry and exit. + // activity to the user. Just report to GlitchTip and exit. Sentry.captureException(exception) IDEApplication.instance.uncaughtExceptionHandler?.uncaughtException(thread, exception) diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt index b673341529..e196ba4506 100755 --- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt +++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt @@ -29,7 +29,7 @@ import androidx.work.Configuration import com.itsaky.androidide.BuildConfig import com.itsaky.androidide.di.coreModule import com.itsaky.androidide.di.pluginModule -import com.itsaky.androidide.handlers.SentryDiagnosticsContext +import com.itsaky.androidide.handlers.GlitchTipDiagnosticsContext import com.itsaky.androidide.plugins.manager.core.PluginManager import com.itsaky.androidide.treesitter.TreeSitter import com.itsaky.androidide.utils.RecyclableObjectPool @@ -85,9 +85,9 @@ class IDEApplication : ) { if (intent?.action == Intent.ACTION_USER_UNLOCKED) { runCatching { unregisterReceiver(this) } - // Stamp the unlock time so Sentry's boot_mode context reflects the + // Stamp the unlock time so GlitchTip's boot_mode context reflects the // live state and can report the direct-boot locked duration. - SentryDiagnosticsContext.onUserUnlocked() + GlitchTipDiagnosticsContext.onUserUnlocked() coroutineScope.launch(Dispatchers.Default) { logger.info("Device unlocked! Loading all components...") CredentialProtectedApplicationLoader.load(this@IDEApplication) @@ -99,8 +99,8 @@ class IDEApplication : companion object { private val logger = LoggerFactory.getLogger(IDEApplication::class.java) - const val SENTRY_ENV_DEV = "development" - const val SENTRY_ENV_PROD = "production" + const val GLITCHTIP_ENV_DEV = "development" + const val GLITCHTIP_ENV_PROD = "production" @JvmStatic @SuppressLint("StaticFieldLeak") @@ -202,7 +202,7 @@ class IDEApplication : } override val workManagerConfiguration: Configuration - get() = Configuration.Builder().build() + get() = Configuration.Builder().build() private fun ensureKoinStarted() { runCatching { GlobalContext.get() }.getOrNull()?.let { return } @@ -243,7 +243,10 @@ class IDEApplication : } } - private fun isFinalizerWatchdogTimeout(thread: Thread, exception: Throwable): Boolean { + private fun isFinalizerWatchdogTimeout( + thread: Thread, + exception: Throwable, + ): Boolean { if (exception !is java.util.concurrent.TimeoutException) return false return thread.name.contains("FinalizerWatchdogDaemon") || exception.stackTrace.any { it.className.contains("Daemons\$FinalizerWatchdogDaemon") } diff --git a/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt b/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt index 3efc221669..39f668e428 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt @@ -5,8 +5,10 @@ import android.os.StatFs import androidx.annotation.WorkerThread import com.aayushatharva.brotli4j.Brotli4jLoader import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider -import com.itsaky.androidide.utils.useEntriesEach +import com.itsaky.androidide.resources.R import com.itsaky.androidide.utils.Environment.DEFAULT_ROOT +import com.itsaky.androidide.utils.flashError +import com.itsaky.androidide.utils.useEntriesEach import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -22,8 +24,6 @@ import org.adfa.constants.GRADLE_DISTRIBUTION_ARCHIVE_NAME import org.adfa.constants.LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME import org.adfa.constants.TEMPLATE_CORE_ARCHIVE import org.slf4j.LoggerFactory -import com.itsaky.androidide.resources.R -import com.itsaky.androidide.utils.flashError import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -43,8 +43,8 @@ import kotlin.math.pow typealias AssetsInstallerProgressConsumer = (AssetsInstallationHelper.Progress) -> Unit object AssetsInstallationHelper { - private const val STATUS_INSTALLING = "Installing" - private const val STATUS_FINISHED = "FINISHED" + private const val STATUS_INSTALLING = "Installing" + private const val STATUS_FINISHED = "FINISHED" sealed interface Result { data object Success : Result @@ -52,7 +52,7 @@ object AssetsInstallationHelper { data class Failure( val cause: Throwable?, val errorMessage: String? = cause?.message, - val shouldReportToSentry: Boolean = true + val shouldReportToGlitchTip: Boolean = true, ) : Result } @@ -60,8 +60,8 @@ object AssetsInstallationHelper { val message: String, ) - const val PLUGIN_ARTIFACTS_ZIP = "plugin-artifacts.zip" - private val logger = LoggerFactory.getLogger(AssetsInstallationHelper::class.java) + const val PLUGIN_ARTIFACTS_ZIP = "plugin-artifacts.zip" + private val logger = LoggerFactory.getLogger(AssetsInstallationHelper::class.java) private val ASSETS_INSTALLER = AssetsInstaller.CURRENT_INSTALLER const val BOOTSTRAP_ENTRY_NAME = "bootstrap.zip" @@ -83,14 +83,15 @@ object AssetsInstallationHelper { val isMissingAsset = generateSequence(e) { it.cause }.any { it is FileNotFoundException } val cause = if (isMissingAsset) MissingAssetsEntryException(e) else e - val msg = if (isMissingAsset) { - context.getString(R.string.err_missing_or_corrupt_assets, context.getString(R.string.app_name)) - } else { - e.message ?: context.getString(R.string.error_installation_failed) - } + val msg = + if (isMissingAsset) { + context.getString(R.string.err_missing_or_corrupt_assets, context.getString(R.string.app_name)) + } else { + e.message ?: context.getString(R.string.error_installation_failed) + } logger.error("Failed to install assets", e) onProgress(Progress(msg)) - return@withContext Result.Failure(cause, errorMessage = msg, shouldReportToSentry = !isMissingAsset) + return@withContext Result.Failure(cause, errorMessage = msg, shouldReportToGlitchTip = !isMissingAsset) } return@withContext Result.Success @@ -113,8 +114,8 @@ object AssetsInstallationHelper { LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME, BOOTSTRAP_ENTRY_NAME, GRADLE_API_NAME_JAR_ZIP, - PLUGIN_ARTIFACTS_ZIP, - TEMPLATE_CORE_ARCHIVE, + PLUGIN_ARTIFACTS_ZIP, + TEMPLATE_CORE_ARCHIVE, ) val stagingDir = Files.createTempDirectory(UUID.randomUUID().toString()) @@ -130,7 +131,7 @@ object AssetsInstallationHelper { true } catch (e: FileNotFoundException) { logger.error("ZIP file not found: {}", e.message) - flashError("File not found - ${e.message}") + flashError("File not found - ${e.message}") false } catch (e: ZipException) { logger.error("Invalid ZIP format: {}", e.message) @@ -147,9 +148,10 @@ object AssetsInstallationHelper { } try { - val entrySizes: Map = expectedEntries.associateWith { entry -> - ASSETS_INSTALLER.expectedSize(entry) - } + val entrySizes: Map = + expectedEntries.associateWith { entry -> + ASSETS_INSTALLER.expectedSize(entry) + } val totalSize = entrySizes.values.sum() @@ -175,14 +177,18 @@ object AssetsInstallationHelper { launch { var previousSnapshot = "" while (isActive) { - val installedSize = entryStatusMap - .filterValues { it == STATUS_FINISHED } - .keys - .sumOf { entrySizes[it] ?: 0 } - - val percent = if (totalSize > 0) { - (installedSize * 100.0 / totalSize) - } else 0.0 + val installedSize = + entryStatusMap + .filterValues { it == STATUS_FINISHED } + .keys + .sumOf { entrySizes[it] ?: 0 } + + val percent = + if (totalSize > 0) { + (installedSize * 100.0 / totalSize) + } else { + 0.0 + } val freeStorage = getAvailableStorage(File(DEFAULT_ROOT)) @@ -239,15 +245,15 @@ object AssetsInstallationHelper { Files.createDirectories(destDir) // Normalize and make destDir absolute for secure path validation val normalizedDestDir = destDir.toAbsolutePath().normalize() - + ZipInputStream(srcStream.buffered()).useEntriesEach { zipInput, entry -> // Validate entry name doesn't contain dangerous patterns if (entry.name.contains("..") || entry.name.startsWith("/") || entry.name.startsWith("\\")) { throw IllegalStateException("Zip entry contains dangerous path components: ${entry.name}") } - + val destFile = normalizedDestDir.resolve(entry.name).normalize() - + // Use Path.startsWith() for proper path validation instead of string comparison if (!destFile.startsWith(normalizedDestDir)) { // DO NOT allow extraction to outside of the target dir @@ -264,32 +270,29 @@ object AssetsInstallationHelper { } } - private fun getAvailableStorage(path: File): Long { - return try { - val stat = StatFs(path.absolutePath) - stat.availableBytes - } catch (e: Exception) { - logger.warn("Failed to get available storage for {}: {}", path, e.message) - -1L - } - } - - private fun formatBytes(bytes: Long): String { - val unit = 1024 - if (bytes < unit) return "$bytes B" - val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() - val pre = "KMGTPE"[exp - 1] - return String.format( - Locale.getDefault(), // use device locale - "%.1f %sB", - bytes / unit.toDouble().pow(exp.toDouble()), - pre - ) - } - - private fun formatPercent(value: Double): String { - return String.format(Locale.getDefault(), "%.1f%%", value) - } + private fun getAvailableStorage(path: File): Long = + try { + val stat = StatFs(path.absolutePath) + stat.availableBytes + } catch (e: Exception) { + logger.warn("Failed to get available storage for {}: {}", path, e.message) + -1L + } + + private fun formatBytes(bytes: Long): String { + val unit = 1024 + if (bytes < unit) return "$bytes B" + val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() + val pre = "KMGTPE"[exp - 1] + return String.format( + Locale.getDefault(), // use device locale + "%.1f %sB", + bytes / unit.toDouble().pow(exp.toDouble()), + pre, + ) + } + + private fun formatPercent(value: Double): String = String.format(Locale.getDefault(), "%.1f%%", value) private fun checkStorageAccessibility( context: Context, @@ -310,7 +313,7 @@ object AssetsInstallationHelper { return Result.Failure( IllegalStateException(errorMsg), errorMsg, - false + false, ) } return null diff --git a/app/src/main/java/com/itsaky/androidide/fragments/onboarding/GreetingFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/onboarding/GreetingFragment.kt index ffbcbb8bd2..d23817e9c9 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/onboarding/GreetingFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/onboarding/GreetingFragment.kt @@ -33,7 +33,7 @@ class GreetingFragment : FragmentWithBinding( ) { super.onViewCreated(view, savedInstanceState) - // EXPERIMENTAL MODE - Crash app in order to capture exception on Sentry + // EXPERIMENTAL MODE - Crash app in order to capture exception on GlitchTip if (FeatureFlags.isExperimentsEnabled) { binding.icon.setOnLongClickListener { crashApp() diff --git a/app/src/main/java/com/itsaky/androidide/handlers/CrashEventSubscriber.kt b/app/src/main/java/com/itsaky/androidide/handlers/CrashEventSubscriber.kt index 158ebf4f93..501597daa4 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/CrashEventSubscriber.kt +++ b/app/src/main/java/com/itsaky/androidide/handlers/CrashEventSubscriber.kt @@ -12,32 +12,33 @@ import org.slf4j.LoggerFactory import kotlin.system.exitProcess const val EXIT_CODE_CRASH = 1 + class CrashEventSubscriber { - private val log = LoggerFactory.getLogger(CrashEventSubscriber::class.java) + private val log = LoggerFactory.getLogger(CrashEventSubscriber::class.java) - @Suppress("unused") + @Suppress("unused") @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onReportCaughtException(ev: ReportCaughtExceptionEvent) { - try { - Sentry.configureScope { scope -> - ev.extras.forEach { (k, v) -> scope.setTag(k, v) } - ev.message?.let { scope.setExtra("message", it) } - } - Sentry.captureException(ev.throwable) + fun onReportCaughtException(ev: ReportCaughtExceptionEvent) { + try { + Sentry.configureScope { scope -> + ev.extras.forEach { (k, v) -> scope.setTag(k, v) } + ev.message?.let { scope.setExtra("message", it) } + } + Sentry.captureException(ev.throwable) - try { - val intent = Intent() - intent.action = CrashHandlerActivity.REPORT_ACTION - intent.putExtra(CrashHandlerActivity.TRACE_KEY, getFullStackTrace(ev.throwable)) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) + try { + val intent = Intent() + intent.action = CrashHandlerActivity.REPORT_ACTION + intent.putExtra(CrashHandlerActivity.TRACE_KEY, getFullStackTrace(ev.throwable)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) - exitProcess(EXIT_CODE_CRASH) - } catch (error: Throwable) { - log.error("Unable to show crash handler activity", error) - } - } catch (t: Throwable) { - log.error("Failed to forward exception to Sentry", t) - } - } -} \ No newline at end of file + exitProcess(EXIT_CODE_CRASH) + } catch (error: Throwable) { + log.error("Unable to show crash handler activity", error) + } + } catch (t: Throwable) { + log.error("Failed to forward exception to GlitchTip", t) + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt b/app/src/main/java/com/itsaky/androidide/handlers/GlitchTipDiagnosticsContext.kt similarity index 80% rename from app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt rename to app/src/main/java/com/itsaky/androidide/handlers/GlitchTipDiagnosticsContext.kt index 640b797624..fa889602ce 100644 --- a/app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt +++ b/app/src/main/java/com/itsaky/androidide/handlers/GlitchTipDiagnosticsContext.kt @@ -36,9 +36,11 @@ import io.sentry.SentryOptions import org.slf4j.LoggerFactory /** - * Enriches every Sentry event with app-specific diagnostic context (SELinux - * labels, boot mode, install location, signing certificate, active plugins, - * release/ABI/device posture). + * Enriches every crash event sent to GlitchTip with app-specific diagnostic + * context (SELinux labels, boot mode, install location, signing certificate, + * active plugins, release/ABI/device posture). GlitchTip is the backend; the + * `io.sentry` SDK is the client we report through (GlitchTip speaks the Sentry + * protocol), so the event types below are the SDK's. * * The context is attached through a single [EventProcessor] registered in * [io.sentry.android.core.SentryAndroid.init]. Running at capture time means a @@ -56,9 +58,8 @@ import org.slf4j.LoggerFactory * * @author Hal Eisen */ -object SentryDiagnosticsContext { - - private val log = LoggerFactory.getLogger(SentryDiagnosticsContext::class.java) +object GlitchTipDiagnosticsContext { + private val log = LoggerFactory.getLogger(GlitchTipDiagnosticsContext::class.java) /** Process/boot start stamp, used to compute the direct-boot locked duration. */ private val bootElapsedStartMs = SystemClock.elapsedRealtime() @@ -102,12 +103,17 @@ object SentryDiagnosticsContext { // DeviceProtectedApplicationLoader, which is reachable in direct boot. startedInDirectBoot = runCatching { !IDEApplication.instance.isUserUnlocked }.getOrDefault(false) - options.addEventProcessor(object : EventProcessor { - override fun process(event: SentryEvent, hint: Hint): SentryEvent { - runCatching { enrich(event) }.onFailure { log.warn("Failed to enrich Sentry event", it) } - return event - } - }) + options.addEventProcessor( + object : EventProcessor { + override fun process( + event: SentryEvent, + hint: Hint, + ): SentryEvent { + runCatching { enrich(event) }.onFailure { log.warn("Failed to enrich GlitchTip event", it) } + return event + } + }, + ) } /** @@ -129,7 +135,8 @@ object SentryDiagnosticsContext { buildMap { runCatching { SELinuxUtils.getContext() }.getOrNull()?.let { put("process_context", it) } runCatching { SELinuxUtils.getFileContext(app.filesDir.absolutePath) } - .getOrNull()?.let { put("file_context", it) } + .getOrNull() + ?.let { put("file_context", it) } runCatching { seInfo }.getOrNull()?.let { put("seinfo", it) } } } @@ -155,18 +162,21 @@ object SentryDiagnosticsContext { // ⑤ Active plugins (enabled + loaded) with version and recent crash count. context(event, "active_plugins") { val pm = PluginManager.getInstance() ?: return@context null - val plugins = pm.getAllPlugins() - .filter { it.isEnabled && it.isLoaded } - .map { info -> - mapOf( - "id" to info.metadata.id, - "version" to info.metadata.version, - "min_ide_version" to info.metadata.minIdeVersion, - "crash_count" to runCatching { - pm.crashTracker.getCrashCount(info.metadata.id) - }.getOrDefault(0), - ) - } + val plugins = + pm + .getAllPlugins() + .filter { it.isEnabled && it.isLoaded } + .map { info -> + mapOf( + "id" to info.metadata.id, + "version" to info.metadata.version, + "min_ide_version" to info.metadata.minIdeVersion, + "crash_count" to + runCatching { + pm.crashTracker.getCrashCount(info.metadata.id) + }.getOrDefault(0), + ) + } mapOf("count" to plugins.size, "plugins" to plugins) } @@ -187,17 +197,25 @@ object SentryDiagnosticsContext { } /** Sets a single tag, guarded so a failing collector drops only that tag. */ - private inline fun tag(event: SentryEvent, key: String, value: () -> String?) { + private inline fun tag( + event: SentryEvent, + key: String, + value: () -> String?, + ) { runCatching { value()?.let { event.setTag(key, it) } } - .onFailure { log.debug("Sentry diagnostics: dropped tag '{}'", key, it) } + .onFailure { log.debug("GlitchTip diagnostics: dropped tag '{}'", key, it) } } /** * Attaches a structured context group, guarded so a failing collector drops * only that group. Empty/null maps are skipped. */ - private inline fun context(event: SentryEvent, key: String, value: () -> Map?) { + private inline fun context( + event: SentryEvent, + key: String, + value: () -> Map?, + ) { runCatching { value()?.takeIf { it.isNotEmpty() }?.let { event.contexts.put(key, it) } } - .onFailure { log.debug("Sentry diagnostics: dropped context '{}'", key, it) } + .onFailure { log.debug("GlitchTip diagnostics: dropped context '{}'", key, it) } } } diff --git a/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt b/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt index c82bb603fe..3eda88b076 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/SafeLineChart.kt @@ -31,37 +31,37 @@ import org.slf4j.LoggerFactory * drawn from more than one thread at once, a reader can observe the new `mEntryCount` while `mEntries` is * still the old (shorter) array, throwing an [IndexOutOfBoundsException] from the label renderer. * - * This happens in AndroidIDE because Sentry Session Replay records the screen by drawing the view + * This happens in AndroidIDE because Sentry Session Replay (the SDK feature we use to report to + * GlitchTip) records the screen by drawing the view * hierarchy on a background thread, which races the main-thread updates of the memory-usage chart. The * chart is a non-critical diagnostic view, so dropping the occasional frame is preferable to crashing the * whole IDE. The next `invalidate()` recovers cleanly. * */ class SafeLineChart : LineChart { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int, - ) : super(context, attrs, defStyleAttr) + companion object { + private val log = LoggerFactory.getLogger(SafeLineChart::class.java) + } - companion object { - private val log = LoggerFactory.getLogger(SafeLineChart::class.java) - } + private var skippedFrames = 0L - private var skippedFrames = 0L - - override fun onDraw(canvas: Canvas) { - try { - super.onDraw(canvas) - } catch (e: IndexOutOfBoundsException) { - // Transient race in MPAndroidChart's axis renderer (see class doc). Skip this frame. - // Only log occasionally to avoid flooding logcat, since onDraw runs every frame. - if (skippedFrames++ % 60L == 0L) { - log.warn("Skipped {} chart frame(s) due to a transient axis-rendering race", skippedFrames, e) - } - } - } + override fun onDraw(canvas: Canvas) { + try { + super.onDraw(canvas) + } catch (e: IndexOutOfBoundsException) { + // Transient race in MPAndroidChart's axis renderer (see class doc). Skip this frame. + // Only log occasionally to avoid flooding logcat, since onDraw runs every frame. + if (skippedFrames++ % 60L == 0L) { + log.warn("Skipped {} chart frame(s) due to a transient axis-rendering race", skippedFrames, e) + } + } + } } diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt index 2da418b61e..8fb69212e4 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/InstallationViewModel.kt @@ -21,7 +21,6 @@ import com.itsaky.androidide.viewmodel.InstallationState.InstallationGranted import com.itsaky.androidide.viewmodel.InstallationState.InstallationPending import com.itsaky.androidide.viewmodel.InstallationState.Installing import io.sentry.Sentry -import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -32,9 +31,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory +import kotlin.coroutines.cancellation.CancellationException class InstallationViewModel : ViewModel() { - private val log = LoggerFactory.getLogger(InstallationViewModel::class.java) private val _state = MutableStateFlow(InstallationPending) @@ -90,12 +89,14 @@ class InstallationViewModel : ViewModel() { _state.update { InstallationComplete } } + is AssetsInstallationHelper.Result.Failure -> { - if (result.shouldReportToSentry) { + if (result.shouldReportToGlitchTip) { result.cause?.let { Sentry.captureException(it) } } - val errorMsg = result.errorMessage - ?: context.getString(R.string.title_installation_failed) + val errorMsg = + result.errorMessage + ?: context.getString(R.string.title_installation_failed) _events.emit(InstallationEvent.ShowError(errorMsg)) _state.update { InstallationError(errorMsg) @@ -127,42 +128,44 @@ class InstallationViewModel : ViewModel() { IJdkDistributionProvider.getInstance().installedDistributions.isNotEmpty() && Environment.ANDROID_HOME.exists() - /** - * Checks the app's internal storage and returns detailed information. - */ - private fun getStorageInfo(context: Context): StorageInfo { - val internalStoragePath = context.filesDir.path - val stat = StatFs(internalStoragePath) + /** + * Checks the app's internal storage and returns detailed information. + */ + private fun getStorageInfo(context: Context): StorageInfo { + val internalStoragePath = context.filesDir.path + val stat = StatFs(internalStoragePath) - val availableStorageInBytes = stat.availableBlocksLong * stat.blockSizeLong - val requiredStorageInBytes = getMinimumStorageNeeded().gigabytesToBytes() + val availableStorageInBytes = stat.availableBlocksLong * stat.blockSizeLong + val requiredStorageInBytes = getMinimumStorageNeeded().gigabytesToBytes() - val isLowStorage = availableStorageInBytes < requiredStorageInBytes + val isLowStorage = availableStorageInBytes < requiredStorageInBytes - val additionalBytesNeeded = (requiredStorageInBytes - availableStorageInBytes) - .coerceAtLeast(0L) + val additionalBytesNeeded = + (requiredStorageInBytes - availableStorageInBytes) + .coerceAtLeast(0L) - return StorageInfo(isLowStorage, availableStorageInBytes, additionalBytesNeeded) - } - - suspend fun checkStorageAndNotify(context: Context): Boolean = withContext(Dispatchers.IO) { - val storageInfo = getStorageInfo(context) + return StorageInfo(isLowStorage, availableStorageInBytes, additionalBytesNeeded) + } - if (storageInfo.isLowStorage) { - val additionalGBNeeded = storageInfo.additionalBytesNeeded.bytesToGigabytes() - val availableGB = storageInfo.availableBytes.bytesToGigabytes() + suspend fun checkStorageAndNotify(context: Context): Boolean = + withContext(Dispatchers.IO) { + val storageInfo = getStorageInfo(context) - val errorMessage = context.getString( - R.string.not_enough_storage, - additionalGBNeeded, - availableGB - ) + if (storageInfo.isLowStorage) { + val additionalGBNeeded = storageInfo.additionalBytesNeeded.bytesToGigabytes() + val availableGB = storageInfo.availableBytes.bytesToGigabytes() - _events.emit(InstallationEvent.ShowError(errorMessage)) - return@withContext false - } + val errorMessage = + context.getString( + R.string.not_enough_storage, + additionalGBNeeded, + availableGB, + ) - return@withContext true - } + _events.emit(InstallationEvent.ShowError(errorMessage)) + return@withContext false + } + return@withContext true + } } diff --git a/app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt b/app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt index 81660f0ca9..1cd9c8455b 100644 --- a/app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt +++ b/app/src/test/java/com/itsaky/androidide/assets/AssetsInstallationHelperTest.kt @@ -14,38 +14,38 @@ import org.junit.Test import java.io.FileNotFoundException class AssetsInstallationHelperTest { - - private val ctx: Context = mockk(relaxed = true) - - @Before - fun setup() { - mockkObject(AssetsInstallationHelper) - } - - @Test - fun `install with missing asset skips sentry`() = runBlocking { - val helper = AssetsInstallationHelper - - every { - helper["checkStorageAccessibility"](any(), any()) - } returns null - - coEvery { - helper["doInstall"](any(), any()) - } throws FileNotFoundException("data/common/gradle.zip.br") - - val result = helper.install(ctx) - - assertTrue("Expected Result.Failure", result is Failure) - val failure = result as Failure - assertFalse("Should skip Sentry report", failure.shouldReportToSentry) - assertTrue( - "Expected MissingAssetsEntryException as cause", - failure.cause is MissingAssetsEntryException - ) - assertTrue( - "Expected FileNotFoundException as root cause", - (failure.cause?.cause) is FileNotFoundException - ) - } + private val ctx: Context = mockk(relaxed = true) + + @Before + fun setup() { + mockkObject(AssetsInstallationHelper) + } + + @Test + fun `install with missing asset skips glitchtip`() = + runBlocking { + val helper = AssetsInstallationHelper + + every { + helper["checkStorageAccessibility"](any(), any()) + } returns null + + coEvery { + helper["doInstall"](any(), any()) + } throws FileNotFoundException("data/common/gradle.zip.br") + + val result = helper.install(ctx) + + assertTrue("Expected Result.Failure", result is Failure) + val failure = result as Failure + assertFalse("Should skip GlitchTip report", failure.shouldReportToGlitchTip) + assertTrue( + "Expected MissingAssetsEntryException as cause", + failure.cause is MissingAssetsEntryException, + ) + assertTrue( + "Expected FileNotFoundException as root cause", + (failure.cause?.cause) is FileNotFoundException, + ) + } } diff --git a/app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt b/app/src/test/java/com/itsaky/androidide/handlers/GlitchTipDiagnosticsContextTest.kt similarity index 91% rename from app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt rename to app/src/test/java/com/itsaky/androidide/handlers/GlitchTipDiagnosticsContextTest.kt index fc590e6589..a183f9c14b 100644 --- a/app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt +++ b/app/src/test/java/com/itsaky/androidide/handlers/GlitchTipDiagnosticsContextTest.kt @@ -35,15 +35,14 @@ import org.robolectric.RobolectricTestRunner import java.io.File /** - * Verifies that [SentryDiagnosticsContext] enriches events and, crucially, that + * Verifies that [GlitchTipDiagnosticsContext] enriches events and, crucially, that * a single failing field collector never prevents the rest of the event from * being reported. * * @author Hal Eisen */ @RunWith(RobolectricTestRunner::class) -class SentryDiagnosticsContextTest { - +class GlitchTipDiagnosticsContextTest { private lateinit var app: IDEApplication @Before @@ -68,9 +67,10 @@ class SentryDiagnosticsContextTest { /** Installs the processor on a fresh options instance and enriches a new event. */ private fun enrichNewEvent(): SentryEvent { val options = SentryOptions() - SentryDiagnosticsContext.install(options) - val processor = options.eventProcessors - .first { it.javaClass.name.contains("SentryDiagnosticsContext") } + GlitchTipDiagnosticsContext.install(options) + val processor = + options.eventProcessors + .first { it.javaClass.name.contains("GlitchTipDiagnosticsContext") } return processor.process(SentryEvent(), Hint())!! } diff --git a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/LayoutEditorEvents.kt b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/LayoutEditorEvents.kt index 9abede67ad..e5e875c643 100644 --- a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/LayoutEditorEvents.kt +++ b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/LayoutEditorEvents.kt @@ -2,10 +2,10 @@ package com.itsaky.androidide.eventbus.events.editor /** * Event to report *caught* (non-fatal) exceptions via EventBus, - * so that the :app module forwards them to Sentry without coupling here. + * so that the :app module forwards them to GlitchTip without coupling here. */ data class ReportCaughtExceptionEvent( - val throwable: Throwable, - val message: String? = null, - val extras: Map = emptyMap() -) \ No newline at end of file + val throwable: Throwable, + val message: String? = null, + val extras: Map = emptyMap() +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3b799a422..9f8e02e606 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -149,7 +149,8 @@ compose-activity = { module = "androidx.activity:activity-compose", version = "1 firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } -# Sentry +# Sentry SDK - our GlitchTip client (GlitchTip speaks the Sentry protocol). +# Aliases stay sentry-* to match the io.sentry: coordinates below. sentry-core = { module = "io.sentry:sentry", version.ref = "sentry" } sentry-android-core = { module = "io.sentry:sentry-android-core", version.ref = "sentry" } sentry-logback = { module = "io.sentry:sentry-logback", version.ref = "sentry" } diff --git a/resources/src/main/res/values-in-rID/strings.xml b/resources/src/main/res/values-in-rID/strings.xml index 64c60e7ef1..ac217e0794 100644 --- a/resources/src/main/res/values-in-rID/strings.xml +++ b/resources/src/main/res/values-in-rID/strings.xml @@ -585,7 +585,7 @@ Privasi Privasi & analitik - Code on the Go menggunakan Firebase Analytics dan Sentry untuk membantu kami meningkatkan aplikasi.\n\nFirebase Analytics mengumpulkan data penggunaan anonim untuk membantu kami memahami bagaimana aplikasi digunakan.\n\nSentry membantu kami melacak dan memperbaiki masalah.\n\nTidak ada informasi pribadi yang dikumpulkan atau dibagikan. Semua data diproses sesuai dengan kebijakan privasi kami. + Code on the Go menggunakan Firebase Analytics dan GlitchTip untuk membantu kami meningkatkan aplikasi.\n\nFirebase Analytics mengumpulkan data penggunaan anonim untuk membantu kami memahami bagaimana aplikasi digunakan.\n\nGlitchTip membantu kami melacak dan memperbaiki masalah.\n\nTidak ada informasi pribadi yang dikumpulkan atau dibagikan. Semua data diproses sesuai dengan kebijakan privasi kami. Saya mengerti Pelajari lebih lanjut diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index fa547343a5..059e605be8 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -657,7 +657,7 @@ Privacy Privacy & analytics - Code on the Go uses Firebase Analytics and Sentry to help us improve the app.\n\nFirebase Analytics collects anonymous usage data to help us understand how the app is used. \n\nSentry helps us track and fix errors.\n\nNo personal information is collected or shared. All data is processed in accordance with our privacy policy. + Code on the Go uses Firebase Analytics and GlitchTip to help us improve the app.\n\nFirebase Analytics collects anonymous usage data to help us understand how the app is used. \n\nGlitchTip helps us track and fix errors.\n\nNo personal information is collected or shared. All data is processed in accordance with our privacy policy. I understand Learn more From 97b16203e35fdab50db54c389eec0a7d12db62eb Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 17 Jul 2026 13:07:59 -0700 Subject: [PATCH 2/2] ADFA-4751: Use ASCII hyphen in touched WhitelistEngineTest comment Follow-up to code review: the retab re-touched this comment line, so swap its pre-existing em-dash for an ASCII '-' per CLAUDE.md code-style. --- .../com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt b/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt index 8f004512d1..c86b7abaf1 100644 --- a/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt +++ b/app/src/androidTest/kotlin/com/itsaky/androidide/app/strictmode/WhitelistEngineTest.kt @@ -94,7 +94,7 @@ class WhitelistEngineTest { stackTraceElement("com.oplus.uifirst.Utils", "writeProcNode", "Utils.java", 139), stackTraceElement("com.oplus.uifirst.OplusUIFirstManager", "writeProcNode", "OplusUIFirstManager.java", 382), stackTraceElement("com.oplus.uifirst.OplusUIFirstManager", "setBinderThreadUxFlag", "OplusUIFirstManager.java", 877), - // Minimal tail (system server / wm — optional but matches GlitchTip shape) + // Minimal tail (system server / wm - optional but matches GlitchTip shape) stackTraceElement("com.android.server.wm.ActivityRecordExtImpl", "hookSetBinderUxFlag", "ActivityRecordExtImpl.java", 3008), )