From 90a9846f8de29b5a274206303da83f0360e8a4b1 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:13:09 -0600 Subject: [PATCH] fix(analytics): redact MCP client telemetry buckets --- src/db/repositories.ts | 12 ++++-- test/integration/api.test.ts | 4 +- test/unit/mcp-server-telemetry.test.ts | 4 +- test/unit/product-usage-mcp-adoption.test.ts | 39 ++++++++++++++++++++ test/unit/product-usage.test.ts | 4 ++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index bfe3dcd735..33a5c34064 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -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(), }; @@ -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" { diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 46e8e1e424..782f62021c 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -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--cli", clientVersion: "0.4.0" }), expect.objectContaining({ surface: "mcp", eventName: "mcp_tool_called", outcome: "success", - clientName: "gittensory-mcp-cli", + clientName: "gittensory--cli", clientVersion: "0.4.0", metadata: expect.objectContaining({ toolName: "gittensory_get_bounty_advisory", diff --git a/test/unit/mcp-server-telemetry.test.ts b/test/unit/mcp-server-telemetry.test.ts index ba7f963553..4c1c5f41e5 100644 --- a/test/unit/mcp-server-telemetry.test.ts +++ b/test/unit/mcp-server-telemetry.test.ts @@ -53,7 +53,7 @@ describe("MCP server telemetry", () => { surface: "mcp", eventName: "mcp_tool_called", outcome: "error", - clientName: "gittensory-mcp-cli", + clientName: "gittensory--cli", clientVersion: "0.4.0", metadata: expect.objectContaining({ toolName: "gittensory_local_status", @@ -99,7 +99,7 @@ describe("MCP server telemetry", () => { surface: "mcp", eventName: "mcp_request", outcome: "success", - clientName: "mcp", + clientName: "", metadata: expect.objectContaining({ rpcMethod: "ping", compatibilityStatus: "unknown" }), }), ]); diff --git a/test/unit/product-usage-mcp-adoption.test.ts b/test/unit/product-usage-mcp-adoption.test.ts index c5ada65e56..5a84388840 100644 --- a/test/unit/product-usage-mcp-adoption.test.ts +++ b/test/unit/product-usage-mcp-adoption.test.ts @@ -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); + }); }); diff --git a/test/unit/product-usage.test.ts b/test/unit/product-usage.test.ts index 8e7e503d5d..c16df6d7c6 100644 --- a/test/unit/product-usage.test.ts +++ b/test/unit/product-usage.test.ts @@ -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" } }, }); @@ -43,6 +45,8 @@ describe("product usage events", () => { route: "/v1/app/commands/preview", repoFullName: "/private-tool", targetKey: ":private-tool#136", + clientName: "-mcp", + clientVersion: "0.3.0+", metadata: { command: "packet", viewer: "", nested: { note: "for " } }, }); expect(JSON.stringify(row)).not.toMatch(/Oktofeesh1|gts_session_secret/i);