fix: cache overview stats in gist for fast combined card#13
Conversation
The combined endpoint was calling fetchOverview which paginates through all repos — too slow for Vercel's 10s timeout. Now: - GitHub Action computes and caches overview stats (stars, forks, contributions, repos count) in the gist alongside lines/views - Combined endpoint reads overview from the gist (instant) instead of calling fetchOverview - Separate /api/overview endpoint still uses live GraphQL fetcher Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #13 +/- ##
=======================================
Coverage 96.53% 96.53%
=======================================
Files 36 36
Lines 7363 7363
Branches 614 614
=======================================
Hits 7108 7108
Misses 246 246
Partials 9 9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses combined-card timeouts on Vercel by moving expensive “overview” aggregation work into a GitHub Action that caches results in a public gist, and updating the combined endpoint to read those cached values instead of paginating via live GraphQL.
Changes:
- Extend
scripts/update-stats.jsto compute and persist overview stats (name, stars, forks, all-time contributions, contributed-to count) into the gist JSON alongside existing lines-changed and views. - Update
/api/combinedto fetch overview stats from the cached gist (while keeping streak and top-languages fetched live).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
scripts/update-stats.js |
Adds GraphQL helpers + overview aggregation and writes the expanded stats payload to github-stats.json in the gist. |
api/combined.js |
Replaces live fetchOverview usage with a gist-backed fetchCachedOverview to make combined-card generation fast. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| while (hasOwnedNext || hasContribNext) { | ||
| const json = await graphql(query, { | ||
| login: USERNAME, | ||
| ownedAfter: hasOwnedNext ? ownedAfter : null, | ||
| contribAfter: hasContribNext ? contribAfter : null, | ||
| }); |
There was a problem hiding this comment.
fetchAllRepos can get stuck in an endless pagination loop. When hasOwnedNext (or hasContribNext) becomes false, the code starts sending ownedAfter: null again, which restarts that connection at page 1; pageInfo.hasNextPage can then flip back to true and the loop never terminates for users with >100 repos on the other connection. Consider paginating owned and contributed repos in separate loops, or use GraphQL @include directives + boolean vars to omit a connection entirely once it’s complete (so you don’t re-read its pageInfo).
| const query = `query($login: String!) { user(login: $login) { ${yearFragments} } }`; | ||
| const json = await graphql(query, { login: USERNAME }); |
There was a problem hiding this comment.
If contributionYears is empty, yearFragments becomes an empty string and this builds a GraphQL query where user { } has an empty selection set, which is invalid GraphQL and will fail the action. Add an early return (e.g., 0) when years.length === 0 (or guard the query construction accordingly).
| const res = await axios({ | ||
| method: "get", | ||
| url: `https://gist.githubusercontent.com/${username}/${gistId}/raw/github-stats.json`, | ||
| }); | ||
| const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data; |
There was a problem hiding this comment.
fetchCachedOverview no longer validates username (previously fetchOverview enforced github-username-regex). With the current code, malformed usernames can produce a bad gist URL / confusing errors. Consider validating username here (or reusing an existing username validation helper) and throwing the same “Invalid username provided.” error for consistency across endpoints.
| /** | ||
| * Fetch cached overview stats from the public gist. | ||
| * | ||
| * @param {string} username GitHub username (gist owner). | ||
| * @returns {Promise<object>} Cached overview stats. | ||
| */ | ||
| const fetchCachedOverview = async (username) => { | ||
| const gistId = process.env.GIST_ID; |
There was a problem hiding this comment.
This gist-fetching/parsing logic is duplicated with fetchGistStats in src/fetchers/overview.js (same URL pattern + JSON parsing). To reduce drift and keep behavior consistent (timeouts, error handling, defaults), consider extracting a shared helper (e.g., src/fetchers/gist-stats.js) and using it from both overview and combined.
| const [overview, streak, langs] = await Promise.all([ | ||
| fetchOverview(username), | ||
| fetchCachedOverview(username), | ||
| fetchStreak(username), | ||
| fetchTopLanguages(username, parseArray(exclude_repo)), | ||
| ]); |
There was a problem hiding this comment.
There are Jest tests for other API routes, but none covering /api/combined, and this PR changes its data source (now depends on the gist fetch/parse). Adding a unit test that mocks the gist HTTP GET and verifies the rendered SVG uses the cached overview fields would help prevent regressions.
Summary
The combined card was timing out because
fetchOverviewpaginates through all repos (too slow for Vercel's 10s timeout). Now:fetchOverview/api/overviewstill uses the live GraphQL fetcher (unchanged)The gist JSON now contains:
name,totalStars,totalForks,totalCommits,contributedTo,linesChanged,repoViews,updatedAtAfter merging, run the GitHub Action manually to populate the new gist fields.
Test plan
🤖 Generated with Claude Code