From 30c2478f8a4b5e184b08ff896858968d0d54b1c6 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Sun, 9 Aug 2026 02:42:15 +0530 Subject: [PATCH] fix(api): paginate issue comments past the first 100 fetchIssueComments only requested a single per_page=100 page, silently dropping comments on issues with more than 100. Comment counts drive history activity rates and analysis staleness checks, so truncated comments produced wrong metrics. Now pages through the comments API until a short page is returned, preserving order and the existing single-page behavior. --- README.md | 1 - src/lib/api/github.test.ts | 39 +++++++++++++++++++ src/lib/api/github.ts | 77 +++++++++++++++++++++----------------- 3 files changed, 82 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 07c81d5..adfb2c6 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ These instructions have been tested on a clean machine to ensure a reliable setu > **Note:** If the repository already includes `.env.example`, you only need to copy it to `.env`. Creating a new `.env.example` is only necessary if the file is missing. - 5. **Start the development server:** ```bash bun dev diff --git a/src/lib/api/github.test.ts b/src/lib/api/github.test.ts index 1dd3c88..f901107 100644 --- a/src/lib/api/github.test.ts +++ b/src/lib/api/github.test.ts @@ -229,6 +229,45 @@ describe('GitHub API Client', () => { expect(comments[0].body).toBe('First comment'); }); + it('fetchIssueComments paginates past the first 100 comments', async () => { + const page1 = Array.from({ length: 100 }, (_, i) => ({ + id: i + 1, + user: { login: 'u' }, + body: `comment ${i + 1}`, + created_at: '2026-05-18T10:00:00Z', + updated_at: '2026-05-18T10:00:00Z', + })); + const page2 = Array.from({ length: 100 }, (_, i) => ({ + id: i + 101, + user: { login: 'u' }, + body: `comment ${i + 101}`, + created_at: '2026-05-18T10:00:00Z', + updated_at: '2026-05-18T10:00:00Z', + })); + const page3 = Array.from({ length: 50 }, (_, i) => ({ + id: i + 201, + user: { login: 'u' }, + body: `comment ${i + 201}`, + created_at: '2026-05-18T10:00:00Z', + updated_at: '2026-05-18T10:00:00Z', + })); + + mockFetch + .mockResolvedValueOnce(createMockResponse(200, page1)) + .mockResolvedValueOnce(createMockResponse(200, page2)) + .mockResolvedValueOnce(createMockResponse(200, page3)); + + const comments = await fetchIssueComments('owner', 'repo', 42); + + expect(mockFetch).toHaveBeenCalledTimes(3); + expect(mockFetch.mock.calls[0][0]).toContain('page=1'); + expect(mockFetch.mock.calls[1][0]).toContain('page=2'); + expect(mockFetch.mock.calls[2][0]).toContain('page=3'); + expect(comments).toHaveLength(250); + expect(comments[0].id).toBe(1); + expect(comments[249].id).toBe(250); + }); + it('fetchIssueTimeline requests correct URL and maps events', async () => { mockFetch.mockResolvedValue( createMockResponse(200, [ diff --git a/src/lib/api/github.ts b/src/lib/api/github.ts index 3816c83..0b4029e 100644 --- a/src/lib/api/github.ts +++ b/src/lib/api/github.ts @@ -137,21 +137,19 @@ export async function searchIssues( const issues = data.items .filter((item) => !item.pull_request) - .map( - (item): Issue => ({ - number: item.number, - title: item.title, - body: item.body, - user: item.user, - labels: item.labels, - assignees: item.assignees, - comments_count: item.comments, - created_at: item.created_at, - updated_at: item.updated_at, - html_url: item.html_url, - state: item.state, - }), - ); + .map((item): Issue => ({ + number: item.number, + title: item.title, + body: item.body, + user: item.user, + labels: item.labels, + assignees: item.assignees, + comments_count: item.comments, + created_at: item.created_at, + updated_at: item.updated_at, + html_url: item.html_url, + state: item.state, + })); allIssues.push(...issues); @@ -182,23 +180,36 @@ export async function fetchIssueComments( repo: string, issueNumber: number, ): Promise { - const url = - `${CONFIG.GITHUB_API_BASE}/repos/${owner}/${repo}` + - `/issues/${issueNumber}/comments?per_page=100`; + const comments: Comment[] = []; + let page = 1; - const response = await fetchWithRetry(url); + while (true) { + const url = + `${CONFIG.GITHUB_API_BASE}/repos/${owner}/${repo}` + + `/issues/${issueNumber}/comments?per_page=100&page=${page}`; - const data = await response.json(); + const response = await fetchWithRetry(url); + + const data = await response.json(); - return data.map( - (c: Record): Comment => ({ + const mapped = data.map((c: Record): Comment => ({ id: c.id as number, user: c.user as Comment['user'], body: c.body as string, created_at: c.created_at as string, updated_at: c.updated_at as string, - }), - ); + })); + + comments.push(...mapped); + + if (mapped.length < 100) { + break; + } + + page++; + } + + return comments; } export async function fetchIssueTimeline( @@ -217,16 +228,14 @@ export async function fetchIssueTimeline( const data: Record[] = await response.json(); - return data.map( - (e): TimelineEvent => ({ - event: e.event as string, - created_at: e.created_at as string, - actor: e.actor as TimelineEvent['actor'], - source: e.source as TimelineEvent['source'], - commit_id: e.commit_id as string | undefined, - label: e.label as TimelineEvent['label'], - }), - ); + return data.map((e): TimelineEvent => ({ + event: e.event as string, + created_at: e.created_at as string, + actor: e.actor as TimelineEvent['actor'], + source: e.source as TimelineEvent['source'], + commit_id: e.commit_id as string | undefined, + label: e.label as TimelineEvent['label'], + })); } catch { return []; }