debugbundle 1.0.0-alpha.1

Safe, bounded runtime error reporting for Rust, Axum and Actix Web
Documentation
mod helpers;
use debugbundle::{Client, Context, ExceptionSnapshot, ProbeOptions};
use helpers::{configured, Recorder};
use serde_json::json;
use std::{
    sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    },
    time::Duration,
};
#[test]
fn panic_observation_is_qualified_in_an_isolated_process() {
    let output = std::process::Command::new(std::env::current_exe().unwrap())
        .args(["--exact", "panic_observer_child", "--nocapture"])
        .env("DEBUGBUNDLE_PANIC_TEST_CHILD", "1")
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}
#[test]
fn panic_observer_child() {
    if std::env::var("DEBUGBUNDLE_PANIC_TEST_CHILD").as_deref() != Ok("1") {
        return;
    }
    let previous = Arc::new(AtomicUsize::new(0));
    let observed = previous.clone();
    std::panic::set_hook(Box::new(move |_| {
        observed.fetch_add(1, Ordering::SeqCst);
    }));
    let recorder = Arc::new(Recorder::default());
    let hooks = Arc::new(AtomicUsize::new(0));
    let observed = hooks.clone();
    let mut config = configured(recorder.clone());
    config.evidence_capabilities = Some(json!({"version":1,"features":["exception_evidence_v1"]}));
    config.before_send = Some(Arc::new(move |event| {
        observed.fetch_add(1, Ordering::SeqCst);
        Some(event)
    }));
    let client = Client::new(config);
    let registration = client.install_panic_observer();
    assert!(registration.is_active());
    assert!(!client.install_panic_observer().is_active());
    let mut retired = Vec::new();
    for _ in 0..8 {
        let deadline = std::time::Instant::now() + Duration::from_secs(2);
        let temporary = loop {
            let candidate = Client::new(configured(Arc::new(Recorder::default())));
            if candidate.status().state == debugbundle::ConnectionStatus::Healthy {
                break candidate;
            }
            assert!(std::time::Instant::now() < deadline);
            std::thread::sleep(Duration::from_millis(1));
        };
        let guard = temporary.install_panic_observer();
        assert!(
            guard.is_active(),
            "stopped client retained observer capacity"
        );
        temporary.shutdown(Duration::ZERO);
        drop(temporary);
        retired.push(guard);
    }
    client.probe(
        "retained",
        &json!(true),
        ProbeOptions::default(),
        &Context::default(),
    );
    assert!(std::panic::catch_unwind(|| panic!("password=SYNTHETIC_SECRET")).is_err());
    assert!(client.flush(Duration::from_secs(2)).drained);
    assert_eq!(hooks.load(Ordering::SeqCst), 0);
    assert_eq!(previous.load(Ordering::SeqCst), 1);
    {
        let events = recorder.events.lock().unwrap();
        assert_eq!(events.len(), 1);
        let event = &events[0];
        assert_eq!(event["payload"]["handled"], false);
        assert_eq!(
            event["payload"]["exception_evidence"]["nodes"][0]["trace_origin"],
            "unavailable"
        );
        assert!(event["context"]["panic"]["location"]["line"]
            .as_u64()
            .is_some());
        assert!(event["payload"].get("probe_data").is_none());
        assert!(!serde_json::to_string(event)
            .unwrap()
            .contains("SYNTHETIC_SECRET"));
    }
    struct Opaque(Arc<AtomicUsize>);
    impl Drop for Opaque {
        fn drop(&mut self) {
            self.0.fetch_add(1, Ordering::SeqCst);
        }
    }
    let drops = Arc::new(AtomicUsize::new(0));
    let value = drops.clone();
    let payload = std::panic::catch_unwind(move || std::panic::panic_any(Opaque(value)));
    assert_eq!(drops.load(Ordering::SeqCst), 0);
    assert!(client.flush(Duration::from_secs(2)).drained);
    drop(payload);
    assert_eq!(drops.load(Ordering::SeqCst), 1);
    assert_eq!(previous.load(Ordering::SeqCst), 2);
    client.capture_exception(
        &ExceptionSnapshot::new("Ordinary", "failure"),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(2)).drained);
    assert_eq!(hooks.load(Ordering::SeqCst), 1);
    assert_eq!(recorder.events.lock().unwrap().len(), 3);
    let replacement = Arc::new(AtomicUsize::new(0));
    let observed = replacement.clone();
    std::panic::set_hook(Box::new(move |_| {
        observed.fetch_add(1, Ordering::SeqCst);
    }));
    drop(registration);
    assert!(std::panic::catch_unwind(|| panic!("after detach")).is_err());
    assert_eq!(replacement.load(Ordering::SeqCst), 1);
    assert_eq!(previous.load(Ordering::SeqCst), 2);
    assert!(client.flush(Duration::from_secs(1)).drained);
    assert_eq!(recorder.events.lock().unwrap().len(), 3);
}