Skip to content

Commit bd37c36

Browse files
committed
web: analytics updates and ReviewView fixes
Made-with: Cursor
1 parent de1644d commit bd37c36

File tree

5 files changed

+520
-20
lines changed

5 files changed

+520
-20
lines changed

web/dist/.gitkeep

Lines changed: 0 additions & 1 deletion
This file was deleted.

web/src/lib/analytics.ts

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ import type {
66
} from '../api/types'
77

88
export function computeAnalytics(reviews: ReviewSession[]) {
9-
const reviewTimestamp = (value: ReviewSession['started_at']) => toTimestampMs(value) ?? 0
10-
11-
const completed = reviews
12-
.filter(r => r.status === 'Complete' && r.summary)
13-
.sort((a, b) => reviewTimestamp(a.started_at) - reviewTimestamp(b.started_at))
9+
const completed = getCompletedReviews(reviews)
1410

1511
const scoreOverTime = completed.map((r, i) => ({
12+
reviewId: r.id,
1613
idx: i + 1,
1714
label: `#${i + 1}`,
1815
score: r.summary!.overall_score,
@@ -21,6 +18,7 @@ export function computeAnalytics(reviews: ReviewSession[]) {
2118
}))
2219

2320
const severityOverTime = completed.map((r, i) => ({
21+
reviewId: r.id,
2422
idx: i + 1,
2523
label: `#${i + 1}`,
2624
Error: r.summary!.by_severity.Error || 0,
@@ -78,6 +76,7 @@ export function computeAnalytics(reviews: ReviewSession[]) {
7876
const labeled = accepted + rejected
7977

8078
return {
79+
reviewId: r.id,
8180
idx: i + 1,
8281
label: `#${i + 1}`,
8382
coverage: totalComments > 0 ? labeled / totalComments : 0,
@@ -136,6 +135,7 @@ export function computeAnalytics(reviews: ReviewSession[]) {
136135
.slice(0, 5)
137136

138137
const lifecycleSeries = completed.map((r, i) => ({
138+
reviewId: r.id,
139139
idx: i + 1,
140140
label: `#${i + 1}`,
141141
open: r.summary!.open_comments,
@@ -149,6 +149,7 @@ export function computeAnalytics(reviews: ReviewSession[]) {
149149
const totalFindings = completeness.total_findings
150150

151151
return {
152+
reviewId: r.id,
152153
idx: i + 1,
153154
label: `#${i + 1}`,
154155
totalFindings,
@@ -179,6 +180,7 @@ export function computeAnalytics(reviews: ReviewSession[]) {
179180
const totalHours = resolutionHours.reduce((sum, hours) => sum + hours, 0)
180181

181182
return {
183+
reviewId: r.id,
182184
idx: i + 1,
183185
label: `#${i + 1}`,
184186
meanHours: resolutionHours.length > 0 ? totalHours / resolutionHours.length : null,
@@ -283,6 +285,129 @@ export function computeAnalytics(reviews: ReviewSession[]) {
283285
}
284286
}
285287

288+
export type AnalyticsDrilldownSelection =
289+
| { type: 'review'; reviewId: string }
290+
| { type: 'category'; category: string }
291+
| { type: 'rule'; ruleId: string }
292+
293+
export interface AnalyticsDrilldown {
294+
title: string
295+
description: string
296+
reviews: Array<{
297+
id: string
298+
label: string
299+
startedAt: string | number
300+
overallScore?: number
301+
findingCount: number
302+
}>
303+
comments: Array<{
304+
reviewId: string
305+
reviewLabel: string
306+
id: string
307+
filePath: string
308+
lineNumber: number
309+
content: string
310+
category: string
311+
ruleId?: string
312+
}>
313+
relatedRules: string[]
314+
}
315+
316+
export function buildAnalyticsDrilldown(
317+
reviews: ReviewSession[],
318+
selection: AnalyticsDrilldownSelection,
319+
): AnalyticsDrilldown | null {
320+
const completed = getCompletedReviews(reviews)
321+
const labeledReviews = completed.map((review, index) => ({
322+
review,
323+
label: `#${index + 1}`,
324+
}))
325+
326+
if (selection.type === 'review') {
327+
const match = labeledReviews.find(entry => entry.review.id === selection.reviewId)
328+
if (!match) {
329+
return null
330+
}
331+
332+
const relatedRules = Array.from(new Set(
333+
match.review.comments
334+
.map(comment => comment.rule_id?.trim())
335+
.filter((ruleId): ruleId is string => Boolean(ruleId)),
336+
)).sort()
337+
338+
return {
339+
title: `Review ${match.label}`,
340+
description: `${match.review.comments.length} finding${match.review.comments.length === 1 ? '' : 's'} across ${match.review.files_reviewed} reviewed file${match.review.files_reviewed === 1 ? '' : 's'}.`,
341+
reviews: [{
342+
id: match.review.id,
343+
label: match.label,
344+
startedAt: match.review.started_at,
345+
overallScore: match.review.summary?.overall_score,
346+
findingCount: match.review.comments.length,
347+
}],
348+
comments: match.review.comments.map(comment => ({
349+
reviewId: match.review.id,
350+
reviewLabel: match.label,
351+
id: comment.id,
352+
filePath: comment.file_path,
353+
lineNumber: comment.line_number,
354+
content: comment.content,
355+
category: comment.category,
356+
ruleId: comment.rule_id?.trim(),
357+
})),
358+
relatedRules,
359+
}
360+
}
361+
362+
const matches = labeledReviews.flatMap(({ review, label }) => review.comments
363+
.filter(comment => selection.type === 'category'
364+
? comment.category === selection.category
365+
: comment.rule_id?.trim() === selection.ruleId)
366+
.map(comment => ({ review, label, comment })))
367+
368+
if (matches.length === 0) {
369+
return null
370+
}
371+
372+
const reviewMap = new Map<string, AnalyticsDrilldown['reviews'][number]>()
373+
for (const { review, label } of matches) {
374+
if (!reviewMap.has(review.id)) {
375+
reviewMap.set(review.id, {
376+
id: review.id,
377+
label,
378+
startedAt: review.started_at,
379+
overallScore: review.summary?.overall_score,
380+
findingCount: review.comments.length,
381+
})
382+
}
383+
}
384+
385+
const relatedRules = Array.from(new Set(
386+
matches
387+
.map(({ comment }) => comment.rule_id?.trim())
388+
.filter((ruleId): ruleId is string => Boolean(ruleId)),
389+
)).sort()
390+
391+
return {
392+
title: selection.type === 'category'
393+
? `Category · ${selection.category}`
394+
: `Rule · ${selection.ruleId}`,
395+
description: `${matches.length} finding${matches.length === 1 ? '' : 's'} across ${reviewMap.size} review${reviewMap.size === 1 ? '' : 's'}.`,
396+
reviews: Array.from(reviewMap.values()),
397+
comments: matches.map(({ review, label, comment }) => ({
398+
reviewId: review.id,
399+
reviewLabel: label,
400+
id: comment.id,
401+
filePath: comment.file_path,
402+
lineNumber: comment.line_number,
403+
content: comment.content,
404+
category: comment.category,
405+
ruleId: comment.rule_id?.trim(),
406+
})),
407+
relatedRules,
408+
}
409+
}
410+
286411
export function formatTrendLabel(timestamp: string, index: number): string {
287412
const parsed = new Date(timestamp)
288413
if (Number.isNaN(parsed.getTime())) return `#${index + 1}`
@@ -651,6 +776,14 @@ function toTimestampMs(value: string | number | undefined): number | null {
651776
return toDate(value)?.getTime() ?? null
652777
}
653778

779+
function getCompletedReviews(reviews: ReviewSession[]) {
780+
return reviews
781+
.filter((review): review is ReviewSession & { summary: NonNullable<ReviewSession['summary']> } => (
782+
review.status === 'Complete' && Boolean(review.summary)
783+
))
784+
.sort((left, right) => (toTimestampMs(left.started_at) ?? 0) - (toTimestampMs(right.started_at) ?? 0))
785+
}
786+
654787
function getCompletenessSummary(summary: NonNullable<ReviewSession['summary']>) {
655788
return summary.completeness ?? {
656789
total_findings: summary.total_comments,

0 commit comments

Comments
 (0)