debugbundle 1.0.0-alpha.1

Safe, bounded runtime error reporting for Rust, Axum and Actix Web
Documentation
mod helpers;
use debugbundle::{Client, Config, Context, ExceptionSnapshot, ProbeOptions};
use helpers::{configured, Recorder};
use serde_json::{json, Value};
use std::{
    sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    },
    time::Duration,
};

#[test]
fn always_on_probes_wrap_values_and_clear_only_with_an_admitted_error() {
    let recorder = Arc::new(Recorder::default());
    let client = Client::new(configured(recorder.clone()));
    client.probe(
        "cart.total",
        &json!(42),
        ProbeOptions::default(),
        &Context::default(),
    );
    client.probe(
        "cart.auth",
        &json!({"password":"SYNTHETIC_SECRET"}),
        ProbeOptions::default(),
        &Context::default(),
    );
    client.capture_exception(
        &ExceptionSnapshot::new("First", "failure"),
        &Context::default(),
    );
    client.capture_exception(
        &ExceptionSnapshot::new("Second", "failure"),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(2)).drained);
    let events = recorder.events.lock().unwrap();
    let items = events[0]["payload"]["probe_data"]["items"]
        .as_array()
        .unwrap();
    assert_eq!(items.len(), 2);
    assert!(items.iter().any(|i| i["data"] == json!({"value":42})));
    assert!(!serde_json::to_string(items)
        .unwrap()
        .contains("SYNTHETIC_SECRET"));
    assert!(events[1]["payload"].get("probe_data").is_none());
}

#[test]
fn lazy_callbacks_run_off_capture_once_and_heavy_callbacks_stay_dormant() {
    let recorder = Arc::new(Recorder::default());
    let client = Client::new(configured(recorder.clone()));
    let calls = Arc::new(AtomicUsize::new(0));
    let observed = calls.clone();
    let producer = Arc::new(move || {
        observed.fetch_add(1, Ordering::SeqCst);
        json!({"answer":42})
    });
    client.probe_lazy(
        "heavy",
        &producer,
        ProbeOptions { heavy: true },
        &Context::default(),
    );
    assert_eq!(calls.load(Ordering::SeqCst), 0);
    client.probe_lazy(
        "normal",
        &producer,
        ProbeOptions::default(),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(2)).drained);
    assert_eq!(calls.load(Ordering::SeqCst), 1);
    client.capture_exception(
        &ExceptionSnapshot::new("Error", "failure"),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(2)).drained);
    assert_eq!(
        recorder.events.lock().unwrap()[0]["payload"]["probe_data"]["items"][0]["data"],
        json!({"answer":42})
    );
}

#[test]
fn disabled_or_full_probe_labels_invoke_no_producer() {
    let recorder = Arc::new(Recorder::default());
    let mut config = configured(recorder);
    config.max_probe_labels = 1;
    let client = Client::new(config);
    client.probe(
        "occupied",
        &Value::Null,
        ProbeOptions::default(),
        &Context::default(),
    );
    let calls = Arc::new(AtomicUsize::new(0));
    let observed = calls.clone();
    let producer = Arc::new(move || {
        observed.fetch_add(1, Ordering::SeqCst);
        Value::Null
    });
    client.probe_lazy(
        "new",
        &producer,
        ProbeOptions::default(),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(1)).drained);
    assert_eq!(calls.load(Ordering::SeqCst), 0);
    let disabled = Client::new(Config {
        enabled: false,
        ..Config::default()
    });
    disabled.probe_lazy(
        "ignored",
        &producer,
        ProbeOptions::default(),
        &Context::default(),
    );
    assert_eq!(calls.load(Ordering::SeqCst), 0);
}

#[test]
fn probe_flush_option_and_stale_operation_ownership_are_respected() {
    let recorder = Arc::new(Recorder::default());
    let mut config = configured(recorder.clone());
    config.probe_flush_on_error = false;
    let client = Client::new(config);
    client.probe(
        "retained",
        &json!(true),
        ProbeOptions::default(),
        &Context::default(),
    );
    client.capture_exception(
        &ExceptionSnapshot::new("Error", "failure"),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(2)).drained);
    assert!(recorder.events.lock().unwrap()[0]["payload"]
        .get("probe_data")
        .is_none());
    let operation = client.start_operation(debugbundle::OperationOptions::default());
    let stale = operation.context();
    drop(operation);
    let calls = Arc::new(AtomicUsize::new(0));
    let observed = calls.clone();
    let producer = Arc::new(move || {
        observed.fetch_add(1, Ordering::SeqCst);
        json!(true)
    });
    client.probe_lazy("stale", &producer, ProbeOptions::default(), &stale);
    assert!(client.flush(Duration::from_secs(1)).drained);
    assert_eq!(calls.load(Ordering::SeqCst), 0);
}

#[test]
fn full_protocol_probe_array_is_not_mistaken_for_an_untrusted_application_container() {
    let recorder = Arc::new(Recorder::default());
    let client = Client::new(configured(recorder.clone()));
    for label in 0..50 {
        for _ in 0..10 {
            client.probe(
                &format!("label.{label}"),
                &json!(1),
                ProbeOptions::default(),
                &Context::default(),
            );
        }
    }
    client.capture_exception(
        &ExceptionSnapshot::new("Error", "failure"),
        &Context::default(),
    );
    assert!(client.flush(Duration::from_secs(3)).drained);
    let events = recorder.events.lock().unwrap();
    assert_eq!(events.len(), 1);
    assert_eq!(
        events[0]["payload"]["probe_data"]["items"]
            .as_array()
            .unwrap()
            .len(),
        500
    );
}