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
77 changes: 76 additions & 1 deletion apps/gittensory-ui/src/routes/app.analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ export const Route = createFileRoute("/app/analytics")({
type OperatorDashboard = {
metrics: Array<{ label: string; value: string; delta: string }>;
noiseReduction: Array<{ label: string; value: number; spark: number[] }>;
usageRollupStatus?: {
status: "empty" | "ready" | "partial" | "stale" | "incomplete";
latestRollupDay?: string | null;
warnings: string[];
};
usageRollups?: Array<{
day: string;
status: "complete" | "partial" | "incomplete";
totalEvents: number;
activeActors: number;
activeRepos: number;
activation: {
fullyActivatedActors: number;
githubActivatedRepos: number;
};
}>;
};

function ProductAnalytics() {
Expand Down Expand Up @@ -48,7 +64,18 @@ function ProductAnalytics() {
</p>
</div>
<div className="flex items-center gap-2">
<StatusPill status="ready">Live API</StatusPill>
<StatusPill
status={
data.usageRollupStatus?.status === "ready" ||
data.usageRollupStatus?.status === "partial"
? "ready"
: data.usageRollupStatus?.status === "empty"
? "info"
: "degraded"
}
>
{data.usageRollupStatus?.status ?? "Live API"}
</StatusPill>
<BoundaryBadge boundary="private-api" />
</div>
</header>
Expand Down Expand Up @@ -86,6 +113,54 @@ function ProductAnalytics() {
))}
</div>
</section>

{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">
<div>
<h2 className="font-display text-token-lg font-semibold">
Daily activation rollups
</h2>
<p className="mt-1 text-token-xs text-muted-foreground">
Hashed actor, repo, command, tool, and maintainer-action funnels by UTC day.
</p>
</div>
<StatusPill status={data.usageRollupStatus?.warnings.length ? "degraded" : "ready"}>
{data.usageRollupStatus?.latestRollupDay ?? "current"}
</StatusPill>
</div>
<div className="mt-4 overflow-x-auto">
<table className="w-full min-w-[680px] text-left text-token-sm">
<thead className="border-b border-border text-token-xs uppercase text-muted-foreground">
<tr>
<th className="py-2 pr-4 font-medium">Day</th>
<th className="py-2 pr-4 font-medium">Status</th>
<th className="py-2 pr-4 font-medium">Events</th>
<th className="py-2 pr-4 font-medium">Actors</th>
<th className="py-2 pr-4 font-medium">Repos</th>
<th className="py-2 pr-4 font-medium">Activated</th>
<th className="py-2 font-medium">GitHub activated</th>
</tr>
</thead>
<tbody>
{data.usageRollups.slice(0, 7).map((rollup) => (
<tr key={rollup.day} className="border-b border-border/60 last:border-0">
<td className="py-2 pr-4 font-mono text-token-xs">{rollup.day}</td>
<td className="py-2 pr-4">{rollup.status}</td>
<td className="py-2 pr-4 font-mono">{rollup.totalEvents}</td>
<td className="py-2 pr-4 font-mono">{rollup.activeActors}</td>
<td className="py-2 pr-4 font-mono">{rollup.activeRepos}</td>
<td className="py-2 pr-4 font-mono">
{rollup.activation.fullyActivatedActors}
</td>
<td className="py-2 font-mono">{rollup.activation.githubActivatedRepos}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
) : null}
</div>
) : null}
</StateBoundary>
Expand Down
25 changes: 25 additions & 0 deletions migrations/0016_product_usage_daily_rollups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS product_usage_daily_rollups (
day TEXT PRIMARY KEY,
status TEXT NOT NULL,
total_events INTEGER NOT NULL DEFAULT 0,
active_actors INTEGER NOT NULL DEFAULT 0,
active_sessions INTEGER NOT NULL DEFAULT 0,
active_repos INTEGER NOT NULL DEFAULT 0,
source_event_count INTEGER NOT NULL DEFAULT 0,
max_event_capacity INTEGER NOT NULL DEFAULT 0,
first_event_at TEXT,
last_event_at TEXT,
surfaces_json TEXT NOT NULL DEFAULT '[]',
outcomes_json TEXT NOT NULL DEFAULT '[]',
events_json TEXT NOT NULL DEFAULT '[]',
repos_json TEXT NOT NULL DEFAULT '[]',
commands_json TEXT NOT NULL DEFAULT '[]',
tools_json TEXT NOT NULL DEFAULT '[]',
route_classes_json TEXT NOT NULL DEFAULT '[]',
activation_json TEXT NOT NULL DEFAULT '{}',
generated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS product_usage_daily_rollups_status_idx
ON product_usage_daily_rollups(status, updated_at);
34 changes: 33 additions & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
getRepositorySettings,
recordAuditEvent,
getContributorEvidence,
getProductUsageRollupStatus,
listAllPullRequestDetailSyncStates,
listCheckSummaries,
listBounties,
Expand All @@ -53,6 +54,7 @@ import {
listIssueSignalSample,
listAgentRunsForActor,
listDigestSubscriptionsForLogin,
listProductUsageDailyRollups,
listOpenPullRequests,
listPullRequestFiles,
listPullRequestReviews,
Expand All @@ -70,6 +72,7 @@ import {
persistScorePreview,
persistSignalSnapshot,
recordProductUsageEvent,
rollupProductUsageDaily,
summarizeProductUsageEvents,
upsertDigestSubscription,
upsertBounty,
Expand Down Expand Up @@ -752,7 +755,7 @@ 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] = await Promise.all([
const [repositories, installations, health, registry, scoring, upstreamDrift, activeSessions, digestSubscriptions, rateLimits, usageSummary, usageRollups, usageRollupStatus] = await Promise.all([
listRepositories(c.env),
listInstallations(c.env),
listInstallationHealth(c.env),
Expand All @@ -763,6 +766,8 @@ export function createApp() {
countActiveDigestSubscriptions(c.env),
listLatestGitHubRateLimitObservations(c.env, 20),
summarizeProductUsageEvents(c.env, usageSince),
listProductUsageDailyRollups(c.env, { limit: 14 }),
getProductUsageRollupStatus(c.env),
]);
const installedRepos = repositories.filter((repo) => repo.isInstalled).length;
const registeredRepos = repositories.filter((repo) => repo.isRegistered).length;
Expand All @@ -775,6 +780,7 @@ export function createApp() {
{ label: "Digest subscriptions", value: String(digestSubscriptions), delta: "store-only" },
{ 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: "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 @@ -785,12 +791,22 @@ export function createApp() {
],
weeklyReport: buildOperatorWeeklyReport({ repositories, installations, health, registry, scoring, upstreamDrift }),
usageSummary,
usageRollups,
usageRollupStatus,
registry,
scoringModel: scoring,
upstreamDrift,
});
Comment thread
oktofeesh1 marked this conversation as resolved.
});

app.get("/v1/app/analytics/daily-rollups", async (c) => {
const forbidden = await requireAppRole(c, ["operator"]);
if (forbidden) return forbidden;
const limit = Math.max(1, Math.min(90, Number(c.req.query("limit") ?? 14) || 14));
const [rollups, status] = await Promise.all([listProductUsageDailyRollups(c.env, { limit }), getProductUsageRollupStatus(c.env)]);
return c.json({ generatedAt: nowIso(), status, rollups });
});

app.get("/v1/app/commands", async (c) =>
c.json({
generatedAt: nowIso(),
Expand Down Expand Up @@ -1705,6 +1721,15 @@ export function createApp() {
return c.json({ ok: true, status: "queued", repoFullName }, 202);
});

app.post("/v1/internal/jobs/rollup-product-usage", async (c) => {
const body = await c.req.json().catch(() => ({}));
const day = typeof body?.day === "string" ? body.day : undefined;
const days = Number.isFinite(Number(body?.days)) ? Math.max(1, Math.min(31, Math.round(Number(body.days)))) : undefined;
const message: JobMessage = { type: "rollup-product-usage", requestedBy: "api", ...(day ? { day } : {}), ...(days === undefined ? {} : { days }) };
await c.env.JOBS.send(message);
return c.json({ ok: true, status: "queued", day, days }, 202);
});

app.post("/v1/internal/jobs/repair-data-fidelity", async (c) => {
const message: JobMessage = { type: "repair-data-fidelity", requestedBy: "api" };
await c.env.JOBS.send(message);
Expand All @@ -1718,6 +1743,13 @@ export function createApp() {
return c.json({ ok: true, status: "completed", repoFullName });
});

app.post("/v1/internal/jobs/rollup-product-usage/run", async (c) => {
const body = await c.req.json().catch(() => ({}));
const day = typeof body?.day === "string" ? body.day : undefined;
const days = Number.isFinite(Number(body?.days)) ? Math.max(1, Math.min(31, Math.round(Number(body.days)))) : undefined;
return c.json(await rollupProductUsageDaily(c.env, { ...(day ? { day } : {}), ...(days === undefined ? {} : { days }) }));
});

app.post("/v1/internal/jobs/refresh-installation-health/run", async (c) => {
return c.json(await refreshInstallationHealth(c.env));
});
Expand Down
Loading