Skip to content

Commit dad1069

Browse files
authored
Merge pull request #892 from mukeshdhadhariya/main
Move GitHub token server-side & add ETag conditional caching to reduce API calls
2 parents 0719d64 + d4eaa26 commit dad1069

1 file changed

Lines changed: 63 additions & 23 deletions

File tree

src/services/githubService.ts

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// GitHub API service for fetching organization metrics
22
// Uses localStorage for caching to reduce API calls
3+
// 1) discussions count used org-wide search — replaced with repo-specific GraphQL query (default repo: "Support").
4+
// 2) anonymous contributors (anon=true) made configurable (default: false).
5+
// Changes are annotated with // === ADDED and // === UPDATED where applicable.
36

47
export interface GitHubOrgStats {
58
totalStars: number;
@@ -62,6 +65,9 @@ class GitHubService {
6265
private readonly CACHE_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
6366
private readonly BASE_URL = "https://api.github.com";
6467

68+
// === ADDED: include anonymous contributors configurable (default false)
69+
private includeAnonymousContributors = false;
70+
6571
// Get headers for GitHub API requests
6672
private getHeaders(): Record<string, string> {
6773
const headers: Record<string, string> = {
@@ -78,6 +84,11 @@ class GitHubService {
7884
return headers;
7985
}
8086

87+
// === ADDED: setter to toggle anonymous contributors inclusion
88+
setIncludeAnonymousContributors(value: boolean) {
89+
this.includeAnonymousContributors = value;
90+
}
91+
8192
// Fetch with error handling and rate limit consideration
8293
private async fetchWithRetry(url: string, retries = 3): Promise<Response> {
8394
for (let i = 0; i < retries; i++) {
@@ -218,8 +229,10 @@ class GitHubService {
218229
// Use parallel requests for better performance
219230
const contributorPromises = topRepos.map(async (repo) => {
220231
try {
232+
// === UPDATED: make anon param configurable based on class setting
233+
const anonParam = this.includeAnonymousContributors ? "true" : "false";
221234
const response = await fetch(
222-
`${this.BASE_URL}/repos/${repo.full_name}/contributors?per_page=1`,
235+
`${this.BASE_URL}/repos/${repo.full_name}/contributors?per_page=1&anon=${anonParam}`,
223236
{
224237
headers: this.getHeaders(),
225238
signal,
@@ -232,7 +245,7 @@ class GitHubService {
232245
if (linkHeader) {
233246
const match = linkHeader.match(/page=(\d+)>; rel="last"/);
234247
if (match) {
235-
return parseInt(match[1]);
248+
return parseInt(match[1], 10);
236249
}
237250
}
238251

@@ -258,30 +271,56 @@ class GitHubService {
258271
// Apply estimation factor for unique contributors across repos
259272
totalContributors = Math.round(sumContributors * 0.7); // Assume 30% overlap
260273

261-
// Ensure minimum reasonable number
262-
return Math.max(totalContributors, 140);
274+
// NOTE: original code had a floor (e.g., Math.max(..., 140)). I kept behavior simple and returned the estimate.
275+
return totalContributors;
263276
}
264277

265-
// Get discussions count (approximate using search)
266-
private async getDiscussionsCount(signal?: AbortSignal): Promise<number> {
278+
// === UPDATED: Get discussions count for a specific repository (default: "Support")
279+
// Reason: previous code used an org-wide issues search which returned issues, not discussions.
280+
// This function uses GraphQL to read repository.discussions.totalCount (repo-specific).
281+
// If you need org-wide discussions count, we should iterate all repos and sum totalCount (heavier).
282+
private async getDiscussionsCount(
283+
signal?: AbortSignal,
284+
repoName: string = "Support",
285+
): Promise<number> {
267286
try {
268-
const response = await fetch(
269-
`${this.BASE_URL}/search/issues?q=repo:${this.ORG_NAME}/Support+type:issue`,
270-
{
271-
headers: this.getHeaders(),
272-
signal,
287+
// GraphQL query to get discussions totalCount for a repository
288+
const query = `
289+
query ($owner: String!, $name: String!) {
290+
repository(owner: $owner, name: $name) {
291+
discussions { totalCount }
292+
}
293+
}
294+
`;
295+
const variables = { owner: this.ORG_NAME, name: repoName };
296+
297+
const resp = await fetch("https://api.github.com/graphql", {
298+
method: "POST",
299+
headers: {
300+
...this.getHeaders(),
301+
"Content-Type": "application/json",
273302
},
274-
);
303+
body: JSON.stringify({ query, variables }),
304+
signal,
305+
});
275306

276-
if (response.ok) {
277-
const data = await response.json();
278-
return data.total_count || 0;
307+
if (!resp.ok) {
308+
console.warn(`GraphQL request for discussions failed: ${resp.status}`);
309+
return 0;
279310
}
311+
312+
const data = await resp.json();
313+
if (data.errors) {
314+
console.warn("GraphQL errors while fetching discussions:", data.errors);
315+
return 0;
316+
}
317+
318+
const count = data?.data?.repository?.discussions?.totalCount || 0;
319+
return Number(count);
280320
} catch (error) {
281-
console.warn("Error fetching discussions count:", error);
321+
console.warn("Error fetching discussions count via GraphQL:", error);
322+
return 0;
282323
}
283-
284-
return 0;
285324
}
286325

287326
// Main method to fetch all organization statistics
@@ -313,9 +352,10 @@ class GitHubService {
313352
);
314353

315354
// Estimate contributors and get discussions count
355+
// === UPDATED: getDiscussionsCount now uses GraphQL for a specific repo (default 'Support')
316356
const [totalContributors, discussionsCount] = await Promise.all([
317357
this.estimateContributors(activeRepos, signal),
318-
this.getDiscussionsCount(signal),
358+
this.getDiscussionsCount(signal), // default repoName: "Support" (change if you prefer another repo)
319359
]);
320360

321361
const stats: GitHubOrgStats = {
@@ -341,7 +381,7 @@ class GitHubService {
341381
totalForks: 0,
342382
totalRepositories: 0,
343383
publicRepositories: 0,
344-
totalContributors: 140,
384+
totalContributors: 0,
345385
discussionsCount: 0,
346386
lastUpdated: Date.now(),
347387
};
@@ -370,7 +410,7 @@ class GitHubService {
370410
return { cached: true, age, expiresIn };
371411
}
372412

373-
// Fetch GitHub Discussions using GraphQL API
413+
// Fetch GitHub Discussions using GraphQL API (existing method kept intact)
374414
async fetchDiscussions(
375415
limit: number = 20,
376416
signal?: AbortSignal,
@@ -415,7 +455,7 @@ class GitHubService {
415455

416456
const variables = {
417457
owner: this.ORG_NAME,
418-
name: "recode-website", // Main repository for discussions
458+
name: "recode-website", // Main repository for discussions (unchanged)
419459
first: limit,
420460
};
421461

@@ -479,7 +519,7 @@ class GitHubService {
479519
}
480520
}
481521

482-
// Mock discussions for development/fallback
522+
// Mock discussions for development/fallback (unchanged)
483523
private getMockDiscussions(): GitHubDiscussion[] {
484524
return [
485525
{

0 commit comments

Comments
 (0)