Skip to content

perf(api): sum star totals via REST; shrink profile GraphQL documents - #317

Merged
vn7n24fzkq merged 3 commits into
mainfrom
perf/rest-star-totals
Jul 29, 2026
Merged

perf(api): sum star totals via REST; shrink profile GraphQL documents#317
vn7n24fzkq merged 3 commits into
mainfrom
perf/rest-star-totals

Conversation

@vn7n24fzkq

@vn7n24fzkq vn7n24fzkq commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What

Star totals move off GraphQL onto the (otherwise idle) REST quota pool, and the profile GraphQL documents get lighter — the next burn-rate lever after #309/#310/#316.

  • fetchTotalStars() sums stargazer_count over GET /users/:login/repos pages, running concurrently with whichever GraphQL path (combined or split) fetches the rest of the profile — separate quota pools, no contention. Fork filtering and public-only semantics match the old isFork: false / privacy: PUBLIC exactly; [BUG] Inaccurate star count #164's every-page accuracy and the Vercel page/time budgets are unchanged.
  • The combined UserDetails and split UserDetailsCore documents drop their 100-node repositories page to repositories(first: 1) { totalCount } — the exact repo count survives, and the lighter document scores lower with GitHub's cost estimator, so heavy accounts should hit the resource-limit split less often.
  • The GraphQL UserStars pagination document is deleted; the split path no longer threads star state through fetchUserDetailsSplit.
  • Cache shape (v2:pd compact payload) is unchanged — same fields, same numbers — so existing cache entries stay valid.

Testing

26 suites / 223 tests green, typecheck + lint clean. Profile-details tests now mock both transports; the star pagination test covers a full REST page (100 repos) spilling to page 2 and excludes a fork's stars; split/gateway-timeout/half-window-calendar fallbacks re-verified against the slimmer documents.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Profile star totals now aggregate across paginated repository results while excluding forked repositories.
    • Profile data retrieval is more efficient and resilient when determining star totals.
    • Cached data remains fresh for up to 24 hours by default.
    • If star-total retrieval fails, the profile won’t fall back to stale star totals.

vn7n24fzkq and others added 2 commits July 28, 2026 18:08
Peak hours run close to the GitHub hourly quota ceiling (#308 follow-up);
halving steady-state refresh traffic is the cheapest lever. Behind the
48h CDN window, data refreshed daily is indistinguishable from 12h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Star totals came from GraphQL repo pages (100 nodes per query, plus a
follow-up pagination document), spending constrained GraphQL points on
data REST hands out on the same repos listing the service already reads
elsewhere. Sum stargazer_count over REST repo pages instead — a separate,
otherwise-idle hourly pool — running concurrently with whichever GraphQL
path (combined or split) fetches the rest of the profile.

The combined UserDetails and split UserDetailsCore documents drop their
100-node repositories page down to repositories(first: 1) { totalCount }:
the exact repo count survives, and the lighter document scores lower with
GitHub's cost estimator, so heavy accounts should hit the resource-limit
split path less often. Fork filtering and public-only semantics match the
old isFork: false / privacy: PUBLIC exactly; the #164 every-page star
accuracy is preserved (REST pagination, same Vercel page/time budgets).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 22743b6b-1ecf-4552-9dc6-1ca8fe3837ce

📥 Commits

Reviewing files that changed from the base of the PR and between 0da7b59 and d59fcec.

📒 Files selected for processing (2)
  • src/github-api/profile-details.ts
  • tests/github-api/profile-details.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/github-api/profile-details.test.ts
  • src/github-api/profile-details.ts

📝 Walkthrough

Walkthrough

Profile star totals move from GraphQL repository pagination to concurrent REST repository pagination, excluding forks and using the existing fetch-budget window. GraphQL queries now request only repository counts. The default cache freshness window increases from 12 to 24 hours.

Changes

Profile data and cache behavior

Layer / File(s) Summary
REST star pagination and profile integration
src/github-api/profile-details.ts, tests/github-api/profile-details.test.ts
GraphQL repository selections are reduced to totalCount; fetchTotalStars sums paginated REST stargazers_count values while excluding forks, and getProfileDetails starts star fetching alongside profile retrieval. Tests cover fallback paths, pagination, fork exclusion, fetch failures, and cache payloads.
24-hour cache freshness window
src/utils/data-cache.ts, tests/utils/data-cache.test.ts
The default freshness duration changes from 12 hours to 24 hours, with stale-cache tests updated to use 25-hour-old timestamps.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant getProfileDetails
  participant fetchTotalStars
  participant restRequest
  participant GitHubREST
  getProfileDetails->>fetchTotalStars: start star aggregation
  fetchTotalStars->>restRequest: request repository page
  restRequest->>GitHubREST: GET /users/:login/repos
  GitHubREST-->>restRequest: repository data
  restRequest-->>fetchTotalStars: paginated repositories
  fetchTotalStars-->>getProfileDetails: fork-excluded totalStars
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving star-total calculation to REST and reducing GraphQL profile query size.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/rest-star-totals

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/github-api/profile-details.ts (3)

364-383: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Pagination relies on repos.length === 100 instead of the actual Link header.

Using a full page as the "has more" signal means that when a user's public/non-fork repo count is an exact multiple of 100, one extra REST call is made that always returns an empty page before pagination stops. It's harmless functionally (no double counting), just a minor extra round trip. Parsing the Link: rel="next" header from the restRequest response (available via axios res.headers.link) would make the stop condition exact rather than inferred.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github-api/profile-details.ts` around lines 364 - 383, The
fetchTotalStars pagination currently infers continuation from repos.length;
update it to parse res.headers.link for a rel="next" URL and pass that presence
to shouldFetchNextPage. Preserve the existing page, budget, and star-count logic
while stopping immediately when the response has no next link.

240-266: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale comment: "three smaller queries" undercounts the actual fan-out.

fetchUserDetailsSplit issues four concurrent fetches (coreFetcher, fetchCalendarWeeks, contributionYearsFetcher, countsFetcher); the comment at Lines 424-425 still says "three smaller queries," likely left over from before stars were split out of this path. Worth a quick wording fix so the count stays accurate for future readers.

Also applies to: 423-428

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github-api/profile-details.ts` around lines 240 - 266, The comment
describing fetchUserDetailsSplit’s concurrent fan-out is stale and understates
the number of requests. Update the nearby comment to accurately describe all
four fetches—coreFetcher, fetchCalendarWeeks, contributionYearsFetcher, and
countsFetcher—without changing the implementation.

399-407: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Star-fetch failures fail the entire cached fetch, not just the star total.

Since starsPromise isn't independently caught before await starsPromise at Line 428, any REST-side error (network blip, GitHub REST outage, 403/404) will reject the whole withDataCache callback even if the GraphQL profile fetch succeeded — the comment at Lines 399-403 confirms this is deliberate. It's cushioned by the stale-cache fallback in data-cache.ts, but a cold cache (new profile, first fetch) would surface a full error card purely due to a transient REST hiccup that has nothing to do with the GraphQL data that did succeed. Consider defaulting totalStars to a fallback (e.g. 0 or the previous cached value) on a starsPromise rejection instead of letting it fail the combined result, to keep GraphQL-only degradation graceful.

Also applies to: 428-428

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github-api/profile-details.ts` around lines 399 - 407, Update the
starsPromise handling in the profile fetch flow so a fetchTotalStars rejection
is converted to a safe totalStars fallback instead of rejecting the
withDataCache callback. Preserve the successful GraphQL profile result and use
the existing cached value when available, otherwise the established zero/default
value, while keeping genuine GraphQL failures propagating normally.
tests/github-api/profile-details.test.ts (1)

69-76: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add coverage for a REST star-fetch failure.

Given getProfileDetails now lets a fetchTotalStars rejection fail the whole cached fetch (see profile-details.ts Lines 399-428), it'd be valuable to add a test that mocks a /repos failure (e.g. mock.onGet(...).networkError() or a 500) alongside a successful GraphQL response, and assert the documented behavior (rejection propagates / stale-cache fallback engages). This locks in the current intentional design and would catch regressions if that coupling is changed later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/github-api/profile-details.test.ts` around lines 69 - 76, Add a test in
the profile-details suite that provides a successful GraphQL response but makes
the mocked /users/{username}/repos request fail, then assert the documented
getProfileDetails outcome—propagated rejection or stale-cache fallback—matching
the existing cache setup and assertions. Reuse mockRestStars or the surrounding
request-mocking symbols and cover the fetchTotalStars failure path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/github-api/profile-details.ts`:
- Around line 364-383: The fetchTotalStars logic reads the wrong GitHub
repository star field; update its comment and property access to use
stargazers_count. Also update the repository mock fixture in
tests/github-api/profile-details.test.ts (lines 69-76) to provide
stargazers_count, with no other test changes.

---

Nitpick comments:
In `@src/github-api/profile-details.ts`:
- Around line 364-383: The fetchTotalStars pagination currently infers
continuation from repos.length; update it to parse res.headers.link for a
rel="next" URL and pass that presence to shouldFetchNextPage. Preserve the
existing page, budget, and star-count logic while stopping immediately when the
response has no next link.
- Around line 240-266: The comment describing fetchUserDetailsSplit’s concurrent
fan-out is stale and understates the number of requests. Update the nearby
comment to accurately describe all four fetches—coreFetcher, fetchCalendarWeeks,
contributionYearsFetcher, and countsFetcher—without changing the implementation.
- Around line 399-407: Update the starsPromise handling in the profile fetch
flow so a fetchTotalStars rejection is converted to a safe totalStars fallback
instead of rejecting the withDataCache callback. Preserve the successful GraphQL
profile result and use the existing cached value when available, otherwise the
established zero/default value, while keeping genuine GraphQL failures
propagating normally.

In `@tests/github-api/profile-details.test.ts`:
- Around line 69-76: Add a test in the profile-details suite that provides a
successful GraphQL response but makes the mocked /users/{username}/repos request
fail, then assert the documented getProfileDetails outcome—propagated rejection
or stale-cache fallback—matching the existing cache setup and assertions. Reuse
mockRestStars or the surrounding request-mocking symbols and cover the
fetchTotalStars failure path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee4a210e-5e4d-4b7c-886d-baff75732183

📥 Commits

Reviewing files that changed from the base of the PR and between ecd8361 and 0da7b59.

📒 Files selected for processing (4)
  • src/github-api/profile-details.ts
  • src/utils/data-cache.ts
  • tests/github-api/profile-details.test.ts
  • tests/utils/data-cache.test.ts

Comment thread src/github-api/profile-details.ts
… behavior

Review catch: GitHub REST returns stargazers_count (plural); the code and
the test fixture both said stargazer_count, so the suite was green while
production would have summed ?? 0 into a zero-star card. Also fixes the
stale "three smaller queries" comment and adds a test pinning the
deliberate choice that a REST star failure rejects the fetch (stale
rescue owns the fallback) rather than caching a wrong zero for a day.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vn7n24fzkq

Copy link
Copy Markdown
Owner Author

Fixed in d59fcec — and the stargazers_count catch is a genuinely good one: the fixture repeated the implementation's typo, so the suite stayed green while production would have summed zeros. Also fixed the stale "three smaller queries" comment and added a test pinning the star-failure behavior.

Two nitpicks intentionally skipped:

  • Link-header pagination: kept length === 100 for consistency with the identical loop in repos-per-language; the cost is one empty extra call only when a repo count is an exact multiple of 100.
  • Fallback-to-0 on star failure: deliberate — a zero-star total would be cached as wrong data for a day, while failing lets the stale-cache rescue serve the previous correct copy (or one error card on a truly cold key). The new test locks this in.

@vn7n24fzkq
vn7n24fzkq merged commit 2e6735f into main Jul 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant