diff --git a/libdd-crashtracker/src/crash_info/builder.rs b/libdd-crashtracker/src/crash_info/builder.rs index 2cae463ff7..818a024b56 100644 --- a/libdd-crashtracker/src/crash_info/builder.rs +++ b/libdd-crashtracker/src/crash_info/builder.rs @@ -85,13 +85,11 @@ impl ErrorDataBuilder { if let Some(stack) = &mut self.stack { stack.set_complete()?; } else { - // With https://github.com/DataDog/libdatadog/pull/1076 it happens that stack trace are - // empty on musl based Linux (Alpine) because stack unwinding may not be able to unwind - // passed the signal handler. This by-passing for musl is temporary and needs a fix. - #[cfg(target_env = "musl")] - return Ok(()); - #[cfg(not(target_env = "musl"))] - anyhow::bail!("Can't set non-existant stack complete"); + // No frames were received, but we got the end marker. This can happen on + // certain platforms/contexts where stack unwinding fails to capture any + // frames (e.g., musl-based Linux). Initialize an empty but incomplete stack + // to indicate that stack collection did not succeed. + self.stack = Some(StackTrace::new_incomplete()); } Ok(()) } @@ -355,7 +353,15 @@ impl CrashInfoBuilder { } pub fn with_stack_set_complete(&mut self) -> anyhow::Result<()> { - self.error.with_stack_set_complete() + let had_no_frames = self.error.stack.is_none(); + self.error.with_stack_set_complete()?; + if had_no_frames { + self.with_log_message( + "No native stack frames received; stack unwinding may have failed".to_string(), + true, + )?; + } + Ok(()) } pub fn with_thread(&mut self, thread: ThreadData) -> anyhow::Result<()> { @@ -545,4 +551,67 @@ mod tests { let report = builder.build().unwrap(); assert!(report.error.message.as_ref().unwrap().len() >= 10000); } + + #[test] + fn test_no_frames_is_incomplete() { + // When we receive an end stacktrace marker but no frames were collected, + // we should create an empty incomplete stack rather than erroring. + let mut builder = ErrorDataBuilder::new(); + assert!(builder.stack.is_none()); + + // This should succeed and create an empty incomplete stack + let result = builder.with_stack_set_complete(); + assert!(result.is_ok()); + + // Verify we now have a stack that is incomplete (no frames were captured) + assert!(builder.stack.is_some()); + let stack = builder.stack.as_ref().unwrap(); + assert!(stack.frames.is_empty()); + assert!(stack.incomplete); + } + + #[test] + fn test_with_stack_set_complete_with_frames() { + // When we have frames and call set_complete, it should mark them complete + let mut builder = ErrorDataBuilder::new(); + + // Add a frame (which creates an incomplete stack) + let frame = StackFrame::test_instance(1); + builder.with_stack_frame(frame, true).unwrap(); + assert!(builder.stack.as_ref().unwrap().incomplete); + + // Mark complete + builder.with_stack_set_complete().unwrap(); + + // Verify stack is now complete + let stack = builder.stack.as_ref().unwrap(); + assert_eq!(stack.frames.len(), 1); + assert!(!stack.incomplete); + } + + #[test] + fn test_crash_info_builder_empty_stack_is_incomplete() { + // When no frames were captured, the stack and CrashInfo should be marked + // incomplete to indicate that stack collection did not succeed. + let mut builder = CrashInfoBuilder::new(); + builder.with_kind(ErrorKind::UnixSignal).unwrap(); + + // Simulate receiving BEGIN_STACKTRACE then END_STACKTRACE with no frames + builder.with_stack_set_complete().unwrap(); + + let crash_info = builder.build().unwrap(); + + // The stack should be empty and incomplete (no frames were captured) + assert!(crash_info.error.stack.frames.is_empty()); + assert!(crash_info.error.stack.incomplete); + + // The overall crash info should be marked complete + assert!(!crash_info.incomplete); + + // A log message should be recorded noting that no frames were received + assert!(crash_info + .log_messages + .iter() + .any(|msg| msg.contains("No native stack frames received"))); + } } diff --git a/libdd-crashtracker/src/receiver/receive_report.rs b/libdd-crashtracker/src/receiver/receive_report.rs index 8cf1557572..3528337a48 100644 --- a/libdd-crashtracker/src/receiver/receive_report.rs +++ b/libdd-crashtracker/src/receiver/receive_report.rs @@ -678,4 +678,94 @@ mod tests { .unwrap(); assert!(matches!(state, StdinState::Waiting)); } + + #[test] + fn test_stacktrace_empty_workflow() { + // Test that receiving BEGIN_STACKTRACE followed by END_STACKTRACE + // (with no frames) creates an empty but complete stack + let mut builder = CrashInfoBuilder::new(); + let mut config = None; + + let mut state = StdinState::Waiting; + + state = process_line( + &mut builder, + &mut config, + DD_CRASHTRACK_BEGIN_STACKTRACE, + state, + &None, + ) + .unwrap(); + assert!(matches!(state, StdinState::StackTrace)); + + // End stacktrace immediately (no frames) + state = process_line( + &mut builder, + &mut config, + DD_CRASHTRACK_END_STACKTRACE, + state, + &None, + ) + .unwrap(); + assert!(matches!(state, StdinState::Waiting)); + + // Verify we have an empty but incomplete stack (no frames captured = stack unwinding + // failed) + let stack = builder.error.stack.as_ref().expect("Stack should exist"); + assert!(stack.frames.is_empty()); + assert!( + stack.incomplete, + "Stack should be marked incomplete when no frames were captured" + ); + + // Verify a log message was recorded about no frames + assert!(builder + .log_messages + .as_ref() + .map(|msgs| msgs + .iter() + .any(|msg| msg.contains("No native stack frames received"))) + .unwrap_or(false)); + } + + #[test] + fn test_stacktrace_with_frames_workflow() { + let mut builder = CrashInfoBuilder::new(); + let mut config = None; + + let mut state = StdinState::Waiting; + + // Begin stacktrace + state = process_line( + &mut builder, + &mut config, + DD_CRASHTRACK_BEGIN_STACKTRACE, + state, + &None, + ) + .unwrap(); + assert!(matches!(state, StdinState::StackTrace)); + + // Add a frame + let frame_json = r#"{"ip":"0x1234"}"#; + state = process_line(&mut builder, &mut config, frame_json, state, &None).unwrap(); + assert!(matches!(state, StdinState::StackTrace)); + + // End stacktrace + state = process_line( + &mut builder, + &mut config, + DD_CRASHTRACK_END_STACKTRACE, + state, + &None, + ) + .unwrap(); + assert!(matches!(state, StdinState::Waiting)); + + // Verify we have a stack with one frame, marked complete + let stack = builder.error.stack.as_ref().expect("Stack should exist"); + assert_eq!(stack.frames.len(), 1); + assert!(!stack.incomplete, "Stack should be marked complete"); + assert_eq!(stack.frames[0].ip, Some("0x1234".to_string())); + } }