Skip to content

Commit

Permalink
Separate agenda handling from the "soon" SLO.
Browse files Browse the repository at this point in the history
Initially give agenda items a 5-week SLO.

Count agenda time from the last time an issue was
added to the agenda, unlike other SLOs that count
the total time the relevant label was applied.
  • Loading branch information
jyasskin committed Mar 4, 2024
1 parent a574996 commit 9794a20
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 53 deletions.
52 changes: 38 additions & 14 deletions frontend/src/components/Issue.astro
Original file line number Diff line number Diff line change
@@ -1,30 +1,54 @@
---
import { formatRoundAge } from "@lib/formatRoundAge";
import type { IssueSummary } from "@lib/repo-summaries";
import { slo, sloMap } from "@lib/slo";
import { agendaSLO, slo, sloMap } from "@lib/slo";
import assert from "node:assert";
interface Props {
issue: IssueSummary;
// Show the issue's time on the agenda rather than its prioritized SLO time.
agenda?: boolean;
}
const { issue } = Astro.props;
const { issue, agenda } = Astro.props;
const { withinSlo } = slo(issue);
const { withinSlo, onAgendaTooLong } = slo(issue);
let timeToReport = issue.sloTimeUsed;
if (!withinSlo) {
if (issue.whichSlo === "none") {
throw new Error(
`Can't be out of SLO with a 'none' SLO type: ${JSON.stringify(
issue
)}`
);
const timeToReport = (function () {
if (agenda) {
assert(issue.onAgendaFor);
if (onAgendaTooLong) {
return issue.onAgendaFor.subtract(agendaSLO);
}
return issue.onAgendaFor;
} else {
if (withinSlo) {
return issue.sloTimeUsed;
} else {
if (issue.whichSlo === "none") {
throw new Error(
`Can't be out of SLO with a 'none' SLO type: ${JSON.stringify(
issue
)}`
);
}
return issue.sloTimeUsed.subtract(sloMap[issue.whichSlo]);
}
}
timeToReport = issue.sloTimeUsed.subtract(sloMap[issue.whichSlo]);
}
})();
---

<a href={issue.url}>{issue.title}</a>: {
withinSlo ? "on maintainers' plate" : <span class="error">out of SLO</span>
agenda ? (
onAgendaTooLong ? (
<span class="error">out of SLO</span>
) : (
"on the agenda"
)
) : withinSlo ? (
"on maintainers' plate"
) : (
<span class="error">out of SLO</span>
)
} for {formatRoundAge(timeToReport)}

<style is:global>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lib/published-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ export const SummaryJson = z.object({}).catchall(z.object({
triageViolations: z.number(),
urgentViolations: z.number(),
soonViolations: z.number(),
agendaViolations: z.number(),
needTriage: z.number(),
urgent: z.number(),
soon: z.number(),
agenda: z.number(),
other: z.number(),
}));
export type SummaryJson = z.infer<typeof SummaryJson>;
Expand All @@ -22,14 +24,17 @@ export const RepoJson = z.object({
triageViolations: z.number(),
urgentViolations: z.number(),
soonViolations: z.number(),
agendaViolations: z.number(),
needTriage: z.number(),
urgent: z.number(),
soon: z.number(),
agenda: z.number(),
other: z.number(),
}),
triage: IssueSummaryWithSlo.array(),
urgent: IssueSummaryWithSlo.array(),
soon: IssueSummaryWithSlo.array(),
agenda: IssueSummaryWithSlo.array(),
other: IssueSummaryWithSlo.array(),
});
export type RepoJson = z.infer<typeof RepoJson>;
3 changes: 3 additions & 0 deletions frontend/src/lib/repo-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const IssueSummaryInContent = z.object({
}).optional(),
sloTimeUsed: checkDuration,
whichSlo: SloType,
// For Agenda+ issues, this is how long since the label was most-recently added.
onAgendaFor: checkDuration.optional(),
stats: z.object({
numTimelineItems: z.number(),
numComments: z.number().optional(),
Expand All @@ -29,6 +31,7 @@ export type IssueSummaryInContent = z.infer<typeof IssueSummaryInContent>;
export const IssueSummary = IssueSummaryInContent.extend({
createdAt: instant,
sloTimeUsed: duration,
onAgendaFor: duration.optional(),
});
export type IssueSummary = z.infer<typeof IssueSummary>;

Expand Down
45 changes: 36 additions & 9 deletions frontend/src/lib/slo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import { Temporal } from "@js-temporal/polyfill";
import type { IssueSummary, SloType } from "./repo-summaries.js";

const triageSLO = Temporal.Duration.from({ days: 7 });
const urgentSLO = Temporal.Duration.from({ days: 14 });
const soonSLO = Temporal.Duration.from({ days: 91 });
export const triageSLO = Temporal.Duration.from({ days: 7 });
export const urgentSLO = Temporal.Duration.from({ days: 14 });
export const soonSLO = Temporal.Duration.from({ days: 91 });
// A 5-week SLO to address something on the agenda allows a twice-a-month meeting to miss the item
// once.
export const agendaSLO = Temporal.Duration.from({ days: 35 });

export const sloMap = {
"urgent": urgentSLO,
Expand All @@ -16,46 +19,61 @@ export const sloMap = {
export interface SloStatus {
whichSlo: SloType;
withinSlo: boolean;
onAgenda: boolean;
onAgendaTooLong: boolean;
};

export function slo(issue: Pick<IssueSummary, "whichSlo"|"sloTimeUsed">): SloStatus {
export function slo(issue: Pick<IssueSummary, "whichSlo" | "sloTimeUsed" | "onAgendaFor">): SloStatus {
const onAgenda = issue.onAgendaFor !== undefined;
const onAgendaTooLong = issue.onAgendaFor !== undefined &&
Temporal.Duration.compare(issue.onAgendaFor, agendaSLO) > 0;
if (issue.whichSlo === "none") {
return { whichSlo: "none", withinSlo: true };
return { whichSlo: "none", withinSlo: true, onAgenda, onAgendaTooLong };
}
const slo = sloMap[issue.whichSlo];

return {
whichSlo: issue.whichSlo,
withinSlo:
Temporal.Duration.compare(issue.sloTimeUsed, slo) < 0
Temporal.Duration.compare(issue.sloTimeUsed, slo) < 0,
onAgenda,
onAgendaTooLong,
};
}

export function cmpByTimeUsed(a: IssueSummary, b: IssueSummary) {
return Temporal.Duration.compare(b.sloTimeUsed, a.sloTimeUsed);
}
const zeroDuration = Temporal.Duration.from({ seconds: 0 });
export function cmpByAgendaUsed(a: IssueSummary, b: IssueSummary) {
return Temporal.Duration.compare(b.onAgendaFor ?? zeroDuration, a.onAgendaFor ?? zeroDuration);
}

export interface SloGroups {
untriaged: IssueSummary[];
urgent: IssueSummary[];
soon: IssueSummary[];
agenda: IssueSummary[];
triageViolations: IssueSummary[];
urgentViolations: IssueSummary[];
soonViolations: IssueSummary[];
agendaViolations: IssueSummary[];
other: IssueSummary[];
}
export function groupBySlo(issues: IssueSummary[]): SloGroups {
const result: SloGroups = {
untriaged: [],
urgent: [],
soon: [],
agenda: [],
triageViolations: [],
urgentViolations: [],
soonViolations: [],
agendaViolations: [],
other: [],
};
for (const issue of issues) {
const { whichSlo, withinSlo } = slo(issue);
const { whichSlo, withinSlo, onAgenda, onAgendaTooLong } = slo(issue);
switch (whichSlo) {
case "urgent":
if (withinSlo) {
Expand All @@ -82,9 +100,18 @@ export function groupBySlo(issues: IssueSummary[]): SloGroups {
result.other.push(issue);
break;
}
if (onAgendaTooLong) {
result.agendaViolations.push(issue);
} else if (onAgenda) {
result.agenda.push(issue);
}
}
for (const list of Object.values(result)) {
list.sort(cmpByTimeUsed);
for (const [key, list] of Object.entries(result)) {
if (key.startsWith('agenda')) {
list.sort(cmpByAgendaUsed);
} else {
list.sort(cmpByTimeUsed);
}
}
return result;
}
32 changes: 31 additions & 1 deletion frontend/src/pages/[org]/[repo].astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import GhLabel from "@components/GhLabel.astro";
import Issue from "@components/Issue.astro";
import Layout from "@layouts/Layout.astro";
import { IssueSummary } from "@lib/repo-summaries";
import { cmpByTimeUsed, groupBySlo } from "@lib/slo";
import { cmpByAgendaUsed, cmpByTimeUsed, groupBySlo } from "@lib/slo";
import * as ghLabels from "@lib/triage-labels";
import type {
GetStaticPaths,
Expand All @@ -29,6 +29,8 @@ const { details } = Astro.props as Props;
const {
untriaged,
triageViolations,
agenda,
agendaViolations,
soon,
soonViolations,
urgent,
Expand All @@ -38,6 +40,8 @@ urgent.push(...urgentViolations);
urgent.sort(cmpByTimeUsed);
soon.push(...soonViolations);
soon.sort(cmpByTimeUsed);
agenda.push(...agendaViolations);
agenda.sort(cmpByAgendaUsed);
untriaged.push(...triageViolations);
untriaged.sort(cmpByTimeUsed);
---
Expand Down Expand Up @@ -84,6 +88,17 @@ untriaged.sort(cmpByTimeUsed);
</li>
) : null
}
{
agenda.length > 0 ? (
<li>
<a href="#agenda">
{agendaViolations.length
? `${agendaViolations.length} issues on the agenda that have been waiting too long`
: `${agenda.length} issues on the agenda`}
</a>
</li>
) : null
}
{
soon.length > 0 ? (
<li>
Expand Down Expand Up @@ -127,6 +142,21 @@ untriaged.sort(cmpByTimeUsed);
) : null
}

{
agenda.length > 0 ? (
<>
<h2 id="agenda">Agenda</h2>
<ul>
{agenda.map((issue) => (
<li>
<Issue {issue} agenda />
</li>
))}
</ul>
</>
) : null
}

{
soon.length > 0 ? (
<>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/pages/[org]/[repo].json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ export const GET: APIRoute = ({ props }) => {
triageViolations: groups.triageViolations.length,
urgentViolations: groups.urgentViolations.length,
soonViolations: groups.soonViolations.length,
agendaViolations: groups.agendaViolations.length,
needTriage: groups.untriaged.length,
urgent: groups.urgent.length,
soon: groups.soon.length,
agenda: groups.agenda.length,
other: groups.other.length,
},
triage: groups.triageViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.untriaged),
urgent: groups.urgentViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.urgent),
soon: groups.soonViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.soon),
agenda: groups.agendaViolations.map(issue => Object.assign(issue, outOfSloObj)).concat(groups.agenda),
other: groups.other,
} satisfies RepoJson;
return new Response(JSON.stringify(summary));
Expand Down

0 comments on commit 9794a20

Please sign in to comment.