Repository navigation
feat(desktop): rotating logs, Copy logs, and Reveal log file - #95
leoisadev1 wants to merge 6 commits into
Conversation
Adds a persistent rotating convt.log next to PR #82's crash files, Copy logs (scrubbed diagnostics bundle on the clipboard), and Reveal log file in Settings. Failed conversions now include format ids on the existing reporter. Resend only uploads crash-*.log so the new app log is not treated as a panic. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
#82's scrubber treats dotted versions as filenames. OS version is not a secret, so the bundle leaves it alone. report_error stays as the generic wrapper for non-conversion errors. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
| if handle.metadata()?.len() >= ROTATE_BYTES { | ||
| let rotated = path.with_extension("log.1"); | ||
| let _ = std::fs::rename(path, rotated); | ||
| *handle = open_log(path)?; | ||
| } |
There was a problem hiding this comment.
🔴 Windows log grows without rotating
On Windows, write_rotated cannot rename the open log and ignores the failure. It reopens that same log, so every later rotation also fails and the file grows without limit.
Learn more
Rotation renames the active log once it reaches two megabytes. Windows does not permit renaming this file while the File handle remains open, and the result is ignored. Reopening the unchanged path leaves the same oversized log in place, so every subsequent write retries the failing rotation. An existing convt.log.1 also needs to be handled under Windows replacement rules.
Example: On Windows, convt.log reaches 2 MiB. The next write cannot rename its open handle; after another million log entries, the same file is still active instead of being capped by rotation.
Recommended fix: Drop or take the active File before renaming. Handle an existing rotated file on Windows, restore or reopen the active log if rotation fails, and test two consecutive rotations.
Was this helpful? React with 👍 or 👎 to provide feedback.
| #[cfg(target_os = "macos")] | ||
| { | ||
| return home.map(|p| p.join("Library/Logs/convt")); | ||
| return home.map(|p| p.join("Library/Logs/Convt")); |
There was a problem hiding this comment.
🟡 Pending macOS crash reports lose retries
After an upgrade on macOS, logs_dir points at Convt instead of the previous convt directory. resend scans only the new directory, leaving earlier unsent crashes unreported.
Learn more
The crash hook previously saved reports under ~/Library/Logs/convt. On case-sensitive macOS volumes, the new ~/Library/Logs/Convt directory is distinct. At startup, resend reads only the new location and does not discover pending reports in the former location. On a case-insensitive volume both names resolve to the same directory, so this specifically affects case-sensitive filesystems.
Example: A Mac with a case-sensitive volume has ~/Library/Logs/convt/crash-123-456.log without its .sent marker. After the upgrade, resend looks only under ~/Library/Logs/Convt, and that crash never retries.
Recommended fix: Migrate pending crash logs from the former directory or include the legacy macOS directory when scanning for unsent reports.
Was this helpful? React with 👍 or 👎 to provide feedback.
| fn write_rotated(file: &mut Option<File>, path: &Path, line: &str) -> io::Result<()> { | ||
| let Some(handle) = file.as_mut() else { | ||
| return Ok(()); | ||
| }; |
There was a problem hiding this comment.
🟡 Startup log failure persists all session
If open_log fails at startup, LogStore retains no file handle. write_rotated returns success for that state, so later writes never retry after the log folder becomes writable.
Learn more
The log store opens its file once during initialization. If the folder is temporarily unavailable, LogStore::open keeps None. Each later event enters write_rotated, which returns Ok(()) immediately for None, bypassing the retry in LogStore::push. Memory logs continue, but no persistent app log is written for the rest of the process.
Example: A configured log directory is unavailable at launch and becomes available a minute later. New warnings continue appearing on stderr but never appear in convt.log until restart.
Recommended fix: Attempt open_log(path) when the stored handle is None, or return an error from write_rotated for that case so the caller retries opening it.
Was this helpful? React with 👍 or 👎 to provide feedback.
| let _ = std::fs::create_dir_all(&dir); | ||
| #[cfg(not(test))] | ||
| cx.open_with_system(&dir); | ||
| #[cfg(test)] | ||
| self.revealed.push(dir); | ||
| self.logs_notice = Some("Opened the log folder.".into()); |
There was a problem hiding this comment.
🟡 Missing log folder reports successful reveal
When create_dir_all fails, reveal_logs still attempts to open the missing folder and announces success. Users receive no indication that Reveal log file failed.
Learn more
The reveal action creates the configured log directory before passing it to the system file manager. A filesystem error is ignored and the action unconditionally records a successful notice. The Support section shows that notice as the action result.
Example: CONVT_LOG_DIR names a directory under a read-only mount. Creation fails, the file manager receives a nonexistent path, yet Settings shows “Opened the log folder.”
Recommended fix: Check the create_dir_all result before opening the path. Set logs_notice to the error and return early when the directory cannot be created.
Was this helpful? React with 👍 or 👎 to provide feedback.
| let (settings, _) = f.settings(SettingsTab::General, cx); | ||
| assert!(shown(cx, settings, "copy-logs")); | ||
| assert!(shown(cx, settings, "reveal-log-file")); | ||
| click(cx, settings, "copy-logs"); | ||
| let text = clipboard_text(cx).expect("Copy logs wrote the clipboard"); | ||
| assert!(text.starts_with("convt-app "), "{text}"); | ||
| assert!(text.contains("os: "), "{text}"); | ||
| assert!(text.contains("os_version: "), "{text}"); | ||
| assert!(text.contains("arch: "), "{text}"); | ||
| assert!(text.contains("license: licensed (desktop)\n"), "{text}"); | ||
| assert!(text.contains("--- logs ---"), "{text}"); |
| } | ||
| #[cfg(target_os = "windows")] | ||
| { | ||
| "Windows".into() | ||
| } |
| for line in input.lines { | ||
| out.push_str(&crash_report::scrub(line)); | ||
| out.push('\n'); |
There was a problem hiding this comment.
🟨 Copied diagnostics can expose credentials
When a log line contains a credential without path characters, format_bundle copies it unchanged. scrub removes paths, not tokens, so sharing the advertised sanitized bundle can disclose the credential.
Was this helpful? React with 👍 or 👎 to provide feedback.
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { | ||
| let line = String::from_utf8_lossy(buf) | ||
| .trim_end_matches(['\n', '\r']) | ||
| .to_string(); | ||
| self.store.push(line); | ||
| Ok(buf.len()) |
There was a problem hiding this comment.
|
| let Some(handle) = file.as_mut() else { | ||
| return Ok(()); | ||
| }; |
There was a problem hiding this comment.
Once opening convt.log fails, disk logging cannot recover during that run. write_rotated returns success for a missing handle, so push never tries reopening it. Retry opening when the handle is missing so logging resumes after a temporary folder or storage failure.
Confidence: 5/5.
| #[cfg(target_os = "macos")] | ||
| { | ||
| return home.map(|p| p.join("Library/Logs/convt")); | ||
| return home.map(|p| p.join("Library/Logs/Convt")); |
There was a problem hiding this comment.
Old crash reports stop retrying
If a macOS user has a case-sensitive volume, changing the folder from Library/Logs/convt to Library/Logs/Convt leaves pending crash reports behind. resend reads only the new folder. Migrate the old crash files and their .sent markers, or also check the old folder.
Confidence: 5/5.
| cx.notify(); | ||
| return; | ||
| }; | ||
| let _ = std::fs::create_dir_all(&dir); |
There was a problem hiding this comment.
| #[cfg(target_os = "windows")] | ||
| { | ||
| "Windows".into() |
There was a problem hiding this comment.
…rt clicks Sanitize paths and secrets before they reach the in-memory ring or convt.log. Close the file handle before rename so rotation works on Windows. Open Settings taller in the Copy/Reveal tests so the Finder block on macOS does not hide the buttons. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
| if line.is_empty() { | ||
| return; | ||
| } | ||
| let line = sanitize(&line); |
There was a problem hiding this comment.
LogStore::push now passes the whole formatted entry through crash_report::scrub. That function treats words with short dotted suffixes as filenames, so a default tracing timestamp such as 2026-10-07T12:34:56.123456Z becomes <PATH> before it reaches memory or disk. Support can no longer tell when events happened or how far apart failures occurred. Keep the timestamp separate from the text that needs scrubbing.
Keep report_conversion format ids and clear automation clipboard state on a failed job. Resolves the Settings/model conflict after #82 merged main. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
| } else if lower.starts_with("cvt_") | ||
| || lower.starts_with("convt_") |
There was a problem hiding this comment.
The convt_ prefix check also removes tracing source names such as convt_app::ui and convt_core::registry. The default log layout includes these names, so ordinary app logs store <license-key> instead of the component that wrote them. This makes failures harder for support to trace. Match the actual key format, or keep tracing source names outside credential scrubbing.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
The shared scrubber stopped at the first space, so names after that in a home path leaked into logs and Copy logs. Consume the rest of the path on Mac, Windows and Linux, with a test for each. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
The desktop jobs passed. Database integration tests failed on billed_at and paid_at are not moved by a later snapshot after Postgres dropped the test database mid-run. That suite is unchanged by this PR. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Relation to #82
This PR is stacked on #82 (
cnv-57-posthog-error-tracking). #82 is still open, so this stays targeted there. After #82 merges, this PR will retarget to main and update from main.#82 already adds PostHog
$exceptionreporting for the desktop app, web Worker, and API, plus path/secret scrubbing and the shared telemetry opt-out. This PR does not add a second reporter, a Send report button, or another opt-out switch.What this PR adds on top of #82:
convt.log(last 200 lines also kept in memory) in the same folder as feat(telemetry): report desktop and web exceptions to PostHog #82's crash files. Override withCONVT_LOG_DIR. On macOS the folder is~/Library/Logs/Convt; Windows and Linux stay as in feat(telemetry): report desktop and web exceptions to PostHog #82.CopyLogs/RevealLogFileactions are registered so PR Redesign the convt desktop app UI on one visual system #68 can put them on a Help menu later.from_format/to_format(format ids only). Next-launch resend only uploadscrash-*.log, so the rotating app log is not treated as a panic.Review follow-up
convt.log./Users, WindowsC:\Users, Linux/home). A path such as a home folder with a space in a directory name is replaced as one<PATH>, so later words from that folder do not leak into logs or Copy logs.Verification
cargo test -p convt-app— 138 passed.cargo clippy -p convt-app --all-targets -- -D warnings