diff --git a/src/github.js b/src/github.js
index c713cad..991609a 100644
--- a/src/github.js
+++ b/src/github.js
@@ -5,32 +5,52 @@
const GITHUB_API = "https://api.github.com";
/**
- * Fetch all pull requests by a user across all public repos.
+ * Robust fetch wrapper with timeout and better error handling
*/
-async function fetchUserPullRequests(username) {
- const perPage = 100;
- let page = 1;
- let allPRs = [];
+async function safeFetch(url, options = {}) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
- while (true) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:pr&per_page=${perPage}&page=${page}`;
+ try {
const res = await fetch(url, {
+ ...options,
headers: {
"User-Agent": "gitly-app",
Accept: "application/vnd.github.v3+json",
+ ...(options.headers || {}),
},
+ signal: controller.signal,
});
+ clearTimeout(timeoutId);
+
if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
+ console.warn(`GitHub API warning: ${res.status} for ${url}`);
+ return null;
}
+ return await res.json();
+ } catch (err) {
+ clearTimeout(timeoutId);
+ console.error(`Fetch error for ${url}:`, err.message);
+ return null;
+ }
+}
- const data = await res.json();
- allPRs = allPRs.concat(data.items || []);
+/**
+ * Fetch all pull requests by a user across all public repos.
+ */
+async function fetchUserPullRequests(username) {
+ const perPage = 100;
+ let page = 1;
+ let allPRs = [];
- if (allPRs.length >= data.total_count || data.items.length < perPage) {
- break;
- }
+ while (page <= 5) {
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr&per_page=${perPage}&page=${page}`;
+ const data = await safeFetch(url);
+ if (!data || !data.items || data.items.length === 0) break;
+
+ allPRs = allPRs.concat(data.items);
+ if (allPRs.length >= data.total_count || data.items.length < perPage) break;
page++;
}
@@ -38,123 +58,45 @@ async function fetchUserPullRequests(username) {
}
/**
- * Fetch open pull requests by a user across all public repos.
+ * PR counts
*/
async function fetchOpenPullRequests(username) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:pr+state:open&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr+state:open&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
-/**
- * Fetch closed (merged + rejected) pull requests by a user across all public repos.
- */
async function fetchClosedPullRequests(username) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:pr+state:closed&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr+state:closed&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
-/**
- * Fetch merged pull requests by a user across all public repos.
- */
async function fetchMergedPullRequests(username) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:pr+is:merged&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:pr+is:merged&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
/**
- * Fetch total issues by a user across all public repos.
+ * Issue counts
*/
async function fetchUserIssues(username) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:issue&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:issue&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
-/**
- * Fetch open issues by a user across all public repos.
- */
async function fetchOpenIssues(username) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:issue+state:open&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:issue+state:open&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
-/**
- * Fetch closed issues by a user across all public repos.
- */
async function fetchClosedIssues(username) {
- const url = `${GITHUB_API}/search/issues?q=author:${username}+type:issue+state:closed&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/issues?q=author:${encodeURIComponent(username)}+type:issue+state:closed&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
/**
@@ -162,6 +104,7 @@ async function fetchClosedIssues(username) {
*/
function groupPRsByRepo(prs) {
const repoMap = {};
+ if (!Array.isArray(prs)) return repoMap;
for (const pr of prs) {
const repoUrl = pr.repository_url || "";
const repoName = repoUrl.split("/repos/")[1] || "unknown";
@@ -174,64 +117,43 @@ function groupPRsByRepo(prs) {
* Fetch user profile info.
*/
async function fetchUserProfile(username) {
- const url = `${GITHUB_API}/users/${username}`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
- }
-
- return res.json();
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}`;
+ const data = await safeFetch(url);
+ if (!data) return { login: username, name: username, public_repos: 0, followers: 0, following: 0 };
+ return data;
}
/**
* Fetch REAL contribution data by scraping GitHub's contribution page.
- * Works without authentication - gets the exact data GitHub displays.
*/
async function fetchContributionData(username) {
- const url = `https://github.com/users/${username}/contributions`;
- const res = await fetch(url, {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- Accept: "text/html,application/xhtml+xml",
- },
- });
-
- if (!res.ok) {
- throw new Error(`Failed to fetch contributions page: ${res.status}`);
+ const url = `https://github.com/users/${encodeURIComponent(username)}/contributions`;
+ try {
+ const res = await fetch(url, {
+ headers: {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ Accept: "text/html",
+ },
+ });
+ if (!res.ok) return { totalContributions: 0, days: [] };
+ const html = await res.text();
+ return parseContributionHTML(html);
+ } catch (err) {
+ console.error("Contribution scrape error:", err.message);
+ return { totalContributions: 0, days: [] };
}
-
- const html = await res.text();
- return parseContributionHTML(html);
}
/**
* Parse GitHub contributions HTML to extract real contribution data.
- *
- * GitHub's HTML structure:
- *
|
- * 7 contributions on March 30th.
- *
- * We extract dates from td elements and counts from tool-tip elements,
- * then match them in order.
*/
function parseContributionHTML(html) {
- // Extract total contributions from the header text
let totalContributions = 0;
- const totalMatch = html.match(
- /(\d[\d,]*)\s+contributions?\s+in\s+the\s+last\s+year/i
- );
+ const totalMatch = html.match(/(\d[\d,]*)\s+contributions?\s+in\s+the\s+last\s+year/i);
if (totalMatch) {
totalContributions = parseInt(totalMatch[1].replace(/,/g, ""), 10);
}
- // Extract day cells with stable ids, dates, and levels.
const tdRegex = /]*\bdata-date="\d{4}-\d{2}-\d{2}"[^>]*><\/td>/g;
const dates = [];
let match;
@@ -249,13 +171,7 @@ function parseContributionHTML(html) {
});
}
- // Extract contribution counts from tooltips keyed by their target day cell id.
- // Formats include:
- // - "7 contributions on March 30th."
- // - "1 contribution on ..."
- // - "No contributions on ..."
- const tipRegex =
- /]*for="([^"]+)"[^>]*>([\s\S]*?)<\/tool-tip>/g;
+ const tipRegex = /]*for="([^"]+)"[^>]*>([\s\S]*?)<\/tool-tip>/g;
const countById = new Map();
while ((match = tipRegex.exec(html)) !== null) {
const targetId = match[1];
@@ -265,10 +181,9 @@ function parseContributionHTML(html) {
countById.set(targetId, count);
}
- // Build days array by id so each date receives its own exact count.
const days = [];
for (let i = 0; i < dates.length; i++) {
- const count = countById.has(dates[i].id) ? countById.get(dates[i].id) : 0;
+ const count = countById.has(dates[i].id) ? countById.get(dates[i].id) : (dates[i].level * 2);
days.push({
date: dates[i].date,
count: count,
@@ -276,59 +191,28 @@ function parseContributionHTML(html) {
});
}
- // If no tooltips found at all, estimate from level.
- if (countById.size === 0 && days.length > 0) {
- const levelEstimates = { 0: 0, 1: 2, 2: 5, 3: 8, 4: 15 };
- for (const day of days) {
- day.count = levelEstimates[day.level] || 0;
- }
- }
-
- // Sort by date
days.sort((a, b) => a.date.localeCompare(b.date));
-
- // If total wasn't found, sum the days
if (totalContributions === 0) {
totalContributions = days.reduce((sum, d) => sum + d.count, 0);
}
- return {
- totalContributions,
- days,
- };
+ return { totalContributions, days };
}
/**
- * Fetch all commits by a user with timestamps to calculate actual working time.
- * Formula: TWt = Σ (Ti+1 - Ti) for all i where (Ti+1 - Ti) < 5 hours
- * Where TWt = Total Working Time, Ti = Timestamp of commit i
+ * Fetch commit timestamps for working hours.
*/
async function fetchUserCommitTimestamps(username) {
const perPage = 100;
let page = 1;
let allCommits = [];
- while (page <= 10) { // Limit to ~1000 most recent commits
- const url = `${GITHUB_API}/search/commits?q=author:${username}&per_page=${perPage}&page=${page}&sort=author-date`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- console.warn(`GitHub API warning: ${res.status} on page ${page}`);
- break;
- }
+ while (page <= 10) {
+ const url = `${GITHUB_API}/search/commits?q=author:${encodeURIComponent(username)}&per_page=${perPage}&page=${page}&sort=author-date`;
+ const data = await safeFetch(url);
+ if (!data || !data.items || data.items.length === 0) break;
- const data = await res.json();
- const commits = data.items || [];
-
- if (commits.length === 0) break;
-
- // Extract commit timestamps
- for (const commit of commits) {
+ for (const commit of data.items) {
if (commit.commit && commit.commit.author && commit.commit.author.date) {
allCommits.push({
timestamp: new Date(commit.commit.author.date).getTime(),
@@ -336,8 +220,7 @@ async function fetchUserCommitTimestamps(username) {
});
}
}
-
- if (commits.length < perPage) break;
+ if (data.items.length < perPage) break;
page++;
}
@@ -345,13 +228,9 @@ async function fetchUserCommitTimestamps(username) {
return { totalWorkingHours: 0, commitCount: 0 };
}
- // Sort commits by timestamp ascending
allCommits.sort((a, b) => a.timestamp - b.timestamp);
-
- // Calculate working time: sum all gaps < 5 hours (18000000 ms)
- const MAX_GAP = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
+ const MAX_GAP = 5 * 60 * 60 * 1000;
let totalWorkingMs = 0;
-
for (let i = 0; i < allCommits.length - 1; i++) {
const gap = allCommits[i + 1].timestamp - allCommits[i].timestamp;
if (gap > 0 && gap < MAX_GAP) {
@@ -360,69 +239,37 @@ async function fetchUserCommitTimestamps(username) {
}
const totalWorkingHours = Math.round((totalWorkingMs / (1000 * 60 * 60)) * 100) / 100;
-
- return {
- totalWorkingHours,
- commitCount: allCommits.length,
- };
+ return { totalWorkingHours, commitCount: allCommits.length };
}
/**
- * Fetch total commit count by a user across all public repos.
+ * Fetch total commit count.
*/
async function fetchTotalCommitCount(username) {
- const url = `${GITHUB_API}/search/commits?q=author:${username}&per_page=1`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- console.warn(`GitHub commits API warning: ${res.status}`);
- return 0;
- }
-
- const data = await res.json();
- return data.total_count || 0;
+ const url = `${GITHUB_API}/search/commits?q=author:${encodeURIComponent(username)}&per_page=1`;
+ const data = await safeFetch(url);
+ return data ? (data.total_count || 0) : 0;
}
/**
- * Fetch total stars of a user's repositories.
+ * Fetch total stars.
*/
async function fetchUserTotalStars(username) {
let page = 1;
let totalStars = 0;
- while (true) {
+ while (page <= 5) {
const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?type=owner&per_page=100&page=${page}`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) {
- throw new Error(`GitHub repos API error: ${res.status} ${res.statusText}`);
- }
+ const data = await safeFetch(url);
+ if (!data || !Array.isArray(data) || data.length === 0) break;
- const repos = await res.json();
- if (!Array.isArray(repos) || repos.length === 0) {
- break;
- }
-
- for (const repo of repos) {
+ for (const repo of data) {
if (!repo.fork) {
totalStars += repo.stargazers_count || 0;
}
}
- if (repos.length < 100) {
- break;
- }
-
+ if (data.length < 100) break;
page++;
}
@@ -430,21 +277,14 @@ async function fetchUserTotalStars(username) {
}
/**
- * Fetch lines changed in recent PRs.
+ * Fetch lines changed.
*/
async function fetchRecentPRLinesChanged(prs, maxPRs = 30) {
- const targetPRs = prs
+ const targetPRs = (prs || [])
.filter((pr) => pr && pr.pull_request && pr.pull_request.url)
.slice(0, maxPRs);
- if (targetPRs.length === 0) {
- return 0;
- }
-
- const headers = {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- };
+ if (targetPRs.length === 0) return 0;
const concurrency = 6;
let totalChanged = 0;
@@ -453,14 +293,9 @@ async function fetchRecentPRLinesChanged(prs, maxPRs = 30) {
const batch = targetPRs.slice(i, i + concurrency);
const results = await Promise.all(
batch.map(async (pr) => {
- try {
- const res = await fetch(pr.pull_request.url, { headers });
- if (!res.ok) return 0;
- const data = await res.json();
- return (data.additions || 0) + (data.deletions || 0);
- } catch {
- return 0;
- }
+ const data = await safeFetch(pr.pull_request.url);
+ if (!data) return 0;
+ return (data.additions || 0) + (data.deletions || 0);
})
);
@@ -471,45 +306,23 @@ async function fetchRecentPRLinesChanged(prs, maxPRs = 30) {
}
/**
- * Fetch language usage across user's public repos.
- * Aggregates language bytes from all repos.
+ * Fetch user languages.
*/
async function fetchUserLanguages(username) {
let page = 1;
const langMap = {};
let totalBytes = 0;
- while (page <= 5) {
- const url = `${GITHUB_API}/users/${username}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
- const res = await fetch(url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
-
- if (!res.ok) break;
+ while (page <= 3) {
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
+ const repos = await safeFetch(url);
+ if (!repos || !repos.length) break;
- const repos = await res.json();
- if (!repos.length) break;
-
- // Fetch language data for each repo (only fork=false to count unique repos)
const promises = repos
.filter((r) => !r.fork && r.size > 0)
- .slice(0, 30) // limit to 30 repos to avoid rate limits
+ .slice(0, 20)
.map(async (repo) => {
- try {
- const langRes = await fetch(repo.languages_url, {
- headers: {
- "User-Agent": "gitly-app",
- Accept: "application/vnd.github.v3+json",
- },
- });
- if (langRes.ok) {
- return langRes.json();
- }
- } catch {}
- return {};
+ return await safeFetch(repo.languages_url) || {};
});
const results = await Promise.all(promises);
@@ -525,53 +338,26 @@ async function fetchUserLanguages(username) {
page++;
}
- // Convert to array and calculate percentages
- let languages = Object.entries(langMap)
+ const languages = Object.entries(langMap)
.map(([name, bytes]) => ({
name,
bytes,
- rawPercentage: totalBytes > 0 ? ((bytes / totalBytes) * 100) : 0,
+ percentage: totalBytes > 0 ? Math.round(((bytes / totalBytes) * 100) * 100) / 100 : 0,
}))
.sort((a, b) => b.bytes - a.bytes);
- // Fix percentages to add up to 100%
- // Round each to 2 decimal places, ensure minimum 0.01%
- languages = languages.map((lang, idx) => {
- let pct = Math.round(lang.rawPercentage * 100) / 100;
- if (pct < 0.01 && lang.bytes > 0) pct = 0.01;
- return { name: lang.name, bytes: lang.bytes, percentage: pct };
- });
-
- // Adjust last item to make total exactly 100%
- const totalPct = languages.reduce((sum, l) => sum + l.percentage, 0);
- if (languages.length > 0 && Math.abs(totalPct - 100) > 0.01) {
- const diff = 100 - totalPct;
- languages[languages.length - 1].percentage += diff;
- languages[languages.length - 1].percentage = Math.round(languages[languages.length - 1].percentage * 100) / 100;
- }
-
- // Remove languages with 0 bytes
- languages = languages.filter(l => l.bytes > 0);
-
return { languages, totalBytes };
}
-/**
- * Fetch languages by repo count (how many repos use each language).
- */
async function fetchUserLanguagesByRepos(username) {
let page = 1;
const langRepoCount = {};
let totalRepos = 0;
- while (page <= 5) {
- const url = `${GITHUB_API}/users/${username}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
- const res = await fetch(url, {
- headers: { "User-Agent": "gitly-app", Accept: "application/vnd.github.v3+json" },
- });
- if (!res.ok) break;
- const repos = await res.json();
- if (!repos.length) break;
+ while (page <= 3) {
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
+ const repos = await safeFetch(url);
+ if (!repos || !repos.length) break;
for (const repo of repos) {
if (repo.fork || repo.size <= 0) continue;
@@ -586,7 +372,7 @@ async function fetchUserLanguagesByRepos(username) {
page++;
}
- let languages = Object.entries(langRepoCount)
+ const languages = Object.entries(langRepoCount)
.map(([name, count]) => ({
name,
count,
@@ -594,49 +380,27 @@ async function fetchUserLanguagesByRepos(username) {
}))
.sort((a, b) => b.count - a.count);
- const totalPct = languages.reduce((sum, l) => sum + l.percentage, 0);
- if (languages.length > 0 && Math.abs(totalPct - 100) > 0.01) {
- languages[languages.length - 1].percentage += (100 - totalPct);
- languages[languages.length - 1].percentage = Math.round(languages[languages.length - 1].percentage * 100) / 100;
- }
-
return { languages, totalRepos };
}
-/**
- * Fetch languages by commits (uses stargazers_count + size as activity proxy).
- * Since GitHub API doesn't give commit counts per language without auth,
- * we weight by repo size * (1 + stars) as a proxy for commit activity.
- */
async function fetchUserLanguagesByCommits(username) {
let page = 1;
const langActivity = {};
let totalActivity = 0;
- while (page <= 5) {
- const url = `${GITHUB_API}/users/${username}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
- const res = await fetch(url, {
- headers: { "User-Agent": "gitly-app", Accept: "application/vnd.github.v3+json" },
- });
- if (!res.ok) break;
- const repos = await res.json();
- if (!repos.length) break;
+ while (page <= 3) {
+ const url = `${GITHUB_API}/users/${encodeURIComponent(username)}/repos?per_page=100&page=${page}&type=owner&sort=updated`;
+ const repos = await safeFetch(url);
+ if (!repos || !repos.length) break;
const promises = repos
.filter((r) => !r.fork && r.size > 0)
- .slice(0, 30)
+ .slice(0, 20)
.map(async (repo) => {
- try {
- const langRes = await fetch(repo.languages_url, {
- headers: { "User-Agent": "gitly-app", Accept: "application/vnd.github.v3+json" },
- });
- if (langRes.ok) {
- const langs = await langRes.json();
- const weight = 1 + (repo.stargazers_count || 0) * 0.1;
- return { langs, weight };
- }
- } catch {}
- return { langs: {}, weight: 1 };
+ const langs = await safeFetch(repo.languages_url);
+ if (!langs) return { langs: {}, weight: 1 };
+ const weight = 1 + (repo.stargazers_count || 0) * 0.1;
+ return { langs, weight };
});
const results = await Promise.all(promises);
@@ -653,7 +417,7 @@ async function fetchUserLanguagesByCommits(username) {
page++;
}
- let languages = Object.entries(langActivity)
+ const languages = Object.entries(langActivity)
.map(([name, activity]) => ({
name,
activity: Math.round(activity),
@@ -661,12 +425,6 @@ async function fetchUserLanguagesByCommits(username) {
}))
.sort((a, b) => b.activity - a.activity);
- const totalPct = languages.reduce((sum, l) => sum + l.percentage, 0);
- if (languages.length > 0 && Math.abs(totalPct - 100) > 0.01) {
- languages[languages.length - 1].percentage += (100 - totalPct);
- languages[languages.length - 1].percentage = Math.round(languages[languages.length - 1].percentage * 100) / 100;
- }
-
return { languages, totalActivity };
}
|