From 3671a273e55d6a98bc50df9413da2b6e3749342f Mon Sep 17 00:00:00 2001 From: Daniel Schwartz-Narbone Date: Thu, 29 Jan 2026 16:18:31 -0500 Subject: [PATCH] feat(profiling): make MIME type optional --- libdd-profiling/src/cxx.rs | 14 ++++ .../src/exporter/profile_exporter.rs | 16 ++-- libdd-profiling/tests/exporter_e2e.rs | 77 +++++++++++++++---- libdd-profiling/tests/file_exporter_test.rs | 5 ++ 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/libdd-profiling/src/cxx.rs b/libdd-profiling/src/cxx.rs index 2c1ad0e879..a9d9a57639 100644 --- a/libdd-profiling/src/cxx.rs +++ b/libdd-profiling/src/cxx.rs @@ -20,6 +20,7 @@ pub mod ffi { #[derive(Debug)] #[repr(u8)] enum MimeType { + None = 0, ApplicationJson, ApplicationOctetStream, TextCsv, @@ -335,6 +336,7 @@ impl TryFrom for exporter::MimeType { fn try_from(mime: ffi::MimeType) -> Result { match mime { + ffi::MimeType::None => Ok(exporter::MimeType::None), ffi::MimeType::ApplicationJson => Ok(exporter::MimeType::ApplicationJson), ffi::MimeType::ApplicationOctetStream => Ok(exporter::MimeType::ApplicationOctetStream), ffi::MimeType::TextCsv => Ok(exporter::MimeType::TextCsv), @@ -1115,6 +1117,18 @@ mod tests { exporter::MimeType::ApplicationOctetStream )); + // AttachmentFile with MimeType::None + let file_none: exporter::File = (&ffi::AttachmentFile { + name: "test.dat", + data: &data, + mime: ffi::MimeType::None, + }) + .try_into() + .expect("Failed to convert AttachmentFile with MimeType::None"); + assert_eq!(file_none.name, "test.dat"); + assert_eq!(file_none.bytes, data.as_slice()); + assert!(matches!(file_none.mime, exporter::MimeType::None)); + // Tag conversion with special characters let tag: libdd_common::tag::Tag = (&ffi::Tag { key: "test-key.with_special:chars", diff --git a/libdd-profiling/src/exporter/profile_exporter.rs b/libdd-profiling/src/exporter/profile_exporter.rs index efc9a54907..d2db3fde30 100644 --- a/libdd-profiling/src/exporter/profile_exporter.rs +++ b/libdd-profiling/src/exporter/profile_exporter.rs @@ -51,6 +51,7 @@ pub struct ProfileExporter { #[repr(C)] #[derive(Debug, Copy, Clone)] pub enum MimeType { + None = 0, ApplicationJson, ApplicationOctetStream, TextCsv, @@ -61,6 +62,7 @@ pub enum MimeType { impl MimeType { pub fn as_str(&self) -> &'static str { match self { + MimeType::None => "", MimeType::ApplicationJson => mime::APPLICATION_JSON.as_ref(), MimeType::ApplicationOctetStream => mime::APPLICATION_OCTET_STREAM.as_ref(), MimeType::TextCsv => mime::TEXT_CSV.as_ref(), @@ -426,16 +428,20 @@ impl ProfileExporter { .context("failed to create compressor")?; encoder.write_all(file.bytes)?; - form = form.part( - file.name.to_string(), - reqwest::multipart::Part::bytes(encoder.finish()?).file_name(file.name.to_string()), - ); + let mut part = + reqwest::multipart::Part::bytes(encoder.finish()?).file_name(file.name.to_string()); + if !matches!(file.mime, MimeType::None) { + part = part.mime_str(file.mime.as_str())?; + } + form = form.part(file.name.to_string(), part); } // Add profile Ok(form.part( "profile.pprof", - reqwest::multipart::Part::bytes(profile.buffer).file_name("profile.pprof"), + reqwest::multipart::Part::bytes(profile.buffer) + .file_name("profile.pprof") + .mime_str(mime::APPLICATION_OCTET_STREAM.as_ref())?, )) } } diff --git a/libdd-profiling/tests/exporter_e2e.rs b/libdd-profiling/tests/exporter_e2e.rs index 3bca79c48e..5078fc333b 100644 --- a/libdd-profiling/tests/exporter_e2e.rs +++ b/libdd-profiling/tests/exporter_e2e.rs @@ -233,6 +233,11 @@ async fn export_full_profile( bytes: b"{\"test\": true}", mime: MimeType::ApplicationJson, }, + File { + name: "custom.dat", + bytes: b"custom data", + mime: MimeType::None, + }, ]; // Build metadata @@ -348,8 +353,8 @@ fn validate_full_export(req: &ReceivedRequest, expected_path: &str) -> anyhow::R let attachments = event_json["attachments"] .as_array() .ok_or_else(|| anyhow::anyhow!("Missing attachments"))?; - assert_eq!(attachments.len(), 3); - for attachment in &["profile.pprof", "jit.pprof", "metadata.json"] { + assert_eq!(attachments.len(), 4); + for attachment in &["profile.pprof", "jit.pprof", "metadata.json", "custom.dat"] { assert!( attachments.contains(&serde_json::json!(attachment)), "Missing attachment: {}", @@ -371,18 +376,62 @@ fn validate_full_export(req: &ReceivedRequest, expected_path: &str) -> anyhow::R assert_eq!(event_json["info"]["platform"]["hostname"], "test-host"); assert_eq!(event_json["info"]["runtime"]["engine"], "rust"); - // Verify parts exist (files are compressed, just check non-empty) - for part_name in &["profile.pprof", "jit.pprof", "metadata.json"] { - let part = parts - .iter() - .find(|p| p.name == *part_name) - .ok_or_else(|| anyhow::anyhow!("Missing part: {}", part_name))?; - assert!( - !part.content.is_empty(), - "{} should not be empty", - part_name - ); - } + // Verify parts exist (files are compressed, just check non-empty) and mime types + let profile_part = parts + .iter() + .find(|p| p.name == "profile.pprof") + .ok_or_else(|| anyhow::anyhow!("Missing part: profile.pprof"))?; + assert!( + !profile_part.content.is_empty(), + "profile.pprof should not be empty" + ); + assert_eq!( + profile_part.content_type.as_deref(), + Some("application/octet-stream"), + "profile.pprof should have application/octet-stream mime type" + ); + + let jit_part = parts + .iter() + .find(|p| p.name == "jit.pprof") + .ok_or_else(|| anyhow::anyhow!("Missing part: jit.pprof"))?; + assert!( + !jit_part.content.is_empty(), + "jit.pprof should not be empty" + ); + assert_eq!( + jit_part.content_type.as_deref(), + Some("application/octet-stream"), + "jit.pprof should have application/octet-stream mime type" + ); + + let metadata_part = parts + .iter() + .find(|p| p.name == "metadata.json") + .ok_or_else(|| anyhow::anyhow!("Missing part: metadata.json"))?; + assert!( + !metadata_part.content.is_empty(), + "metadata.json should not be empty" + ); + assert_eq!( + metadata_part.content_type.as_deref(), + Some("application/json"), + "metadata.json should have application/json mime type" + ); + + let custom_part = parts + .iter() + .find(|p| p.name == "custom.dat") + .ok_or_else(|| anyhow::anyhow!("Missing part: custom.dat"))?; + assert!( + !custom_part.content.is_empty(), + "custom.dat should not be empty" + ); + // When MimeType::None is used, no content_type should be set + assert!( + custom_part.content_type.is_none(), + "custom.dat should not have a mime type (MimeType::None)" + ); Ok(()) } diff --git a/libdd-profiling/tests/file_exporter_test.rs b/libdd-profiling/tests/file_exporter_test.rs index e6912ecda5..f59538cb95 100644 --- a/libdd-profiling/tests/file_exporter_test.rs +++ b/libdd-profiling/tests/file_exporter_test.rs @@ -168,6 +168,11 @@ mod tests { !profile_part.content.is_empty(), "profile should have content" ); + assert_eq!( + profile_part.content_type.as_deref(), + Some("application/octet-stream"), + "profile.pprof should have application/octet-stream mime type" + ); } #[test]