use std::cell::RefCell;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::pii_scrubber::Value;
#[derive(Clone, Debug, PartialEq)]
pub struct Breadcrumb {
pub category: String,
pub message: String,
pub level: String,
pub timestamp: String,
pub data: HashMap<String, Value>,
}
thread_local! {
static BREADCRUMBS: RefCell<Vec<Breadcrumb>> = const { RefCell::new(Vec::new()) };
}
pub fn add_breadcrumb(
track_breadcrumbs: bool,
max_breadcrumbs: usize,
message: &str,
category: &str,
level: &str,
data: HashMap<String, Value>,
) {
if !track_breadcrumbs || max_breadcrumbs == 0 {
return;
}
let category = if category.is_empty() {
"custom"
} else {
category
};
let level = if level.is_empty() { "info" } else { level };
BREADCRUMBS.with(|trail| {
let mut trail = trail.borrow_mut();
trail.push(Breadcrumb {
category: category.to_string(),
message: message.to_string(),
level: level.to_string(),
timestamp: format_now(),
data,
});
if trail.len() > max_breadcrumbs {
let overflow = trail.len() - max_breadcrumbs;
trail.drain(0..overflow);
}
});
}
pub fn clear_breadcrumbs() {
BREADCRUMBS.with(|trail| trail.borrow_mut().clear());
}
pub fn current_breadcrumbs() -> Vec<Breadcrumb> {
BREADCRUMBS.with(|trail| trail.borrow().clone())
}
fn format_now() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
crate::event_builder::format_unix_timestamp(now.as_secs())
}
#[cfg(test)]
mod tests {
use super::*;
fn reset() {
clear_breadcrumbs();
}
#[test]
fn add_breadcrumb_preserves_order_and_defaults() {
reset();
add_breadcrumb(true, 30, "first", "", "", HashMap::new());
add_breadcrumb(
true,
30,
"second",
"custom",
"warning",
HashMap::from([("n".to_string(), Value::Number(1.0))]),
);
let trail = current_breadcrumbs();
assert_eq!(trail.len(), 2);
assert_eq!(trail[0].message, "first");
assert_eq!(trail[0].category, "custom");
assert_eq!(trail[0].level, "info");
assert!(trail[0].data.is_empty());
assert_eq!(trail[1].message, "second");
assert_eq!(trail[1].level, "warning");
assert_eq!(trail[1].data.get("n"), Some(&Value::Number(1.0)));
assert!(!trail[0].timestamp.is_empty());
reset();
}
#[test]
fn add_breadcrumb_drops_oldest_once_over_max_size() {
reset();
add_breadcrumb(true, 2, "one", "", "", HashMap::new());
add_breadcrumb(true, 2, "two", "", "", HashMap::new());
add_breadcrumb(true, 2, "three", "", "", HashMap::new());
let trail = current_breadcrumbs();
assert_eq!(trail.len(), 2);
assert_eq!(trail[0].message, "two");
assert_eq!(trail[1].message, "three");
reset();
}
#[test]
fn add_breadcrumb_is_a_no_op_when_tracking_is_off() {
reset();
add_breadcrumb(false, 30, "hello", "", "", HashMap::new());
assert!(current_breadcrumbs().is_empty());
reset();
}
#[test]
fn add_breadcrumb_is_a_no_op_when_max_size_is_zero() {
reset();
add_breadcrumb(true, 0, "hello", "", "", HashMap::new());
assert!(current_breadcrumbs().is_empty());
reset();
}
#[test]
fn clear_breadcrumbs_empties_the_current_thread_trail() {
reset();
add_breadcrumb(true, 30, "hello", "", "", HashMap::new());
assert_eq!(current_breadcrumbs().len(), 1);
clear_breadcrumbs();
assert!(current_breadcrumbs().is_empty());
}
}