feat(diagnostics): persistent redacted logging and an exportable bundle
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m12s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 37s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m10s
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m12s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Successful in 37s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 11s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m10s
The app forgot everything it did the moment it exited. The Rust half
logged through env_logger to stdout only -- invisible to anyone who
launched from a desktop icon, and on Android worse than that: stdout is
not logcat, so the backend produced no visible output at all on the
platform carrying this project's hardest bugs. The autoplay deadlock,
the truncated-stream restart and the background-audio stall were all
diagnosed by talking a user through `adb logcat`, because there was no
other way to see anything. A panic left nothing behind at all.
Logs now go to a size-capped rotating file, to logcat on Android, and to
the webview console in dev. A panic is recorded with its backtrace before
the process dies. The frontend's messages are forwarded into the same
file, so one timeline holds both halves of the app in order -- which is
what makes a race between them legible after the fact, and races between
them are the expensive bug class here.
Redaction runs in the log FORMATTER, not at export time. A credential
sitting in a file on the device is already a disclosure; stripping it on
the way out would be too late. The exporter redacts a second time to
cover files written by builds that predate this. api_key, X-Emby-Token,
Authorization, "AccessToken" and Token="..." all reduce to [REDACTED],
while host, item ids and filenames are deliberately kept -- a log scrubbed
of those is one nobody can debug anything from. Server URLs keep scheme
and host and drop any embedded user:pass@.
Two things the tests caught that review would not have:
- redact_headers recursed on its own output. The replacement keeps the
header NAME, so the next call matched the same header forever; the
test died with a stack overflow. It is a forward scan now.
- The frontend forwarder used `void plugin.error(...)`. `void` discards
a promise's value but not its rejection, so in any webview without
IPC -- a unit test, SSR, a browser preview -- every log line became an
unhandled rejection. 20 of them showed up the first time coverage
ran. Each call now attaches a catch.
Only info and above cross the IPC boundary: debug is per-tick player
state and forwarding it would be thousands of calls a minute for output
nobody reads. A failing forwarder never propagates and never prevents the
console write.
Nothing is transmitted anywhere. The export writes a zip and reports its
path; the user attaches it themselves, which is also what keeps this from
becoming telemetry. An Android share intent is explicitly out of scope --
it is Kotlin work that belongs with the other native code.
The panic hook chains to the previous hook rather than replacing it,
because utils/lock.rs installs a silencing hook around tests that provoke
poisoned locks on purpose.
Spec in docs/specs/diagnostics-and-logging.md; UR-078 / DR-218 / UT-209.
Verified: 1079 frontend tests and the coverage gate, 759 Rust tests,
clippy -D warnings, svelte-check 0 errors, and cargo check for
aarch64-linux-android.
This commit is contained in:
@@ -1763,6 +1763,36 @@ async playlistRemoveItems(handle: string, playlistId: string, entryIds: string[]
|
||||
async playlistMoveItem(handle: string, playlistId: string, itemId: string, newIndex: number) : Promise<null> {
|
||||
return await TAURI_INVOKE("playlist_move_item", { handle, playlistId, itemId, newIndex });
|
||||
},
|
||||
/**
|
||||
* Current log level and where the files are.
|
||||
*
|
||||
* TRACES: UR-078 | DR-218
|
||||
*/
|
||||
async diagnosticsGetInfo() : Promise<DiagnosticsInfo> {
|
||||
return await TAURI_INVOKE("diagnostics_get_info");
|
||||
},
|
||||
/**
|
||||
* Set the log level, for this session and the next.
|
||||
*
|
||||
* TRACES: UR-078 | DR-218
|
||||
*/
|
||||
async diagnosticsSetLevel(level: string) : Promise<string> {
|
||||
return await TAURI_INVOKE("diagnostics_set_level", { level });
|
||||
},
|
||||
/**
|
||||
* Write a redacted diagnostics archive and return where it went.
|
||||
*
|
||||
* # Blocking I/O
|
||||
*
|
||||
* This reads and rewrites every log file. It is an `async` command so it does
|
||||
* not block the IPC thread, but it must never be called from a player event
|
||||
* callback — see the deadlock note in CLAUDE.md.
|
||||
*
|
||||
* TRACES: UR-078 | DR-218
|
||||
*/
|
||||
async diagnosticsExport(serverUrl: string | null) : Promise<DiagnosticsBundle> {
|
||||
return await TAURI_INVOKE("diagnostics_export", { serverUrl });
|
||||
},
|
||||
/**
|
||||
* Format time in seconds to MM:SS display string
|
||||
*
|
||||
@@ -2037,6 +2067,34 @@ connectionError: string | null;
|
||||
* Whether we're currently checking connectivity
|
||||
*/
|
||||
isChecking: boolean }
|
||||
/**
|
||||
* Where an export landed, so the UI can tell the user where to find it.
|
||||
*/
|
||||
export type DiagnosticsBundle = {
|
||||
/**
|
||||
* Absolute path to the written archive.
|
||||
*/
|
||||
path: string; sizeBytes: number;
|
||||
/**
|
||||
* How many log files went in, excluding the environment summary.
|
||||
*/
|
||||
fileCount: number }
|
||||
/**
|
||||
* Where logs live and how verbose they currently are.
|
||||
*/
|
||||
export type DiagnosticsInfo = {
|
||||
/**
|
||||
* Directory holding the rotating log files.
|
||||
*/
|
||||
logDir: string;
|
||||
/**
|
||||
* Active level, lowercase: "error" | "warn" | "info" | "debug" | "trace".
|
||||
*/
|
||||
level: string;
|
||||
/**
|
||||
* Total bytes currently held by log files.
|
||||
*/
|
||||
totalSizeBytes: number }
|
||||
/**
|
||||
* On-disk usage of downloaded content, for the Downloads surface.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user