Skip to content

Commit 6d509cc

Browse files
authored
Feat: Add issue triage labels into the leaderboard (#182)
1 parent 13639da commit 6d509cc

2 files changed

Lines changed: 203 additions & 4 deletions

File tree

components/Leaderboard/LeaderboardCard.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Card, CardContent } from "@/components/ui/card";
44
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
55
import { Badge } from "@/components/ui/badge";
6-
import { Trophy, GitMerge, GitPullRequest, AlertCircle, Eye } from "lucide-react";
6+
import { Trophy, GitMerge, GitPullRequest, AlertCircle, Eye, Tag, UserPlus, CheckCircle } from "lucide-react";
77
import { cn } from "@/lib/utils";
88
import ActivityTrendChart from "./ActivityTrendChart";
99
import "./LeaderboardCard.css";
@@ -37,6 +37,24 @@ const activityStyles: Record<string, {
3737
bgColor: "bg-green-500/10 dark:bg-green-500/15",
3838
textColor: "text-green-700 dark:text-green-400",
3939
borderColor: "border-l-green-500"
40+
},
41+
"Issue labeled": {
42+
icon: Tag,
43+
bgColor: "bg-yellow-500/10 dark:bg-yellow-500/15",
44+
textColor: "text-yellow-700 dark:text-yellow-400",
45+
borderColor: "border-l-yellow-500"
46+
},
47+
"Issue assigned": {
48+
icon: UserPlus,
49+
bgColor: "bg-indigo-500/10 dark:bg-indigo-500/15",
50+
textColor: "text-indigo-700 dark:text-indigo-400",
51+
borderColor: "border-l-indigo-500"
52+
},
53+
"Issue closed": {
54+
icon: CheckCircle,
55+
bgColor: "bg-emerald-500/10 dark:bg-emerald-500/15",
56+
textColor: "text-emerald-700 dark:text-emerald-400",
57+
borderColor: "border-l-emerald-500"
4058
}
4159
};
4260

@@ -92,8 +110,11 @@ export function LeaderboardCard({
92110
const activityPriority: Record<string, number> = {
93111
"PR merged": 1,
94112
"PR opened": 2,
95-
"Issue opened": 3,
96-
"Review submitted": 4,
113+
"Issue closed": 3,
114+
"Issue assigned": 4,
115+
"Issue opened": 5,
116+
"Issue labeled": 6,
117+
"Review submitted": 7,
97118
};
98119
const priorityA = activityPriority[a[0]] ?? 99;
99120
const priorityB = activityPriority[b[0]] ?? 99;

scripts/generateLeaderboard.ts

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ const POINTS = {
2727
"PR merged": 5,
2828
"Issue opened": 1,
2929
"Review submitted": 4,
30+
"Issue labeled": 2,
31+
"Issue assigned": 2,
32+
"Issue closed": 1,
3033
} as const;
3134

3235
/* -------------------------------------------------------
3336
TYPES (EXPORTED — IMPORTANT)
3437
------------------------------------------------------- */
3538

3639
export type RawActivity = {
37-
type: "PR opened" | "PR merged" | "Issue opened" | "Review submitted";
40+
type: "PR opened" | "PR merged" | "Issue opened" | "Review submitted" | "Issue labeled" | "Issue assigned" | "Issue closed";
3841
occured_at: string;
3942
title?: string | null;
4043
link?: string | null;
@@ -306,6 +309,15 @@ interface GitHubReview {
306309
submitted_at: string;
307310
}
308311

312+
interface GitHubIssueEvent {
313+
event: string;
314+
actor: { login: string; avatar_url?: string; type?: string };
315+
created_at: string;
316+
label?: { name: string };
317+
assignee?: { login: string };
318+
}
319+
320+
309321
async function fetchOrgRepos(): Promise<string[]> {
310322
const repos: string[] = [];
311323
let page = 1;
@@ -464,6 +476,169 @@ async function fetchAllReviews(
464476
}
465477
}
466478

479+
/* -------------------------------------------------------
480+
FETCH ISSUE TRIAGING ACTIVITIES
481+
------------------------------------------------------- */
482+
483+
async function fetchIssueTriagingActivities(
484+
users: Map<string, Contributor>,
485+
since: Date,
486+
now: Date
487+
) {
488+
console.log("🔍 Issue triaging activities");
489+
490+
// Use GitHub Search API for better historical coverage
491+
console.log(" 📌 Fetching issue events (labeled, assigned, closed)...");
492+
493+
// Search for issues that were updated in our timeframe to capture triaging activities
494+
const updatedIssues = await searchByDateChunks(
495+
`org:${ORG}+is:issue`,
496+
since,
497+
now,
498+
30,
499+
"updated"
500+
);
501+
502+
console.log(` 📊 Found ${updatedIssues.length} updated issues to scan for triaging activities`);
503+
504+
// Process issues in batches to avoid rate limiting
505+
const batchSize = 10;
506+
const issueBatches = chunk(updatedIssues, batchSize);
507+
508+
for (const [batchIndex, batch] of issueBatches.entries()) {
509+
console.log(` 🔄 Processing issue batch ${batchIndex + 1}/${issueBatches.length}...`);
510+
511+
// Process each issue for events
512+
await Promise.all(
513+
batch.map(issue => processIssueTriagingEvents(users, issue, since, now))
514+
);
515+
516+
// Small delay between batches
517+
await sleep(1000);
518+
}
519+
520+
console.log("✅ Issue triaging activities scan completed");
521+
}
522+
523+
async function processIssueTriagingEvents(
524+
users: Map<string, Contributor>,
525+
issue: GitHubSearchItem,
526+
since: Date,
527+
now: Date
528+
) {
529+
try {
530+
// Extract repo name from html_url
531+
const url = new URL(issue.html_url);
532+
const pathParts = url.pathname.split('/').filter(Boolean);
533+
// Expected: [org, repo, 'issues', number]
534+
if (pathParts.length < 4 || pathParts[2] !== 'issues') return;
535+
536+
const repoName = pathParts[1];
537+
const issueNumber = pathParts[3];
538+
539+
if (!repoName || !issueNumber || isNaN(Number(issueNumber))) return;
540+
541+
// Fetch issue events (labeled, assigned, closed)
542+
const eventsRes = await fetch(
543+
`${GITHUB_API}/repos/${ORG}/${repoName}/issues/${issueNumber}/events`,
544+
{
545+
headers: {
546+
Authorization: `Bearer ${TOKEN}`,
547+
Accept: "application/vnd.github+json",
548+
},
549+
}
550+
);
551+
552+
if (!eventsRes.ok) {
553+
console.error(` ⚠️ Failed to fetch events for ${repoName}#${issueNumber}: ${eventsRes.status}`);
554+
return;
555+
}
556+
557+
const events: GitHubIssueEvent[] = await eventsRes.json();
558+
await smartSleep(eventsRes, 500);
559+
560+
// Process events for triaging activities
561+
for (const event of events) {
562+
if (!event.actor?.login || isBotUser(event.actor)) continue;
563+
564+
const eventDate = new Date(event.created_at);
565+
if (eventDate < since || eventDate > now) continue;
566+
567+
const user = ensureUser(users, event.actor);
568+
569+
switch (event.event) {
570+
case "labeled":
571+
// Only count meaningful labels (not automated ones)
572+
if (event.label?.name && !isAutomatedLabel(event.label.name)) {
573+
addActivity(
574+
user,
575+
"Issue labeled",
576+
event.created_at,
577+
POINTS["Issue labeled"],
578+
{
579+
title: `Labeled issue #${issueNumber}: ${event.label.name}`,
580+
link: issue.html_url
581+
}
582+
);
583+
}
584+
break;
585+
586+
case "assigned":
587+
// Only count assignments where the actor is not assigning themselves
588+
if (event.assignee && event.actor.login !== event.assignee.login) {
589+
addActivity(
590+
user,
591+
"Issue assigned",
592+
event.created_at,
593+
POINTS["Issue assigned"],
594+
{
595+
title: `Assigned issue #${issueNumber} to ${event.assignee.login}`,
596+
link: issue.html_url
597+
}
598+
);
599+
}
600+
break;
601+
602+
case "closed":
603+
// Only count manual closures by maintainers
604+
if (event.actor.login !== issue.user.login) {
605+
addActivity(
606+
user,
607+
"Issue closed",
608+
event.created_at,
609+
POINTS["Issue closed"],
610+
{
611+
title: `Closed issue #${issueNumber}: ${sanitizeTitle(issue.title)}`,
612+
link: issue.html_url
613+
}
614+
);
615+
}
616+
break;
617+
}
618+
}
619+
} catch (error) {
620+
console.error(` ❌ Error processing issue events: ${error}`);
621+
}
622+
}
623+
624+
// Helper function to filter out automated labels
625+
function isAutomatedLabel(labelName: string): boolean {
626+
const automatedLabels = [
627+
'stale',
628+
'wontfix',
629+
'duplicate',
630+
'invalid',
631+
'dependencies',
632+
'security',
633+
'github_actions'
634+
];
635+
636+
return automatedLabels.some(auto =>
637+
labelName.toLowerCase().includes(auto.toLowerCase())
638+
);
639+
}
640+
641+
467642
/* -------------------------------------------------------
468643
INCREMENTAL UPDATE HELPERS
469644
------------------------------------------------------- */
@@ -651,6 +826,9 @@ async function generateYear() {
651826
// Fetch reviews
652827
await fetchAllReviews(users, since, now);
653828

829+
// Fetch issue triaging activities
830+
await fetchIssueTriagingActivities(users, since, now);
831+
654832
// Merge existing activities (incremental mode)
655833
if (isIncremental) {
656834
console.log("🔄 Merging with existing data...");

0 commit comments

Comments
 (0)