From 4b7b0667aeeca925a621faf74e8495db3e596e78 Mon Sep 17 00:00:00 2001 From: sid sri Date: Sat, 25 Jul 2026 19:33:21 +0530 Subject: [PATCH] fix(types): Support source map debug images Accept source map debug images emitted by browser SDKs so events with debug IDs deserialize. Preserve unknown image payloads so future variants do not invalidate otherwise usable events. Fixes #1267 --- CHANGELOG.md | 1 + sentry-types/src/protocol/v7.rs | 94 +++++++++++++++++++++++++- sentry-types/tests/test_protocol_v7.rs | 43 ++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513383656..b2fc2e37d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ ### Fixes - Restored the reqwest transport's pre-0.13 protocol features by disabling HTTP/2 and native-TLS ALPN ([#1258](https://github.com/getsentry/sentry-rust/pull/1258)). +- Added support for source map debug images and preserved unknown debug image types when deserializing events. ## 0.48.5 diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index f05394258..9b9e9eaba 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -908,8 +908,7 @@ pub struct SystemSdkInfo { } /// Represents a debug image. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "snake_case", tag = "type")] +#[derive(Debug, Clone, PartialEq)] pub enum DebugImage { /// Apple debug images (machos). This is currently also used for /// non apple platforms with similar debug setups. @@ -921,6 +920,76 @@ pub enum DebugImage { /// Image used for WebAssembly. Their structure is identical to other native /// images. Wasm(WasmDebugImage), + /// A source map debug image. + SourceMap(SourceMapDebugImage), + /// A debug image type that is not yet known to this SDK. + Other(Map), +} + +impl Serialize for DebugImage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + #[serde(rename_all = "snake_case", tag = "type")] + enum TaggedDebugImage<'a> { + Apple(&'a AppleDebugImage), + Symbolic(&'a SymbolicDebugImage), + Proguard(&'a ProguardDebugImage), + Wasm(&'a WasmDebugImage), + #[serde(rename = "sourcemap")] + SourceMap(&'a SourceMapDebugImage), + } + + match self { + Self::Apple(image) => TaggedDebugImage::Apple(image).serialize(serializer), + Self::Symbolic(image) => TaggedDebugImage::Symbolic(image).serialize(serializer), + Self::Proguard(image) => TaggedDebugImage::Proguard(image).serialize(serializer), + Self::Wasm(image) => TaggedDebugImage::Wasm(image).serialize(serializer), + Self::SourceMap(image) => TaggedDebugImage::SourceMap(image).serialize(serializer), + Self::Other(image) => image.serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for DebugImage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + let Value::Object(mut image) = value else { + return Err(de::Error::custom("debug image must be an object")); + }; + let type_name = image + .get("type") + .ok_or_else(|| de::Error::missing_field("type"))? + .as_str() + .ok_or_else(|| de::Error::custom("debug image type must be a string"))? + .to_owned(); + + match type_name.as_str() { + "apple" => deserialize_debug_image(&mut image, Self::Apple), + "symbolic" => deserialize_debug_image(&mut image, Self::Symbolic), + "proguard" => deserialize_debug_image(&mut image, Self::Proguard), + "wasm" => deserialize_debug_image(&mut image, Self::Wasm), + "sourcemap" => deserialize_debug_image(&mut image, Self::SourceMap), + _ => Ok(Self::Other(image.into_iter().collect())), + } + .map_err(de::Error::custom) + } +} + +fn deserialize_debug_image( + image: &mut serde_json::Map, + wrap: impl FnOnce(T) -> DebugImage, +) -> serde_json::Result +where + T: serde::de::DeserializeOwned, +{ + image.remove("type"); + serde_json::from_value(Value::Object(std::mem::take(image))).map(wrap) } impl DebugImage { @@ -931,6 +1000,11 @@ impl DebugImage { DebugImage::Symbolic(..) => "symbolic", DebugImage::Proguard(..) => "proguard", DebugImage::Wasm(..) => "wasm", + DebugImage::SourceMap(..) => "sourcemap", + DebugImage::Other(ref image) => image + .get("type") + .and_then(Value::as_str) + .unwrap_or("unknown"), } } } @@ -1036,10 +1110,26 @@ pub struct WasmDebugImage { pub code_file: String, } +/// Represents a source map debug image. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SourceMapDebugImage { + /// The absolute path or URL to the minified JavaScript file. + pub code_file: String, + /// Unique debug identifier of the source map. + pub debug_id: DebugId, + /// Path or URL to the associated source map. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub debug_file: Option, + /// Additional arbitrary fields for forwards compatibility. + #[serde(flatten)] + pub other: Map, +} + into_debug_image!(Apple, AppleDebugImage); into_debug_image!(Symbolic, SymbolicDebugImage); into_debug_image!(Proguard, ProguardDebugImage); into_debug_image!(Wasm, WasmDebugImage); +into_debug_image!(SourceMap, SourceMapDebugImage); /// Represents debug meta information. #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] diff --git a/sentry-types/tests/test_protocol_v7.rs b/sentry-types/tests/test_protocol_v7.rs index 841cf19d1..fb1797b72 100644 --- a/sentry-types/tests/test_protocol_v7.rs +++ b/sentry-types/tests/test_protocol_v7.rs @@ -815,6 +815,49 @@ mod test_debug_meta { \"8c954262-f905-4992-8a61-f60825f4553b\"}]}}" ); } + + #[test] + fn test_sourcemap_debug_image_roundtrip() { + let json = serde_json::json!({ + "type": "sourcemap", + "code_file": "https://example.com/static/app.min.js", + "debug_id": "395835f4-03e0-4436-80d3-136f0749a893", + "debug_file": "https://example.com/static/app.min.js.map", + "source": "browser", + }); + + let image: v7::DebugImage = serde_json::from_value(json.clone()).unwrap(); + + let v7::DebugImage::SourceMap(ref source_map) = image else { + panic!("expected a source map debug image"); + }; + assert!(!source_map.other.contains_key("type")); + assert_eq!(image.type_name(), "sourcemap"); + assert_eq!( + serde_json::to_string(&image) + .unwrap() + .match_indices("\"type\"") + .count(), + 1 + ); + assert_eq!(serde_json::to_value(image).unwrap(), json); + } + + #[test] + fn test_unknown_debug_image_roundtrip() { + let json = serde_json::json!({ + "type": "future_image", + "debug_id": "395835f4-03e0-4436-80d3-136f0749a893", + "metadata": { + "revision": 2, + }, + }); + + let image: v7::DebugImage = serde_json::from_value(json.clone()).unwrap(); + + assert_eq!(image.type_name(), "future_image"); + assert_eq!(serde_json::to_value(image).unwrap(), json); + } } mod test_exception {