feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: export a diagnostic bundle from the developer screen - #475

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export
Sep 1, 2026
Merged

feat: export a diagnostic bundle from the developer screen#475
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/android-diagnostics-export

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

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.xml already 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's if (!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 SelectedNames means 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.IO with 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 second os.Create truncates the file the first zip writer is still streaming into, and the share sheet then hands support a corrupt archive.

Scope

strings.xml additions 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@main with gomobile naming and Long/Int boundaries checked, brace balance, and every R.string/R.plurals reference confirmed present.

🤖 Generated with Claude Code

Ryanmello07and others added 2 commits August 31, 2026 22:08
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_014vN7zh8WeQYCtirhcA3aWw
@Ryanmello07
Ryanmello07 merged commit 5d23462 into urnetwork:mainSep 1, 2026
2 checks passed
Sign up for freeto 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.

1 participant

@Ryanmello07