@@ -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
3639export 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+
309321async 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