near-protocol-rewards
Version:
A transparent, metric-based rewards system for NEAR projects
237 lines (236 loc) • 8.37 kB
JavaScript
"use strict";
/**
* GitHub Metrics Collector
*
* Collects metrics from GitHub repositories including:
* - Commits
* - Pull requests
* - Issues and comments
*
* Features built-in rate limiting and error handling for API calls.
* Uses exponential backoff for retries on transient failures.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitHubCollector = void 0;
const rest_1 = require("@octokit/rest");
const base_1 = require("./base");
const errors_1 = require("../types/errors");
class GitHubCollector extends base_1.BaseCollector {
constructor({ token, repo, logger, rateLimiter, }) {
super(logger, rateLimiter);
const [owner, repoName] = repo.split("/");
if (!owner || !repoName) {
throw new Error("Invalid repository format. Expected 'owner/repo'");
}
this.owner = owner;
this.repo = repoName;
this.octokit = new rest_1.Octokit({ auth: token });
}
async testConnection() {
try {
await this.withRateLimit(async () => {
await this.octokit.rest.repos.get({
owner: this.owner,
repo: this.repo,
});
});
}
catch (error) {
this.error("Failed to test GitHub connection", { error });
throw error;
}
}
async collectCommitMetrics() {
const commits = await this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/commits', {
owner: this.owner,
repo: this.repo,
per_page: 100,
});
return response;
});
const authors = new Map();
const daily = new Array(7).fill(0);
let weekly = 0;
let monthly = 0;
for (const commit of commits) {
const login = commit.author?.login;
if (login) {
authors.set(login, (authors.get(login) || 0) + 1);
}
const date = new Date(commit.commit.author?.date || "");
const dayIndex = date.getDay();
if (!isNaN(dayIndex)) {
daily[dayIndex]++;
}
const now = new Date();
const daysDiff = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (daysDiff <= 7) {
weekly++;
}
if (daysDiff <= 30) {
monthly++;
}
}
return {
count: commits.length,
frequency: {
daily,
weekly,
monthly,
},
authors: Array.from(authors.entries()).map(([login, count]) => ({
login,
count,
})),
};
}
async collectPullRequestMetrics() {
const [openPRs, closedPRs] = await Promise.all([
this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls', {
owner: this.owner,
repo: this.repo,
state: "open",
per_page: 100,
});
return response;
}),
this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls', {
owner: this.owner,
repo: this.repo,
state: "closed",
per_page: 100,
});
return response;
}),
]);
const authors = new Set();
[...openPRs, ...closedPRs].forEach((pr) => {
if (pr.user?.login) {
authors.add(pr.user.login);
}
});
return {
open: openPRs.length,
merged: closedPRs.filter((pr) => pr.merged_at !== null).length,
closed: closedPRs.filter((pr) => pr.merged_at === null).length,
authors: Array.from(authors),
};
}
async collectReviewMetrics() {
const allPRs = await this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls', {
owner: this.owner,
repo: this.repo,
state: "all",
per_page: 100,
});
return response;
});
const authors = new Set();
let count = 0;
for (const pr of allPRs) {
const reviews = await this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews', {
owner: this.owner,
repo: this.repo,
pull_number: pr.number,
per_page: 100,
});
return response;
});
count += reviews.length;
reviews.forEach((review) => {
if (review.user?.login) {
authors.add(review.user.login);
}
});
}
return {
count,
authors: Array.from(authors),
};
}
async collectIssueMetrics() {
const [openIssues, closedIssues] = await Promise.all([
this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
owner: this.owner,
repo: this.repo,
state: "open",
per_page: 100,
});
return response;
}),
this.withRateLimit(async () => {
const response = await this.octokit.paginate('GET /repos/{owner}/{repo}/issues', {
owner: this.owner,
repo: this.repo,
state: "closed",
per_page: 100,
});
return response;
}),
]);
const participants = new Set();
[...openIssues, ...closedIssues].forEach((issue) => {
if (issue.user?.login) {
participants.add(issue.user.login);
}
if (issue.assignee?.login) {
participants.add(issue.assignee.login);
}
issue.assignees?.forEach((assignee) => {
if (assignee.login) {
participants.add(assignee.login);
}
});
});
return {
open: openIssues.length,
closed: closedIssues.length,
participants: Array.from(participants),
};
}
async collectMetrics() {
try {
const [commits, pullRequests, reviews, issues] = await Promise.all([
this.collectCommitMetrics(),
this.collectPullRequestMetrics(),
this.collectReviewMetrics(),
this.collectIssueMetrics(),
]);
return {
commits,
pullRequests,
reviews,
issues,
metadata: {
collectionTimestamp: Date.now(),
source: "github",
projectId: `${this.owner}/${this.repo}`,
},
};
}
catch (error) {
if (error &&
typeof error === 'object' &&
'status' in error &&
'message' in error &&
error.status === 403 &&
typeof error.message === 'string' &&
error.message.includes('not accessible by integration')) {
throw new errors_1.BaseError('GitHub permissions error: Please ensure your workflow has the correct permissions. ' +
'Add these permissions to your workflow file:\n\n' +
'permissions:\n' +
' contents: read\n' +
' issues: read\n' +
' pull-requests: read\n', errors_1.ErrorCode.UNAUTHORIZED, { originalError: error });
}
throw error;
}
}
}
exports.GitHubCollector = GitHubCollector;