add basic crash analytics and very light telemetry to dioxus-cli (#4224)

* wip

* wip: basic telemetry

* fix merge

* keep errors for telemetry

* fix upload when queued events are empty

* add per session id and keep track of build timing

* keep track of what commands are run

* capture panics

* keep track of panics

* better path stripping

* better tracing error telemetry

* only sync every week and check settings before sending any telemetry

* keep track of the args passed to commands

* anonymize commands

* more error tolerant

* wip

* wip

* wip

* merge cnflict

* other merg conflict

* migrate everything from telemetry to logging

* update docs

* wip... about to merge trace controller

* lil bit more refactor to clean up tracer

* more performant log writer

* cleanup ci detecting

* wire up to posthog

* re-uploads work

* yoink out the anon from each file

* cleanup sig

* lil bit cleaner

* dont write empty files

* wip log handler

* fmt

* cleanup logging of backtraces

* add proper backtrace support

* use stable reporter

* adhere to posthog format

* fix stacktraces

* use anyhow for capturing backtraces

* I think we're done?

* cleanup logging

* properly collect!

* move out keycode handling

* she works

* typo

* add extra fields

* fix cargo doc

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
Jonathan KelleyandEvan Almloff authored and GitHub committed 2025-08-08 19:00:00 -07:00
1 parent a610aaefc3
commit e43bf05b69
30 files changed
+1748 -441

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ pub(crate) use update::*;
/// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly.
/// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
/// to a dynamic one on the fly.
pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Result<()> {
// Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
let exit_on_error = args.exit_on_error;
let mut builder = AppServer::new(args).await?;
+72 -23
View File
@@ -1,7 +1,9 @@
use crate::Result;
use crate::{
serve::{ansi_buffer::ansi_string_to_line, ServeUpdate, WebServer},
BuildId, BuildStage, BuilderUpdate, BundleFormat, TraceContent, TraceMsg, TraceSrc,
};
use anyhow::{anyhow, bail, Context};
use cargo_metadata::diagnostic::Diagnostic;
use crossterm::{
cursor::{Hide, Show},
@@ -104,7 +106,7 @@ impl Output {
/// Call the startup functions that might mess with the terminal settings.
/// This is meant to be paired with "shutdown" to restore the terminal to its original state.
fn startup(&mut self) -> io::Result<()> {
fn startup(&mut self) -> Result<()> {
if self.interactive {
// Check if writing the terminal is going to block infinitely.
// If it does, we should disable interactive mode. This ensures we work with programs like `bg`
@@ -138,7 +140,7 @@ impl Output {
///
/// This lets us check if writing to tty is going to block forever and then recover, allowing
/// interopability with programs like `bg`.
fn enable_raw_mode() -> io::Result<()> {
fn enable_raw_mode() -> Result<()> {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
@@ -152,7 +154,7 @@ impl Output {
use std::io::IsTerminal;
if !stdout().is_terminal() {
return io::Result::Err(io::Error::other("Not a terminal"));
bail!("Cannot enable raw mode on a non-terminal output");
}
enable_raw_mode()?;
@@ -164,7 +166,7 @@ impl Output {
Ok(())
}
pub(crate) fn remote_shutdown(interactive: bool) -> io::Result<()> {
pub(crate) fn remote_shutdown(interactive: bool) -> Result<()> {
if interactive && crossterm::terminal::is_raw_mode_enabled().unwrap_or(true) {
stdout()
.execute(Show)?
@@ -213,23 +215,42 @@ impl Output {
}
/// Handle an input event, returning `true` if the event should cause the program to restart.
fn handle_input(&mut self, input: Event) -> io::Result<Option<ServeUpdate>> {
// handle ctrlc
if let Event::Key(key) = input {
if let KeyCode::Char('c') = key.code {
if key.modifiers.contains(KeyModifiers::CONTROL) {
return Ok(Some(ServeUpdate::Exit { error: None }));
}
}
}
fn handle_input(&mut self, input: Event) -> Result<Option<ServeUpdate>> {
match input {
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_keypress(key),
_ => Ok(Some(ServeUpdate::Redraw)),
}
}
fn handle_keypress(&mut self, key: KeyEvent) -> io::Result<Option<ServeUpdate>> {
fn handle_keypress(&mut self, key: KeyEvent) -> Result<Option<ServeUpdate>> {
// Some dev helpers for testing panic propagation and error handling. Remove this eventually.
if cfg!(debug_assertions) && std::env::var("DEBUG_PANICS").is_ok() {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(Some(ServeUpdate::Exit { error: None }))
}
KeyCode::Char('Z') => panic!("z pressed so we panic -> {}", uuid::Uuid::new_v4()),
KeyCode::Char('X') => bail!("x pressed so we bail -> {}", uuid::Uuid::new_v4()),
KeyCode::Char('E') => {
Err(anyhow!(
"E pressed so we bail with context -> {}",
uuid::Uuid::new_v4()
))
.context("With a message")
.context("With a context")?;
}
KeyCode::Char('C') => {
return Ok(Some(ServeUpdate::Exit {
error: Some(anyhow!(
"C pressed, so exiting with safe error -> {}",
uuid::Uuid::new_v4()
)),
}));
}
_ => {}
}
}
match key.code {
KeyCode::Char('r') => return Ok(Some(ServeUpdate::RequestRebuild)),
KeyCode::Char('o') => return Ok(Some(ServeUpdate::OpenApp)),
@@ -255,7 +276,6 @@ impl Output {
id: BuildId::CLIENT,
}));
}
KeyCode::Char('c') => {
stdout()
.execute(Clear(ClearType::All))?
@@ -394,7 +414,9 @@ impl Output {
};
// First, dequeue any logs that have built up from event handling
_ = self.drain_logs(term);
while let Some(log) = self.pending_logs.pop_back() {
_ = self.render_log(term, log);
}
// Then, draw the frame, passing along all the state of the TUI so we can render it properly
_ = term.draw(|frame| {
@@ -844,16 +866,13 @@ impl Output {
/// TODO(jon): we could look into implementing scroll regions ourselves, but I think insert_before will
/// land in a reasonable amount of time.
#[deny(clippy::manual_saturating_arithmetic)]
fn drain_logs(
fn render_log(
&mut self,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> io::Result<()> {
log: TraceMsg,
) -> Result<()> {
use unicode_segmentation::UnicodeSegmentation;
let Some(log) = self.pending_logs.pop_back() else {
return Ok(());
};
// Only show debug logs if verbose is enabled
if log.level == Level::DEBUG && !self.verbose {
return Ok(());
@@ -1029,3 +1048,33 @@ impl Output {
lines
}
}
impl std::ops::Drop for Output {
fn drop(&mut self) {
if !self.interactive {
return;
}
// Get a handle to the terminal with a different lifetime so we can continue to call &self methods
let owned_term = self.term.clone();
let mut term = owned_term.borrow_mut();
let Some(term) = term.as_mut() else {
return;
};
// First, dequeue any logs that have built up from event handling
while let Some(log) = self.pending_logs.pop_back() {
_ = self.render_log(term, log);
}
// And then lets move the cursor to the bottom of the terminal
let frame_rect = term.get_frame().area();
_ = term.set_cursor_position(Position {
x: 0,
y: frame_rect.y + frame_rect.height,
});
// Tear down the TUI if it's active at this point.
_ = crate::serve::Output::remote_shutdown(self.interactive);
}
}
+3 -3
View File
@@ -143,15 +143,15 @@ pub(crate) fn proxy_to(
|| uri.path().starts_with("/public/")
|| uri.path().starts_with("/wasm/")
{
tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
tracing::trace!(dx_src = ?TraceSrc::App(crate::BundleFormat::Server), "[{}] {}", res.status().as_u16(), uri);
} else {
tracing::info!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
tracing::info!(dx_src = ?TraceSrc::App(crate::BundleFormat::Server), "[{}] {}", res.status().as_u16(), uri);
}
Ok(res.into_response())
}
Err(err) => {
tracing::error!(dx_src = ?TraceSrc::Dev, "[{}] {}", err.status().as_u16(), uri);
tracing::error!(dx_src = ?TraceSrc::App(crate::BundleFormat::Server), "[{}] {}", err.status().as_u16(), uri);
Err(err)
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ impl AppServer {
let open_browser = args
.open
.unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or(true))
.unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or(false))
&& interactive;
let wsl_file_poll_interval = args