Adds PII-free AppLog latency breadcrumbs to the message-open path (issue #358), so a debug report can show where the reader's spinner time actually goes:
ImapClient.withStore: splits each op's connect (CONNECT+TLS+LOGIN) time from its own work time, plus a live connect-per-op connection gauge (useful context alongside issue #125's provider connection-ceiling findings).
fetchBodyMarkingSeen: adds select/body/flag phase timings plus PII-free size counts (RFC822 wire size, body char count, attachment count).
MailRepositoryImpl.openMessage: end-to-end open latency plus the cached-vs-fetched branch taken, keyed by the account's hashed accountLogRef and the folder's logSafeFolderLabel.
ReaderViewModel: spinner-to-ready latency, logged separately for the success and failure paths.
All of this is PII-free: accounts are only ever logged via the existing accountLogRef one-way hash (never the raw account id/email), folders via the existing logSafeFolderLabel allowlist (system folders only; anything else logs a fixed placeholder), and every other value is a size, duration, or boolean — never message content, subjects, or addresses.
Test tax
Four existing unit-test classes exercise this code but hadn't been touched by the earlier commits in this branch, so they crashed on the now-hit (but unmocked) android.util.Log calls — a throwing no-op stub under plain JVM unit tests:
Each now installs mockkStatic(Log::class) in setUp()/tears it down in tearDown(), following this repo's existing convention (MailBackfillerTest.kt's inline import, or ImapClientTest.kt's fully-qualified android.util.Log form for the GreenMail-backed IMAP tests, which deliberately never import Log). detekt.yml gains two more targeted ForbiddenImport excludes (MailRepositoryImplCoverageTest.kt, ReaderViewModelActionsTest.kt) alongside the existing ones for MailRepositoryImplTest.kt / ReaderViewModelTest.kt, plus the pre-existing targeted LargeClass exclude for the already boundary-sized MailRepositoryImplTest.
ImapFolderOpenLatencyTest specifically asserts IMAP connection/LOGIN counts against a real in-process server — those assertions are untouched and still pass, since the new breadcrumbs are pure local timing/counter bookkeeping plus a log call, not additional protocol traffic.
Logging behavior itself is covered by unit tests (matching the existing logging-epic precedent: assert on breadcrumb content via RingLogBuffer, not by mocking Log calls), and the existing reader E2E/instrumented suite continues to exercise the message-open UI path end to end.
Test plan
:app:assembleDebug
:app:testDebugUnitTest (all green, including the 4 previously-broken classes — MailRepositoryImplCoverageTest 36/36, ImapClientBackfillTest 3/3, ImapFolderOpenLatencyTest 7/7 with its connection/LOGIN count assertions intact, ReaderViewModelActionsTest 10/10)
:app:compileDebugAndroidTestKotlin
:app:lintDebug
:app:ktlintCheck
:app:detekt
CI's full multi-API E2E matrix + API 37 preview (left to CI per this repo's fast-gate convention for logging-only changes)
## Summary
Adds PII-free `AppLog` latency breadcrumbs to the message-open path (issue #358), so a debug report can show where the reader's spinner time actually goes:
- **`ImapClient.withStore`**: splits each op's connect (CONNECT+TLS+LOGIN) time from its own work time, plus a live connect-per-op connection gauge (useful context alongside issue #125's provider connection-ceiling findings).
- **`fetchBodyMarkingSeen`**: adds select/body/flag phase timings plus PII-free size counts (RFC822 wire size, body char count, attachment count).
- **`MailRepositoryImpl.openMessage`**: end-to-end open latency plus the cached-vs-fetched branch taken, keyed by the account's hashed `accountLogRef` and the folder's `logSafeFolderLabel`.
- **`ReaderViewModel`**: spinner-to-ready latency, logged separately for the success and failure paths.
All of this is **PII-free**: accounts are only ever logged via the existing `accountLogRef` one-way hash (never the raw account id/email), folders via the existing `logSafeFolderLabel` allowlist (system folders only; anything else logs a fixed placeholder), and every other value is a size, duration, or boolean — never message content, subjects, or addresses.
### Test tax
Four existing unit-test classes exercise this code but hadn't been touched by the earlier commits in this branch, so they crashed on the now-hit (but unmocked) `android.util.Log` calls — a throwing no-op stub under plain JVM unit tests:
- `MailRepositoryImplCoverageTest` (calls `openMessage`)
- `ImapClientBackfillTest` (real `ImapClient` via GreenMail)
- `ImapFolderOpenLatencyTest` (real `ImapClient` via GreenMail + a counting proxy)
- `ReaderViewModelActionsTest` (constructs `ReaderViewModel`)
Each now installs `mockkStatic(Log::class)` in `setUp()`/tears it down in `tearDown()`, following this repo's existing convention (`MailBackfillerTest.kt`'s inline import, or `ImapClientTest.kt`'s fully-qualified `android.util.Log` form for the GreenMail-backed IMAP tests, which deliberately never import `Log`). `detekt.yml` gains two more targeted `ForbiddenImport` excludes (`MailRepositoryImplCoverageTest.kt`, `ReaderViewModelActionsTest.kt`) alongside the existing ones for `MailRepositoryImplTest.kt` / `ReaderViewModelTest.kt`, plus the pre-existing targeted `LargeClass` exclude for the already boundary-sized `MailRepositoryImplTest`.
`ImapFolderOpenLatencyTest` specifically asserts IMAP connection/LOGIN *counts* against a real in-process server — those assertions are untouched and still pass, since the new breadcrumbs are pure local timing/counter bookkeeping plus a log call, not additional protocol traffic.
Logging behavior itself is covered by unit tests (matching the existing logging-epic precedent: assert on breadcrumb content via `RingLogBuffer`, not by mocking `Log` calls), and the existing reader E2E/instrumented suite continues to exercise the message-open UI path end to end.
## Test plan
- [x] `:app:assembleDebug`
- [x] `:app:testDebugUnitTest` (all green, including the 4 previously-broken classes — `MailRepositoryImplCoverageTest` 36/36, `ImapClientBackfillTest` 3/3, `ImapFolderOpenLatencyTest` 7/7 with its connection/LOGIN count assertions intact, `ReaderViewModelActionsTest` 10/10)
- [x] `:app:compileDebugAndroidTestKotlin`
- [x] `:app:lintDebug`
- [x] `:app:ktlintCheck`
- [x] `:app:detekt`
- [ ] CI's full multi-API E2E matrix + API 37 preview (left to CI per this repo's fast-gate convention for logging-only changes)
🤖 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.
Summary
Adds PII-free
AppLoglatency breadcrumbs to the message-open path (issue #358), so a debug report can show where the reader's spinner time actually goes:ImapClient.withStore: splits each op's connect (CONNECT+TLS+LOGIN) time from its own work time, plus a live connect-per-op connection gauge (useful context alongside issue #125's provider connection-ceiling findings).fetchBodyMarkingSeen: adds select/body/flag phase timings plus PII-free size counts (RFC822 wire size, body char count, attachment count).MailRepositoryImpl.openMessage: end-to-end open latency plus the cached-vs-fetched branch taken, keyed by the account's hashedaccountLogRefand the folder'slogSafeFolderLabel.ReaderViewModel: spinner-to-ready latency, logged separately for the success and failure paths.All of this is PII-free: accounts are only ever logged via the existing
accountLogRefone-way hash (never the raw account id/email), folders via the existinglogSafeFolderLabelallowlist (system folders only; anything else logs a fixed placeholder), and every other value is a size, duration, or boolean — never message content, subjects, or addresses.Test tax
Four existing unit-test classes exercise this code but hadn't been touched by the earlier commits in this branch, so they crashed on the now-hit (but unmocked)
android.util.Logcalls — a throwing no-op stub under plain JVM unit tests:MailRepositoryImplCoverageTest(callsopenMessage)ImapClientBackfillTest(realImapClientvia GreenMail)ImapFolderOpenLatencyTest(realImapClientvia GreenMail + a counting proxy)ReaderViewModelActionsTest(constructsReaderViewModel)Each now installs
mockkStatic(Log::class)insetUp()/tears it down intearDown(), following this repo's existing convention (MailBackfillerTest.kt's inline import, orImapClientTest.kt's fully-qualifiedandroid.util.Logform for the GreenMail-backed IMAP tests, which deliberately never importLog).detekt.ymlgains two more targetedForbiddenImportexcludes (MailRepositoryImplCoverageTest.kt,ReaderViewModelActionsTest.kt) alongside the existing ones forMailRepositoryImplTest.kt/ReaderViewModelTest.kt, plus the pre-existing targetedLargeClassexclude for the already boundary-sizedMailRepositoryImplTest.ImapFolderOpenLatencyTestspecifically asserts IMAP connection/LOGIN counts against a real in-process server — those assertions are untouched and still pass, since the new breadcrumbs are pure local timing/counter bookkeeping plus a log call, not additional protocol traffic.Logging behavior itself is covered by unit tests (matching the existing logging-epic precedent: assert on breadcrumb content via
RingLogBuffer, not by mockingLogcalls), and the existing reader E2E/instrumented suite continues to exercise the message-open UI path end to end.Test plan
:app:assembleDebug:app:testDebugUnitTest(all green, including the 4 previously-broken classes —MailRepositoryImplCoverageTest36/36,ImapClientBackfillTest3/3,ImapFolderOpenLatencyTest7/7 with its connection/LOGIN count assertions intact,ReaderViewModelActionsTest10/10):app:compileDebugAndroidTestKotlin:app:lintDebug:app:ktlintCheck:app:detekt🤖 Generated with Claude Code