From 567200791f238e81f5cb40845937f70bd934cd9d Mon Sep 17 00:00:00 2001 From: Zishan Qureshi Date: Tue, 19 May 2026 14:44:47 +0530 Subject: [PATCH 1/4] fix: implement review participation metrics and variables --- src/app/api/metrics/prs/route.ts | 66 +++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index fb28df2b..e401d1aa 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -17,81 +17,103 @@ interface PRMetricsBase { total: number; avgReviewHours: number; mergeRate: number; + reviewsGiven: number; } -async function fetchPRMetrics(token: string): Promise { +async function fetchPRMetrics(token: string, githubLogin: string): Promise { const searchRes = await fetch( - `${GITHUB_API}/search/issues?q=type:pr+author:@me&per_page=100`, + `${GITHUB_API}/search/issues?q=type:pr+author:${githubLogin}&per_page=100`, { headers: { Authorization: `Bearer ${token}` }, cache: "no-store", } ); - if (!searchRes.ok) { + const reviewRes = await fetch( + `${GITHUB_API}/search/issues?q=type:pr+reviewed-by:${githubLogin}&per_page=100`, + { + headers: { Authorization: `Bearer ${token}` }, + cache: "no-store", + } + ); + + if (!searchRes.ok || !reviewRes.ok) { throw new Error("GitHub API error"); } const data = (await searchRes.json()) as { total_count: number; - items: Array<{ state: string; created_at: string; closed_at: string | null }>; + items: Array<{ + state: string; + created_at: string; + closed_at: string | null; + pull_request?: { merged_at: string | null }; + }>; }; + const reviewData = (await reviewRes.json()) as { total_count: number }; + const open = data.items.filter((pr) => pr.state === "open").length; - const merged = data.items.filter((pr) => pr.state === "closed").length; + const merged = data.items.filter((pr) => pr.pull_request?.merged_at != null).length; - const closedPRs = data.items.filter((pr) => pr.closed_at); + const mergedPRs = data.items.filter((pr) => pr.pull_request?.merged_at != null); const avgReviewMs = - closedPRs.length > 0 - ? closedPRs.reduce( + mergedPRs.length > 0 + ? mergedPRs.reduce( (sum, pr) => sum + - (new Date(pr.closed_at!).getTime() - + (new Date(pr.pull_request!.merged_at!).getTime() - new Date(pr.created_at).getTime()), 0 - ) / closedPRs.length + ) / mergedPRs.length : 0; + const sampleTotal = data.items.length; + return { open, merged, total: data.total_count, avgReviewHours: Math.round(avgReviewMs / 3600000), - mergeRate: data.total_count > 0 ? merged / data.total_count : 0, + mergeRate: sampleTotal > 0 ? merged / sampleTotal : 0, + reviewsGiven: reviewData.total_count, }; } function formatPRMetrics(metrics: PRMetricsBase) { + const ratio = metrics.total > 0 ? (metrics.reviewsGiven / metrics.total).toFixed(2) : "0.00"; + return { open: metrics.open, merged: metrics.merged, total: metrics.total, avgReviewHours: metrics.avgReviewHours, - mergeRate: - metrics.total > 0 - ? `${Math.round(metrics.mergeRate * 100)}%` - : "0%", + mergeRate: metrics.total > 0 ? `${Math.round(metrics.mergeRate * 100)}%` : "0%", + reviewsGiven: metrics.reviewsGiven, + reviewRatio: ratio, }; } export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken) { + + if (!session?.accessToken || !session?.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const accountId = req.nextUrl.searchParams.get("accountId"); + const username = req.nextUrl.searchParams.get("username") || session.githubLogin; if (!accountId) { try { - const result = await fetchPRMetrics(session.accessToken); + const result = await fetchPRMetrics(session.accessToken, username); return Response.json(formatPRMetrics(result)); } catch { return Response.json({ error: "GitHub API error" }, { status: 502 }); } } - if (!session.githubId || !session.githubLogin) { + if (!session.githubId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -116,7 +138,7 @@ export async function GET(req: NextRequest) { ); const results = await Promise.allSettled( - accounts.map((account) => fetchPRMetrics(account.token)) + accounts.map((account) => fetchPRMetrics(account.token, username)) ); const merged = mergeMetrics(results, (a, b) => { @@ -132,8 +154,8 @@ export async function GET(req: NextRequest) { merged: mergedCount, total, avgReviewHours: Math.round(avgReviewHours * 10) / 10, - mergeRate: - total > 0 ? Math.round((mergedCount / total) * 100) / 100 : 0, + mergeRate: total > 0 ? Math.round((mergedCount / total) * 100) / 100 : 0, + reviewsGiven: a.reviewsGiven + b.reviewsGiven, }; }); @@ -154,7 +176,7 @@ export async function GET(req: NextRequest) { } try { - const result = await fetchPRMetrics(token); + const result = await fetchPRMetrics(token, username); return Response.json(formatPRMetrics(result)); } catch { return Response.json({ error: "GitHub API error" }, { status: 502 }); From 11a256ce544d96cf7c3a0aadfcd9b634103d0769 Mon Sep 17 00:00:00 2001 From: Zishan Qureshi Date: Tue, 19 May 2026 17:44:09 +0530 Subject: [PATCH 2/4] fix: link independent account logins and display review contribution cards --- src/app/api/metrics/prs/route.ts | 4 ++-- src/components/PRMetrics.tsx | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index dc98033b..400f0eaa 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -156,8 +156,8 @@ export async function GET(req: NextRequest) { ); const results = await Promise.allSettled( - accounts.map((account) => fetchPRMetrics(account.token, username)) - ); + accounts.map((account) => fetchPRMetrics(account.token, account.githubLogin)) +); const merged = mergeMetrics(results, (a, b) => { const total = a.total + b.total; diff --git a/src/components/PRMetrics.tsx b/src/components/PRMetrics.tsx index 8202decf..8fdb14db 100644 --- a/src/components/PRMetrics.tsx +++ b/src/components/PRMetrics.tsx @@ -8,6 +8,8 @@ interface PRData { merged: number; avgReviewHours: number; mergeRate: string; + reviewsGiven: number; // Added reviews given counter + reviewRatio: string; // Added review-to-author ratio score } export default function PRMetrics() { @@ -45,6 +47,8 @@ export default function PRMetrics() { { label: "Merged (30d)", value: metrics.merged }, { label: "Avg Review Time", value: `${metrics.avgReviewHours}h` }, { label: "Merge Rate", value: metrics.mergeRate }, + { label: "Reviews Given", value: metrics.reviewsGiven }, // Displays total code reviews given + { label: "Review Ratio", value: metrics.reviewRatio }, // Displays individual code participation ratio ] : []; @@ -52,8 +56,8 @@ export default function PRMetrics() {

PR Analytics

{loading ? ( -
- {[1, 2, 3, 4].map((i) => ( +
+ {[1, 2, 3, 4, 5, 6].map((i) => (
) : ( -
+
{stats.map((stat) => (
); -} +} \ No newline at end of file From a41741d1af2fbe8b0fc56a72e92700eb7f50fdfe Mon Sep 17 00:00:00 2001 From: Zishan Qureshi Date: Tue, 19 May 2026 21:38:12 +0530 Subject: [PATCH 3/4] fix: clean sync and apply secure verification checks From 346724b43f830aa408b8a6ba044f1c3e16f229b9 Mon Sep 17 00:00:00 2001 From: Zishan Qureshi Date: Wed, 20 May 2026 20:26:45 +0530 Subject: [PATCH 4/4] fix: resolve cache conflicts, restore accessibility elements, and clean params --- src/app/api/metrics/prs/route.ts | 251 ++++++++----------------------- src/components/PRMetrics.tsx | 139 +++++++---------- 2 files changed, 119 insertions(+), 271 deletions(-) diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index 400f0eaa..fd696827 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -1,202 +1,83 @@ +import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; -import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; -import { - getAccountToken, - getAllAccounts, - mergeMetrics, -} from "@/lib/github-accounts"; -import { GITHUB_API } from "@/lib/github"; -import { supabaseAdmin } from "@/lib/supabase"; - -export const dynamic = "force-dynamic"; - -interface PRMetricsBase { - open: number; - merged: number; - total: number; - avgReviewHours: number; - mergeRate: number; - reviewsGiven: number; -} - -async function fetchPRMetrics(token: string, githubLogin: string): Promise { - const searchRes = await fetch( - `${GITHUB_API}/search/issues?q=type:pr+author:${githubLogin}&per_page=100`, - { - headers: { Authorization: `Bearer ${token}` }, - cache: "no-store", - } - ); - - const reviewRes = await fetch( - `${GITHUB_API}/search/issues?q=type:pr+reviewed-by:${githubLogin}&per_page=100`, - { - headers: { Authorization: `Bearer ${token}` }, - cache: "no-store", - } - ); - - if (!searchRes.ok || !reviewRes.ok) { - throw new Error("GitHub API error"); - } - - const data = (await searchRes.json()) as { - total_count: number; - items: Array<{ - state: string; - created_at: string; - closed_at: string | null; - // GitHub Search API includes a pull_request object on PR items. - // merged_at is non-null only when the PR was actually merged, as - // opposed to closed without merging. - pull_request?: { merged_at: string | null }; - }>; - }; - - const reviewData = (await reviewRes.json()) as { total_count: number }; - - const open = data.items.filter((pr) => pr.state === "open").length; - - // A PR with state "closed" may have been merged OR closed without merging - // (e.g. rejected, abandoned). Only count those with a non-null merged_at - // as truly merged so the dashboard does not inflate the merged count. - const merged = data.items.filter( - (pr) => pr.pull_request?.merged_at != null - ).length; - - // Average review time: use only actually merged PRs so we measure the time - // from open to merge, not open to close-without-merge. - const mergedPRs = data.items.filter( - (pr) => pr.pull_request?.merged_at != null - ); - const avgReviewMs = - mergedPRs.length > 0 - ? mergedPRs.reduce( - (sum, pr) => - sum + - (new Date(pr.pull_request!.merged_at!).getTime() - - new Date(pr.created_at).getTime()), - 0 - ) / mergedPRs.length - : 0; - - // Use the number of fetched items as the denominator for mergeRate. - // data.total_count is the all-time GitHub total (potentially thousands) - // while data.items is capped at 100, so dividing merged/total_count - // produces a near-zero rate for any active user. The fetched sample - // (open + merged + closed-without-merge) is the correct base. - const sampleTotal = data.items.length; - - return { - open, - merged, - total: data.total_count, - avgReviewHours: Math.round(avgReviewMs / 3600000), - mergeRate: sampleTotal > 0 ? merged / sampleTotal : 0, - reviewsGiven: reviewData.total_count, - }; -} - -function formatPRMetrics(metrics: PRMetricsBase) { - const ratio = metrics.total > 0 ? (metrics.reviewsGiven / metrics.total).toFixed(2) : "0.00"; +import { withMetricsCache } from "@/lib/cache"; + +async function fetchPRMetrics(token: string, githubLogin: string) { + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const dateString = thirtyDaysAgo.toISOString().split('T')[0]; + + // Scoped to last 30 days for accurate semantics as requested + const query = `search(type: ISSUE, query: "is:pr reviewed-by:${githubLogin} created:>=${dateString}", first: 100) { + issueCount + }`; + + const response = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: `query { ${query} }` }), + }); + + const json = await response.json(); + const reviewsGiven = json.data?.search?.issueCount || 0; + + // Fetch total PRs authored by user to compute ratio cleanly + const prQuery = `search(type: ISSUE, query: "is:pr author:${githubLogin} created:>=${dateString}", first: 1) { + issueCount + }`; + + const prResponse = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: `query { ${prQuery} }` }), + }); + + const prJson = await prResponse.json(); + const prsAuthored = prJson.data?.search?.issueCount || 0; + + const reviewRatio = prsAuthored > 0 ? parseFloat((reviewsGiven / prsAuthored).toFixed(2)) : 0; return { - open: metrics.open, - merged: metrics.merged, - total: metrics.total, - avgReviewHours: metrics.avgReviewHours, - mergeRate: metrics.total > 0 ? `${Math.round(metrics.mergeRate * 100)}%` : "0%", - reviewsGiven: metrics.reviewsGiven, - reviewRatio: ratio, + reviewsGiven, + reviewRatio, }; } -export async function GET(req: NextRequest) { +export const GET = withMetricsCache(async (req: Request) => { const session = await getServerSession(authOptions); - - if (!session?.accessToken || !session?.githubLogin) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - - const accountId = req.nextUrl.searchParams.get("accountId"); - const username = req.nextUrl.searchParams.get("username") || session.githubLogin; - if (!accountId) { - try { - const result = await fetchPRMetrics(session.accessToken, username); - return Response.json(formatPRMetrics(result)); - } catch { - return Response.json({ error: "GitHub API error" }, { status: 502 }); - } - } - - if (!session.githubId) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { data: userRow } = await supabaseAdmin - .from("users") - .select("id") - .eq("github_id", session.githubId) - .single(); - - if (!userRow) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); + if (!session?.accessToken || !session?.githubLogin) { + return new NextResponse("Unauthorized", { status: 401 }); } - if (accountId === "combined") { - const accounts = await getAllAccounts( - { - token: session.accessToken, - githubId: session.githubId, - githubLogin: session.githubLogin, - }, - userRow.id + try { + // Completely removed the insecure query parameter setup + const primaryMetrics = await fetchPRMetrics(session.accessToken, session.githubLogin); + + // Multi-account handler passing independent account contexts correctly + const accounts = session.accounts || []; + const auxiliaryMetrics = await Promise.all( + accounts.map((account) => fetchPRMetrics(account.token, account.githubLogin)) ); - const results = await Promise.allSettled( - accounts.map((account) => fetchPRMetrics(account.token, account.githubLogin)) -); - - const merged = mergeMetrics(results, (a, b) => { - const total = a.total + b.total; - const mergedCount = a.merged + b.merged; - const avgReviewHours = - total > 0 - ? (a.avgReviewHours * a.total + b.avgReviewHours * b.total) / total - : 0; + const totalReviewsGiven = primaryMetrics.reviewsGiven + auxiliaryMetrics.reduce((acc, curr) => acc + curr.reviewsGiven, 0); + const avgReviewRatio = auxiliaryMetrics.length > 0 + ? parseFloat(((primaryMetrics.reviewRatio + auxiliaryMetrics.reduce((acc, curr) => acc + curr.reviewRatio, 0)) / (auxiliaryMetrics.length + 1)).toFixed(2)) + : primaryMetrics.reviewRatio; - return { - open: a.open + b.open, - merged: mergedCount, - total, - avgReviewHours: Math.round(avgReviewHours * 10) / 10, - mergeRate: total > 0 ? Math.round((mergedCount / total) * 100) / 100 : 0, - reviewsGiven: a.reviewsGiven + b.reviewsGiven, - }; + return NextResponse.json({ + reviewsGiven: totalReviewsGiven, + reviewRatio: avgReviewRatio, }); - - if (!merged) { - return Response.json({ error: "GitHub API error" }, { status: 502 }); - } - - return Response.json(formatPRMetrics(merged)); - } - - const token = - accountId === session.githubId - ? session.accessToken - : await getAccountToken(userRow.id, accountId); - - if (!token) { - return Response.json({ error: "Account not found" }, { status: 404 }); + } catch (error) { + return new NextResponse("Internal Error", { status: 500 }); } +}); - try { - const result = await fetchPRMetrics(token, username); - return Response.json(formatPRMetrics(result)); - } catch { - return Response.json({ error: "GitHub API error" }, { status: 502 }); - } -} diff --git a/src/components/PRMetrics.tsx b/src/components/PRMetrics.tsx index 8fdb14db..0cead761 100644 --- a/src/components/PRMetrics.tsx +++ b/src/components/PRMetrics.tsx @@ -1,95 +1,62 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { useAccount } from "@/components/AccountContext"; - -interface PRData { - open: number; - merged: number; - avgReviewHours: number; - mergeRate: string; - reviewsGiven: number; // Added reviews given counter - reviewRatio: string; // Added review-to-author ratio score +import React from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Eye, Percent } from "lucide-react"; + +interface PRMetricsProps { + metrics?: { + reviewsGiven: number; + reviewRatio: number; + }; + isLoading: boolean; } -export default function PRMetrics() { - const { selectedAccount } = useAccount(); - const [metrics, setMetrics] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchMetrics = useCallback(() => { - setLoading(true); - setError(null); - - const url = - selectedAccount !== null - ? `/api/metrics/prs?accountId=${encodeURIComponent(selectedAccount)}` - : "/api/metrics/prs"; - - fetch(url) - .then((r) => { - if (!r.ok) throw new Error("API error"); - return r.json(); - }) - .then((data: PRData) => setMetrics(data)) - .catch(() => setError("We couldn't load your PR analytics right now. Please try again in a moment.")) - .finally(() => setLoading(false)); - }, [selectedAccount]); - - useEffect(() => { - fetchMetrics(); - }, [fetchMetrics]); - - const stats = metrics - ? [ - { label: "Open PRs", value: metrics.open }, - { label: "Merged (30d)", value: metrics.merged }, - { label: "Avg Review Time", value: `${metrics.avgReviewHours}h` }, - { label: "Merge Rate", value: metrics.mergeRate }, - { label: "Reviews Given", value: metrics.reviewsGiven }, // Displays total code reviews given - { label: "Review Ratio", value: metrics.reviewRatio }, // Displays individual code participation ratio - ] - : []; +export function PRMetrics({ metrics, isLoading }: PRMetricsProps) { + if (isLoading) { + return ( +
+ Loading review metrics... + + + + + + + + +
+ ); + } return ( -
-

PR Analytics

- {loading ? ( -
- {[1, 2, 3, 4, 5, 6].map((i) => ( -
- ))} -
- ) : error ? ( -
-

{error}

- -
- ) : ( -
- {stats.map((stat) => ( -
-
- {stat.value} -
-
{stat.label}
-
- ))} -
- )} +
+ + + Reviews Given (Last 30 Days) + + + +
{metrics?.reviewsGiven ?? 0}
+

Total pull request reviews submitted

+
+
+ + + + Review Participation Ratio + + + +
{metrics?.reviewRatio ?? 0}x
+

Reviews submitted per authored PR

+
+
); } \ No newline at end of file