A simple personal GitHub activity dashboard that shows deeper information than the default contribution graph. Language habits, streaks, personal records & achievements, and Spotify wrapped style week/month/year recaps, all hosted on Cloudflare.
Live: gitnalysis
- Overview stats - commits, PRs, issues, reviews, repos contributed to, current/longest streak, filterable by period (7d / 30d / 3mo / 6mo / 1yr)
- Activity timeline - interactive daily contribution graph showing your activity on GitHub
- Language breakdown - top languages and per repo splits, clearly labeled as an estimate (see Known limitations)
- Personal records - best day, best week, most active month, most active repo
- Recaps - week / month / year "wrapped" views with a previous period comparison
- Achievements - badges for streaks, language diversity, open-source contribution, and more
- Optional sign-in - viewing anyones stats requires no login and only ever shows public activity; signing in with your own GitHub account additionally counts your own private contributions toward your own stats (see Authentication below)
- Node.js 18+
- A Cloudflare account (free tier is enough)
- A GitHub Personal Access Token scoped to public repositories, readonly (used for public lookups)
- A GitHub OAuth App (used for the optional sign-in feature - see below)
git clone https://github.com/Rhylanscript/gitnalysis.git
cd gitnalysisSign in is optional to use, but the worker expects OAuth credentials to exist even if you never click "Sign in." Register one at GitHub -> Settings -> Developer settings -> OAuth Apps -> New OAuth App:
- Application name: anything, e.g.
Gitnalysis (dev) - Homepage URL:
http://localhost:5173 - Authorization callback URL:
http://localhost:8787/auth/callback - Leave "Enable Device Flow" unchecked
You'll get a Client ID (safe to show) and can generate a Client Secret (never commit this).
If youre deploying your own production copy, register a second OAuth App with your production URLs instead, and keep its credentials separate from your local dev ones.
cd worker
npm install
npx wrangler loginCreate worker/.dev.vars (gitignored) with:
GITHUB_TOKEN=your_public_readonly_pat
GITHUB_OAUTH_CLIENT_ID=your_dev_oauth_app_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_dev_oauth_app_client_secret
GITHUB_OAUTH_CALLBACK_URL=http://localhost:8787/auth/callback
FRONTEND_URL=http://localhost:5173Create the two KV namespaces (or reuse the ones referenced in wrangler.toml if you're forking this repo and want your own):
npx wrangler kv namespace create STATS_CACHE
npx wrangler kv namespace create SESSIONSUpdate the ids in wrangler.toml to match, then run the Worker locally:
npx wrangler devRun the worker tests with:
npm testWindows/PowerShell users: if you change any cached routes response shape during development, clear the local kv before restarting
wrangler dev, or you might get stale data:Remove-Item -Recurse -Force .wrangler\state\v3\kv
cd frontend
npm install
cp .env.example .envSet VITE_API_URL in .env to your local Worker URL (http://localhost:8787 by default), then:
npm run devGitnalysis has two tiers of access, and which one you get depends only on whether you're viewing your own profile while signed in:
- Anonymous / viewing someone else's profile - always public data only, using a shared readonly PAT. No login is required for this.
- Signed in and viewing your own profile - your own private contributions are additionally counted toward your stats, using a GitHub OAuth token scoped to
read:user. This only unlocks your own aggregate contribution counts - it does not grant Gitnalysis read access to your private repository contents (that would need a broaderreposcope, which this app deliberately doesn't request).
Signing in also requests read:org, which unlocks two org features on your own profile:
- Org repos in your language stats - org repos you've actually committed to in the period are included alongside your own (repos you never touch are left out so they don't skew the breakdown).
- Org leaderboards - for any organisation you're a member of, a ranked list of members by commits, PRs, reviews and issues, plus org totals and most active repos.
If you signed in before read:org was added, sign out and back in to grant it. Orgs that restrict third-party OAuth apps need an owner to approve the app before their private data shows up. Signing in again won't fix this, so the leaderboard shows a message explaining that an org owner needs to approve Gitnalysis (members can request approval from their GitHub settings).
Signing in never affects what anyone else sees when they look up your username, and your own token is never used to view anyone else's private data.
Sessions are stored serverside (Cloudflare KV) behind an HttpOnly cookie, your GitHub token is never exposed to frontend JavaScript.
| Variable | Where | Purpose |
|---|---|---|
GITHUB_TOKEN |
Worker secret (wrangler secret put) |
Authenticates public GitHub REST/GraphQL requests |
GITHUB_OAUTH_CLIENT_ID |
Worker var (wrangler.toml / .dev.vars) |
GitHub OAuth App Client ID |
GITHUB_OAUTH_CLIENT_SECRET |
Worker secret (wrangler secret put / .dev.vars locally) |
GitHub OAuth App Client Secret |
GITHUB_OAUTH_CALLBACK_URL |
Worker var (wrangler.toml / .dev.vars) |
Must exactly match the OAuth App's registered callback URL |
FRONTEND_URL |
Worker var (wrangler.toml / .dev.vars) |
Where /auth/callback and /auth/logout redirect back to |
VITE_API_URL |
Frontend .env (local) / Cloudflare Pages env var (production) |
Base URL the frontend calls for the API |
All routes are served by the Worker and proxy/aggregate GitHub data. Every response includes an X-Cache: HIT|MISS header.
| Route | Description |
|---|---|
GET /api/user?username= |
GitHub profile |
GET /api/stats?username=&period=&includePrivate= |
Contributions, streaks, and records for a period |
GET /api/languages?username=&period=&includePrivate= |
Language breakdown (optionally period filtered) |
GET /api/achievements?username=&includePrivate= |
Achievement badges, computed off a fixed 1-year lookback |
GET /api/recap?username=&type=week|month|year&year=&includePrivate= |
Full recap: stats, records, languages, and previous period comparison |
GET /api/orgs |
Organisations the signed-in user belongs to (sign-in required) |
GET /api/org/leaderboard?org=&period= |
Member leaderboard, totals, and most active repos for one of your orgs (sign-in required) |
GET /auth/login |
Redirects to GitHub's OAuth consent screen |
GET /auth/callback |
GitHub redirects here after consent; sets the session cookie |
GET /auth/logout |
Clears the current session |
GET /auth/me |
Returns { signedIn, username? } for the current session |
includePrivate defaults to true and is only honored when the requested username matches the signed-in session - it's silently ignored otherwise, so it's safe to always pass it from the frontend.
Both halves deploy automatically on push to main, through two separate pipelines:
- Frontend - Cloudflare Pages Git integration builds
frontend/(root directoryfrontend, build commandnpm run build, output directorydist) whenever anything changes. - Worker - a GitHub Actions workflow (
.github/workflows/deploy-worker.yml) runswrangler deploywhenever anything underworker/changes. This requires aCLOUDFLARE_API_TOKENrepo secret (Cloudflare dashboard -> My Profile -> API Tokens -> "Edit Cloudflare Workers" template).
- Private data is opt in and limited in scope. Signing in only unlocks your own aggregate private contribution counts (
read:user reposcope) - not private repo names or content. Every page that shows contribution counts also shows how many private contributions aren't reflected when you're not signed in. - Language stats are an estimate, computed from current repo language bytesplits (GitHub's
/languagesendpoint), not per commit diffing, capped at the 30 most recently active repos to stay within Cloudflare 50 subrequest per invocation limit - Org leaderboards only count members, not outside collaborators, and only activity in repos the viewer can see. Very large orgs are capped at the first 200 members to stay within the subrequest limit.
- "All time" data loops year by year, since GitHub's GraphQL contribution data is limited to ~1 year per query.
- Commit timestamps reflect the committers local machine clock (or UTC if unset), any time of day feature would need to be labeled as approximate, which is part of why coding habits/time of day stats arent implemented yet (see below)
- Local dev auth relies on the browser's
localhostsecure-context exemption for the session/OAuth-state cookies (Securecookies work over plainhttp://localhostin modern browsers, but not on other non-HTTPS hosts). If you ever run this outside Cloudflare's HTTPS terminated production environment orlocalhostdev, e.g. a plain-HTTP LAN deployment, cookie-based auth would need reworking.
Deferred to a later pass, mostly because they need per commit data rather than aggregate contribution data and I dont want to implement them right now:
- Coding habits (activity by hour, day*hour heatmap)
- Precise language usage over time / fastest growing language
- Private repo language/content detail via a broader OAuth scope
- Shareable public stats card
- Period over period comparison on the main dashboard (already exists on recap pages)
This is a personal project, but issues and PRs are welcome. A few conventions if youre sending a PR:
- One feature branch per concern -
feat/...,fix/...,chore/...,ci/...,docs/...,refactor/... - Commit messages use the same prefixes (
feat:,fix:,chore:, etc.) - All calendar boundary date math must use
Date.UTC(...)explicitly, never a localDate()constructor for anything that needs to align to midnight/month start/etc.
This repository is licensed under the MIT License.