use std::time::{Duration, UNIX_EPOCH};
use serde_json::{json, Value};
use crate::{
request::{
ArtifactTimingPhase, ArtifactTranscript, ArtifactTranscriptDirection,
ExecutionArtifact, ExecutionRecord, RequestFailure, RetainedArtifact,
},
};
use super::{common::assertion_results_json, text::describe_text, BodyReportOptions};
pub(crate) fn request_failure_json(
failure: &RequestFailure,
body_options: BodyReportOptions,
) -> Value {
let protocol_context = failure
.artifact()
.and_then(artifact_protocol_context_json)
.or_else(|| failure.protocol_context().cloned());
let started_at_unix_ms = failure
.started_at()
.and_then(|started_at| started_at.duration_since(UNIX_EPOCH).ok())
.map(|value| value.as_millis() as u64);
let duration_ms = failure.duration().map(|duration| duration.as_millis() as u64);
let transcripts = failure
.artifact()
.map(|artifact| {
artifact
.transcripts
.iter()
.map(|transcript| artifact_transcript_json(transcript, body_options))
.collect::<Vec<_>>()
})
.unwrap_or_default();
let retained_artifacts = failure
.artifact()
.map(|artifact| {
artifact
.retained_artifacts
.iter()
.map(|artifact| retained_artifact_json(artifact, body_options))
.collect::<Vec<_>>()
})
.unwrap_or_default();
let timing = failure
.duration()
.map(|duration| artifact_timing_json(duration, failure.artifact()));
json!({
"index": failure.index(),
"request": failure.request(),
"protocol": failure.protocol().map(|protocol| protocol.as_str()),
"protocolContext": protocol_context,
"kind": format!("{:?}", failure.kind()),
"message": failure.to_string(),
"startedAtUnixMs": started_at_unix_ms,
"durationMs": duration_ms,
"timing": timing,
"transcripts": transcripts,
"retainedArtifacts": retained_artifacts,
"assertions": assertion_results_json(failure.assertions()),
})
}
pub(crate) fn run_record_json(record: &ExecutionRecord, body_options: BodyReportOptions) -> Value {
let body = describe_text(&record.execution.output, body_options);
json!({
"index": record.index,
"description": record.description,
"method": record.method.as_str(),
"url": record.url,
"protocol": record.execution.artifact.protocol.as_str(),
"protocolContext": artifact_protocol_context_json(&record.execution.artifact),
"status": record.execution.artifact.http_status().map(|status| status.as_u16()),
"statusText": record.execution.artifact.http_status().and_then(|status| status.canonical_reason()),
"startedAtUnixMs": record.started_at.duration_since(UNIX_EPOCH).ok().map(|value| value.as_millis() as u64),
"durationMs": record.duration.as_millis() as u64,
"timing": artifact_timing_json(record.duration, Some(&record.execution.artifact)),
"body": body.value,
"bodyChars": body.char_count as u64,
"bodyCharLimit": body.char_limit.map(|value| value as u64),
"bodyTruncated": body.truncated,
"transcripts": record.execution.artifact.transcripts.iter().map(|transcript| artifact_transcript_json(transcript, body_options)).collect::<Vec<_>>(),
"retainedArtifacts": record.execution.artifact.retained_artifacts.iter().map(|artifact| retained_artifact_json(artifact, body_options)).collect::<Vec<_>>(),
"assertions": assertion_results_json(&record.execution.assertions),
})
}
fn artifact_transcript_json(transcript: &ArtifactTranscript, body_options: BodyReportOptions) -> Value {
let body = describe_text(&transcript.body, body_options);
json!({
"direction": artifact_transcript_direction_label(transcript.direction),
"label": transcript.label,
"body": body.value,
"bodyChars": body.char_count as u64,
"bodyCharLimit": body.char_limit.map(|value| value as u64),
"bodyTruncated": body.truncated,
"attributes": transcript.attributes,
})
}
fn retained_artifact_json(artifact: &RetainedArtifact, body_options: BodyReportOptions) -> Value {
let body = describe_text(&artifact.body, body_options);
json!({
"name": artifact.name,
"contentType": artifact.content_type,
"body": body.value,
"bodyChars": body.char_count as u64,
"bodyCharLimit": body.char_limit.map(|value| value as u64),
"bodyTruncated": body.truncated,
})
}
fn artifact_timing_json(total_duration: Duration, artifact: Option<&ExecutionArtifact>) -> Value {
let phases = artifact
.map(|artifact| {
artifact
.timing_phases
.iter()
.map(timing_phase_json)
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"totalMs": total_duration.as_millis() as u64,
"phases": phases,
})
}
fn timing_phase_json(phase: &ArtifactTimingPhase) -> Value {
json!({
"name": phase.name,
"durationMs": phase.duration.as_millis() as u64,
})
}
fn artifact_transcript_direction_label(direction: ArtifactTranscriptDirection) -> &'static str {
match direction {
ArtifactTranscriptDirection::Outgoing => "outgoing",
ArtifactTranscriptDirection::Incoming => "incoming",
}
}
fn artifact_protocol_context_json(artifact: &ExecutionArtifact) -> Option<Value> {
let outgoing = artifact
.transcripts
.iter()
.find(|transcript| transcript.direction == crate::request::ArtifactTranscriptDirection::Outgoing)?;
match artifact.protocol {
crate::request::RequestProtocol::Http => None,
crate::request::RequestProtocol::Graphql => {
let mut context = serde_json::Map::new();
if let Some(operation_name) = outgoing.attributes.get("operationName") {
context.insert(
"operationName".to_string(),
Value::String(operation_name.to_string()),
);
}
if let Some(variables) = outgoing.attributes.get("variables") {
match serde_json::from_str::<Value>(variables) {
Ok(value) => {
context.insert("variables".to_string(), value);
}
Err(_) => {
context.insert(
"variablesText".to_string(),
Value::String(variables.to_string()),
);
}
}
}
if context.is_empty() {
None
} else {
Some(Value::Object(context))
}
}
crate::request::RequestProtocol::Sse => {
let mut context = serde_json::Map::new();
if let Some(action) = outgoing.attributes.get("action") {
context.insert("action".to_string(), Value::String(action.to_string()));
}
if let Some(session_name) = outgoing.attributes.get("sessionName") {
context.insert(
"sessionName".to_string(),
Value::String(session_name.to_string()),
);
}
if let Some(within) = outgoing.attributes.get("within") {
context.insert("within".to_string(), Value::String(within.to_string()));
}
if let Some(incoming) = artifact
.transcripts
.iter()
.find(|transcript| transcript.direction == crate::request::ArtifactTranscriptDirection::Incoming)
{
if let Some(event) = incoming.attributes.get("event") {
context.insert("event".to_string(), Value::String(event.to_string()));
}
if let Some(id) = incoming.attributes.get("id") {
context.insert("id".to_string(), Value::String(id.to_string()));
}
}
if context.is_empty() {
None
} else {
Some(Value::Object(context))
}
}
crate::request::RequestProtocol::Ws => {
let mut context = serde_json::Map::new();
if let Some(action) = outgoing.attributes.get("action") {
context.insert("action".to_string(), Value::String(action.to_string()));
}
if let Some(session_name) = outgoing.attributes.get("sessionName") {
context.insert(
"sessionName".to_string(),
Value::String(session_name.to_string()),
);
}
if let Some(kind) = outgoing.attributes.get("kind") {
context.insert("kind".to_string(), Value::String(kind.to_string()));
}
if let Some(within) = outgoing.attributes.get("within") {
context.insert("within".to_string(), Value::String(within.to_string()));
}
if let Some(incoming) = artifact
.transcripts
.iter()
.find(|transcript| {
transcript.direction == crate::request::ArtifactTranscriptDirection::Incoming
})
{
if let Some(kind) = incoming.attributes.get("kind") {
context.insert("kind".to_string(), Value::String(kind.to_string()));
}
}
if context.is_empty() {
None
} else {
Some(Value::Object(context))
}
}
crate::request::RequestProtocol::Mcp => {
let mut context = serde_json::Map::new();
for (attribute, key) in [
("call", "call"),
("tool", "tool"),
("sessionName", "sessionName"),
("protocolVersion", "protocolVersion"),
("clientName", "clientName"),
("clientVersion", "clientVersion"),
] {
if let Some(value) = outgoing.attributes.get(attribute) {
context.insert(key.to_string(), Value::String(value.to_string()));
}
}
for (attribute, json_key, text_key) in [
("arguments", "arguments", "argumentsText"),
("capabilities", "capabilities", "capabilitiesText"),
] {
if let Some(value) = outgoing.attributes.get(attribute) {
match serde_json::from_str::<Value>(value) {
Ok(parsed) => {
context.insert(json_key.to_string(), parsed);
}
Err(_) => {
context.insert(text_key.to_string(), Value::String(value.to_string()));
}
}
}
}
if context.is_empty() {
None
} else {
Some(Value::Object(context))
}
}
}
}