Skip to content

Commit 7a21202

Browse files
committed
Fix TraceTransaction deserialization: API returns snake_case, not camelCase
- Remove #[serde(rename_all = "camelCase")] from TraceTransaction - Fix field renames: span.op → transaction.op, span.duration → transaction.duration - Make span_description and span_status optional with #[serde(default)] - Add missing fields: parent_event_id, generation, profiler_id, performance_issues - Update all tests to use snake_case field names matching real API - Add reproduction test with actual API response format - Bump version to 0.2.5
1 parent 2185c6a commit 7a21202

6 files changed

Lines changed: 83 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sentry-mcp"
3-
version = "0.2.4"
3+
version = "0.2.5"
44
edition = "2024"
55
license = "MIT"
66
repository = "https://github.com/utapyngo/sentry-mcp-rs"

src/api_client.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,13 @@ pub struct TraceResponse {
123123
}
124124

125125
#[derive(Debug, Clone, Deserialize)]
126-
#[serde(rename_all = "camelCase")]
127126
#[allow(dead_code)]
128127
pub struct TraceTransaction {
129128
pub event_id: String,
130129
pub project_id: i64,
131130
pub project_slug: String,
132131
pub transaction: String,
133-
#[serde(rename = "start_timestamp")]
134132
pub start_timestamp: f64,
135-
#[serde(rename = "sdk.name")]
136133
pub sdk_name: Option<String>,
137134
pub timestamp: f64,
138135
#[serde(default)]
@@ -141,14 +138,20 @@ pub struct TraceTransaction {
141138
pub errors: Vec<serde_json::Value>,
142139
pub span_id: Option<String>,
143140
pub parent_span_id: Option<String>,
144-
#[serde(rename = "span.op")]
141+
pub parent_event_id: Option<String>,
142+
#[serde(default)]
143+
pub generation: i32,
144+
pub profiler_id: Option<String>,
145+
#[serde(default)]
146+
pub performance_issues: Vec<serde_json::Value>,
147+
#[serde(rename = "transaction.op")]
145148
pub span_op: Option<String>,
146-
#[serde(rename = "span.description")]
149+
#[serde(rename = "transaction.duration")]
150+
pub span_duration: Option<f64>,
151+
#[serde(default)]
147152
pub span_description: Option<String>,
148-
#[serde(rename = "span.status")]
153+
#[serde(default)]
149154
pub span_status: Option<String>,
150-
#[serde(rename = "span.duration")]
151-
pub span_duration: Option<f64>,
152155
}
153156

154157
#[derive(Debug, Serialize)]
@@ -404,9 +407,9 @@ mod tests {
404407
let mock_server = MockServer::start().await;
405408
let response = r#"{
406409
"transactions": [{
407-
"eventId": "tx1",
408-
"projectId": 1,
409-
"projectSlug": "test",
410+
"event_id": "tx1",
411+
"project_id": 1,
412+
"project_slug": "test",
410413
"transaction": "GET /api",
411414
"start_timestamp": 1704067200.0,
412415
"timestamp": 1704067201.0

tests/api_client_tests.rs

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,9 @@ fn test_trace_response_deserialize_empty() {
178178
#[test]
179179
fn test_trace_transaction_deserialize_minimal() {
180180
let json = json!({
181-
"eventId": "abc123",
182-
"projectId": 1,
183-
"projectSlug": "proj",
181+
"event_id": "abc123",
182+
"project_id": 1,
183+
"project_slug": "proj",
184184
"transaction": "test-tx",
185185
"start_timestamp": 1000.0,
186186
"timestamp": 1001.0
@@ -197,46 +197,42 @@ fn test_trace_transaction_deserialize_minimal() {
197197
#[test]
198198
fn test_trace_transaction_deserialize_full() {
199199
let json = json!({
200-
"eventId": "abc123",
201-
"projectId": 1,
202-
"projectSlug": "proj",
200+
"event_id": "abc123",
201+
"project_id": 1,
202+
"project_slug": "proj",
203203
"transaction": "test-tx",
204204
"start_timestamp": 1000.0,
205-
"sdk.name": "sentry.python",
205+
"sdk_name": "sentry.python",
206206
"timestamp": 1001.0,
207207
"children": [],
208208
"errors": [{"title": "Error"}],
209-
"spanId": "span1",
210-
"parentSpanId": "span0",
211-
"span.op": "http.request",
212-
"span.description": "GET /api",
213-
"span.status": "ok",
214-
"span.duration": 150.5
209+
"span_id": "span1",
210+
"parent_span_id": "span0",
211+
"transaction.op": "http.request",
212+
"transaction.duration": 150.5
215213
});
216214
let tx: TraceTransaction = serde_json::from_value(json).unwrap();
217215
assert_eq!(tx.sdk_name.as_deref(), Some("sentry.python"));
218216
assert_eq!(tx.span_id.as_deref(), Some("span1"));
219217
assert_eq!(tx.parent_span_id.as_deref(), Some("span0"));
220218
assert_eq!(tx.span_op.as_deref(), Some("http.request"));
221-
assert_eq!(tx.span_description.as_deref(), Some("GET /api"));
222-
assert_eq!(tx.span_status.as_deref(), Some("ok"));
223219
assert_eq!(tx.span_duration, Some(150.5));
224220
assert_eq!(tx.errors.len(), 1);
225221
}
226222

227223
#[test]
228224
fn test_trace_transaction_with_children() {
229225
let json = json!({
230-
"eventId": "parent",
231-
"projectId": 1,
232-
"projectSlug": "proj",
226+
"event_id": "parent",
227+
"project_id": 1,
228+
"project_slug": "proj",
233229
"transaction": "parent-tx",
234230
"start_timestamp": 1000.0,
235231
"timestamp": 1001.0,
236232
"children": [{
237-
"eventId": "child",
238-
"projectId": 1,
239-
"projectSlug": "proj",
233+
"event_id": "child",
234+
"project_id": 1,
235+
"project_slug": "proj",
240236
"transaction": "child-tx",
241237
"start_timestamp": 1000.1,
242238
"timestamp": 1000.5,
@@ -248,3 +244,33 @@ fn test_trace_transaction_with_children() {
248244
assert_eq!(tx.children[0].event_id, "child");
249245
assert_eq!(tx.children[0].transaction, "child-tx");
250246
}
247+
248+
#[test]
249+
fn test_trace_transaction_actual_api_format() {
250+
let json = json!({
251+
"event_id": "4ff9a0a8138a447c9e0572a2eeff55d8",
252+
"span_id": "91958dc2ae005f54",
253+
"timestamp": 1771164551.832868,
254+
"transaction": "/api/wf__model__doc_type/pk/",
255+
"transaction.duration": 326,
256+
"transaction.op": "http.server",
257+
"project_id": 19,
258+
"project_slug": "platform_test_project",
259+
"parent_span_id": null,
260+
"parent_event_id": null,
261+
"generation": 0,
262+
"errors": [],
263+
"performance_issues": [],
264+
"start_timestamp": 1771164551.506479,
265+
"sdk_name": "sentry.python.django",
266+
"profiler_id": null,
267+
"children": []
268+
});
269+
let tx: TraceTransaction = serde_json::from_value(json).unwrap();
270+
assert_eq!(tx.event_id, "4ff9a0a8138a447c9e0572a2eeff55d8");
271+
assert_eq!(tx.project_id, 19);
272+
assert_eq!(tx.project_slug, "platform_test_project");
273+
assert_eq!(tx.span_op.as_deref(), Some("http.server"));
274+
assert_eq!(tx.span_duration, Some(326.0));
275+
assert_eq!(tx.sdk_name.as_deref(), Some("sentry.python.django"));
276+
}

tests/execute_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ fn make_trace() -> TraceResponse {
124124
span_duration: Some(1000.0),
125125
span_description: Some("GET /api/test".to_string()),
126126
span_status: Some("ok".to_string()),
127+
parent_event_id: None,
128+
generation: 0,
129+
profiler_id: None,
130+
performance_issues: vec![],
127131
}],
128132
orphan_errors: vec![],
129133
}

tests/get_trace_details_tests.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ fn make_tx(
3636
errors: vec![],
3737
span_id: Some("span1".to_string()),
3838
parent_span_id: None,
39+
parent_event_id: None,
40+
generation: 0,
41+
profiler_id: None,
42+
performance_issues: vec![],
3943
span_op: op.map(|s| s.to_string()),
44+
span_duration: duration,
4045
span_description: Some("test description".to_string()),
4146
span_status: Some("ok".to_string()),
42-
span_duration: duration,
4347
}
4448
}
4549

@@ -110,20 +114,20 @@ fn test_format_span_tree_with_depth() {
110114
}
111115

112116
#[test]
113-
fn test_format_span_tree_error_status() {
114-
let mut tx = make_tx(Some("http"), Some(100.0), vec![]);
115-
tx.span_status = Some("internal_error".to_string());
117+
fn test_format_span_tree_unknown_op() {
118+
let tx = make_tx(None, Some(100.0), vec![]);
116119
let mut output = String::new();
117120
format_span_tree(&tx, 0, &mut output);
118-
assert!(output.contains(""));
121+
assert!(output.contains("unknown"));
119122
}
120123

121124
#[test]
122-
fn test_format_span_tree_unknown_op() {
123-
let tx = make_tx(None, Some(100.0), vec![]);
125+
fn test_format_span_tree_error_status() {
126+
let mut tx = make_tx(Some("http"), Some(100.0), vec![]);
127+
tx.span_status = Some("internal_error".to_string());
124128
let mut output = String::new();
125129
format_span_tree(&tx, 0, &mut output);
126-
assert!(output.contains("unknown"));
130+
assert!(output.contains(""));
127131
}
128132

129133
#[test]
@@ -259,10 +263,10 @@ fn test_format_span_tree_deep_nesting() {
259263
format_span_tree(&root, 0, &mut output);
260264
let lines: Vec<&str> = output.lines().collect();
261265
assert_eq!(lines.len(), 4);
262-
assert!(lines[0].starts_with("✓"));
263-
assert!(lines[1].starts_with(" ✓"));
264-
assert!(lines[2].starts_with(" ✓"));
265-
assert!(lines[3].starts_with(" ✓"));
266+
assert!(lines[0].starts_with("✓ [root]"));
267+
assert!(lines[1].starts_with(" ✓ [level1]"));
268+
assert!(lines[2].starts_with(" ✓ [level2]"));
269+
assert!(lines[3].starts_with(" ✓ [level3]"));
266270
}
267271

268272
#[test]

0 commit comments

Comments
 (0)