src/workspace/workspace-stats.service.ts
Methods |
constructor(workspaceRepoRepository: Repository
|
||||||||||||||||||||||||||||||
|
Defined in src/workspace/workspace-stats.service.ts:29
|
||||||||||||||||||||||||||||||
|
Parameters :
|
| baseQueryBuilder |
baseQueryBuilder()
|
|
Defined in src/workspace/workspace-stats.service.ts:43
|
|
Returns :
SelectQueryBuilder<DbWorkspaceRepo>
|
| Async findContributorStatsByWorkspaceIdForUserId | ||||||||||||
findContributorStatsByWorkspaceIdForUserId(pageOptionsDto: MostActiveContributorsDto, id: string, userId: number | undefined)
|
||||||||||||
|
Defined in src/workspace/workspace-stats.service.ts:209
|
||||||||||||
|
Parameters :
Returns :
Promise<PageDto<DbContributorStat>>
|
| Async findRossByWorkspaceIdForUserId | ||||||||||||
findRossByWorkspaceIdForUserId(options: WorkspaceStatsOptionsDto, id: string, userId: number | undefined)
|
||||||||||||
|
Defined in src/workspace/workspace-stats.service.ts:164
|
||||||||||||
|
Parameters :
Returns :
Promise<DbWorkspaceRossIndex>
|
| Async findStatsByWorkspaceIdForUserId | ||||||||||||
findStatsByWorkspaceIdForUserId(options: WorkspaceStatsOptionsDto, id: string, userId: number | undefined)
|
||||||||||||
|
Defined in src/workspace/workspace-stats.service.ts:49
|
||||||||||||
|
Parameters :
Returns :
Promise<DbWorkspaceStats>
|
import { cpus } from "node:os";
import { Injectable, NotFoundException } from "@nestjs/common";
import { Repository, SelectQueryBuilder } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";
import PromisePool from "@supercharge/promise-pool/dist";
import { orderDbContributorStats } from "../timescale/common/most-active-contributors";
import { sanitizeRepos } from "../timescale/common/repos";
import { RepoDevstatsService } from "../timescale/repo-devstats.service";
import { IssuesGithubEventsService } from "../timescale/issues_github_events.service";
import { PullRequestGithubEventsService } from "../timescale/pull_request_github_events.service";
import { ForkGithubEventsService } from "../timescale/fork_github_events.service";
import { WatchGithubEventsService } from "../timescale/watch_github_events.service";
import { ContributionsPageDto } from "../timescale/dtos/contrib-page.dto";
import { DbContributorStat } from "../timescale/entities/contributor_devstat.entity";
import { ContributionPageMetaDto } from "../timescale/dtos/contrib-page-meta.dto";
import { MostActiveContributorsDto } from "../timescale/dtos/most-active-contrib.dto";
import { PageDto } from "../common/dtos/page.dto";
import { ContributorDevstatsService } from "../timescale/contrib-stats.service";
import { DbWorkspaceRepo } from "./entities/workspace-repos.entity";
import { WorkspaceService } from "./workspace.service";
import { canUserViewWorkspace } from "./common/memberAccess";
import { DbWorkspaceStats } from "./entities/workspace-stats.entity";
import { WorkspaceStatsOptionsDto } from "./dtos/workspace-stats.dto";
import { DbWorkspaceRossIndex } from "./entities/workspace-ross.entity";
import { WorkspaceContributorsService } from "./workspace-contributors.service";
@Injectable()
export class WorkspaceStatsService {
constructor(
@InjectRepository(DbWorkspaceRepo, "ApiConnection")
private workspaceRepoRepository: Repository<DbWorkspaceRepo>,
private workspaceService: WorkspaceService,
private worksapceContributorsService: WorkspaceContributorsService,
private pullRequestGithubEventsService: PullRequestGithubEventsService,
private issueGithubEventsService: IssuesGithubEventsService,
private forkGithubEventsService: ForkGithubEventsService,
private watchGithubEventsService: WatchGithubEventsService,
private repoDevstatsService: RepoDevstatsService,
private contributorDevstatService: ContributorDevstatsService
) {}
baseQueryBuilder(): SelectQueryBuilder<DbWorkspaceRepo> {
const builder = this.workspaceRepoRepository.createQueryBuilder("workspace_repos");
return builder;
}
async findStatsByWorkspaceIdForUserId(
options: WorkspaceStatsOptionsDto,
id: string,
userId: number | undefined
): Promise<DbWorkspaceStats> {
const range = options.range!;
const prevDaysStartDate = options.prev_days_start_date!;
const workspace = await this.workspaceService.findOneById(id);
/*
* viewers, editors, and owners can see what repos belongs to a workspace
*/
const canView = canUserViewWorkspace(workspace, userId);
if (!canView) {
throw new NotFoundException();
}
// get the repos
const queryBuilder = this.baseQueryBuilder();
queryBuilder
.withDeleted()
.leftJoinAndSelect(
"workspace_repos.repo",
"workspace_repos_repo",
"workspace_repos.repo_id = workspace_repos_repo.id"
)
.where("workspace_repos.deleted_at IS NULL")
.andWhere("workspace_repos.workspace_id = :id", { id });
if (options.repos) {
const sanitizedRepos = sanitizeRepos(options.repos);
queryBuilder.andWhere("LOWER(workspace_repos_repo.full_name) IN (:...sanitizedRepos)", { sanitizedRepos });
}
const entities = await queryBuilder.getMany();
const { results } = await PromisePool.withConcurrency(Math.max(2, cpus().length))
.for(entities)
.handleError((error) => {
throw error;
})
.process(async (entity) => {
const localResult = new DbWorkspaceStats();
// get PR stats for each repo found through filtering
const prStats = await this.pullRequestGithubEventsService.findPrStatsByRepo(
entity.repo.full_name,
range,
prevDaysStartDate
);
localResult.pull_requests.opened += prStats.open_prs;
localResult.pull_requests.merged += prStats.accepted_prs;
localResult.pull_requests.velocity += prStats.pr_velocity;
// get issue stats for each repo found through filtering
const issuesStats = await this.issueGithubEventsService.findIssueStatsByRepo(
entity.repo.full_name,
range,
prevDaysStartDate
);
localResult.issues.opened += issuesStats.opened_issues;
localResult.issues.closed += issuesStats.closed_issues;
localResult.issues.velocity += issuesStats.issue_velocity;
// get the repo's activity ratio
const activityRatio = await this.repoDevstatsService.calculateRepoActivityRatio(entity.repo.full_name, range);
localResult.repos.activity_ratio += activityRatio;
// get forks within time range
const forks = await this.forkGithubEventsService.genForkHistogram({ range, repo: entity.repo.full_name });
forks.forEach((bucket) => (localResult.repos.forks += bucket.forks_count));
// get stars (watch events) within time range
const stars = await this.watchGithubEventsService.genStarsHistogram({ range, repo: entity.repo.full_name });
stars.forEach((bucket) => (localResult.repos.stars += bucket.star_count));
return localResult;
});
const finalResult = results.reduce((acc, curr) => {
acc.pull_requests.opened += curr.pull_requests.opened;
acc.pull_requests.merged += curr.pull_requests.merged;
acc.pull_requests.velocity += curr.pull_requests.velocity;
acc.issues.opened += curr.issues.opened;
acc.issues.closed += curr.issues.closed;
acc.issues.velocity += curr.issues.velocity;
acc.repos.activity_ratio += curr.repos.activity_ratio;
acc.repos.forks += curr.repos.forks;
acc.repos.stars += curr.repos.stars;
return acc;
}, new DbWorkspaceStats());
finalResult.pull_requests.velocity /= entities.length;
finalResult.issues.velocity /= entities.length;
finalResult.repos.activity_ratio /= entities.length;
// activity ratio is currently the only stat that is used to inform health
finalResult.repos.health = finalResult.repos.activity_ratio;
return finalResult;
}
async findRossByWorkspaceIdForUserId(
options: WorkspaceStatsOptionsDto,
id: string,
userId: number | undefined
): Promise<DbWorkspaceRossIndex> {
const range = options.range!;
const workspace = await this.workspaceService.findOneById(id);
/*
* viewers, editors, and owners can see what repos belongs to a workspace
*/
const canView = canUserViewWorkspace(workspace, userId);
if (!canView) {
throw new NotFoundException();
}
const result = new DbWorkspaceRossIndex();
// get the repos
const queryBuilder = this.baseQueryBuilder();
queryBuilder
.withDeleted()
.leftJoinAndSelect(
"workspace_repos.repo",
"workspace_repos_repo",
"workspace_repos.repo_id = workspace_repos_repo.id"
)
.where("workspace_repos.deleted_at IS NULL")
.andWhere("workspace_repos.workspace_id = :id", { id });
const entities = await queryBuilder.getMany();
const entityRepos = entities.map((entity) => entity.repo.full_name);
const rossIndex = await this.pullRequestGithubEventsService.findRossIndexByRepos(entityRepos, range);
const rossContributors = await this.pullRequestGithubEventsService.findRossContributorsByRepos(entityRepos, range);
result.ross = rossIndex;
result.contributors = rossContributors;
return result;
}
async findContributorStatsByWorkspaceIdForUserId(
pageOptionsDto: MostActiveContributorsDto,
id: string,
userId: number | undefined
): Promise<PageDto<DbContributorStat>> {
const workspace = await this.workspaceService.findOneById(id);
/*
* viewers, editors, and owners can see what repos belongs to a workspace
*/
const canView = canUserViewWorkspace(workspace, userId);
if (!canView) {
throw new NotFoundException();
}
const allContributors = await this.worksapceContributorsService.findAllContributors(id);
if (allContributors.length === 0) {
return new ContributionsPageDto(
new Array<DbContributorStat>(),
new ContributionPageMetaDto({ itemCount: 0, pageOptionsDto }, 0)
);
}
/*
* ignores 2 usernames that cause problems when crunching this data:
*
* 1. Usernames that somehow are an empty string. This shouldn't happen
* since a username is more or less a required field in the users table.
* but we have seen this from time to time which can cause problems trying
* to crunch timescale data on all an empty username
*
* 2. Ignores bot accounts: many bot accounts make an astronomical number of
* commits / comments / reviews etc. etc. And attempting to crunch all that data
* for the bot accounts won't work and would require massive resources.
*/
const contributors = allContributors
.map((c) => (c.contributor.login ? c.contributor.login.toLowerCase() : ""))
.filter((c) => c !== "" && !c.endsWith("[bot]"));
if (contributors.length === 0) {
return new ContributionsPageDto(
new Array<DbContributorStat>(),
new ContributionPageMetaDto({ itemCount: 0, pageOptionsDto }, 0)
);
}
const userStats = await this.contributorDevstatService.findAllContributorStats(pageOptionsDto, contributors);
orderDbContributorStats(pageOptionsDto, userStats);
const { skip } = pageOptionsDto;
const limit = pageOptionsDto.limit!;
const slicedUserStats = userStats.slice(skip, skip + limit);
let totalCount = 0;
userStats.forEach((entity) => {
totalCount += entity.total_contributions;
});
const pageMetaDto = new ContributionPageMetaDto({ itemCount: slicedUserStats.length, pageOptionsDto }, totalCount);
return new ContributionsPageDto(slicedUserStats, pageMetaDto);
}
}