Skip to main content

atomcode_telemetry/
scrub.rs

1//! Scrub paths and truncate strings before events leave the process.
2
3use std::path::Path;
4
5pub const HEAD_MAX: usize = 200;
6
7pub fn scrub_path(s: &str, home: Option<&Path>, cwd: Option<&Path>) -> String {
8    let mut out = s.to_string();
9    if let Some(h) = home.and_then(|p| p.to_str()) {
10        if !h.is_empty() {
11            out = out.replace(h, "<HOME>");
12        }
13    }
14    if let Some(c) = cwd.and_then(|p| p.to_str()) {
15        if !c.is_empty() {
16            out = out.replace(c, "<CWD>");
17        }
18    }
19    out
20}
21
22pub fn truncate_head(s: &str, max_chars: usize) -> String {
23    if s.chars().count() <= max_chars {
24        return s.to_string();
25    }
26    s.chars().take(max_chars).collect()
27}
28
29pub fn backtrace_top_k(bt: &str, k: usize, home: Option<&Path>, cwd: Option<&Path>) -> Vec<String> {
30    bt.lines()
31        .take(k)
32        .map(|line| {
33            let scrubbed = scrub_path(line, home, cwd);
34            if let Some(idx) = scrubbed.find(" at ") {
35                let (head, rest) = scrubbed.split_at(idx + 4);
36                let short = match rest.rsplit_once('/') {
37                    Some((_, tail)) => tail.to_string(),
38                    None => rest.to_string(),
39                };
40                format!("{}{}", head, short)
41            } else {
42                scrubbed
43            }
44        })
45        .collect()
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use std::path::PathBuf;
52
53    #[test]
54    fn scrubs_home_path() {
55        let home = PathBuf::from("/Users/lichao");
56        let s = "panic at /Users/lichao/project/foo.rs:10";
57        assert_eq!(
58            scrub_path(s, Some(&home), None),
59            "panic at <HOME>/project/foo.rs:10"
60        );
61    }
62
63    #[test]
64    fn scrubs_cwd_path() {
65        let cwd = PathBuf::from("/tmp/proj");
66        let s = "error in /tmp/proj/src/main.rs:3";
67        assert_eq!(
68            scrub_path(s, None, Some(&cwd)),
69            "error in <CWD>/src/main.rs:3"
70        );
71    }
72
73    #[test]
74    fn truncate_head_respects_char_boundary() {
75        let s = "中文字串";
76        let out = truncate_head(s, 3);
77        assert_eq!(out, "中文字");
78        assert!(out.is_char_boundary(out.len()));
79    }
80
81    #[test]
82    fn truncate_head_returns_full_when_short() {
83        assert_eq!(truncate_head("hi", 200), "hi");
84    }
85
86    #[test]
87    fn backtrace_strips_abs_paths_keeps_basename() {
88        let bt = "\
89   0: my_crate::fn_a
90      at /Users/lichao/p/src/a.rs:10
91   1: my_crate::fn_b
92      at /Users/lichao/p/src/b.rs:20
93   2: my_crate::fn_c
94      at /Users/lichao/p/src/c.rs:30
95   3: frame4
96   4: frame5
97   5: frame6_should_be_dropped";
98        let frames = backtrace_top_k(bt, 5, Some(Path::new("/Users/lichao")), None);
99        assert_eq!(frames.len(), 5);
100        assert!(frames[1].ends_with("a.rs:10"), "got {:?}", frames[1]);
101        assert!(!frames[1].contains("/Users/lichao"));
102    }
103}