use crate::config::Config;
use crate::event::Frame;
pub const MAX_FRAMES: usize = 50;
pub fn current_frames(config: &Config) -> Vec<Frame> {
let bt = backtrace::Backtrace::new();
frames_from(&bt, config)
}
pub fn frames_from(bt: &backtrace::Backtrace, config: &Config) -> Vec<Frame> {
let mut raw: Vec<RawFrame> = Vec::new();
for frame in bt.frames() {
for symbol in frame.symbols() {
let function = symbol
.name()
.map(|n| format!("{n:#}"));
let filename = symbol.filename().map(|p| p.display().to_string());
raw.push(RawFrame {
function,
filename,
lineno: symbol.lineno(),
colno: symbol.colno(),
});
}
}
finalize_raw(raw, config)
}
#[cfg(feature = "anyhow")]
pub(crate) fn parse_std_backtrace(text: &str, config: &Config) -> Vec<Frame> {
let mut raw: Vec<RawFrame> = Vec::new();
let mut pending: Option<RawFrame> = None;
for line in text.lines() {
let trimmed = line.trim_start();
if let Some(function) = parse_frame_header(trimmed) {
if let Some(prev) = pending.take() {
raw.push(prev);
}
pending = Some(RawFrame {
function: Some(function),
filename: None,
lineno: None,
colno: None,
});
} else if let Some((file, lineno, colno)) = parse_at_line(trimmed) {
if let Some(p) = pending.as_mut() {
p.filename = Some(file);
p.lineno = lineno;
p.colno = colno;
}
}
}
if let Some(prev) = pending.take() {
raw.push(prev);
}
finalize_raw(raw, config)
}
#[cfg(feature = "anyhow")]
fn parse_frame_header(s: &str) -> Option<String> {
let (idx, rest) = s.split_once(':')?;
if idx.is_empty() || !idx.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let func = rest.trim();
if func.is_empty() {
None
} else {
Some(func.to_string())
}
}
#[cfg(feature = "anyhow")]
fn parse_at_line(s: &str) -> Option<(String, Option<u32>, Option<u32>)> {
let rest = s.strip_prefix("at ")?.trim();
if let Some((head, tail)) = rest.rsplit_once(':') {
if let Ok(n) = tail.parse::<u32>() {
if let Some((head2, mid)) = head.rsplit_once(':') {
if let Ok(m) = mid.parse::<u32>() {
return Some((head2.to_string(), Some(m), Some(n)));
}
}
return Some((head.to_string(), Some(n), None));
}
}
Some((rest.to_string(), None, None))
}
fn finalize_raw(mut raw: Vec<RawFrame>, config: &Config) -> Vec<Frame> {
trim_leading_noise(&mut raw);
let project_root = project_root();
raw.into_iter()
.take(MAX_FRAMES)
.map(|rf| {
let in_app = classify_in_app(rf.filename.as_deref(), rf.function.as_deref(), config);
let display = rf
.filename
.as_deref()
.map(|f| relative_filename(f, project_root.as_deref()))
.unwrap_or_else(|| "<unknown>".to_string());
let mut frame = Frame {
filename: display,
abs_path: rf.filename.clone(),
lineno: rf.lineno,
colno: rf.colno,
function: rf.function,
in_app,
pre_context: None,
context_line: None,
post_context: None,
};
if in_app {
if let (Some(path), Some(line)) = (rf.filename.as_deref(), rf.lineno) {
if let Some(ctx) = crate::source::fetch(path, line) {
frame.pre_context = Some(ctx.pre);
frame.context_line = Some(ctx.line);
frame.post_context = Some(ctx.post);
}
}
}
frame
})
.collect()
}
pub fn frames_to_string(frames: &[Frame]) -> String {
let mut out = String::new();
for f in frames {
let func = f.function.as_deref().unwrap_or("<unknown>");
match (f.lineno, f.colno) {
(Some(l), Some(c)) => {
out.push_str(&format!("{func}\n at {}:{l}:{c}\n", f.filename));
}
(Some(l), None) => {
out.push_str(&format!("{func}\n at {}:{l}\n", f.filename));
}
_ => {
out.push_str(&format!("{func}\n at {}\n", f.filename));
}
}
}
out
}
struct RawFrame {
function: Option<String>,
filename: Option<String>,
lineno: Option<u32>,
colno: Option<u32>,
}
fn trim_leading_noise(frames: &mut Vec<RawFrame>) {
const NOISE_PREFIXES: &[&str] = &[
"errsight::",
"errsight ", "backtrace::",
"std::backtrace",
"std::panicking",
"std::panic::",
"core::panicking",
"std::sys_common::backtrace",
"std::rt::",
"__rust_",
"rust_begin_unwind",
"___rust",
"_rust_panic",
];
let first_real = frames.iter().position(|f| {
f.function
.as_deref()
.is_some_and(|func| !NOISE_PREFIXES.iter().any(|p| func.starts_with(p)))
});
match first_real {
Some(0) | None => {}
Some(n) => {
frames.drain(0..n);
}
}
}
fn classify_in_app(path: Option<&str>, function: Option<&str>, config: &Config) -> bool {
let matches = |needle: &str| {
path.map(|p| p.contains(needle)).unwrap_or(false)
|| function.map(|f| f.contains(needle)).unwrap_or(false)
};
if config.in_app_exclude.iter().any(|n| matches(n)) {
return false;
}
if config.in_app_include.iter().any(|n| matches(n)) {
return true;
}
match path {
Some(p) => !is_dependency_path(p),
None => false,
}
}
fn is_dependency_path(path: &str) -> bool {
let p = path.replace('\\', "/");
p.contains("/.cargo/")
|| p.contains("/registry/src/")
|| p.contains("/git/checkouts/")
|| p.contains("/rustc/")
|| p.starts_with("/rustc")
|| p.contains("/.rustup/")
|| p.contains("/lib/rustlib/")
|| p.contains("/toolchains/")
}
fn project_root() -> Option<String> {
std::env::current_dir()
.ok()
.map(|p| p.display().to_string())
}
fn relative_filename(abs: &str, root: Option<&str>) -> String {
if let Some(root) = root {
if let Some(rest) = abs.strip_prefix(root) {
if rest.is_empty() || rest.starts_with('/') || rest.starts_with('\\') {
return rest.trim_start_matches(['/', '\\']).to_string();
}
}
}
abs.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ConfigBuilder;
#[test]
fn dependency_paths_are_not_in_app() {
assert!(is_dependency_path(
"/Users/me/.cargo/registry/src/index.crates.io-x/serde-1.0/src/lib.rs"
));
assert!(is_dependency_path("/rustc/abc123/library/std/src/panic.rs"));
assert!(is_dependency_path(
"/Users/me/.rustup/toolchains/stable/lib/rustlib/src/rust/library/core/src/result.rs"
));
assert!(!is_dependency_path("/Users/me/proj/src/main.rs"));
}
#[test]
fn classify_respects_overrides() {
let cfg = ConfigBuilder::from_default()
.in_app_exclude(["generated/"])
.in_app_include(["my_lib"])
.build();
assert!(!classify_in_app(
Some("/proj/generated/schema.rs"),
None,
&cfg
));
assert!(classify_in_app(
Some("/Users/me/.cargo/registry/src/x/my_lib-1.0/src/lib.rs"),
None,
&cfg
));
assert!(classify_in_app(None, Some("my_lib::do_thing"), &cfg));
assert!(classify_in_app(Some("/proj/src/api.rs"), None, &cfg));
assert!(!classify_in_app(None, Some("core::ops::drop"), &cfg));
}
#[test]
fn relative_filename_only_strips_on_boundary() {
let root = Some("/Users/me/proj");
assert_eq!(
relative_filename("/Users/me/proj/src/main.rs", root),
"src/main.rs"
);
assert_eq!(relative_filename("/Users/me/proj", root), "");
assert_eq!(
relative_filename("/Users/me/proj-other/src/main.rs", root),
"/Users/me/proj-other/src/main.rs"
);
assert_eq!(
relative_filename("/Users/me/projsrc/main.rs", root),
"/Users/me/projsrc/main.rs"
);
}
#[test]
fn trim_skips_symbolless_leading_frames() {
let mut frames = vec![
RawFrame {
function: Some("std::panicking::begin_panic".into()),
filename: None,
lineno: None,
colno: None,
},
RawFrame {
function: None, filename: None,
lineno: None,
colno: None,
},
RawFrame {
function: Some("errsight::panic::capture_panic".into()),
filename: None,
lineno: None,
colno: None,
},
RawFrame {
function: Some("my_app::handler".into()),
filename: Some("/proj/src/handler.rs".into()),
lineno: Some(7),
colno: None,
},
];
trim_leading_noise(&mut frames);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].function.as_deref(), Some("my_app::handler"));
}
#[test]
fn trims_leading_sdk_frames() {
let mut frames = vec![
RawFrame {
function: Some("backtrace::Backtrace::new".into()),
filename: None,
lineno: None,
colno: None,
},
RawFrame {
function: Some("errsight::capture_error".into()),
filename: None,
lineno: None,
colno: None,
},
RawFrame {
function: Some("my_app::run".into()),
filename: Some("/proj/src/main.rs".into()),
lineno: Some(10),
colno: None,
},
];
trim_leading_noise(&mut frames);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].function.as_deref(), Some("my_app::run"));
}
#[test]
fn real_backtrace_parses_and_marks_in_app() {
let cfg = ConfigBuilder::from_default().build();
let frames = current_frames(&cfg);
assert!(!frames.is_empty(), "expected to capture some frames");
assert!(frames.iter().any(|f| f.function.is_some()));
}
}