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
2 changes: 1 addition & 1 deletion apps/gittensory-ui/src/lib/mcp-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const MCP_PACKAGE_NAME = "@jsonbored/gittensory-mcp";
export const MCP_PACKAGE_ENCODED_NAME = "@jsonbored%2fgittensory-mcp";
export const MCP_PACKAGE_REGISTRY_URL = `https://registry.npmjs.org/${MCP_PACKAGE_ENCODED_NAME}`;
export const MCP_PACKAGE_NPM_URL = `https://www.npmjs.com/package/${MCP_PACKAGE_NAME}`;
export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.3.0";
export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.4.0";
export const MCP_MINIMUM_SUPPORTED_VERSION = "0.2.0";

export type NpmPackageMetadata = {
Expand Down
11 changes: 11 additions & 0 deletions migrations/0017_product_usage_role_retention_rollups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ALTER TABLE product_usage_daily_rollups
ADD COLUMN roles_json TEXT NOT NULL DEFAULT '[]';

ALTER TABLE product_usage_daily_rollups
ADD COLUMN activation_by_role_json TEXT NOT NULL DEFAULT '[]';

ALTER TABLE product_usage_daily_rollups
ADD COLUMN activation_by_surface_json TEXT NOT NULL DEFAULT '[]';

ALTER TABLE product_usage_daily_rollups
ADD COLUMN retention_json TEXT NOT NULL DEFAULT '[]';
269 changes: 253 additions & 16 deletions src/db/repositories.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,10 @@ export const productUsageDailyRollups = sqliteTable(
toolsJson: text("tools_json").notNull().default("[]"),
routeClassesJson: text("route_classes_json").notNull().default("[]"),
activationJson: text("activation_json").notNull().default("{}"),
rolesJson: text("roles_json").notNull().default("[]"),
activationByRoleJson: text("activation_by_role_json").notNull().default("[]"),
activationBySurfaceJson: text("activation_by_surface_json").notNull().default("[]"),
retentionJson: text("retention_json").notNull().default("[]"),
generatedAt: text("generated_at").notNull().default("CURRENT_TIMESTAMP"),
updatedAt: text("updated_at").notNull().default("CURRENT_TIMESTAMP"),
},
Expand Down
44 changes: 44 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,8 @@ export type ProductUsageSurface = "api" | "mcp" | "github_app" | "control_panel"

export type ProductUsageOutcome = "success" | "denied" | "error" | "queued" | "completed" | "skipped";

export type ProductUsageRole = "miner" | "maintainer" | "owner" | "operator" | "contributor" | "unknown";

export type ProductUsageEventRecord = {
id: string;
surface: ProductUsageSurface;
Expand Down Expand Up @@ -1020,6 +1022,13 @@ export type ProductUsageDimensionCount = {
count: number;
};

export type ProductUsageRoleDimensionCount = {
role: ProductUsageRole;
count: number;
activeActors: number;
activeRepos: number;
};

export type ProductUsageActivationFunnel = {
loginActors: number;
doctorPassActors: number;
Expand All @@ -1031,6 +1040,37 @@ export type ProductUsageActivationFunnel = {
githubActivatedRepos: number;
};

export type ProductUsageRoleActivationFunnel = ProductUsageActivationFunnel & {
role: ProductUsageRole;
};

export type ProductUsageSurfaceActivationFunnel = ProductUsageActivationFunnel & {
surface: ProductUsageSurface;
};

export type ProductUsageRetentionWindow = "previous_7_days" | "previous_30_days";

export type ProductUsageRetentionDimension = {
activeActors: number;
retainedActors: number;
retentionRate: number;
};

export type ProductUsageRoleRetention = ProductUsageRetentionDimension & {
role: ProductUsageRole;
};

export type ProductUsageSurfaceRetention = ProductUsageRetentionDimension & {
surface: ProductUsageSurface;
};

export type ProductUsageRetentionRollup = ProductUsageRetentionDimension & {
window: ProductUsageRetentionWindow;
capped: boolean;
byRole: ProductUsageRoleRetention[];
bySurface: ProductUsageSurfaceRetention[];
};

export type ProductUsageDailyRollupRecord = {
day: string;
status: ProductUsageDailyRollupStatus;
Expand All @@ -1050,6 +1090,10 @@ export type ProductUsageDailyRollupRecord = {
byTool: ProductUsageDimensionCount[];
byRouteClass: ProductUsageDimensionCount[];
activation: ProductUsageActivationFunnel;
byRole: ProductUsageRoleDimensionCount[];
activationByRole: ProductUsageRoleActivationFunnel[];
activationBySurface: ProductUsageSurfaceActivationFunnel[];
retention: ProductUsageRetentionRollup[];
generatedAt: string;
updatedAt: string;
};
Expand Down
43 changes: 40 additions & 3 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2399,10 +2399,47 @@ describe("api routes", () => {

const dailyRollups = await app.request("/v1/app/analytics/daily-rollups?limit=3", { headers: apiHeaders(env) }, env);
expect(dailyRollups.status).toBe(200);
await expect(dailyRollups.json()).resolves.toMatchObject({
const dailyRollupsBody = (await dailyRollups.json()) as {
status: { status: string; latestRollupDay?: string };
rollups: Array<{
day: string;
activation: Record<string, number>;
byRole: Array<{ role: string; count: number; activeActors: number; activeRepos: number }>;
activationByRole: Array<Record<string, number | string>>;
activationBySurface: Array<Record<string, number | string>>;
retention: Array<{
window: string;
activeActors: number;
retainedActors: number;
retentionRate: number;
capped: boolean;
byRole: Array<Record<string, number | string>>;
bySurface: Array<Record<string, number | string>>;
}>;
}>;
};
expect(dailyRollupsBody).toMatchObject({
status: expect.objectContaining({ status: "partial", latestRollupDay: "2026-05-28" }),
rollups: [expect.objectContaining({ day: "2026-05-28", activation: expect.any(Object) })],
});
const [dailyRollup] = dailyRollupsBody.rollups;
expect(dailyRollup).toBeDefined();
if (!dailyRollup) throw new Error("expected daily usage rollup");
expect(dailyRollup.byRole).toEqual(expect.arrayContaining([expect.objectContaining({ role: "miner", count: expect.any(Number), activeActors: expect.any(Number), activeRepos: expect.any(Number) })]));
expect(dailyRollup.activationByRole).toEqual(expect.arrayContaining([expect.objectContaining({ role: "miner", doctorPassActors: expect.any(Number), firstUsefulActionActors: expect.any(Number) })]));
expect(dailyRollup.activationBySurface).toEqual(
expect.arrayContaining([
expect.objectContaining({ surface: "mcp", doctorPassActors: expect.any(Number) }),
expect.objectContaining({ surface: "browser_extension", firstUsefulActionActors: expect.any(Number) }),
]),
);
expect(dailyRollup.retention).toEqual(
expect.arrayContaining([
expect.objectContaining({ window: "previous_7_days", activeActors: expect.any(Number), retainedActors: expect.any(Number), retentionRate: expect.any(Number), capped: false, byRole: expect.any(Array), bySurface: expect.any(Array) }),
expect.objectContaining({ window: "previous_30_days", activeActors: expect.any(Number), retainedActors: expect.any(Number), retentionRate: expect.any(Number), capped: false, byRole: expect.any(Array), bySurface: expect.any(Array) }),
]),
);
expect(JSON.stringify(dailyRollupsBody)).not.toMatch(/oktofeesh1|operator@example.com|mcp-user|old-mcp-user|current-mcp-user|mcp-session|old-mcp-session|current-mcp-session|gittensory_session|\/Users|github_pat|ghp_|private-repo|wallet|hotkey|raw trust/i);
const fallbackLimitRollups = await app.request("/v1/app/analytics/daily-rollups?limit=invalid", { headers: apiHeaders(env) }, env);
expect(fallbackLimitRollups.status).toBe(200);
await expect(fallbackLimitRollups.json()).resolves.toMatchObject({
Expand All @@ -2421,7 +2458,7 @@ describe("api routes", () => {
const usageOperatorBody = (await usageOperator.json()) as {
metrics: Array<{ label: string; value: string }>;
usageSummary: { totalEvents: number };
usageRollups: Array<{ day: string }>;
usageRollups: Array<{ day: string; byRole: unknown[]; activationBySurface: unknown[]; retention: unknown[] }>;
usageRollupStatus: { status: string };
mcpCompatibilityAdoption: {
totalEvents: number;
Expand Down Expand Up @@ -2449,7 +2486,7 @@ describe("api routes", () => {
]),
);
expect(usageOperatorBody.usageSummary.totalEvents).toBe(productUsageEvents.length);
expect(usageOperatorBody.usageRollups).toEqual([expect.objectContaining({ day: "2026-05-28" })]);
expect(usageOperatorBody.usageRollups).toEqual([expect.objectContaining({ day: "2026-05-28", byRole: expect.any(Array), activationBySurface: expect.any(Array), retention: expect.any(Array) })]);
expect(usageOperatorBody.usageRollupStatus.status).toBe("partial");
expect(usageOperatorBody.mcpCompatibilityAdoption).toMatchObject({
totalEvents: 4,
Expand Down
Loading