Skip to content

feat: add GitHub activity intelligence engine with mobile UI(fixes #132)#160

Open
LALA22-7 wants to merge 2 commits into
Dev-Card:mainfrom
LALA22-7:feat/github-activity-intelligence
Open

feat: add GitHub activity intelligence engine with mobile UI(fixes #132)#160
LALA22-7 wants to merge 2 commits into
Dev-Card:mainfrom
LALA22-7:feat/github-activity-intelligence

Conversation

@LALA22-7
Copy link
Copy Markdown

Summary

Hey @Suyash2527 @Midoriya-w 👋

I went ahead and implemented the core slice of this feature — the full backend + mobile UI for GitHub Activity Intelligence. Here's what's been done so far. If you want to pick up any of the remaining pieces (web UI, predictive analytics, recruiter scoring, etc.), feel free to build on top of this — the foundation is all wired up.

Closes #132


Type of Change

  • New feature
  • Tests only

What Changed

  • packages/shared/src/types.ts — Added GitHubInsights, GitHubRepo, and GitHubLanguageStat shared types consumed by both backend and mobile
  • apps/backend/src/routes/github-insights.ts — New route GET /api/analytics/github-insights that fetches repos + user stats from GitHub REST API, computes language breakdown, top repos, star/fork totals, and optionally generates an AI developer summary via Gemini 1.5 Flash
  • apps/backend/prisma/schema.prisma — Added GitHubInsightsCache model for DB-level cache fallback
  • apps/backend/prisma/migrations/... — Migration for the new cache table
  • apps/backend/src/app.ts — Registered the new route at /api/analytics/github-insights
  • apps/backend/src/__tests__/github-insights.test.ts — 7 tests covering: no token, live fetch, Redis cache hit, DB cache hit, force refresh, expired token, fork exclusion logic (all passing ✅)
  • apps/mobile/src/screens/GitHubInsightsScreen.tsx — New screen with stats grid, AI summary card, stacked language bar, top repos list (tappable → opens GitHub), pull-to-refresh, and all edge-case states (loading, not connected, error, empty)
  • apps/mobile/src/navigation/MainTabs.tsx — Wired GitHubInsights into the root stack
  • apps/mobile/src/screens/HomeScreen.tsx — Added 🐙 GitHub shortcut button in the home actions row
  • .env.example — Added optional GEMINI_API_KEY with a link to get a free key

How to Test

  1. Start the backend: pnpm dev:backend
  2. Log in with a GitHub account that has public repos
  3. Hit GET /api/analytics/github-insights with a valid JWT — should return live data with source: "live"
  4. Hit the same endpoint again — should return source: "cache" (Redis)
  5. Hit with ?refresh=true — should bypass cache and return source: "live" again
  6. On mobile, tap the 🐙 GitHub button on the Home screen — should open the GitHub Insights screen
  7. Pull down to refresh on the insights screen — triggers a forced refresh
  8. Run tests: pnpm --filter @devcard/backend test — all 15 tests should pass

Checklist

  • My code follows the project's coding style (pnpm -r run lint passes).
  • TypeScript compiles without errors in new files (pre-existing TS errors in the repo are unrelated to this PR).
  • I have added tests for the changes I made (7 new tests, all passing).
  • All tests pass locally (pnpm -r run test).
  • I have updated .env.example where necessary.
  • No new console.log or debug statements left in the code.

What's Still Open (for collaborators to pick up)

If you want to contribute on top of this, here are natural next pieces:

  • Web UI — A /dashboard/github-insights page in the SvelteKit app consuming the same endpoint
  • Contribution heatmap — GitHub's contribution calendar data via GraphQL API (contributionsCollection)
  • Predictive analytics — Trend lines using historical cached snapshots
  • Extended AI prompts — More detailed Gemini prompts using repo topics, commit frequency, etc.
  • user:follow scope upgrade — Prompt users to re-authorize with broader scopes for richer data

The endpoint is at GET /api/analytics/github-insights and the shared types are in @devcard/shared. Everything is documented in the route file.


Additional Context

  • The AI summary is fully optional — if GEMINI_API_KEY is not set, aiSummary returns null and the mobile UI simply hides that card. No hard dependency on any paid API.
  • Cache is two-layered: Redis (fast, 1hr TTL) with a PostgreSQL fallback. If Redis is down, the DB cache kicks in silently.
  • Forked repos are excluded from all star counts, fork counts, and language stats — only original work is counted.

- Add GET /api/analytics/github-insights backend route

- Fetch repos, language stats, top repos, star/fork totals from GitHub REST API

- Two-layer cache: Redis (1hr TTL) + PostgreSQL fallback

- Optional AI developer summary via Gemini 1.5 Flash (GEMINI_API_KEY)

- Add GitHubInsightsCache Prisma model + migration

- Add GitHubInsights, GitHubRepo, GitHubLanguageStat shared types

- Add GitHubInsightsScreen mobile UI with stats grid, language bar, top repos

- Wire GitHubInsights into root navigation stack

- Add GitHub shortcut button on HomeScreen

- 7 new backend tests, all passing
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements the backend + mobile portion of issue #132 (GitHub Activity Intelligence). A new authenticated Fastify route fetches the user's repos/profile from the GitHub REST API, aggregates language/star/fork stats and an optional Gemini-generated AI summary, and caches the result in Redis with a Postgres fallback. The mobile app adds a new GitHubInsights screen reachable from the Home screen shortcut.

Changes:

  • New GET /api/analytics/github-insights endpoint with two-layer (Redis + DB) caching and optional Gemini AI summary, plus shared GitHubInsights/GitHubRepo/GitHubLanguageStat types and a GitHubInsightsCache Prisma model/migration.
  • New GitHubInsightsScreen mobile component (stats grid, AI card, language bar, top-repo list, pull-to-refresh, error/empty/not-connected states) wired into the root stack and Home screen.
  • Backend tests (7) covering cache hits, refresh bypass, missing token, expired token, and fork exclusion; .env.example updated with optional GEMINI_API_KEY.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/shared/src/types.ts Adds shared GitHubInsights/GitHubRepo/GitHubLanguageStat types.
apps/backend/src/routes/github-insights.ts New route: fetch + aggregate + AI summary + two-layer cache.
apps/backend/src/app.ts Registers the new route under /api/analytics/github-insights.
apps/backend/prisma/schema.prisma Adds GitHubInsightsCache model with FK to User.
apps/backend/prisma/migrations/20260519000000_github_insights_cache/migration.sql Creates the github_insights_cache table.
apps/backend/src/tests/github-insights.test.ts 7 unit tests for the new route.
apps/mobile/src/screens/GitHubInsightsScreen.tsx New screen rendering insights with all edge states.
apps/mobile/src/navigation/MainTabs.tsx Registers GitHubInsights in the root stack.
apps/mobile/src/screens/HomeScreen.tsx Adds 🐙 GitHub shortcut button.
.env.example Adds optional GEMINI_API_KEY entry.
Comments suppressed due to low confidence (3)

apps/backend/src/routes/github-insights.ts:174

  • The languageBytes map and bytes field on GitHubLanguageStat claim to represent raw byte counts (see the shared type's JSDoc: "Raw byte count"), but the implementation here simply increments by 1 per repo, so bytes is actually a count of repositories where that language is the primary language. The resulting percentage is therefore a percentage of repos, not of code volume. Either rename the field/comment to reflect that it's a repo count, or actually call GET /repos/{owner}/{repo}/languages for each repo and sum real byte counts.
  // ── Language aggregation ──
  const languageBytes: Record<string, number> = {};
  for (const repo of allRepos) {
    if (repo.language && !repo.fork) {
      languageBytes[repo.language] = (languageBytes[repo.language] ?? 0) + 1;
    }
  }

  const totalLangCount = Object.values(languageBytes).reduce((a, b) => a + b, 0);
  const languageStats: GitHubLanguageStat[] = Object.entries(languageBytes)
    .sort(([, a], [, b]) => b - a)
    .map(([language, bytes]) => ({
      language,
      bytes,
      percentage: totalLangCount > 0
        ? Math.round((bytes / totalLangCount) * 1000) / 10
        : 0,
    }));

apps/backend/src/routes/github-insights.ts:214

  • totalRepos is set from ghUser.public_repos, which includes forks, while totalStars, totalForks, and languageStats deliberately exclude forks. This makes the "Repos" stat inconsistent with the other stats shown to the user. Consider either using the count of non-forked repos for totalRepos (e.g., allRepos.filter(r => !r.fork).length) or surfacing both totals explicitly.
  return {
    username: ghUser.login,
    totalRepos: ghUser.public_repos,
    totalStars,
    totalForks,
    followers: ghUser.followers,
    following: ghUser.following,
    topRepos,
    languageStats,
    primaryLanguage: languageStats[0]?.language ?? null,
    accountCreatedAt: ghUser.created_at,
    fetchedAt: new Date().toISOString(),
    aiSummary: null, // filled in after
  };

apps/backend/src/routes/github-insights.ts:190

  • topRepos is built by filtering !r.fork, so the isForked field on every returned GitHubRepo here will always be false. If the field is intended to be meaningful in the API, include forks (and let the consumer filter) or drop the field for this endpoint.
  const topRepos: GitHubRepo[] = allRepos
    .filter((r) => !r.fork)
    .sort((a, b) => b.stargazers_count - a.stargazers_count)
    .slice(0, TOP_REPOS_COUNT)
    .map((r) => ({
      name: r.name,
      description: r.description ?? null,
      url: r.html_url,
      stars: r.stargazers_count,
      forks: r.forks_count,
      language: r.language ?? null,
      isForked: r.fork,
      updatedAt: r.updated_at,
    }));

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +143 to +155
// If user has more than 100 repos, fetch the second page too (cap at 200)
let allRepos = firstPageRepos;
if (ghUser.public_repos > MAX_REPOS_FOR_ANALYSIS) {
try {
const secondPage = await githubFetch<any[]>(
`/user/repos?per_page=100&sort=updated&type=owner&page=2`,
accessToken,
);
allRepos = [...firstPageRepos, ...secondPage];
} catch {
// Non-fatal — proceed with what we have
}
}
Comment on lines +176 to +185
if (res.status === 400) {
const body = await res.json();
if (body.requiresAuth) {
setRequiresConnect(true);
return;
}
}

if (!res.ok) {
const body = await res.json().catch(() => ({}));
Comment thread apps/backend/src/app.ts Outdated
await app.register(followRoutes, { prefix: '/api/follow' });
await app.register(connectRoutes, { prefix: '/api/connect' });
await app.register(analyticsRoutes, { prefix: '/api/analytics' });
await app.register(githubInsightsRoutes, { prefix: '/api/analytics/github-insights' });
- Fix trailing slash 404: move handler to /github-insights under /api/analytics prefix instead of separate plugin registration

- Fix consumed response body bug in mobile: read body once, branch on requiresAuth before checking res.ok

- Add statsAreCapped field to GitHubInsights type and response: true when user has 200+ repos and stats are a subset

- Surface statsAreCapped disclaimer in mobile UI with warning banner

- Document pagination cap in constants and JSDoc comments

- Add 2 new tests for statsAreCapped (true/false), now 17 tests total
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.

[Feature] Add AI-Powered GitHub Activity Intelligence & Developer Insight Engine

2 participants