From f52810bd59cbd50ce8e53c464ecaf3d3def9817a Mon Sep 17 00:00:00 2001 From: "Dominik K." Date: Thu, 4 Jun 2026 22:50:35 +0200 Subject: [PATCH] feat: track accept header dimension Co-authored-by: Cursor --- src/analytics.test.ts | 11 +++++++++-- src/analytics.ts | 1 + src/request.test.ts | 28 ++++++++++++++++++++++++++++ src/request.ts | 5 +++++ src/types.ts | 2 ++ 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/request.test.ts diff --git a/src/analytics.test.ts b/src/analytics.test.ts index 75998b7..f5aa73b 100644 --- a/src/analytics.test.ts +++ b/src/analytics.test.ts @@ -130,7 +130,11 @@ describe("ingestion", () => { test("track(Request) infers dimensions and writes the event", async () => { const req = new Request("https://upstash.com/blog", { - headers: { "user-agent": "PerplexityBot/1.0", referer: "https://www.perplexity.ai/" }, + headers: { + "user-agent": "PerplexityBot/1.0", + referer: "https://www.perplexity.ai/", + accept: "Text/Markdown, text/html;q=0.8", + }, }); expect(await analytics.track(req)).toBe(1); @@ -146,6 +150,7 @@ describe("ingestion", () => { ); expect(tracked).toBeDefined(); expect(String(tracked!.provider)).toBe("perplexity"); + expect(String(tracked!.accept)).toBe("text/markdown, text/html;q=0.8"); }); }); @@ -235,7 +240,7 @@ describe("querying without an index", () => { describe("getIndex schema reconciliation", () => { // The schema getIndex() should converge to, regardless of what existed before. - const EXPECTED_FIELDS = ["count", "hour", "path", "provider"]; + const EXPECTED_FIELDS = ["accept", "count", "hour", "path", "provider"]; /** A bare index handle for inspecting the server-side schema via describe(). */ function indexHandle(name: string) { @@ -244,6 +249,7 @@ describe("getIndex schema reconciliation", () => { schema: { count: { type: "U64" as const, fast: true as const }, hour: { type: "U64" as const, fast: true as const }, + accept: { type: "KEYWORD" as const }, provider: { type: "KEYWORD" as const }, path: { type: "KEYWORD" as const }, }, @@ -299,6 +305,7 @@ describe("getIndex schema reconciliation", () => { schema: { count: { type: "U64", fast: true }, hour: { type: "U64", fast: true }, + accept: { type: "KEYWORD" }, provider: { type: "KEYWORD" }, path: { type: "KEYWORD" }, sourceUrl: { type: "KEYWORD" }, diff --git a/src/analytics.ts b/src/analytics.ts index 763cea4..1ff363e 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -40,6 +40,7 @@ export class IndexNotFoundError extends Error { const EVENT_SCHEMA = { count: { type: "U64" as const, fast: true as const }, hour: { type: "U64" as const, fast: true as const }, + accept: { type: "KEYWORD" as const }, provider: { type: "KEYWORD" as const }, path: { type: "KEYWORD" as const }, }; diff --git a/src/request.test.ts b/src/request.test.ts new file mode 100644 index 0000000..e514ff8 --- /dev/null +++ b/src/request.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; + +import { eventFromRequest } from "./request.ts"; + +describe("eventFromRequest", () => { + test("captures the normalized Accept header", () => { + const event = eventFromRequest( + new Request("https://upstash.com/blog#section", { + headers: { + "user-agent": "ClaudeBot/1.0", + accept: " Text/Markdown, text/html;q=0.8 ", + }, + }), + ); + + expect(event).toEqual({ + provider: "claude", + path: "https://upstash.com/blog", + accept: "text/markdown, text/html;q=0.8", + }); + }); + + test("omits Accept when the header is missing", () => { + const event = eventFromRequest(new Request("https://upstash.com/docs")); + + expect(event.accept).toBeUndefined(); + }); +}); diff --git a/src/request.ts b/src/request.ts index 581cdc1..8b1f1de 100644 --- a/src/request.ts +++ b/src/request.ts @@ -15,6 +15,7 @@ export function eventFromRequest(req: Request): TrackedEvent { return { provider: detectProvider(request), path: normalizeUrl(request.nextUrl?.href ?? request.url), + accept: normalizeHeader(request.headers.get("accept")), }; } @@ -29,6 +30,10 @@ function normalizeUrl(url: string): string { } } +function normalizeHeader(value: string | null): string | undefined { + return value?.trim().toLowerCase() || undefined; +} + /** Infer the AI agent from the user-agent and referrer headers. */ export function detectProvider(req: Request): Provider { const userAgent = req.headers.get("user-agent")?.toLowerCase() ?? ""; diff --git a/src/types.ts b/src/types.ts index 3f616a5..744aa8e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,8 @@ export type TrackedEvent = { provider: Provider; /** The path on our site that was cited. */ path: string; + /** The request's Accept header, when present. */ + accept?: string; }; /** The dimensions that can be grouped/filtered on. */