Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/mean fault correction efficiency #61

Merged
merged 9 commits into from
Jan 21, 2022
187 changes: 0 additions & 187 deletions src/database/statistic.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,191 +557,4 @@ export class StatisticService {

return avg;
}

/**
* The Time to Resolution describes the development team's capability to respond to bug reports. It assesses the time it took to resolve a single bug report.
* We calculate this information point for resolved issues labeled bug (or some other equivalent label).
* (issue[label = "bug", state="closed"], T_bugfix) => {
* return (issue.closed_at - issue.created_at)
* }
* @param repoIdent
* @param labelName
* @param userLimit
*/
async timeToResolution(
repoIdent: RepositoryNameDto,
labelName?: string,
userLimit?: number,
) {
const limit = userLimit ? userLimit : 100;

const filter = {
repo: repoIdent.repo,
owner: repoIdent.owner,
};

const getIssuesWithEvents = {
from: 'issuewithevents',
localField: 'issuesWithEvents',
foreignField: '_id',
as: 'expandedIssuesWithEvents',
};

const getIssue = {
from: 'issues',
localField: 'expandedIssuesWithEvents.issue',
foreignField: '_id',
as: 'expandedIssue',
};

const getReleases = {
from: 'releases',
localField: 'releases',
foreignField: '_id',
as: 'expandedReleases',
};

const getLabel = {
from: 'labels',
localField: 'expandedIssue.label',
foreignField: '_id',
as: 'expandedLabels',
};

const res: { _id: string; avg: number }[] = await this.repoModel
.aggregate()
.match(filter)
.project({ issuesWithEvents: 1 })
.unwind('$issuesWithEvents')
.lookup(getIssuesWithEvents)
.unwind('$expandedIssuesWithEvents')
.lookup(getIssue)
.unwind('$expandedIssue')
// TODO: for now we ignore the labels and calculate the time for every ticket regardless of the label
// .lookup(getLabel)
// .unwind('$expandedLabels')
// .match({ 'expandedLabels.name': { $exists: true, $eq: 'bug' } })
.match({ 'expandedIssue.closed_at': { $exists: true, $ne: null } })
.project({
issueCreatedAtTime: { $toDate: '$expandedIssue.created_at' },
issueClosedAtTime: { $toDate: '$expandedIssue.closed_at' },
})
.addFields({
subtractedDate: {
$subtract: ['$issueClosedAtTime', '$issueCreatedAtTime'],
},
})
.group({
_id: '$_id',
avg: { $avg: '$subtractedDate' },
})
.exec();

this.logger.log(
`average time to resolution for each ticket is ${msToDateString(
res[0].avg,
)}`,
);

return res;
}

/**
* The Fault Correction Efficiency describes the development team's capability to respond to bug reports.
* In more detail, it assesses if a single fault was corrected within the time frame the organization aims to adhere to for fault corrections.
* We calculate this qualitative indicator for resolved issues labeled bug (or some other equivalent label).
* A value greater than 1 indicates that the fault was not corrected within the desired time.
* A value less than 1 indicates that the fault was corrected within the desired time.
*
* (issue[label = "bug", state="closed"], T_bugfix) => {
* return (issue.closed_at - issue.created_at) / T_bugfix
* }
* @param repoIdent
* @param userLimit
*/
async faultCorrectionEfficiency(
repoIdent: RepositoryNameDto,
userLimit?: number,
timeFrame?: number,
) {
const limit = userLimit ? userLimit : 100;
// This constiable defines the fixed time set for the bugs to be resolved.
// Since such an information cannot be derived from git (milestones can be looked at,
// however they are hardly properly utilized by most projects).
// Although information like this can be derived from Jira, but for now, it is manually defined.
// duration value is considered to be 14 Days, i.e, 1209600000 ms.
const duration = timeFrame ? timeFrame : 1209600000;

const filter = {
repo: repoIdent.repo,
owner: repoIdent.owner,
};

const getIssuesWithEvents = {
from: 'issuewithevents',
localField: 'issuesWithEvents',
foreignField: '_id',
as: 'expandedIssuesWithEvents',
};

const getIssue = {
from: 'issues',
localField: 'expandedIssuesWithEvents.issue',
foreignField: '_id',
as: 'expandedIssue',
};

const getLabel = {
from: 'labels',
localField: 'expandedIssue.label',
foreignField: '_id',
as: 'expandedLabels',
};

const res: { _id: string; count: number }[] = await this.repoModel
.aggregate()
.match(filter)
.project({ issuesWithEvents: 1 })
.unwind('$issuesWithEvents')
.lookup(getIssuesWithEvents)
.unwind('$expandedIssuesWithEvents')
.lookup(getIssue)
.unwind('$expandedIssue')
.lookup(getLabel)
.unwind('$expandedLabels')
// .match({ 'expandedLabels.name': { $exists: true, $eq: 'bug' } })
// .match({ 'expandedIssue.closed_at': { $exists: true, $ne: null } })
.project({
Issue_created_at_Time: { $toDate: '$expandedIssue.created_at' },
Issue_closed_at_Time: { $toDate: '$expandedIssue.closed_at' },
})
.addFields({
_id: null,
subtractedDate: {
$subtract: ['$Issue_closed_at_Time', '$Issue_created_at_Time'],
},
})
.exec();

const inTime = res.reduce((prev, curr) => {
if (this.wasCorrectedInTime(curr['subtractedDate'], duration)) {
return prev + 1;
} else {
return prev;
}
}, 0);
console.log(inTime);

//prints the Fault correction efficiency for each element.
this.logger.log(
`${inTime} of ${res.length} tickets were corrected in ${msToDateString(
duration,
)} `,
);
return { noInTime: inTime, total: res.length };
}

wasCorrectedInTime(time: number, maximumDuration: number): boolean {
return time > maximumDuration;
}
}
25 changes: 25 additions & 0 deletions src/database/statistics/faultCorrection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RepositoryNameDto } from 'src/github-api/model/Repository';
import { RepositoryDocument } from '../schemas/repository.schema';
import {
calculateAvgCapability,
calculateAvgEfficiency,
calculateAvgRate,
mapReleasesToIssues,
} from './issueUtil';
Expand Down Expand Up @@ -100,4 +101,28 @@ export class FaultCorrection {

return { avgCapability, rawData: transformMapToObject(capabilityMap) };
}

async faultCorrectionEfficiency(
repoIdent: RepositoryNameDto,
labelNames?: string[],
timeToCorrect: number = 14 * 24 * 60 * 60 * 1000,
) {
const queries = [
getReleaseQuery(this.repoModel, repoIdent).exec(),
getIssueQuery(this.repoModel, repoIdent, labelNames).exec(),
];
const promiseResults = await Promise.all(queries);

const releases = promiseResults[0] as Release[];
const issues = promiseResults[1] as Issue[];

const releaseIssueMap = mapReleasesToIssues(releases, issues);

const { efficiencyMap, avgEfficiency } = calculateAvgEfficiency(
releaseIssueMap,
timeToCorrect,
);

return { avgEfficiency, rawData: transformMapToObject(efficiencyMap) };
}
}
34 changes: 31 additions & 3 deletions src/database/statistics/featureCompletion.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { Model } from 'mongoose';
import { Issue, Release } from 'src/github-api/model/PullRequest';
import { RepositoryNameDto } from 'src/github-api/model/Repository';
import { RepositoryDocument } from '../schemas/repository.schema';
import { calculateAvgCapability, calculateAvgRate, mapReleasesToIssues } from './issueUtil';
import {
calculateAvgCapability,
calculateAvgEfficiency,
calculateAvgRate,
mapReleasesToIssues,
} from './issueUtil';
import { getIssueQuery } from './lib/issueQuery';
import { getReleaseQuery } from './lib/releaseQuery';
import { transformMapToObject } from './lib/transformMapToObject';
Expand Down Expand Up @@ -53,7 +58,6 @@ export class FeatureCompletion {
};
}


/**
* The Feature Completion Capability describes the development team's capability to add features to the project.
* In more detail, it assesses the rate of features completed within the time frame the organization aims to adhere to for feature completion.
Expand All @@ -76,7 +80,7 @@ export class FeatureCompletion {
async featureCompletionCapability(
repoIdent: RepositoryNameDto,
labelNames?: string[],
timeToComplete: number = 14*24*60*60*1000,
timeToComplete: number = 14 * 24 * 60 * 60 * 1000,
) {
const queries = [
getReleaseQuery(this.repoModel, repoIdent).exec(),
Expand All @@ -96,4 +100,28 @@ export class FeatureCompletion {

return { avgCapability, rawData: transformMapToObject(capabilityMap) };
}

async featureCompletionEfficiency(
repoIdent: RepositoryNameDto,
labelNames?: string[],
timeToComplete: number = 14 * 24 * 60 * 60 * 1000,
) {
const queries = [
getReleaseQuery(this.repoModel, repoIdent).exec(),
getIssueQuery(this.repoModel, repoIdent, labelNames).exec(),
];
const promiseResults = await Promise.all(queries);

const releases = promiseResults[0] as Release[];
const issues = promiseResults[1] as Issue[];

const releaseIssueMap = mapReleasesToIssues(releases, issues);

const { efficiencyMap, avgEfficiency } = calculateAvgEfficiency(
releaseIssueMap,
timeToComplete,
);

return { avgEfficiency, rawData: transformMapToObject(efficiencyMap) };
}
}
72 changes: 70 additions & 2 deletions src/database/statistics/issueUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function calculateAvgCapability(
if (!currData.closed.length) {
noOfEmptyReleases += 1;
} else {
sumOfCapabilities += capability.capability
sumOfCapabilities += capability.capability;
}
capabilityMap.set(currData.release.id, {
...capability,
Expand All @@ -121,7 +121,7 @@ export function calculateAvgCapability(

return {
capabilityMap,
avgCapability: sumOfCapabilities / releaseIssueMap.size - noOfEmptyReleases,
avgCapability: sumOfCapabilities / (releaseIssueMap.size - noOfEmptyReleases),
};
}

Expand Down Expand Up @@ -155,3 +155,71 @@ function calculateCapability(
failures,
};
}

export function calculateAvgEfficiency(
releaseIssueMap: Map<
number,
{ closed: Issue[]; open: Issue[]; release: Release }
>,
allowedTime: number,
) {
const efficiencyMap = new Map<
number,
{
issues: (Issue & { efficiency: number })[];
efficiency: number;
release: Release;
}
>();

let sumOfEfficiencies = 0;
let noOfEmptyReleases = 0;

releaseIssueMap.forEach((currData) => {
const efficiency = calculateEfficiency(currData, allowedTime);
if (!currData.closed.length) {
noOfEmptyReleases += 1;
} else {
sumOfEfficiencies += efficiency.efficiency;
}
efficiencyMap.set(currData.release.id, {
...efficiency,
release: currData.release,
});
});

return {
efficiencyMap,
avgEfficiency: sumOfEfficiencies / (releaseIssueMap.size - noOfEmptyReleases),
};
}

function calculateEfficiency(
data: {
closed: Issue[];
},
allowedTime: number,
): { efficiency: number; issues: (Issue & { efficiency: number })[] } {
const issues: (Issue & { efficiency: number })[] = [];

let sumOfEfficiencies = 0;
for (const currIssue of data.closed) {
const closedAt = new Date(currIssue.closed_at).valueOf();
const createdAt = new Date(currIssue.created_at).valueOf();
const implementationTime = closedAt - createdAt;

const efficiency = implementationTime / allowedTime;
sumOfEfficiencies += efficiency;
issues.push({ ...currIssue, efficiency });
}

let efficiency = 0;
if (issues.length) {
efficiency = sumOfEfficiencies / issues.length;
}

return {
efficiency,
issues,
};
}
Loading