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/fault correction rate #53

Merged
merged 13 commits into from
Jan 19, 2022
10 changes: 8 additions & 2 deletions src/database/database.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { StatisticService } from './statistic.service';
import { LanguageSchema } from './schemas/language.schema';
import { CommitSchema } from './schemas/commit.schema';
import { DeveloperFocus } from './statistics/developerFocus.service';
import { FaultCorrection } from './statistics/faultCorrection.service';

@Module({
imports: [
Expand All @@ -39,7 +40,12 @@ import { DeveloperFocus } from './statistics/developerFocus.service';
{ name: 'Commit', schema: CommitSchema },
]),
],
providers: [DatabaseService, StatisticService, DeveloperFocus],
exports: [DatabaseService, StatisticService, DeveloperFocus],
providers: [
DatabaseService,
StatisticService,
DeveloperFocus,
FaultCorrection,
],
exports: [DatabaseService, StatisticService, DeveloperFocus, FaultCorrection],
})
export class DatabaseModule {}
20 changes: 10 additions & 10 deletions src/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InjectModel } from '@nestjs/mongoose';
import { Model, OnlyFieldsOfType } from 'mongoose';
import {
Diff,
Releases,
Release,
Issue,
IssueEventTypes,
Language,
Expand Down Expand Up @@ -105,21 +105,21 @@ export class DatabaseService {

/**
* function to save releases
* @param releases
* @param release
* @param repoId
* @returns
*/
async saveReleases(releases: Releases, repoId: string) {
async saveReleases(release: Release, repoId: string) {
this.logger.debug('saving Releases to database');
const releasesModel = new this.releasesModel();

this.logger.debug(releases);
releasesModel.url = releases.url;
releasesModel.id = releases.id;
releasesModel.node_id = releases.node_id;
releasesModel.name = releases.name;
releasesModel.created_at = releases.created_at;
releasesModel.published_at = releases.published_at;
this.logger.debug(release);
releasesModel.url = release.url;
releasesModel.id = release.id;
releasesModel.node_id = release.node_id;
releasesModel.name = release.name;
releasesModel.created_at = release.created_at;
releasesModel.published_at = release.published_at;

const releasesModels = await releasesModel.save();

Expand Down
2 changes: 1 addition & 1 deletion src/database/schemas/issue.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Document, Schema as mSchema } from 'mongoose';
@Schema()
export class Issue {
@Prop([{ type: mSchema.Types.ObjectId, ref: 'Labels' }])
label: Label[];
label: Label[]; // TODO: rename this. but then every dev database will stop working...

@Prop({ type: mSchema.Types.ObjectId, ref: 'Assignee' })
assignee: Assignee;
Expand Down
118 changes: 118 additions & 0 deletions src/database/statistics/faultCorrection.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
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 { getIssueQuery } from './lib/issueQuery';
import { getReleaseQuery } from './lib/releaseQuery';

@Injectable()
export class FaultCorrection {
private readonly logger = new Logger(FaultCorrection.name);

constructor(
@InjectModel('Repository')
private readonly repoModel: Model<RepositoryDocument>,
) {}

/**
* The Fault Correction Rate describes the development team's capability to respond to bug reports.
* It is a quantitative indicator for which we consider all issues labeled bug (or some other equivalent tag) that existed at the time of a release.
* The Fault Correction Rate is the amount of closed bug issues divided by the total amount of bug issues.
*
* (release, issues) => {
* closed_bugs = issues[ label = bug, state = closed, closed_at <= release.created_at, closed_at >= release.previous().created_at ]
* open_bugs = issues[ label = bug, state = open, created_at <= release.created_at ]
* return |closed_bugs| / |closed_bugs| + |open_bugs|
* }
* @param repoIdent
* @param userLimit
*/
async faultCorrectionRate(
repoIdent: RepositoryNameDto,
labelNames?: string[],
) {
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 issuesInTimespan = this.createReleaseToIssue(releases, issues);

return this.calculateCorrectionRate(issuesInTimespan);
}

calculateCorrectionRate(
issuesInTimespan: Map<
Release,
{ closed: Issue[]; open: Issue[]; rate: number }
>,
) {
let avgRate = 0;
let noEmptyReleases = 0;
issuesInTimespan.forEach((issue) => {
const noOpen = issue.open.length;
const noClosed = issue.closed.length;
if (noOpen > 0 && noClosed > 0) {
issue.rate =
issue.closed.length / (issue.open.length + issue.closed.length);
avgRate += issue.rate;
} else {
noEmptyReleases += 1;
}
});
avgRate = avgRate / (issuesInTimespan.size - noEmptyReleases);

return { avgRate: avgRate, rawData: this.mapToJson(issuesInTimespan) };
}

mapToJson(
map: Map<Release, { closed: Issue[]; open: Issue[]; rate: number }>,
) {
const json = {};
map.forEach((value, key) => {
json[key.id] = { ...value, release: key };
});
return json;
}

createReleaseToIssue(releases: Release[], issues: Issue[]) {
const issuesInTimespan = new Map<
Release,
{ closed: Issue[]; open: Issue[]; rate: number }
>();
// we start at 1, because everything happening before the first release doesn't provide
// helpful information.
for (let i = 1; i < releases.length; i++) {
const currRelease = releases[i];
const prevRelease = releases[i - 1];
issuesInTimespan.set(currRelease, { open: [], closed: [], rate: 0 });

for (const currIssue of issues) {
if (
currIssue.state === 'closed' &&
currIssue.closed_at <= currRelease.created_at &&
currIssue.closed_at >= prevRelease.created_at
) {
// closed issues in interval
issuesInTimespan.get(currRelease).closed.push(currIssue);
}

if (
currIssue.created_at <= currRelease.created_at &&
currIssue.closed_at >= currRelease.created_at
) {
// open issues in interval
issuesInTimespan.get(currRelease).open.push(currIssue);
}
}
}

return issuesInTimespan;
}
}
67 changes: 67 additions & 0 deletions src/database/statistics/lib/issueQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Aggregate, Model } from 'mongoose';
import { RepositoryDocument } from 'src/database/schemas/repository.schema';
import { Issue } from 'src/github-api/model/PullRequest';
import { RepositoryNameDto } from 'src/github-api/model/Repository';
import { getRepoFilter } from './repoQuery';

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

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

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

export function getIssueQuery(
repoModel: Model<RepositoryDocument>,
repo: RepositoryNameDto,
labelNames?: string[],
): Aggregate<Issue[]> {
const query = repoModel
.aggregate()
.match(getRepoFilter(repo))
.project({ issuesWithEvents: 1 })
.unwind('$issuesWithEvents')
.lookup(lookupIssueWithEvents)
.unwind('$expandedIssuesWithEvents')
.lookup(lookupIssues)
.unwind('$expandedIssue')
.lookup(lookupLabels)
.replaceRoot('$expandedIssue');

if (labelNames) {
query.match(getMatchQueryForLabelNames(labelNames));
}
query
.addFields({ labels: '$expandedLabels' })
.project({ _id: 0, __v: 0, expandedLabels: 0, label: 0 })
.sort({ created_at: 1 });
return query;
}

function getMatchQueryForLabelNames(labelNames: string[]) {
return labelNames.reduce(
(acc, curr) => {
acc.$and.push({
expandedLabels: { $elemMatch: { name: curr } },
});
return acc;
},
{
$and: [],
},
);
}
27 changes: 27 additions & 0 deletions src/database/statistics/lib/releaseQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Model, Aggregate } from 'mongoose';
import { RepositoryDocument } from 'src/database/schemas/repository.schema';
import { Release } from 'src/github-api/model/PullRequest';
import { RepositoryNameDto } from 'src/github-api/model/Repository';
import { getRepoFilter } from './repoQuery';

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

export function getReleaseQuery(
repoModel: Model<RepositoryDocument>,
repo: RepositoryNameDto,
): Aggregate<Release[]> {
return repoModel
.aggregate()
.match(getRepoFilter(repo))
.unwind('$releases')
.lookup(releaseLookup)
.unwind('$expandedReleases')
.replaceRoot('expandedReleases')
.project({ _id: 0, __v: 0 })
.sort({ created_at: 1 });
}
8 changes: 8 additions & 0 deletions src/database/statistics/lib/repoQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RepositoryNameDto } from 'src/github-api/model/Repository';

export function getRepoFilter(repo: RepositoryNameDto) {
return {
repo: repo.repo,
owner: repo.owner,
};
}
8 changes: 7 additions & 1 deletion src/github-api/github-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Octokit } from 'octokit';
import { DatabaseService } from 'src/database/database.service';
import { StatisticService } from 'src/database/statistic.service';
import { DeveloperFocus } from 'src/database/statistics/developerFocus.service';
import { FaultCorrection } from 'src/database/statistics/faultCorrection.service';
import { PullRequest, RepositoryFile, Commit } from './model/PullRequest';
import { CreateRepositoryDto, RepositoryNameDto } from './model/Repository';

Expand Down Expand Up @@ -43,6 +44,7 @@ export class GithubApiService {
private statisticService: StatisticService,
private dbService: DatabaseService,
private devFocus: DeveloperFocus,
private faultCorrection: FaultCorrection,
) {
// init octokit
this.octokit = this.getOctokitClient();
Expand All @@ -66,7 +68,11 @@ export class GithubApiService {

// this.statisticService.avgTimeTillTicketWasAssigned(repoIdent);
//this.statisticService.workInProgress(repoIdent);
this.statisticService.faultCorrectionEfficiency(repoIdent);
return await this.faultCorrection.faultCorrectionRate(repoIdent, [
'support',
'awaiting response',
]);
//this.statisticService.faultCorrectionEfficiency(repoIdent);
// this.statisticService.workInProgress(repoIdent);
// this.devFocus.devSpreadTotal(
// repoIdent.owner,
Expand Down
6 changes: 3 additions & 3 deletions src/github-api/model/PullRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface IssueEventTypes {

export interface Issue {
state: string;
labels: Labels[];
labels: Label[];
assignee: Assignee;
assignees?: Assignees[];
milestone: Milestones;
Expand All @@ -27,7 +27,7 @@ export interface Issue {
node_id: string;
locked: boolean;
}
export interface Releases {
export interface Release {
url: string;
id: number;
node_id: string;
Expand All @@ -36,7 +36,7 @@ export interface Releases {
published_at: string;
}

export interface Labels {
export interface Label {
id: number;
node_id: string;
url: string;
Expand Down