Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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 }}
Expand Down
12 changes: 6 additions & 6 deletions REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -149,6 +150,7 @@ android {
}
}

// Sentry gradle plugin config (crash reporting to GlitchTip).
sentry {
includeProguardMapping = false
}
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DiskReadViolation>(violatingFrames)
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
android:windowSoftInputMode="adjustResize" />

<!-- Required: set your sentry.io project identifier (DSN) -->
<!-- Crash/log reporting. These io.sentry.* keys configure the Sentry SDK,
which reports to our GlitchTip backend (GlitchTip speaks the Sentry
protocol); ${sentryDsn} carries the GlitchTip DSN. -->
<!-- Required: the GlitchTip project identifier (DSN) -->
<meta-data
android:name="io.sentry.dsn"
android:value="${sentryDsn}" />
Expand All @@ -151,7 +154,7 @@
<meta-data
android:name="io.sentry.send-default-pii"
android:value="true" />
<!-- Enable logs to be sent to Sentry -->
<!-- Enable logs to be sent to GlitchTip -->
<meta-data
android:name="io.sentry.logs.enabled"
android:value="true" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -74,24 +74,26 @@ 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)
setMinimumBreadcrumbLevel(Level.INFO)
setMinimumLevel(Level.WARN)
start()
}
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(sentryLogAppender)
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(glitchTipLogAppender)

Sentry.setUser(
User().apply {
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 10 additions & 7 deletions app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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") }
Expand Down
Loading
Loading