use std::collections::{BTreeMap, VecDeque};
use crate::event::{Breadcrumb, User};
pub const DEFAULT_MAX_BREADCRUMBS: usize = 100;
#[derive(Debug, Clone)]
pub struct Scope {
user: Option<User>,
tags: BTreeMap<String, String>,
breadcrumbs: VecDeque<Breadcrumb>,
max_breadcrumbs: usize,
}
impl Default for Scope {
fn default() -> Self {
Scope {
user: None,
tags: BTreeMap::new(),
breadcrumbs: VecDeque::new(),
max_breadcrumbs: DEFAULT_MAX_BREADCRUMBS,
}
}
}
impl Scope {
pub fn new() -> Self {
Self::default()
}
pub fn set_user(&mut self, user: User) -> &mut Self {
self.user = if user.is_empty() { None } else { Some(user) };
self
}
pub fn clear_user(&mut self) -> &mut Self {
self.user = None;
self
}
pub fn user(&self) -> Option<&User> {
self.user.as_ref()
}
pub fn set_tag(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.tags.insert(key.into(), value.into());
self
}
pub fn remove_tag(&mut self, key: &str) -> &mut Self {
self.tags.remove(key);
self
}
pub fn clear_tags(&mut self) -> &mut Self {
self.tags.clear();
self
}
pub fn tags(&self) -> &BTreeMap<String, String> {
&self.tags
}
pub fn add_breadcrumb(&mut self, crumb: Breadcrumb) -> &mut Self {
self.breadcrumbs.push_back(crumb);
while self.breadcrumbs.len() > self.max_breadcrumbs {
self.breadcrumbs.pop_front();
}
self
}
pub fn clear_breadcrumbs(&mut self) -> &mut Self {
self.breadcrumbs.clear();
self
}
pub fn set_max_breadcrumbs(&mut self, max: usize) -> &mut Self {
self.max_breadcrumbs = max;
while self.breadcrumbs.len() > self.max_breadcrumbs {
self.breadcrumbs.pop_front();
}
self
}
pub fn breadcrumbs(&self, limit: usize) -> Vec<Breadcrumb> {
let n = self.breadcrumbs.len();
let start = n.saturating_sub(limit);
self.breadcrumbs.iter().skip(start).cloned().collect()
}
pub fn apply_to(&self, event: &mut crate::event::Event, breadcrumb_limit: usize) {
if event.user.is_none() {
if let Some(u) = &self.user {
event.user = Some(u.clone());
}
}
for (k, v) in &self.tags {
event.tags.entry(k.clone()).or_insert_with(|| v.clone());
}
if event.breadcrumbs.is_empty() && !self.breadcrumbs.is_empty() {
event.breadcrumbs = self.breadcrumbs(breadcrumb_limit);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Breadcrumb, Event, User};
use crate::level::Level;
#[test]
fn breadcrumbs_are_bounded() {
let mut s = Scope::new();
s.set_max_breadcrumbs(3);
for i in 0..10 {
s.add_breadcrumb(Breadcrumb::new("test", format!("crumb {i}")));
}
let crumbs = s.breadcrumbs(100);
assert_eq!(crumbs.len(), 3);
assert_eq!(crumbs[0].message, "crumb 7");
assert_eq!(crumbs[2].message, "crumb 9");
}
#[test]
fn empty_user_clears() {
let mut s = Scope::new();
s.set_user(User::new().email("a@b.c"));
assert!(s.user().is_some());
s.set_user(User::new());
assert!(s.user().is_none());
}
#[test]
fn apply_does_not_override_per_call_values() {
let mut s = Scope::new();
s.set_user(User::new().id("scope-user"));
s.set_tag("region", "us");
let mut e = Event::new(Level::Error, "x");
e.user = Some(User::new().id("call-user"));
e.tags.insert("region".into(), "eu".into());
s.apply_to(&mut e, 100);
assert_eq!(e.user.unwrap().id.unwrap(), "call-user");
assert_eq!(e.tags["region"], "eu");
}
#[test]
fn apply_fills_missing_values() {
let mut s = Scope::new();
s.set_user(User::new().id("scope-user"));
s.set_tag("region", "us");
s.add_breadcrumb(Breadcrumb::new("nav", "home"));
let mut e = Event::new(Level::Error, "x");
s.apply_to(&mut e, 100);
assert_eq!(e.user.unwrap().id.unwrap(), "scope-user");
assert_eq!(e.tags["region"], "us");
assert_eq!(e.breadcrumbs.len(), 1);
}
}