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
12 changes: 9 additions & 3 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1145,8 +1145,8 @@ export async function recordProductUsageEvent(
targetKey: redactProductUsageActor(boundedProductUsageField(event.targetKey, 256), actorRedactor),
outcome: normalizeProductUsageOutcome(event.outcome),
latencyMs: normalizeProductUsageLatency(event.latencyMs),
clientName: boundedProductUsageField(event.clientName, 80),
clientVersion: boundedProductUsageField(event.clientVersion, 80),
clientName: redactProductUsageActor(boundedProductUsageField(event.clientName, 80), actorRedactor),
clientVersion: redactProductUsageActor(boundedProductUsageField(event.clientVersion, 80), actorRedactor),
metadata: sanitizedMetadata,
occurredAt: event.occurredAt ?? nowIso(),
};
Expand Down Expand Up @@ -4063,7 +4063,13 @@ function isProductUsageUsefulMaintainerEvent(event: ProductUsageEventRecord): bo
}

function mcpClientVersionForEvent(event: ProductUsageEventRecord): string {
return event.clientVersion ?? productUsageMetadataString(event, "packageVersion") ?? "unknown";
return aggregateMcpClientVersion(event.clientVersion ?? productUsageMetadataString(event, "packageVersion"));
}

function aggregateMcpClientVersion(version: string | null | undefined): string {
if (!version) return "unknown";
const semver = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/.exec(version.trim());
return semver?.[1] ?? "unknown";
}

function mcpCompatibilityStatusForEvent(event: ProductUsageEventRecord): "current" | "stale" | "incompatible" | "unknown" {
Expand Down
4 changes: 2 additions & 2 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4912,12 +4912,12 @@ describe("api routes", () => {
const mcpUsageEvents = await listProductUsageEvents(env, { limit: 100 });
expect(mcpUsageEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({ surface: "mcp", eventName: "mcp_request", outcome: "success", clientName: "gittensory-mcp-cli", clientVersion: "0.4.0" }),
expect.objectContaining({ surface: "mcp", eventName: "mcp_request", outcome: "success", clientName: "gittensory-<redacted-actor>-cli", clientVersion: "0.4.0" }),
expect.objectContaining({
surface: "mcp",
eventName: "mcp_tool_called",
outcome: "success",
clientName: "gittensory-mcp-cli",
clientName: "gittensory-<redacted-actor>-cli",
clientVersion: "0.4.0",
metadata: expect.objectContaining({
toolName: "gittensory_get_bounty_advisory",
Expand Down
4 changes: 2 additions & 2 deletions test/unit/mcp-server-telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("MCP server telemetry", () => {
surface: "mcp",
eventName: "mcp_tool_called",
outcome: "error",
clientName: "gittensory-mcp-cli",
clientName: "gittensory-<redacted-actor>-cli",
clientVersion: "0.4.0",
metadata: expect.objectContaining({
toolName: "gittensory_local_status",
Expand Down Expand Up @@ -99,7 +99,7 @@ describe("MCP server telemetry", () => {
surface: "mcp",
eventName: "mcp_request",
outcome: "success",
clientName: "mcp",
clientName: "<redacted-actor>",
metadata: expect.objectContaining({ rpcMethod: "ping", compatibilityStatus: "unknown" }),
}),
]);
Expand Down
39 changes: 39 additions & 0 deletions test/unit/product-usage-mcp-adoption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,43 @@ describe("MCP compatibility adoption summaries", () => {
truncated: true,
});
});

it("canonicalizes client version buckets before returning aggregate analytics", async () => {
const env = createTestEnv({ PRODUCT_USAGE_HASH_SALT: "mcp-adoption-test-salt" });

await recordProductUsageEvent(env, {
surface: "mcp",
eventName: "mcp_request",
actor: "alice",
clientName: "gittensory-mcp",
clientVersion: "0.3.0+alice",
metadata: { packageVersion: "0.3.0+alice" },
occurredAt: "2026-05-28T00:03:00.000Z",
});
await recordProductUsageEvent(env, {
surface: "mcp",
eventName: "mcp_request",
actor: "bob",
clientName: "gittensory-mcp",
clientVersion: "v0.4.0-beta.1+bob",
metadata: { packageVersion: "v0.4.0-beta.1+bob" },
occurredAt: "2026-05-28T00:04:00.000Z",
});
await recordProductUsageEvent(env, {
surface: "mcp",
eventName: "mcp_request",
actor: "carol",
clientName: "gittensory-mcp",
clientVersion: "canary+carol",
metadata: { packageVersion: "canary+carol" },
occurredAt: "2026-05-28T00:05:00.000Z",
});

const summary = await summarizeMcpCompatibilityAdoption(env);

expect(summary.byClientVersion).toContainEqual({ key: "0.3.0", count: 1 });
expect(summary.byClientVersion).toContainEqual({ key: "0.4.0-beta.1", count: 1 });
expect(summary.byClientVersion).toContainEqual({ key: "unknown", count: 1 });
expect(JSON.stringify(summary)).not.toMatch(/alice|bob|carol/i);
});
});
4 changes: 4 additions & 0 deletions test/unit/product-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ describe("product usage events", () => {
repoFullName: "oktofeesh1/private-tool",
targetKey: "Oktofeesh1:private-tool#136",
outcome: "success",
clientName: "Oktofeesh1-mcp",
clientVersion: "0.3.0+Oktofeesh1",
metadata: { command: "packet", viewer: "Oktofeesh1", nested: { note: "for oktofeesh1" } },
});

Expand All @@ -43,6 +45,8 @@ describe("product usage events", () => {
route: "/v1/app/commands/preview",
repoFullName: "<redacted-actor>/private-tool",
targetKey: "<redacted-actor>:private-tool#136",
clientName: "<redacted-actor>-mcp",
clientVersion: "0.3.0+<redacted-actor>",
metadata: { command: "packet", viewer: "<redacted-actor>", nested: { note: "for <redacted-actor>" } },
});
expect(JSON.stringify(row)).not.toMatch(/Oktofeesh1|gts_session_secret/i);
Expand Down