Skip to content

Commit adad365

Browse files
committed
Wide events: JSON log line, Events page server stats (p50/p95/p99, by-model, daily)
- emit_wide_event: log full ReviewEvent as one JSON line (target review.event.json) for log pipelines - Events: use useEventStats() for top cards; add P50/P95/P99 latency, reviews by model, daily activity chart - ReviewView test: remove unused user in skipped tests (fix TS build) Made-with: Cursor
1 parent 82a699a commit adad365

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

src/server/state.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ impl ReviewEventBuilder {
699699
}
700700

701701
/// Emit a review wide event via structured tracing.
702+
/// Also logs one full JSON line per event (target "review.event.json") for log pipelines / OTEL.
702703
pub fn emit_wide_event(event: &ReviewEvent) {
703704
info!(
704705
review_id = %event.review_id,
@@ -720,6 +721,9 @@ pub fn emit_wide_event(event: &ReviewEvent) {
720721
error = ?event.error,
721722
"review.event"
722723
);
724+
if let Ok(json) = serde_json::to_string(event) {
725+
info!(target: "review.event.json", "{}", json);
726+
}
723727
}
724728

725729
/// Build a progress callback that updates the review session during review.

web/src/pages/Events.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
AreaChart, Area, BarChart, Bar,
55
ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid,
66
} from 'recharts'
7-
import { useEvents } from '../api/hooks'
7+
import { useEvents, useEventStats } from '../api/hooks'
88
import {
99
Loader2, Search, ChevronDown, ChevronLeft, ChevronRight,
1010
Download, Radio, GitCompareArrows, ExternalLink,
@@ -439,6 +439,7 @@ function ensureStyles() {
439439
/* ---- Main Component ---- */
440440
export function Events() {
441441
const { data: events, isLoading } = useEvents()
442+
const { data: serverStats } = useEventStats()
442443
const [search, setSearch] = useState('')
443444
const [sourceFilter, setSourceFilter] = useState<string>('all')
444445
const [modelFilter, setModelFilter] = useState<string>('all')
@@ -526,7 +527,17 @@ export function Events() {
526527
return list
527528
}, [allEvents, sourceFilter, modelFilter, search, sortField, sortDir])
528529

529-
const stats = useMemo(() => computeStats(allEvents), [allEvents])
530+
const clientStats = useMemo(() => computeStats(allEvents), [allEvents])
531+
const stats = serverStats != null
532+
? {
533+
totalReviews: serverStats.total_reviews,
534+
avgDuration: serverStats.avg_duration_ms,
535+
totalTokens: serverStats.total_tokens,
536+
avgScore: serverStats.avg_score ?? 0,
537+
failedCount: serverStats.failed_count,
538+
timeline: clientStats.timeline,
539+
}
540+
: clientStats
530541
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
531542
const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
532543

@@ -573,15 +584,15 @@ export function Events() {
573584
</button>
574585
</div>
575586

576-
{/* Stat cards */}
587+
{/* Stat cards (server stats when available) */}
577588
<div className="grid grid-cols-2 md:grid-cols-6 gap-3 mb-6">
578589
{[
579590
{ label: 'REVIEWS', value: String(stats.totalReviews), sub: stats.failedCount > 0 ? `${stats.failedCount} failed` : undefined, subColor: 'text-sev-error' },
580591
{ label: 'AVG DURATION', value: formatDuration(stats.avgDuration) },
581592
{ label: 'TOTAL TOKENS', value: fmtTokens(stats.totalTokens) },
582593
{ label: 'AVG SCORE', value: stats.avgScore.toFixed(1), valueColor: stats.avgScore >= 7 ? 'text-sev-suggestion' : stats.avgScore >= 4 ? 'text-sev-warning' : 'text-sev-error' },
583594
{ label: 'TOTAL FILES', value: String(allEvents.reduce((s, e) => s + e.diff_files_reviewed, 0)) },
584-
{ label: 'TOTAL COST', value: formatCost(totalCost(allEvents)) },
595+
{ label: 'TOTAL COST', value: formatCost(serverStats != null ? serverStats.total_cost_estimate : totalCost(allEvents)) },
585596
].map(card => (
586597
<div key={card.label} className="bg-surface-1 border border-border rounded-lg p-3">
587598
<div className="text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code">{card.label}</div>
@@ -591,6 +602,61 @@ export function Events() {
591602
))}
592603
</div>
593604

605+
{/* Server latency percentiles + by-model + daily (when available) */}
606+
{serverStats && (serverStats.p50_latency_ms > 0 || serverStats.p95_latency_ms > 0 || serverStats.p99_latency_ms > 0) && (
607+
<div className="grid grid-cols-3 gap-3 mb-6">
608+
<div className="bg-surface-1 border border-border rounded-lg p-3">
609+
<div className="text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code">P50 LATENCY</div>
610+
<div className="text-lg font-bold font-code mt-1 text-text-primary">{formatDuration(serverStats.p50_latency_ms)}</div>
611+
</div>
612+
<div className="bg-surface-1 border border-border rounded-lg p-3">
613+
<div className="text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code">P95 LATENCY</div>
614+
<div className="text-lg font-bold font-code mt-1 text-text-primary">{formatDuration(serverStats.p95_latency_ms)}</div>
615+
</div>
616+
<div className="bg-surface-1 border border-border rounded-lg p-3">
617+
<div className="text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code">P99 LATENCY</div>
618+
<div className="text-lg font-bold font-code mt-1 text-text-primary">{formatDuration(serverStats.p99_latency_ms)}</div>
619+
</div>
620+
</div>
621+
)}
622+
{serverStats && (serverStats.by_model.length > 0 || serverStats.daily_counts.length > 0) && (
623+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
624+
{serverStats.by_model.length > 0 && (
625+
<div className="bg-surface-1 border border-border rounded-lg p-4">
626+
<div className="text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code mb-3">REVIEWS BY MODEL</div>
627+
<div className="space-y-1.5">
628+
{serverStats.by_model.map(m => (
629+
<div key={m.model} className="flex items-center justify-between text-[11px]">
630+
<span className="font-code text-text-primary truncate max-w-40" title={m.model}>{m.model}</span>
631+
<span className="font-code text-text-secondary tabular-nums">{m.count} reviews · {formatDuration(m.avg_duration_ms)} avg</span>
632+
</div>
633+
))}
634+
</div>
635+
</div>
636+
)}
637+
{serverStats.daily_counts.length > 0 && (
638+
<div className="bg-surface-1 border border-border rounded-lg p-4">
639+
<div className="text-[10px] font-semibold text-text-muted tracking-[0.08em] font-code mb-3">DAILY ACTIVITY</div>
640+
<div className="h-28">
641+
<ResponsiveContainer width="100%" height="99%" minWidth={50} minHeight={50}>
642+
<BarChart
643+
data={serverStats.daily_counts.slice(-14).map(d => ({ date: d.date.slice(0, 10), completed: d.completed, failed: d.failed }))}
644+
margin={{ top: 4, right: 4, left: 0, bottom: 0 }}
645+
>
646+
<CartesianGrid {...gridProps} />
647+
<XAxis dataKey="date" tick={{ ...axisTick, fontSize: 9 }} axisLine={false} tickLine={false} />
648+
<YAxis tick={axisTick} axisLine={false} tickLine={false} />
649+
<Tooltip {...tooltipStyle} />
650+
<Bar dataKey="completed" name="Completed" fill={CHART_THEME.accent} radius={[2, 2, 0, 0]} stackId="a" />
651+
<Bar dataKey="failed" name="Failed" fill="#ef4444" radius={[2, 2, 0, 0]} stackId="a" />
652+
</BarChart>
653+
</ResponsiveContainer>
654+
</div>
655+
</div>
656+
)}
657+
</div>
658+
)}
659+
594660
{/* Charts */}
595661
{stats.timeline.length > 1 && (
596662
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">

web/src/pages/__tests__/ReviewView.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ describe('ReviewView blocker mode', () => {
124124
})
125125

126126
it.skip('shows only open blockers and hides non-blocking files when enabled', async () => {
127-
const user = userEvent.setup()
128127
useReviewMock.mockReturnValue({ data: makeReview(), isLoading: false })
129128

130129
render(<ReviewView />)
@@ -198,7 +197,6 @@ describe('ReviewView blocker mode', () => {
198197
})
199198

200199
it.skip('groups list view comments into unresolved, informational, and fixed sections', async () => {
201-
const user = userEvent.setup()
202200
useReviewMock.mockReturnValue({ data: makeReview(), isLoading: false })
203201

204202
render(<ReviewView />)

0 commit comments

Comments
 (0)