mod support;
use core::num::ParseIntError;
use std::{
error::Error as StdError,
fmt::{Display, Formatter},
};
use exceptionless::ExceptionlessClient;
use serde_json::json;
use support::{CapturingTransport, payload_events, test_config};
#[derive(Debug)]
struct InnerError;
impl Display for InnerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "inner boom")
}
}
impl StdError for InnerError {}
#[derive(Debug)]
struct OuterError {
source: InnerError,
}
impl Display for OuterError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "outer boom")
}
}
impl StdError for OuterError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.source)
}
}
#[tokio::test]
async fn error_entrypoint_shapes_payload_and_preserves_context() -> Result<(), Box<dyn StdError>> {
let transport = CapturingTransport::success();
let client = ExceptionlessClient::new(test_config(), transport.clone());
let error = OuterError { source: InnerError };
client
.error(&error)
.source("checkout")
.tag("ops")
.tag("ops")
.tag(" ")
.data("tenant", json!("acme"))
.user_identity("user-42")
.version("1.2.3")
.send()
.await?;
let requests = transport.requests();
assert_eq!(requests.len(), 1);
let events = payload_events(&requests[0]);
assert_eq!(events.len(), 1);
let event = &events[0];
assert_eq!(event["type"], "error");
assert_eq!(event["source"], "checkout");
assert_eq!(event["tags"], json!(["ops"]));
assert_eq!(event["data"]["tenant"], "acme");
assert_eq!(event["data"]["@user"], "user-42");
assert_eq!(event["data"]["@version"], "1.2.3");
assert!(
event["date"]
.as_str()
.is_some_and(|value| !value.is_empty())
);
let error_payload = &event["data"]["@error"];
assert_eq!(error_payload["message"], "outer boom");
assert!(
error_payload["type"]
.as_str()
.is_some_and(|value| value.ends_with("OuterError"))
);
let frames = error_payload["stack_trace"]
.as_array()
.expect("stack_trace should be an array");
assert!(!frames.is_empty(), "stack_trace should not be empty");
assert!(
frames[0]["method"]
.as_str()
.is_some_and(|m| m.contains("::")),
"first frame method should be a qualified Rust path, got: {}",
frames[0]["method"]
);
assert!(
frames
.iter()
.any(|f| f["file_name"].as_str().is_some_and(|v| v.ends_with(".rs"))),
"no frame had a .rs file_name"
);
assert!(
frames.iter().any(|f| f["line_number"].is_number()),
"no frame had a line_number"
);
assert_eq!(error_payload["inner"]["message"], "inner boom");
Ok(())
}
#[tokio::test]
async fn stack_trace_from_stdlib_error_has_real_frames() -> Result<(), Box<dyn StdError>> {
let transport = CapturingTransport::success();
let client = ExceptionlessClient::new(test_config(), transport.clone());
let err: ParseIntError = "not a number".parse::<i32>().unwrap_err();
client.error(&err).send().await?;
let requests = transport.requests();
let events = payload_events(&requests[0]);
let error_payload = &events[0]["data"]["@error"];
let frames = error_payload["stack_trace"]
.as_array()
.expect("stack_trace should be an array");
assert!(
frames.len() > 1,
"expected multiple stack frames, got {}",
frames.len()
);
assert!(
frames
.iter()
.any(|f| f["file_name"].as_str().is_some_and(|v| v.ends_with(".rs"))),
"no frame had a .rs file_name — stack trace looks like a debug dump, not real frames"
);
assert!(
frames.iter().any(|f| f["line_number"].is_number()),
"no frame had a line_number — stack trace is missing source location"
);
for frame in frames {
let method = frame["method"].as_str().unwrap_or("");
assert!(
!method.contains('{') || method.contains("::"),
"frame method looks like a raw Debug repr, not a function name: {method:?}"
);
}
Ok(())
}
#[tokio::test]
async fn stack_trace_frames_include_error_site() -> Result<(), Box<dyn StdError>> {
let transport = CapturingTransport::success();
let client = ExceptionlessClient::new(test_config(), transport.clone());
let err: ParseIntError = "bad".parse::<i32>().unwrap_err();
client.error(&err).send().await?;
let requests = transport.requests();
let events = payload_events(&requests[0]);
let error_payload = &events[0]["data"]["@error"];
let frames = error_payload["stack_trace"]
.as_array()
.expect("stack_trace should be an array");
assert!(
frames.iter().any(|f| {
f["file_name"]
.as_str()
.is_some_and(|v| v.contains("acceptance_errors"))
}),
"no frame referenced user code — backtrace may be capturing only SDK frames"
);
Ok(())
}