From 6635fb345235bbe3f6d9ee5edfa425045e069035 Mon Sep 17 00:00:00 2001 From: Bruno Perez Date: Tue, 20 Oct 2026 09:23:46 +0200 Subject: [PATCH] chore: remove Vercel Web Analytics The site runs on Cloudflare Workers now, where /_vercel/insights does not exist: the page snippet 404'd on every view and the edge middleware never runs. Drops the snippet, the Vercel middleware and the server-side ingest; classifyEndpoint stays for the Worker's usage log line. --- CONTRIBUTING.md | 2 +- README.md | 2 +- middleware.ts | 22 ------ src/build/build.ts | 2 +- src/build/render.ts | 5 -- src/tracking/api-usage.ts | 121 +------------------------------ src/views/layout.ejs | 11 --- tests/api-usage.test.ts | 149 +------------------------------------- worker/index.ts | 9 +-- 9 files changed, 13 insertions(+), 310 deletions(-) delete mode 100644 middleware.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6e67022..b3cbf700 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,7 +195,7 @@ The website code lives under `src/`: - `src/client/` — browser-side TypeScript (search, filter, dark mode), Tailwind entry, and the vendored Outfit fonts under `fonts/` that social cards are rendered with. - `src/build/` — SSG pipeline (renders pages, compiles assets, emits JSON API, generates a social card per page). - `src/server/` — Express dev server. -- `src/tracking/` — API usage tracking; the root `middleware.ts` posts one Web Analytics custom event per JSON API request at the edge. +- `src/tracking/` — classifies API paths for the Worker's usage log line in Workers Logs. Everything the catalog ships beyond the static site: diff --git a/README.md b/README.md index bad52c24..2de89a11 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ curl https://modelparams.dev/api/v1/models/openai/gpt-5.5.json Schema at `https://modelparams.dev/api/v1/schema.json`, per the [Model Parameters convention](docs/model-parameters-schema.md). -API requests are counted at the edge and reported to Vercel Web Analytics as `api_request` custom events (endpoint, model, client type), so API usage lands in the same dashboard as page views. On Cloudflare Workers the Worker logs the same event as a JSON line in Workers Logs instead; see [`src/tracking/api-usage.ts`](src/tracking/api-usage.ts). +Requests that reach the Cloudflare Worker (MCP, validate) are logged as an `api_request` JSON line in Workers Logs (endpoint, model, auth mode); see [`src/tracking/api-usage.ts`](src/tracking/api-usage.ts). Static API files are served straight from Cloudflare's assets and are not logged. ### Validate a request diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 73d8d872..00000000 --- a/middleware.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { trackApiUsage } from "./src/tracking/api-usage.js"; - -/** - * Vercel Edge Middleware: counts requests to the JSON API, the MCP server, - * and the llms.txt companion files as Web Analytics custom events. Middleware - * runs before the CDN cache and before rewrites, so /mcp needs its own matcher - * entry even though it rewrites to /api/mcp. What gets recorded is documented - * in src/tracking/api-usage.ts. - */ -export const config = { - matcher: ["/api/:path*", "/mcp", "/llms.txt", "/llms-full.txt"], -}; - -interface MiddlewareContext { - waitUntil(promise: Promise): void; -} - -export default function middleware(request: Request, context: MiddlewareContext): void { - const pending = trackApiUsage(request); - if (pending) context.waitUntil(pending); - // Returning nothing lets the request fall through to the static files. -} diff --git a/src/build/build.ts b/src/build/build.ts index 1389ad58..0eea4a9f 100644 --- a/src/build/build.ts +++ b/src/build/build.ts @@ -206,7 +206,7 @@ export async function build(): Promise<{ models: number }> { const catalog = buildCatalog(models); const capabilities = buildCapabilityFacets(models); console.log(`Rendering HTML for ${models.length} model(s)...`); - const html = await renderIndex({ catalog, capabilities, analytics: true }); + const html = await renderIndex({ catalog, capabilities }); await fs.writeFile(path.join(DIST_DIR, "index.html"), html, "utf8"); console.log("Writing JSON API..."); diff --git a/src/build/render.ts b/src/build/render.ts index 2b51926a..0f5df9d9 100644 --- a/src/build/render.ts +++ b/src/build/render.ts @@ -94,7 +94,6 @@ export interface ShellMeta { /** Robots directive; omitted (index, follow) for every indexable page. */ robots?: string; initialThemeClass?: string; - analytics?: boolean; } /** Wrap a rendered body in the shared HTML layout (head, header, footer, modal). */ @@ -111,7 +110,6 @@ export async function renderShell(meta: ShellMeta, body: string): Promise { ), providerHubs: hubLinks(opts.catalog.models), initialThemeClass: opts.initialThemeClass, - analytics: opts.analytics, }, body, ); diff --git a/src/tracking/api-usage.ts b/src/tracking/api-usage.ts index 3bb850b4..cf08f6fd 100644 --- a/src/tracking/api-usage.ts +++ b/src/tracking/api-usage.ts @@ -1,32 +1,9 @@ /** - * Server-side usage tracking for the JSON API, reported to Vercel Web - * Analytics so API traffic shows up in the same dashboard as page views. - * - * The API is static files on Vercel's CDN, so the Web Analytics - <% if (analytics) { %> - - - - <% } %> diff --git a/tests/api-usage.test.ts b/tests/api-usage.test.ts index 7e10d106..d397efba 100644 --- a/tests/api-usage.test.ts +++ b/tests/api-usage.test.ts @@ -1,11 +1,5 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - classifyClient, - classifyEndpoint, - eventProperties, - ingestUrl, - trackApiUsage, -} from "../src/tracking/api-usage.js"; +import { describe, expect, it } from "vitest"; +import { classifyEndpoint } from "../src/tracking/api-usage.js"; describe("classifyEndpoint", () => { it("recognises the fixed endpoints", () => { @@ -62,142 +56,3 @@ describe("classifyEndpoint", () => { expect(classifyEndpoint("/").endpoint).toBe("other"); }); }); - -describe("classifyClient", () => { - it.each([ - ["curl/8.6.0", "curl"], - ["Wget/1.21", "wget"], - ["python-requests/2.32.0", "python"], - ["python-httpx/0.27.0", "python"], - ["node-fetch/3.3.2", "node"], - ["undici", "node"], - ["axios/1.7.2", "node"], - ["Go-http-client/2.0", "go"], - ["Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)", "bot"], - ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/126.0 Safari/537.36", "browser"], - ["SomethingElse/1.0", "other"], - ])("classifies %s as %s", (userAgent, expected) => { - expect(classifyClient(userAgent)).toBe(expected); - }); - - it("treats a missing user agent as unknown", () => { - expect(classifyClient(null)).toBe("unknown"); - expect(classifyClient("")).toBe("unknown"); - }); -}); - -describe("eventProperties", () => { - it("describes a model params request", () => { - const request = new Request("https://modelparams.dev/api/v1/params/gpt-5.5.json", { - headers: { "user-agent": "python-requests/2.32.0" }, - }); - expect(eventProperties(request)).toEqual({ - path: "/api/v1/params/gpt-5.5.json", - method: "GET", - endpoint: "params_by_model", - client: "python", - auth_mode: "api_key", - model: "gpt-5.5", - }); - }); - - it("omits absent values instead of sending null", () => { - const request = new Request("https://modelparams.dev/api/v1/models.json"); - expect(eventProperties(request)).toEqual({ - path: "/api/v1/models.json", - method: "GET", - endpoint: "catalog", - client: "unknown", - }); - }); -}); - -describe("ingestUrl", () => { - it("builds the endpoint from VERCEL_URL and is off without it", () => { - expect(ingestUrl({ VERCEL_URL: "modelparams-abc123.vercel.app" })).toBe( - "https://modelparams-abc123.vercel.app/_vercel/insights/event", - ); - expect(ingestUrl({})).toBe(null); - }); - - it("uses a full VERCEL_WEB_ANALYTICS_ENDPOINT override verbatim", () => { - expect( - ingestUrl({ - VERCEL_WEB_ANALYTICS_ENDPOINT: "https://example.com/ingest", - VERCEL_URL: "ignored.vercel.app", - }), - ).toBe("https://example.com/ingest"); - }); -}); - -describe("trackApiUsage", () => { - afterEach(() => { - vi.unstubAllGlobals(); - }); - - const env = { VERCEL_URL: "modelparams-abc123.vercel.app" }; - const request = (method = "GET") => - new Request("https://modelparams.dev/api/v1/models.json", { - method, - headers: { - "user-agent": "curl/8.6.0", - "x-forwarded-for": "203.0.113.7", - }, - }); - - it("is a no-op off Vercel and for CORS preflights", () => { - const fetchMock = vi.fn(); - vi.stubGlobal("fetch", fetchMock); - expect(trackApiUsage(request(), {})).toBe(null); - expect(trackApiUsage(request("OPTIONS"), env)).toBe(null); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("posts the @vercel/analytics wire format to the ingest route", async () => { - const fetchMock = vi.fn().mockResolvedValue(new Response("ok")); - vi.stubGlobal("fetch", fetchMock); - - await trackApiUsage(request(), env); - - expect(fetchMock).toHaveBeenCalledTimes(1); - const [url, init] = fetchMock.mock.calls[0] as [ - string, - { method: string; headers: Record; body: string }, - ]; - expect(url).toBe("https://modelparams-abc123.vercel.app/_vercel/insights/event"); - expect(init.method).toBe("POST"); - expect(init.headers).toMatchObject({ - "content-type": "application/json", - "x-va-server": "1", - "user-agent": "curl/8.6.0", - "x-vercel-ip": "203.0.113.7", - }); - expect(init.headers).not.toHaveProperty("cookie"); - - const body = JSON.parse(init.body) as { - en: string; - o: string; - sdkn: string; - ed: Record; - }; - expect(body.en).toBe("api_request"); - expect(body.o).toBe("https://modelparams.dev/api/v1/models.json"); - expect(body.sdkn).toBe("@vercel/analytics/server"); - expect(body.ed).toMatchObject({ endpoint: "catalog", client: "curl" }); - }); - - it("sends the protection bypass secret when configured", async () => { - const fetchMock = vi.fn().mockResolvedValue(new Response("ok")); - vi.stubGlobal("fetch", fetchMock); - - await trackApiUsage(request(), { ...env, VERCEL_AUTOMATION_BYPASS_SECRET: "shh" }); - - const [, init] = fetchMock.mock.calls[0] as [string, { headers: Record }]; - expect(init.headers["x-vercel-protection-bypass"]).toBe("shh"); - }); - - it("swallows network failures", async () => { - vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("offline"))); - await expect(trackApiUsage(request(), env)).resolves.toBe(undefined); - }); -}); diff --git a/worker/index.ts b/worker/index.ts index ecc65bd3..6249bd6b 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -1,12 +1,11 @@ // Cloudflare Worker for modelparams.dev. Serves the static build in dist/ and -// the two dynamic endpoints, and carries what vercel.json and middleware.ts -// did on Vercel: +// the two dynamic endpoints, and carries what vercel.json did on Vercel: // // - modelparameters.dev and the www hosts redirect (308) to modelparams.dev // - /mcp and /api/mcp → api/mcp.ts; /api/v1/validate → api/v1/validate.ts // - the response headers vercel.json set on /api/*, /api/v1/* and llms*.txt // - clean URLs: the asset layer's 307s become 308s, as on Vercel -// - API usage is logged (Workers Logs) instead of sent to Vercel Analytics +// - API usage is logged to Workers Logs import mcp from "../api/mcp.js"; import validate from "../api/v1/validate.js"; import { classifyEndpoint } from "../src/tracking/api-usage.js"; @@ -30,7 +29,7 @@ const ROUTES: Record = { "/api/v1/validate": validate.fetch, }; -/** Paths the Vercel middleware counted: the JSON API, MCP and llms*.txt. */ +/** Paths worth a usage log line: the JSON API, MCP and llms*.txt. */ function isTracked(pathname: string): boolean { return ( pathname.startsWith("/api/") || @@ -40,7 +39,7 @@ function isTracked(pathname: string): boolean { ); } -/** Stand-in for Vercel Analytics custom events: one structured log line. */ +/** One structured usage log line per tracked request. */ function logApiUsage(request: Request, url: URL): void { if (!isTracked(url.pathname)) return; console.log(