Uh oh!
There was an error while loading. Please reload this page.
feat: export a diagnostic bundle from the developer screen - #475
Merged
Ryanmello07 merged 2 commits intoSep 1, 2026
Merged
Conversation
MainApplication pointed glog at filesDir itself. Move it to <filesDir>/logs/app via Sdk.setLogDirForProcess, inside the try/catch that urnetwork#473 added -- the sdk still returns an error when the directory cannot be created, because its os.TempDir() fallback lands on /data/local/tmp, which an app uid cannot write. The retention pass (clearOldLogs) only ever prunes the directory glog is currently pointed at, so moving the root would strand whatever pre-upgrade builds wrote into filesDir: up to four files of up to 16MB each, never pruned again, and no longer reachable through Sdk.getLogDir() -- which is what the feedback screen's share and export buttons read. A user who upgraded and then reported an incident that predated the upgrade would attach none of the logs that recorded it. So migrateLegacyLogFiles moves those files into the new per-process directory BEFORE glog is repointed, which hands the merged set to the same retention pass rather than doubling the storage. It renames rather than copies, never overwrites a file already under the new root (a name collision means an earlier launch already migrated it), and never deletes a log it could not move. Android is single-process, so "app" is the only subdirectory that ever appears; the layout matches iOS, where the app and the network extension would otherwise prune each other's history out of a shared directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014vN7zh8WeQYCtirhcA3aWw
A user reporting a connection fault has no way to hand over the logs
that recorded it. glog writes up to four 16MB files per process under
<filesDir>/logs, and nothing in the app reads them: the feedback
screen's share button attaches one file at a time, unredacted, and only
the app's own. There is no manifest saying what the device was doing,
no logcat, and nothing that can be safely posted in public.
This adds a Diagnostics section to the developer screen with three
controls -- "Export all logs (raw)", "Export redacted logs", and
"Choose logs…" with a per-file picker -- each producing one zip in
cacheDir/share, handed to the existing FileProvider share intent
(authority ${applicationId}.fileprovider, which res/xml/file_paths.xml
already exposes cacheDir/share to, so no manifest change).
Three things are load-bearing:
The section renders ABOVE DeveloperContent's `if (!connected) return`
guard. `connected` is `reliability != null`, which needs a live device,
so below that guard the export would be unreachable whenever the tunnel
is down -- exactly when someone needs to export logs. The export path
already tolerates a null device (deviceManager.device?.let { ... } for
the manifest), which is what makes that placement safe.
"Export selected" with nothing checked is refused, in the row's
`enabled` and again in exportSelectedDiagnostics. An empty
SelectedNames means "no filter" to the sdk -- a control labelled as a
narrow subset would otherwise write a complete RAW bundle: every
severity, every rotation, the logcat dump, and a manifest carrying
client_id and instance_id in the clear.
The export runs on Dispatchers.IO behind an `exporting` re-entrancy
guard, with "Exporting…" on screen while it does. It walks the on-disk
inventory, spawns logcat and deflates up to 4x16MB per process; on the
main thread that is an ANR past the ~5s input-dispatch watchdog, and
two taps inside one second name the same destination (the file name has
one-second resolution and only a `-redacted` discriminator), so the
second os.Create truncates the zip the first is still streaming into
and the share sheet hands support a corrupt archive.
The logcat dump goes in as platform/logcat.txt. `logcat -d -v
threadtime` dumps and exits, and since android 4.1 an app reads only
its OWN buffer, so no permission is involved and no other app's entries
are reachable; `-t` and a character cap bound it, because a developer
debugging this kind of fault has usually run `logcat -G 8M` and the
dump is live three times over -- kotlin String, Go string, deflate
input -- under a Go soft memory limit of 3/4 of the app heap.
A source that cannot be READ is recorded, never silently dropped. The
sdk reports per-file open/stat failures but swallows directory-read
ones, so logSourceUnavailableReason is the only place android can
notice an unreadable log directory; the reason is deliberately
path-free, because the sdk copies it verbatim into README.txt, the one
bundle entry written without the redaction transform.
The summary carries the file count as a number all the way to the
screen so R.plurals.dev_export_summary can select on it. Formatted in
the viewmodel it reads "Exported 1 log files" -- and one file is what a
selective export produces most often. Sizes go through the app's own
formatByteCountCompact rather than byteCount / 1024, which renders a
freshly rotated 400-byte log as "0 KiB", i.e. as an empty file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vN7zh8WeQYCtirhcA3aWwUh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #474. 4 files, +952/−2. Needs urnetwork/sdk#151 (merged).
A Diagnostics section in the existing developer screen: Export all logs (raw), Export redacted logs, and Choose logs… with a per-file picker. Delivery via the existing FileProvider share intent — no manifest change, since
res/xml/file_paths.xmlalready covers both paths.Also captures the app's own logcat (
logcat -d -v threadtime, which since Android 4.1 reads only this app's buffer).Three requirements here were each found the hard way
Worth stating, because none is visible from reading the diff and each cost a review round or a device test to discover:
1. The section renders ABOVE
DeveloperContent'sif (!connected) { …; return }guard. Below it, the export is unreachable whenever the tunnel is down — which is exactly when someone needs to export logs. A diagnostics affordance gated on a healthy connection is close to useless.2. "Export selected" is blocked when nothing is checked. An empty
SelectedNamesmeans no filter to the SDK, so a control labelled as a narrow subset would produce a complete raw bundle — every file, every severity, unredacted, plus a manifest carrying the client id. The opposite of what the label promises.3. The export runs on
Dispatchers.IOwith a re-entrancy guard and an in-progress indication. It does file I/O, spawns a logcat subprocess and zips up to 4×16 MB — on the main thread it ANRs. And because a large export takes seconds with no feedback, repeat tapping is the expected user behaviour: two taps inside one second reuse the same destination path, so the secondos.Createtruncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.Scope
strings.xmladditions are purely additive; no string deletions and no other locale files touched.Verification
Not compiled locally — no JDK/SDK/NDK. Upstream CI is the first real compile and the first execution of the new tests. Verified by inspection: SDK symbols greped against
sdk@mainwith gomobile naming and Long/Int boundaries checked, brace balance, and everyR.string/R.pluralsreference confirmed present.🤖 Generated with Claude Code