Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10850,6 +10850,36 @@
]
}
},
"/v1/app/analytics/mcp-compatibility": {
"get": {
"responses": {
"200": {
"description": "Live app API response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"nullable": true
}
}
}
}
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/app/commands/preview": {
"post": {
"responses": {
Expand Down
115 changes: 115 additions & 0 deletions apps/gittensory-ui/src/routes/app.analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ type OperatorDashboard = {
githubActivatedRepos: number;
};
}>;
mcpCompatibilityAdoption?: {
totalEvents: number;
activeActors: number;
staleEvents: number;
incompatibleEvents: number;
minimumSupportedVersion: string;
latestRecommendedVersion: string;
truncated: boolean;
byClientVersion: Array<{ key: string; count: number }>;
byProtocolVersion: Array<{ key: string; count: number }>;
byCompatibilityStatus: Array<{
status: "current" | "stale" | "incompatible" | "unknown";
count: number;
}>;
};
};

function ProductAnalytics() {
Expand Down Expand Up @@ -114,6 +129,80 @@ function ProductAnalytics() {
</div>
</section>

{data.mcpCompatibilityAdoption ? (
<section className="rounded-token border border-border bg-transparent p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="font-display text-token-lg font-semibold">
MCP compatibility adoption
</h2>
<p className="mt-1 text-token-xs text-muted-foreground">
Version distribution from redacted MCP product events.
</p>
</div>
<StatusPill
status={
data.mcpCompatibilityAdoption.incompatibleEvents > 0
? "degraded"
: data.mcpCompatibilityAdoption.staleEvents > 0
? "info"
: "ready"
}
>
{data.mcpCompatibilityAdoption.latestRecommendedVersion}
</StatusPill>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Stat
label="MCP events"
value={String(data.mcpCompatibilityAdoption.totalEvents)}
hint={<span className="text-muted-foreground">last 7 days</span>}
/>
<Stat
label="Active clients"
value={String(data.mcpCompatibilityAdoption.activeActors)}
hint={<span className="text-muted-foreground">hashed actors</span>}
/>
<Stat
label="Stale clients"
value={String(data.mcpCompatibilityAdoption.staleEvents)}
hint={<span className="text-muted-foreground">upgrade available</span>}
/>
<Stat
label="Unsupported"
value={String(data.mcpCompatibilityAdoption.incompatibleEvents)}
hint={
<span className="text-muted-foreground">
min {data.mcpCompatibilityAdoption.minimumSupportedVersion}
</span>
}
/>
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-3">
<CompatibilityList
title="Client versions"
rows={data.mcpCompatibilityAdoption.byClientVersion}
/>
<CompatibilityList
title="Protocol versions"
rows={data.mcpCompatibilityAdoption.byProtocolVersion}
/>
<CompatibilityList
title="Compatibility"
rows={data.mcpCompatibilityAdoption.byCompatibilityStatus.map((row) => ({
key: row.status,
count: row.count,
}))}
/>
</div>
{data.mcpCompatibilityAdoption.truncated ? (
<p className="mt-3 text-token-xs text-muted-foreground">
Displayed distribution is capped to keep dashboard reads bounded.
</p>
) : null}
</section>
) : null}

{data.usageRollups && data.usageRollups.length > 0 ? (
<section className="rounded-token border border-border bg-transparent p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
Expand Down Expand Up @@ -166,3 +255,29 @@ function ProductAnalytics() {
</StateBoundary>
);
}

function CompatibilityList({
title,
rows,
}: {
title: string;
rows: Array<{ key: string; count: number }>;
}) {
return (
<div className="rounded-token border border-border bg-background/40 p-3">
<div className="text-token-xs font-medium uppercase text-muted-foreground">{title}</div>
<div className="mt-3 space-y-2">
{rows.length > 0 ? (
rows.slice(0, 5).map((row) => (
<div key={row.key} className="flex items-center justify-between gap-3 text-token-sm">
<span className="min-w-0 truncate font-mono text-token-xs">{row.key}</span>
<span className="font-mono text-mint">{row.count}</span>
</div>
))
) : (
<div className="text-token-xs text-muted-foreground">No events</div>
)}
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ server.registerTool(
}
return toolResult("Gittensory local MCP status.", {
apiUrl,
package: {
name: packageName,
version: packageVersion,
},
hasToken: Boolean(getApiToken()),
authLogin: config.session?.login ?? null,
sessionExpiresAt: config.session?.expiresAt ?? null,
Expand Down Expand Up @@ -1203,6 +1207,9 @@ async function apiFetch(path, init, options = {}) {
...(token && options.auth !== false ? { authorization: `Bearer ${token}` } : {}),
"content-type": "application/json",
accept: "application/json",
"x-gittensory-mcp-package": packageName,
"x-gittensory-mcp-version": packageVersion,
"x-gittensory-mcp-client": "gittensory-mcp-cli",
},
}).finally(() => clearTimeout(timeout));
const text = await response.text();
Expand Down
36 changes: 32 additions & 4 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
persistSignalSnapshot,
recordProductUsageEvent,
rollupProductUsageDaily,
summarizeMcpCompatibilityAdoption,
summarizeProductUsageEvents,
upsertDigestSubscription,
upsertBounty,
Expand Down Expand Up @@ -106,6 +107,7 @@ import {
preflightBranchWithAgent,
startAgentRun,
} from "../services/agent-orchestrator";
import { buildMcpClientTelemetry } from "../services/client-telemetry";
import {
buildAndPersistContributorDecisionPack,
loadContributorDecisionPackForServing,
Expand Down Expand Up @@ -186,6 +188,7 @@ async function recordRouteProductUsage(
metadata?: Record<string, unknown> | null | undefined;
},
): Promise<void> {
const telemetry = buildMcpClientTelemetry(c.req.raw.headers, { requireGittensoryHeader: true });
await recordProductUsageEvent(c.env, {
surface: event.surface,
eventName: event.eventName,
Expand All @@ -196,9 +199,9 @@ async function recordRouteProductUsage(
targetKey: event.targetKey,
outcome: event.outcome,
latencyMs: event.latencyMs,
clientName: event.clientName,
clientVersion: event.clientVersion,
metadata: event.metadata,
clientName: event.clientName ?? telemetry?.clientName,
clientVersion: event.clientVersion ?? telemetry?.clientVersion,
metadata: telemetry ? Object.assign({}, event.metadata, telemetry.metadata) : event.metadata,
}).catch(() => undefined);
}

Expand Down Expand Up @@ -755,7 +758,21 @@ export function createApp() {
const forbidden = await requireAppRole(c, ["operator"]);
if (forbidden) return forbidden;
const usageSince = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const [repositories, installations, health, registry, scoring, upstreamDrift, activeSessions, digestSubscriptions, rateLimits, usageSummary, usageRollups, usageRollupStatus] = await Promise.all([
const [
repositories,
installations,
health,
registry,
scoring,
upstreamDrift,
activeSessions,
digestSubscriptions,
rateLimits,
usageSummary,
usageRollups,
usageRollupStatus,
mcpCompatibilityAdoption,
] = await Promise.all([
listRepositories(c.env),
listInstallations(c.env),
listInstallationHealth(c.env),
Expand All @@ -768,6 +785,7 @@ export function createApp() {
summarizeProductUsageEvents(c.env, usageSince),
listProductUsageDailyRollups(c.env, { limit: 14 }),
getProductUsageRollupStatus(c.env),
summarizeMcpCompatibilityAdoption(c.env, usageSince),
]);
const installedRepos = repositories.filter((repo) => repo.isInstalled).length;
const registeredRepos = repositories.filter((repo) => repo.isRegistered).length;
Expand All @@ -781,6 +799,7 @@ export function createApp() {
{ label: "Product events", value: String(usageSummary.totalEvents), delta: "last 7 days" },
{ label: "Active users", value: String(usageSummary.activeActors), delta: "hashed, last 7 days" },
{ label: "Activation rollups", value: usageRollupStatus.status, delta: usageRollupStatus.latestRollupDay ?? "not generated" },
{ label: "MCP stale clients", value: String(mcpCompatibilityAdoption.staleEvents + mcpCompatibilityAdoption.incompatibleEvents), delta: `${mcpCompatibilityAdoption.totalEvents} MCP event(s)` },
{ label: "Install issues", value: String(health.filter((record) => record.status !== "healthy").length), delta: "current health cache" },
{ label: "Rate-limit events", value: String(rateLimits.length), delta: "latest observations" },
],
Expand All @@ -793,12 +812,21 @@ export function createApp() {
usageSummary,
usageRollups,
usageRollupStatus,
mcpCompatibilityAdoption,
registry,
scoringModel: scoring,
Comment thread
oktofeesh1 marked this conversation as resolved.
upstreamDrift,
});
});

app.get("/v1/app/analytics/mcp-compatibility", async (c) => {
const forbidden = await requireAppRole(c, ["operator"]);
if (forbidden) return forbidden;
const days = Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7));
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
return c.json({ generatedAt: nowIso(), days, adoption: await summarizeMcpCompatibilityAdoption(c.env, since) });
});

app.get("/v1/app/analytics/daily-rollups", async (c) => {
const forbidden = await requireAppRole(c, ["operator"]);
if (forbidden) return forbidden;
Expand Down
Loading