@@ -5,7 +5,7 @@ import type {
55} from "@/lib/test-results-downloader.ts" ;
66import type { GitHubApiClient , WorkflowRun } from "@/lib/github-api-client.ts" ;
77import type { Logger } from "@/lib/logger.ts" ;
8- import { formatDuration } from "@/lib/render.tsx" ;
8+ import { formatDuration , TestTimeline } from "@/lib/render.tsx" ;
99
1010export const handler = define . handlers ( {
1111 GET ( ctx ) {
@@ -29,14 +29,17 @@ export class InsightsPageController {
2929 }
3030
3131 async get ( ) {
32- // Fetch the last 100 runs (to ensure we get at least 20 completed main branch runs)
33- const { runs : allRuns } = await this . #githubClient. listWorkflowRuns ( 100 , 1 ) ;
34-
35- // Filter to only completed CI runs on main branch
32+ // fetch main branch runs directly
33+ const [ page1 , page2 ] = await Promise . all ( [
34+ this . #githubClient. listWorkflowRuns ( 100 , 1 , "main" ) ,
35+ this . #githubClient. listWorkflowRuns ( 100 , 2 , "main" ) ,
36+ ] ) ;
37+ const allRuns = [ ...page1 . runs , ...page2 . runs ] ;
38+
39+ // Filter to only completed CI runs
3640 const mainBranchRuns = allRuns
3741 . filter (
3842 ( run : WorkflowRun ) =>
39- run . head_branch === "main" &&
4043 run . status === "completed" &&
4144 run . name . toLowerCase ( ) === "ci" ,
4245 )
@@ -59,6 +62,12 @@ export class InsightsPageController {
5962 }
6063 } ) ) ) . filter ( ( r ) => r != null ) ;
6164
65+ // Build a map of runId to date for timeline lookups
66+ const runIdToDate = new Map < number , string > ( ) ;
67+ allResults . forEach ( ( { runId, run } ) => {
68+ runIdToDate . set ( runId , run . created_at . split ( "T" ) [ 0 ] ) ;
69+ } ) ;
70+
6271 // Analyze flaky tests across all runs
6372 const flakyTestsMap = new Map <
6473 string ,
@@ -70,6 +79,7 @@ export class InsightsPageController {
7079 avgFlakyCount : number ;
7180 runIds : number [ ] ;
7281 jobCounts : Map < string , number > ;
82+ dailyCounts : Map < string , number > ; // date -> flaky count
7383 }
7484 > ( ) ;
7585
@@ -81,6 +91,7 @@ export class InsightsPageController {
8191 path : string ;
8292 failureCount : number ;
8393 runIds : number [ ] ;
94+ dailyCounts : Map < string , number > ; // date -> failure count
8495 }
8596 > ( ) ;
8697
@@ -92,6 +103,8 @@ export class InsightsPageController {
92103 runId : number ,
93104 jobName : string ,
94105 ) {
106+ const runDate = runIdToDate . get ( runId ) ! ;
107+
95108 // Track flaky tests
96109 if ( test . flakyCount && test . flakyCount > 0 ) {
97110 const key = `${ test . path } ::${ test . name } ` ;
@@ -109,6 +122,10 @@ export class InsightsPageController {
109122 jobName ,
110123 ( existing . jobCounts . get ( jobName ) || 0 ) + test . flakyCount ,
111124 ) ;
125+ existing . dailyCounts . set (
126+ runDate ,
127+ ( existing . dailyCounts . get ( runDate ) || 0 ) + test . flakyCount ,
128+ ) ;
112129 } else {
113130 flakyTestsMap . set ( key , {
114131 name : test . name ,
@@ -118,6 +135,7 @@ export class InsightsPageController {
118135 avgFlakyCount : test . flakyCount ,
119136 runIds : [ runId ] ,
120137 jobCounts : new Map ( [ [ jobName , test . flakyCount ] ] ) ,
138+ dailyCounts : new Map ( [ [ runDate , test . flakyCount ] ] ) ,
121139 } ) ;
122140 }
123141
@@ -138,12 +156,17 @@ export class InsightsPageController {
138156 if ( ! existing . runIds . includes ( runId ) ) {
139157 existing . runIds . push ( runId ) ;
140158 }
159+ existing . dailyCounts . set (
160+ runDate ,
161+ ( existing . dailyCounts . get ( runDate ) || 0 ) + 1 ,
162+ ) ;
141163 } else {
142164 failedTestsMap . set ( key , {
143165 name : test . name ,
144166 path : test . path ,
145167 failureCount : 1 ,
146168 runIds : [ runId ] ,
169+ dailyCounts : new Map ( [ [ runDate , 1 ] ] ) ,
147170 } ) ;
148171 }
149172 }
@@ -177,11 +200,48 @@ export class InsightsPageController {
177200 }
178201 > ( ) ;
179202
180- allResults . forEach ( ( { runId, results, jobs } ) => {
203+ // Track daily statistics for the chart
204+ const dailyStatsMap = new Map <
205+ string ,
206+ {
207+ date : string ;
208+ failureCount : number ;
209+ flakyCount : number ;
210+ runCount : number ;
211+ }
212+ > ( ) ;
213+
214+ allResults . forEach ( ( { runId, run, results, jobs } ) => {
215+ // Aggregate daily stats
216+ const dateKey = run . created_at . split ( "T" ) [ 0 ] ;
217+ let dayStats = dailyStatsMap . get ( dateKey ) ;
218+ if ( ! dayStats ) {
219+ dayStats = {
220+ date : dateKey ,
221+ failureCount : 0 ,
222+ flakyCount : 0 ,
223+ runCount : 0 ,
224+ } ;
225+ dailyStatsMap . set ( dateKey , dayStats ) ;
226+ }
227+ dayStats . runCount ++ ;
228+
229+ // Count failures and flakes for this run
230+ const countTestStats = ( tests : RecordedTestResult [ ] ) => {
231+ tests . forEach ( ( test ) => {
232+ if ( test . failed ) dayStats ! . failureCount ++ ;
233+ if ( test . flakyCount && test . flakyCount > 0 ) {
234+ dayStats ! . flakyCount += test . flakyCount ;
235+ }
236+ if ( test . subTests ) countTestStats ( test . subTests ) ;
237+ } ) ;
238+ } ;
239+
181240 results . forEach ( ( jobResult ) => {
182241 jobResult . tests . forEach ( ( test ) =>
183242 processTest ( test , runId , jobResult . name )
184243 ) ;
244+ countTestStats ( jobResult . tests ) ;
185245 } ) ;
186246
187247 // Process job timing data
@@ -259,12 +319,26 @@ export class InsightsPageController {
259319 name,
260320 count,
261321 } ) ) ,
322+ dailyCounts : Array . from ( test . dailyCounts . entries ( ) ) . map ( (
323+ [ date , count ] ,
324+ ) => ( {
325+ date,
326+ count,
327+ } ) ) ,
262328 } ) ) . sort (
263329 ( a , b ) => b . totalFlakyCounts - a . totalFlakyCounts ,
264330 ) ;
265331
266332 // Convert to array and sort by failure count
267- const failedTests = Array . from ( failedTestsMap . values ( ) ) . sort (
333+ const failedTests = Array . from ( failedTestsMap . values ( ) ) . map ( ( test ) => ( {
334+ ...test ,
335+ dailyCounts : Array . from ( test . dailyCounts . entries ( ) ) . map ( (
336+ [ date , count ] ,
337+ ) => ( {
338+ date,
339+ count,
340+ } ) ) ,
341+ } ) ) . sort (
268342 ( a , b ) => b . failureCount - a . failureCount ,
269343 ) ;
270344
@@ -295,13 +369,30 @@ export class InsightsPageController {
295369 } ) )
296370 . sort ( ( a , b ) => b . avgDuration - a . avgDuration ) ;
297371
372+ // Get the date range from oldest run to today
373+ const allDates = Array . from ( runIdToDate . values ( ) ) . sort ( ) ;
374+ const oldestDate = allDates [ 0 ] ;
375+ const today = new Date ( ) . toISOString ( ) . split ( "T" ) [ 0 ] ;
376+
377+ // Generate all dates from oldest to today
378+ const dateRange : string [ ] = [ ] ;
379+ if ( oldestDate ) {
380+ const current = new Date ( oldestDate + "T00:00:00" ) ;
381+ const end = new Date ( today + "T00:00:00" ) ;
382+ while ( current <= end ) {
383+ dateRange . push ( current . toISOString ( ) . split ( "T" ) [ 0 ] ) ;
384+ current . setDate ( current . getDate ( ) + 1 ) ;
385+ }
386+ }
387+
298388 return {
299389 data : {
300390 flakyTests,
301391 failedTests,
302392 flakyJobs,
303393 jobPerformance,
304394 stepPerformance,
395+ dateRange,
305396 totalRunsAnalyzed : mainBranchRuns . length ,
306397 oldestRun : mainBranchRuns [ mainBranchRuns . length - 1 ] ,
307398 newestRun : mainBranchRuns [ 0 ] ,
@@ -317,6 +408,7 @@ export default define.page<typeof handler>(function InsightsPage({ data }) {
317408 flakyJobs,
318409 jobPerformance,
319410 stepPerformance,
411+ dateRange,
320412 totalRunsAnalyzed,
321413 oldestRun,
322414 newestRun,
@@ -394,6 +486,11 @@ export default define.page<typeof handler>(function InsightsPage({ data }) {
394486 </ span >
395487 </ span >
396488 </ div >
489+ < TestTimeline
490+ dateRange = { dateRange }
491+ dailyCounts = { test . dailyCounts }
492+ color = "red"
493+ />
397494 </ div >
398495 < div class = "flex-shrink-0" >
399496 < div class = "bg-red-100 text-red-800 px-3 py-2 rounded text-center" >
@@ -479,6 +576,11 @@ export default define.page<typeof handler>(function InsightsPage({ data }) {
479576 ) ) }
480577 </ div >
481578 ) }
579+ < TestTimeline
580+ dateRange = { dateRange }
581+ dailyCounts = { test . dailyCounts }
582+ color = "yellow"
583+ />
482584 </ div >
483585 < div class = "flex-shrink-0" >
484586 < div class = "bg-yellow-100 text-yellow-800 px-3 py-2 rounded text-center" >
0 commit comments