Skip to content

feat: one zip a user can actually produce — the diagnostics bundle - #2354

Merged
laurentiu021 merged 1 commit into
mainfrom
feat/diagnostics-bundle
Sep 18, 2026
Merged

laurentiu021 merged 1 commit into
mainfrom
feat/diagnostics-bundle

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Problem

Producing evidence for a bug took four manually-coordinated steps, and the last two were: leave the app, open %LOCALAPPDATA%\SysManager\logs\ in Explorer, and work out which of up to fourteen daily rolling files covers the moment the bug happened.

The person most likely to hit a bug cannot do either. So their bug was not badly reported — it was unreportable.

What changed

About has a "Save diagnostics bundle" button. One zip holding the system report, the environment block the tab already builds for the clipboard, and the three newest rolling logs, written wherever the user points the dialog. Every input already existed as a working service; this is composition, not new capability.

The privacy half, which is the part worth reading

The report inside is a sharable variant with each adapter's MAC dropped and the IPv4 host part masked. That is not caution. This file is built specifically to be attached to a public issue; a MAC address is a permanent hardware identifier that survives a Windows reinstall; and nobody can withdraw one once it is in a GitHub thread. Neither it nor the exact host address answers any question a bug report asks.

The full report is deliberately untouched — a file you keep is a different thing from one you hand to a stranger. #2352, filed while building this, carries the question of whether the existing exports should redact by default too.

Redacted from the data, not by pattern-matching the rendered report. A generic IPv4 regex over the finished text would also match a four-part version string: 1.111.0.0 has four valid octets. It would have quietly rewritten the version line while claiming to protect an address.

The logs need no such treatment — the sink already strips the Windows user name from every line, message, property and exception text alike, before it reaches disk — but the README.txt inside says so, along with every file in the zip, that nothing was uploaded, and that the logs record which tabs were opened and which cleanups were run. A bundle whose contents you have to take on trust is one a careful user will not send, and the careful users file the good bugs.

Two decisions that look arbitrary and are not

Logs are picked by filename, not by LastWriteTime. The names sort chronologically, and a backup tool or a sync client touching a file rewrites its timestamp — which would silently select three old logs and omit the one holding the failure. The unit test writes its fixtures oldest-last, so an implementation ordering by write time returns exactly the wrong three and fails.

An oversized log is cut from its front. A log reads in order, so the failure being reported is at the tail; truncating the other way round compiles, passes a size check, and reliably discards the only part anyone needs. The note replacing the partial first line says how much went.

Also FileShare.ReadWrite on the log read, because the sink holds the current day's file open. Without it every bundle would have omitted today's log — the one most likely to contain the failure — and no size check would have noticed.

Verification

16 unit tests, 2 integration tests. The split is deliberate: WriteAsync reads WMI twice through the report service, so the packaging goes through an internal PackAsync that takes an already-gathered report. That keeps the unit suite free of system dependencies (0.25s for all 16) while the redaction decision stays on the one path a caller can reach.

The integration pair is what makes the bundle safe rather than intended to be safe. It asserts against this machine's real adapters, and asserts the full report still carries them — so "nothing found" cannot pass for "something was removed". Neither failure message prints a value: they reach a public CI log, so a naive Assert.DoesNotContain(mac, report) would have published the exact identifier it exists to protect, on the day it broke.

Red ritual — eight mutations, each rebuilt before running, each restored from saved bytes:

Mutation Red
the bundle packages the full report 1
the MAC survives redaction 1
host masking becomes a no-op 3
logs ordered by write time 1
oversized log keeps its head instead of its tail 1
log opened without a write share 1
README stops naming the omissions 1
the command loses its only binding 1 — caught by EveryViewModelCommand_IsReachableFromTheUi

The ritual found a hole in one of my own tests. The truncation test put its only early marker on line one — which the implementation drops anyway, to remove the partial line a byte-offset read starts on — so a mutation that read the whole file and dropped just that line passed. A second marker further in, plus an assertion on the returned size, are what closed it.

Two existing guards caught real problems in the new code, and both were right. DiagnosticsBundleService wrote the zip in place — now AtomicFile, because a torn zip the user attaches makes the round trip longer rather than shorter — and its constructor documented its parameters without a <summary>.

  • Unit suite: 5815 passed, 0 failed (5799 before).
  • All four projects: 0 errors, 0 warnings.
  • dotnet format --verify-no-changes: clean on app, unit and integration.
  • No network code in the bundle service, checked by grep as well as by reading.
  • Leak scan: 43 patterns over 11 changed files; only the long-standing false positive from a Windows privacy-toggle name, not in this diff.
  • CRLF preserved across the change set.

Docs

README's About section describes the bundle, what it omits and why. ARCHITECTURE documents both the new service and GenerateSharableReportAsync, including why the redaction happens on the data. SECURITY.md moved to 1.111.x.

Closes #1650
Refs #2352

Producing evidence for a bug took four manually-coordinated steps, and the last two
were: leave the app, open %LOCALAPPDATA%\SysManager\logs\ in Explorer, and work out
which of up to fourteen daily rolling files covers the moment the bug happened. The
person most likely to hit a bug cannot do either. So their bug was not badly
reported — it was unreportable (#1650).

About now has "Save diagnostics bundle": one zip holding the system report, the
environment block the tab already builds for the clipboard, and the three newest
rolling logs, written wherever the user points the dialog. Every input already
existed as a working service; this is composition.

The privacy half is the part worth reading.

The report inside is a SHARABLE variant with each adapter's MAC dropped and the
IPv4 host part masked. That is not caution: this file is built specifically to be
attached to a public issue, a MAC address is a permanent hardware identifier that
survives a Windows reinstall, and nobody can withdraw one once it is in a GitHub
thread. Neither it nor the exact host address answers any question a bug report
asks. The full report is deliberately untouched — a file you keep is a different
thing from one you hand to a stranger — and #2352, filed while building this,
carries whether the existing exports should redact by default too.

Redacted from the DATA, not by pattern-matching the rendered report. A generic IPv4
regex over the finished text would also match a four-part version string: 1.111.0.0
has four valid octets. It would have quietly rewritten the version line while
claiming to protect an address.

The logs need no such treatment — the sink already strips the Windows user name from
every line, message, property and exception text alike, before it reaches disk — but
the README inside says so, along with everything else in the zip, that nothing was
uploaded, and that the logs record which tabs were opened and which cleanups were
run. A bundle whose contents you have to take on trust is one a careful user will
not send, and the careful users file the good bugs.

Two decisions that look arbitrary and are not:

Logs are picked by FILENAME, not by LastWriteTime. The names sort chronologically,
and a backup tool or a sync client touching a file rewrites its timestamp — which
would silently select three old logs and omit the one holding the failure. The unit
test writes its fixtures oldest-last, so an implementation ordering by write time
returns exactly the wrong three and fails.

An oversized log is cut from its FRONT. A log reads in order, so the failure being
reported is at the tail; truncating the other way round compiles, passes a size
check, and reliably discards the only part anyone needs. The note that replaces the
partial first line says how much went.

Also: FileShare.ReadWrite on the log read, because the sink holds the current day's
file open. Without it every bundle would have omitted today's log — the one most
likely to contain the failure — and no size check would have noticed.

16 unit tests and 2 integration tests. The split is deliberate: WriteAsync reads WMI
twice through the report service, so the packaging goes through an internal PackAsync
that takes an already-gathered report, keeping the unit suite free of system
dependencies while the redaction decision stays on the one path a caller can reach.

The integration pair is the one that makes the bundle safe rather than intended to be
safe: it asserts against this machine's real adapters, AND asserts the FULL report
still carries them, so "nothing found" cannot pass for "something was removed".
Neither failure message prints a value — they reach a public CI log, so a naive
Assert.DoesNotContain would have published the exact identifier it exists to protect
on the day it broke.

Red ritual, eight mutations, each rebuilt before running and restored from saved
bytes: the bundle packaging the full report; the MAC surviving redaction; masking
becoming a no-op; logs ordered by write time; an oversized log keeping its head; the
log opened without a write share; the README dropping its disclosure; and the command
losing its only binding, which the reachability guard catches.

The ritual also found a hole in one of my own tests. The truncation test put its only
early marker on line one — which the implementation drops anyway, to remove the
partial line a byte-offset read starts on — so a mutation that read the whole file
and dropped just that line passed. A second marker further in, and an assertion on
the returned SIZE, are what closed it.

Two existing guards caught real problems in the new code and both were right:
DiagnosticsBundleService wrote the zip in place (now AtomicFile — a torn zip the user
attaches makes the round trip longer, not shorter), and its constructor documented
its parameters without a summary.

Unit 5815 passed, 0 failed. All four projects 0 warnings. dotnet format clean.
No network code in the bundle service, asserted by grep as well as by reading.
SECURITY.md moved to 1.111.x.

Closes #1650
Refs #2352
@laurentiu021
laurentiu021 merged commit 8ce1c37 into main Sep 18, 2026
6 checks passed
@laurentiu021
laurentiu021 deleted the feat/diagnostics-bundle branch September 18, 2026 07:29
Sign up for free to 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.

[Enhancement]: About - No one-click diagnostics bundle

1 participant