Skip to content

Refactor: Modularize scraper pipeline into GitHubService and ScoringEngine#246

Open
kunal-10-cloud wants to merge 7 commits into
CircuitVerse:mainfrom
kunal-10-cloud:refactor/scraper-pipeline
Open

Refactor: Modularize scraper pipeline into GitHubService and ScoringEngine#246
kunal-10-cloud wants to merge 7 commits into
CircuitVerse:mainfrom
kunal-10-cloud:refactor/scraper-pipeline

Conversation

@kunal-10-cloud

@kunal-10-cloud kunal-10-cloud commented Jan 26, 2026

Copy link
Copy Markdown

Description

This PR refactors the monolithic generateLeaderboard.ts script into modular GitHubService and ScoringEngine components to improve maintainability. It also introduces centralized type definitions and comprehensive unit tests (using Vitest) to ensure stability, while preserving all original logic and variable names.

Related Issue

Fixes #245

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation

Checklist

  • Code follows project style
  • Tested locally
  • No unnecessary files added
  • PR title is clear and descriptive

Summary by CodeRabbit

  • New Features

    • None
  • Refactor

    • Centralized types into a shared types module.
    • Introduced a GitHub API service with pagination and rate‑limit awareness.
    • Added a scoring engine to centralize activity tracking and point calculations.
    • Streamlined recent-activity grouping and enriched activity metadata.
  • Bug Fixes

    • Minor spacing and null-check normalizations.
  • Tests

    • Added tests covering GitHub API interactions and scoring behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@netlify

netlify Bot commented Jan 26, 2026

Copy link
Copy Markdown

Deploy Preview for cv-community-dashboard ready!

Name Link
🔨 Latest commit 41f10d9
🔍 Latest deploy log https://app.netlify.com/projects/cv-community-dashboard/deploys/69791b254507d80008755069
😎 Deploy Preview https://deploy-preview-246--cv-community-dashboard.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jan 26, 2026

Copy link
Copy Markdown

Walkthrough

This PR modularizes the leaderboard pipeline: it extracts types to scripts/types.ts, moves GitHub API interaction into scripts/services/github.service.ts, and centralizes scoring logic into scripts/services/scoring.service.ts. scripts/generateLeaderboard.ts is refactored to orchestrate via those services and no longer declares in-file types. lib/db.ts was refactored to group recent activities by type and the public ActivityGroup type now includes an optional activity_description field. components/home-dashboard.tsx import path for RepoStats was updated. Unit tests for GitHubService and ScoringEngine were added.

Possibly related PRs

  • CircuitVerse/community-dashboard PR 115: Overlaps in refactoring generateLeaderboard.ts and introducing/using GitHub and scoring abstractions (ScoringEngine, GitHubService, POINTS).
  • CircuitVerse/community-dashboard PR 105: Restructures generateLeaderboard.ts and the exported activity/types (RawActivity, Contributor, RepoStats) that were centralized into scripts/types.ts here.
  • CircuitVerse/community-dashboard PR 200: Adjusts usage and imports of RepoStats in dashboard components, directly related to moving RepoStats into scripts/types.ts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately and concisely summarizes the main refactor into GitHubService and ScoringEngine.
Description check ✅ Passed Description follows the repository template and includes Description, Related Issue (Fixes #245), Type of change, and a completed Checklist.
Linked Issues check ✅ Passed PR implements the linked-issue objectives: extracts GitHubService, adds ScoringEngine, centralizes types, and adds unit tests as requested in #245.
Out of Scope Changes check ✅ Passed Changes are focused on modularizing the scraper, centralizing types, and adding tests; no unrelated features or large unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/generateLeaderboard.ts (1)

204-222: Implement bounded pagination to avoid fetching full closed-PR history.

The fetchPRsMerge() function uses github.fetchAll() to fetch all closed PRs for each repo, causing unnecessary API calls and potential rate-limit/timeout issues on large repositories. The codebase already uses bounded pagination in fetchPRsOpened() (lines 191–202); apply the same pattern here: paginate manually and exit once updated_at falls before previous_start.

🛠️ Proposed fix (bounded pagination)
 async function fetchPRsMerge(repo: string, current_start: Date, previous_start: Date, now: Date) {
   console.log("      🔎 Fetching PRs merged...");
-  const prs = (await github.fetchAll(
-    `${GITHUB_API}/repos/${ORG}/${repo}/pulls?state=closed&sort=updated&direction=desc`
-  )) as any[];
-
   let current = 0;
   let previous = 0;
+  let page = 1;
+
+  while (true) {
+    const res = await github.get(
+      `${GITHUB_API}/repos/${ORG}/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`
+    );
+    if (!res.ok) break;
+    const prs = (await res.json()) as any[];
+    if (!prs.length) break;
 
-  for (const pr of prs) {
-    if (!pr.merged_at) continue;
-    if (isBotUser(pr.user)) continue;
-    const mergedAt = new Date(pr.merged_at);
-    if (mergedAt >= current_start && mergedAt <= now) current++;
-    if (mergedAt >= previous_start && mergedAt < current_start) previous++;
-  }
+    for (const pr of prs) {
+      if (!pr.merged_at) continue;
+      if (isBotUser(pr.user)) continue;
+      const mergedAt = new Date(pr.merged_at);
+      if (mergedAt >= current_start && mergedAt <= now) current++;
+      if (mergedAt >= previous_start && mergedAt < current_start) previous++;
+    }
+
+    const lastUpdated = prs[prs.length - 1]?.updated_at;
+    if (lastUpdated && new Date(lastUpdated) < previous_start) break;
+    if (prs.length < 100) break;
+    page++;
+  }
 
   return { current, previous };
 }
🤖 Fix all issues with AI agents
In `@lib/db.ts`:
- Around line 117-145: Remove the dev-inline comments and the redundant type
cast: delete the checkmark comments (e.g., "// ✅ REAL title", "// ✅ REAL GitHub
link", "// ✅ Extract repo name") inside the activities object pushed to groups,
and simplify the contributor_role assignment by removing "?? null as string |
null" and using user.role directly; keep all other fields and calls (e.g.,
extractRepoFromUrl, slug generation, groups, activities) unchanged.

In `@scripts/services/scoring.service.ts`:
- Around line 89-98: The deduplication key in deduplicateAndRecalculate uses
`${act.type}:${act.occured_at}:${act.link ?? act.title}` which collapses
different activities when both act.link and act.title are null; update the key
construction in the user.raw_activities filter to use a more robust fallback
(e.g., include an explicit unique field if available such as act.id or act.repo,
or append a stable hash/JSON.stringify of the activity when link/title are
missing) or skip deduplication for activities lacking identifying info; ensure
changes reference the deduplicateAndRecalculate function and the
user.raw_activities filter so the key becomes something like
`${act.type}:${act.occured_at}:${act.link ?? act.title ?? act.id ??
JSON.stringify(act)}` or an equivalent safe fallback.
🧹 Nitpick comments (1)
scripts/services/github.service.ts (1)

89-103: fetchOrgRepos silently continues on error, which may return partial results.

When a fetch fails mid-pagination (e.g., page 3 of 5), the method logs a warning but returns whatever repos were collected so far. This could lead to incomplete data without clear indication to the caller.

Consider either:

  1. Throwing an error on failure (consistent with fetchAll)
  2. Returning a result object that indicates partial success

This is acceptable for now if the caller handles potential partial data, but worth noting for reliability.

Comment thread lib/db.ts
Comment thread scripts/services/scoring.service.ts
@kunal-10-cloud
kunal-10-cloud force-pushed the refactor/scraper-pipeline branch from 482d5eb to bb012d6 Compare January 27, 2026 09:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/services/github.service.test.ts`:
- Around line 4-16: The test currently assigns global.fetch = vi.fn() at module
scope which isn't restored by vi.restoreAllMocks(); change this to create a spy
in the test lifecycle: remove the module-scope assignment and instead call
vi.spyOn(globalThis, 'fetch') inside beforeEach (and set its
mockImplementation/resolution there as needed), keep vi.clearAllMocks() in
beforeEach and vi.restoreAllMocks() in afterEach so the original global.fetch is
restored between tests; update any references to the mock to use the spy
returned by vi.spyOn.
🧹 Nitpick comments (2)
scripts/services/github.service.ts (1)

224-257: Guard against non‑positive stepDays to avoid an infinite loop.
If stepDays is 0 or negative, cursor never advances and the loop won’t terminate.

♻️ Proposed defensive guard
 async searchByDateChunks(
     baseQuery: string,
     start: Date,
     end: Date,
     stepDays = 30,
     dateField = "created"
 ): Promise<GitHubSearchItem[]> {
+    if (stepDays <= 0) {
+        throw new Error("stepDays must be > 0");
+    }
     const all: GitHubSearchItem[] = [];
     let cursor = new Date(start);
scripts/generateLeaderboard.ts (1)

110-115: Use isBotUser to avoid duplicated bot filtering logic.

This keeps bot detection consistent across the file and reduces drift if the rules change.

♻️ Suggested refactor
-          if (!review.user?.login) continue;
-          if (review.user.login.endsWith("[bot]")) continue;
-          if (review.user.type && review.user.type !== "User") continue;
+          if (isBotUser(review.user)) continue;

Comment thread tests/services/github.service.test.ts Outdated

@Atharva7126 Atharva7126 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @kunal-10-cloud you need to revert all the json files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
public/analytics/analytics.json (1)

743-750: Fix repository field for mobile-app issues.

Line 743-750, 977-984, and 1013-1021 list mobile-app issue URLs but set "repository": "CircuitVerseDocs". This misclassifies analytics and per-repo filters. Update these to "mobile-app" and fix the mapping in the generator.

🛠 Proposed fix
-        "url": "https://github.com/CircuitVerse/mobile-app/issues/484",
-        "repository": "CircuitVerseDocs",
+        "url": "https://github.com/CircuitVerse/mobile-app/issues/484",
+        "repository": "mobile-app",
-        "url": "https://github.com/CircuitVerse/mobile-app/issues/482",
-        "repository": "CircuitVerseDocs",
+        "url": "https://github.com/CircuitVerse/mobile-app/issues/482",
+        "repository": "mobile-app",
-        "url": "https://github.com/CircuitVerse/mobile-app/issues/481",
-        "repository": "CircuitVerseDocs",
+        "url": "https://github.com/CircuitVerse/mobile-app/issues/481",
+        "repository": "mobile-app",

Also applies to: 977-984, 1013-1021

🤖 Fix all issues with AI agents
In `@public/leaderboard/recent-activities.json`:
- Around line 4-6: The array of recent-activities date groups in
recent-activities.json is not in strict chronological order (e.g., "2026-01-26"
appears before a later "2026-01-27" group); before writing this file ensure the
top-level array of date-group tuples is sorted in descending (or the
UI-expected) chronological order by the date string key so all date buckets
(e.g., "2026-01-26", "2026-01-27") are contiguous and correctly ordered.

Comment thread public/leaderboard/recent-activities.json
@kunal-10-cloud
kunal-10-cloud force-pushed the refactor/scraper-pipeline branch from dd824c1 to 589930a Compare January 27, 2026 18:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lib/db.ts (2)

50-96: Remove commented-out code before merging.

This large block of commented-out code (the old implementation) should be removed. It creates noise and the git history preserves the original if needed.


98-100: Add derivePeriod call for the 2-month period or remove "2month" from the union type.

The function accepts "2month" as valid input, and public/leaderboard/2month.json exists. However, the file is not regenerated by generateLeaderboard.ts since derivePeriod() is only called for week and month. This means the JSON file will contain stale data over time as activities accumulate.

Either add derivePeriod(yearData, 60, "2month"); to keep the 2-month period current, or remove "2month" from the valid union type if it's not intended to be actively maintained.

scripts/generateLeaderboard.ts (1)

604-608: Add await before generateRepoOverview() and consider generating 2month.json.

Two observations:

  1. generateRepoOverview() is an async function but is called without await. While the outer catch will handle rejection, any errors from generateRepoOverview won't interrupt the flow or provide proper sequencing.

  2. lib/db.ts accepts "2month" as a valid period parameter, but no 2month.json is generated here. Add a derivePeriod call for 60 days if this period is needed.

🔧 Suggested fix
   derivePeriod(yearData, 7, "week");
   derivePeriod(yearData, 30, "month");
+  derivePeriod(yearData, 60, "2month");
   generateRecentActivities(yearData);
-  await generateRepoOverview()
+  await generateRepoOverview();
 }
🧹 Nitpick comments (4)
scripts/types.ts (2)

67-87: Minor style inconsistency: missing semicolons in nested object types.

The nested object types within RepoStats are missing semicolons after their closing braces, while other interfaces in this file use semicolons consistently.

🔧 Suggested fix
 export interface RepoStats {
     name: string;
     description: string | null;
     language: string | null;
     avatar_url: string;
     html_url: string;
     stars: number;
     forks: number;
     current: {
         pr_opened: number;
         pr_merged: number;
         issue_created: number;
         currentTotalContribution: number;
-    }
+    };
     previous: {
         pr_merged: number;
-    }
+    };
     growth: {
         pr_merged: number;
-    };
-};
+    };
+}

89-105: Consider reusing the Contributor type instead of inline definition.

The entries array in ExistingYearData has an inline type definition that nearly duplicates the Contributor type. Consider referencing Contributor directly to maintain DRY principles and ensure consistency.

♻️ Suggested refactor
 export interface ExistingYearData {
     period: string;
     updatedAt: number;
     lastFetchedAt?: number;
     startDate?: string;
     endDate?: string;
-    entries: Array<{
-        username: string;
-        name: string | null;
-        avatar_url: string | null;
-        role: string;
-        total_points: number;
-        activity_breakdown: Record<string, { count: number; points: number }>;
-        daily_activity: Array<{ date: string; count: number; points: number }>;
-        raw_activities: RawActivity[];
-    }>;
+    entries: Contributor[];
 }
scripts/generateLeaderboard.ts (2)

163-172: Consider adding proper TypeScript types for API responses.

Multiple as any[] type assertions reduce type safety. Consider defining interfaces for GitHub API responses (issues, PRs, etc.) to catch potential issues at compile time.

💡 Example type definition
interface GitHubIssue {
  id: number;
  pull_request?: object;
  created_at: string;
  user: { login: string; type?: string } | null;
  // ... other fields
}

// Then use:
const issues = await github.fetchAll<GitHubIssue>(...);

534-541: Non-null assertion on pr.closed_at is safe but could use a guard.

The search query filters for merged PRs which should always have closed_at, but a defensive check would prevent potential runtime errors if the API behavior changes.

🛡️ Defensive check
   for (const pr of await github.searchByDateChunks(
     `org:${ORG}+is:pr+is:merged`,
     since,
     now,
     30,
     "merged"
   )) {
     if (isBotUser(pr.user)) continue;
+    if (!pr.closed_at) continue;
     ScoringEngine.addActivity(
       ScoringEngine.ensureUser(users, pr.user),
       "PR merged",
-      pr.closed_at!,
+      pr.closed_at,
       POINTS["PR merged"],
       { title: pr.title, link: pr.html_url }
     );
   }

- Remove commented-out code in lib/db.ts
- Fix style inconsistencies in scripts/types.ts (missing semicolons)
- Reuse Contributor type in scripts/types.ts for DRY
- Add await to generateRepoOverview() call
- Add derivation for 2-month period in generateLeaderboard.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/db.ts`:
- Around line 65-90: getRecentActivitiesGroupedByType currently only iterates
user.activities so records coming from year.json (which use raw_activities on
Contributor objects) are ignored when valid="year"; update the function (or the
loop processing entries) to check for and prefer user.activities but fall back
to user.raw_activities when activities is undefined, and treat both arrays with
the same shape when building groups (use the same act variable handling and
extractRepoFromUrl). Alternatively, if you intend to drop year support, remove
"year" from the valid union where getRecentActivitiesGroupedByType is
called/validated so callers cannot pass valid="year". Ensure you reference
getRecentActivitiesGroupedByType and the loop that builds groups (places using
user.activities and act) when making the change.
🧹 Nitpick comments (1)
scripts/generateLeaderboard.ts (1)

102-106: Isolate PR review fetch failures per PR.

A single review request failure will currently throw and abort the batch. Consider catching errors per PR and continuing with an empty review list so one bad response doesn’t stop the entire repo scan.

Suggested fix
-      const reviewResults = await Promise.all(
-        batch.map(pr => github.fetchPRReviews(ORG, repoName, pr.number).then(reviews => ({ pr, reviews: reviews as GitHubReview[] })))
-      );
+      const reviewResults = await Promise.all(
+        batch.map(async pr => {
+          try {
+            const reviews = await github.fetchPRReviews(ORG, repoName, pr.number);
+            return { pr, reviews: reviews as GitHubReview[] };
+          } catch (err) {
+            console.warn(`      ⚠️ Failed to fetch reviews for PR #${pr.number}`, err);
+            return { pr, reviews: [] as GitHubReview[] };
+          }
+        })
+      );

Comment thread lib/db.ts
…dByType

- Resolves CodeRabbit review regarding year.json records being ignored
- Ensures backward compatibility with older data files using 'activities'
- Fixed unused UserEntry import warning in lib/db.ts

@naman79820 naman79820 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @kunal-10-cloud thanks for the effort on this However the current code is stable, working well in production and we're not actively developing this area anymore. Since everything is functioning fine and there are no planned changes to this part of the codebase so i think there's no need for it. Once again thanksss for the efforts really appreciated :))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Modularize & Test the Scraper Pipeline

3 participants