Skip to content

Conversation

@banu4prasad
Copy link

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

  • ✨ Added all_time_contribs parameter to toggle between last year and all-time contributions
  • 🔧 Implemented parallel fetching of contribution data across all years for performance
  • 📊 Deduplicated repository counting (counts each repo only once, even with multiple contribution types)
  • ⚡ Added timeout protection (9s) with fallback to standard last-year count
  • 🕒 Increased cache duration for all-time stats (6 hours vs 4 hours for standard)
  • 🌐 Added translation key statcard.contribs-alltime for proper label display
  • 🛠️ Added ALL_TIME_CONTRIBS environment variable to enable/disable feature globally

Usage

Standard (last year only)
?username=YOUR_USERNAME

All-time contributions (deduplicated)
?username=YOUR_USERNAME&all_time_contribs=true

Technical Details

  • Uses GitHub GraphQL API to fetch contribution years and yearly data
  • Fetches all years in parallel using Promise.all() for speed
  • Counts unique repositories using Set data structure
  • falls back to standard count on timeout or error
  • Requires PAT_1 environment variable with GitHub token in vercel's env variables

Environment Variables

  • ALL_TIME_CONTRIBS=true - Enable all-time contributions feature
  • PAT_1=<github_token> - GitHub Personal Access Token with read:user scope

fixes #2282

Screenshot 2025-11-08 at 4 46 26 PM Screenshot 2025-11-08 at 4 40 17 PM Screenshot 2025-11-08 at 4 40 05 PM

@vercel
Copy link

vercel bot commented Nov 8, 2025

@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.

@github-actions github-actions bot added stats-card Feature, Enhancement, Fixes related to stats the stats card. card-i18n Card text translations. labels Nov 8, 2025
Copilot AI review requested due to automatic review settings December 18, 2025 06:44
Copy link
Contributor

Copilot AI left a 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_contribs query 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";
Copy link

Copilot AI Dec 18, 2025

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".

Suggested change
const ALL_TIME_CONTRIBS=process.env.ALL_TIME_CONTRIBS == "true";
const ALL_TIME_CONTRIBS=process.env.ALL_TIME_CONTRIBS === "true";

Copilot uses AI. Check for mistakes.
const totalCommitsFetcher = async (username) => {
if (!githubUsernameRegex.test(username)) {
logger.log("Invalid username provided.");
throw new Error("Invalid username provided.");
Copy link

Copilot AI Dec 18, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +408 to +409
label: all_time_contribs
? i18n.t("statcard.contribs-alltime")
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
label: all_time_contribs
? i18n.t("statcard.contribs-alltime")
label: all_time_contribs ?
i18n.t("statcard.contribs-alltime")

Copilot uses AI. Check for mistakes.
const res = await retryer(fetcher, { login });

if (res.data.errors) {
throw new Error("Failed to fetch contribution years");
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
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}`);

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +324

const allTimePromise = fetchAllTimeContributions(
username,
process.env.PAT_1,
);

Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
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,
);

Copilot uses AI. Check for mistakes.
*/
const fetchYearContributions = async (login, year, token) => {
const from = `${year}-01-01T00:00:00Z`;
const to = `${year}-12-31T23:59:59Z`;
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
const to = `${year}-12-31T23:59:59Z`;
const to = `${year + 1}-01-01T00:00:00Z`;

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +118
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,
);
Copy link

Copilot AI Dec 18, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +119
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,
);

Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
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);
}
}

Copilot uses AI. Check for mistakes.
// @ts-check

import { retryer } from "../common/retryer.js";
import { MissingParamError, CustomError } from "../common/error.js";
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Unused import CustomError.

Suggested change
import { MissingParamError, CustomError } from "../common/error.js";
import { MissingParamError } from "../common/error.js";

Copilot uses AI. Check for mistakes.
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";
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Unused import logger.

Suggested change
import { logger } from "../common/log.js";

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

card-i18n Card text translations. stats-card Feature, Enhancement, Fixes related to stats the stats card.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show all time contributed to

1 participant