# 9. Bug Reporting System The application has a built-in, automatic bug-reporting channel that forwards **warning-and-above** log events to the PureLogicCode BugReport API. This page documents the full pipeline, the API contract, and — importantly — the noise-filtering rules. --- ## 9.1 Pipeline ``` LogMessage / LogWarning / LogError (MainWindow.axaml.cs:606–622) │ (Serilog: Information / Warning / Error) ▼ Serilog Logger (App.axaml.cs:56–78) ├── Debug sink ├── File sink → %LocalAppData%\CHDStudio\logs\CHDStudio-YYYYMMDD.log └── BugReportApiSink.Emit (Services/BugReportApiSink.cs:33) ├─ ignore events below Warning ├─ ignore messages matching exclusion patterns └─ fire-and-forget SendBugReportAsync (single in-flight send via interlocked flag) │ ▼ BugReportService.SendBugReportAsync (Services/BugReportService.cs:94) │ POST https://www.purelogiccode.com/bugreport/api/send-bug-report ▼ BugReport API (server) ``` Additionally, **unhandled exceptions** are reported directly (not via the sink): - `AppDomain.CurrentDomain.UnhandledException` → `Log.Fatal` + synchronous `ReportException` (the process is about to terminate, so the report must complete inline — `App.axaml.cs:236–250`). For dispatcher and task-scheduler exceptions the report is fire-and-forget to avoid blocking the UI thread. - `Dispatcher.UIThread.UnhandledException` → `Log.Error` + `ReportException`, then `e.Handled = true` so the application survives the exception (`CHDStudio/App.axaml.cs`). There is no framework-specific suppression allowlist: every dispatcher exception is logged and reported, and known-noise filtering happens downstream in the Serilog sink's exclusion patterns (see §9.4). - `TaskScheduler.UnobservedTaskException` → `Log.Error` + `ReportException`, then `SetObserved()`. - **Stats-rate-limit handling**: `StatsService.RecordUsageAsync` returns early on HTTP 429 (Too Many Requests) and logs at Debug level, so these transient conditions never reach the warning-level sink. ## 9.2 API Contract `SendBugReportAsync` (`BugReportService.cs:94`) POSTs JSON with header `X-API-KEY`: | Field | Source | |-------|--------| | `message` | `BuildFormattedReport` — three sections: `=== Environment Details ===` (includes both the **process** and the **OS** architecture, so a crash report from an emulated build is instantly classifiable), `=== Error Details ===` (the raw message), `=== Exception Details ===` (inner-exception chain, max depth 5) | | `applicationName` | `AppConfig.ApplicationName` = `"CHDStudio"` | | `version` | Assembly version (e.g. `3.4.0.0`) | | `userInfo` | `Environment.UserName` | | `environment` | `"Production"` (release) / `"Development"` (DEBUG build) | | `stackTrace` | Formatted exception details, or `"N/A"` | Environment Details includes: date/time, app name + version, OS version, architecture, bitness, Windows version, processor count, base directory, temp path. ## 9.3 Flood Control & Failure Semantics - Only **one** bug report is in flight at a time (`BugReportApiSink` interlocked flag); bursts of warnings are coalesced. A 12-second safety timer clears the flag even if the HTTP call hangs, preventing indefinite throttling. - **Duplicate suppression**: an identical message already forwarded within the last 10 minutes (`DuplicateWindow`) is dropped — a failing batch that retries the same input, or a loop logging the same warning per file, sends one report instead of one per occurrence. - **Unhandled exceptions are sent once**: the `Log.Fatal`/`Log.Error` events for `AppDomain.UnhandledException`, `Dispatcher.UnhandledException` and `TaskScheduler.UnobservedTaskException` are skipped by the sink (`IsDirectlyReportedByApp`) because `App` already reports them through `ReportException`; without the skip every crash landed in the tracker twice, once from the sink and once from the direct send. - Sending is fire-and-forget from the sink; failures are logged at `Debug` and never surface to the user. - Cancellation tokens are respected; `OperationCanceledException` is rethrown only when the caller's token is cancelled. ## 9.4 Exclusion Patterns `IsExcludedFromBugReport` (`BugReportService.cs:116`) performs a case-insensitive substring match against `ExcludedMessagePatterns` (`:17–91`). Any match → the report is **dropped entirely** (no HTTP call). The categories: | Category | Example patterns | |----------|------------------| | **Stats noise** | `"Failed to record usage statistics"` | | **Drive / temp-space info** | `"Temp drive ("`, `"Output drive ("`, `"drive has "`, `"drive ("`, `"input files total"`, `"CHD files total"`, `"You may run out of disk space"`, `"disk space"`, `"disk full"`, `"free on"` | | **Encoder presence / start** | `"chdman.exe not found"`, `"chdman.exe was not found"`, `"Failed to start chdman"` — the built-in CHDSharp encoder always exists, so only chdman-presence notices remain. | | **Extraction outcomes** | `"No supported primary files found in archive"`, `"Partial extraction:"`, `"File not found, skipping:"` | | **Tooling** | `"CRITICAL ERROR: The following required component"` | | **Corrupt/unopenable CHD data** | `"Not a valid CHD file"`, `"Invalid or corrupt data"`, `"Cannot open file"`, `"Failed to open '"` (CHD open/read failures during extraction) | | **chdman output (user data)** | `"Fatal error occurred"` (chdman exit summary), `"cannot create std::vector"` (chdman C++ crash on user input), `"Error during compression"`, `"Error parsing input file"`, `"failed due to an I/O error"` | | **Encoder fallback (routine)** | `"chdman failed for"`, `"Falling back to the built-in CHDSharp"` — when both encoders fail, the classified error reported afterwards still reaches the API | | **File moves (environment)** | `"Failed to move temp output to destination"`, `"Failed to move CHDSharp output to destination"` | | **Cue/dependency validation** | `"referenced files are missing"`, `"could not be resolved"`, `"the .mdf data file was not found"` (Alcohol descriptor without its data file), `"could not validate referenced files"`, `"MP3 audio track could not be decoded"`, `"is not divisible by"`, `"The file or directory is corrupted and unreadable"`, `"Retry via temp failed"` | | **Archive errors** | `"archive file may be corrupted"`, `"archive is invalid or corrupt"`, `"archive file appears to be incomplete"`, `"multi-part RAR with a missing volume"`, `"unavailable network location"`, `"Archive is encrypted"`, `"compression method that is not supported"`, `"CCDSharp: Conversion error"` | | **Transient network hiccups** | `"Direct extraction failed"`, `"will fall back to temp-copy extraction"` — a failed direct NAS extraction is demoted to `Debug` and the automatic temp-copy fallback (with retries) takes over; a genuinely broken network path is reported by the classified failure afterwards | | **Split volume sets (extracted in-app)** | `"the extracted set contained no supported disc image"`, `"The split archive may be corrupted or incomplete"`, `"the split archive cannot be extracted"` — skips of user-data origin (missing volumes, no convertible image inside, no 7za on disk) | | **Extension mismatch (skip by design)** | `"and it is not a usable disc image"`, `"this file is already a CHD"` (an existing CHD is skipped on purpose and the user is told to copy it) | | **Encoder startup incompatibility** | `"terminated abnormally during the startup check"` — a negative exit code during the startup probe (0xC0000139 entry point not found, illegal instruction, …) means the bundled chdman build does not run on the user's CPU/Windows version. The conversion-time counterpart, `"may be incompatible with this computer's CPU"`, is excluded for the same reason. | | **Output folder environment** | `"output folder is not available"` (the drive holding the destination is gone — USB unplugged, network drive dropped), `"output folder is not writable"` (root of `C:\`, `Program Files`, read-only drive — the batch-start probe already shows one actionable dialog, so nothing was converted yet) | | **Device disappears mid-conversion** | `"A device which does not exist was specified"` — chdman's Win32 error for a source/output drive that was unplugged or a network share that dropped while converting. | | **Corrupt Alcohol descriptors / password-protected MDS v2** | `"so it is corrupt or truncated"`, `"could not be read as an Alcohol descriptor"` (a corrupt descriptor), `"track data is encrypted"` (an MDS v2/MDX image whose track data needs a password the app cannot prompt for) | | **Truncated ECM data** | `"ends part way through a block"` — the decoder already tells the user to re-download the file. | **Design intent**: the exclusion list only contains messages that describe **user-data or environmental conditions** (corrupt files, full disks, missing volumes, rate limits) — conditions the application handles gracefully and that would otherwise flood the bug database. Genuine code defects (exceptions, unexpected failures) still reach the API — including **CHDSharp and PBPSharp extraction failures**, which are reported by design (with debug details such as file size, disc index/count, and numeric error codes) so the library maintainer can fix them. Two exceptions are deliberate: a CHDSharp decode failure whose automatic **chdman fallback succeeds** is not reported (the extraction worked), and a PBP that turns out to be an **incomplete/truncated download or a non-disc PBP** (`InvalidPsarHeader`/`TruncatedPsar`) is a user-data condition logged without a report. Every exclusion is covered by unit tests (`BugReportServiceTests`). ## 9.5 Server-Side Notes - **Timestamps** in reports are local time formatted `yyyy-MM-dd HH:mm:ss` inside the message body; the API's own `reportedAt` is UTC ISO-8601. - Bug reports are stored per application and can be retrieved/deleted via the agent API (`https://www.purelogiccode.com/bugreport/api/agent/...`) — see `InstructionsToRetrieveBugs.md` in the AspNet_BugReportEmailService repository. - Depending on server-side storage settings, non-ASCII characters in messages (e.g. the em dash in "— referenced files are missing") can be mangled (U+FFFD) on retrieval; messages containing them are excluded from reporting anyway.