//! HTML report generator for diagnostics plugin
//!
//! Generates HTML reports with embedded diagnostic data and JavaScript visualizations.
//! Supports two modes with different resource loading strategies:
//!
//! - **Dashboard mode**: Live interactive dashboard
//! - Links to external CSS via `<link>` tag
//! - Links to external JS files via `<script src="...">`
//! - Loads data dynamically from API endpoints
//!
//! - **Embedded mode**: Self-contained portable report
//! - Embeds CSS inline in `<style>` tag
//! - Embeds JS inline in `<script>` tags
//! - Embeds all data as base64 in JavaScript variables
//!
//! The generator uses a template-based approach with injection points for:
//! - CSS styles (linked externally or embedded inline)
//! - JavaScript visualization libraries (flamegraphs, call graphs, heap profiling)
//! - Diagnostic data (system info, router config, supergraph schema, memory dumps)
use base64::Engine;
use crate::plugins::diagnostics::DiagnosticsError;
use crate::plugins::diagnostics::DiagnosticsResult;
use crate::plugins::diagnostics::memory::MemoryDump;
/// Embedded Tailwind CSS
const TAILWIND_CSS: &str = include_str!("resources/styles.css");
/// Parameters for generating diagnostic reports
#[derive(Debug, Default)]
pub(crate) struct ReportData<'a> {
/// System information content
pub system_info: Option<&'a str>,
/// Router configuration content
pub router_config: Option<&'a str>,
/// Supergraph schema content
pub supergraph_schema: Option<&'a str>,
/// Memory dump data
pub memory_dumps: &'a [MemoryDump],
}
impl<'a> ReportData<'a> {
/// Create a new ReportData with all fields
pub(super) fn new(
system_info: Option<&'a str>,
router_config: Option<&'a str>,
supergraph_schema: Option<&'a str>,
memory_dumps: &'a [MemoryDump],
) -> Self {
Self {
system_info,
router_config,
supergraph_schema,
memory_dumps,
}
}
}
/// HTML report generator that creates self-contained diagnostic reports
pub(crate) struct HtmlGenerator {
template: String,
}
impl HtmlGenerator {
/// Create a new HTML generator by loading the template
pub(crate) fn new() -> DiagnosticsResult<Self> {
// Load the HTML template from the resources directory
let template = include_str!("resources/template.html").to_string();
Ok(Self { template })
}
/// Generate dashboard HTML with separate script resources (for live mode)
pub(crate) fn generate_dashboard_html(&self) -> DiagnosticsResult<String> {
let mut html = self.template.clone();
// Inject script tags pointing to separate files
let script_injection = r#"
<script src="/diagnostics/custom_elements.js"></script>
<script src="/diagnostics/backtrace-processor.js"></script>
<script src="/diagnostics/viz-js-integration.js"></script>
<script src="/diagnostics/flamegraph-renderer.js"></script>
<script src="/diagnostics/callgraph-svg-renderer.js"></script>
<script src="/diagnostics/data-access.js"></script>
<script src="/diagnostics/main.js"></script>"#;
// Inject dashboard mode configuration
let data_injection = r#"
<script>
const IS_DASHBOARD_MODE = true;
const EMBEDDED_DATA = null;
</script>"#;
// Link to external CSS file for dashboard mode
let styles_injection = r#"<link rel="stylesheet" href="/diagnostics/styles.css">"#;
html = html.replace("<!-- STYLES_INJECTION_POINT -->", styles_injection);
html = html.replace("<!-- SCRIPT_INJECTION_POINT -->", script_injection);
html = html.replace("<!-- DATA_INJECTION_POINT -->", data_injection);
Ok(html)
}
/// Generate a complete HTML report with embedded data (for embedded mode)
pub(crate) fn generate_embedded_html(&self, data: ReportData<'_>) -> DiagnosticsResult<String> {
let mut html = self.template.clone();
// Build embedded script injection
let script_injection = self.build_embedded_scripts()?;
// Build data injection
let data_injection = self.build_data_injection(data)?;
// Inject styles
let styles_injection = format!("<style>{}</style>", TAILWIND_CSS);
html = html.replace("<!-- STYLES_INJECTION_POINT -->", &styles_injection);
// Perform injections
html = html.replace("<!-- SCRIPT_INJECTION_POINT -->", &script_injection);
html = html.replace("<!-- DATA_INJECTION_POINT -->", &data_injection);
Ok(html)
}
/// Build embedded script tags with inline JavaScript content
fn build_embedded_scripts(&self) -> DiagnosticsResult<String> {
let js_files = [
include_str!("resources/custom_elements.js"),
include_str!("resources/backtrace-processor.js"),
include_str!("resources/viz-js-integration.js"),
include_str!("resources/flamegraph-renderer.js"),
include_str!("resources/callgraph-svg-renderer.js"),
include_str!("resources/data-access.js"),
include_str!("resources/main.js"),
];
let scripts = js_files
.iter()
.map(|content| format!("\n <script>\n{content}\n </script>"))
.collect::<String>();
Ok(scripts)
}
/// Build data injection script with embedded data
fn build_data_injection(&self, data: ReportData<'_>) -> DiagnosticsResult<String> {
// Encode data as base64
let system_info = data
.system_info
.map(|s| base64::engine::general_purpose::STANDARD.encode(s))
.unwrap_or_default();
let router_config = data
.router_config
.map(|s| base64::engine::general_purpose::STANDARD.encode(s))
.unwrap_or_default();
let schema = data
.supergraph_schema
.map(|s| base64::engine::general_purpose::STANDARD.encode(s))
.unwrap_or_default();
// Serialize memory dumps
let memory_dumps_json = serde_json::to_string(data.memory_dumps).map_err(|e| {
DiagnosticsError::Internal(format!("Failed to serialize memory dumps: {}", e))
})?;
// Build the injection script
let injection = format!(
r#"
<script>
const IS_DASHBOARD_MODE = false;
const EMBEDDED_DATA = {{
systemInfo: '{}',
routerConfig: '{}',
schema: '{}',
memoryDumps: {}
}};
</script>"#,
system_info, router_config, schema, memory_dumps_json
);
Ok(injection)
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
use crate::plugins::diagnostics::memory;
#[tokio::test]
async fn test_html_generator_creation() {
let generator = HtmlGenerator::new();
assert!(generator.is_ok());
let generator = generator.unwrap();
assert!(
generator
.template
.contains("<!-- SCRIPT_INJECTION_POINT -->")
);
assert!(generator.template.contains("<!-- DATA_INJECTION_POINT -->"));
}
#[tokio::test]
async fn test_process_empty_memory_directory() {
let temp_dir = tempdir().unwrap();
let result = memory::load_memory_dumps(temp_dir.path()).await;
assert!(result.is_ok());
let dumps = result.unwrap();
assert!(dumps.is_empty());
}
#[tokio::test]
async fn test_generate_report_basic() {
let generator = HtmlGenerator::new().unwrap();
let _temp_dir = tempdir().unwrap();
let report_data = ReportData::new(
Some("System info content"),
Some("Router config content"),
Some("Schema content"),
&[], // empty memory dumps
);
let html = generator.generate_embedded_html(report_data);
assert!(html.is_ok());
let html_content = html.unwrap();
// Verify injection points were replaced
assert!(!html_content.contains("<!-- SCRIPT_INJECTION_POINT -->"));
assert!(!html_content.contains("<!-- DATA_INJECTION_POINT -->"));
// Verify it contains base64 encoded data
assert!(
html_content
.contains(&base64::engine::general_purpose::STANDARD.encode("System info content"))
);
// Verify embedded data structure
assert!(html_content.contains("const IS_DASHBOARD_MODE = false"));
assert!(html_content.contains("const EMBEDDED_DATA = {"));
// Verify Tailwind CSS is embedded
assert!(
html_content.contains("<style>") && html_content.contains("tailwindcss"),
"Tailwind CSS should be embedded in <style> tag"
);
assert!(
!html_content.contains("<!-- STYLES_INJECTION_POINT -->"),
"STYLES_INJECTION_POINT should be replaced"
);
}
#[tokio::test]
async fn test_generate_complete_html_report_with_mock_data() {
// Test that generates a complete HTML report using predictable mock data
let generator = HtmlGenerator::new().unwrap();
// Use predictable mock data instead of filesystem fixtures
let mock_system_info = "SYSTEM INFORMATION\n==================\n\nOperating System: linux (linux)\nArchitecture: x86_64 (amd64)\nTarget Family: unix\nProcess ID: 12345\nRouter Version: 2.5.0\n\nMEMORY INFORMATION\n------------------\nTotal Memory: 16.00 GB (17179869184 bytes)\nAvailable Memory: 8.00 GB (8589934592 bytes)\n\nCPU INFORMATION\n---------------\nPhysical CPU cores: 8\nCPU Model: Test CPU\n\nSYSTEM LOAD\n-----------\nLoad Average (1min): 1.50\nLoad per CPU (1min): 0.19 (18.8% utilization)\n\nRELEVANT ENVIRONMENT VARIABLES\n------------------------------\nNo relevant Apollo environment variables set";
let mock_router_config = r#"server:
listen: 127.0.0.1:4000
experimental_diagnostics:
enabled: true
listen: 127.0.0.1:8089
telemetry:
exporters:
tracing:
common:
enabled: true"#;
let mock_schema = r#"type Query {
me: User
topProducts(first: Int = 5): [Product]
}
type User @key(fields: "id") {
id: ID!
username: String
reviews: [Review]
}
type Product @key(fields: "upc") {
upc: String!
name: String
price: Int
reviews: [Review]
}
type Review @key(fields: "id") {
id: ID!
body: String
author: User @provides(fields: "username")
product: Product
}"#;
// Create mock memory dumps data
let mock_memory_dumps = vec![
MemoryDump {
name: "router_heap_dump_1234567890.prof".to_string(),
data: base64::engine::general_purpose::STANDARD.encode("heap profile: 1024: 8192 [ 1024: 8192] @ 0x1234 0x5678\n\nMAPPED_LIBRARIES:\n7f0000000000-7f0000001000 r-xp 00000000 08:01 123456 /usr/bin/router\n\n"),
size: 150,
timestamp: Some(1704110400), // 2024-01-01 12:00:00 UTC
}
];
// Generate HTML report
let report_data = ReportData::new(
Some(mock_system_info),
Some(mock_router_config),
Some(mock_schema),
&mock_memory_dumps,
);
let html_content = generator.generate_embedded_html(report_data).unwrap();
// Verify HTML structure (basic HTML validity)
assert!(html_content.starts_with("<!DOCTYPE html>"));
assert!(html_content.contains("<html"));
assert!(html_content.contains("</html>"));
assert!(html_content.contains("<head>"));
// Verify embedded content is present (base64 encoded)
let system_info_b64 = base64::engine::general_purpose::STANDARD.encode(mock_system_info);
assert!(html_content.contains(&system_info_b64));
let config_b64 = base64::engine::general_purpose::STANDARD.encode(mock_router_config);
assert!(html_content.contains(&config_b64));
let schema_b64 = base64::engine::general_purpose::STANDARD.encode(mock_schema);
assert!(html_content.contains(&schema_b64));
// Verify JavaScript components are embedded
assert!(html_content.contains("HeapProfileParser"));
assert!(html_content.contains("renderCallGraphWithVizJS"));
assert!(html_content.contains("renderFlameGraph"));
// Verify tab structure exists
assert!(html_content.contains("showTab"));
assert!(html_content.contains("data-tab="));
// Verify injection points were replaced
assert!(
!html_content.contains("<!-- SCRIPT_INJECTION_POINT -->"),
"SCRIPT_INJECTION_POINT should be replaced"
);
assert!(
!html_content.contains("<!-- DATA_INJECTION_POINT -->"),
"DATA_INJECTION_POINT should be replaced"
);
// Verify mode and data structure
assert!(html_content.contains("const IS_DASHBOARD_MODE = false"));
assert!(html_content.contains("const EMBEDDED_DATA = {"));
// Verify memory dump processing worked with our mock file
// Should contain JSON structure for memory dumps
assert!(html_content.contains("\"name\""));
assert!(html_content.contains("\"data\""));
assert!(html_content.contains("router_heap_dump_1234567890.prof"));
// Verify the HTML is substantial (contains all embedded content)
assert!(
html_content.len() > 10000,
"HTML should be substantial with embedded JavaScript and data"
);
// Verify Tailwind CSS is embedded
assert!(
html_content.contains("<style>") && html_content.contains("tailwindcss"),
"Tailwind CSS should be embedded in <style> tag"
);
assert!(
!html_content.contains("<!-- STYLES_INJECTION_POINT -->"),
"STYLES_INJECTION_POINT should be replaced"
);
assert!(
!html_content.contains("cdn.tailwindcss.com"),
"Should not contain Tailwind CDN reference"
);
}
}