The contribution data system fetches activity statistics from GitHub’s GraphQL API and WhatPulse, processes them into a normalized format, and stores them as static JSON for deployment.
GitHub GraphQL API Integration
The system uses GitHub’s GraphQL API v4 to fetch contribution calendar data.
GraphQL Query
From scripts/stats.ts:
const body = {
query: `query ($username: String!, $from: DateTime, $to: DateTime) {
user(login: $username) {
contributionsCollection(from: $from, to: $to) {
contributionCalendar {
totalContributions
weeks {
contributionDays {
contributionCount
date
contributionLevel
}
}
}
}
}
}`,
variables: {
username: "aidrecabrera",
from: date.from?.toISOString(),
to: date.to?.toISOString(),
},
};
Making the Request
const response = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "aidrecabrera/readme",
Authorization: `bearer ${process.env.GH_SECRET}`,
},
body: JSON.stringify(body),
});
You must provide a GitHub Personal Access Token via the GH_SECRET environment variable. The token needs read:user scope.
API Response Structure
type Response = {
data: {
user: {
contributionsCollection: {
contributionCalendar: {
totalContributions: number;
weeks: {
contributionDays: Contribution[];
}[];
};
};
};
};
};
type Contribution = {
contributionCount: number;
date: string;
contributionLevel:
| "NONE"
| "FIRST_QUARTILE"
| "SECOND_QUARTILE"
| "THIRD_QUARTILE"
| "FOURTH_QUARTILE";
};
Contribution Level Calculation
GitHub returns contribution levels as quartile strings. These are converted to integers for rendering:
const levelToInt = (level: Contribution["contributionLevel"]) => {
switch (level) {
case "NONE":
return 0; // No contributions
case "FIRST_QUARTILE":
return 1; // 1-25th percentile
case "SECOND_QUARTILE":
return 2; // 25th-50th percentile
case "THIRD_QUARTILE":
return 3; // 50th-75th percentile
case "FOURTH_QUARTILE":
return 4; // 75th-100th percentile (most active)
}
};
How GitHub Calculates Levels
GitHub determines quartiles based on your personal contribution distribution:
- Collects all your daily contribution counts for the period
- Sorts them and divides into four equal groups (quartiles)
- Assigns each day to a quartile based on where it falls in the distribution
Contribution levels are personalized - your “FOURTH_QUARTILE” represents your most active days, not a universal threshold.
Multi-Year Data Fetching
GitHub’s API limits contribution queries to one year at a time. The system handles this with automatic pagination:
async function getAllContributions(start: Date, end = new Date()) {
const years: Year[] = [];
let cursor = start;
let contributions = 0;
while (cursor < end) {
let next = new Date(cursor.getFullYear() + 1, 0, 1);
// Prevent fetching data beyond the current date
if (next > end) next = end;
console.info("...Fetching from", cursor.toISOString(), next.toISOString());
const data = await request({ to: next, from: cursor });
contributions += data.contributions;
years.push({
from: cursor.toISOString(),
to: next.toISOString(),
days: data.weeks
.flatMap((week) =>
week.contributionDays.map((day) => levelToInt(day.contributionLevel))
)
.reverse(), // Show most recent first
});
cursor = next;
}
return [years.reverse(), contributions];
}
Default Date Range
export const START_DATE = new Date();
START_DATE.setFullYear(START_DATE.getFullYear() - 1);
const [years, contributions] = await getAllContributions(START_DATE);
By default, the system fetches one year of contribution history. To fetch more:
// Fetch 3 years of data
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 3);
const [years, contributions] = await getAllContributions(startDate);
Processed data is written to src/stats.json:
await writeFile("src/stats.json", JSON.stringify({ years, contributions }));
Output Structure
{
"years": [
{
"from": "2024-03-04T00:00:00.000Z",
"to": "2025-01-01T00:00:00.000Z",
"days": [0, 1, 2, 3, 4, 2, 1, 0, ...]
},
{
"from": "2023-01-01T00:00:00.000Z",
"to": "2024-01-01T00:00:00.000Z",
"days": [1, 2, 3, 4, 3, 2, 1, 0, ...]
}
],
"contributions": 1247
}
The days array contains integers 0-4 representing contribution levels, with the most recent day first (reversed order).
WhatPulse Integration
WhatPulse provides computer usage statistics (keyboard, mouse, network activity).
Fetching WhatPulse Data
const fetchPulse = async (username: string): Promise<PulseStats> => {
const url = `https://api.whatpulse.org/user.php?user=${username}&formatted=yes&format=json`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error fetching WhatPulse stats: ${response.statusText}`);
}
const data = await response.json() as PulseStats;
return data;
};
const pulseData = await fetchPulse("svenesismar");
await writeFile("src/pulse.json", JSON.stringify(pulseData, null, 2));
WhatPulse Data Structure
interface PulseStats {
GeneratedTime: string;
UserID: string;
AccountName: string;
Keys: string; // Total key presses
Clicks: string; // Total mouse clicks
Scrolls: string; // Total scroll events
Download: string; // Total download in formatted string
Upload: string; // Total upload in formatted string
DownloadMB: number; // Download in megabytes
UploadMB: number; // Upload in megabytes
UptimeSeconds: string;
UptimeShort: string; // e.g., "52w 3d"
UptimeLong: string; // e.g., "52 weeks, 3 days, 14 hours"
Computers: {
[key: string]: Computer; // Per-computer breakdowns
};
}
Rendering WhatPulse Stats
The pulse() function in render.ts displays these statistics:
export const pulse = (data: PulseStats, props: Pick<Props, "height" | "theme">) => {
const html = `
<div class="wrapper grid label">
<p class="fade-in uptime">Uptime: ${data.UptimeLong}</p>
<p class="fade-in up">Download: ${data.Download}</p>
<p class="fade-in down">Upload: ${data.Upload}</p>
<p class="fade-in keys">Keys: ${data.Keys}</p>
<p class="fade-in clicks">Clicks: ${data.Clicks}</p>
</div>
`;
return svg(styles, html, { height: `${props.height}`, "data-theme": props.theme });
};
Caching Strategy
The system uses a static generation approach rather than runtime caching:
Build-Time Generation
// package.json
{
"scripts": {
"stats": "tsm --env-file .env scripts/stats.ts",
"deploy": "pnpm stats && wrangler deploy"
}
}
pnpm stats fetches latest data and writes to JSON files
wrangler deploy bundles Worker code + JSON data
- Cloudflare serves the bundled assets from edge locations
Why Not Runtime Caching?
Advantages of static generation:
- ✅ Zero API calls at runtime (faster response)
- ✅ No rate limiting concerns
- ✅ Predictable costs
- ✅ Works even if GitHub API is down
Disadvantages:
- ❌ Data only updates on deployment
- ❌ Requires manual or scheduled deploys
For real-time updates, set up a GitHub Action to run pnpm deploy on a schedule (e.g., daily at midnight).
Error Handling
API Request Failures
const response = await fetch("https://api.github.com/graphql", { /* ... */ })
.then((res) => res.json() as Promise<Response>);
if (!response.data || !response.data.user) {
throw new Error(
`Failed to fetch contributions: ${JSON.stringify(response)}`
);
}
if (!data || typeof data !== "object") {
throw new Error(`Invalid data format: ${JSON.stringify(data)}`);
}
If the stats script fails, the deployment will also fail. Always check logs when deployments fail to identify API issues.
Running the Stats Script
Execute manually to update data:
This will:
- Load environment variables from
.env
- Fetch GitHub contributions for the past year
- Fetch WhatPulse statistics
- Write
src/stats.json and src/pulse.json
- Display ”…Done” when complete
Required Environment Variables
# .env
GH_SECRET=ghp_your_personal_access_token_here
The tsm command (TypeScript Module loader) allows running TypeScript directly without a build step, using .env file support.