/** * GitHub Stats Route * * GET /api/profile/[username]/github-stats * Fetches comprehensive GitHub statistics for a user using GraphQL API */ import { NextRequest, NextResponse } from "next/server"; import { db } from "@/db"; import { users } from "@/db/schema"; import { eq, sql } from "drizzle-orm"; import { fetchGitHubContributions, calculateStreakFromContributions } from "@/lib/github-contributions"; const GITHUB_API = 'https://api.github.com'; const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'; interface GitHubStatsResponse { // Basic profile stats public_repos: number; followers: number; following: number; // Contribution stats totalContributions: number; contributionsByYear: { year: number; contributions: number }[]; // Commit stats totalCommits: number; // PR stats (lifetime across all GitHub) totalPRs: number; openPRs: number; mergedPRs: number; closedPRs: number; // Issue stats (lifetime) totalIssues: number; openIssues: number; closedIssues: number; // Review stats totalReviews: number; // Streak data currentStreak: number; longestStreak: number; // Stars received (on own repos) total_stars: number; // Contribution graph data contributionWeeks?: any[]; } export async function GET( request: NextRequest, { params }: { params: Promise<{ username: string }> } ) { try { const { username } = await params; const { searchParams } = new URL(request.url); const refresh = searchParams.get('refresh') === 'true'; if (!username) { return NextResponse.json({ error: "Username required" }, { status: 400 }); } // Try to get user's GitHub token for more complete data const userRecord = await db.select({ githubAccessToken: users.githubAccessToken }) .from(users) .where(sql`LOWER(${users.username}) = LOWER(${username})`) .limit(1); const userToken = userRecord[0]?.githubAccessToken; const token = userToken || process.env.GITHUB_TOKEN; if (!token) { return NextResponse.json({ error: "GitHub token not available" }, { status: 500 }); } // Fetch data in parallel for better performance const [profileData, contributionData, graphqlStats, totalStars] = await Promise.all([ fetchGitHubProfile(username, token), fetchGitHubContributions(username, userToken), fetchGitHubGraphQLStats(username, token), fetchTotalStars(username, token) ]); if (!profileData) { return NextResponse.json({ error: "Failed to fetch GitHub profile" }, { status: 404 }); } // Calculate streaks from contribution data let currentStreak = 0; let longestStreak = 0; if (contributionData) { const streakData = calculateStreakFromContributions(contributionData); currentStreak = streakData.currentStreak; longestStreak = streakData.longestStreak; } // Build response with accurate PR/Issue counts from GraphQL const stats: GitHubStatsResponse = { // Basic profile public_repos: profileData.public_repos || 0, followers: profileData.followers || 0, following: profileData.following || 0, // Contributions totalContributions: contributionData?.totalContributions || 0, contributionsByYear: graphqlStats.contributionsByYear || [], // Commits from contribution stats totalCommits: graphqlStats.totalCommits || 0, // PRs - accurate lifetime counts from GraphQL totalPRs: graphqlStats.totalPRs || 0, openPRs: graphqlStats.openPRs || 0, mergedPRs: graphqlStats.mergedPRs || 0, closedPRs: graphqlStats.closedPRs || 0, // Issues - accurate lifetime counts totalIssues: graphqlStats.totalIssues || 0, openIssues: graphqlStats.openIssues || 0, closedIssues: graphqlStats.closedIssues || 0, // Reviews totalReviews: graphqlStats.totalReviews || 0, // Streaks currentStreak, longestStreak, // Stars total_stars: totalStars, // Include contribution weeks for graph contributionWeeks: contributionData?.weeks }; return NextResponse.json(stats); } catch (error: any) { console.error("GitHub stats error:", error); return NextResponse.json({ error: "Failed to fetch GitHub stats" }, { status: 500 }); } } /** * Fetch comprehensive stats using GitHub GraphQL API * This gets accurate lifetime PR, Issue, Commit counts */ async function fetchGitHubGraphQLStats(username: string, token: string) { const query = ` query($username: String!) { user(login: $username) { contributionsCollection { totalCommitContributions totalPullRequestContributions totalIssueContributions totalPullRequestReviewContributions contributionYears } pullRequests(first: 1) { totalCount } openPRs: pullRequests(states: OPEN, first: 1) { totalCount } mergedPRs: pullRequests(states: MERGED, first: 1) { totalCount } closedPRs: pullRequests(states: CLOSED, first: 1) { totalCount } issues(first: 1) { totalCount } openIssues: issues(states: OPEN, first: 1) { totalCount } closedIssues: issues(states: CLOSED, first: 1) { totalCount } repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, PULL_REQUEST, ISSUE]) { totalCount } } } `; try { const response = await fetch(GITHUB_GRAPHQL_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables: { username } }) }); if (!response.ok) { console.error(`[GitHub GraphQL] API error: ${response.status}`); return getEmptyStats(); } const result = await response.json(); if (result.errors) { console.error('[GitHub GraphQL] Errors:', result.errors); return getEmptyStats(); } const user = result.data?.user; if (!user) { return getEmptyStats(); } const contrib = user.contributionsCollection; return { totalCommits: contrib?.totalCommitContributions || 0, totalReviews: contrib?.totalPullRequestReviewContributions || 0, contributionYears: contrib?.contributionYears || [], contributionsByYear: [], // Could fetch per-year if needed // PRs - lifetime counts totalPRs: user.pullRequests?.totalCount || 0, openPRs: user.openPRs?.totalCount || 0, mergedPRs: user.mergedPRs?.totalCount || 0, closedPRs: user.closedPRs?.totalCount || 0, // Issues - lifetime counts totalIssues: user.issues?.totalCount || 0, openIssues: user.openIssues?.totalCount || 0, closedIssues: user.closedIssues?.totalCount || 0, // Repos contributed to repositoriesContributedTo: user.repositoriesContributedTo?.totalCount || 0 }; } catch (error) { console.error('[GitHub GraphQL] Fetch error:', error); return getEmptyStats(); } } function getEmptyStats() { return { totalCommits: 0, totalReviews: 0, contributionYears: [], contributionsByYear: [], totalPRs: 0, openPRs: 0, mergedPRs: 0, closedPRs: 0, totalIssues: 0, openIssues: 0, closedIssues: 0, repositoriesContributedTo: 0 }; } async function fetchGitHubProfile(username: string, token: string) { try { const response = await fetch(`${GITHUB_API}/users/${username}`, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'OpenTriage' } }); if (!response.ok) return null; return await response.json(); } catch (error) { console.error("Error fetching GitHub profile:", error); return null; } } async function fetchTotalStars(username: string, token: string): Promise { try { // Fetch user's repos and sum stars const response = await fetch( `${GITHUB_API}/users/${username}/repos?per_page=100&sort=stars`, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'OpenTriage' } } ); if (!response.ok) return 0; const repos = await response.json(); return repos.reduce((sum: number, repo: any) => sum + (repo.stargazers_count || 0), 0); } catch (error) { console.error("Error fetching stars:", error); return 0; } }