Skip to content

Commit c933c85

Browse files
committed
feat: add repository code volume to insights and user detail pages
Collect AI code commits per repo (schema migration adds repo_name to ai_code_commits PK). Show top repos by lines with % of total and AI % on both the insights page and per-user detail page. Remove misleading tab/composer/manual breakdown (unreliable due to squash merges). Fix card height mismatch between Daily Spend and AI Adoption cards. Made-with: Cursor
1 parent d292fab commit c933c85

9 files changed

Lines changed: 336 additions & 65 deletions

File tree

.cursor/rules/cursor-api-data-guide.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Investigated Feb 2026 by comparing real user data against git history and studyi
142142
- Plan mode adoption from `/analytics/team/plans` - plan mode usage by model. Stored in `analytics_plans` table.
143143
- Per-user MCP tool usage from `/analytics/by-user/mcp` - which MCP tools each user uses. Stored in `analytics_user_mcp` table.
144144
- Per-user command usage from `/analytics/by-user/commands` - which commands each user uses. Stored in `analytics_user_commands` table.
145-
- AI Code Tracking from `/analytics/ai-code/commits` - per-commit AI vs manual line attribution. Stored in `ai_code_commits` table (aggregated per user per day). Provides `tabLinesAdded`, `composerLinesAdded`, `nonAiLinesAdded` per commit. Used for AI Adoption % on user detail page. Only tracks commits made through Cursor's Source Control panel — terminal git commits are not captured.
145+
- AI Code Tracking from `/analytics/ai-code/commits` - per-commit AI vs manual line attribution. Stored in `ai_code_commits` table (aggregated per user per day per repo). Primary key is `(email, date, repo_name)`. Provides `tabLinesAdded`, `composerLinesAdded`, `nonAiLinesAdded` per commit. Used for repository code volume on insights page and per-user repo breakdown. Note: tab/composer/nonAI breakdown is unreliable (see limitations above) — only total lines and AI % are shown in the UI. Only tracks commits made through Cursor's Source Control panel — terminal git commits are not captured.
146146

147147
### Not Currently Collected (but available)
148148
- Per-user breakdowns from `/analytics/by-user/*` endpoints for: agent-edits, tabs, models, plans, ask-mode, client-versions, top-file-extensions (we collect mcp and commands per-user, but not these others)

.cursor/rules/project-context.mdc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ Single cron endpoint `POST /api/cron` does both: collect → detect → alert in
6767
## Dashboard Pages
6868

6969
- `/` — Team overview: stat cards, model cost comparison table ($/request relative multipliers), daily spend trend (sourced from `usage_events` with `daily_spend` fallback, last 2 days marked provisional), spend breakdown by user, members table with search/sort, **group filter dropdown**, time range picker (24h/3d/7d/14d/30d), billing cycle progress
70-
- `/insights` — Analytics: DAU chart, model adoption, model efficiency rankings, MCP tool usage, file extensions, client versions
71-
- `/users/[email]` — Per-user detail: KPI cards (cycle spend, $/req, agent reqs, diffs accepted, team rank), spend trend chart, AI adoption card (tier, score bar, stat pills with tooltips), cost breakdown by model, tools & features (MCP tools + commands per user), model preferences, daily activity table, anomaly history
70+
- `/insights` — Analytics: DAU chart, model adoption, model efficiency rankings, repository code volume (lines, % of total, AI %), MCP tool usage, file extensions, client versions
71+
- `/users/[email]` — Per-user detail: KPI cards (cycle spend, $/req, agent reqs, diffs accepted, team rank), spend trend chart, AI adoption card (tier, score bar, stat pills with tooltips), cost breakdown by model, tools & features (MCP tools + commands per user), repositories (top 15 by lines, % of total, AI %), model preferences, daily activity table, anomaly history
7272
- `/anomalies` — MTTD/MTTI/MTTR metrics, open incidents (acknowledge/resolve), anomaly table
7373
- `/settings` — Detection thresholds, **billing group management** (rename, assign, create), **HiBob CSV import** with change preview
7474

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,13 @@ Also supports **email alerts** via [Resend](https://resend.com) (one API key, no
155155

156156
### Web Dashboard
157157

158-
| Page | What you see |
159-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160-
| **Team Overview** | Stat cards, spend by user, daily spend trend, spend breakdown, members table with search/sort, **group filter dropdown**, billing cycle progress, time range picker |
161-
| **Insights** | DAU chart, model adoption trends, model efficiency rankings (cost/precision), MCP tool usage, file extensions, client versions |
162-
| **User Drilldown** | Per-user token timeline, model breakdown, feature usage, activity profile, anomaly history |
163-
| **Anomalies** | Open incidents, MTTD/MTTI/MTTR metrics, full anomaly timeline |
164-
| **Settings** | Detection thresholds, expensive model alerts, billing group management, HiBob CSV import, group export/import |
158+
| Page | What you see |
159+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160+
| **Team Overview** | Stat cards, spend by user, daily spend trend, spend breakdown, members table with search/sort, **group filter dropdown**, billing cycle progress, time range picker |
161+
| **Insights** | DAU chart, model adoption trends, model efficiency rankings (cost/precision), repository code volume (lines, % of total, AI %), MCP tool usage, file extensions, client versions |
162+
| **User Drilldown** | Per-user token timeline, model breakdown, feature usage, repository breakdown, activity profile, anomaly history |
163+
| **Anomalies** | Open incidents, MTTD/MTTI/MTTR metrics, full anomaly timeline |
164+
| **Settings** | Detection thresholds, expensive model alerts, billing group management, HiBob CSV import, group export/import |
165165

166166
> For a detailed breakdown of every section, metric, badge, and chart, see [FEATURES.md](docs/FEATURES.md).
167167

src/app/api/analytics/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getPlanExhaustionStats,
1414
getModelEfficiency,
1515
getGroupsWithMembers,
16+
getRepoAIAttribution,
1617
} from "@/lib/data";
1718

1819
export const dynamic = "force-dynamic";
@@ -56,6 +57,7 @@ export function GET(request: Request) {
5657
versionUsers: getUsersByClientVersion(),
5758
planExhaustion: getPlanExhaustionStats(emails),
5859
modelEfficiency: getModelEfficiency(emails),
60+
repoAttribution: getRepoAIAttribution(days, emails),
5961
});
6062
} catch {
6163
return NextResponse.json({ error: "No analytics data yet" }, { status: 404 });

src/app/insights/insights-client.tsx

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ interface PlanExhaustionData {
105105
}>;
106106
}
107107

108+
interface RepoEntry {
109+
repo_name: string;
110+
commits: number;
111+
total_lines: number;
112+
tab_lines: number;
113+
composer_lines: number;
114+
non_ai_lines: number;
115+
ai_pct: number;
116+
}
117+
108118
interface InsightsData {
109119
dau: DAUEntry[];
110120
modelSummary: ModelSummary[];
@@ -118,6 +128,7 @@ interface InsightsData {
118128
versionUsers: VersionUsers;
119129
modelEfficiency: ModelEfficiencyEntry[];
120130
planExhaustion: PlanExhaustionData;
131+
repoAttribution: RepoEntry[];
121132
}
122133

123134
import { formatDateTick, formatDateLabel } from "@/lib/date-utils";
@@ -238,7 +249,6 @@ export function InsightsClient({
238249

239250
const totalAgentLines = data.agentEdits.reduce((s, d) => s + d.lines_accepted, 0);
240251
const totalTabLines = data.tabs.reduce((s, d) => s + d.lines_accepted, 0);
241-
const totalMessages = data.modelSummary.reduce((s, d) => s + d.total_messages, 0);
242252

243253
const mergedCommands = useMemo(() => {
244254
const map = new Map<string, number>();
@@ -569,53 +579,88 @@ export function InsightsClient({
569579
</ExpandableCard>
570580
</div>
571581

572-
{/* Tables Row: Model Breakdown + File Extensions */}
582+
{/* Tables Row: Repo Attribution + File Extensions */}
573583
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
574584
<ExpandableCard>
575-
<ChartCard title={`Model Usage Breakdown (${days}d)`}>
576-
<div className="overflow-y-auto max-h-[200px]">
577-
<table className="w-full text-xs">
578-
<thead className="sticky top-0 bg-zinc-900">
579-
<tr className="text-zinc-500 border-b border-zinc-800">
580-
<th className="text-left py-1 font-medium">Model</th>
581-
<th className="text-right py-1 font-medium">Messages</th>
582-
<th className="text-right py-1 font-medium">Users</th>
583-
<th className="text-right py-1 font-medium">% of Total</th>
584-
</tr>
585-
</thead>
586-
<tbody>
587-
{data.modelSummary.map((row) => {
588-
const pct = totalMessages > 0 ? (row.total_messages / totalMessages) * 100 : 0;
589-
return (
590-
<tr
591-
key={row.model}
592-
className="border-b border-zinc-800/30 hover:bg-zinc-800/30"
593-
>
594-
<td
595-
className="py-1 text-zinc-300 font-mono cursor-default"
596-
title={row.model}
597-
>
598-
{shortModel(row.model)}
599-
</td>
600-
<td className="text-right py-1 font-mono">{fmt(row.total_messages)}</td>
601-
<td className="text-right py-1 text-zinc-400">{row.total_users}</td>
602-
<td className="text-right py-1">
603-
<div className="flex items-center justify-end gap-1">
604-
<div className="w-12 h-1.5 bg-zinc-800 rounded-full overflow-hidden">
605-
<div
606-
className="h-full bg-blue-500 rounded-full"
607-
style={{ width: `${Math.min(pct, 100)}%` }}
608-
/>
609-
</div>
610-
<span className="text-zinc-500 w-8 text-right">{pct.toFixed(0)}%</span>
611-
</div>
612-
</td>
613-
</tr>
614-
);
615-
})}
616-
</tbody>
617-
</table>
618-
</div>
585+
<ChartCard title={`Repository Code Volume (${days}d)`}>
586+
{data.repoAttribution?.length > 0 ? (
587+
(() => {
588+
const grandTotal = data.repoAttribution.reduce((s, r) => s + r.total_lines, 0) || 1;
589+
return (
590+
<div className="overflow-y-auto" style={{ height: 200 }}>
591+
<table className="w-full text-xs">
592+
<thead className="sticky top-0 bg-zinc-900">
593+
<tr className="text-zinc-500 border-b border-zinc-800">
594+
<th
595+
className="text-left py-1 font-medium cursor-help"
596+
title="Repository tracked via Cursor Source Control commits"
597+
>
598+
Repository
599+
</th>
600+
<th
601+
className="text-right py-1 font-medium cursor-help"
602+
title="Total lines added across all commits in this repo"
603+
>
604+
Lines
605+
</th>
606+
<th
607+
className="text-right py-1 font-medium cursor-help"
608+
title="This repo's share of all lines across all repos"
609+
>
610+
% of Total
611+
</th>
612+
<th
613+
className="text-right py-1 font-medium cursor-help"
614+
title="Percentage of lines attributed to AI (tab completions + composer/agent). May undercount due to squash merges"
615+
>
616+
AI %
617+
</th>
618+
</tr>
619+
</thead>
620+
<tbody>
621+
{data.repoAttribution.slice(0, 10).map((row) => {
622+
const sharePct = ((row.total_lines / grandTotal) * 100).toFixed(1);
623+
const shortRepo = row.repo_name.includes("/")
624+
? row.repo_name.split("/").slice(-1)[0]
625+
: row.repo_name;
626+
return (
627+
<tr
628+
key={row.repo_name}
629+
className="border-b border-zinc-800/30 hover:bg-zinc-800/30"
630+
>
631+
<td
632+
className="py-1 text-zinc-300 font-mono truncate max-w-[140px]"
633+
title={row.repo_name}
634+
>
635+
{shortRepo}
636+
</td>
637+
<td className="text-right py-1 font-mono">{fmt(row.total_lines)}</td>
638+
<td className="text-right py-1 text-zinc-400">{sharePct}%</td>
639+
<td className="text-right py-1">
640+
<span
641+
className={
642+
row.ai_pct >= 50 ? "text-emerald-400" : "text-zinc-400"
643+
}
644+
>
645+
{row.ai_pct}%
646+
</span>
647+
</td>
648+
</tr>
649+
);
650+
})}
651+
</tbody>
652+
</table>
653+
<span className="text-[10px] text-zinc-600 block text-right mt-1 pr-1">
654+
via Cursor Source Control
655+
</span>
656+
</div>
657+
);
658+
})()
659+
) : (
660+
<div className="flex items-center justify-center h-[200px] text-zinc-500 text-xs">
661+
No repo data yet. Commits through Cursor Source Control will appear here.
662+
</div>
663+
)}
619664
</ChartCard>
620665
</ExpandableCard>
621666

src/app/insights/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getModelEfficiency,
1313
getPlanExhaustionStats,
1414
getGroupsWithMembers,
15+
getRepoAIAttribution,
1516
} from "@/lib/data";
1617
import { InsightsClient } from "./insights-client";
1718

@@ -32,6 +33,7 @@ export default function InsightsPage() {
3233
versionUsers: getUsersByClientVersion(),
3334
modelEfficiency: getModelEfficiency(),
3435
planExhaustion: getPlanExhaustionStats(),
36+
repoAttribution: getRepoAIAttribution(30),
3537
};
3638

3739
const groups = getGroupsWithMembers();

src/app/users/[email]/user-detail-client.tsx

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ interface UserStats {
9393
usageBasedReqs: number;
9494
cycleDay: number;
9595
} | null;
96+
repoBreakdown: Array<{
97+
repo_name: string;
98+
commits: number;
99+
total_lines: number;
100+
tab_lines: number;
101+
composer_lines: number;
102+
non_ai_lines: number;
103+
ai_pct: number;
104+
}>;
96105
}
97106

98107
interface UserDetailClientProps {
@@ -471,6 +480,87 @@ export function UserDetailClient({ email, stats }: UserDetailClientProps) {
471480
</ExpandableCard>
472481
)}
473482

483+
{stats.repoBreakdown && stats.repoBreakdown.length > 0 && (
484+
<ExpandableCard>
485+
<div className="bg-zinc-900 rounded-lg border border-zinc-800 overflow-hidden">
486+
<div className="px-4 py-2.5 border-b border-zinc-800 flex items-center justify-between">
487+
<h3 className="text-xs font-medium text-zinc-400">Repositories</h3>
488+
<span className="text-[10px] text-zinc-600">via Cursor Source Control</span>
489+
</div>
490+
<div className="overflow-x-auto max-h-[280px] overflow-y-auto">
491+
{(() => {
492+
const top = stats.repoBreakdown.slice(0, 15);
493+
const grandTotal = stats.repoBreakdown.reduce((s, r) => s + r.total_lines, 0) || 1;
494+
return (
495+
<table className="w-full text-xs">
496+
<thead className="sticky top-0 bg-zinc-900 z-10">
497+
<tr className="border-b border-zinc-800 text-zinc-500">
498+
<th
499+
className="text-left px-4 py-2 font-medium cursor-help"
500+
title="Repository tracked via Cursor Source Control commits"
501+
>
502+
Repo
503+
</th>
504+
<th
505+
className="text-right px-4 py-2 font-medium cursor-help"
506+
title="Total lines added across all commits in this repo"
507+
>
508+
Lines
509+
</th>
510+
<th
511+
className="text-right px-4 py-2 font-medium cursor-help"
512+
title="This repo's share of all lines across this user's repos"
513+
>
514+
% of Total
515+
</th>
516+
<th
517+
className="text-right px-4 py-2 font-medium cursor-help"
518+
title="Percentage of lines attributed to AI (tab completions + composer/agent). May undercount due to squash merges"
519+
>
520+
AI %
521+
</th>
522+
</tr>
523+
</thead>
524+
<tbody>
525+
{top.map((r) => {
526+
const sharePct = ((r.total_lines / grandTotal) * 100).toFixed(1);
527+
const shortRepo = r.repo_name.includes("/")
528+
? r.repo_name.split("/").slice(-1)[0]
529+
: r.repo_name;
530+
return (
531+
<tr
532+
key={r.repo_name}
533+
className="border-b border-zinc-800/50 hover:bg-zinc-800/30"
534+
>
535+
<td
536+
className="px-4 py-1.5 text-zinc-300 font-mono truncate max-w-[180px]"
537+
title={r.repo_name}
538+
>
539+
{shortRepo}
540+
</td>
541+
<td className="text-right px-4 py-1.5 font-mono">
542+
{fmt(r.total_lines)}
543+
</td>
544+
<td className="text-right px-4 py-1.5 text-zinc-400">{sharePct}%</td>
545+
<td className="text-right px-4 py-1.5">
546+
<span
547+
className={r.ai_pct >= 50 ? "text-emerald-400" : "text-zinc-400"}
548+
>
549+
{r.ai_pct}%
550+
</span>
551+
</td>
552+
</tr>
553+
);
554+
})}
555+
</tbody>
556+
</table>
557+
);
558+
})()}
559+
</div>
560+
</div>
561+
</ExpandableCard>
562+
)}
563+
474564
{stats.contextMetrics && (
475565
<ExpandableCard>
476566
<ContextEfficiencyCard metrics={stats.contextMetrics} />
@@ -826,7 +916,7 @@ function AIAdoptionCard({ adoption }: { adoption: UserStats["aiAdoption"] }) {
826916
const reqsPerDay = Math.round(adoption.intensity);
827917

828918
return (
829-
<div className={`bg-zinc-900 rounded-lg border ${style.border} p-5 flex flex-col`}>
919+
<div className={`bg-zinc-900 rounded-lg border ${style.border} p-5 flex flex-col h-full`}>
830920
<h3 className="text-xs font-medium text-zinc-400 mb-4">AI Adoption</h3>
831921

832922
<div className="flex-1 flex flex-col justify-center">

0 commit comments

Comments
 (0)