diff --git a/.sampo/changesets/demangler-robust-frame-strip.md b/.sampo/changesets/demangler-robust-frame-strip.md new file mode 100644 index 00000000..ca76e4aa --- /dev/null +++ b/.sampo/changesets/demangler-robust-frame-strip.md @@ -0,0 +1,5 @@ +--- +cargo/posthog-rs: patch +--- + +fix: strip SDK capture frames under newer demangler renderings (`::method::`), which previously survived at the crash-site end of the stack diff --git a/src/error_tracking.rs b/src/error_tracking.rs index 684bd9bd..9905f59a 100644 --- a/src/error_tracking.rs +++ b/src/error_tracking.rs @@ -1176,6 +1176,11 @@ fn is_panic_dispatcher_frame(function: &str) -> bool { // the strip loop stops at them and they survive, classified out-of-app for the // UI to collapse. fn is_internal_capture_frame(function: &str) -> bool { + // Demanglers differ on qualified-path rendering across toolchain versions: + // older output is `Exception::from_error`, newer output wraps the type as + // `::from_error::`. Strip the + // angle brackets before matching so both forms hit. + let function: String = function.replace(['<', '>'], ""); function.starts_with("backtrace::") || function.contains("capture_frames_current_first") || function.contains("capture_raw_frames") @@ -1966,6 +1971,24 @@ mod tests { ); } + #[test] + fn internal_capture_frames_match_both_demangler_renderings() { + // Older toolchains demangle as `Type::method`, newer ones as + // `::method::` — the strip must catch both, or an SDK + // frame survives at the crash-site end of the canonical order. + for name in [ + "posthog_rs::error_tracking::Exception::from_error", + "::from_error::", + "::from_message", + "::capture_exception::", + ] { + assert!(is_internal_capture_frame(name), "should strip {name:?}"); + } + assert!(!is_internal_capture_frame( + "my_app::checkout::Exception_from_error_report" + )); + } + #[test] fn from_error_builds_exception_list_with_stacktrace() { let error = OuterError { source: InnerError };