Know how much time you actually spend on LeetCode, which topics you neglect, and which problems you keep coming back to.
A Chrome/Brave extension that quietly tracks your LeetCode practice — time spent, runs, submits, verdicts — and a web dashboard that turns it into something useful: solved-by-difficulty badges, a topic-time chart, a filterable problem list, and friend comparison.
→ Try the dashboard · → Install the extension (invite-only) · → Read the technical writeup
LeetCode shows you what you've solved. It doesn't show:
- Where your time actually goes — that 30 min on Two Sum vs 3 hours stuck on Trapping Rain Water
- Which topics you avoid — you've done 18 array problems and 0 graph problems this month
- What your real attempt history looks like — every run, every submit, every verdict, timestamped
- How you compare to friends grinding the same problems
This fills those gaps with one Chrome extension and one dashboard. No screenshots, no manual logging, no behavior change on your part — it just runs in the background while you practice as normal.
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Browser Extension │ │ Supabase │ │ Web Dashboard │
│ │ sync │ │ read │ │
│ • Time tracking │ ──────► │ • Postgres + RLS │ ──────► │ • Stats + charts │
│ • Run/Submit obs. │ every │ • Auth (invite) │ live │ • Problem list │
│ • Verdict watcher │ 60s + │ • Single-use codes │ │ • Friends compare │
│ • Local catalogue │ events │ │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
MV3 + JS Postgres + GoTrue React + Vite + Recharts
✦ Zero requests to leetcode.com ✦
(all observation is passive via chrome.webRequest)
A few design choices worth calling out:
- Zero LeetCode network traffic. The extension makes no requests to leetcode.com at all — Run/Submit detection is passive (it watches the user's own POSTs via
chrome.webRequest), and problem metadata is served from a catalogue bundled inside the extension (3,943 problems, ~114 KB gzipped, scraped once by the maintainer). From LeetCode's perspective, a user with the extension installed is indistinguishable from one without it. No basis for an account flag. chrome.webRequestfor Run/Submit detection (not page-level fetch hooks). LeetCode caches a native fetch reference before any content script can patch it, so MAIN-world overrides silently miss every request. webRequest sits at the network layer and catches everything.- Bundled problem catalogue. Difficulty, topics, and premium-status for every public problem are shipped inside the extension (
extension/data/problems.json). The local lookup replaces what used to be a runtime GraphQL call. Refresh vianode scripts/scrape-problems.jsand re-release. - Time tracking treats "opened-and-bounced" as noise. A visit without any run, submit, or 30+ seconds of input is discarded at the sync boundary. Your dashboard reflects real practice, not curiosity.
- Phantom-time safeguard. Tab closed without firing
pagehide? The background worker caps the active interval after 90 seconds of silence so the timer doesn't accumulate hours of imaginary time. - Row Level Security on every table. Anon key is shipped in the client; access control is enforced at the database layer, not the application layer.
- Single-use invite codes gate signups. Codes live in a table the anon key cannot read, validated by a
SECURITY DEFINERtrigger onauth.usersinsert.
| Layer | Stack |
|---|---|
| Extension | Manifest V3 · vanilla JS · ES modules · chrome.webRequest |
| Dashboard | React 19 · Vite 8 · Recharts · date-fns |
| Backend | Supabase (Postgres 15 · GoTrue · PostgREST · pg_cron) |
| Hosting | Netlify (dashboard) · Supabase (DB + auth) · GitHub Releases (extension) |
| Distribution | Unpacked .zip (current) → Chrome Web Store (planned) |
Total footprint: ~3000 lines across extension + dashboard + SQL migrations.
Signups require a single-use code. DM me if you'd like one.
- Download the latest
leetcode-tracker-X.Y.Z.zipfrom the releases page and unzip it. - Open
chrome://extensions(orbrave://extensions) → toggle Developer mode ON. - Load unpacked → pick the unzipped folder.
- Open the dashboard and Sign up with your email, a password, and the invite code you were given.
- Click the extension icon → Sign in with the same credentials.
- Open any leetcode.com/problems/ page. Practice as usual. Your dashboard updates within ~60 seconds of each Run or Submit.
extension/ Manifest V3 Chrome extension
├ background.js service worker (sync, webRequest, attempt lifecycle, catalogue lookup)
├ content.js isolated-world script (slug detection, idle tracking)
├ page-hook.js MAIN-world script (verdict-watch backstop)
├ supabase.js thin REST/auth wrapper, no SDK
├ popup.html/.js extension popup UI
├ config.js public Supabase URL + anon key (safe to commit, RLS enforces)
├ data/
│ └ problems.json bundled metadata for ~3,943 problems (zero runtime LeetCode calls)
└ build-zip.sh packages the release zip
dashboard/ React + Vite SPA
└ src/
├ pages/ Dashboard + Auth + Friends
├ components/ StatsBar, ActivityChart, Heatmap, TopicChart, ProblemList, DifficultyBadges
├ hooks/ useAttempts, useProblems, useFriendships, useSession
└ utils/stats.js pure aggregation (testable, no React, no Supabase)
supabase/ SQL migrations (idempotent, re-runnable)
├ schema.sql core tables + RLS + triggers
├ phase4.sql friends/groups
├ phase4_usernames.sql username uniqueness
├ phase5_event_log.sql granular event tracking + 90-day cron cleanup
├ phase6_invite_codes.sql single-use signup codes
└ phase7_lockdown_problems.sql immutable shared catalogue
scripts/ admin tooling (runs only on owner machine)
├ new-invite.sh generate a single-use invite code
├ scrape-problems.js regenerate extension/data/problems.json from LeetCode
└ .env.example service_role config (real .env is gitignored)
For forking or self-hosting your own instance:
Click to expand
- Create a project at supabase.com (free tier is plenty).
- SQL Editor → run each file in
supabase/in numeric order (schema.sqlfirst, thenphase4.sql, etc). - Project Settings → API → copy the Project URL and anon public key.
- Edit
extension/config.js— replace the URL and anon key with your own project's values. chrome://extensions→ Developer Mode → Load unpacked → pick theextension/folder.
cd dashboard
cp .env.example .env
# Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
npm install
npm run dev # → http://localhost:5173Deploy dashboard/ to Netlify, Vercel, or any static host. Build command: npm run build, output dir: dist.
cp scripts/.env.example scripts/.env
# Fill in SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (the latter NEVER leaves your machine)
./scripts/new-invite.sh "Note for this invite"Re-scrape LeetCode's public problem list, write a fresh extension/data/problems.json, then ship a new extension version:
node scripts/scrape-problems.js
# bumps to ~3,943 problems in ~60 seconds (polite, 1.5s/page, unauthenticated)
# Then bump version in extension/manifest.json, rebuild, release:
./extension/build-zip.sh
gh release create vX.Y.Z extension/leetcode-tracker-X.Y.Z.zip --title "..." --notes "..."Monthly is plenty — LeetCode adds maybe 5-10 problems per week.
A few notes since this is a multi-user system with a public-anon client:
- Anon key is committed. It's safe by design — Supabase anon keys are intended for client use; access control happens at the database via Row Level Security policies, not by hiding the key.
- service_role key is admin-only. Lives only in
scripts/.envon the owner's machine. Never shipped, never committed. - Every table has RLS enabled. Users can read/write only their own rows except
problems(a shared catalogue with read-anyone / insert-only-from-clients / update-admin-only). - No client-initiated LeetCode requests. The extension's only outgoing traffic goes to your own Supabase project (for sync). LeetCode is observed passively via
chrome.webRequest. No theoretical basis for a LeetCode account ban from extension behaviour. - Invite codes use a
SECURITY DEFINERtrigger that locks the row (SELECT FOR UPDATE) to prevent race conditions when two signups attempt the same code. - Events are auto-purged.
pg_cronruns nightly to delete events older than 90 days, keeping storage bounded at the free-tier limit.
MIT — see LICENSE.
Built by @iprashantraj. Open an issue for bugs or feature requests — there's also a "Report a bug" link inside the extension popup.