Skip to content

Commit fb19fa4

Browse files
feat(debugId): Serialize source maps with debugId, not debug_id (#134)
The debug ID field on all source maps is now **serialized as `debugId` rather than `debug_id`**. To maintain backwards-compatibility with source maps that contain a `debug_id` field, we can still read the `debug_id` field. If a source map contains a `debugId` field and a `debug_id` field, **the `debugId` field takes precedence, which is a change from the old behavior**. Corresponding Sentry CLI PR: getsentry/sentry-cli#3005 Closes #96 Closes [CLI-240](https://linear.app/getsentry/issue/CLI-240/turn-debug-id-field-to-debugid)
1 parent f96e3d1 commit fb19fa4

File tree

6 files changed

+164
-58
lines changed

6 files changed

+164
-58
lines changed

src/decoder.rs

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,7 @@ pub fn decode_regular(rsm: RawSourceMap) -> Result<SourceMap> {
267267

268268
let mut sm = SourceMap::new(file, tokens, names, sources, source_content);
269269
sm.set_source_root(rsm.source_root);
270-
// Use _debug_id_new (from "debugId" key) only if debug_id
271-
// from ( "debug_id" key) is unset
272-
sm.set_debug_id(rsm.debug_id.or(rsm._debug_id_new));
270+
sm.set_debug_id(rsm.debug_id.into());
273271
if let Some(ignore_list) = rsm.ignore_list {
274272
for idx in ignore_list {
275273
sm.add_to_ignore_list(idx);
@@ -307,7 +305,7 @@ fn decode_index(rsm: RawSourceMap) -> Result<SourceMapIndex> {
307305
rsm.x_facebook_offsets,
308306
rsm.x_metro_module_paths,
309307
)
310-
.with_debug_id(rsm._debug_id_new.or(rsm.debug_id)))
308+
.with_debug_id(rsm.debug_id.into()))
311309
}
312310

313311
fn decode_common(rsm: RawSourceMap) -> Result<DecodedMap> {
@@ -419,8 +417,7 @@ mod tests {
419417
x_facebook_offsets: None,
420418
x_metro_module_paths: None,
421419
x_facebook_sources: None,
422-
debug_id: None,
423-
_debug_id_new: None,
420+
debug_id: None.into(),
424421
};
425422

426423
let decoded = decode_common(raw).expect("should decoded");
@@ -448,40 +445,7 @@ mod tests {
448445
x_facebook_offsets: None,
449446
x_metro_module_paths: None,
450447
x_facebook_sources: None,
451-
debug_id: None,
452-
_debug_id_new: Some(DEBUG_ID.parse().expect("valid debug id")),
453-
};
454-
455-
let decoded = decode_common(raw).expect("should decode");
456-
assert_eq!(
457-
decoded,
458-
DecodedMap::Index(
459-
SourceMapIndex::new(Some("test.js".into()), vec![])
460-
.with_debug_id(Some(DEBUG_ID.parse().expect("valid debug id")))
461-
)
462-
);
463-
}
464-
465-
#[test]
466-
fn test_decode_sourcemap_index_debug_id_from_legacy_key() {
467-
const DEBUG_ID: &str = "0123456789abcdef0123456789abcdef";
468-
469-
let raw = RawSourceMap {
470-
version: Some(3),
471-
file: Some("test.js".into()),
472-
sources: None,
473-
source_root: None,
474-
sources_content: None,
475-
sections: Some(vec![]),
476-
names: None,
477-
range_mappings: None,
478-
mappings: None,
479-
ignore_list: None,
480-
x_facebook_offsets: None,
481-
x_metro_module_paths: None,
482-
x_facebook_sources: None,
483-
debug_id: Some(DEBUG_ID.parse().expect("valid debug id")),
484-
_debug_id_new: None,
448+
debug_id: Some(DEBUG_ID.parse().expect("valid debug id")).into(),
485449
};
486450

487451
let decoded = decode_common(raw).expect("should decode");

src/encoder.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,7 @@ impl Encodable for SourceMap {
178178
x_facebook_offsets: None,
179179
x_metro_module_paths: None,
180180
x_facebook_sources: None,
181-
debug_id: self.get_debug_id(),
182-
_debug_id_new: None,
181+
debug_id: self.get_debug_id().into(),
183182
}
184183
}
185184
}
@@ -213,9 +212,7 @@ impl Encodable for SourceMapIndex {
213212
x_facebook_offsets: None,
214213
x_metro_module_paths: None,
215214
x_facebook_sources: None,
216-
debug_id: None,
217-
// Put the debug ID on _debug_id_new to serialize it to the debugId field.
218-
_debug_id_new: self.debug_id(),
215+
debug_id: self.debug_id().into(),
219216
}
220217
}
221218
}
@@ -278,8 +275,7 @@ mod tests {
278275
x_facebook_offsets: None,
279276
x_metro_module_paths: None,
280277
x_facebook_sources: None,
281-
debug_id: None,
282-
_debug_id_new: None,
278+
debug_id: None.into(),
283279
}
284280
);
285281
}
@@ -308,8 +304,7 @@ mod tests {
308304
x_facebook_offsets: None,
309305
x_metro_module_paths: None,
310306
x_facebook_sources: None,
311-
debug_id: None,
312-
_debug_id_new: Some(DEBUG_ID.parse().expect("valid debug id")),
307+
debug_id: Some(DEBUG_ID.parse().expect("valid debug id")).into(),
313308
}
314309
);
315310
}

src/jsontypes.rs

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use debugid::DebugId;
22
use serde::de::IgnoredAny;
3-
use serde::{Deserialize, Serialize};
3+
use serde::{Deserialize, Deserializer, Serialize};
44
use serde_json::Value;
5+
use std::fmt::Debug;
56

67
#[derive(Serialize, Deserialize, PartialEq, Debug)]
78
pub struct RawSectionOffset {
@@ -54,12 +55,8 @@ pub struct RawSourceMap {
5455
pub x_metro_module_paths: Option<Vec<String>>,
5556
#[serde(skip_serializing_if = "Option::is_none")]
5657
pub x_facebook_sources: FacebookSources,
57-
#[serde(skip_serializing_if = "Option::is_none")]
58-
pub debug_id: Option<DebugId>,
59-
// This field only exists to be able to deserialize from "debugId" keys
60-
// if "debug_id" is unset.
61-
#[serde(skip_serializing_if = "Option::is_none", rename = "debugId")]
62-
pub(crate) _debug_id_new: Option<DebugId>,
58+
#[serde(flatten)]
59+
pub debug_id: DebugIdField,
6360
}
6461

6562
#[derive(Deserialize)]
@@ -75,3 +72,91 @@ pub struct MinimalRawSourceMap {
7572
pub names: Option<IgnoredAny>,
7673
pub mappings: Option<IgnoredAny>,
7774
}
75+
76+
/// This struct represents a `RawSourceMap`'s debug ID fields.
77+
///
78+
/// The reason this exists as a seperate struct is so that we can have custom deserialization
79+
/// logic, which can read both the legacy snake_case debug_id and the new camelCase debugId
80+
/// fields. In case both are provided, the camelCase field takes precedence.
81+
///
82+
/// The field is always serialized as `debugId`.
83+
#[derive(Serialize, Clone, PartialEq, Debug, Default)]
84+
pub(crate) struct DebugIdField {
85+
#[serde(rename = "debugId", skip_serializing_if = "Option::is_none")]
86+
value: Option<DebugId>,
87+
}
88+
89+
impl<'de> Deserialize<'de> for DebugIdField {
90+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91+
where
92+
D: Deserializer<'de>,
93+
{
94+
// We cannot use serde(alias), as that would cause an error when both fields are present.
95+
96+
#[derive(Deserialize)]
97+
struct Helper {
98+
#[serde(rename = "debugId")]
99+
camel: Option<DebugId>,
100+
#[serde(rename = "debug_id")]
101+
legacy: Option<DebugId>,
102+
}
103+
104+
let Helper { camel, legacy } = Helper::deserialize(deserializer)?;
105+
Ok(camel.or(legacy).into())
106+
}
107+
}
108+
109+
impl From<Option<DebugId>> for DebugIdField {
110+
fn from(value: Option<DebugId>) -> Self {
111+
Self { value }
112+
}
113+
}
114+
115+
impl From<DebugIdField> for Option<DebugId> {
116+
fn from(value: DebugIdField) -> Self {
117+
value.value
118+
}
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
use serde_json::json;
125+
126+
fn parse_debug_id(input: &str) -> DebugId {
127+
input.parse().expect("valid debug id")
128+
}
129+
130+
fn empty_sourcemap() -> RawSourceMap {
131+
serde_json::from_value::<RawSourceMap>(serde_json::json!({}))
132+
.expect("can deserialize empty JSON to RawSourceMap")
133+
}
134+
135+
#[test]
136+
fn raw_sourcemap_serializes_camel_case_debug_id() {
137+
let camel = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
138+
let raw = RawSourceMap {
139+
debug_id: Some(parse_debug_id(camel)).into(),
140+
..empty_sourcemap()
141+
};
142+
143+
let value = serde_json::to_value(raw).expect("should serialize without error");
144+
let obj = value.as_object().expect("should be an object");
145+
assert!(obj.get("debug_id").is_none());
146+
assert_eq!(obj.get("debugId"), Some(&json!(parse_debug_id(camel))));
147+
}
148+
149+
#[test]
150+
fn raw_sourcemap_prefers_camel_case_on_deserialize() {
151+
let legacy = "ffffffffffffffffffffffffffffffff";
152+
let camel = "00000000000000000000000000000000";
153+
let json = serde_json::json!({
154+
"debug_id": legacy,
155+
"debugId": camel
156+
});
157+
let raw: RawSourceMap =
158+
serde_json::from_value(json).expect("can deserialize as RawSourceMap");
159+
let value: Option<DebugId> = raw.debug_id.into();
160+
assert_eq!(value, Some(parse_debug_id(camel)));
161+
}
162+
}

src/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,8 +1441,8 @@ mod tests {
14411441
"sources":["coolstuff.js"],
14421442
"names":["x","alert"],
14431443
"mappings":"AAAA,GAAIA,GAAI,EACR,IAAIA,GAAK,EAAG,CACVC,MAAM",
1444-
"debug_id":"00000000-0000-0000-0000-000000000000",
1445-
"debugId": "11111111-1111-1111-1111-111111111111"
1444+
"debug_id": "11111111-1111-1111-1111-111111111111",
1445+
"debugId":"00000000-0000-0000-0000-000000000000"
14461446
}"#;
14471447

14481448
let sm = SourceMap::from_slice(input).unwrap();

tests/test_encoder.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,35 @@ fn test_empty_range() {
6262
let out = String::from_utf8(out).unwrap();
6363
assert!(!out.contains("rangeMappings"));
6464
}
65+
66+
#[test]
67+
fn test_sourcemap_serializes_camel_case_debug_id() {
68+
const DEBUG_ID: &str = "0123456789abcdef0123456789abcdef";
69+
let input = format!(
70+
r#"{{
71+
"version": 3,
72+
"sources": [],
73+
"names": [],
74+
"mappings": "",
75+
"debug_id": "{}"
76+
}}"#,
77+
DEBUG_ID
78+
);
79+
80+
let sm = SourceMap::from_reader(input.as_bytes()).unwrap();
81+
let expected = sm.get_debug_id().expect("debug id parsed").to_string();
82+
let mut out: Vec<u8> = vec![];
83+
sm.to_writer(&mut out).unwrap();
84+
let serialized = String::from_utf8(out).unwrap();
85+
86+
assert!(
87+
serialized.contains(&format!(r#""debugId":"{}""#, expected)),
88+
"expected camelCase debugId in {}",
89+
serialized
90+
);
91+
assert!(
92+
!serialized.contains("debug_id"),
93+
"unexpected snake_case key in {}",
94+
serialized
95+
);
96+
}

tests/test_index.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,33 @@ fn test_flatten_indexed_sourcemap_with_ignore_list() {
205205
vec![1]
206206
);
207207
}
208+
209+
#[test]
210+
fn test_sourcemap_index_serializes_camel_case_debug_id() {
211+
const DEBUG_ID: &str = "fedcba9876543210fedcba9876543210";
212+
let input = format!(
213+
r#"{{
214+
"version": 3,
215+
"file": "bundle.js",
216+
"sections": [],
217+
"debugId": "{}"
218+
}}"#,
219+
DEBUG_ID
220+
);
221+
222+
let smi = SourceMapIndex::from_reader(input.as_bytes()).unwrap();
223+
let mut out = Vec::new();
224+
smi.to_writer(&mut out).unwrap();
225+
let serialized = String::from_utf8(out).unwrap();
226+
227+
assert!(
228+
serialized.contains(r#""debugId":"#),
229+
"expected camelCase debugId in {}",
230+
serialized
231+
);
232+
assert!(
233+
!serialized.contains("debug_id"),
234+
"unexpected snake_case key in {}",
235+
serialized
236+
);
237+
}

0 commit comments

Comments
 (0)