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
@@ -337,6 +337,8 @@ fn diagnostic_report(
format!("Space: {}", snapshot.active_space_name),
format!("Profile: {}", snapshot.active_profile_name),
format!("Profile kind: {}", profile_kind_label(&snapshot.active_profile_kind)),
format!("Diagnostics reporting: {}", snapshot.diagnostics_reporting_policy.status()),
format!("Local diagnostics: {}", snapshot.diagnostic_events.len()),
format!("URL scope: {}", diagnostic_url_scope(active_tab)),
format!("Tab title: {}", active_tab.title()),
format!("Tab state: {}", tab_state_label(active_tab.state())),
@@ -395,9 +397,10 @@ fn tab_state_label(state: &TabState) -> &'static str {
#[cfg(test)]
mod tests {
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{ProfileId, SpaceId, TabId, UrlText};
use super::{BrowserTab, diagnostic_url_scope};
use super::{BrowserTab, active_tab, diagnostic_report, diagnostic_url_scope, origin_for_tab};
#[test]
fn diagnostic_url_scope_omits_path_and_query() -> Result<(), Box<dyn std::error::Error>> {
@@ -426,4 +429,19 @@ mod tests {
assert_eq!(diagnostic_url_scope(&tab), "ely://settings/advanced");
Ok(())
}
#[test]
fn diagnostic_report_includes_privacy_and_local_event_count()
-> Result<(), Box<dyn std::error::Error>> {
let core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let snapshot = core.snapshot()?;
let active_tab = active_tab(&snapshot)
.ok_or_else(|| std::io::Error::other("default browser starts with an active tab"))?;
let origin = origin_for_tab(active_tab);
let report = diagnostic_report(&snapshot, active_tab, origin.as_ref());
assert!(report.contains("Diagnostics reporting: Diagnostics reporting is on"));
assert!(report.contains("Local diagnostics: 1"));
Ok(())
}
}
+11 -27
View File
@@ -2,10 +2,11 @@ use std::{collections::BTreeMap, time::SystemTime};
use ely_domain::{
ArchivePolicy, ArchivedTab, BookmarkEntry, BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX,
DiagnosticsReportingPolicy, DomainError, DownloadEntry, DownloadPolicy, FavoriteLimit,
HistoryEntry, HistoryRecordingPolicy, NewTabDestination, NoteEntry, Profile, ProfileId,
ProfileKind, ReadingListEntry, SearchEngine, SitePermissionAuditEvent, SitePermissionEntry,
Space, SpaceId, SplitLayout, SyncStatus, TabGroup, TabId, UpdatePolicy, UrlText,
DiagnosticEvent, DiagnosticsReportingPolicy, DomainError, DownloadEntry, DownloadPolicy,
FavoriteLimit, HistoryEntry, HistoryRecordingPolicy, NewTabDestination, NoteEntry, Profile,
ProfileId, ProfileKind, ReadingListEntry, SearchEngine, SitePermissionAuditEvent,
SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, TabGroup, TabId, UpdatePolicy,
UrlText,
};
use crate::{CoreError, navigation::tab_title};
@@ -13,10 +14,12 @@ use sync::SyncObjectPolicies;
mod bookmarks;
mod commands;
mod diagnostics;
mod downloads;
mod history;
mod notes;
mod plugins;
mod privacy;
mod profiles;
mod reading_list;
mod site_data;
@@ -98,6 +101,7 @@ pub struct BrowserSnapshot {
pub split_layouts: Vec<SplitLayout>,
pub installed_plugins: Vec<InstalledPlugin>,
pub plugin_audit_events: Vec<PluginAuditEvent>,
pub diagnostic_events: Vec<DiagnosticEvent>,
pub spaces: Vec<Space>,
pub trashed_spaces: Vec<TrashedSpace>,
pub profiles: Vec<Profile>,
@@ -137,6 +141,7 @@ pub struct BrowserCore {
trashed_spaces: Vec<TrashedSpace>,
installed_plugins: Vec<InstalledPlugin>,
plugin_audit_events: Vec<PluginAuditEvent>,
diagnostic_events: Vec<DiagnosticEvent>,
active_space_id: SpaceId,
active_profile_id: ProfileId,
active_tab_id: TabId,
@@ -213,6 +218,7 @@ impl BrowserCore {
trashed_spaces: Vec::new(),
installed_plugins: Vec::new(),
plugin_audit_events: Vec::new(),
diagnostic_events: vec![DiagnosticEvent::startup_success(SystemTime::now())],
command_query: String::new(),
})
}
@@ -324,29 +330,6 @@ impl BrowserCore {
self.new_tab_destination
}
pub fn set_history_recording_policy(&mut self, policy: HistoryRecordingPolicy) {
self.history_recording_policy = policy;
}
pub fn set_diagnostics_reporting_policy(&mut self, policy: DiagnosticsReportingPolicy) {
self.diagnostics_reporting_policy = policy;
}
pub fn reset_privacy_settings(&mut self) {
self.set_history_recording_policy(HistoryRecordingPolicy::default());
self.set_diagnostics_reporting_policy(DiagnosticsReportingPolicy::default());
}
#[must_use]
pub fn history_recording_policy(&self) -> HistoryRecordingPolicy {
self.history_recording_policy
}
#[must_use]
pub fn diagnostics_reporting_policy(&self) -> DiagnosticsReportingPolicy {
self.diagnostics_reporting_policy
}
pub fn set_favorite_limit(&mut self, favorite_limit: FavoriteLimit) {
self.favorite_limit = favorite_limit;
}
@@ -406,6 +389,7 @@ impl BrowserCore {
split_layouts: self.visible_split_layouts(),
installed_plugins: self.installed_plugins.clone(),
plugin_audit_events: self.plugin_audit_events.clone(),
diagnostic_events: self.diagnostic_events.clone(),
spaces: self.sorted_spaces(),
trashed_spaces: self.trashed_spaces.clone(),
profiles: self.profiles.clone(),
@@ -0,0 +1,16 @@
use std::time::SystemTime;
use ely_domain::{DiagnosticEvent, DiagnosticEventKind};
use super::BrowserCore;
impl BrowserCore {
pub fn record_diagnostic_event(&mut self, kind: DiagnosticEventKind) {
self.diagnostic_events.push(DiagnosticEvent::new(kind, SystemTime::now()));
}
#[must_use]
pub fn diagnostic_events(&self) -> &[DiagnosticEvent] {
&self.diagnostic_events
}
}
@@ -0,0 +1,28 @@
use ely_domain::{DiagnosticsReportingPolicy, HistoryRecordingPolicy};
use super::BrowserCore;
impl BrowserCore {
pub fn set_history_recording_policy(&mut self, policy: HistoryRecordingPolicy) {
self.history_recording_policy = policy;
}
pub fn set_diagnostics_reporting_policy(&mut self, policy: DiagnosticsReportingPolicy) {
self.diagnostics_reporting_policy = policy;
}
pub fn reset_privacy_settings(&mut self) {
self.set_history_recording_policy(HistoryRecordingPolicy::default());
self.set_diagnostics_reporting_policy(DiagnosticsReportingPolicy::default());
}
#[must_use]
pub fn history_recording_policy(&self) -> HistoryRecordingPolicy {
self.history_recording_policy
}
#[must_use]
pub fn diagnostics_reporting_policy(&self) -> DiagnosticsReportingPolicy {
self.diagnostics_reporting_policy
}
}
@@ -1,6 +1,6 @@
use std::time::SystemTime;
use ely_domain::TabId;
use ely_domain::{DiagnosticEventKind, TabId, WebViewCrashKind};
use super::BrowserCore;
use crate::CoreError;
@@ -18,6 +18,9 @@ impl BrowserCore {
.find(|tab| tab.id() == tab_id)
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
tab.mark_crashed();
self.record_diagnostic_event(DiagnosticEventKind::WebViewCrash {
crash_kind: WebViewCrashKind::TabCrashed,
});
Ok(tab_id.clone())
}
@@ -1,7 +1,7 @@
use std::error::Error;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{CommandIntent, TabState, UrlText};
use ely_domain::{CommandIntent, DiagnosticEventKind, TabState, UrlText, WebViewCrashKind};
#[test]
fn crash_active_tab_preserves_tab_metadata() -> Result<(), Box<dyn Error>> {
@@ -18,6 +18,23 @@ fn crash_active_tab_preserves_tab_metadata() -> Result<(), Box<dyn Error>> {
assert_eq!(active_tab.url().as_str(), "https://example.com/form");
assert_eq!(active_tab.title(), title);
assert_eq!(active_tab.favicon_key(), Some("favicons/example.ico"));
assert_eq!(
core.diagnostic_events().last().map(ely_domain::DiagnosticEvent::kind),
Some(&DiagnosticEventKind::WebViewCrash { crash_kind: WebViewCrashKind::TabCrashed })
);
Ok(())
}
#[test]
fn new_browser_core_records_startup_success_diagnostic() -> Result<(), Box<dyn Error>> {
let core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let events = core.diagnostic_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0].kind(),
DiagnosticEventKind::AppStartup { outcome: ely_domain::DiagnosticOutcome::Success }
));
Ok(())
}
+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,