feat: add GitHub contribution heatmap using GraphQL API#713
Conversation
✅ Deploy Preview for github-spy ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR implements a GitHub-style contribution calendar (heatmap) visualization on the Tracker dashboard. It extends the ChangesContribution Heatmap Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/hooks/useGitHubData.ts (1)
16-20: ⚡ Quick winExport and reuse
ContributionDayacross hook/component boundaries.
ContributionDayis currently duplicated (here and insrc/components/ContributionHeatmap.tsx), which risks drift in a shared contract.🤖 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/hooks/useGitHubData.ts` around lines 16 - 20, Duplicate ContributionDay types cause drift; export the ContributionDay interface from where it's defined (ContributionDay in useGitHubData) or move it to a shared type module and then import that single definition into ContributionHeatmap.tsx (and any other consumers) instead of redefining it. Update the export (export interface ContributionDay { ... }) and replace the local duplicate in ContributionHeatmap.tsx with an import of ContributionDay so both use the same type symbol.
🤖 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/hooks/useGitHubData.ts`:
- Around line 198-202: The current Promise.all([Promise.allSettled(requests),
contributionRequest]) will reject if contributionRequest fails and skip handling
the already-settled requests; change the flow in useGitHubData.ts to await
Promise.allSettled(requests) first (assign to results), then await
contributionRequest inside a try/catch (assign to contributionResult or a safe
default on error) so that failures fetching contributions do not prevent
executing setIssues and setPrs; reference the variables requests,
contributionRequest, results, contributionResult, setIssues and setPrs when
making the change.
- Line 100: Create a proper TypeScript interface for the GraphQL response (e.g.,
GitHubContributionsResponse with typed user, contributionsCollection,
contributionCalendar, weeks, days, contributionLevel as string union) and
replace the untyped call by invoking
octokit.graphql<GitHubContributionsResponse>(...) instead of casting to any;
adjust the response variable type accordingly (replace response: any). Update
the flatMap/map callbacks to use explicit types for week and day (e.g., week:
GitHubWeek, day: GitHubDay) and convert contributionLevel
(NONE|FIRST_QUARTILE|SECOND_QUARTILE|THIRD_QUARTILE|FOURTH_QUARTILE) to
ContributionDay['level'] numbers (0–4) in the mapping logic. Ensure you handle
user possibly being null in the typed response before accessing
contributionsCollection. Finally, replace Promise<any>[] with a typed
Promise<YourTypedResult>[] (use the actual return type used by the caller) to
remove any remaining explicit any usage.
In `@src/pages/Tracker/Tracker.tsx`:
- Around line 402-406: The empty-state message is shown before the fetch
completes; update the render logic around ContributionHeatmap and
contributionData to only show "No contribution data available" after the fetch
has finished (e.g., use an existing or new fetch status flag like isLoading or
hasFetched). Concretely, add or use a boolean such as
isContribLoading/isContribFetched in the Tracker component and change the
conditional so ContributionHeatmap is shown when contributionData.length > 0, a
loading spinner/placeholder is shown when isContribLoading is true, and the
empty-state message is shown only when !isContribLoading && isContribFetched &&
contributionData.length === 0; update the fetch handler that fills
contributionData to set these flags (e.g., in the async fetch function that
populates contributionData).
---
Nitpick comments:
In `@src/hooks/useGitHubData.ts`:
- Around line 16-20: Duplicate ContributionDay types cause drift; export the
ContributionDay interface from where it's defined (ContributionDay in
useGitHubData) or move it to a shared type module and then import that single
definition into ContributionHeatmap.tsx (and any other consumers) instead of
redefining it. Update the export (export interface ContributionDay { ... }) and
replace the local duplicate in ContributionHeatmap.tsx with an import of
ContributionDay so both use the same type symbol.
🪄 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
Run ID: 29752a50-eaa9-48d8-ab43-fed405834d9b
📒 Files selected for processing (4)
package.jsonsrc/components/ContributionHeatmap.tsxsrc/hooks/useGitHubData.tssrc/pages/Tracker/Tracker.tsx
| octokit: Octokit, | ||
| username: string | ||
| ): Promise<ContributionDay[]> => { | ||
| const response: any = await (octokit as any).graphql( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/useGitHubData.ts"
echo "== File: $FILE =="
ls -l "$FILE"
echo "== Lines around 80-160 =="
nl -ba "$FILE" | sed -n '80,170p'
echo "== Find explicit any in file =="
rg -n ":\s*any|as\s+any|no-explicit-any|eslint-disable.*any" "$FILE" || true
echo "== Find graphql< or graphql< typed usage in file =="
rg -n "graphql<|\.graphql\(" "$FILE"Repository: GitMetricsLab/github_tracker
Length of output: 274
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/useGitHubData.ts"
echo "== File: $FILE =="
ls -l "$FILE"
echo "== Lines 80-170 (with numbers) =="
python3 - <<'PY'
import itertools
path="src/hooks/useGitHubData.ts"
start, end = 80, 170
with open(path,'r',encoding='utf-8') as f:
for i, line in enumerate(f, start=1):
if start <= i <= end:
print(f"{i:4d}\t{line.rstrip()}")
elif i > end:
break
PY
echo "== Explicit any patterns in file =="
rg -n ":\s*any|as\s+any|no-explicit-any|eslint-disable.*any" "$FILE" || true
echo "== graphql usage in file =="
rg -n "graphql<|\.graphql\(" "$FILE" || trueRepository: GitMetricsLab/github_tracker
Length of output: 3076
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/useGitHubData.ts"
echo "== Find ContributionDay type definition =="
rg -n "type ContributionDay|interface ContributionDay|ContributionDay\[" "$FILE" || true
echo "== Show region around ContributionDay definition (if found) =="
python3 - <<'PY'
import re
path="src/hooks/useGitHubData.ts"
start_lines=[]
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if re.search(r'\btype\s+ContributionDay\b|\binterface\s+ContributionDay\b', line):
start_lines.append(i)
for i in start_lines:
start=max(1,i-10); end=i+60
print(f"\n--- {path}:{start}-{end} ---")
with open(path,'r',encoding='utf-8') as f:
for ln,txt in enumerate(f, start=1):
if start<=ln<=end:
print(f"{ln:4d}\t{txt.rstrip()}")
PYRepository: GitMetricsLab/github_tracker
Length of output: 2419
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/useGitHubData.ts"
echo "== All occurrences of 'any' in file =="
rg -n "\bany\b" "$FILE" || true
echo "== Show region around graphql call and fetchContributionData =="
python3 - <<'PY'
path="src/hooks/useGitHubData.ts"
start,end=90,155
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if start<=i<=end:
print(f"{i:4d}\t{line.rstrip()}")
PY
echo "== Show import section (first 80 lines) =="
python3 - <<'PY'
path="src/hooks/useGitHubData.ts"
end=80
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if i<=end:
print(f"{i:4d}\t{line.rstrip()}")
PYRepository: GitMetricsLab/github_tracker
Length of output: 4615
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "\.graphql<|\.graphql\(" -S . || true
rg -n "function getOctokit|getOctokit\s*=" -S . || true
rg -n "useGitHubData" -S src || trueRepository: GitMetricsLab/github_tracker
Length of output: 448
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/hooks/useGitHubAuth.ts"
echo "== File: $FILE =="
ls -l "$FILE"
python3 - <<'PY'
path="src/hooks/useGitHubAuth.ts"
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f, start=1):
if i<=220:
print(f"{i:4d}\t{line.rstrip()}")
PYRepository: GitMetricsLab/github_tracker
Length of output: 832
🌐 Web query:
GitHub GraphQL contributionLevel values NONE FIRST_QUARTILE SECOND_QUARTILE THIRD_QUARTILE FOURTH_QUARTILE
💡 Result:
In the GitHub GraphQL API, the ContributionLevel enum defines the intensity of a user's contributions on a specific day relative to other days [1][2]. The values are defined as follows: NONE: No contributions occurred [1][2]. FIRST_QUARTILE: The lowest 25% of days with contributions [1][2]. SECOND_QUARTILE: The second lowest 25% of days with contributions; contains more contributions than the first quartile [1][2]. THIRD_QUARTILE: The second highest 25% of days with contributions; contains more contributions than the second quartile, but less than the fourth quartile [1][2]. FOURTH_QUARTILE: The highest 25% of days with contributions; contains more contributions than the third quartile [1][2]. This enum is typically used in the contributionsCollection object to help determine the visual representation of activity on a contribution calendar [1][3].
Citations:
- 1: https://docs.github.com/en/graphql/reference/users
- 2: https://docs.github.com/en/enterprise-server@3.18/graphql/reference/enums
- 3: https://github.com/kawarimidoll/deno-github-contributions-api/blob/main/contributions.ts
🌐 Web query:
@octokit/core Octokit graphql method TypeScript generics graphql<T>
💡 Result:
In @octokit/core, the graphql method supports TypeScript generics to specify the expected return type of your GraphQL query [1]. You can apply these generics directly to the graphql call using the syntax octokit.graphql(query, variables) [1]. When you provide a type , the library will treat the response data as that type [1]. If you do not provide a generic, the return type defaults to any (or a general structure depending on your specific version and configuration) [1]. Example usage: interface MyData { repository: { name: string; stargazers: { totalCount: number; }; }; } const response = await octokit.graphql( query, { owner: "octokit", name: "core" }); Additionally, for general GraphQL usage with Octokit, you can import and use GraphQlQueryResponseData from @octokit/graphql to help define your response structures [2][3]. For more advanced scenarios, such as paginated GraphQL queries, you can also use generics with the pagination plugin, as seen in @octokit/plugin-paginate-graphql [4].
Citations:
- 1: Incorrect return type for
graphql()octokit/graphql.js#80 - 2: https://www.npmjs.com/package/@octokit/graphql
- 3: https://github.com/octokit/graphql.js/
- 4: https://github.com/octokit/plugin-paginate-graphql.js/
Replace explicit any with typed GraphQL response models.
Lines 100, 125, and 126 use any (const response: any = await (octokit as any).graphql(...), week: any, day: any), violating @typescript-eslint/no-explicit-any and weakening type safety in the contributions fetch path (notably user can be null).
- Add a typed GraphQL response interface and use
octokit.graphql<...>(...). - Map
contributionLevel(NONE | FIRST_QUARTILE | ...) to the existingContributionDay['level'](0 | 1 | 2 | 3 | 4) and removeanyfrom theflatMap/mapcallbacks. - Also remove
Promise<any>[]at line 169.
🧰 Tools
🪛 ESLint
[error] 100-100: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
[error] 100-100: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 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/hooks/useGitHubData.ts` at line 100, Create a proper TypeScript interface
for the GraphQL response (e.g., GitHubContributionsResponse with typed user,
contributionsCollection, contributionCalendar, weeks, days, contributionLevel as
string union) and replace the untyped call by invoking
octokit.graphql<GitHubContributionsResponse>(...) instead of casting to any;
adjust the response variable type accordingly (replace response: any). Update
the flatMap/map callbacks to use explicit types for week and day (e.g., week:
GitHubWeek, day: GitHubDay) and convert contributionLevel
(NONE|FIRST_QUARTILE|SECOND_QUARTILE|THIRD_QUARTILE|FOURTH_QUARTILE) to
ContributionDay['level'] numbers (0–4) in the mapping logic. Ensure you handle
user possibly being null in the typed response before accessing
contributionsCollection. Finally, replace Promise<any>[] with a typed
Promise<YourTypedResult>[] (use the actual return type used by the caller) to
remove any remaining explicit any usage.
| const [results, contributionResult] = | ||
| await Promise.all([ | ||
| Promise.allSettled(requests), | ||
| contributionRequest, | ||
| ]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant section around the cited lines
nl -ba src/hooks/useGitHubData.ts | sed -n '150,320p'Repository: GitMetricsLab/github_tracker
Length of output: 115
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect around the cited lines using cat -n (nl isn't available)
cat -n src/hooks/useGitHubData.ts | sed -n '160,290p'
# Find all occurrences of Promise.all / allSettled around contributionRequest
rg -n "contributionRequest|Promise\.all\(|allSettled\(" src/hooks/useGitHubData.tsRepository: GitMetricsLab/github_tracker
Length of output: 4601
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the helper implementation and where contribution state is defined
rg -n "function fetchContributionData|const fetchContributionData|fetchContributionData\\(" src/hooks/useGitHubData.ts
rg -n "setContributionData\\(|contributionData" src/hooks/useGitHubData.ts
# Print the top of the file to capture types/state initialization
head -n 140 src/hooks/useGitHubData.tsRepository: GitMetricsLab/github_tracker
Length of output: 3637
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect further down for the catch block and any other contribution handling
cat -n src/hooks/useGitHubData.ts | sed -n '290,420p'
# Also search for setContributionData usages to ensure no other fallback exists
rg -n "setContributionData\\(" src/hooks/useGitHubData.tsRepository: GitMetricsLab/github_tracker
Length of output: 725
Prevent contribution (heatmap) fetch failures from aborting issues/PR rendering.
Promise.all([Promise.allSettled(requests), contributionRequest]) rejects as soon as contributionRequest rejects, skipping the already-settled results handling and the setIssues/setPrs updates.
💡 Suggested fix
- const [results, contributionResult] =
- await Promise.all([
- Promise.allSettled(requests),
- contributionRequest,
- ]);
+ const [results, contributionResult] = await Promise.all([
+ Promise.allSettled(requests),
+ contributionRequest
+ .then((value) => ({ status: 'fulfilled' as const, value }))
+ .catch((reason) => ({ status: 'rejected' as const, reason })),
+ ]);
...
- setRateLimited(false);
- setContributionData(contributionResult);
+ setRateLimited(false);
+ if (contributionResult.status === 'fulfilled') {
+ setContributionData(contributionResult.value);
+ } else {
+ setContributionData([]);
+ setError((prev) => prev || 'Contribution activity is currently unavailable.');
+ }🤖 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/hooks/useGitHubData.ts` around lines 198 - 202, The current
Promise.all([Promise.allSettled(requests), contributionRequest]) will reject if
contributionRequest fails and skip handling the already-settled requests; change
the flow in useGitHubData.ts to await Promise.allSettled(requests) first (assign
to results), then await contributionRequest inside a try/catch (assign to
contributionResult or a safe default on error) so that failures fetching
contributions do not prevent executing setIssues and setPrs; reference the
variables requests, contributionRequest, results, contributionResult, setIssues
and setPrs when making the change.
| {contributionData.length > 0 ? ( | ||
| <ContributionHeatmap data={contributionData} /> | ||
| ) : ( | ||
| <p>No contribution data available</p> | ||
| )} |
There was a problem hiding this comment.
Gate the empty-state message to post-fetch state.
This block currently shows “No contribution data available” even before a fetch completes, which is misleading.
💡 Suggested fix
- {contributionData.length > 0 ? (
- <ContributionHeatmap data={contributionData} />
- ) : (
- <p>No contribution data available</p>
- )}
+ {username && !loading && !dataError && (
+ contributionData.length > 0 ? (
+ <ContributionHeatmap data={contributionData} />
+ ) : (
+ <p>No contribution data available</p>
+ )
+ )}🤖 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/pages/Tracker/Tracker.tsx` around lines 402 - 406, The empty-state
message is shown before the fetch completes; update the render logic around
ContributionHeatmap and contributionData to only show "No contribution data
available" after the fetch has finished (e.g., use an existing or new fetch
status flag like isLoading or hasFetched). Concretely, add or use a boolean such
as isContribLoading/isContribFetched in the Tracker component and change the
conditional so ContributionHeatmap is shown when contributionData.length > 0, a
loading spinner/placeholder is shown when isContribLoading is true, and the
empty-state message is shown only when !isContribLoading && isContribFetched &&
contributionData.length === 0; update the fetch handler that fills
contributionData to set these flags (e.g., in the async fetch function that
populates contributionData).
Related Issue
Description
Implemented a GitHub-style contribution heatmap for tracked users using real contribution data fetched from the GitHub GraphQL API.
Changes Made
ContributionHeatmapcomponentuseGitHubDataHow Has This Been Tested?
npm run devScreenshots
After
Type of Change
Summary by CodeRabbit