use crate::correlate::Trace;
use crate::diff::DiffReport;
use crate::event::EventType;
use crate::ingest::pg_stat::PgStatReport;
use crate::normalize::NormalizedEvent;
use crate::report::Report;
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use std::path::Path;
const TEMPLATE: &str = include_str!("html_template.html");
const JSON_PLACEHOLDER: &str = "{{REPORT_JSON}}";
const TITLE_PLACEHOLDER: &str = "{{PAGE_TITLE}}";
const DEFAULT_TITLE: &str = "perf-sentinel report";
const DEFAULT_SIZE_TARGET_BYTES: usize = 5 * 1024 * 1024;
const PAYLOAD_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub input_label: String,
pub max_traces_embedded: Option<usize>,
pub pg_stat: Option<PgStatReport>,
pub diff: Option<DiffReport>,
}
#[must_use]
pub fn render(report: &Report, traces: &[Trace], options: &RenderOptions) -> String {
let sanitized_label = sanitize_input_label(&options.input_label);
let payload = build_payload_with_label(report, traces, options, &sanitized_label);
let json = serde_json::to_string(&payload).expect("payload always serializes");
let title = derive_page_title(&sanitized_label);
inject(&json, &title)
}
pub fn write(
report: &Report,
traces: &[Trace],
options: &RenderOptions,
output: &Path,
) -> std::io::Result<()> {
let html = render(report, traces, options);
std::fs::write(output, html)
}
#[derive(Debug, Serialize)]
struct Payload<'a> {
version: &'static str,
input_label: &'a str,
report: &'a Report,
embedded_traces: Vec<EmbeddedTrace<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
trimmed_traces: Option<TrimSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pg_stat: Option<&'a PgStatReport>,
#[serde(skip_serializing_if = "Option::is_none")]
diff: Option<&'a DiffReport>,
}
#[derive(Debug, Serialize)]
struct EmbeddedTrace<'a> {
trace_id: &'a str,
spans: Vec<EmbeddedSpan<'a>>,
}
#[derive(Debug, Serialize)]
struct EmbeddedSpan<'a> {
span_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
parent_span_id: Option<&'a str>,
service: &'a str,
endpoint: &'a str,
event_type: &'static str,
operation: &'a str,
target: &'a str,
template: &'a str,
duration_us: u64,
#[serde(skip_serializing_if = "Option::is_none")]
status_code: Option<u16>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
struct TrimSummary {
kept: usize,
total: usize,
}
fn inject(json: &str, title: &str) -> String {
let safe = json.replace("</", "<\\/");
TEMPLATE
.replacen(JSON_PLACEHOLDER, &safe, 1)
.replacen(TITLE_PLACEHOLDER, title, 1)
}
fn derive_page_title(input_label: &str) -> String {
let trimmed = input_label.trim();
if trimmed.is_empty() || trimmed == "-" {
return DEFAULT_TITLE.to_string();
}
let filename = Path::new(trimmed)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(trimmed);
format!("perf-sentinel: {}", html_escape_text(filename))
}
fn sanitize_input_label(input_label: &str) -> String {
input_label
.chars()
.filter(|c| !c.is_control() && !is_unsafe_format_char(*c))
.collect()
}
fn html_escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c if c.is_control() || is_unsafe_format_char(c) => {}
_ => out.push(c),
}
}
out
}
fn is_unsafe_format_char(c: char) -> bool {
matches!(
c,
'\u{200E}' | '\u{200F}' | '\u{2028}' | '\u{2029}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' | '\u{FEFF}' )
}
fn build_payload_with_label<'a>(
report: &'a Report,
traces: &'a [Trace],
options: &'a RenderOptions,
input_label: &'a str,
) -> Payload<'a> {
let ordered = order_candidates_by_iis(report, traces);
let total = ordered.len();
let (kept_refs, trimmed) = if let Some(cap) = options.max_traces_embedded {
let take = cap.min(total);
let summary = if take < total {
Some(TrimSummary { kept: take, total })
} else {
None
};
(ordered.into_iter().take(take).collect::<Vec<_>>(), summary)
} else {
trim_to_size_target(ordered, report, options, input_label)
};
let embedded_traces = kept_refs.iter().copied().map(embed_trace).collect();
Payload {
version: PAYLOAD_VERSION,
input_label,
report,
embedded_traces,
trimmed_traces: trimmed,
pg_stat: options.pg_stat.as_ref(),
diff: options.diff.as_ref(),
}
}
fn order_candidates_by_iis<'a>(report: &Report, traces: &'a [Trace]) -> Vec<&'a Trace> {
let finding_trace_ids: HashSet<&str> = report
.findings
.iter()
.map(|f| f.trace_id.as_str())
.collect();
let mut rank: HashMap<(&str, &str), usize> = HashMap::new();
for (i, off) in report.green_summary.top_offenders.iter().enumerate() {
rank.insert((off.service.as_str(), off.endpoint.as_str()), i);
}
let mut scored: Vec<(usize, &'a Trace)> = traces
.iter()
.filter(|t| finding_trace_ids.contains(t.trace_id.as_str()))
.map(|t| (trace_rank(t, &rank), t))
.collect();
scored.sort_by_key(|(score, _)| *score);
scored.into_iter().map(|(_, t)| t).collect()
}
fn trace_rank(trace: &Trace, rank: &HashMap<(&str, &str), usize>) -> usize {
trace
.spans
.iter()
.map(|s| {
rank.get(&(s.event.service.as_str(), s.event.source.endpoint.as_str()))
.copied()
.unwrap_or(usize::MAX)
})
.min()
.unwrap_or(usize::MAX)
}
fn trim_to_size_target<'a>(
ordered: Vec<&'a Trace>,
report: &Report,
options: &'a RenderOptions,
input_label: &'a str,
) -> (Vec<&'a Trace>, Option<TrimSummary>) {
let total = ordered.len();
let per_trace_lens: Vec<usize> = ordered
.iter()
.copied()
.map(|t| serde_json::to_string(&embed_trace(t)).map_or(usize::MAX, |s| s.len()))
.collect();
let envelope = Payload {
version: PAYLOAD_VERSION,
input_label,
report,
embedded_traces: Vec::new(),
trimmed_traces: Some(TrimSummary { kept: 0, total }),
pg_stat: options.pg_stat.as_ref(),
diff: options.diff.as_ref(),
};
let envelope_len = serde_json::to_string(&envelope).map_or(usize::MAX, |s| s.len());
let json_budget = DEFAULT_SIZE_TARGET_BYTES.saturating_sub(TEMPLATE.len());
let mut running = envelope_len;
let mut keep_count: usize = 0;
for &len in &per_trace_lens {
let delta = len.saturating_add(1);
let next = running.saturating_add(delta);
if next > json_budget {
break;
}
running = next;
keep_count += 1;
}
let kept: Vec<&'a Trace> = ordered.into_iter().take(keep_count).collect();
let trimmed = if kept.len() < total {
Some(TrimSummary {
kept: kept.len(),
total,
})
} else {
None
};
(kept, trimmed)
}
fn embed_trace(t: &Trace) -> EmbeddedTrace<'_> {
EmbeddedTrace {
trace_id: t.trace_id.as_str(),
spans: t.spans.iter().map(embed_span).collect(),
}
}
fn embed_span(e: &NormalizedEvent) -> EmbeddedSpan<'_> {
EmbeddedSpan {
span_id: e.event.span_id.as_str(),
parent_span_id: e.event.parent_span_id.as_deref(),
service: e.event.service.as_str(),
endpoint: e.event.source.endpoint.as_str(),
event_type: match e.event.event_type {
EventType::Sql => "sql",
EventType::HttpOut => "http_out",
},
operation: e.event.operation.as_str(),
target: e.event.target.as_str(),
template: e.template.as_str(),
duration_us: e.event.duration_us,
status_code: e.event.status_code,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::correlate::Trace;
use crate::detect::{Confidence, Finding, FindingType, Pattern, Severity};
use crate::event::{EventSource, EventType, SpanEvent};
use crate::ingest::IngestSource;
use crate::normalize::NormalizedEvent;
use crate::report::interpret::InterpretationLevel;
use crate::report::{Analysis, GreenSummary, QualityGate, Report, TopOffender};
fn span(
trace_id: &str,
span_id: &str,
parent: Option<&str>,
service: &str,
endpoint: &str,
template: &str,
) -> NormalizedEvent {
NormalizedEvent {
event: SpanEvent {
timestamp: "2026-04-21T00:00:00Z".into(),
trace_id: trace_id.into(),
span_id: span_id.into(),
parent_span_id: parent.map(ToString::to_string),
service: service.into(),
cloud_region: None,
event_type: EventType::Sql,
operation: "SELECT".into(),
target: template.into(),
duration_us: 1200,
source: EventSource {
endpoint: endpoint.into(),
method: "get".into(),
},
status_code: None,
response_size_bytes: None,
code_function: None,
code_filepath: None,
code_lineno: None,
code_namespace: None,
},
template: template.into(),
params: vec![],
}
}
fn finding(trace_id: &str, service: &str, endpoint: &str, template: &str) -> Finding {
Finding {
finding_type: FindingType::NPlusOneSql,
severity: Severity::Critical,
trace_id: trace_id.into(),
service: service.into(),
source_endpoint: endpoint.into(),
pattern: Pattern {
template: template.into(),
occurrences: 12,
window_ms: 100,
distinct_params: 12,
},
suggestion: "use JOIN FETCH".into(),
first_timestamp: "2026-04-21T00:00:00Z".into(),
last_timestamp: "2026-04-21T00:00:01Z".into(),
green_impact: None,
confidence: Confidence::CiBatch,
code_location: None,
suggested_fix: None,
}
}
fn minimal_report(findings: Vec<Finding>) -> Report {
Report {
analysis: Analysis {
duration_ms: 10,
events_processed: 1,
traces_analyzed: 1,
},
findings,
green_summary: GreenSummary {
total_io_ops: 10,
avoidable_io_ops: 4,
io_waste_ratio: 0.4,
io_waste_ratio_band: InterpretationLevel::Moderate,
top_offenders: vec![TopOffender {
endpoint: "/api/orders".into(),
service: "order-svc".into(),
io_intensity_score: 6.4,
io_intensity_band: InterpretationLevel::High,
co2_grams: Some(0.000_050),
}],
co2: None,
regions: vec![],
transport_gco2: None,
},
quality_gate: QualityGate {
passed: true,
rules: vec![],
},
per_endpoint_io_ops: vec![],
correlations: vec![],
}
}
fn opts(label: &str, cap: Option<usize>) -> RenderOptions {
RenderOptions {
input_label: label.into(),
max_traces_embedded: cap,
pg_stat: None,
diff: None,
}
}
#[test]
fn renders_minimal_report_to_valid_html() {
let path = format!(
"{}/../../tests/fixtures/report_minimal.json",
env!("CARGO_MANIFEST_DIR")
);
let raw = std::fs::read(&path).expect("fixture readable");
let cfg = crate::config::Config::default();
let events = crate::ingest::json::JsonIngest::new(cfg.max_payload_size)
.ingest(&raw)
.expect("fixture parses");
let (report, traces) = crate::pipeline::analyze_with_traces(events, &cfg);
assert_eq!(report.findings.len(), 2, "fixture must yield 2 findings");
let html = render(&report, &traces, &opts("report_minimal.json", None));
assert!(html.starts_with("<!DOCTYPE html>"));
assert!(html.contains(r#"<script id="report-data""#));
assert!(html.contains("trace-report-minimal"));
assert!(html.contains("order-svc"));
}
#[test]
fn escapes_closing_script_tag_in_embedded_json() {
let hostile = "</script><img src=x onerror=alert(1)>";
let f = finding("t1", "svc", "/ep", hostile);
let report = minimal_report(vec![f]);
let trace = Trace {
trace_id: "t1".into(),
spans: vec![span("t1", "s1", None, "svc", "/ep", hostile)],
};
let html = render(&report, &[trace], &opts("-", None));
assert_eq!(
html.matches("</script>").count(),
2,
"user-controlled </script> leaked into the document"
);
assert!(html.contains("<\\/script>"));
let start = html.find("<script id=\"report-data\"").expect("script tag");
let open = html[start..]
.find('>')
.expect("script open")
.saturating_add(1);
let rest = &html[start + open..];
let end = rest.find("</script>").expect("script close");
let json_blob = rest[..end].trim().replace("<\\/", "</");
let value: serde_json::Value =
serde_json::from_str(&json_blob).expect("JSON blob parses after <\\/ reversal");
let finding_tpl = value["report"]["findings"][0]["pattern"]["template"]
.as_str()
.expect("template present");
assert_eq!(finding_tpl, hostile);
}
#[test]
fn escapes_adversarial_control_chars() {
let weird = "a\0b\x01c\x7fd\u{1F600}";
let f = finding("t1", "svc", "/ep", weird);
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("traces.json", None));
let start = html.find("<script id=\"report-data\"").expect("script tag");
let open = html[start..]
.find('>')
.expect("script open")
.saturating_add(1);
let rest = &html[start + open..];
let end = rest.find("</script>").expect("script close");
let json_blob = rest[..end].trim().replace("<\\/", "</");
let value: serde_json::Value = serde_json::from_str(&json_blob).expect("JSON round-trips");
assert_eq!(
value["report"]["findings"][0]["pattern"]["template"]
.as_str()
.unwrap(),
weird
);
}
#[test]
fn applies_max_traces_embedded_cap_via_top_waste_fallback() {
let mut findings = Vec::new();
let mut traces = Vec::new();
let mut offenders = Vec::new();
for i in 0..100 {
let tid = format!("t{i:03}");
let svc = format!("svc-{i}");
let ep = format!("/ep-{i}");
let tpl = format!("SELECT * FROM t{i} WHERE id = ?");
findings.push(finding(&tid, &svc, &ep, &tpl));
traces.push(Trace {
trace_id: tid.clone(),
spans: vec![span(&tid, "s", None, &svc, &ep, &tpl)],
});
offenders.push(TopOffender {
endpoint: ep.clone(),
service: svc.clone(),
io_intensity_score: 100.0 - f64::from(i),
io_intensity_band: InterpretationLevel::High,
co2_grams: None,
});
}
let mut report = minimal_report(findings);
report.green_summary.top_offenders = offenders;
let html = render(&report, &traces, &opts("-", Some(10)));
let json_blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&json_blob).unwrap();
let embedded = value["embedded_traces"].as_array().expect("array");
assert_eq!(embedded.len(), 10, "exactly 10 traces kept");
let summary = &value["trimmed_traces"];
assert_eq!(summary["kept"].as_u64().unwrap(), 10);
assert_eq!(summary["total"].as_u64().unwrap(), 100);
}
#[test]
fn omits_greenops_section_when_green_disabled() {
let f = finding("t1", "svc", "/ep", "SELECT * FROM t");
let mut report = minimal_report(vec![f.clone()]);
report.green_summary = GreenSummary::disabled(1);
let trace = Trace {
trace_id: "t1".into(),
spans: vec![span("t1", "s1", None, "svc", "/ep", "SELECT * FROM t")],
};
let html = render(&report, &[trace], &opts("-", None));
let json_blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&json_blob).unwrap();
assert!(
value["report"]["green_summary"]["co2"].is_null()
|| value["report"]["green_summary"].get("co2").is_none(),
"co2 must be absent when green disabled"
);
assert!(html.contains(r#"id="panel-green""#));
}
#[test]
fn no_forbidden_apis_in_template() {
let forbidden = [
".innerHTML",
".outerHTML",
"insertAdjacentHTML",
"document.write",
"eval(",
"new Function(",
"DOMParser(",
"createContextualFragment(",
];
for needle in forbidden {
assert!(
!TEMPLATE.contains(needle),
"template contains forbidden API: {needle}"
);
}
assert!(!TEMPLATE.contains("window.Function("));
assert!(!TEMPLATE.contains("globalThis.Function("));
let no_ws: String = TEMPLATE.chars().filter(|c| !c.is_whitespace()).collect();
assert!(
!no_ws.contains("setAttribute(\"on"),
"template contains forbidden attribute-sink: setAttribute(\"on*\", ...)"
);
assert!(
!no_ws.contains("setAttribute('on"),
"template contains forbidden attribute-sink: setAttribute('on*', ...)"
);
}
const CHEATSHEET_DESCRIPTION_FRAGMENTS: &[&str] = &[
"Move finding selection down",
"Move finding selection up",
"Open selected finding in Explain",
"close search",
"back from Explain",
"Open filter search for active tab",
"Go to Findings",
"Go to Explain",
"Go to pg_stat",
"Go to Diff",
"Go to Correlations",
"Go to GreenOps",
"Show this cheatsheet",
];
#[test]
fn cheatsheet_shortcuts_listed_in_template() {
assert!(
TEMPLATE.contains("id=\"cheatsheet\""),
"cheatsheet modal scaffolding missing"
);
assert!(
TEMPLATE.contains("Keyboard shortcuts"),
"cheatsheet title missing"
);
for description in CHEATSHEET_DESCRIPTION_FRAGMENTS {
assert!(
TEMPLATE.contains(description),
"cheatsheet missing description fragment: {description:?}"
);
}
}
#[test]
fn export_button_rendered_for_listable_tabs_only() {
for tab in ["findings", "pgstat", "diff", "correlations"] {
let needle = format!("id=\"{tab}-export\"");
assert!(
TEMPLATE.contains(&needle),
"expected export button for listable tab: {tab}"
);
}
let export_count = TEMPLATE.matches("data-export-tab=\"").count();
assert_eq!(
export_count, 4,
"expected exactly 4 export buttons (findings, pgstat, diff, correlations), found {export_count}. \
If you added a new listable tab, update this assertion and the positive loop above."
);
assert!(
TEMPLATE.contains(".ps-export-btn"),
".ps-export-btn CSS class missing"
);
}
#[test]
fn sessionstorage_access_is_guarded_by_try_catch() {
assert!(
TEMPLATE.contains("function sessionGet("),
"sessionGet helper missing"
);
assert!(
TEMPLATE.contains("function sessionSet("),
"sessionSet helper missing"
);
let lines: Vec<&str> = TEMPLATE.lines().collect();
let mut hits = 0;
for (idx, line) in lines.iter().enumerate() {
let touches =
line.contains("sessionStorage.getItem") || line.contains("sessionStorage.setItem");
if !touches {
continue;
}
hits += 1;
let start = idx.saturating_sub(5);
let window_has_try = lines[start..=idx].iter().any(|l| l.contains("try {"));
assert!(
window_has_try,
"sessionStorage access on line {} has no `try {{` opener within 5 lines above: {}",
idx + 1,
line.trim()
);
}
assert!(
hits >= 2,
"expected at least one sessionGet and one sessionSet access, found {hits}"
);
}
fn extract_payload_json(html: &str) -> String {
let start = html.find("<script id=\"report-data\"").expect("script tag");
let open = html[start..]
.find('>')
.expect("script open")
.saturating_add(1);
let rest = &html[start + open..];
let end = rest.find("</script>").expect("script close");
rest[..end].trim().replace("<\\/", "</")
}
fn synthetic_pg_stat() -> PgStatReport {
use crate::ingest::pg_stat::{PgStatEntry, PgStatRanking, PgStatReport};
let entries = vec![
PgStatEntry {
query: "SELECT * FROM order_item WHERE order_id = 42".into(),
normalized_template: "SELECT * FROM order_item WHERE order_id = ?".into(),
calls: 120,
total_exec_time_ms: 840.0,
mean_exec_time_ms: 7.0,
rows: 500,
shared_blks_hit: 1000,
shared_blks_read: 0,
seen_in_traces: true,
},
PgStatEntry {
query: "SELECT id FROM orders WHERE id = 7".into(),
normalized_template: "SELECT id FROM orders WHERE id = ?".into(),
calls: 30,
total_exec_time_ms: 60.0,
mean_exec_time_ms: 2.0,
rows: 30,
shared_blks_hit: 120,
shared_blks_read: 0,
seen_in_traces: false,
},
];
PgStatReport {
total_entries: 2,
top_n: 2,
rankings: vec![PgStatRanking {
label: "top by total_exec_time".into(),
entries,
}],
}
}
#[test]
fn embeds_pg_stat_when_provided() {
let f = finding("t1", "svc", "/ep", "SELECT * FROM t");
let report = minimal_report(vec![f]);
let mut options = opts("-", None);
options.pg_stat = Some(synthetic_pg_stat());
let html = render(&report, &[], &options);
let blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&blob).unwrap();
let entries = value["pg_stat"]["rankings"][0]["entries"]
.as_array()
.expect("entries array");
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0]["normalized_template"].as_str().unwrap(),
"SELECT * FROM order_item WHERE order_id = ?"
);
}
#[test]
fn omits_pg_stat_when_absent() {
let f = finding("t1", "svc", "/ep", "SELECT * FROM t");
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("-", None));
let blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&blob).unwrap();
assert!(
value.get("pg_stat").is_none(),
"pg_stat must be absent when not provided (skip_serializing_if)"
);
assert!(html.contains(r#"id="panel-pgstat""#));
}
#[test]
fn embeds_diff_report_when_before_provided() {
let before_finding = finding("t1", "svc", "/ep", "SELECT * FROM t");
let before = minimal_report(vec![before_finding.clone()]);
let after_extra = finding("t2", "svc", "/ep2", "SELECT * FROM u");
let after = minimal_report(vec![before_finding, after_extra]);
let diff = crate::diff::diff_runs(&before, &after);
let mut options = opts("-", None);
options.diff = Some(diff);
let html = render(&after, &[], &options);
let blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&blob).unwrap();
let new = value["diff"]["new_findings"].as_array().expect("new array");
assert_eq!(new.len(), 1, "one new finding introduced in 'after'");
let resolved = value["diff"]["resolved_findings"]
.as_array()
.expect("resolved array");
assert_eq!(resolved.len(), 0, "nothing was removed");
}
#[test]
fn omits_diff_when_absent() {
let f = finding("t1", "svc", "/ep", "SELECT * FROM t");
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("-", None));
let blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&blob).unwrap();
assert!(value.get("diff").is_none());
assert!(html.contains(r#"id="panel-diff""#));
}
#[test]
fn cross_nav_pgstat_link_added_only_when_pg_stat_present() {
let tpl = "SELECT * FROM order_item WHERE order_id = ?";
let f = finding("abc", "svc", "/ep", tpl);
let report = minimal_report(vec![f]);
let trace = Trace {
trace_id: "abc".into(),
spans: vec![span("abc", "s1", None, "svc", "/ep", tpl)],
};
let mut with_pg = opts("-", None);
with_pg.pg_stat = Some(synthetic_pg_stat());
let html_with = render(&report, std::slice::from_ref(&trace), &with_pg);
let blob_with = extract_payload_json(&html_with);
let v_with: serde_json::Value = serde_json::from_str(&blob_with).unwrap();
let pg_templates: Vec<&str> = v_with["pg_stat"]["rankings"][0]["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| e["normalized_template"].as_str().unwrap())
.collect();
assert!(
pg_templates.contains(&tpl),
"pg_stat carries the span template"
);
let span_templates: Vec<&str> = v_with["embedded_traces"][0]["spans"]
.as_array()
.unwrap()
.iter()
.map(|s| s["template"].as_str().unwrap())
.collect();
assert!(
span_templates.contains(&tpl),
"trace carries the same template"
);
assert!(
TEMPLATE.contains("ps-span-pgstat-link"),
"template contains the cross-nav class"
);
let without_pg = opts("-", None);
let html_without = render(&report, &[trace], &without_pg);
let blob_without = extract_payload_json(&html_without);
let v_without: serde_json::Value = serde_json::from_str(&blob_without).unwrap();
assert!(v_without.get("pg_stat").is_none());
}
#[test]
fn pg_stat_sub_switcher_exposes_all_ranking_labels() {
let labels = [
"\"Total time\"",
"\"Calls\"",
"\"Mean time\"",
"\"I/O blocks\"",
];
for needle in labels {
assert!(
TEMPLATE.contains(needle),
"template is missing sub-switcher label {needle}"
);
}
assert!(
TEMPLATE.contains("\"data-ranking-index\""),
"setAttr path must use the attribute name as a string literal"
);
assert!(
!TEMPLATE.contains("data-ranking-index=\""),
"template must not hard-code a literal data-ranking-index attribute"
);
let entries = crate::ingest::pg_stat::parse_pg_stat(
b"query,calls,total_exec_time,mean_exec_time,rows,shared_blks_hit,shared_blks_read\n\
SELECT a FROM t1,10,100.0,10.0,10,20,5\n\
SELECT b FROM t2,20,50.0,2.5,20,100,0\n\
SELECT c FROM t3,5,200.0,40.0,5,200,50\n",
1_048_576,
)
.expect("fixture parses");
let pg_stat = crate::ingest::pg_stat::rank_pg_stat(&entries, 10);
let f = finding("t1", "svc", "/ep", "SELECT * FROM t");
let report = minimal_report(vec![f]);
let mut options = opts("-", None);
options.pg_stat = Some(pg_stat);
let html = render(&report, &[], &options);
let blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&blob).unwrap();
let rankings = value["pg_stat"]["rankings"].as_array().unwrap();
assert_eq!(rankings.len(), 4, "payload carries all four rankings");
assert_eq!(
rankings[0]["label"].as_str().unwrap(),
"top by total_exec_time"
);
assert_eq!(
rankings[3]["label"].as_str().unwrap(),
"top by shared_blks_total"
);
}
#[test]
fn theme_mode_defaults_to_auto_with_tri_state_cycle() {
assert!(
TEMPLATE.contains("\"auto\", \"dark\", \"light\""),
"THEME_MODES tri-state ordering must be auto -> dark -> light"
);
assert!(
TEMPLATE.contains("prefers-color-scheme: dark"),
"matchMedia query for prefers-color-scheme missing"
);
assert!(
TEMPLATE.contains("function applyTheme("),
"applyTheme helper missing"
);
assert!(
TEMPLATE.contains("function currentThemeMode("),
"currentThemeMode helper missing"
);
assert!(
TEMPLATE.contains("data-theme=\"\""),
"<html> data-theme must start empty so applyTheme runs before paint"
);
assert!(
!TEMPLATE.contains("data-theme=\"dark\">"),
"<html> must not force dark at boot time"
);
}
#[test]
fn explain_empty_helper_is_shared_across_call_sites() {
assert!(
TEMPLATE.contains("function renderExplainEmpty("),
"renderExplainEmpty helper missing"
);
assert!(
TEMPLATE.contains("Trace not embedded (cap reached)"),
"cap-reached message missing"
);
assert!(
TEMPLATE.contains("This finding was resolved."),
"resolved-diff empty-state message missing"
);
let use_count = TEMPLATE.matches("renderExplainEmpty(").count();
assert!(
use_count >= 3,
"expected at least 3 renderExplainEmpty uses, found {use_count}"
);
}
#[test]
fn tabs_and_panels_carry_aria_roles() {
assert!(
TEMPLATE.contains("role=\"tablist\""),
"tablist role missing from template"
);
for panel in [
"panel-findings",
"panel-explain",
"panel-pgstat",
"panel-diff",
"panel-correlations",
"panel-green",
] {
let needle = format!("id=\"{panel}\"");
assert!(TEMPLATE.contains(&needle), "{panel} id missing");
}
let tabpanel_count = TEMPLATE.matches("role=\"tabpanel\"").count();
assert_eq!(
tabpanel_count, 6,
"expected 6 tabpanels, found {tabpanel_count}"
);
for tab in [
"findings",
"explain",
"pgstat",
"diff",
"correlations",
"green",
] {
let needle = format!("aria-labelledby=\"tab-{tab}\"");
assert!(
TEMPLATE.contains(&needle),
"aria-labelledby link missing for {tab}"
);
}
assert!(TEMPLATE.contains("\"role\", \"tab\""));
assert!(TEMPLATE.contains("\"aria-selected\""));
assert!(TEMPLATE.contains("\"aria-controls\""));
}
#[test]
fn chips_carry_aria_radio_and_pressed_states() {
assert!(
TEMPLATE.contains("\"role\", \"radiogroup\""),
"radiogroup role setter missing"
);
assert!(
TEMPLATE.contains("\"aria-label\", \"pg_stat ranking\""),
"pg_stat ranking radiogroup label missing"
);
assert!(
TEMPLATE.contains("\"aria-label\", \"Finding severity\""),
"Finding severity radiogroup label missing"
);
assert!(
TEMPLATE.contains("\"aria-label\", \"Finding service\""),
"Finding service group label missing"
);
assert!(
TEMPLATE.contains("\"aria-checked\""),
"aria-checked setter missing"
);
assert!(
TEMPLATE.contains("\"aria-pressed\""),
"aria-pressed setter missing"
);
}
#[test]
fn copy_link_button_present_on_listable_tabs_only() {
for tab in ["findings", "pgstat", "diff", "correlations"] {
let needle = format!("id=\"{tab}-copy-link\"");
assert!(
TEMPLATE.contains(&needle),
"expected copy-link button for listable tab: {tab}"
);
}
let copy_link_count = TEMPLATE.matches("data-copy-link-tab=\"").count();
assert_eq!(
copy_link_count, 4,
"expected exactly 4 copy-link buttons, found {copy_link_count}"
);
let f = finding("t1", "svc", "/ep", "SELECT 1");
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("-", None));
assert!(!html.contains("id=\"explain-copy-link\""));
assert!(!html.contains("id=\"green-copy-link\""));
assert!(
TEMPLATE.contains(".ps-copy-link-btn"),
".ps-copy-link-btn CSS class missing"
);
}
#[test]
fn template_ships_a_strict_content_security_policy() {
assert!(
TEMPLATE.contains("Content-Security-Policy"),
"CSP meta tag missing"
);
assert!(TEMPLATE.contains("default-src 'none'"));
assert!(TEMPLATE.contains("base-uri 'none'"));
assert!(TEMPLATE.contains("form-action 'none'"));
}
#[test]
fn title_placeholder_precedes_json_placeholder_in_template() {
let t = TEMPLATE.find(TITLE_PLACEHOLDER).expect("title placeholder");
let j = TEMPLATE.find(JSON_PLACEHOLDER).expect("json placeholder");
assert!(
t < j,
"title placeholder must appear before the JSON placeholder so replacen order stays stable"
);
}
#[test]
fn hostile_input_label_with_json_placeholder_does_not_double_substitute() {
let f = finding("t1", "svc", "/ep", "SELECT 1");
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("{{REPORT_JSON}}.json", None));
assert!(
html.contains("<title>perf-sentinel: {{REPORT_JSON}}.json</title>"),
"placeholder literal must survive as data"
);
}
#[test]
fn hostile_template_containing_title_placeholder_survives_as_data() {
let f = finding("t1", "svc", "/ep", "SELECT '{{PAGE_TITLE}}'");
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("normal.json", None));
assert!(
html.contains("SELECT '{{PAGE_TITLE}}'"),
"user-controlled placeholder literal must survive in the JSON payload"
);
}
#[test]
fn page_title_strips_control_characters() {
let f = finding("t1", "svc", "/ep", "SELECT 1");
let report = minimal_report(vec![f]);
let html = render(&report, &[], &opts("a\x1b[31mb\x00c\u{202e}d.json", None));
assert!(!html.contains('\x1b'), "ESC must not leak into the title");
assert!(
!html.contains('\x00'),
"null byte must not leak into the title"
);
assert!(
!html.contains('\u{202e}'),
"`BiDi` override must not leak into the title"
);
assert!(html.contains("<title>perf-sentinel: a[31mbcd.json</title>"));
}
#[test]
fn page_title_uses_filename_from_input_label() {
let f = finding("t1", "svc", "/ep", "SELECT 1");
let report = minimal_report(vec![f]);
let html_with_path = render(
&report,
&[],
&opts("/tmp/reports/prod-2026-04-21.json", None),
);
assert!(
html_with_path.contains("<title>perf-sentinel: prod-2026-04-21.json</title>"),
"title should show the filename without path components"
);
let html_stdin = render(&report, &[], &opts("-", None));
assert!(
html_stdin.contains("<title>perf-sentinel report</title>"),
"stdin label falls back to the default title"
);
let html_empty = render(&report, &[], &opts("", None));
assert!(
html_empty.contains("<title>perf-sentinel report</title>"),
"empty label falls back to the default title"
);
let html_hostile = render(&report, &[], &opts("/tmp/<hack>&.json", None));
assert!(
html_hostile.contains("<title>perf-sentinel: <hack>&.json</title>"),
"unsafe characters in the filename are HTML-escaped"
);
assert!(
!html_hostile.contains("<title>perf-sentinel: <hack>"),
"raw < must not leak into the title"
);
}
#[test]
fn embeds_correlations_when_report_carries_them() {
use crate::detect::FindingType;
use crate::detect::correlate_cross::{CorrelationEndpoint, CrossTraceCorrelation};
let correlation = CrossTraceCorrelation {
source: CorrelationEndpoint {
finding_type: FindingType::NPlusOneSql,
service: "order-svc".to_string(),
template: "SELECT * FROM o WHERE id = ?".to_string(),
},
target: CorrelationEndpoint {
finding_type: FindingType::SlowHttp,
service: "payment-svc".to_string(),
template: "POST /api/charge".to_string(),
},
co_occurrence_count: 8,
source_total_occurrences: 10,
confidence: 0.8,
median_lag_ms: 120.0,
first_seen: "2026-04-21T10:00:00Z".to_string(),
last_seen: "2026-04-21T10:05:00Z".to_string(),
sample_trace_id: None,
};
let f = finding("t1", "svc", "/ep", "SELECT * FROM t");
let mut report = minimal_report(vec![f]);
report.correlations = vec![correlation];
let html = render(&report, &[], &opts("-", None));
let blob = extract_payload_json(&html);
let value: serde_json::Value = serde_json::from_str(&blob).unwrap();
let corrs = value["report"]["correlations"].as_array().unwrap();
assert_eq!(corrs.len(), 1);
assert_eq!(corrs[0]["source"]["service"].as_str().unwrap(), "order-svc");
assert_eq!(
corrs[0]["target"]["service"].as_str().unwrap(),
"payment-svc"
);
assert_eq!(corrs[0]["co_occurrence_count"].as_u64().unwrap(), 8);
assert!(html.contains(r#"id="panel-correlations""#));
}
}