From 36d0c628405ff9b9f4a3c4ca7350dd24e25e56ca Mon Sep 17 00:00:00 2001 From: condyl Date: Sat, 5 Sep 2026 12:16:43 -0400 Subject: [PATCH] feat: track WebMCP usage --- src/lib/metrics.js | 37 +++++++++++++++++++ .../__tests__/useScheduleWebMcp.test.jsx | 16 ++++++++ src/lib/webmcp/useScheduleWebMcp.jsx | 23 +++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/lib/metrics.js b/src/lib/metrics.js index b6e6e09..c71e37e 100644 --- a/src/lib/metrics.js +++ b/src/lib/metrics.js @@ -4,6 +4,7 @@ const GA_MEASUREMENT_ID = "G-M2NP1M6YSK"; const isAnalyticsEnabled = import.meta.env.PROD; let initialized = false; +let webMcpAvailabilityTracked = false; const cleanParams = (params) => Object.fromEntries( @@ -115,3 +116,39 @@ export const trackTruncationWarning = () => { has_results: true, }); }; + +export const trackWebMcpAvailable = () => { + if (!isAnalyticsEnabled || webMcpAvailabilityTracked) return; + + initializeAnalytics(); + webMcpAvailabilityTracked = true; + ReactGA.event("webmcp_available"); +}; + +export const trackWebMcpToolCompleted = ({ + toolName, + durationMs, + resultCount, +}) => { + if (!isAnalyticsEnabled) return; + + initializeAnalytics(); + ReactGA.event( + "webmcp_tool_completed", + cleanParams({ + tool_name: toolName, + duration_ms: durationMs, + result_count: resultCount, + }), + ); +}; + +export const trackWebMcpToolFailed = ({ toolName, durationMs }) => { + if (!isAnalyticsEnabled) return; + + initializeAnalytics(); + ReactGA.event("webmcp_tool_failed", { + tool_name: toolName, + duration_ms: durationMs, + }); +}; diff --git a/src/lib/webmcp/__tests__/useScheduleWebMcp.test.jsx b/src/lib/webmcp/__tests__/useScheduleWebMcp.test.jsx index 10db4b1..32bc4de 100644 --- a/src/lib/webmcp/__tests__/useScheduleWebMcp.test.jsx +++ b/src/lib/webmcp/__tests__/useScheduleWebMcp.test.jsx @@ -2,6 +2,13 @@ import { render, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useScheduleWebMcp } from "../useScheduleWebMcp"; +import { trackWebMcpAvailable, trackWebMcpToolCompleted } from "@/lib/metrics"; + +vi.mock("@/lib/metrics", () => ({ + trackWebMcpAvailable: vi.fn(), + trackWebMcpToolCompleted: vi.fn(), + trackWebMcpToolFailed: vi.fn(), +})); const setters = { setTimetables: vi.fn(), @@ -42,6 +49,7 @@ describe("useScheduleWebMcp", () => { const { unmount } = render(); await waitFor(() => expect(registerTool).toHaveBeenCalledTimes(15)); + expect(trackWebMcpAvailable).toHaveBeenCalledTimes(1); expect( registerTool.mock.calls.map(([definition]) => definition.name), @@ -63,6 +71,14 @@ describe("useScheduleWebMcp", () => { "exportSchedule", ]); + const currentScheduleTool = registerTool.mock.calls + .map(([definition]) => definition) + .find(({ name }) => name === "getCurrentSchedule"); + await currentScheduleTool.execute({}); + expect(trackWebMcpToolCompleted).toHaveBeenCalledWith( + expect.objectContaining({ toolName: "getCurrentSchedule" }), + ); + unmount(); expect( registerTool.mock.calls.every(([, options]) => options.signal.aborted), diff --git a/src/lib/webmcp/useScheduleWebMcp.jsx b/src/lib/webmcp/useScheduleWebMcp.jsx index 4e852b6..fe801c6 100644 --- a/src/lib/webmcp/useScheduleWebMcp.jsx +++ b/src/lib/webmcp/useScheduleWebMcp.jsx @@ -46,9 +46,18 @@ import { rankScheduleOptions, toTimeSlots, } from "./scheduleOptions"; +import { + trackWebMcpAvailable, + trackWebMcpToolCompleted, + trackWebMcpToolFailed, +} from "@/lib/metrics"; const json = (value) => JSON.stringify(value); const asArray = (value) => (Array.isArray(value) ? value : []); +const resultCount = (result) => + [result.totalOptions, result.totalMatches, result.conflictFreeOptions].find( + Number.isFinite, + ); const tool = (name, description, inputSchema, execute, annotations = {}) => ({ name, @@ -56,9 +65,20 @@ const tool = (name, description, inputSchema, execute, annotations = {}) => ({ inputSchema, annotations, execute: async (input, context) => { + const startedAt = performance.now(); try { - return json({ ok: true, ...(await execute(input || {}, context || {})) }); + const result = await execute(input || {}, context || {}); + trackWebMcpToolCompleted({ + toolName: name, + durationMs: Math.round(performance.now() - startedAt), + resultCount: resultCount(result), + }); + return json({ ok: true, ...result }); } catch (error) { + trackWebMcpToolFailed({ + toolName: name, + durationMs: Math.round(performance.now() - startedAt), + }); return json({ ok: false, error: @@ -380,6 +400,7 @@ export function useScheduleWebMcp({ useEffect(() => { const modelContext = document.modelContext; if (!modelContext?.registerTool) return undefined; + trackWebMcpAvailable(); const tools = [ tool(