File

src/workspace/workspace-stats.service.ts

Index

Methods

Constructor

constructor(workspaceRepoRepository: Repository, workspaceService: WorkspaceService, worksapceContributorsService: WorkspaceContributorsService, pullRequestGithubEventsService: PullRequestGithubEventsService, issueGithubEventsService: IssuesGithubEventsService, forkGithubEventsService: ForkGithubEventsService, watchGithubEventsService: WatchGithubEventsService, repoDevstatsService: RepoDevstatsService, contributorDevstatService: ContributorDevstatsService)
Parameters :
Name Type Optional
workspaceRepoRepository Repository<DbWorkspaceRepo> No
workspaceService WorkspaceService No
worksapceContributorsService WorkspaceContributorsService No
pullRequestGithubEventsService PullRequestGithubEventsService No
issueGithubEventsService IssuesGithubEventsService No
forkGithubEventsService ForkGithubEventsService No
watchGithubEventsService WatchGithubEventsService No
repoDevstatsService RepoDevstatsService No
contributorDevstatService ContributorDevstatsService No

Methods

baseQueryBuilder
baseQueryBuilder()
Returns : SelectQueryBuilder<DbWorkspaceRepo>
Async findContributorStatsByWorkspaceIdForUserId
findContributorStatsByWorkspaceIdForUserId(pageOptionsDto: MostActiveContributorsDto, id: string, userId: number | undefined)
Parameters :
Name Type Optional
pageOptionsDto MostActiveContributorsDto No
id string No
userId number | undefined No
Async findRossByWorkspaceIdForUserId
findRossByWorkspaceIdForUserId(options: WorkspaceStatsOptionsDto, id: string, userId: number | undefined)
Parameters :
Name Type Optional
options WorkspaceStatsOptionsDto No
id string No
userId number | undefined No
Returns : Promise<DbWorkspaceRossIndex>
Async findStatsByWorkspaceIdForUserId
findStatsByWorkspaceIdForUserId(options: WorkspaceStatsOptionsDto, id: string, userId: number | undefined)
Parameters :
Name Type Optional
options WorkspaceStatsOptionsDto No
id string No
userId number | undefined No
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);
  }
}

results matching ""

    No results matching ""