bevy_fleet 0.1.0

bevy swarm diagnostic, event, metric, and telemetry client
Documentation
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};

type PanicFlushCallback = Arc<Mutex<Option<Box<dyn Fn() + Send + Sync>>>>;

/// Panic information
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PanicInfo {
    pub message: String,
    pub timestamp: u64,
    pub location: Option<String>,
    pub session_id: String,
}

/// Global panic collector
static PANIC_COLLECTOR: once_cell::sync::Lazy<Arc<Mutex<Vec<PanicInfo>>>> =
    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(Vec::new())));

/// Global session ID storage for panic handler
static SESSION_ID: once_cell::sync::Lazy<Arc<Mutex<String>>> =
    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(String::from("unknown"))));

/// Global panic flush callback storage
static PANIC_FLUSH_CALLBACK: once_cell::sync::Lazy<PanicFlushCallback> =
    once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));

/// Sets the session ID for panic reporting
pub fn set_session_id(session_id: String) {
    if let Ok(mut id) = SESSION_ID.lock() {
        *id = session_id;
    }
}

/// Sets a callback to flush panics immediately when they occur
pub fn set_panic_flush_callback<F>(callback: F)
where
    F: Fn() + Send + Sync + 'static,
{
    if let Ok(mut cb) = PANIC_FLUSH_CALLBACK.lock() {
        *cb = Some(Box::new(callback));
    }
}

/// Sets up the panic handler to capture panics
pub fn setup_panic_handler() {
    let default_hook = std::panic::take_hook();

    std::panic::set_hook(Box::new(move |panic_info| {
        let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
            s.to_string()
        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
            s.clone()
        } else {
            "Unknown panic".to_string()
        };

        let location = panic_info
            .location()
            .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));

        // Get the current session ID
        let session_id = SESSION_ID
            .lock()
            .ok()
            .map(|id| id.clone())
            .unwrap_or_else(|| String::from("unknown"));

        let panic_data = PanicInfo {
            message: message.clone(),
            timestamp: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs(),
            location: location.clone(),
            session_id: session_id.clone(),
        };

        // Store panic info for collection by the regular telemetry publisher
        if let Ok(mut collector) = PANIC_COLLECTOR.lock() {
            collector.push(panic_data);
        }

        eprintln!("Fleet: Panic captured - {}", message);
        eprintln!("Fleet: Session ID - {}", session_id);
        if let Some(loc) = location {
            eprintln!("Fleet: Panic location - {}", loc);
        }

        // Trigger immediate flush of panic telemetry
        if let Ok(cb_lock) = PANIC_FLUSH_CALLBACK.lock()
            && let Some(ref callback) = *cb_lock
        {
            eprintln!("Fleet: Flushing panic telemetry...");
            callback();
            #[cfg(not(target_arch = "wasm32"))]
            {
                std::thread::sleep(std::time::Duration::from_millis(500));
            }
        }

        // Call the default hook
        default_hook(panic_info);
    }));
}

/// Gets collected panic information
pub fn get_panics() -> Vec<PanicInfo> {
    PANIC_COLLECTOR
        .lock()
        .ok()
        .and_then(|mut collector| {
            if collector.is_empty() {
                None
            } else {
                Some(std::mem::take(&mut *collector))
            }
        })
        .unwrap_or_default()
}

/// Clears collected panic information
#[allow(dead_code)]
pub fn clear_panics() {
    if let Ok(mut collector) = PANIC_COLLECTOR.lock() {
        collector.clear();
    }
}