Record local diagnostics events

This commit is contained in:
2026-05-09 06:12:05 -04:00
parent c86091a81f
commit a8d2688020
9 changed files with 252 additions and 30 deletions
+149
View File
@@ -0,0 +1,149 @@
use std::time::SystemTime;
use crate::{DomainError, PluginId};
const DIAGNOSTIC_CODE_MAX_LEN: usize = 80;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticOutcome {
Success,
Failure,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebViewCrashKind {
TabCrashed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiagnosticCode(String);
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DiagnosticEventKind {
AppStartup { outcome: DiagnosticOutcome },
AppCrash,
WebViewCrash { crash_kind: WebViewCrashKind },
SyncError { error_code: DiagnosticCode },
PluginCrash { plugin_id: PluginId },
UpdateResult { outcome: DiagnosticOutcome },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiagnosticEvent {
kind: DiagnosticEventKind,
occurred_at: SystemTime,
}
impl DiagnosticOutcome {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Failure => "failure",
}
}
}
impl WebViewCrashKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::TabCrashed => "tab_crashed",
}
}
}
impl DiagnosticCode {
pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
let value = value.into();
let trimmed = value.trim();
if trimmed.is_empty()
|| trimmed.len() > DIAGNOSTIC_CODE_MAX_LEN
|| !trimmed.chars().all(is_diagnostic_code_character)
{
return Err(DomainError::InvalidDiagnosticCode { value });
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl DiagnosticEventKind {
#[must_use]
pub fn event_type(&self) -> &'static str {
match self {
Self::AppStartup { .. } => "app_startup",
Self::AppCrash => "app_crash",
Self::WebViewCrash { .. } => "webview_crash",
Self::SyncError { .. } => "sync_error",
Self::PluginCrash { .. } => "plugin_crash",
Self::UpdateResult { .. } => "update_result",
}
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::AppStartup { .. } => "App startup",
Self::AppCrash => "App crash",
Self::WebViewCrash { .. } => "WebView crash",
Self::SyncError { .. } => "Sync error",
Self::PluginCrash { .. } => "Plugin crash",
Self::UpdateResult { .. } => "Update result",
}
}
}
impl DiagnosticEvent {
#[must_use]
pub fn new(kind: DiagnosticEventKind, occurred_at: SystemTime) -> Self {
Self { kind, occurred_at }
}
#[must_use]
pub fn startup_success(occurred_at: SystemTime) -> Self {
Self::new(
DiagnosticEventKind::AppStartup { outcome: DiagnosticOutcome::Success },
occurred_at,
)
}
#[must_use]
pub fn kind(&self) -> &DiagnosticEventKind {
&self.kind
}
#[must_use]
pub fn occurred_at(&self) -> SystemTime {
self.occurred_at
}
}
fn is_diagnostic_code_character(character: char) -> bool {
character.is_ascii_alphanumeric() || matches!(character, '_' | '.' | ':' | '-')
}
#[cfg(test)]
mod tests {
use super::DiagnosticCode;
#[test]
fn diagnostic_code_accepts_machine_codes() -> Result<(), Box<dyn std::error::Error>> {
let code = DiagnosticCode::parse("sync.pull.5xx")?;
assert_eq!(code.as_str(), "sync.pull.5xx");
Ok(())
}
#[test]
fn diagnostic_code_rejects_url_like_values() {
let result = DiagnosticCode::parse("https://example.com/private?q=token");
assert!(result.is_err());
}
}
+3
View File
@@ -79,4 +79,7 @@ pub enum DomainError {
#[error("invalid plugin signature: {value}")]
InvalidPluginSignature { value: String },
#[error("invalid diagnostic code: {value}")]
InvalidDiagnosticCode { value: String },
}
+4
View File
@@ -1,6 +1,7 @@
mod archive;
mod bookmark;
mod command;
mod diagnostics;
mod download;
mod error;
mod favorite;
@@ -25,6 +26,9 @@ mod url_text;
pub use archive::{ArchiveSource, ArchivedTab};
pub use bookmark::BookmarkEntry;
pub use command::{CommandIntent, CommandScope};
pub use diagnostics::{
DiagnosticCode, DiagnosticEvent, DiagnosticEventKind, DiagnosticOutcome, WebViewCrashKind,
};
pub use download::{
DownloadChecksum, DownloadChecksumAlgorithm, DownloadDestination, DownloadEntry,
DownloadPolicy, DownloadSecurity, DownloadState,