debugbundle 1.0.0-alpha.1

Safe, bounded runtime error reporting for Rust, Axum and Actix Web
Documentation
mod helpers;
use debugbundle::{
    delivery::{Transport, TransportFailure, TransportResponse},
    Client, Config, ConnectionStatus, Context, Level,
};
use helpers::{configured, Recorder};
use serde_json::json;
use std::{
    sync::{mpsc, Arc, Condvar, Mutex},
    time::{Duration, Instant},
};

#[test]
fn process_client_capacity_is_bounded_and_released_after_drop() {
    let clients = (0..8)
        .map(|_| Client::new(configured(Arc::new(Recorder::default()))))
        .collect::<Vec<_>>();
    assert!(clients
        .iter()
        .all(|client| client.status().state == ConnectionStatus::Healthy));
    let rejected = std::panic::catch_unwind(|| {
        Client::new(Config {
            transport: Some(Arc::new(PanickingConfigurationDrop)),
            ..Config::new("synthetic", "service")
        })
    });
    assert!(
        rejected.is_ok(),
        "client-limit rejection unwound into the application"
    );
    assert_eq!(rejected.unwrap().status().reason, Some("client_limit"));
    drop(clients);
    let deadline = Instant::now() + Duration::from_secs(2);
    loop {
        let client = Client::new(configured(Arc::new(Recorder::default())));
        if client.status().state == ConnectionStatus::Healthy {
            break;
        }
        assert!(Instant::now() < deadline);
        std::thread::sleep(Duration::from_millis(1));
    }
}
struct BlockingDrop {
    gate: Arc<(Mutex<bool>, Condvar)>,
    entered: mpsc::Sender<()>,
}
impl Drop for BlockingDrop {
    fn drop(&mut self) {
        let _ = self.entered.send(());
        let mut released = self.gate.0.lock().unwrap();
        while !*released {
            let (result, timeout) = self
                .gate
                .1
                .wait_timeout(released, Duration::from_secs(2))
                .unwrap();
            released = result;
            if timeout.timed_out() {
                break;
            }
        }
    }
}
impl Transport for BlockingDrop {
    fn send(&self, _: &[u8], _: Duration) -> Result<TransportResponse, TransportFailure> {
        Ok(TransportResponse {
            status: 202,
            body: Some(json!({"accepted":1,"rejected":0,"errors":[]})),
            retry_after: None,
        })
    }
}
#[test]
fn application_configuration_destructors_do_not_stall_the_shared_sdk_runtime() {
    let gate = Arc::new((Mutex::new(false), Condvar::new()));
    let (tx, rx) = mpsc::channel();
    let client = Client::new(Config {
        transport: Some(Arc::new(BlockingDrop {
            gate: gate.clone(),
            entered: tx,
        })),
        ..Config::new("synthetic", "service")
    });
    let drop_start = Instant::now();
    drop(client);
    let drop_elapsed = drop_start.elapsed();
    rx.recv_timeout(Duration::from_secs(2)).unwrap();
    let start = Instant::now();
    let recorder = Arc::new(Recorder::default());
    let independent = Client::new(configured(recorder.clone()));
    independent.capture_log(Level::Error, "independent", &Context::default());
    let outcome = independent.flush(Duration::from_millis(500));
    let elapsed = start.elapsed();
    *gate.0.lock().unwrap() = true;
    gate.1.notify_all();
    assert!(
        drop_elapsed < Duration::from_millis(500),
        "client drop ran an application destructor on its caller"
    );
    assert!(
        outcome.drained && elapsed < Duration::from_secs(1),
        "another client's destructor blocked SDK delivery"
    );
    assert_eq!(recorder.events.lock().unwrap().len(), 1);
}

struct PanickingConfigurationDrop;
impl Drop for PanickingConfigurationDrop {
    fn drop(&mut self) {
        panic!("synthetic configuration destructor");
    }
}
impl Transport for PanickingConfigurationDrop {
    fn send(&self, _: &[u8], _: Duration) -> Result<TransportResponse, TransportFailure> {
        unreachable!("rejected configuration must not send")
    }
}

#[test]
fn rejected_configuration_destructor_panics_cannot_escape_client_initialization() {
    for (enabled, service, expected) in [
        (false, "service", "disabled"),
        (true, "", "invalid_configuration"),
    ] {
        let result = std::panic::catch_unwind(|| {
            let captured = PanickingConfigurationDrop;
            Client::new(Config {
                enabled,
                before_send: Some(Arc::new(move |event| {
                    let _ = &captured;
                    Some(event)
                })),
                transport: Some(Arc::new(PanickingConfigurationDrop)),
                ..Config::new("synthetic", service)
            })
        });
        assert!(
            result.is_ok(),
            "configuration rejection unwound into the application"
        );
        assert_eq!(result.unwrap().status().reason, Some(expected));
    }
}

#[test]
fn rejected_configuration_cleanup_restores_application_capture_state() {
    let _ = Client::new(Config {
        enabled: false,
        ..Config::default()
    });
    let recorder = Arc::new(Recorder::default());
    let client = Client::new(configured(recorder.clone()));
    client.capture_log(
        Level::Error,
        "after rejected configuration",
        &Context::default(),
    );
    assert_eq!(client.flush(Duration::from_secs(1)).accepted_events, 1);
    assert_eq!(recorder.events.lock().unwrap().len(), 1);
}