From 2629b1cd5f652c04d3aad65523bb4fbe8026b50e Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 18:12:39 +0530 Subject: [PATCH 01/13] gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b1d9a017c5b80..db5dcb53588b0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ vercel_token *.code-workspace .vercel + +.DS_Store \ No newline at end of file From 72561d741e623d0e263906c75d596ba308bd5e3a Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 18:13:11 +0530 Subject: [PATCH 02/13] [feat] all_time_contribs --- api/index.js | 5 + src/cards/stats.js | 5 +- src/cards/types.d.ts | 1 + src/common/envs.js | 4 +- src/fetchers/all-time-contributions.js | 223 +++++++++++++++++++++++++ src/fetchers/stats.js | 28 +++- src/translations.js | 48 ++++++ 7 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 src/fetchers/all-time-contributions.js diff --git a/api/index.js b/api/index.js index 6ea4ffe0c20e7..84577a654d03e 100644 --- a/api/index.js +++ b/api/index.js @@ -48,6 +48,8 @@ export default async (req, res) => { border_color, rank_icon, show, + all_time_contribs, + deduplicate_contribs, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -88,6 +90,8 @@ export default async (req, res) => { const stats = await fetchStats( username, parseBoolean(include_all_commits), + parseBoolean(all_time_contribs), + parseBoolean(deduplicate_contribs), parseArray(exclude_repo), showStats.includes("prs_merged") || showStats.includes("prs_merged_percentage"), @@ -113,6 +117,7 @@ export default async (req, res) => { card_width: parseInt(card_width, 10), hide_rank: parseBoolean(hide_rank), include_all_commits: parseBoolean(include_all_commits), + all_time_contribs: parseBoolean(all_time_contribs), commits_year: parseInt(commits_year, 10), line_height, title_color, diff --git a/src/cards/stats.js b/src/cards/stats.js index 6b428d48c34ae..a842f059caf7f 100644 --- a/src/cards/stats.js +++ b/src/cards/stats.js @@ -277,6 +277,7 @@ const renderStatsCard = (stats, options = {}) => { card_width, hide_rank = false, include_all_commits = false, + all_time_contribs = false, commits_year, line_height = 25, title_color, @@ -404,7 +405,9 @@ const renderStatsCard = (stats, options = {}) => { STATS.contribs = { icon: icons.contribs, - label: i18n.t("statcard.contribs"), + label: all_time_contribs + ? i18n.t("statcard.contribs-alltime") + : i18n.t("statcard.contribs"), value: contributedTo, id: "contribs", }; diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 7535df35bbe6f..48ef23fa6b3f4 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -20,6 +20,7 @@ export type StatCardOptions = CommonOptions & { card_width: number; hide_rank: boolean; include_all_commits: boolean; + all_time_contribs: boolean; commits_year: number; line_height: number | string; custom_title: string; diff --git a/src/common/envs.js b/src/common/envs.js index 5f1319662b94d..4857357707880 100644 --- a/src/common/envs.js +++ b/src/common/envs.js @@ -8,8 +8,10 @@ const gistWhitelist = process.env.GIST_WHITELIST ? process.env.GIST_WHITELIST.split(",") : undefined; +const ALL_TIME_CONTRIBS=process.env.ALL_TIME_CONTRIBS == "true"; + const excludeRepositories = process.env.EXCLUDE_REPO ? process.env.EXCLUDE_REPO.split(",") : []; -export { whitelist, gistWhitelist, excludeRepositories }; +export { whitelist, gistWhitelist, excludeRepositories, ALL_TIME_CONTRIBS }; diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js new file mode 100644 index 0000000000000..128c52cadbc9b --- /dev/null +++ b/src/fetchers/all-time-contributions.js @@ -0,0 +1,223 @@ +// @ts-check + +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"; + +/** + * GraphQL query to fetch contribution years for a user + */ +const CONTRIBUTION_YEARS_QUERY = ` + query contributionYears($login: String!) { + user(login: $login) { + contributionsCollection { + contributionYears + } + } + } +`; + +/** + * GraphQL query to fetch contributions for a specific year + */ +const YEAR_CONTRIBUTIONS_QUERY = ` + query yearContributions($login: String!, $from: DateTime!, $to: DateTime!) { + user(login: $login) { + contributionsCollection(from: $from, to: $to) { + totalRepositoriesWithContributedCommits + totalRepositoriesWithContributedIssues + totalRepositoriesWithContributedPullRequests + totalRepositoriesWithContributedPullRequestReviews + commitContributionsByRepository(maxRepositories: 100) { + repository { + nameWithOwner + } + } + issueContributionsByRepository(maxRepositories: 100) { + repository { + nameWithOwner + } + } + pullRequestContributionsByRepository(maxRepositories: 100) { + repository { + nameWithOwner + } + } + pullRequestReviewContributionsByRepository(maxRepositories: 100) { + repository { + nameWithOwner + } + } + } + } + } +`; + +/** + * Fetches all contribution years for a user + * @param {string} login - GitHub username + * @param {string} token - GitHub PAT + * @returns {Promise} Array of years + */ +const fetchContributionYears = async (login, token) => { + if (!login) { + throw new MissingParamError(["login"]); + } + + const res = await retryer(request, [ + { + query: CONTRIBUTION_YEARS_QUERY, + variables: { login }, + }, + { + Authorization: `token ${token}`, + }, + ]); + + if (res.data.errors) { + logger.error(res.data.errors); + throw new CustomError( + res.data.errors[0].message || "Could not fetch contribution years", + CustomError.USER_NOT_FOUND, + ); + } + + return res.data.data.user.contributionsCollection.contributionYears; +}; + +/** + * Fetches contributions for a specific year + * @param {string} login - GitHub username + * @param {number} year - Year to fetch + * @param {string} token - GitHub PAT + * @returns {Promise} Contribution data for the year + */ +const fetchYearContributions = async (login, year, token) => { + const from = `${year}-01-01T00:00:00Z`; + const to = `${year}-12-31T23:59:59Z`; + + const res = await retryer(request, [ + { + query: YEAR_CONTRIBUTIONS_QUERY, + variables: { login, from, to }, + }, + { + Authorization: `token ${token}`, + }, + ]); + + if (res.data.errors) { + logger.error(res.data.errors); + throw new CustomError( + res.data.errors[0].message || `Could not fetch contributions for ${year}`, + CustomError.GRAPHQL_ERROR, + ); + } + + return res.data.data.user.contributionsCollection; +}; + +/** + * Deduplicates repositories across all contribution types + * @param {Object} yearData - Contribution data for a year + * @returns {number} Count of unique repositories + */ +const deduplicateRepositories = (yearData) => { + const uniqueRepos = new Set(); + + const addRepos = (contributions) => { + contributions?.forEach((contrib) => { + if (contrib.repository?.nameWithOwner) { + uniqueRepos.add(contrib.repository.nameWithOwner); + } + }); + }; + + addRepos(yearData.commitContributionsByRepository); + addRepos(yearData.issueContributionsByRepository); + addRepos(yearData.pullRequestContributionsByRepository); + addRepos(yearData.pullRequestReviewContributionsByRepository); + + return uniqueRepos.size; +}; + +/** + * Fetches all-time contribution statistics + * @param {string} login - GitHub username + * @param {string} token - GitHub PAT + * @param {boolean} deduplicate - Whether to deduplicate repositories + * @returns {Promise} All-time contribution stats + */ +export const fetchAllTimeContributions = async (login, token, deduplicate = false) => { + if (!login) { + throw new MissingParamError(["login"]); + } + + logger.log(`Fetching all-time contributions for ${login}...`); + + // Fetch all contribution years + const years = await fetchContributionYears(login, token); + logger.log(`Found contribution years: ${years.join(", ")}`); + + if (deduplicate) { + // Deduplicated mode - count unique repositories + const allRepos = new Set(); + + for (const year of years) { + logger.log(`Fetching contributions for year ${year}...`); + const yearData = await fetchYearContributions(login, year, token); + + const addRepos = (contributions) => { + contributions?.forEach((contrib) => { + if (contrib.repository?.nameWithOwner) { + allRepos.add(contrib.repository.nameWithOwner); + } + }); + }; + + addRepos(yearData.commitContributionsByRepository); + addRepos(yearData.issueContributionsByRepository); + addRepos(yearData.pullRequestContributionsByRepository); + addRepos(yearData.pullRequestReviewContributionsByRepository); + } + + logger.log(`Total unique repositories: ${allRepos.size}`); + + return { + totalRepositoriesContributedTo: allRepos.size, + deduplicated: true, + yearsAnalyzed: years.length, + }; + } else { + // Summed mode - sum all totals + let totalCommits = 0; + let totalIssues = 0; + let totalPRs = 0; + let totalReviews = 0; + + for (const year of years) { + logger.log(`Fetching contributions for year ${year}...`); + const yearData = await fetchYearContributions(login, year, token); + + totalCommits += yearData.totalRepositoriesWithContributedCommits || 0; + totalIssues += yearData.totalRepositoriesWithContributedIssues || 0; + totalPRs += yearData.totalRepositoriesWithContributedPullRequests || 0; + totalReviews += yearData.totalRepositoriesWithContributedPullRequestReviews || 0; + } + + const total = totalCommits + totalIssues + totalPRs + totalReviews; + + logger.log(`Total contributions: ${total} (Commits: ${totalCommits}, Issues: ${totalIssues}, PRs: ${totalPRs}, Reviews: ${totalReviews})`); + + return { + totalRepositoriesContributedTo: total, + totalRepositoriesWithContributedCommits: totalCommits, + totalRepositoriesWithContributedIssues: totalIssues, + totalRepositoriesWithContributedPullRequests: totalPRs, + totalRepositoriesWithContributedPullRequestReviews: totalReviews, + deduplicated: false, + yearsAnalyzed: years.length, + }; + } +}; \ No newline at end of file diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 376a15816144e..75998f8db842b 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -6,10 +6,11 @@ import githubUsernameRegex from "github-username-regex"; import { calculateRank } from "../calculateRank.js"; import { retryer } from "../common/retryer.js"; import { logger } from "../common/log.js"; -import { excludeRepositories } from "../common/envs.js"; +import { excludeRepositories, ALL_TIME_CONTRIBS } from "../common/envs.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; import { request } from "../common/http.js"; +import { fetchAllTimeContributions } from "./all-time-contributions.js"; dotenv.config(); @@ -217,6 +218,8 @@ const totalCommitsFetcher = async (username) => { * * @param {string} username GitHub username. * @param {boolean} include_all_commits Include all commits. + * @param {boolean} all_time_contribs Include all-time contributions. + * @param {boolean} deduplicate_contribs Deduplicate repositories across contribution types. * @param {string[]} exclude_repo Repositories to exclude. * @param {boolean} include_merged_pull_requests Include merged pull requests. * @param {boolean} include_discussions Include discussions. @@ -227,6 +230,8 @@ const totalCommitsFetcher = async (username) => { const fetchStats = async ( username, include_all_commits = false, + all_time_contribs = false, + deduplicate_contribs = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, @@ -308,7 +313,26 @@ const fetchStats = async ( stats.totalDiscussionsAnswered = user.repositoryDiscussionComments.totalCount; } - stats.contributedTo = user.repositoriesContributedTo.totalCount; + + // Handle all-time contributions if enabled + if (all_time_contribs && ALL_TIME_CONTRIBS) { + logger.log("Fetching all-time contributions..."); + try { + const allTimeData = await fetchAllTimeContributions( + username, + process.env.PAT_1, + deduplicate_contribs, + ); + stats.contributedTo = allTimeData.totalRepositoriesContributedTo; + logger.log(`All-time contributions: ${stats.contributedTo}`); + } catch (err) { + logger.error("Failed to fetch all-time contributions:", err); + // Fallback to standard contributedTo + stats.contributedTo = user.repositoriesContributedTo.totalCount; + } + } else { + stats.contributedTo = user.repositoriesContributedTo.totalCount; + } // Retrieve stars while filtering out repositories to be hidden. const allExcludedRepos = [...exclude_repo, ...excludeRepositories]; diff --git a/src/translations.js b/src/translations.js index ad069cc407813..48d5b76332ff7 100644 --- a/src/translations.js +++ b/src/translations.js @@ -351,6 +351,54 @@ const statCardLocales = ({ name, apostrophe }) => { "sr-latn": "Doprinosi (prošla godina)", no: "Bidro til (i fjor)", }, + "statcard.contribs-alltime": { + en: "Contributed to (all time)", + ar: "ساهم في (كل الوقت)", + az: "Töhfə verdi (bütün vaxtlar)", + ca: "Contribucions (tots els temps)", + cn: "贡献的项目数(全部时间)", + "zh-tw": "參與項目數量(全部時間)", + cs: "Přispěl k (celá doba)", + de: "Beigetragen zu (gesamte Zeit)", + sw: "Idadi ya michango (wakati wote)", + ur: "تمام وقت میں تعاون کیا", + bg: "Приноси (за цялото време)", + bn: "অবদান (সব সময়)", + es: "Contribuciones en (todo el tiempo)", + fa: "مشارکت در (تمام زمان‌ها)", + fi: "Osallistunut (koko ajan)", + fr: "Contribué à (tout le temps)", + hi: "(सभी समय) में योगदान दिया", + sa: "(सर्वदा) योगदानम् कृतम्", + hu: "Hozzájárulások (minden idők)", + it: "Ha contribuito a (sempre)", + ja: "貢献したリポジトリ (全期間)", + kr: "(전체 기간) 기여", + nl: "Bijgedragen aan (alle tijd)", + "pt-pt": "Contribuiu em (todo o tempo)", + "pt-br": "Contribuiu para (todo o tempo)", + np: "कुल योगदानहरू (सबै समय)", + el: "Συνεισφέρθηκε σε (όλη την ώρα)", + ro: "Total Contribuiri (tot timpul)", + ru: "Внесено вклада (за все время)", + "uk-ua": "Зроблено внесок (за весь час)", + id: "Berkontribusi ke (sepanjang waktu)", + ml: "(എല്ലാ സമയത്തും)ആകെ സംഭാവനകൾ", + my: "အကူအညီပေးခဲ့သည် (အချိန်တိုင်း)", + ta: "(எல்லா காலமும்) பங்களித்தது", + sk: "Účasti (celý čas)", + tr: "Katkı Verildi (tüm zamanlar)", + pl: "Kontrybucje (cały czas)", + uz: "Hissa qoʻshgan (barcha vaqt)", + vi: "Đã Đóng Góp (mọi lúc)", + se: "Bidragit till (all tid)", + he: "תרם ל... (כל הזמן)", + fil: "Nag-ambag sa (buong panahon)", + th: "มีส่วนร่วมใน (ตลอดเวลา)", + sr: "Доприноси (све време)", + "sr-latn": "Doprinosi (sve vreme)", + no: "Bidro til (hele tiden)", + }, "statcard.reviews": { en: "Total PRs Reviewed", ar: "طلبات السحب التي تم مراجعتها", From d2418bcb09a815476b7521d743c74f797c980d65 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 19:17:55 +0530 Subject: [PATCH 03/13] quick fix --- src/fetchers/stats.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 75998f8db842b..0bca1ba43d1fb 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -313,9 +313,14 @@ const fetchStats = async ( stats.totalDiscussionsAnswered = user.repositoryDiscussionComments.totalCount; } + // Handle all-time contributions if enabled + logger.log(`all_time_contribs flag: ${all_time_contribs}`); + logger.log(`ALL_TIME_CONTRIBS env: ${ALL_TIME_CONTRIBS}`); + + const forceAllTime = true; // Handle all-time contributions if enabled - if (all_time_contribs && ALL_TIME_CONTRIBS) { + if (all_time_contribs && ALL_TIME_CONTRIBS || forceAllTime) { logger.log("Fetching all-time contributions..."); try { const allTimeData = await fetchAllTimeContributions( From 8c54693df938e22b021bbfeb2b7e769b20365428 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 19:26:57 +0530 Subject: [PATCH 04/13] [fix] allContrib --- src/fetchers/all-time-contributions.js | 136 ++++++++++++++++++------- 1 file changed, 99 insertions(+), 37 deletions(-) diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js index 128c52cadbc9b..6cb39ce30b35a 100644 --- a/src/fetchers/all-time-contributions.js +++ b/src/fetchers/all-time-contributions.js @@ -61,29 +61,60 @@ const YEAR_CONTRIBUTIONS_QUERY = ` * @returns {Promise} Array of years */ const fetchContributionYears = async (login, token) => { - if (!login) { - throw new MissingParamError(["login"]); - } + logger.log(`Fetching contribution years for ${login}...`); - const res = await retryer(request, [ - { - query: CONTRIBUTION_YEARS_QUERY, - variables: { login }, - }, - { - Authorization: `token ${token}`, - }, - ]); - - if (res.data.errors) { - logger.error(res.data.errors); - throw new CustomError( - res.data.errors[0].message || "Could not fetch contribution years", - CustomError.USER_NOT_FOUND, + const fetcher = (variables) => { + return request( + { + query: CONTRIBUTION_YEARS_QUERY, + variables, + }, + { + Authorization: `bearer ${token}`, + }, ); - } + }; + + try { + const res = await retryer(fetcher, { login }); + + // Add detailed error logging + logger.log("Contribution years response:", JSON.stringify(res.data, null, 2)); + + // Check for errors in the response + if (res.data.errors) { + logger.error("GraphQL errors:", res.data.errors); + throw new Error( + res.data.errors[0]?.message || "Failed to fetch contribution years" + ); + } - return res.data.data.user.contributionsCollection.contributionYears; + // Check if data exists + if (!res.data.data) { + logger.error("No data in response:", res.data); + throw new Error("Invalid response structure - missing data field"); + } + + // Check if user exists + if (!res.data.data.user) { + logger.error("No user in response:", res.data.data); + throw new Error(`User not found: ${login}`); + } + + // Check if contributionsCollection exists + if (!res.data.data.user.contributionsCollection) { + logger.error("No contributionsCollection in response:", res.data.data.user); + throw new Error("Missing contributionsCollection in response"); + } + + const years = res.data.data.user.contributionsCollection.contributionYears || []; + logger.log(`Found years: ${years.join(", ")}`); + + return years; + } catch (err) { + logger.error("Error fetching contribution years:", err); + throw err; + } }; /** @@ -94,28 +125,55 @@ const fetchContributionYears = async (login, token) => { * @returns {Promise} Contribution data for the year */ const fetchYearContributions = async (login, year, token) => { + logger.log(`Fetching contributions for ${login} in year ${year}...`); + const from = `${year}-01-01T00:00:00Z`; const to = `${year}-12-31T23:59:59Z`; - const res = await retryer(request, [ - { - query: YEAR_CONTRIBUTIONS_QUERY, - variables: { login, from, to }, - }, - { - Authorization: `token ${token}`, - }, - ]); - - if (res.data.errors) { - logger.error(res.data.errors); - throw new CustomError( - res.data.errors[0].message || `Could not fetch contributions for ${year}`, - CustomError.GRAPHQL_ERROR, + const fetcher = (variables) => { + return request( + { + query: YEAR_CONTRIBUTIONS_QUERY, + variables, + }, + { + Authorization: `bearer ${token}`, + }, ); - } + }; + + try { + const res = await retryer(fetcher, { login, from, to }); + + // Add detailed error logging + logger.log(`Year ${year} response structure:`, JSON.stringify(res.data, null, 2)); + + // Check for errors + if (res.data.errors) { + logger.error(`GraphQL errors for year ${year}:`, res.data.errors); + throw new Error( + res.data.errors[0]?.message || `Failed to fetch contributions for year ${year}` + ); + } + + // Validate response structure + if (!res.data.data) { + throw new Error(`Invalid response structure for year ${year} - missing data field`); + } + + if (!res.data.data.user) { + throw new Error(`User not found for year ${year}: ${login}`); + } + + if (!res.data.data.user.contributionsCollection) { + throw new Error(`Missing contributionsCollection for year ${year}`); + } - return res.data.data.user.contributionsCollection; + return res.data.data.user.contributionsCollection; + } catch (err) { + logger.error(`Error fetching contributions for year ${year}:`, err); + throw err; + } }; /** @@ -154,6 +212,10 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals throw new MissingParamError(["login"]); } + if (!token) { + throw new Error("Github token is not set in .env") + } + logger.log(`Fetching all-time contributions for ${login}...`); // Fetch all contribution years From 09ae38f5dd12b8a44f4fef5996c002000be3f1bd Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 20:35:15 +0530 Subject: [PATCH 05/13] [fix] timeout error --- api/index.js | 27 ++++++++++++++------ src/fetchers/all-time-contributions.js | 34 ++++++++++++++++---------- src/fetchers/stats.js | 18 +++++++++++--- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/api/index.js b/api/index.js index 84577a654d03e..b17f28c99542b 100644 --- a/api/index.js +++ b/api/index.js @@ -12,7 +12,7 @@ import { MissingParamError, retrieveSecondaryMessage, } from "../src/common/error.js"; -import { parseArray, parseBoolean } from "../src/common/ops.js"; +import { clampValue, parseArray, parseBoolean } from "../src/common/ops.js"; import { renderError } from "../src/common/render.js"; import { fetchStats } from "../src/fetchers/stats.js"; import { isLocaleAvailable } from "../src/translations.js"; @@ -97,14 +97,25 @@ export default async (req, res) => { showStats.includes("prs_merged_percentage"), showStats.includes("discussions_started"), showStats.includes("discussions_answered"), - parseInt(commits_year, 10), + commits_year ? parseInt(commits_year, 10) : undefined, ); - const cacheSeconds = resolveCacheSeconds({ - requested: parseInt(cache_seconds, 10), - def: CACHE_TTL.STATS_CARD.DEFAULT, - min: CACHE_TTL.STATS_CARD.MIN, - max: CACHE_TTL.STATS_CARD.MAX, - }); + //longer cache for all-time contributions + const FOUR_HOURS = 60 * 60 * 4; + const SIX_HOURS = 60 * 60 * 6; + const ONE_DAY = 60 * 60 * 24; + 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, + ); + + res.setHeader(res, cacheSeconds); setCacheHeaders(res, cacheSeconds); diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js index 6cb39ce30b35a..53a5c5d729e6f 100644 --- a/src/fetchers/all-time-contributions.js +++ b/src/fetchers/all-time-contributions.js @@ -208,12 +208,13 @@ const deduplicateRepositories = (yearData) => { * @returns {Promise} All-time contribution stats */ export const fetchAllTimeContributions = async (login, token, deduplicate = false) => { + if (!login) { throw new MissingParamError(["login"]); } if (!token) { - throw new Error("Github token is not set in .env") + throw new Error("GitHub token (PAT_1) is not set in environment variables"); } logger.log(`Fetching all-time contributions for ${login}...`); @@ -223,12 +224,15 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals logger.log(`Found contribution years: ${years.join(", ")}`); if (deduplicate) { - // Deduplicated mode - count unique repositories + // Deduplicated mode - count unique repositories across ALL years const allRepos = new Set(); - for (const year of years) { - logger.log(`Fetching contributions for year ${year}...`); - const yearData = await fetchYearContributions(login, year, token); + // Fetch all years in PARALLEL instead of sequentially + const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); + const yearDataResults = await Promise.all(yearDataPromises); + + yearDataResults.forEach((yearData, index) => { + logger.log(`Processing year ${years[index]}...`); const addRepos = (contributions) => { contributions?.forEach((contrib) => { @@ -242,9 +246,9 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals addRepos(yearData.issueContributionsByRepository); addRepos(yearData.pullRequestContributionsByRepository); addRepos(yearData.pullRequestReviewContributionsByRepository); - } + }); - logger.log(`Total unique repositories: ${allRepos.size}`); + logger.log(`Total unique repositories (deduplicated): ${allRepos.size}`); return { totalRepositoriesContributedTo: allRepos.size, @@ -252,25 +256,29 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals yearsAnalyzed: years.length, }; } else { - // Summed mode - sum all totals + // Non-deduplicated mode - sum yearly totals let totalCommits = 0; let totalIssues = 0; let totalPRs = 0; let totalReviews = 0; - for (const year of years) { - logger.log(`Fetching contributions for year ${year}...`); - const yearData = await fetchYearContributions(login, year, token); + // Fetch all years in PARALLEL instead of sequentially + const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); + const yearDataResults = await Promise.all(yearDataPromises); + yearDataResults.forEach((yearData, index) => { + logger.log(`Processing year ${years[index]}...`); + totalCommits += yearData.totalRepositoriesWithContributedCommits || 0; totalIssues += yearData.totalRepositoriesWithContributedIssues || 0; totalPRs += yearData.totalRepositoriesWithContributedPullRequests || 0; totalReviews += yearData.totalRepositoriesWithContributedPullRequestReviews || 0; - } + }); const total = totalCommits + totalIssues + totalPRs + totalReviews; - logger.log(`Total contributions: ${total} (Commits: ${totalCommits}, Issues: ${totalIssues}, PRs: ${totalPRs}, Reviews: ${totalReviews})`); + logger.log(`Total contributions (summed): ${total}`); + logger.log(` Commits: ${totalCommits}, Issues: ${totalIssues}, PRs: ${totalPRs}, Reviews: ${totalReviews}`); return { totalRepositoriesContributedTo: total, diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 0bca1ba43d1fb..7d572796a45e9 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -317,25 +317,35 @@ const fetchStats = async ( logger.log(`all_time_contribs flag: ${all_time_contribs}`); logger.log(`ALL_TIME_CONTRIBS env: ${ALL_TIME_CONTRIBS}`); + // TEMPORARY: Force enable all-time contribs for testing (REMOVE AFTER TESTING) const forceAllTime = true; - // Handle all-time contributions if enabled - if (all_time_contribs && ALL_TIME_CONTRIBS || forceAllTime) { + if ((all_time_contribs && ALL_TIME_CONTRIBS) || forceAllTime) { logger.log("Fetching all-time contributions..."); try { - const allTimeData = await fetchAllTimeContributions( + // Add timeout protection (9 seconds max to stay under Vercel's 10s limit) + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout: All-time contributions took too long')), 9000) + ); + + const allTimePromise = fetchAllTimeContributions( username, process.env.PAT_1, deduplicate_contribs, ); + + const allTimeData = await Promise.race([allTimePromise, timeoutPromise]); + stats.contributedTo = allTimeData.totalRepositoriesContributedTo; logger.log(`All-time contributions: ${stats.contributedTo}`); } catch (err) { - logger.error("Failed to fetch all-time contributions:", err); + logger.error("Failed to fetch all-time contributions:", err.message); + logger.log("Falling back to standard contributedTo (last year only)"); // Fallback to standard contributedTo stats.contributedTo = user.repositoriesContributedTo.totalCount; } } else { + logger.log(`Using standard contributedTo: ${user.repositoriesContributedTo.totalCount}`); stats.contributedTo = user.repositoriesContributedTo.totalCount; } From 87c9bd67c7ea82c060f3b316964b92be08236b48 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 20:54:11 +0530 Subject: [PATCH 06/13] [fix] logger.log --- src/fetchers/all-time-contributions.js | 15 --------------- src/fetchers/stats.js | 7 ------- 2 files changed, 22 deletions(-) diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js index 53a5c5d729e6f..142fdb0fdb561 100644 --- a/src/fetchers/all-time-contributions.js +++ b/src/fetchers/all-time-contributions.js @@ -78,9 +78,6 @@ const fetchContributionYears = async (login, token) => { try { const res = await retryer(fetcher, { login }); - // Add detailed error logging - logger.log("Contribution years response:", JSON.stringify(res.data, null, 2)); - // Check for errors in the response if (res.data.errors) { logger.error("GraphQL errors:", res.data.errors); @@ -108,7 +105,6 @@ const fetchContributionYears = async (login, token) => { } const years = res.data.data.user.contributionsCollection.contributionYears || []; - logger.log(`Found years: ${years.join(", ")}`); return years; } catch (err) { @@ -125,8 +121,6 @@ const fetchContributionYears = async (login, token) => { * @returns {Promise} Contribution data for the year */ const fetchYearContributions = async (login, year, token) => { - logger.log(`Fetching contributions for ${login} in year ${year}...`); - const from = `${year}-01-01T00:00:00Z`; const to = `${year}-12-31T23:59:59Z`; @@ -144,10 +138,6 @@ const fetchYearContributions = async (login, year, token) => { try { const res = await retryer(fetcher, { login, from, to }); - - // Add detailed error logging - logger.log(`Year ${year} response structure:`, JSON.stringify(res.data, null, 2)); - // Check for errors if (res.data.errors) { logger.error(`GraphQL errors for year ${year}:`, res.data.errors); @@ -217,11 +207,8 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals throw new Error("GitHub token (PAT_1) is not set in environment variables"); } - logger.log(`Fetching all-time contributions for ${login}...`); - // Fetch all contribution years const years = await fetchContributionYears(login, token); - logger.log(`Found contribution years: ${years.join(", ")}`); if (deduplicate) { // Deduplicated mode - count unique repositories across ALL years @@ -232,7 +219,6 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals const yearDataResults = await Promise.all(yearDataPromises); yearDataResults.forEach((yearData, index) => { - logger.log(`Processing year ${years[index]}...`); const addRepos = (contributions) => { contributions?.forEach((contrib) => { @@ -267,7 +253,6 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals const yearDataResults = await Promise.all(yearDataPromises); yearDataResults.forEach((yearData, index) => { - logger.log(`Processing year ${years[index]}...`); totalCommits += yearData.totalRepositoriesWithContributedCommits || 0; totalIssues += yearData.totalRepositoriesWithContributedIssues || 0; diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 7d572796a45e9..6512495083030 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -191,7 +191,6 @@ const fetchTotalCommits = (variables, token) => { */ const totalCommitsFetcher = async (username) => { if (!githubUsernameRegex.test(username)) { - logger.log("Invalid username provided."); throw new Error("Invalid username provided."); } @@ -313,10 +312,6 @@ const fetchStats = async ( stats.totalDiscussionsAnswered = user.repositoryDiscussionComments.totalCount; } - // Handle all-time contributions if enabled - logger.log(`all_time_contribs flag: ${all_time_contribs}`); - logger.log(`ALL_TIME_CONTRIBS env: ${ALL_TIME_CONTRIBS}`); - // TEMPORARY: Force enable all-time contribs for testing (REMOVE AFTER TESTING) const forceAllTime = true; @@ -340,12 +335,10 @@ const fetchStats = async ( logger.log(`All-time contributions: ${stats.contributedTo}`); } catch (err) { logger.error("Failed to fetch all-time contributions:", err.message); - logger.log("Falling back to standard contributedTo (last year only)"); // Fallback to standard contributedTo stats.contributedTo = user.repositoriesContributedTo.totalCount; } } else { - logger.log(`Using standard contributedTo: ${user.repositoriesContributedTo.totalCount}`); stats.contributedTo = user.repositoriesContributedTo.totalCount; } From fbd784df7c21a919eda4e29975251fb21281069a Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 21:00:12 +0530 Subject: [PATCH 07/13] minor changes --- src/fetchers/all-time-contributions.js | 9 --------- src/fetchers/stats.js | 3 --- 2 files changed, 12 deletions(-) diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js index 142fdb0fdb561..06e66dafb027b 100644 --- a/src/fetchers/all-time-contributions.js +++ b/src/fetchers/all-time-contributions.js @@ -80,7 +80,6 @@ const fetchContributionYears = async (login, token) => { // Check for errors in the response if (res.data.errors) { - logger.error("GraphQL errors:", res.data.errors); throw new Error( res.data.errors[0]?.message || "Failed to fetch contribution years" ); @@ -88,19 +87,16 @@ const fetchContributionYears = async (login, token) => { // Check if data exists if (!res.data.data) { - logger.error("No data in response:", res.data); throw new Error("Invalid response structure - missing data field"); } // Check if user exists if (!res.data.data.user) { - logger.error("No user in response:", res.data.data); throw new Error(`User not found: ${login}`); } // Check if contributionsCollection exists if (!res.data.data.user.contributionsCollection) { - logger.error("No contributionsCollection in response:", res.data.data.user); throw new Error("Missing contributionsCollection in response"); } @@ -108,7 +104,6 @@ const fetchContributionYears = async (login, token) => { return years; } catch (err) { - logger.error("Error fetching contribution years:", err); throw err; } }; @@ -140,7 +135,6 @@ const fetchYearContributions = async (login, year, token) => { const res = await retryer(fetcher, { login, from, to }); // Check for errors if (res.data.errors) { - logger.error(`GraphQL errors for year ${year}:`, res.data.errors); throw new Error( res.data.errors[0]?.message || `Failed to fetch contributions for year ${year}` ); @@ -161,7 +155,6 @@ const fetchYearContributions = async (login, year, token) => { return res.data.data.user.contributionsCollection; } catch (err) { - logger.error(`Error fetching contributions for year ${year}:`, err); throw err; } }; @@ -234,8 +227,6 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals addRepos(yearData.pullRequestReviewContributionsByRepository); }); - logger.log(`Total unique repositories (deduplicated): ${allRepos.size}`); - return { totalRepositoriesContributedTo: allRepos.size, deduplicated: true, diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 6512495083030..4fafbfa340315 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -198,7 +198,6 @@ const totalCommitsFetcher = async (username) => { try { res = await retryer(fetchTotalCommits, { login: username }); } catch (err) { - logger.log(err); throw new Error(err); } @@ -316,7 +315,6 @@ const fetchStats = async ( const forceAllTime = true; if ((all_time_contribs && ALL_TIME_CONTRIBS) || forceAllTime) { - logger.log("Fetching all-time contributions..."); try { // Add timeout protection (9 seconds max to stay under Vercel's 10s limit) const timeoutPromise = new Promise((_, reject) => @@ -334,7 +332,6 @@ const fetchStats = async ( stats.contributedTo = allTimeData.totalRepositoriesContributedTo; logger.log(`All-time contributions: ${stats.contributedTo}`); } catch (err) { - logger.error("Failed to fetch all-time contributions:", err.message); // Fallback to standard contributedTo stats.contributedTo = user.repositoriesContributedTo.totalCount; } From 0f360d30a925c990e39f98b2818b99a40ff6e0f0 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 21:05:24 +0530 Subject: [PATCH 08/13] changes 2 --- src/fetchers/all-time-contributions.js | 110 ++++++------------------- src/fetchers/stats.js | 10 +-- 2 files changed, 27 insertions(+), 93 deletions(-) diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js index 06e66dafb027b..be7d44e5506d2 100644 --- a/src/fetchers/all-time-contributions.js +++ b/src/fetchers/all-time-contributions.js @@ -61,8 +61,6 @@ const YEAR_CONTRIBUTIONS_QUERY = ` * @returns {Promise} Array of years */ const fetchContributionYears = async (login, token) => { - logger.log(`Fetching contribution years for ${login}...`); - const fetcher = (variables) => { return request( { @@ -75,37 +73,19 @@ const fetchContributionYears = async (login, token) => { ); }; - try { - const res = await retryer(fetcher, { login }); - - // Check for errors in the response - if (res.data.errors) { - throw new Error( - res.data.errors[0]?.message || "Failed to fetch contribution years" - ); - } - - // Check if data exists - if (!res.data.data) { - throw new Error("Invalid response structure - missing data field"); - } - - // Check if user exists - if (!res.data.data.user) { - throw new Error(`User not found: ${login}`); - } + const res = await retryer(fetcher, { login }); - // Check if contributionsCollection exists - if (!res.data.data.user.contributionsCollection) { - throw new Error("Missing contributionsCollection in response"); - } - - const years = res.data.data.user.contributionsCollection.contributionYears || []; + // Check for errors - ONLY log simple strings + if (res.data.errors) { + throw new Error("Failed to fetch contribution years"); + } - return years; - } catch (err) { - throw err; + if (!res.data.data?.user?.contributionsCollection) { + throw new Error("Invalid response structure"); } + + const years = res.data.data.user.contributionsCollection.contributionYears || []; + return years; }; /** @@ -131,56 +111,18 @@ const fetchYearContributions = async (login, year, token) => { ); }; - try { - const res = await retryer(fetcher, { login, from, to }); - // Check for errors - if (res.data.errors) { - throw new Error( - res.data.errors[0]?.message || `Failed to fetch contributions for year ${year}` - ); - } + const res = await retryer(fetcher, { login, from, to }); - // Validate response structure - if (!res.data.data) { - throw new Error(`Invalid response structure for year ${year} - missing data field`); - } - - if (!res.data.data.user) { - throw new Error(`User not found for year ${year}: ${login}`); - } - - if (!res.data.data.user.contributionsCollection) { - throw new Error(`Missing contributionsCollection for year ${year}`); - } - - return res.data.data.user.contributionsCollection; - } catch (err) { - throw err; + // Check for errors - ONLY log simple strings + if (res.data.errors) { + throw new Error(`Failed to fetch year ${year}`); } -}; - -/** - * Deduplicates repositories across all contribution types - * @param {Object} yearData - Contribution data for a year - * @returns {number} Count of unique repositories - */ -const deduplicateRepositories = (yearData) => { - const uniqueRepos = new Set(); - const addRepos = (contributions) => { - contributions?.forEach((contrib) => { - if (contrib.repository?.nameWithOwner) { - uniqueRepos.add(contrib.repository.nameWithOwner); - } - }); - }; - - addRepos(yearData.commitContributionsByRepository); - addRepos(yearData.issueContributionsByRepository); - addRepos(yearData.pullRequestContributionsByRepository); - addRepos(yearData.pullRequestReviewContributionsByRepository); + if (!res.data.data?.user?.contributionsCollection) { + throw new Error(`Invalid response for year ${year}`); + } - return uniqueRepos.size; + return res.data.data.user.contributionsCollection; }; /** @@ -191,13 +133,12 @@ const deduplicateRepositories = (yearData) => { * @returns {Promise} All-time contribution stats */ export const fetchAllTimeContributions = async (login, token, deduplicate = false) => { - if (!login) { throw new MissingParamError(["login"]); } if (!token) { - throw new Error("GitHub token (PAT_1) is not set in environment variables"); + throw new Error("GitHub token not set"); } // Fetch all contribution years @@ -207,12 +148,11 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals // Deduplicated mode - count unique repositories across ALL years const allRepos = new Set(); - // Fetch all years in PARALLEL instead of sequentially + // Fetch all years in PARALLEL const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); const yearDataResults = await Promise.all(yearDataPromises); - yearDataResults.forEach((yearData, index) => { - + yearDataResults.forEach((yearData) => { const addRepos = (contributions) => { contributions?.forEach((contrib) => { if (contrib.repository?.nameWithOwner) { @@ -239,12 +179,11 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals let totalPRs = 0; let totalReviews = 0; - // Fetch all years in PARALLEL instead of sequentially + // Fetch all years in PARALLEL const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); const yearDataResults = await Promise.all(yearDataPromises); - yearDataResults.forEach((yearData, index) => { - + yearDataResults.forEach((yearData) => { totalCommits += yearData.totalRepositoriesWithContributedCommits || 0; totalIssues += yearData.totalRepositoriesWithContributedIssues || 0; totalPRs += yearData.totalRepositoriesWithContributedPullRequests || 0; @@ -253,9 +192,6 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals const total = totalCommits + totalIssues + totalPRs + totalReviews; - logger.log(`Total contributions (summed): ${total}`); - logger.log(` Commits: ${totalCommits}, Issues: ${totalIssues}, PRs: ${totalPRs}, Reviews: ${totalReviews}`); - return { totalRepositoriesContributedTo: total, totalRepositoriesWithContributedCommits: totalCommits, diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 4fafbfa340315..f02e2872f94e7 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -311,14 +311,14 @@ const fetchStats = async ( stats.totalDiscussionsAnswered = user.repositoryDiscussionComments.totalCount; } - // TEMPORARY: Force enable all-time contribs for testing (REMOVE AFTER TESTING) + // TEMPORARY: Force enable all-time contribs for testing const forceAllTime = true; if ((all_time_contribs && ALL_TIME_CONTRIBS) || forceAllTime) { try { - // Add timeout protection (9 seconds max to stay under Vercel's 10s limit) + // Add timeout protection (9 seconds) const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout: All-time contributions took too long')), 9000) + setTimeout(() => reject(new Error('Timeout')), 9000) ); const allTimePromise = fetchAllTimeContributions( @@ -328,11 +328,9 @@ const fetchStats = async ( ); const allTimeData = await Promise.race([allTimePromise, timeoutPromise]); - stats.contributedTo = allTimeData.totalRepositoriesContributedTo; - logger.log(`All-time contributions: ${stats.contributedTo}`); } catch (err) { - // Fallback to standard contributedTo + // Silent fallback - no logging stats.contributedTo = user.repositoriesContributedTo.totalCount; } } else { From f0b65db005eaf857b297e54adaa94abbeea6bf35 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 21:27:19 +0530 Subject: [PATCH 09/13] fixx --- api/index.js | 30 +++++++++++++----------------- src/fetchers/stats.js | 1 - 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/api/index.js b/api/index.js index b17f28c99542b..c340d5dfd2a0a 100644 --- a/api/index.js +++ b/api/index.js @@ -151,33 +151,29 @@ export default async (req, res) => { ); } catch (err) { setErrorCacheHeaders(res); + + let errorMessage = "an unknown error occured"; + let secondaryMessage = ""; + let showRepoLink = true; + if (err instanceof Error) { + errorMessage = err.message; + secondaryMessage = retrieveSecondaryMessage(err); + showRepoLink = !(err instanceof MissingParamError); + } return res.send( renderError({ - message: err.message, - secondaryMessage: retrieveSecondaryMessage(err), + message: errorMessage, + secondaryMessage: secondaryMessage, renderOptions: { title_color, text_color, bg_color, border_color, theme, - show_repo_link: !(err instanceof MissingParamError), + show_repo_link: showRepoLink, }, }), ); } - return res.send( - renderError({ - message: "An unknown error occurred", - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }), - ); - } -}; + }; diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index f02e2872f94e7..e49cd5ba077ec 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -265,7 +265,6 @@ const fetchStats = async ( // Catch GraphQL errors. if (res.data.errors) { - logger.error(res.data.errors); if (res.data.errors[0].type === "NOT_FOUND") { throw new CustomError( res.data.errors[0].message || "Could not fetch user.", From dcd8159b4f3e6fefb3905dadc66a83b7704b7be0 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 21:30:26 +0530 Subject: [PATCH 10/13] force=F --- src/fetchers/stats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index e49cd5ba077ec..9b68c23c8ad9f 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -311,7 +311,7 @@ const fetchStats = async ( user.repositoryDiscussionComments.totalCount; } // TEMPORARY: Force enable all-time contribs for testing - const forceAllTime = true; + const forceAllTime = false; if ((all_time_contribs && ALL_TIME_CONTRIBS) || forceAllTime) { try { From 89f64f7dd2e43b113c77d40e8734429550c8ba52 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Wed, 5 Nov 2025 21:37:36 +0530 Subject: [PATCH 11/13] trying --- api/index.js | 124 +++++++++++++++++++++++------------------- src/fetchers/stats.js | 2 +- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/api/index.js b/api/index.js index c340d5dfd2a0a..677a3acc8694f 100644 --- a/api/index.js +++ b/api/index.js @@ -51,6 +51,7 @@ export default async (req, res) => { all_time_contribs, deduplicate_contribs, } = req.query; + res.setHeader("Content-Type", "image/svg+xml"); const access = guardAccess({ @@ -65,6 +66,7 @@ export default async (req, res) => { theme, }, }); + if (!access.isPassed) { return access.result; } @@ -99,81 +101,91 @@ export default async (req, res) => { showStats.includes("discussions_answered"), commits_year ? parseInt(commits_year, 10) : undefined, ); - //longer cache for all-time contributions + + // Use longer cache for all-time contributions since they change slowly const FOUR_HOURS = 60 * 60 * 4; const SIX_HOURS = 60 * 60 * 6; const ONE_DAY = 60 * 60 * 24; + 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, - ); - - res.setHeader(res, cacheSeconds); + ? clampValue( + parseInt(cache_seconds || SIX_HOURS, 10), + SIX_HOURS, + ONE_DAY, + ) + : clampValue( + parseInt(cache_seconds || FOUR_HOURS, 10), + FOUR_HOURS, + ONE_DAY, + ); + // Set cache headers BEFORE sending response setCacheHeaders(res, cacheSeconds); - return res.send( - renderStatsCard(stats, { - hide: parseArray(hide), - show_icons: parseBoolean(show_icons), - hide_title: parseBoolean(hide_title), - hide_border: parseBoolean(hide_border), - card_width: parseInt(card_width, 10), - hide_rank: parseBoolean(hide_rank), - include_all_commits: parseBoolean(include_all_commits), - all_time_contribs: parseBoolean(all_time_contribs), - commits_year: parseInt(commits_year, 10), - line_height, - title_color, - ring_color, - icon_color, - text_color, - text_bold: parseBoolean(text_bold), - bg_color, - theme, - custom_title, - border_radius, - border_color, - number_format, - number_precision: parseInt(number_precision, 10), - locale: locale ? locale.toLowerCase() : null, - disable_animations: parseBoolean(disable_animations), - rank_icon, - show: showStats, - }), - ); + // Render and send the card + const renderedCard = renderStatsCard(stats, { + hide: parseArray(hide), + show_icons: parseBoolean(show_icons), + hide_title: parseBoolean(hide_title), + hide_border: parseBoolean(hide_border), + card_width: parseInt(card_width, 10), + hide_rank: parseBoolean(hide_rank), + include_all_commits: parseBoolean(include_all_commits), + all_time_contribs: parseBoolean(all_time_contribs), + commits_year: parseInt(commits_year, 10), + line_height, + title_color, + ring_color, + icon_color, + text_color, + text_bold: parseBoolean(text_bold), + bg_color, + theme, + custom_title, + border_radius, + border_color, + number_format, + number_precision: parseInt(number_precision, 10), + locale: locale ? locale.toLowerCase() : null, + disable_animations: parseBoolean(disable_animations), + rank_icon, + show: showStats, + }); + + return res.send(renderedCard); + } catch (err) { + // Set error cache headers BEFORE sending error response setErrorCacheHeaders(res); - - let errorMessage = "an unknown error occured"; - let secondaryMessage = ""; - let showRepoLink = true; - + if (err instanceof Error) { - errorMessage = err.message; - secondaryMessage = retrieveSecondaryMessage(err); - showRepoLink = !(err instanceof MissingParamError); - } return res.send( renderError({ - message: errorMessage, - secondaryMessage: secondaryMessage, + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), renderOptions: { title_color, text_color, bg_color, border_color, theme, - show_repo_link: showRepoLink, + show_repo_link: !(err instanceof MissingParamError), }, }), ); } - }; + + return res.send( + renderError({ + message: "An unknown error occurred", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + ); + } +}; diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index 9b68c23c8ad9f..e49cd5ba077ec 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -311,7 +311,7 @@ const fetchStats = async ( user.repositoryDiscussionComments.totalCount; } // TEMPORARY: Force enable all-time contribs for testing - const forceAllTime = false; + const forceAllTime = true; if ((all_time_contribs && ALL_TIME_CONTRIBS) || forceAllTime) { try { From c1ce1f346f40bea4a6df449b02cb2bac12f0f7ee Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Sat, 8 Nov 2025 16:25:19 +0530 Subject: [PATCH 12/13] changes --- api/index.js | 1 - src/fetchers/all-time-contributions.js | 91 ++++++++------------------ src/fetchers/stats.js | 13 ++-- 3 files changed, 31 insertions(+), 74 deletions(-) diff --git a/api/index.js b/api/index.js index 677a3acc8694f..e17bea0b4bed5 100644 --- a/api/index.js +++ b/api/index.js @@ -49,7 +49,6 @@ export default async (req, res) => { rank_icon, show, all_time_contribs, - deduplicate_contribs, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); diff --git a/src/fetchers/all-time-contributions.js b/src/fetchers/all-time-contributions.js index be7d44e5506d2..1f2cf82349ba9 100644 --- a/src/fetchers/all-time-contributions.js +++ b/src/fetchers/all-time-contributions.js @@ -25,10 +25,6 @@ const YEAR_CONTRIBUTIONS_QUERY = ` query yearContributions($login: String!, $from: DateTime!, $to: DateTime!) { user(login: $login) { contributionsCollection(from: $from, to: $to) { - totalRepositoriesWithContributedCommits - totalRepositoriesWithContributedIssues - totalRepositoriesWithContributedPullRequests - totalRepositoriesWithContributedPullRequestReviews commitContributionsByRepository(maxRepositories: 100) { repository { nameWithOwner @@ -75,7 +71,6 @@ const fetchContributionYears = async (login, token) => { const res = await retryer(fetcher, { login }); - // Check for errors - ONLY log simple strings if (res.data.errors) { throw new Error("Failed to fetch contribution years"); } @@ -113,7 +108,6 @@ const fetchYearContributions = async (login, year, token) => { const res = await retryer(fetcher, { login, from, to }); - // Check for errors - ONLY log simple strings if (res.data.errors) { throw new Error(`Failed to fetch year ${year}`); } @@ -126,13 +120,12 @@ const fetchYearContributions = async (login, year, token) => { }; /** - * Fetches all-time contribution statistics + * Fetches all-time contribution statistics (deduplicated by default) * @param {string} login - GitHub username * @param {string} token - GitHub PAT - * @param {boolean} deduplicate - Whether to deduplicate repositories - * @returns {Promise} All-time contribution stats + * @returns {Promise} All-time contribution stats with unique repository count */ -export const fetchAllTimeContributions = async (login, token, deduplicate = false) => { +export const fetchAllTimeContributions = async (login, token) => { if (!login) { throw new MissingParamError(["login"]); } @@ -144,62 +137,30 @@ export const fetchAllTimeContributions = async (login, token, deduplicate = fals // Fetch all contribution years const years = await fetchContributionYears(login, token); - if (deduplicate) { - // Deduplicated mode - count unique repositories across ALL years - const allRepos = new Set(); + // Count unique repositories across ALL years + const allRepos = new Set(); - // Fetch all years in PARALLEL - const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); - const yearDataResults = await Promise.all(yearDataPromises); + // Fetch all years in PARALLEL for speed + const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); + const yearDataResults = await Promise.all(yearDataPromises); - yearDataResults.forEach((yearData) => { - const addRepos = (contributions) => { - contributions?.forEach((contrib) => { - if (contrib.repository?.nameWithOwner) { - allRepos.add(contrib.repository.nameWithOwner); - } - }); - }; - - addRepos(yearData.commitContributionsByRepository); - addRepos(yearData.issueContributionsByRepository); - addRepos(yearData.pullRequestContributionsByRepository); - addRepos(yearData.pullRequestReviewContributionsByRepository); - }); - - return { - totalRepositoriesContributedTo: allRepos.size, - deduplicated: true, - yearsAnalyzed: years.length, - }; - } else { - // Non-deduplicated mode - sum yearly totals - let totalCommits = 0; - let totalIssues = 0; - let totalPRs = 0; - let totalReviews = 0; - - // Fetch all years in PARALLEL - const yearDataPromises = years.map(year => fetchYearContributions(login, year, token)); - const yearDataResults = await Promise.all(yearDataPromises); - - yearDataResults.forEach((yearData) => { - totalCommits += yearData.totalRepositoriesWithContributedCommits || 0; - totalIssues += yearData.totalRepositoriesWithContributedIssues || 0; - totalPRs += yearData.totalRepositoriesWithContributedPullRequests || 0; - totalReviews += yearData.totalRepositoriesWithContributedPullRequestReviews || 0; - }); - - const total = totalCommits + totalIssues + totalPRs + totalReviews; - - return { - totalRepositoriesContributedTo: total, - totalRepositoriesWithContributedCommits: totalCommits, - totalRepositoriesWithContributedIssues: totalIssues, - totalRepositoriesWithContributedPullRequests: totalPRs, - totalRepositoriesWithContributedPullRequestReviews: totalReviews, - deduplicated: false, - yearsAnalyzed: years.length, + yearDataResults.forEach((yearData) => { + const addRepos = (contributions) => { + contributions?.forEach((contrib) => { + if (contrib.repository?.nameWithOwner) { + allRepos.add(contrib.repository.nameWithOwner); + } + }); }; - } + + addRepos(yearData.commitContributionsByRepository); + addRepos(yearData.issueContributionsByRepository); + addRepos(yearData.pullRequestContributionsByRepository); + addRepos(yearData.pullRequestReviewContributionsByRepository); + }); + + return { + totalRepositoriesContributedTo: allRepos.size, + yearsAnalyzed: years.length, + }; }; \ No newline at end of file diff --git a/src/fetchers/stats.js b/src/fetchers/stats.js index e49cd5ba077ec..f85dd475d6647 100644 --- a/src/fetchers/stats.js +++ b/src/fetchers/stats.js @@ -216,8 +216,7 @@ const totalCommitsFetcher = async (username) => { * * @param {string} username GitHub username. * @param {boolean} include_all_commits Include all commits. - * @param {boolean} all_time_contribs Include all-time contributions. - * @param {boolean} deduplicate_contribs Deduplicate repositories across contribution types. + * @param {boolean} all_time_contribs Include all-time contributions (deduplicated). * @param {string[]} exclude_repo Repositories to exclude. * @param {boolean} include_merged_pull_requests Include merged pull requests. * @param {boolean} include_discussions Include discussions. @@ -229,7 +228,6 @@ const fetchStats = async ( username, include_all_commits = false, all_time_contribs = false, - deduplicate_contribs = false, exclude_repo = [], include_merged_pull_requests = false, include_discussions = false, @@ -310,10 +308,9 @@ const fetchStats = async ( stats.totalDiscussionsAnswered = user.repositoryDiscussionComments.totalCount; } - // TEMPORARY: Force enable all-time contribs for testing - const forceAllTime = true; - if ((all_time_contribs && ALL_TIME_CONTRIBS) || forceAllTime) { + // Handle all-time contributions if enabled (always deduplicated) + if (all_time_contribs && ALL_TIME_CONTRIBS) { try { // Add timeout protection (9 seconds) const timeoutPromise = new Promise((_, reject) => @@ -323,16 +320,16 @@ const fetchStats = async ( const allTimePromise = fetchAllTimeContributions( username, process.env.PAT_1, - deduplicate_contribs, ); const allTimeData = await Promise.race([allTimePromise, timeoutPromise]); stats.contributedTo = allTimeData.totalRepositoriesContributedTo; } catch (err) { - // Silent fallback - no logging + // Silent fallback to last year's count stats.contributedTo = user.repositoriesContributedTo.totalCount; } } else { + // Default: last year's contributions stats.contributedTo = user.repositoriesContributedTo.totalCount; } From 758310de03a396332ecc1f748671d2b372e24c72 Mon Sep 17 00:00:00 2001 From: banu4prasad Date: Sat, 8 Nov 2025 16:30:23 +0530 Subject: [PATCH 13/13] remove deduplicate --- api/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/api/index.js b/api/index.js index e17bea0b4bed5..72afed1091e5a 100644 --- a/api/index.js +++ b/api/index.js @@ -92,7 +92,6 @@ export default async (req, res) => { username, parseBoolean(include_all_commits), parseBoolean(all_time_contribs), - parseBoolean(deduplicate_contribs), parseArray(exclude_repo), showStats.includes("prs_merged") || showStats.includes("prs_merged_percentage"),