diff --git a/frontend/src/lib/slo.ts b/frontend/src/lib/slo.ts new file mode 100644 index 0000000..f3f26a3 --- /dev/null +++ b/frontend/src/lib/slo.ts @@ -0,0 +1,32 @@ +// Defines constants and functions to check Github issue triage state. + +import { Temporal } from "@js-temporal/polyfill"; +import type { IssueSummary, SloType } from "./repo-summaries"; + +const triageSLO = Temporal.Duration.from({ days: 7 }); +const urgentSLO = Temporal.Duration.from({ days: 14 }); +const importantSLO = Temporal.Duration.from({ days: 91 }); + +const sloMap = { + "urgent": urgentSLO, + "important": importantSLO, + "triage": triageSLO +} as const; + +export interface SloStatus { + whichSlo: SloType; + withinSlo: boolean; +}; + +export function slo(issue: IssueSummary): SloStatus { + if (issue.whichSlo === "none") { + return { whichSlo: "none", withinSlo: true }; + } + const slo = sloMap[issue.whichSlo]; + + return { + whichSlo: issue.whichSlo, + withinSlo: + Temporal.Duration.compare(issue.sloTimeUsed, slo) < 0 + }; +} diff --git a/frontend/src/lib/triage.ts b/frontend/src/lib/triage.ts deleted file mode 100644 index dbc803c..0000000 --- a/frontend/src/lib/triage.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Defines constants and functions to check Github issue triage state. - -import type { IssueSummary } from "./repo-summaries"; - -export const PRIORITY_URGENT = "Priority: Urgent"; -export const PRIORITY_IMPORTANT = "Priority: Important"; -export const PRIORITY_EVENTUALLY = "Priority: Eventually"; -export const NEEDS_REPORTER_FEEDBACK = "Needs Reporter Feedback"; -export const BLOCKS_IMPLEMENTATION = "Blocks Implementation"; - -export function triaged(issue: IssueSummary) { - return [PRIORITY_URGENT, PRIORITY_IMPORTANT, PRIORITY_EVENTUALLY - ].some(priority => issue.labels.includes(priority)); - // TODO: Should issues go back to untriaged if they "need reporter feedback" and the reporter - // comments? Then they're untriaged until someone else (a repo member?) comments? -} - -/** - * @returns true if the issue is inside its SLO, which depends on the issue's labels. - */ -export function meetsSLO(issue: IssueSummary) { - -} diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro index 8eb9e59..7ea804d 100644 --- a/frontend/src/pages/index.astro +++ b/frontend/src/pages/index.astro @@ -1,105 +1,196 @@ --- -import BugStats from "@components/BugStats.astro"; -import { Temporal } from "@js-temporal/polyfill"; -import { AgeStats, RepoSummary } from "@lib/repo-summaries"; -import { z } from "zod"; +import { RepoSummary } from "@lib/repo-summaries"; +import { slo } from "@lib/slo"; import Layout from "../layouts/Layout.astro"; const repos = (await Astro.glob("../../../scanner/summaries/*/*.json")).map( (repo) => RepoSummary.parse(repo) ); -const GlobalStats = z.object({ - ageAtCloseMs: AgeStats, - openAgeMs: AgeStats, - closedFirstCommentLatencyMs: AgeStats, - openFirstCommentLatencyMs: AgeStats, -}); -const globalStats = GlobalStats.parse( - (await Astro.glob("../../../scanner/summaries/global.json"))[0] -); -const maxAge = Temporal.Duration.from({ - // Use the open age because it's bigger in practice. - seconds: Math.round(globalStats.openAgeMs[90].total("seconds") * 1.2), + +let totalTriageViolations = 0; +let totalUrgentViolations = 0; +let totalImportantViolations = 0; +let totalNeedTriage = 0; +let totalUrgent = 0; +let totalImportant = 0; +let totalOther = 0; + +const andFormatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }); + +const repoSummaries = repos.map((repo) => { + let triageViolations = 0; + let urgentViolations = 0; + let importantViolations = 0; + let needTriage = 0; + let urgent = 0; + let important = 0; + let other = 0; + for (const issue of repo.issues) { + const sloStatus = slo(issue); + switch (sloStatus.whichSlo) { + case "triage": + if (sloStatus.withinSlo) { + needTriage++; + } else { + triageViolations++; + } + break; + case "urgent": + if (sloStatus.withinSlo) { + urgent++; + } else { + urgentViolations++; + } + break; + case "important": + if (sloStatus.withinSlo) { + important++; + } else { + importantViolations++; + } + break; + case "none": + other++; + } + } + totalTriageViolations += triageViolations; + totalUrgentViolations += urgentViolations; + totalImportantViolations += importantViolations; + totalNeedTriage += needTriage; + totalUrgent += urgent; + totalImportant += important; + totalOther += other; + + let message :string[]= []; + if (triageViolations > 0 || urgentViolations > 0) { + if (triageViolations > 0) { + message.push(`${triageViolations} triage SLO violations`); + } + if (urgentViolations > 0) { + message.push(`${urgentViolations} urgent SLO violations`); + } + } else if (importantViolations > 0) { + message.push(`${importantViolations} important SLO violations`); + } else if (needTriage > 0 || urgent > 0) { + if (needTriage > 0) { + message.push(`${needTriage} issues that need triage`); + } + if (urgent > 0) { + message.push(`${urgent} urgent issues`); + } + } else if (important > 0) { + message.push(`${important} important issues`); + } + + return Object.assign({}, repo, { + triageViolations, + urgentViolations, + importantViolations, + needTriage, + urgent, + important, + other, + class_: + triageViolations > 0 || urgentViolations > 0 + ? "error" + : importantViolations > 0 + ? "warning" + : "", + message: andFormatter.format(message), + }); }); + +function sortKey(summary: typeof repoSummaries[0]) : {priority: number, count: number} { + if (summary.triageViolations > 0 || summary.urgentViolations > 0) { + return {priority: 4, count: summary.triageViolations + summary.urgentViolations}; + } else if (summary.importantViolations > 0) { + return {priority: 3, count: summary.importantViolations}; + } else if (summary.needTriage > 0 || summary.urgent > 0) { + return {priority: 2, count: summary.needTriage + summary.urgent}; + } else if (summary.important > 0) { + return {priority: 1, count: summary.important}; + } else { + return {priority: 0, count: summary.other}; + } +} +function compareByKey(a:typeof repoSummaries[0], b:typeof repoSummaries[0]): number { + const aKey = sortKey(a), bKey = sortKey(b); + if (aKey.priority != bKey.priority) { + return bKey.priority - aKey.priority; + } + return bKey.count - aKey.count; +} +repoSummaries.sort(compareByKey); + ---

Browser Spec Maintenance Status

- - - - - - - - - - - - - - - - - - - - - - { - repos.map((repo) => ( - - - - - )) - } - -
{globalStats.openAgeMs.count} total open bugs - -
{globalStats.ageAtCloseMs.count} total closed bugs - -
{globalStats.openFirstCommentLatencyMs.count} open - issues with a comment - -
{globalStats.closedFirstCommentLatencyMs.count} closed - issues with a comment - -
Open Bugs
- - {repo.org}/{repo.repo} - - - {repo.openAgeMs ? ( - - ) : ( - "No open bugs" - )} -
+ +

Across all browser specs, we have:

+ + +
+ { + repoSummaries.map((repo) => ( + <> +
+ + {repo.org}/{repo.repo} + +
+
+ {repo.message} +
+ + )) + } +
+ +