-
-
Notifications
You must be signed in to change notification settings - Fork 3
diagnostics and bug reports
Status: in progress in #56.
eepview keeps a small diagnostics log on your computer. When something goes wrong, you can turn that log into a GitHub issue. The app never sends the log. You send it yourself, from your normal web browser, after you read it.
A log line or a report never holds anything that identifies you or what you visited. This holds by construction: the log takes typed values only, never text. A second layer removes anything that still looks private from the final report text.
- An event code, for example
router-downorgatekeeper-refused. - Typed fields: enum states (router state, refuse reason, error kind, tab kind), numbers (duration in ms, count, HTTP status code, source line), and booleans.
- The UTC time, to the second.
No URL, host, path, query, page title, .i2p or b32 name, bookmark, history entry, search text, IP address, port of a remote peer, user name, home folder path, machine name, email address, router identity, hash or destination, cookie, header value, panic message, or free text from a page or from the OS. A refused request logs the reason kind, never the host. An error logs its kind, never its text.
| System | Folder |
|---|---|
| macOS | ~/Library/Logs/io.github.tcivie.eepview/ |
| Windows | %LOCALAPPDATA%\io.github.tcivie.eepview\logs\ |
| Linux | ~/.local/share/io.github.tcivie.eepview/logs/ |
The folder holds eepview.log, eepview.1.log and eepview.2.log (512 KB each at most), and a crashed marker after a crash. Report files go to your Downloads folder.
To delete the logs, open Settings > Privacy > Delete diagnostics logs. This also deletes the crashed marker. Report files you saved stay in Downloads: delete them there. Or delete the folder by hand while eepview is closed.
- Open the report page. There are three ways: the toolbar menu item "Report a problem", the crash banner on the home page, and the "Report this problem" link on the blocked and router-down pages.
- Describe the problem. Do not include site addresses.
- Read the preview. It is exactly the text that goes out.
- Click "Open a GitHub issue". eepview opens a prefilled issue form in your normal web browser, outside I2P. It also saves the same text to a file in Downloads and shows the file.
- Drag the file into the GitHub issue to attach it. GitHub has no URL parameter to attach a file, so a drag is the only way.
- Nothing is sent until you submit the issue on GitHub. GitHub sees your IP address and your GitHub account.
The requirement tests are written from this section. Names in code are the interface.
- R1.1
diag::event(code: Code, fields: &[Field])is the only logging path. It records the current UTC second, the code and the fields. - R1.2
Fieldhas noStringor&strvariant. Its variants and their keys:
| Variant | Key | Value |
|---|---|---|
Field::Router(RouterState) |
router |
verifying, ok, down, not-i2p, paused
|
Field::Refuse(RefuseReason) |
refuse |
not-i2p, bad-request, length-required, upstream, busy
|
Field::Error(ErrorKind) |
error |
see R1.4 |
Field::Tab(TabKind) |
tab |
internal, web
|
Field::Store(StoreKind) |
store |
bookmarks, history, settings, sites
|
Field::Op(OpKind) |
op |
reload, hard-reload, zoom, find, find-clear, back, forward, stop, harden, hover, input, clipboard, first-load, engine-filter, main-thread, spawn, reveal, open-url, write-report, console-load, console-close
|
Field::Thread(ThreadName) |
thread |
main, router-watch, router-check, gatekeeper, other
|
Field::Source(SourceFile) |
file |
a source file name, no folder |
Field::Line(u32) |
line |
decimal |
Field::DurationMs(u64) |
ms |
decimal |
Field::Count(u64) |
count |
decimal |
Field::Status(HttpStatus) |
status |
500, 502, 503, 504, other-5xx (R12.1) |
Field::Managed(bool) |
managed |
true or false
|
Field::Js(bool) |
js |
true or false
|
Field::Ok(bool) |
ok |
true or false
|
- R1.3
Codeis a closed enum.Code::as_str()gives the kebab-case name, andCode::level()gives aLevel:
| Code | Name | Level |
|---|---|---|
Startup |
startup |
Info |
Shutdown |
shutdown |
Info |
StartFailed |
start-failed |
Error |
Panic |
panic |
Error |
VerifyPassed |
verify-passed |
Info |
VerifyFailed |
verify-failed |
Warn |
RouterUp |
router-up |
Info |
RouterDown |
router-down |
Warn |
GatekeeperRefused |
gatekeeper-refused |
Info |
GatekeeperStartFailed |
gatekeeper-start-failed |
Error |
PageLoadFailed |
page-load-failed |
Warn |
WebviewCreateFailed |
webview-create-failed |
Error |
EngineCallFailed |
engine-call-failed |
Warn |
FindFailed |
find-failed |
Warn |
ZoomFailed |
zoom-failed |
Warn |
StoreCorrupt |
store-corrupt |
Warn |
ThreadFailed |
thread-failed |
Error |
ReportOpened |
report-opened |
Info |
ReportFailed |
report-failed |
Warn |
Level::as_str() is INFO, WARN or ERROR.
- R1.4
ErrorKindvalues:not-found,permission-denied,connection-refused,connection-reset,connection-aborted,broken-pipe,timed-out,addr-in-use,addr-not-available,invalid-input,invalid-data,unexpected-eof,write-zero,interrupted,unsupported,out-of-memory,parse,webview,platform,other.From<std::io::ErrorKind>,From<&std::io::Error>,From<&tauri::Error>(an I/O error maps by its kind, anything else iswebview) andFrom<&serde_json::Error>(parse) exist. Anio::ErrorKindwith no row isother. - R1.5 Every enum in R1.2 to R1.4 has
as_str(self) -> &'static str.FieldimplementsDisplayaskey=value. - R1.6
SourceFile::from_path(path: &str) -> SourceFilekeeps the part after the last/or\only when it is indiag::SOURCE_FILES; any other name becomesother.diag::SOURCE_FILES: &[&str]is the sorted list of the basenames of eepview's own.rsfiles (src-tauri/src/**andsrc-tauri/crates/*/src/**), generated bybuild.rs.SourceFile::as_str()returns the name orother. So a panic location holds a value from a closed list. - R1.7
ThreadName::of(name: Option<&str>) -> ThreadNamemapsmain,router-watch,router-checkandgatekeeperto their variant, and anything else (alsoNone) toOther. - R1.8
Record { at: u64, code: Code, fields: Vec<Field> },atin Unix seconds.diag::format_line(&Record) -> Stringgives<utc> <LEVEL> <code>and thenkey=valueper field, in order. Example:2026-10-03T12:00:00Z INFO gatekeeper-refused refuse=not-i2p. - R1.9
diag::utc(secs: u64) -> StringgivesYYYY-MM-DDTHH:MM:SSZ.utc(0)is1970-01-01T00:00:00Z.
- R2.1
Ring::new(capacity),Ring::push(Record),Ring::len(),Ring::is_empty(),Ring::last(n) -> Vec<Record>(oldest first),Ring::clear().Ring::CAPACITYis 2000. A full ring drops its oldest record. - R2.2
LogFiles::new(dir: PathBuf)usesLogFiles::MAX_BYTES(524288) andLogFiles::FILES(3).LogFiles::with_limit(dir, max_bytes)sets another size.append(&mut self, line: &str) -> io::Result<()>writes the line and\ntodir/eepview.log, and createsdirwhen it is missing. - R2.3 Rotation: when the current file is not empty and the line would make it larger than the limit,
eepview.1.logbecomeseepview.2.log(the old one is lost),eepview.logbecomeseepview.1.log, and the line starts a neweepview.log. There are never more than 3 log files.LogFiles::paths()lists the files that exist, newest first.LogFiles::delete_all()removes them all. - R2.4
diag::init(log_dir: &Path)starts writing toLogFilesin that folder. Events beforeinitstay in the ring.diag::recent(n) -> Vec<Record>reads the ring.diag::delete_logs() -> io::Result<()>empties the ring, and deletes the files and the crash marker. No file handle stays open between two lines, so the delete also works on Windows while eepview runs. - R2.6
diag::default_log_dir(identifier: &str) -> Option<PathBuf>gives the app log folder without Tauri, the same folder asapp_log_dir(): macOS$HOME/Library/Logs/<identifier>, Windows%LOCALAPPDATA%\<identifier>\logs, Linux$XDG_DATA_HOME/<identifier>/logs(or$HOME/.local/share/<identifier>/logs).run()installs the panic hook and callsinitwith it before the Tauri builder, soStartFailedand an early panic reach the disk. - R2.7
LogFiles::appendwrites each line with one unbuffered write, so a line is on disk before apanic = "abort"ends the process. - R2.5 Nothing is ever sent. The diag module opens no socket.
- R3.1
diag::install_panic_hook()sets a hook that recordsCode::Panicwithdiag::panic_fields(file, line, thread)and writes the crash marker. It never records the panic message. The hook takes the log lock withtry_lock; when this thread already holds it, or it is poisoned and held, the hook skips the record and does not block. - R3.2
diag::panic_fields(file: &str, line: u32, thread: Option<&str>) -> Vec<Field>is[Source(SourceFile::from_path(file)), Line(line), Thread(ThreadName::of(thread))]. - R3.3
diag::CRASH_MARKERiscrashed.diag::write_crash_marker(dir: &Path) -> io::Result<()>createsdir/crashed(empty).diag::take_crash_marker(dir: &Path) -> boolsays whether it existed and removes it. - R3.4
diag::inittakes the marker.diag::crashed_last_run() -> boolthen says whether it existed, untildiag::dismiss_crash(). - R3.5 On the next start, the home page shows a banner: "eepview closed unexpectedly. Report the problem?" with the buttons Report and Dismiss. Report opens
eepview://report?kind=crash. Both buttons calldiag_crash_dismiss.
- R4.1
diag::scrub(text: &str) -> Stringisdiag::scrub_with(text, user, host)with the current user name and machine host name. - R4.2
diag::scrub_with(text: &str, user: Option<&str>, host: Option<&str>) -> Stringreplaces each match below with[removed](diag::REMOVED), and leaves all other text as it is:- a URL:
scheme://and the text after it, up to white space or one of"'<>; - a name that ends in
.i2p(any case), with any port, path or query after it; - a host name: two or more dot-separated labels whose last label is 2 to 24 letters, except a file name that ends in
.rs,.txt,.log,.json,.html,.ts,.js,.css,.md,.toml,.ymlor.plist; - a run of 52 or more base32 characters (
a-z,2-7, any case); - a run of 44 or more base64 characters (
A-Z a-z 0-9 + / = - ~) with both upper-case and lower-case letters: an I2P destination (about 516) or a router hash (44); - an IPv4 address (four numbers from 0 to 255) that is not part of a longer dotted run;
- an IPv6 address, also inside
[ ]and with a zone; - an email address;
-
/Users/<name>,/home/<name>,<drive>:\Users\<name>and<drive>:/Users/<name>(also after\\?\), the drive letter and the name included; -
userandhost(case does not matter, whole words only, 3 characters or more). For a host name with dots, its first label is removed too.
- a URL:
- R4.3 Version numbers such as
0.1.0,16.0,621.1.15.10.7and10.0.26100, and times such as12:00:00and2026-10-03T12:00:00Z, stay. - R4.4 Property: for any text, an injected URL, IPv4 address, IPv6 address,
.i2pname, email address, home folder path or base64 destination, set apart by spaces, is not in the output.
- R5.1
SystemInfoholds only:version: String,commit: String,os: String,arch: &'static str,engine: String,router_kind: RouterKind,router_version: Option<String>,router_state: RouterState,managed: bool,js_default: bool,tabs: usize,uptime: UptimeBucket. - R5.2
SystemInfo::lines(&self) -> Vec<String>, in this order:eepview: <version> (<commit>)OS: <os>CPU: <arch>Web engine: <engine>-
Router: <kind> <version> (<managed|external>), state <state>(no version:Router: <kind> (<…>), state <state>) JavaScript default: <on|off>Open tabs: <n>Uptime: <bucket>
- R5.3
UptimeBucket::from_secs(s): under 60 is<1 min, under 600 is<10 min, under 3600 is<1 h, else>1 h(as_str). - R5.4
RouterKind::from_text(text) -> RouterKind:i2pd(any case) isI2pd(i2pd),javaori2p(any case) isJavaI2p(Java I2P), elseUnknown(unknown). - R5.5
sysinfo::clean_version(text) -> Option<String>: 1 to 5 dot-separated groups of digits, 32 characters at most, after a trim. Anything else isNone. - R5.6
sysinfo::macos_version(plist: &str) -> Option<String>readsProductVersionfromSystemVersion.plisttext asmacOS <v>.sysinfo::linux_os(os_release: &str) -> Option<String>givesLinux <ID> <VERSION_ID>(ID:a-z 0-9 -only, quotes removed; version:clean_version).sysinfo::windows_version(ver: &str) -> Option<String>reads[Version 10.0.26100.1234]asWindows 10.0.26100. - R5.7 There is no locale, screen, memory size or time zone.
- R6.1
ReportKind::from_param(&str):crash,blocked,router-down,load-failed, anything elseGeneral.ReportKind::title():Problem report,Crash report,Blocked page report,Router down report,Page load report(forGeneral,Crash,Blocked,RouterDown,LoadFailed). - R6.2
Report { kind: ReportKind, description: String, info: SystemInfo, log: Vec<String>, include_log: bool }.logholds report lines, oldest first, and then the counter lines of R12.3. The shell makes them withdiag::report_lines(&[Record]) -> Vec<String>(R13.1) anddiag::counter_lines()(R12.3). - R6.3
report::text(&Report) -> Stringis the preview and the file. It is scrubbed (R4), and is:
What happened:
<description, or "(not given)" when it is blank>
System:
<SystemInfo::lines, one per line>
Diagnostics log (last <n> events):
<log lines>
The log part is left out when include_log is false. report::PREVIEW_EVENTS is 200: the shell passes at most that many lines.
- R6.4
report::issue_url(&Report) -> (String, bool)gives the URL and whether the log was trimmed. The URL isreport::ISSUE_PREFIX(https://github.com/tcivie/eepview/issues/new), then?template=bug.yml&labels=bug&title=…&version=…&os=…&what-happened=…&diagnostics=…, in that order.versionis<version> (<commit>),osis<os> (<arch>),what-happenedis the description,diagnosticsis theSystem:part and the log part of R6.3. Every value is scrubbed, then encoded withreport::percent_encode. - R6.5
report::percent_encode(&str) -> StringkeepsA-Z a-z 0-9 - _ . ~and writes every other UTF-8 byte as%XX(upper-case hex). A space is%20. - R6.6 The URL is at most
report::MAX_URL(8000) characters. The oldest log lines go first. When any line goes, the linereport::TRIM_NOTE([log trimmed, the full log is in the attached file]) is the first line of the log part. When the URL is still too long without log lines, the description is cut: on the raw text, at a char boundary, beforepercent_encode, with…at the end. - R6.7
report::is_issue_url(&str) -> boolis true only forISSUE_PREFIXalone or followed by?. - R6.8
report::file_name(n: u32) -> Stringiseepview-report.txtforn0 or 1, andeepview-report (<n>).txtforn2 or more. The name holds no date (R13.2).
- R6.9
.github/ISSUE_TEMPLATE/bug.ymlhas the idsversion,os(inputs),router(dropdown, not required),what-happened(textarea, required),steps(textarea, not required) anddiagnostics(textarea,render: text, not required). So every prefilled value of R6.4 lands in a field, and no required field is left empty that the URL cannot fill.
Only the internal webview may call these (capability capabilities/report.json). See IPC contract.
| Command | Arguments | Result |
|---|---|---|
report_preview |
{ kind: string, description: string, includeLog: boolean } |
string, the text of R6.3 |
report_open |
the same |
{ file: string, trimmed: boolean }; file is the file name, no folder |
diag_crash_status |
none |
boolean, crashed_last_run()
|
diag_crash_dismiss |
none | nothing |
diag_logs_delete |
none | nothing; an error string on failure |
report_open builds the report from the last 200 events, writes text to <Downloads>/<file_name>, shows the file in the file manager, and opens issue_url in the system browser with tauri-plugin-opener. It refuses a URL for which is_issue_url is false, and a file outside the Downloads folder. It records ReportOpened or ReportFailed.
- R8.1
capabilities/report.jsonnames only theinternalwebview, and grants onlyallow-report-preview,allow-report-open,allow-diag-crash-status,allow-diag-crash-dismissandallow-diag-logs-delete. - R8.2 No capability names these five permissions for another webview.
- R8.3 No capability grants an
opener:permission. The plugin's ownreveal_item_in_dircommand has no scope, so JavaScript gets no opener command at all. The Rust command is the only caller, and it checks the URL prefix and the Downloads folder itself. The plugin starts withopen_js_links_on_click(false). - R8.4 Only
src/shell/report.rscallsopen_urlandreveal_item_in_dir. - R8.5 No HTTP client crate and no new socket. The issue opens in the user's own browser, after a click.
- R9.1
println!,eprintln!,print!,eprint!,dbg!,log::,tracing::,io::stdoutandio::stderrappear only undersrc/diag/, insrc/and incrates/*/src/. - R9.1a The one output outside
diag::eventis the leak-test trace,diag::trace. It prints to stderr only whenEEPVIEW_LOGis set, never to the log files or a report. See Leak test. - R9.2
console.appears in no.tsfile undersrc/ui/(tests included). - R9.3 No call
diag::event(…)has a string literal,format!,to_string,to_ownedorStringin its arguments. - R9.4 R8.1 to R8.4 hold.
| Place | Event |
|---|---|
| App start, app exit |
Startup, Shutdown
|
| Tauri fails to start |
StartFailed with Error
|
| A VERIFY result that changes the router state |
VerifyPassed, or VerifyFailed with Router
|
| The router state turns ok, or stops being ok |
RouterUp, RouterDown with Router
|
| The gatekeeper refuses a request | counter GatekeeperRefused with Refuse (R12.2) |
| The gatekeeper cannot start |
GatekeeperStartFailed with Error
|
| The router answers 5xx, or a forwarded request fails | counter PageLoadFailed with Status or Error (R12.2) |
| A tab webview cannot be built |
WebviewCreateFailed with Error
|
| An engine call fails |
EngineCallFailed, FindFailed or ZoomFailed, with Op and Error
|
| A store file is broken |
StoreCorrupt with Store and Error
|
| A thread cannot start, or the main thread queue fails |
ThreadFailed with Op and Error
|
| A panic |
Panic (R3) |
- R11.0
reportis innav::INTERNAL_PAGES, soeepview://reportandsrc/ui/report.htmlload in theinternalwebview. - R11.1 The page
eepview://report(src/ui/report.html) has a description box (empty, placeholder "What did you do, and what went wrong? Do not include site addresses."), a read-only preview, the checkbox "Include the diagnostics log" (on), the note "This opens GitHub in your normal web browser, outside I2P. GitHub sees your IP address and your GitHub account. Nothing is sent until you submit the issue on GitHub.", and the button "Open a GitHub issue". After a click it says "Drag this file into the GitHub issue to attach it." - R11.2
src/ui/lib/report-page.tsexports:-
reportKind(search: string): "general" | "crash" | "blocked" | "router-down" | "load-failed"; -
reportHref(kind): string, which is./report.html?kind=<kind>; -
PLACEHOLDER,BROWSER_NOTE,DRAG_NOTEandCRASH_TEXT, the texts of R11.1 and R3.5; -
openedMessage(result: { file: string; trimmed: boolean }): string, which names the file and ends withDRAG_NOTE; whentrimmed, it also says that the issue has a shorter log than the file.
-
- R11.3 The toolbar menu has "Report a problem" (
eepview://report). The blocked and router-down pages have the link "Report this problem" toreportHref("blocked")andreportHref("router-down"). The link carries only the kind, never the address. - R11.4 Settings > Privacy has the button "Delete diagnostics logs" (
id="delete-logs"). It callsdiag_logs_deleteand says "Diagnostics logs deleted."
A site can make the router answer with any status, and it can make any number of requests. So these values are a channel from the site into a public issue. They enter the log only in a closed, coarse form.
- R12.1
HttpStatusis a closed enum:S500,S502,S503,S504,Other5xx(as_str:500,502,503,504,other-5xx).HttpStatus::from_code(code: u16) -> Option<HttpStatus>isNonebelow 500 and above 599. No field holds a raw status number. - R12.2 Per-request results are counted, never recorded one by one.
diag::count(code: Code, field: Field)adds 1 (saturating atu64::MAX) to the session counter of that pair. The gatekeeper calls it for each refusal (Code::GatekeeperRefused,Field::Refuse), each router 5xx answer (Code::PageLoadFailed,Field::Status), and each failed forwarded request (Code::PageLoadFailed,Field::Error).diag::eventis never called for these. - R12.3
diag::counters() -> Vec<(Code, Field, u64)>lists the counters above 0, sorted by the code name and then the field text.diag::count_bucket(n: u64) -> &'static stris0,1+(1 to 9),10+(10 to 99),100+(100 to 999) or1000+.diag::counter_lines() -> Vec<String>gives<code> <key>=<value> count=<bucket>per counter. A report shows only the bucket, never the exact number. - R12.4
diag::delete_logsalso clears the counters.
- R13.1
diag::report_lines(&[Record]) -> Vec<String>gives<LEVEL> <code>andkey=valueper field, in record order. It holds no time and no time offset. The local log files keep the UTC second (R1.8). - R13.2 The report file in Downloads is
eepview-report.txt. When that name exists, it iseepview-report (2).txt, then(3), up to(99).report_openreturns that name.
- R14.1 On Unix, eepview creates the log folder with mode
0700, and sets0700on it when it exists. The log files, the crash marker and the report file get mode0600. - R14.2 eepview never follows a symbolic link when it writes these files. The report file is created with
create_new. The log files and the crash marker are opened withO_NOFOLLOWon Unix. A link in place of one of them makes the write fail. - R14.3
report_openchecks that the written report file lies in the Downloads folder after both paths are resolved (canonicalize). Otherwise it fails withReportFailed.
- R15.1 Before matching,
%2Eand%2F(any case) become.and/. - R15.2 A word with a dot and a non-ASCII letter is a host name (an internationalised name). The word is the run of letters, digits,
.and-, with any port or path after it. - R15.3 A run of 32 or more hex digits (
0-9 a-f A-F) that holds at least one decimal digit and at least one letter is removed. (A run of one letter, such as 51 timesa, stays, as R4.2 says.) - R15.4 Home folders:
Usersorhomebetween two separators, where each separator is/or\, and the name after it up to the next separator, line end or quote (spaces are part of the name). Also<drive>:\Documents and Settings\<name>(either separator) and a UNC path\<server>\<share>\<name>, the server, share and name included. - R15.5
rs,mdandtsare not file endings any more (they are country domains): in free text,apply.rsis removed like a host name. The file endings aretxt,log,json,html,js,css,toml,ymlandplist. - R15.6 Typed fields do not go through the free-text scrubber.
diag::scrub_report(text: &str) -> Stringisscrub, except that afile=<name>token whose name is inSOURCE_FILESstays as it is.report::textand every value ofreport::issue_urlusescrub_report. So a crash report still says where the panic happened, and the name can only be one of eepview's own source files.
- R16.1
diag::initkeeps the log folder in a place apart from the log lock. The panic hook writes the crash marker there first, and then tries the log lock (R3.1). So the marker is written even when another thread holds the log lock.
- The gatekeeper does not know which request is the page and which is a part of it. So the
PageLoadFailedcounter counts every failed request, and there is no separate "load failed" page yet. - A crash in the web engine process itself is not detected yet.
Generated from docs/wiki in the repository. Edit there, not here.