-
-
Notifications
You must be signed in to change notification settings - Fork 28.3k
feat: Add all-time contributions support with deduplicated repository count #4644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
@banu4prasad is attempting to deploy a commit to the github readme stats Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for displaying all-time contribution statistics across a user's entire GitHub history, with automatic deduplication to count unique repositories only once even when a user has multiple contribution types (commits, issues, PRs, reviews) to the same repository.
Key Changes:
- Added
all_time_contribsquery parameter to enable fetching contributions across all years instead of just the last year - Implemented parallel fetching of contribution data using
Promise.all()with a 9-second timeout and graceful fallback - Extended cache duration for all-time stats (6 hours default vs 4 hours for standard stats)
Reviewed changes
Copilot reviewed 6 out of 8 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
src/fetchers/all-time-contributions.js |
New module that fetches contribution years and yearly data in parallel, deduplicating repositories across all contribution types |
src/fetchers/stats.js |
Integrated all-time contributions feature with timeout protection and fallback logic; removed some logger statements |
api/index.js |
Added parameter handling for all_time_contribs and custom cache logic for longer TTL |
src/common/envs.js |
Added ALL_TIME_CONTRIBS environment variable to enable/disable the feature globally |
src/cards/stats.js |
Added conditional label rendering to show "all time" vs "last year" based on parameter |
src/cards/types.d.ts |
Added TypeScript type definition for the new all_time_contribs option |
src/translations.js |
Added statcard.contribs-alltime translation key in 48 languages |
.gitignore |
Added .DS_Store to ignore macOS system files |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ? process.env.GIST_WHITELIST.split(",") | ||
| : undefined; | ||
|
|
||
| const ALL_TIME_CONTRIBS=process.env.ALL_TIME_CONTRIBS == "true"; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use strict equality (===) instead of loose equality (==) for comparing strings. Loose equality can lead to unexpected type coercion. Change to process.env.ALL_TIME_CONTRIBS === "true".
| const ALL_TIME_CONTRIBS=process.env.ALL_TIME_CONTRIBS == "true"; | |
| const ALL_TIME_CONTRIBS=process.env.ALL_TIME_CONTRIBS === "true"; |
| const totalCommitsFetcher = async (username) => { | ||
| if (!githubUsernameRegex.test(username)) { | ||
| logger.log("Invalid username provided."); | ||
| throw new Error("Invalid username provided."); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed logger statements provide useful debugging information when errors occur. While the error is still thrown, removing the logging makes it harder to debug issues in production. Consider keeping these logger calls for operational visibility, especially since they were previously present for error tracking purposes.
| label: all_time_contribs | ||
| ? i18n.t("statcard.contribs-alltime") |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after all_time_contribs in the ternary expression. The code should be formatted as label: all_time_contribs ? with proper spacing for consistency with the project's code style.
| label: all_time_contribs | |
| ? i18n.t("statcard.contribs-alltime") | |
| label: all_time_contribs ? | |
| i18n.t("statcard.contribs-alltime") |
| const res = await retryer(fetcher, { login }); | ||
|
|
||
| if (res.data.errors) { | ||
| throw new Error("Failed to fetch contribution years"); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic error message "Failed to fetch contribution years" doesn't include the underlying error details. This makes debugging difficult when API calls fail. Consider including the original error message or logging it for better error diagnostics.
| throw new Error("Failed to fetch contribution years"); | |
| const errorDetails = Array.isArray(res.data.errors) | |
| ? res.data.errors | |
| .map((e) => e && e.message ? e.message : JSON.stringify(e)) | |
| .join("; ") | |
| : JSON.stringify(res.data.errors); | |
| logger.error( | |
| `Failed to fetch contribution years for login '${login}': ${errorDetails}`, | |
| ); | |
| throw new Error(`Failed to fetch contribution years: ${errorDetails}`); |
|
|
||
| const allTimePromise = fetchAllTimeContributions( | ||
| username, | ||
| process.env.PAT_1, | ||
| ); | ||
|
|
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding the use of process.env.PAT_1 makes this feature inflexible and potentially insecure. If PAT_1 is not set or expires, the feature silently falls back without any indication. Consider either: 1) using the token rotation mechanism that the rest of the codebase uses, or 2) accepting a token parameter from the caller to maintain consistency with other fetcher functions.
| const allTimePromise = fetchAllTimeContributions( | |
| username, | |
| process.env.PAT_1, | |
| ); | |
| const tokenEnvName = | |
| process.env.ALL_TIME_CONTRIB_TOKEN_NAME || "PAT_1"; | |
| const token = process.env[tokenEnvName]; | |
| const allTimePromise = fetchAllTimeContributions( | |
| username, | |
| token, | |
| ); |
| */ | ||
| const fetchYearContributions = async (login, year, token) => { | ||
| const from = `${year}-01-01T00:00:00Z`; | ||
| const to = `${year}-12-31T23:59:59Z`; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The end timestamp uses 23:59:59Z which could miss contributions made in the last second of the year (between 23:59:59 and 24:00:00). Consider using the next year's start time (e.g., ${year + 1}-01-01T00:00:00Z) as the end boundary to ensure all contributions for the year are captured inclusively.
| const to = `${year}-12-31T23:59:59Z`; | |
| const to = `${year + 1}-01-01T00:00:00Z`; |
| const cacheSeconds = parseBoolean(all_time_contribs) | ||
| ? clampValue( | ||
| parseInt(cache_seconds || SIX_HOURS, 10), | ||
| SIX_HOURS, | ||
| ONE_DAY, | ||
| ) | ||
| : clampValue( | ||
| parseInt(cache_seconds || FOUR_HOURS, 10), | ||
| FOUR_HOURS, | ||
| ONE_DAY, | ||
| ); |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache configuration for all-time contributions (min: 6 hours, max: 24 hours) is less restrictive than the standard STATS_CARD configuration (min: 12 hours, max: 48 hours). Since all-time data changes even more slowly than last-year data, the cache duration should be longer, not shorter. Consider using higher minimum values (e.g., 12-24 hours minimum) to reduce unnecessary API load.
| const cacheSeconds = parseBoolean(all_time_contribs) | ||
| ? clampValue( | ||
| parseInt(cache_seconds || SIX_HOURS, 10), | ||
| SIX_HOURS, | ||
| ONE_DAY, | ||
| ) | ||
| : clampValue( | ||
| parseInt(cache_seconds || FOUR_HOURS, 10), | ||
| FOUR_HOURS, | ||
| ONE_DAY, | ||
| ); | ||
|
|
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The custom cache logic using clampValue doesn't respect the CACHE_SECONDS environment variable, unlike the removed resolveCacheSeconds function which checks for this override. This breaks existing functionality where administrators can globally override cache durations via environment variables. Consider using resolveCacheSeconds with appropriate parameters or replicating its env var check logic.
| const cacheSeconds = parseBoolean(all_time_contribs) | |
| ? clampValue( | |
| parseInt(cache_seconds || SIX_HOURS, 10), | |
| SIX_HOURS, | |
| ONE_DAY, | |
| ) | |
| : clampValue( | |
| parseInt(cache_seconds || FOUR_HOURS, 10), | |
| FOUR_HOURS, | |
| ONE_DAY, | |
| ); | |
| const isAllTimeContribs = parseBoolean(all_time_contribs); | |
| const minCacheSeconds = isAllTimeContribs ? SIX_HOURS : FOUR_HOURS; | |
| // Baseline cache value from request parameter (or default), clamped | |
| const requestedCacheSeconds = parseInt( | |
| cache_seconds || (isAllTimeContribs ? SIX_HOURS : FOUR_HOURS), | |
| 10, | |
| ); | |
| let cacheSeconds = clampValue( | |
| requestedCacheSeconds, | |
| minCacheSeconds, | |
| ONE_DAY, | |
| ); | |
| // Global override via CACHE_SECONDS environment variable, if set | |
| const envCacheSecondsRaw = process.env.CACHE_SECONDS; | |
| if (envCacheSecondsRaw !== undefined) { | |
| const envCacheSeconds = parseInt(envCacheSecondsRaw, 10); | |
| if (Number.isFinite(envCacheSeconds) && envCacheSeconds > 0) { | |
| cacheSeconds = clampValue(envCacheSeconds, minCacheSeconds, ONE_DAY); | |
| } | |
| } |
| // @ts-check | ||
|
|
||
| import { retryer } from "../common/retryer.js"; | ||
| import { MissingParamError, CustomError } from "../common/error.js"; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import CustomError.
| import { MissingParamError, CustomError } from "../common/error.js"; | |
| import { MissingParamError } from "../common/error.js"; |
| import { retryer } from "../common/retryer.js"; | ||
| import { MissingParamError, CustomError } from "../common/error.js"; | ||
| import { request } from "../common/http.js"; | ||
| import { logger } from "../common/log.js"; |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import logger.
| import { logger } from "../common/log.js"; |
Summary
Adds support for displaying all-time contributions across all years on GitHub, with automatic deduplication of repositories to show accurate unique contribution counts.
Changes
all_time_contribsparameter to toggle between last year and all-time contributionsstatcard.contribs-alltimefor proper label displayALL_TIME_CONTRIBSenvironment variable to enable/disable feature globallyUsage
Standard (last year only)
?username=YOUR_USERNAME
All-time contributions (deduplicated)
?username=YOUR_USERNAME&all_time_contribs=true
Technical Details
Promise.all()for speedSetdata structurePAT_1environment variable with GitHub token in vercel's env variablesEnvironment Variables
ALL_TIME_CONTRIBS=true- Enable all-time contributions featurePAT_1=<github_token>- GitHub Personal Access Token withread:userscopefixes #2282