#294 (HIGH/security) — scrub PII from crash stack traces
DiagnosticsCollector captured throwable.stackTraceToString() verbatim, so mail/network exceptions (Jakarta Mail, java.net) could embed server host:port tokens and account emails/usernames into the report's stackTrace field — the one unscrubbed free-text field, violating the PII-free-reports constraint.
New StackTraceScrubber, applied in DiagnosticsCollector before the trace enters toSubmissionPayload()/toStorageJson():
keeps the non-PII value that makes a report useful — exception class names and every frame (at pkg.Class.method(File.kt:42));
drops the free-text message from each exception header line (the only place a hostname/username appears — a regex alone can't tell a hostname from a dotted class name);
as defense-in-depth, redacts any residual email or host:port left on a wrapped continuation line.
Frame lines are never touched, so a frame's File.kt:42 is never mistaken for a host:port.
Before → after (first line of a ConnectException): java.net.ConnectException: Failed to connect to imap.example.com/93.184.216.34:993 for user@example.com → java.net.ConnectException (frames below it kept intact).
#296 (MEDIUM/perf) — move ReportStore's initial scan off the main thread
ReportStore did MutableStateFlow(scan()) in its constructor (dir list + read + JSON-parse of every stored report). As an eager @Singleton dep of CrashReporter, whose install() runs on the main thread in Application.onCreate(), this was main-thread disk I/O that grows with the 30-day retention. The flow is now seeded empty and the initial scan dispatched to an injectable scope (Dispatchers.IO by default); reactive consumers update when it lands, and writes still re-scan synchronously so a crash-time save is never lost.
Tests
StackTraceScrubberTest: host/ip/port/email dropped from a ConnectException + auth-failure trace while classes/frames survive; regex redaction of a continuation line; null-message trace preserved verbatim.
DiagnosticsCollectorTest: end-to-end scrub through the collector.
ReportStoreTest: empty-seed + off-thread populate driven by a StandardTestDispatcher; existing tests use an Unconfined scope to keep their synchronous reopen semantics.
Gate: :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt all green (no emulator per scope).
Two Phase-3 reporting review findings.
## #294 (HIGH/security) — scrub PII from crash stack traces
`DiagnosticsCollector` captured `throwable.stackTraceToString()` verbatim, so mail/network exceptions (Jakarta Mail, `java.net`) could embed server `host:port` tokens and account emails/usernames into the report's `stackTrace` field — the one unscrubbed free-text field, violating the PII-free-reports constraint.
New `StackTraceScrubber`, applied in `DiagnosticsCollector` before the trace enters `toSubmissionPayload()`/`toStorageJson()`:
- keeps the non-PII value that makes a report useful — exception **class names** and every **frame** (`at pkg.Class.method(File.kt:42)`);
- **drops the free-text message** from each exception header line (the only place a hostname/username appears — a regex alone can't tell a hostname from a dotted class name);
- as defense-in-depth, **redacts** any residual email or `host:port` left on a wrapped continuation line.
Frame lines are never touched, so a frame's `File.kt:42` is never mistaken for a `host:port`.
Before → after (first line of a `ConnectException`):
`java.net.ConnectException: Failed to connect to imap.example.com/93.184.216.34:993 for user@example.com` → `java.net.ConnectException` (frames below it kept intact).
## #296 (MEDIUM/perf) — move ReportStore's initial scan off the main thread
`ReportStore` did `MutableStateFlow(scan())` in its constructor (dir list + read + JSON-parse of every stored report). As an eager `@Singleton` dep of `CrashReporter`, whose `install()` runs on the **main thread** in `Application.onCreate()`, this was main-thread disk I/O that grows with the 30-day retention. The flow is now seeded empty and the initial scan dispatched to an injectable scope (`Dispatchers.IO` by default); reactive consumers update when it lands, and writes still re-scan synchronously so a crash-time save is never lost.
## Tests
- `StackTraceScrubberTest`: host/ip/port/email dropped from a `ConnectException` + auth-failure trace while classes/frames survive; regex redaction of a continuation line; null-message trace preserved verbatim.
- `DiagnosticsCollectorTest`: end-to-end scrub through the collector.
- `ReportStoreTest`: empty-seed + off-thread populate driven by a `StandardTestDispatcher`; existing tests use an Unconfined scope to keep their synchronous reopen semantics.
Gate: `:app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detekt` all green (no emulator per scope).
Closes #294
Closes #296
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two Phase-3 reporting review findings.
#294 (HIGH/security) — scrub PII from crash stack traces
DiagnosticsCollectorcapturedthrowable.stackTraceToString()verbatim, so mail/network exceptions (Jakarta Mail,java.net) could embed serverhost:porttokens and account emails/usernames into the report'sstackTracefield — the one unscrubbed free-text field, violating the PII-free-reports constraint.New
StackTraceScrubber, applied inDiagnosticsCollectorbefore the trace enterstoSubmissionPayload()/toStorageJson():at pkg.Class.method(File.kt:42));host:portleft on a wrapped continuation line.Frame lines are never touched, so a frame's
File.kt:42is never mistaken for ahost:port.Before → after (first line of a
ConnectException):java.net.ConnectException: Failed to connect to imap.example.com/93.184.216.34:993 for user@example.com→java.net.ConnectException(frames below it kept intact).#296 (MEDIUM/perf) — move ReportStore's initial scan off the main thread
ReportStoredidMutableStateFlow(scan())in its constructor (dir list + read + JSON-parse of every stored report). As an eager@Singletondep ofCrashReporter, whoseinstall()runs on the main thread inApplication.onCreate(), this was main-thread disk I/O that grows with the 30-day retention. The flow is now seeded empty and the initial scan dispatched to an injectable scope (Dispatchers.IOby default); reactive consumers update when it lands, and writes still re-scan synchronously so a crash-time save is never lost.Tests
StackTraceScrubberTest: host/ip/port/email dropped from aConnectException+ auth-failure trace while classes/frames survive; regex redaction of a continuation line; null-message trace preserved verbatim.DiagnosticsCollectorTest: end-to-end scrub through the collector.ReportStoreTest: empty-seed + off-thread populate driven by aStandardTestDispatcher; existing tests use an Unconfined scope to keep their synchronous reopen semantics.Gate:
:app:testDebugUnitTest :app:compileDebugAndroidTestKotlin :app:ktlintCheck :app:detektall green (no emulator per scope).Closes #294
Closes #296
🤖 Generated with Claude Code