feat(reporting): opt-in debug reporting client (capture + review/submit) #42

Merged
JMR-dev merged 3 commits from feat-debug-reporting into main 2026-07-01 15:28:45 +00:00
JMR-dev commented 2026-07-01 05:03:52 +00:00 (Migrated from github.com)

Implements #32 (error/crash capture + entry point) and #33 (review & opt-in submit) in one PR. The design is strictly opt-in / user-initiated (F-Droid-safe): nothing is ever sent automatically — a report only leaves the device when the user taps Submit and an ingest endpoint is configured.

#32 — Capture + entry point

  • CrashReporter installs Thread.setDefaultUncaughtExceptionHandler (wired in LibreMailApplication), persists a structured crash record (stack trace + app/version/device metadata + recent logs) locally, then delegates to the previous handler so the normal crash flow still runs. Never auto-sent.
  • RingLogBuffer + AppLog — a bounded, in-memory, non-PII log ring buffer mirrored from Logcat.
  • DiagnosticsCollector assembles a minimal bundle: app version, Android version/device, a fixed non-PII settings allow-list (booleans/enum only — no emails or server names), and the log buffer.
  • ReportStore persists pending reports as JSON files under filesDir/debug_reports/ — deliberately not Room, so crash-time saves are simple and independent of the (possibly encrypted / mid-migration) app DB.
  • "Report a problem" entry in SettingsScreen creates a report on demand.

#33 — Review & opt-in submit

  • ReportReviewScreen shows the full payload verbatim (DebugReport.toSubmissionPayload() — the single rendering used for the preview, Copy, Save, and the POST body, so what the user sees is byte-for-byte what is sent), a free-text comment field, and a prominent PII disclaimer.
  • Explicit Submit / Discard, plus Copy and Save-to-file (SAF) alternatives.
  • ReportUploadWorker (WorkManager, network constraint, exponential backoff + retry cap, success/failure surfaced) POSTs to BuildConfig.DEBUG_REPORT_ENDPOINT.
  • On next launch a saved crash report is offered for review via a dialog (Review / Not now / Discard).

Acceptance

  • A forced crash produces a saved report offered for review on next launch (unit-tested).
  • "Report a problem" creates a report on demand.
  • The user can read the entire payload, add comments, see the PII disclaimer, and choose Submit or Discard.
  • Nothing leaves the device without an explicit Submit tap — the submit path (ReportSubmitter → WorkManager) is the only sender and is referenced only from ReportReviewViewModel.submit(); capture never touches it (unit-tested invariant).

Testing

Fast CI gate green locally (JDK 21): assembleDebug + testDebugUnitTest + lintDebug + ktlintCheck + detekt. 23 new JVM unit tests cover crash capture/persistence (offered next launch), diagnostic-bundle assembly (minimal, non-PII), JSON round-trip, and the "nothing sent without Submit" invariant.

Limitations / out of scope

  • Server delivery is not exercised here. BuildConfig.DEBUG_REPORT_ENDPOINT is empty by default; the Cloudflare Worker ingest (#34) is out of scope for this repo. With no endpoint, Submit short-circuits to an "online submission unavailable — use Copy/Save" state and never enqueues an upload. Real end-to-end delivery (and its scrubbing) is verified against #34.
  • No emulator here, so the capture→review logic is verified via unit tests only.

Closes #32
Closes #33

🤖 Generated with Claude Code

Implements **#32** (error/crash capture + entry point) and **#33** (review & opt-in submit) in one PR. The design is strictly **opt-in / user-initiated (F-Droid-safe)**: nothing is ever sent automatically — a report only leaves the device when the user taps **Submit** *and* an ingest endpoint is configured. ## #32 — Capture + entry point - **`CrashReporter`** installs `Thread.setDefaultUncaughtExceptionHandler` (wired in `LibreMailApplication`), persists a structured crash record (stack trace + app/version/device metadata + recent logs) **locally**, then delegates to the previous handler so the normal crash flow still runs. Never auto-sent. - **`RingLogBuffer` + `AppLog`** — a bounded, in-memory, non-PII log ring buffer mirrored from Logcat. - **`DiagnosticsCollector`** assembles a minimal bundle: app version, Android version/device, a **fixed non-PII settings allow-list** (booleans/enum only — no emails or server names), and the log buffer. - **`ReportStore`** persists pending reports as JSON files under `filesDir/debug_reports/` — deliberately **not** Room, so crash-time saves are simple and independent of the (possibly encrypted / mid-migration) app DB. - **"Report a problem"** entry in `SettingsScreen` creates a report on demand. ## #33 — Review & opt-in submit - **`ReportReviewScreen`** shows the **full payload verbatim** (`DebugReport.toSubmissionPayload()` — the single rendering used for the preview, Copy, Save, *and* the POST body, so what the user sees is byte-for-byte what is sent), a free-text **comment** field, and a **prominent PII disclaimer**. - Explicit **Submit** / **Discard**, plus **Copy** and **Save-to-file** (SAF) alternatives. - **`ReportUploadWorker`** (WorkManager, network constraint, exponential backoff + retry cap, success/failure surfaced) POSTs to `BuildConfig.DEBUG_REPORT_ENDPOINT`. - On next launch a saved **crash report is offered for review** via a dialog (Review / Not now / Discard). ## Acceptance - [x] A forced crash produces a saved report offered for review on next launch (unit-tested). - [x] "Report a problem" creates a report on demand. - [x] The user can read the entire payload, add comments, see the PII disclaimer, and choose Submit or Discard. - [x] **Nothing leaves the device without an explicit Submit tap** — the submit path (`ReportSubmitter` → WorkManager) is the only sender and is referenced only from `ReportReviewViewModel.submit()`; capture never touches it (unit-tested invariant). ## Testing Fast CI gate green locally (JDK 21): `assembleDebug` + `testDebugUnitTest` + `lintDebug` + `ktlintCheck` + `detekt`. 23 new JVM unit tests cover crash capture/persistence (offered next launch), diagnostic-bundle assembly (minimal, non-PII), JSON round-trip, and the "nothing sent without Submit" invariant. ## Limitations / out of scope - **Server delivery is not exercised here.** `BuildConfig.DEBUG_REPORT_ENDPOINT` is **empty by default**; the Cloudflare Worker ingest (#34) is out of scope for this repo. With no endpoint, Submit short-circuits to an "online submission unavailable — use Copy/Save" state and never enqueues an upload. Real end-to-end delivery (and its scrubbing) is verified against #34. - No emulator here, so the capture→review logic is verified via unit tests only. Closes #32 Closes #33 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.