debugbundle 1.0.0-alpha.1

Safe, bounded runtime error reporting for Rust, Axum and Actix Web
Documentation
#![cfg(all(feature = "local", unix))]
use debugbundle::{storage::SecureDirectory, wire::valid_event};
use serde_json::{json, Value};
use std::os::unix::fs::{symlink, PermissionsExt};
fn event() -> Value {
    json!({"schema_version":"2026-03-01","event_id":uuid::Uuid::new_v4().to_string(),"event_type":"log_event","sdk_name":"@debugbundle/sdk-rust","sdk_version":"1.0.0-alpha.1","service":{"name":"checkout","environment":"test","runtime":"rust"},"occurred_at":"2026-11-12T15:00:00.000Z","payload":{"level":"warning","message":"password=SYNTHETIC_SECRET","attributes":{}}})
}
#[test]
fn local_files_are_private_protected_canonical_arrays() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("owned/events");
    let directory = SecureDirectory::open(&root).unwrap();
    let path = directory.write_events(&[event()], "checkout").unwrap();
    let bytes = std::fs::read(&path).unwrap();
    assert!(!String::from_utf8_lossy(&bytes).contains("SYNTHETIC_SECRET"));
    let events: Vec<Value> = serde_json::from_slice(&bytes).unwrap();
    assert!(valid_event(&events[0]));
    assert_eq!(
        std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
        0o600
    );
    assert_eq!(
        std::fs::metadata(root).unwrap().permissions().mode() & 0o777,
        0o700
    );
}
#[test]
fn symlink_and_unsafe_service_paths_fail_closed() {
    let temp = tempfile::tempdir().unwrap();
    let outside = tempfile::tempdir().unwrap();
    let link = temp.path().join("link");
    symlink(outside.path(), &link).unwrap();
    assert!(SecureDirectory::open(&link.join("events")).is_err());
    assert!(SecureDirectory::open(&temp.path().join("../escape")).is_err());
    let directory = SecureDirectory::open(&temp.path().join("events")).unwrap();
    assert!(directory.write_events(&[event()], "../escape").is_err());
    assert!(outside.path().read_dir().unwrap().next().is_none());
}

#[test]
fn owned_reads_reject_hard_link_aliases_and_replaced_directories() {
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("events");
    let directory = SecureDirectory::open(&root).unwrap();
    let path = directory.write_events(&[event()], "checkout").unwrap();
    let alias = temp.path().join("alias.json");
    std::fs::hard_link(&path, &alias).unwrap();
    assert!(directory
        .read(path.file_name().unwrap().to_str().unwrap())
        .is_err());
    std::fs::remove_file(alias).unwrap();
    std::fs::rename(&root, temp.path().join("moved-events")).unwrap();
    let outside = tempfile::tempdir().unwrap();
    symlink(outside.path(), &root).unwrap();
    assert!(directory.write_events(&[event()], "checkout").is_err());
    assert!(outside.path().read_dir().unwrap().next().is_none());
}

#[tokio::test]
async fn local_sender_uses_a_safe_filename_without_changing_final_service_identity() {
    use debugbundle::{Client, Config, Context, Level};
    let temp = tempfile::tempdir().unwrap();
    let root = temp.path().join("events");
    let client = Client::new(Config {
        before_send: Some(std::sync::Arc::new(|mut event| {
            event["service"]["name"] = json!("worker.eu-west.v2");
            Some(event)
        })),
        ..Config::local("checkout.api", &root)
    });
    client.capture_log(Level::Error, "synthetic", &Context::default());
    let outcome = client.flush_async(std::time::Duration::from_secs(2)).await;
    assert_eq!(outcome.accepted_events, 1);
    let file = std::fs::read_dir(&root).unwrap().next().unwrap().unwrap();
    assert!(file
        .file_name()
        .to_str()
        .unwrap()
        .ends_with("-worker-eu-west-v2.events.json"));
    let events: Value = serde_json::from_slice(&std::fs::read(file.path()).unwrap()).unwrap();
    assert_eq!(events[0]["service"]["name"], "worker.eu-west.v2");
}