Address review blockers and medium/low findings: - F1: HTML-escape all dynamic analytics fields (html.escape quote=True) - F2: Gate POST /api/v1/analytics/usage through console_authz record_analytics_usage (fail closed for unauthenticated / Phase 1) - F3: Enforce usage_events retention (max rows + max age) - F4: Coerce None remote/org/repo to empty strings in load_analytics Add tests for XSS escaping, unauthorized ingest deny, retention, and None scope coercion. Document authz + retention in analytics guide. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+74
-27
@@ -2,52 +2,59 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
||||
from webui.analytics_loader import AnalyticsSnapshot, GroupMetrics, UsageEvent
|
||||
from webui.layout import render_page
|
||||
|
||||
|
||||
def _escape(text: object) -> str:
|
||||
"""HTML-escape dynamic analytics fields (mirrors audit_views / project_views)."""
|
||||
return html.escape(str(text), quote=True)
|
||||
|
||||
|
||||
def _render_badge(text: str, badge_type: str = "muted") -> str:
|
||||
return f'<span class="badge badge-{badge_type}">{text}</span>'
|
||||
return f'<span class="badge badge-{_escape(badge_type)}">{_escape(text)}</span>'
|
||||
|
||||
|
||||
def _render_group_table(title: str, groups: dict[str, GroupMetrics], key_header: str = "Group") -> str:
|
||||
if not groups:
|
||||
return (
|
||||
f"<h3>{title}</h3>"
|
||||
f"<h3>{_escape(title)}</h3>"
|
||||
'<div class="card"><p class="muted">No telemetry events recorded for this dimension.</p></div>'
|
||||
)
|
||||
|
||||
rows = []
|
||||
for key, g in sorted(groups.items(), key=lambda x: x[1].total_events, reverse=True):
|
||||
cost_cell = (
|
||||
f'<span class="accent">{g.display_cost}</span>'
|
||||
f'<span class="accent">{_escape(g.display_cost)}</span>'
|
||||
if g.events_with_cost > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
tokens_cell = (
|
||||
g.display_tokens
|
||||
_escape(g.display_tokens)
|
||||
if g.events_with_tokens > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
lat_p50 = (
|
||||
g.display_latency_p50
|
||||
_escape(g.display_latency_p50)
|
||||
if g.events_with_latency > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
lat_p90 = (
|
||||
g.display_latency_p90
|
||||
_escape(g.display_latency_p90)
|
||||
if g.events_with_latency > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
dur_avg = (
|
||||
g.display_duration_avg
|
||||
_escape(g.display_duration_avg)
|
||||
if g.events_with_duration > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td><strong>{key}</strong></td>"
|
||||
f"<td><strong>{_escape(key)}</strong></td>"
|
||||
f"<td>{g.total_events}</td>"
|
||||
f"<td>{tokens_cell}</td>"
|
||||
f"<td>{cost_cell}</td>"
|
||||
@@ -59,12 +66,12 @@ def _render_group_table(title: str, groups: dict[str, GroupMetrics], key_header:
|
||||
|
||||
rows_html = "".join(rows)
|
||||
return f"""
|
||||
<h3>{title}</h3>
|
||||
<h3>{_escape(title)}</h3>
|
||||
<div class="card" style="overflow-x: auto;">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{key_header}</th>
|
||||
<th>{_escape(key_header)}</th>
|
||||
<th>Events</th>
|
||||
<th>Total Tokens</th>
|
||||
<th>Est. Cost</th>
|
||||
@@ -85,26 +92,49 @@ def _render_events_table(events: tuple[UsageEvent, ...]) -> str:
|
||||
if not events:
|
||||
return (
|
||||
"<h3>Recent Usage & Instrumentation Events</h3>"
|
||||
'<div class="card"><p class="muted">No individual telemetry events recorded yet. Opt-in instrumentation via session logging or POST /api/v1/analytics/usage.</p></div>'
|
||||
'<div class="card"><p class="muted">No individual telemetry events recorded yet. Opt-in instrumentation via session logging or authorized POST /api/v1/analytics/usage.</p></div>'
|
||||
)
|
||||
|
||||
rows = []
|
||||
for e in list(events)[-50:]: # Display latest 50
|
||||
work_item = f"issue #{e.issue_number}" if e.issue_number else (f"pr #{e.pr_number}" if e.pr_number else "unlinked")
|
||||
tokens = f"{e.total_tokens:,}" if e.total_tokens is not None else _render_badge("Unknown")
|
||||
cost = f"${e.estimated_cost_usd:.4f}" if e.estimated_cost_usd is not None else _render_badge("Unknown")
|
||||
latency = f"{e.latency_ms} ms" if e.latency_ms is not None else _render_badge("Unknown")
|
||||
duration = f"{e.duration_ms} ms" if e.duration_ms is not None else _render_badge("Unknown")
|
||||
status_badge = _render_badge(e.status, "success" if e.status == "success" else "danger")
|
||||
if e.issue_number is not None:
|
||||
work_item = f"issue #{e.issue_number}"
|
||||
elif e.pr_number is not None:
|
||||
work_item = f"pr #{e.pr_number}"
|
||||
else:
|
||||
work_item = "unlinked"
|
||||
tokens = (
|
||||
_escape(f"{e.total_tokens:,}")
|
||||
if e.total_tokens is not None
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
cost = (
|
||||
_escape(f"${e.estimated_cost_usd:.4f}")
|
||||
if e.estimated_cost_usd is not None
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
latency = (
|
||||
_escape(f"{e.latency_ms} ms")
|
||||
if e.latency_ms is not None
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
duration = (
|
||||
_escape(f"{e.duration_ms} ms")
|
||||
if e.duration_ms is not None
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
status_badge = _render_badge(
|
||||
e.status, "success" if e.status == "success" else "danger"
|
||||
)
|
||||
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td>#{e.usage_id}</td>"
|
||||
f"<td><small>{e.created_at}</small></td>"
|
||||
f"<td><span class=\"badge\">{e.role}</span></td>"
|
||||
f"<td><strong>{e.model}</strong></td>"
|
||||
f"<td>{e.stage}</td>"
|
||||
f"<td>{work_item}</td>"
|
||||
f"<td><small>{_escape(e.created_at)}</small></td>"
|
||||
f"<td><span class=\"badge\">{_escape(e.role)}</span></td>"
|
||||
f"<td><strong>{_escape(e.model)}</strong></td>"
|
||||
f"<td>{_escape(e.stage)}</td>"
|
||||
f"<td>{_escape(work_item)}</td>"
|
||||
f"<td>{tokens}</td>"
|
||||
f"<td>{cost}</td>"
|
||||
f"<td>{latency}</td>"
|
||||
@@ -145,15 +175,32 @@ def render_analytics_page(snapshot: AnalyticsSnapshot) -> str:
|
||||
"""Render the main Model Usage & Performance Analytics console page."""
|
||||
summary = snapshot.overall_summary
|
||||
|
||||
kpi_tokens = summary.display_tokens if summary.events_with_tokens > 0 else _render_badge("Unknown")
|
||||
kpi_cost = summary.display_cost if summary.events_with_cost > 0 else _render_badge("Unknown")
|
||||
kpi_lat_p50 = summary.display_latency_p50 if summary.events_with_latency > 0 else _render_badge("Unknown")
|
||||
kpi_dur_avg = summary.display_duration_avg if summary.events_with_duration > 0 else _render_badge("Unknown")
|
||||
kpi_tokens = (
|
||||
_escape(summary.display_tokens)
|
||||
if summary.events_with_tokens > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
kpi_cost = (
|
||||
_escape(summary.display_cost)
|
||||
if summary.events_with_cost > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
kpi_lat_p50 = (
|
||||
_escape(summary.display_latency_p50)
|
||||
if summary.events_with_latency > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
kpi_dur_avg = (
|
||||
_escape(summary.display_duration_avg)
|
||||
if summary.events_with_duration > 0
|
||||
else _render_badge("Unknown")
|
||||
)
|
||||
|
||||
status_notice = ""
|
||||
if not snapshot.ok:
|
||||
status_notice = (
|
||||
f'<div class="card warning-card"><strong>Degraded Data Source:</strong> {snapshot.reason}</div>'
|
||||
f'<div class="card warning-card"><strong>Degraded Data Source:</strong> '
|
||||
f'{_escape(snapshot.reason)}</div>'
|
||||
)
|
||||
|
||||
body_html = f"""
|
||||
|
||||
Reference in New Issue
Block a user