Skip to content

Commit

Permalink
Format code, parameterise bug issue types
Browse files Browse the repository at this point in the history
  • Loading branch information
danielnixon committed Sep 6, 2024
1 parent de2512f commit fcbcabb
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 72 deletions.
23 changes: 11 additions & 12 deletions forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const calculateTicketTarget = async (
jiraBoardID: string,
jiraTicketID: string,
tickets: TicketResponse,
userSuppliedTicketTarget: number
userSuppliedTicketTarget: number,
): Promise<{
numberOfTicketsAboveTarget: number;
lowTicketTarget: number;
Expand All @@ -28,7 +28,7 @@ export const calculateTicketTarget = async (
const numberOfTicketsAboveTarget = tickets.issues.indexOf(jiraTicketID);
if (numberOfTicketsAboveTarget === -1) {
throw new Error(
`Ticket ${jiraTicketID} not found in ticket list for board ${jiraBoardID}`
`Ticket ${jiraTicketID} not found in ticket list for board ${jiraBoardID}`,
);
}

Expand All @@ -47,7 +47,7 @@ export const calculateTicketTarget = async (
// should we treat the two ratios differently, since more bugs tend to be created by
// feature tickets and bugs usually don't take as long as features?
highTicketTarget: Math.round(
ticketTarget + ticketTarget / bugRatio + ticketTarget / discoveryRatio
ticketTarget + ticketTarget / bugRatio + ticketTarget / discoveryRatio,
),
};
};
Expand All @@ -67,7 +67,7 @@ export const calculateTicketTarget = async (
export const simulations = async (
resolvedTicketCounts: readonly number[],
ticketTarget: number,
numSimulations: number
numSimulations: number,
): Promise<readonly number[]> => {
const results: number[] = Array(numSimulations).fill(0);

Expand All @@ -82,9 +82,8 @@ export const simulations = async (
let storiesDone = 0;
while (storiesDone <= ticketTarget) {
const numTimeIntervals = resolvedTicketCounts.length;
storiesDone += resolvedTicketCounts[
Math.floor(Math.random() * numTimeIntervals)
]!;
storiesDone +=
resolvedTicketCounts[Math.floor(Math.random() * numTimeIntervals)]!;
results[i]!++;
}
}
Expand All @@ -105,11 +104,11 @@ export const printPredictions = (
simulationResults: readonly number[],
numSimulations: number,
confidencePercentageThreshold: number,
durationInDays: number
durationInDays: number,
) => {
console.log(
`Amount of time required to ship ${lowTicketTarget} to ${highTicketTarget} tickets ` +
`(and the number of simulations that arrived at that result):`
`(and the number of simulations that arrived at that result):`,
);

const percentages: Record<string, number> = {};
Expand Down Expand Up @@ -160,11 +159,11 @@ export const printPredictions = (
console.log(
`${Number(numIntervalsPredicted) * durationInDays} days, ` +
`${Math.floor(
cumulativePercentages[numIntervalsPredicted] ?? 0
cumulativePercentages[numIntervalsPredicted] ?? 0,
)}% confidence ` +
`(${numSimulationsPredicting[numIntervalsPredicted]} simulation` +
// Pluralize
`${numSimulationsPredicting[numIntervalsPredicted] === 1 ? "" : "s"})`
`${numSimulationsPredicting[numIntervalsPredicted] === 1 ? "" : "s"})`,
);
}

Expand All @@ -176,6 +175,6 @@ export const printPredictions = (
}% confident all ` +
`${lowTicketTarget} to ${highTicketTarget} tickets will take no more than ${
Number(resultAboveThreshold) * durationInDays
} days to complete.`
} days to complete.`,
);
};
74 changes: 42 additions & 32 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const numDaysOfHistory =
Number.parseInt(process.env.NUM_WEEKS_OF_HISTORY ?? "10") * daysInWeek;

const confidencePercentageThreshold = Number.parseInt(
process.env.CONFIDENCE_PERCENTAGE_THRESHOLD ?? "80"
process.env.CONFIDENCE_PERCENTAGE_THRESHOLD ?? "80",
);
const numSimulations = Number.parseInt(process.env.NUM_SIMULATIONS ?? "1000");
// length and units are in separate variables
Expand All @@ -29,7 +29,7 @@ const timeLength = Number.parseInt(process.env.TIME_LENGTH ?? "2");
const timeUnit = process.env.TIME_UNIT ?? "weeks";

const userSuppliedTicketTarget = Number.parseInt(
process.env.TICKET_TARGET ?? "60"
process.env.TICKET_TARGET ?? "60",
);
const bugRatioOverride = process.env.BUG_RATIO
? Number.parseInt(process.env.BUG_RATIO)
Expand All @@ -38,6 +38,18 @@ const discoveryRatioOverride = process.env.DISCOVERY_RATIO
? Number.parseInt(process.env.DISCOVERY_RATIO)
: undefined;

// The default Jira bug/fault issue type. Overridable if you use something
// else (or if you use multiple types that should all be considered 'bugs' by the forecast)
const defaultBugIssueTypes = ["Bug"];
const bugIssueTypes =
process.env.BUG_ISSUE_TYPES === undefined ||
process.env.BUG_ISSUE_TYPES === null ||
process.env.BUG_ISSUE_TYPES?.trim() === ""
? defaultBugIssueTypes
: process.env.BUG_ISSUE_TYPES.split(",")
.map((issueType) => issueType.trim())
.filter((issueType) => issueType !== "");

// convert provided time interval into days
const durationInDays =
timeUnit === "days" ? timeLength : timeLength * daysInWeek;
Expand All @@ -51,7 +63,7 @@ const main = async () => {
jiraTicketID === undefined
) {
console.error(
`Usage: JIRA_HOST="example.com" JIRA_BOARD_ID=74 JIRA_TICKET_ID=ADE-1234 JIRA_USERNAME=foo JIRA_PASSWORD=bar npm run start`
`Usage: JIRA_HOST="example.com" JIRA_BOARD_ID=74 JIRA_TICKET_ID=ADE-1234 JIRA_USERNAME=foo JIRA_PASSWORD=bar npm run start`,
);
return;
}
Expand All @@ -60,7 +72,7 @@ const main = async () => {
!(timeUnit === "weeks" || timeUnit === "days" || timeUnit === undefined)
) {
console.error(
"Only weeks and days are supported for project interval time units"
"Only weeks and days are supported for project interval time units",
);
return;
}
Expand All @@ -74,12 +86,12 @@ const main = async () => {
jiraPassword,
jiraBoardID,
durationInDays,
numDaysOfHistory
numDaysOfHistory,
);

// All in progress or to do Jira tickets for the given board (either kanban or scrum).
console.log(
`Counting tickets ahead of ${jiraTicketID} in board ${jiraBoardID}...`
`Counting tickets ahead of ${jiraTicketID} in board ${jiraBoardID}...`,
);
const tickets = await jira.issuesForBoard();

Expand All @@ -96,49 +108,47 @@ const main = async () => {
: inferredJiraProjectIDs;

const bugRatio =
bugRatioOverride ?? (await jira.fetchBugRatio(jiraProjectIDs));
bugRatioOverride ??
(await jira.fetchBugRatio(jiraProjectIDs, bugIssueTypes));
const discoveryRatio =
discoveryRatioOverride ?? (await jira.fetchDiscoveryRatio(jiraProjectIDs));
const {
numberOfTicketsAboveTarget,
lowTicketTarget,
highTicketTarget,
} = await calculateTicketTarget(
bugRatio,
discoveryRatio,
jiraBoardID,
jiraTicketID,
tickets,
userSuppliedTicketTarget
);
discoveryRatioOverride ??
(await jira.fetchDiscoveryRatio(jiraProjectIDs, bugIssueTypes));
const { numberOfTicketsAboveTarget, lowTicketTarget, highTicketTarget } =
await calculateTicketTarget(
bugRatio,
discoveryRatio,
jiraBoardID,
jiraTicketID,
tickets,
userSuppliedTicketTarget,
);

console.log(
`There are ${tickets.issues.length} tickets in board ${jiraBoardID} that are either in progress or still to do. Of those, ${numberOfTicketsAboveTarget} tickets are ahead of ${jiraTicketID} in priority order.`
`There are ${tickets.issues.length} tickets in board ${jiraBoardID} that are either in progress or still to do. Of those, ${numberOfTicketsAboveTarget} tickets are ahead of ${jiraTicketID} in priority order.`,
);

console.log(`Project interval is ${timeLength} ${timeUnit}`);
console.log(
`The team's past performance will be measured based on tickets in project(s) ${jiraProjectIDs.join(
", "
", ",
)} that have been resolved in the last ${
numDaysOfHistory / durationInDays
} project intervals (${numDaysOfHistory} days of history will be considered in total).`
);
const resolvedTicketCounts = await jira.fetchResolvedTicketsPerTimeInterval(
jiraProjectIDs
} project intervals (${numDaysOfHistory} days of history will be considered in total).`,
);
const resolvedTicketCounts =
await jira.fetchResolvedTicketsPerTimeInterval(jiraProjectIDs);

await Promise.all(
resolvedTicketCounts.map(async (ticketsInTimeInterval, idx) => {
console.log(
`Resolved ${ticketsInTimeInterval.total} tickets in project interval ${
idx + 1
}:`
}:`,
);
// Print the ticket IDs. This is useful if you're running simulations regularly and saving
// the results.
console.log(ticketsInTimeInterval.issues.join(", "));
})
}),
);

if (isFinite(bugRatio)) {
Expand All @@ -149,22 +159,22 @@ const main = async () => {

if (isFinite(discoveryRatio)) {
console.log(
`1 new non-bug ticket created for every ${discoveryRatio} tickets resolved.`
`1 new non-bug ticket created for every ${discoveryRatio} tickets resolved.`,
);
} else {
console.log("No non-bug tickets created.");
}

console.log(
`If the team continues to create new tickets at this rate, we predict the ${lowTicketTarget} outstanding tickets ` +
`will have grown to ${highTicketTarget} tickets by the time they have all been completed.`
`will have grown to ${highTicketTarget} tickets by the time they have all been completed.`,
);

console.log(`Running ${numSimulations} simulations...`);
const simulationResults = await simulations(
resolvedTicketCounts.map((tickets) => tickets.total),
highTicketTarget,
numSimulations
numSimulations,
);

printPredictions(
Expand All @@ -173,7 +183,7 @@ const main = async () => {
simulationResults,
numSimulations,
confidencePercentageThreshold,
durationInDays
durationInDays,
);
};

Expand Down
Loading

0 comments on commit fcbcabb

Please sign in to comment.