@@ -6,8 +6,7 @@ import type {
66} from '../api/types'
77
88export function computeAnalytics ( reviews : ReviewSession [ ] ) {
9- const reviewTimestamp = ( value : ReviewSession [ 'started_at' ] ) =>
10- typeof value === 'number' ? value : Date . parse ( value )
9+ const reviewTimestamp = ( value : ReviewSession [ 'started_at' ] ) => toTimestampMs ( value ) ?? 0
1110
1211 const completed = reviews
1312 . filter ( r => r . status === 'Complete' && r . summary )
@@ -145,6 +144,48 @@ export function computeAnalytics(reviews: ReviewSession[]) {
145144 openBlockers : r . summary ! . open_blockers ,
146145 } ) )
147146
147+ const completenessSeries = completed . map ( ( r , i ) => {
148+ const completeness = getCompletenessSummary ( r . summary ! )
149+ const totalFindings = completeness . total_findings
150+
151+ return {
152+ idx : i + 1 ,
153+ label : `#${ i + 1 } ` ,
154+ totalFindings,
155+ acknowledged : completeness . acknowledged_findings ,
156+ fixed : completeness . fixed_findings ,
157+ stale : completeness . stale_findings ,
158+ acknowledgedRate : totalFindings > 0 ? completeness . acknowledged_findings / totalFindings : 0 ,
159+ fixedRate : totalFindings > 0 ? completeness . fixed_findings / totalFindings : 0 ,
160+ }
161+ } )
162+
163+ const meanTimeToResolutionSeries = completed . map ( ( r , i ) => {
164+ const startedAtMs = toTimestampMs ( r . started_at )
165+ const resolutionHours = startedAtMs == null
166+ ? [ ]
167+ : r . comments . flatMap ( comment => {
168+ if ( comment . status !== 'Resolved' ) {
169+ return [ ]
170+ }
171+
172+ const resolvedAtMs = toTimestampMs ( comment . resolved_at )
173+ if ( resolvedAtMs == null || resolvedAtMs < startedAtMs ) {
174+ return [ ]
175+ }
176+
177+ return [ ( resolvedAtMs - startedAtMs ) / ( 1000 * 60 * 60 ) ]
178+ } )
179+ const totalHours = resolutionHours . reduce ( ( sum , hours ) => sum + hours , 0 )
180+
181+ return {
182+ idx : i + 1 ,
183+ label : `#${ i + 1 } ` ,
184+ meanHours : resolutionHours . length > 0 ? totalHours / resolutionHours . length : null ,
185+ resolvedCount : resolutionHours . length ,
186+ }
187+ } )
188+
148189 const totalFindings = completed . reduce ( ( s , r ) => s + r . summary ! . total_comments , 0 )
149190 const avgFindings = completed . length > 0 ? totalFindings / completed . length : 0
150191 const avgScore = completed . length > 0
@@ -154,6 +195,31 @@ export function computeAnalytics(reviews: ReviewSession[]) {
154195 const totalResolvedComments = completed . reduce ( ( sum , r ) => sum + r . summary ! . resolved_comments , 0 )
155196 const totalDismissedComments = completed . reduce ( ( sum , r ) => sum + r . summary ! . dismissed_comments , 0 )
156197 const totalOpenBlockers = completed . reduce ( ( sum , r ) => sum + r . summary ! . open_blockers , 0 )
198+ const totalAcknowledgedFindings = completed . reduce (
199+ ( sum , r ) => sum + getCompletenessSummary ( r . summary ! ) . acknowledged_findings ,
200+ 0 ,
201+ )
202+ const totalFixedFindings = completed . reduce (
203+ ( sum , r ) => sum + getCompletenessSummary ( r . summary ! ) . fixed_findings ,
204+ 0 ,
205+ )
206+ const totalStaleFindings = completed . reduce (
207+ ( sum , r ) => sum + getCompletenessSummary ( r . summary ! ) . stale_findings ,
208+ 0 ,
209+ )
210+ const totalCompletenessFindings = completed . reduce (
211+ ( sum , r ) => sum + getCompletenessSummary ( r . summary ! ) . total_findings ,
212+ 0 ,
213+ )
214+ const resolvedWithTimestampCount = meanTimeToResolutionSeries . reduce (
215+ ( sum , point ) => sum + point . resolvedCount ,
216+ 0 ,
217+ )
218+ const totalResolutionHours = meanTimeToResolutionSeries . reduce (
219+ ( sum , point ) => sum + ( point . meanHours ?? 0 ) * point . resolvedCount ,
220+ 0 ,
221+ )
222+ const reviewsWithTimedResolutions = meanTimeToResolutionSeries . filter ( point => point . resolvedCount > 0 ) . length
157223 const totalLifecycleComments = totalOpenComments + totalResolvedComments + totalDismissedComments
158224 const labeledFeedbackTotal = feedbackCoverageSeries . reduce ( ( sum , point ) => sum + point . labeled , 0 )
159225 const acceptedFeedbackTotal = feedbackCoverageSeries . reduce ( ( sum , point ) => sum + point . accepted , 0 )
@@ -176,6 +242,8 @@ export function computeAnalytics(reviews: ReviewSession[]) {
176242 severityOverTime,
177243 categoryData,
178244 lifecycleSeries,
245+ completenessSeries,
246+ meanTimeToResolutionSeries,
179247 feedbackCoverageSeries,
180248 topAcceptedCategories,
181249 topRejectedCategories,
@@ -196,6 +264,15 @@ export function computeAnalytics(reviews: ReviewSession[]) {
196264 totalResolvedComments,
197265 totalDismissedComments,
198266 totalOpenBlockers,
267+ totalAcknowledgedFindings,
268+ totalFixedFindings,
269+ totalStaleFindings,
270+ completenessRate : totalCompletenessFindings > 0 ? totalAcknowledgedFindings / totalCompletenessFindings : 0 ,
271+ meanTimeToResolutionHours : resolvedWithTimestampCount > 0
272+ ? totalResolutionHours / resolvedWithTimestampCount
273+ : null ,
274+ resolvedWithTimestampCount,
275+ reviewsWithTimedResolutions,
199276 resolutionRate : totalLifecycleComments > 0
200277 ? ( totalResolvedComments + totalDismissedComments ) / totalLifecycleComments
201278 : 0 ,
@@ -216,6 +293,19 @@ export function formatPercent(value: number | undefined): string {
216293 return value == null ? 'n/a' : `${ ( value * 100 ) . toFixed ( 0 ) } %`
217294}
218295
296+ export function formatDurationHours ( value : number | null | undefined ) : string {
297+ if ( value == null || Number . isNaN ( value ) ) {
298+ return 'n/a'
299+ }
300+ if ( value >= 48 ) {
301+ return `${ ( value / 24 ) . toFixed ( 1 ) } d`
302+ }
303+ if ( value >= 1 ) {
304+ return `${ value . toFixed ( 1 ) } h`
305+ }
306+ return `${ Math . round ( value * 60 ) } m`
307+ }
308+
219309export function computeTrendAnalytics ( trends : AnalyticsTrendsResponse | undefined ) {
220310 const evalEntries = trends ?. eval_trend . entries ?? [ ]
221311 const feedbackEntries = trends ?. feedback_eval_trend . entries ?? [ ]
@@ -290,9 +380,17 @@ export interface AnalyticsExportReport {
290380 totalResolvedComments : number
291381 totalDismissedComments : number
292382 totalOpenBlockers : number
383+ totalAcknowledgedFindings : number
384+ totalFixedFindings : number
385+ totalStaleFindings : number
386+ completenessRate : number
387+ meanTimeToResolutionHours ?: number
388+ resolvedWithTimestampCount : number
293389 resolutionRate : number
294390 }
295391 byReview : AnalyticsSnapshot [ 'lifecycleSeries' ]
392+ completenessByReview : AnalyticsSnapshot [ 'completenessSeries' ]
393+ meanTimeToResolutionByReview : AnalyticsSnapshot [ 'meanTimeToResolutionSeries' ]
296394 }
297395 reinforcement : {
298396 summary : {
@@ -401,9 +499,17 @@ export function buildAnalyticsExportReport(
401499 totalResolvedComments : analytics . stats . totalResolvedComments ,
402500 totalDismissedComments : analytics . stats . totalDismissedComments ,
403501 totalOpenBlockers : analytics . stats . totalOpenBlockers ,
502+ totalAcknowledgedFindings : analytics . stats . totalAcknowledgedFindings ,
503+ totalFixedFindings : analytics . stats . totalFixedFindings ,
504+ totalStaleFindings : analytics . stats . totalStaleFindings ,
505+ completenessRate : analytics . stats . completenessRate ,
506+ meanTimeToResolutionHours : analytics . stats . meanTimeToResolutionHours ?? undefined ,
507+ resolvedWithTimestampCount : analytics . stats . resolvedWithTimestampCount ,
404508 resolutionRate : analytics . stats . resolutionRate ,
405509 } ,
406510 byReview : analytics . lifecycleSeries ,
511+ completenessByReview : analytics . completenessSeries ,
512+ meanTimeToResolutionByReview : analytics . meanTimeToResolutionSeries ,
407513 } ,
408514 reinforcement : {
409515 summary : {
@@ -463,6 +569,20 @@ export function buildAnalyticsCsv(report: AnalyticsExportReport): string {
463569 rows . push ( { report : 'lifecycle' , group : 'by_review' , label : point . label , metric : 'dismissed' , value : point . dismissed } )
464570 rows . push ( { report : 'lifecycle' , group : 'by_review' , label : point . label , metric : 'open_blockers' , value : point . openBlockers } )
465571 } )
572+ report . lifecycle . completenessByReview . forEach ( point => {
573+ rows . push ( { report : 'lifecycle' , group : 'completeness_by_review' , label : point . label , metric : 'total_findings' , value : point . totalFindings } )
574+ rows . push ( { report : 'lifecycle' , group : 'completeness_by_review' , label : point . label , metric : 'acknowledged' , value : point . acknowledged } )
575+ rows . push ( { report : 'lifecycle' , group : 'completeness_by_review' , label : point . label , metric : 'fixed' , value : point . fixed } )
576+ rows . push ( { report : 'lifecycle' , group : 'completeness_by_review' , label : point . label , metric : 'stale' , value : point . stale } )
577+ rows . push ( { report : 'lifecycle' , group : 'completeness_by_review' , label : point . label , metric : 'acknowledged_rate' , value : point . acknowledgedRate } )
578+ rows . push ( { report : 'lifecycle' , group : 'completeness_by_review' , label : point . label , metric : 'fixed_rate' , value : point . fixedRate } )
579+ } )
580+ report . lifecycle . meanTimeToResolutionByReview . forEach ( point => {
581+ if ( point . meanHours != null ) {
582+ rows . push ( { report : 'lifecycle' , group : 'mean_time_to_resolution_by_review' , label : point . label , metric : 'mean_hours' , value : point . meanHours } )
583+ }
584+ rows . push ( { report : 'lifecycle' , group : 'mean_time_to_resolution_by_review' , label : point . label , metric : 'resolved_count' , value : point . resolvedCount } )
585+ } )
466586
467587 appendSummaryRows ( rows , 'reinforcement' , 'summary' , report . reinforcement . summary )
468588 report . reinforcement . coverageByReview . forEach ( point => {
@@ -515,3 +635,27 @@ export function exportAnalyticsJson(report: AnalyticsExportReport) {
515635 'diffscope-analytics-report.json' ,
516636 )
517637}
638+
639+ function toDate ( value : string | number | undefined ) : Date | null {
640+ if ( value == null ) {
641+ return null
642+ }
643+
644+ const date = typeof value === 'number'
645+ ? new Date ( value * 1000 )
646+ : new Date ( value )
647+ return Number . isNaN ( date . getTime ( ) ) ? null : date
648+ }
649+
650+ function toTimestampMs ( value : string | number | undefined ) : number | null {
651+ return toDate ( value ) ?. getTime ( ) ?? null
652+ }
653+
654+ function getCompletenessSummary ( summary : NonNullable < ReviewSession [ 'summary' ] > ) {
655+ return summary . completeness ?? {
656+ total_findings : summary . total_comments ,
657+ acknowledged_findings : summary . resolved_comments + summary . dismissed_comments ,
658+ fixed_findings : summary . resolved_comments ,
659+ stale_findings : 0 ,
660+ }
661+ }
0 commit comments