Skip to content

ADFA-2314 | Enrich Sentry reports with diagnostic context#1468

Merged
hal-eisen-adfa merged 2 commits into
stagefrom
ADFA-2314-more-sentry-context
Jul 2, 2026
Merged

ADFA-2314 | Enrich Sentry reports with diagnostic context#1468
hal-eisen-adfa merged 2 commits into
stagefrom
ADFA-2314-more-sentry-context

Conversation

@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

Summary

Sentry crash reports were missing app-specific diagnostic info needed to reproduce and triage bugs without round-tripping the reporter (ADFA-2285, the SELinux-mislabel-on-update issue, is the motivating example).

This wires a single Sentry EventProcessor, registered in SentryAndroid.init, that enriches every event — fatal crashes, caught exceptions (ReportCaughtExceptionEvent) and plugin crashes alike — at capture time. Running at capture time (rather than a one-shot configureScope at startup) means it sees live boot state and the currently loaded plugins, with no per-call-site wiring.

Nearly every field already had a helper in the codebase, so this is primarily a wiring task.

Fields attached

  • ① SELinux context group — process context, private-data-dir file label, seinfo
  • ② boot_modedirect_boot vs credential_unlocked, queried live, plus boot_locked_duration_ms when the process started locked
  • ③ install_location — internal vs external/SD
  • ④ signing — SHA-256 certificate digest + official/unofficial build flag
  • ⑤ active_plugins context — enabled+loaded plugins with id, version, minIdeVersion, recent crash count
  • A release name/code + git commit + CI flag
  • B process ABI / cpu arch
  • H device posture — emulator vs physical, rooted

Reliability & PII

  • Each field is collected inside its own runCatching, so a single failing collector (an SELinux denial, or credential storage being inaccessible in direct boot) drops only that field — the event is still reported with everything else intact.
  • Only the SELinux file label is stored — never a path, project file, or source. Preserves the "no source code / no project file paths" guarantee.

Wiring (2 edits)

  • DeviceProtectedApplicationLoader.ktSentryDiagnosticsContext.install(options) inside the existing SentryAndroid.init block.
  • IDEApplication.ktSentryDiagnosticsContext.onUserUnlocked() in the existing ACTION_USER_UNLOCKED receiver (no new receiver needed).

No changes to CrashEventSubscriber, handleUncaughtException, or handlePluginCrash — the processor enriches those events automatically.

Implementation note

Sentry's Contexts type is not a Map, so context groups are attached via event.contexts.put(...). EventProcessor has only default methods (not SAM-convertible), so it's implemented as an anonymous object rather than a lambda.

Testing

  • :app:compileV8DebugKotlin — clean
  • :app:testV8DebugUnitTest — new SentryDiagnosticsContextTest (Robolectric + mockk), 2 tests, 0 failures:
    • enrich populates the expected tags/contexts on a fresh event
    • a throwing collector leaves the event intact and still populates the other fields (proves "one field failing never breaks reporting")
  • ⏳ Manual on-device verification (real crash → confirm tags in Sentry; boot-mode transition; multi-plugin list) — not yet run.

🤖 Generated with Claude Code

Register a single Sentry EventProcessor that attaches app-specific
diagnostics to every event (fatal crashes, caught exceptions and plugin
crashes alike):

- SELinux contexts (process, private-data-dir file label, seinfo)
- boot mode (direct-boot vs credential-unlocked) + locked duration
- install location (internal vs external)
- signing certificate SHA-256 digest + official-build flag
- active plugins (id, version, minIdeVersion, recent crash count)
- release name/code + git commit, process ABI/arch, device posture

Each field is collected inside its own runCatching so a single failing
collector (e.g. an SELinux denial, or credential storage being
inaccessible in direct boot) drops only that field and the event is
still reported. Running at capture time means the processor sees live
boot state and the currently loaded plugins with no per-call-site
wiring. Only the SELinux file label is stored, never any path or source.

Wiring: install() in SentryAndroid.init; onUserUnlocked() in the
existing ACTION_USER_UNLOCKED receiver.

Adds a Robolectric test proving enrich populates the event and that a
throwing collector never breaks the rest of the report.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough
  • Added a single Sentry EventProcessor that enriches all captured events with device, build, boot, SELinux, plugin, install, and process context at capture time.

  • Wired the processor into SentryAndroid.init and added an unlock callback so direct-boot timing can be included once the user unlocks the device.

  • Added tests covering successful enrichment and resilience when one diagnostic collector fails.

  • Risk/best-practice note: diagnostics are collected via multiple runCatching blocks, which protects event delivery, but the added context increases telemetry volume and should be reviewed for privacy/data-minimization compliance.

  • Risk/best-practice note: the new processor depends on runtime device state and package metadata; any API or platform edge cases may cause partial context loss, though the implementation is designed to degrade gracefully.

Walkthrough

Introduces a SentryDiagnosticsContext singleton that registers a Sentry EventProcessor to enrich events with SELinux, boot mode, install location, signing, plugin, version, and device posture data. Wires installation into Sentry init and unlock stamping into the app's unlock receiver, with accompanying unit tests.

Changes

Sentry Diagnostics Context

Layer / File(s) Summary
SentryDiagnosticsContext core implementation
app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt
New singleton caches app-derived values, exposes install(options) to register an EventProcessor, tracks boot/unlock timing via onUserUnlocked(), and enriches events with SELinux, boot mode, install location, signing, plugin, version/ABI, and device posture data using fault-isolated tag/context helpers.
Application wiring for install and unlock hook
app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt, app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
Calls SentryDiagnosticsContext.install(options) during SentryAndroid.init and SentryDiagnosticsContext.onUserUnlocked() in the ACTION_USER_UNLOCKED receiver flow.
Unit tests for enrichment behavior
app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt
New test suite verifies expected tags are set on enriched events and that a single failing collector does not prevent other tags from being applied.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Loader as DeviceProtectedApplicationLoader
  participant App as IDEApplication
  participant Diag as SentryDiagnosticsContext
  participant Sentry as Sentry SDK

  Loader->>Sentry: SentryAndroid.init(options)
  Loader->>Diag: install(options)
  Diag->>Sentry: register EventProcessor

  App->>App: onReceive(ACTION_USER_UNLOCKED)
  App->>Diag: onUserUnlocked()
  Diag->>Diag: record unlockElapsedMs

  Sentry->>Diag: process(event)
  Diag->>Diag: enrich(event) - selinux, boot mode, plugins, version, posture
  Diag-->>Sentry: enriched event
Loading

Poem

A rabbit hops through Sentry's log,
Enriching each event, unblocking the fog. 🐇
Boot mode, plugins, SHA in tow,
Unlocked at last, the context will show.
Hop, tag, enrich — off it goes!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding diagnostic context to Sentry reports.
Description check ✅ Passed The description matches the changeset and explains the Sentry enrichment and wiring accurately.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-2314-more-sentry-context

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt (1)

184-186: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache device posture like the other static values.

DeviceUtils.isEmulator()/isDeviceRooted() are recomputed on every enriched event. isDeviceRooted() loops over ~11 hardcoded paths doing File(...).exists() I/O each call — this doesn't change during the process lifetime, so it should be memoized like appInfo/seInfo/signingDigest/installLocation rather than re-run per event, especially since enrich() may run on the crashing thread for fatal events.

⚡ Proposed fix
+	private val isEmulator: Boolean by lazy { DeviceUtils.isEmulator() }
+	private val isRooted: Boolean by lazy { DeviceUtils.isDeviceRooted() }
+
 		// H — device posture (emulator vs physical, rooted).
-		tag(event, "device_emulator") { DeviceUtils.isEmulator().toString() }
-		tag(event, "device_rooted") { DeviceUtils.isDeviceRooted().toString() }
+		tag(event, "device_emulator") { isEmulator.toString() }
+		tag(event, "device_rooted") { isRooted.toString() }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt`
around lines 184 - 186, Cache the device posture values in
SentryDiagnosticsContext the same way as appInfo, seInfo, signingDigest, and
installLocation so they are computed once per process instead of on every
enrich() call. Update the enrich() tagging for device_emulator and device_rooted
to read from memoized values, and keep the caching near the existing static
diagnostics fields so DeviceUtils.isEmulator() and DeviceUtils.isDeviceRooted()
are not re-run per event.
app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt (1)

69-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify processor lookup.

Since a fresh SentryOptions() has no default processors, options.eventProcessors.single() is simpler than matching on the anonymous class name, and doubles as an assertion that install() registers exactly one processor.

🧹 Proposed simplification
 	private fun enrichNewEvent(): SentryEvent {
 		val options = SentryOptions()
 		SentryDiagnosticsContext.install(options)
-		val processor = options.eventProcessors
-			.first { it.javaClass.name.contains("SentryDiagnosticsContext") }
+		val processor = options.eventProcessors.single()
 		return processor.process(SentryEvent(), Hint())!!
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt`
around lines 69 - 75, Simplify the processor lookup in enrichNewEvent() by using
the single processor registered on SentryOptions after
SentryDiagnosticsContext.install(options) instead of searching by anonymous
class name. This should rely on options.eventProcessors.single() so the test
both locates the processor and asserts that install() registers exactly one
processor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt`:
- Around line 184-186: Cache the device posture values in
SentryDiagnosticsContext the same way as appInfo, seInfo, signingDigest, and
installLocation so they are computed once per process instead of on every
enrich() call. Update the enrich() tagging for device_emulator and device_rooted
to read from memoized values, and keep the caching near the existing static
diagnostics fields so DeviceUtils.isEmulator() and DeviceUtils.isDeviceRooted()
are not re-run per event.

In
`@app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt`:
- Around line 69-75: Simplify the processor lookup in enrichNewEvent() by using
the single processor registered on SentryOptions after
SentryDiagnosticsContext.install(options) instead of searching by anonymous
class name. This should rely on options.eventProcessors.single() so the test
both locates the processor and asserts that install() registers exactly one
processor.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a88af3c6-7fc8-4df2-98b5-7db70aecd309

📥 Commits

Reviewing files that changed from the base of the PR and between 75dfe19 and fc842d6.

📒 Files selected for processing (4)
  • app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt
  • app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
  • app/src/main/java/com/itsaky/androidide/handlers/SentryDiagnosticsContext.kt
  • app/src/test/java/com/itsaky/androidide/handlers/SentryDiagnosticsContextTest.kt

@hal-eisen-adfa hal-eisen-adfa merged commit 1d27a8a into stage Jul 2, 2026
2 checks passed
@hal-eisen-adfa hal-eisen-adfa deleted the ADFA-2314-more-sentry-context branch July 2, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants