From 64d369a16040d65f1ad3f1061561ec982503943d Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Sun, 31 May 2026 02:33:36 +0300 Subject: [PATCH 1/6] fix: analytics harness tests and prefix catch --- packages/docs/src/analytics.test.ts | 65 ++++++++++++++++++++++++ packages/docs/src/cloud-analytics.ts | 74 +++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/packages/docs/src/analytics.test.ts b/packages/docs/src/analytics.test.ts index df02f06c..05886a16 100644 --- a/packages/docs/src/analytics.test.ts +++ b/packages/docs/src/analytics.test.ts @@ -343,6 +343,71 @@ describe("analytics", () => { ); }); + it("adds agent traffic hints to Docs Cloud agent and MCP events", async () => { + process.env.NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID = "project_agents"; + + const fetchMock = vi.fn<(input: string, init?: RequestInit) => Promise>( + async () => new Response(null, { status: 202 }), + ); + vi.stubGlobal("fetch", fetchMock); + + await emitDocsAnalyticsEvent( + { + enabled: true, + console: false, + }, + { + type: "agent_read", + source: "server", + path: "/docs/install.md", + properties: { + delivery: "markdown_route", + }, + }, + ); + + await emitDocsAnalyticsEvent( + { + enabled: true, + console: false, + }, + { + type: "mcp_tool", + source: "mcp", + path: "/docs/install", + properties: { + tool: "read_page", + }, + }, + ); + + const requestBodies = fetchMock.mock.calls.map((call) => JSON.parse(String(call[1]?.body))); + expect(requestBodies[0]).toMatchObject({ + event: { + type: "agent_read", + source: "server", + properties: { + delivery: "markdown_route", + trafficType: "agent", + agentName: "Docs agent", + botProvider: "Docs agent", + }, + }, + }); + expect(requestBodies[1]).toMatchObject({ + event: { + type: "mcp_tool", + source: "mcp", + properties: { + tool: "read_page", + trafficType: "agent", + agentName: "MCP client", + botProvider: "MCP client", + }, + }, + }); + }); + it("posts Docs Cloud analytics from plain config when public env is present", async () => { process.env.NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID = "project_public"; process.env.NEXT_PUBLIC_DOCS_CLOUD_ANALYTICS_ENDPOINT = diff --git a/packages/docs/src/cloud-analytics.ts b/packages/docs/src/cloud-analytics.ts index cee2a6b1..167602da 100644 --- a/packages/docs/src/cloud-analytics.ts +++ b/packages/docs/src/cloud-analytics.ts @@ -87,6 +87,77 @@ export function resolveDocsCloudAnalyticsOptions( }; } +function asRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +} + +function asString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; +} + +function normalizeAnalyticsLabel(value: string | undefined) { + return value?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? ""; +} + +function isAgentAnalyticsEvent(event: DocsAnalyticsEvent) { + const type = normalizeAnalyticsLabel(event.type); + const source = normalizeAnalyticsLabel(event.source); + + return ( + source === "mcp" || + type.startsWith("mcp_") || + type.startsWith("agent_") || + ["agents_request", "llms_request", "markdown_request", "skill_request"].includes(type) + ); +} + +function inferAgentProvider(event: DocsAnalyticsEvent) { + const type = normalizeAnalyticsLabel(event.type); + const source = normalizeAnalyticsLabel(event.source); + + if (source === "mcp" || type.startsWith("mcp_")) { + return "MCP client"; + } + + if (type.startsWith("agent_") || type === "agents_request") { + return "Docs agent"; + } + + if (["llms_request", "markdown_request", "skill_request"].includes(type)) { + return "Docs reader"; + } + + return undefined; +} + +function withDocsCloudAnalyticsHints(event: DocsAnalyticsEvent): DocsAnalyticsEvent { + if (!isAgentAnalyticsEvent(event)) { + return event; + } + + const properties = asRecord(event.properties); + const agentProvider = + asString(properties.agentName) ?? + asString(properties.agent) ?? + asString(properties.botProvider) ?? + asString(properties.provider) ?? + asString(properties.crawler) ?? + asString(asRecord(properties.bot).provider) ?? + inferAgentProvider(event); + + return { + ...event, + properties: { + ...properties, + trafficType: "agent", + ...(agentProvider && !properties.agentName ? { agentName: agentProvider } : {}), + ...(agentProvider && !properties.botProvider ? { botProvider: agentProvider } : {}), + }, + }; +} + export async function sendDocsCloudAnalyticsEvent( options: DocsCloudAnalyticsOptions, event: DocsAnalyticsEvent, @@ -102,6 +173,7 @@ export async function sendDocsCloudAnalyticsEvent( } try { + const normalizedEvent = withDocsCloudAnalyticsHints(event); await fetch(endpoint, { method: "POST", headers: { @@ -114,7 +186,7 @@ export async function sendDocsCloudAnalyticsEvent( }, body: JSON.stringify({ projectId, - event, + event: normalizedEvent, }), keepalive: true, }); From 8d33dfeb3841f24a5597d04c5924bf9c230dfe01 Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Sun, 31 May 2026 02:33:50 +0300 Subject: [PATCH 2/6] chore: format --- packages/docs/src/cloud-analytics.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/docs/src/cloud-analytics.ts b/packages/docs/src/cloud-analytics.ts index 167602da..6c1d1762 100644 --- a/packages/docs/src/cloud-analytics.ts +++ b/packages/docs/src/cloud-analytics.ts @@ -98,7 +98,12 @@ function asString(value: unknown): string | undefined { } function normalizeAnalyticsLabel(value: string | undefined) { - return value?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? ""; + return ( + value + ?.trim() + .toLowerCase() + .replace(/[-\s]+/g, "_") ?? "" + ); } function isAgentAnalyticsEvent(event: DocsAnalyticsEvent) { From fa7f93d8b466350a6b5919904f9cf6efc59754f6 Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Sun, 31 May 2026 02:50:31 +0300 Subject: [PATCH 3/6] fix: header based harness --- packages/docs/src/analytics.test.ts | 35 ++++++++++++-- packages/docs/src/analytics.ts | 8 ++++ packages/docs/src/cloud-analytics.ts | 63 +++++++++++++++++++++----- packages/docs/src/index.ts | 1 + packages/docs/src/server.ts | 1 + packages/fumadocs/src/docs-api.test.ts | 5 ++ packages/fumadocs/src/docs-api.ts | 34 ++++++++++++++ 7 files changed, 133 insertions(+), 14 deletions(-) diff --git a/packages/docs/src/analytics.test.ts b/packages/docs/src/analytics.test.ts index 05886a16..077638ec 100644 --- a/packages/docs/src/analytics.test.ts +++ b/packages/docs/src/analytics.test.ts @@ -343,7 +343,7 @@ describe("analytics", () => { ); }); - it("adds agent traffic hints to Docs Cloud agent and MCP events", async () => { + it("adds agent traffic hints to Docs Cloud events from caller identity", async () => { process.env.NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID = "project_agents"; const fetchMock = vi.fn<(input: string, init?: RequestInit) => Promise>( @@ -362,6 +362,23 @@ describe("analytics", () => { path: "/docs/install.md", properties: { delivery: "markdown_route", + userAgent: "ChatGPT-User/1.0", + }, + }, + ); + + await emitDocsAnalyticsEvent( + { + enabled: true, + console: false, + }, + { + type: "agent_read", + source: "server", + path: "/docs/install.md", + properties: { + delivery: "markdown_route", + userAgent: "Mozilla/5.0", }, }, ); @@ -388,13 +405,25 @@ describe("analytics", () => { source: "server", properties: { delivery: "markdown_route", + userAgent: "ChatGPT-User/1.0", trafficType: "agent", - agentName: "Docs agent", - botProvider: "Docs agent", + agentName: "ChatGPT", + botProvider: "ChatGPT", }, }, }); expect(requestBodies[1]).toMatchObject({ + event: { + type: "agent_read", + source: "server", + properties: { + delivery: "markdown_route", + userAgent: "Mozilla/5.0", + }, + }, + }); + expect(requestBodies[1].event.properties).not.toHaveProperty("trafficType"); + expect(requestBodies[2]).toMatchObject({ event: { type: "mcp_tool", source: "mcp", diff --git a/packages/docs/src/analytics.ts b/packages/docs/src/analytics.ts index d107cab0..61d44c08 100644 --- a/packages/docs/src/analytics.ts +++ b/packages/docs/src/analytics.ts @@ -116,6 +116,14 @@ export function createDocsAgentTraceContext(name = "agent.run"): DocsAgentTraceC }; } +export function getDocsRequestAnalyticsProperties(request: Request): Record { + const userAgent = request.headers.get("user-agent")?.trim(); + + return { + ...(userAgent ? { userAgent } : {}), + }; +} + export function resolveDocsAnalyticsConfig( analytics?: boolean | DocsAnalyticsConfig, ): ResolvedDocsAnalyticsConfig { diff --git a/packages/docs/src/cloud-analytics.ts b/packages/docs/src/cloud-analytics.ts index 6c1d1762..4e4de3e7 100644 --- a/packages/docs/src/cloud-analytics.ts +++ b/packages/docs/src/cloud-analytics.ts @@ -106,16 +106,11 @@ function normalizeAnalyticsLabel(value: string | undefined) { ); } -function isAgentAnalyticsEvent(event: DocsAnalyticsEvent) { +function isProtocolAgentEvent(event: DocsAnalyticsEvent) { const type = normalizeAnalyticsLabel(event.type); const source = normalizeAnalyticsLabel(event.source); - return ( - source === "mcp" || - type.startsWith("mcp_") || - type.startsWith("agent_") || - ["agents_request", "llms_request", "markdown_request", "skill_request"].includes(type) - ); + return source === "mcp" || type.startsWith("mcp_"); } function inferAgentProvider(event: DocsAnalyticsEvent) { @@ -137,12 +132,53 @@ function inferAgentProvider(event: DocsAnalyticsEvent) { return undefined; } -function withDocsCloudAnalyticsHints(event: DocsAnalyticsEvent): DocsAnalyticsEvent { - if (!isAgentAnalyticsEvent(event)) { - return event; +function detectAgentProviderFromUserAgent(userAgent: string | undefined) { + const value = userAgent?.toLowerCase() ?? ""; + + if (!value) { + return undefined; + } + + const providers: Array<[RegExp, string]> = [ + [/cursor/i, "Cursor"], + [/codex/i, "Codex"], + [/chatgpt-user|chatgpt/i, "ChatGPT"], + [/gptbot/i, "GPTBot"], + [/oai-searchbot|openai-search/i, "ChatGPT Search"], + [/openai/i, "ChatGPT"], + [/github-copilot|githubcopilot|copilot/i, "GitHub Copilot"], + [/claudebot|claude-user|anthropic/i, "Claude"], + [/perplexitybot|perplexity-user/i, "Perplexity"], + [/google-extended|googlebot|apis-google/i, "Google"], + [/bingbot|msnbot/i, "Bing"], + [/duckduckbot/i, "DuckDuckGo"], + [/applebot/i, "Apple"], + [/bytespider|bytedance/i, "ByteDance"], + [/ccbot|common crawl/i, "Common Crawl"], + [/ahrefsbot/i, "Ahrefs"], + [/semrushbot/i, "Semrush"], + ]; + + for (const [pattern, provider] of providers) { + if (pattern.test(value)) { + return provider; + } } + if (/bot|crawler|spider|slurp|facebookexternalhit|ia_archiver/.test(value)) { + return "Other bot"; + } + + return undefined; +} + +function withDocsCloudAnalyticsHints(event: DocsAnalyticsEvent): DocsAnalyticsEvent { const properties = asRecord(event.properties); + const userAgent = asString(properties.userAgent) ?? asString(properties.user_agent); + const detectedAgent = detectAgentProviderFromUserAgent(userAgent); + const protocolAgent = isProtocolAgentEvent(event); + const incomingTrafficType = asString(properties.trafficType)?.toLowerCase(); + const explicitAgent = incomingTrafficType === "agent" || incomingTrafficType === "bot"; const agentProvider = asString(properties.agentName) ?? asString(properties.agent) ?? @@ -150,7 +186,12 @@ function withDocsCloudAnalyticsHints(event: DocsAnalyticsEvent): DocsAnalyticsEv asString(properties.provider) ?? asString(properties.crawler) ?? asString(asRecord(properties.bot).provider) ?? - inferAgentProvider(event); + detectedAgent ?? + (protocolAgent || explicitAgent ? inferAgentProvider(event) : undefined); + + if (!explicitAgent && !protocolAgent && !detectedAgent && !agentProvider) { + return event; + } return { ...event, diff --git a/packages/docs/src/index.ts b/packages/docs/src/index.ts index 396aa0da..ccd045ed 100644 --- a/packages/docs/src/index.ts +++ b/packages/docs/src/index.ts @@ -17,6 +17,7 @@ export { emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, emitDocsObservabilityEvent, + getDocsRequestAnalyticsProperties, resolveDocsAnalyticsConfig, resolveDocsObservabilityConfig, } from "./analytics.js"; diff --git a/packages/docs/src/server.ts b/packages/docs/src/server.ts index b3a448f9..19b5b9c0 100644 --- a/packages/docs/src/server.ts +++ b/packages/docs/src/server.ts @@ -6,6 +6,7 @@ export { emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, emitDocsObservabilityEvent, + getDocsRequestAnalyticsProperties, resolveDocsAnalyticsConfig, resolveDocsObservabilityConfig, } from "./analytics.js"; diff --git a/packages/fumadocs/src/docs-api.test.ts b/packages/fumadocs/src/docs-api.test.ts index 7c1c7ebf..bf859f31 100644 --- a/packages/fumadocs/src/docs-api.test.ts +++ b/packages/fumadocs/src/docs-api.test.ts @@ -1276,6 +1276,11 @@ Install the package. "accept_header", "user_agent", ]); + expect(agentReads.find((event) => event.properties?.delivery === "user_agent")).toMatchObject({ + properties: expect.objectContaining({ + userAgent: "ClaudeBot/1.0", + }), + }); expect(agentReads).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/fumadocs/src/docs-api.ts b/packages/fumadocs/src/docs-api.ts index 6652f232..4efbac46 100644 --- a/packages/fumadocs/src/docs-api.ts +++ b/packages/fumadocs/src/docs-api.ts @@ -2241,6 +2241,14 @@ function safeUrlOrigin(value: string): string { } } +function getRequestAnalyticsProperties(request: Request): Record { + const userAgent = request.headers.get("user-agent")?.trim(); + + return { + ...(userAgent ? { userAgent } : {}), + }; +} + async function handleAskAI( request: Request, indexes: DocsSearchSourcePage[], @@ -2251,6 +2259,7 @@ async function handleAskAI( analyticsContext: { locale?: string } = {}, ): Promise { const url = new URL(request.url); + const requestAnalyticsProperties = getRequestAnalyticsProperties(request); const requestStartedAt = Date.now(); const trace = createDocsAgentTraceContext("ask-ai"); const runSpanId = createDocsAgentTraceId("span"); @@ -2330,6 +2339,7 @@ async function handleAskAI( path: url.pathname, locale: analyticsContext.locale, properties: { + ...requestAnalyticsProperties, reason: "invalid_json", durationMs: Math.max(0, Date.now() - requestStartedAt), }, @@ -2350,6 +2360,7 @@ async function handleAskAI( path: url.pathname, locale: analyticsContext.locale, properties: { + ...requestAnalyticsProperties, reason: "missing_messages", durationMs: Math.max(0, Date.now() - requestStartedAt), }, @@ -2370,6 +2381,7 @@ async function handleAskAI( path: url.pathname, locale: analyticsContext.locale, properties: { + ...requestAnalyticsProperties, reason: "missing_user_message", messageCount: messages.length, durationMs: Math.max(0, Date.now() - requestStartedAt), @@ -2504,6 +2516,7 @@ async function handleAskAI( locale: analyticsContext.locale, input: { question: query }, properties: { + ...requestAnalyticsProperties, reason: "missing_api_key", messageCount: messages.length, questionLength: query.length, @@ -2535,6 +2548,7 @@ async function handleAskAI( locale: analyticsContext.locale, input: { question: query }, properties: { + ...requestAnalyticsProperties, messageCount: messages.length, questionLength: query.length, retrievedCount: scored.length, @@ -2604,6 +2618,7 @@ async function handleAskAI( locale: analyticsContext.locale, input: { question: query }, properties: { + ...requestAnalyticsProperties, reason: "llm_fetch_error", messageCount: messages.length, questionLength: query.length, @@ -2650,6 +2665,7 @@ async function handleAskAI( locale: analyticsContext.locale, input: { question: query }, properties: { + ...requestAnalyticsProperties, reason: "llm_error", status: llmResponse.status, messageCount: messages.length, @@ -2681,6 +2697,7 @@ async function handleAskAI( locale: analyticsContext.locale, input: { question: query }, properties: { + ...requestAnalyticsProperties, messageCount: messages.length, questionLength: query.length, retrievedCount: scored.length, @@ -3235,6 +3252,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { async GET(request: Request) { const ctx = resolveContextFromRequest(request); const url = new URL(request.url); + const requestAnalyticsProperties = getRequestAnalyticsProperties(request); if (resolveAgentSpecRequest(url)) { await emitDocsAnalyticsEvent(analytics, { type: "agent_spec_request", @@ -3243,6 +3261,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, method: "GET", }, }); @@ -3316,6 +3335,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, method: "GET", }, }); @@ -3336,6 +3356,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, method: "GET", }, }); @@ -3370,6 +3391,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, method: "GET", }, }); @@ -3451,6 +3473,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, requestedPath: markdownRequest.requestedPath, delivery: markdownRequest.delivery, found: false, @@ -3463,6 +3486,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, requestedPath: markdownRequest.requestedPath, delivery: markdownRequest.delivery, found: false, @@ -3505,6 +3529,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, requestedPath: markdownRequest.requestedPath, delivery: markdownRequest.delivery, found: true, @@ -3518,6 +3543,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, requestedPath: markdownRequest.requestedPath, delivery: markdownRequest.delivery, found: true, @@ -3583,6 +3609,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { path: url.pathname, locale: ctx.locale, properties: { + ...requestAnalyticsProperties, format: llmsRequest.format, section: llmsRequest.section?.route, contentLength: selected.content.length, @@ -3620,6 +3647,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { locale: ctx.locale, input: { query }, properties: { + ...requestAnalyticsProperties, queryLength: query.length, resultCount: results.length, pathname: url.searchParams.get("pathname") ?? undefined, @@ -3639,6 +3667,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { */ async POST(request: Request): Promise { const url = new URL(request.url); + const requestAnalyticsProperties = getRequestAnalyticsProperties(request); const agentFeedbackRequest = resolveAgentFeedbackRequest(url, agentFeedbackConfig); if (agentFeedbackRequest) { @@ -3662,6 +3691,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { url: request.url, path: url.pathname, properties: { + ...requestAnalyticsProperties, reason: "invalid_body", }, }); @@ -3679,6 +3709,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { url: request.url, path: url.pathname, properties: { + ...requestAnalyticsProperties, reason: "invalid_payload", error: payloadError, }, @@ -3693,6 +3724,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { url: request.url, path: url.pathname, properties: { + ...requestAnalyticsProperties, handled: false, payloadKeys: Object.keys(parsed.data.payload), hasContext: Boolean(parsed.data.context), @@ -3708,6 +3740,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { url: request.url, path: url.pathname, properties: { + ...requestAnalyticsProperties, handled: true, payloadKeys: Object.keys(parsed.data.payload), hasContext: Boolean(parsed.data.context), @@ -3723,6 +3756,7 @@ export function createDocsAPI(options?: DocsAPIOptions) { url: request.url, path: url.pathname, properties: { + ...requestAnalyticsProperties, reason: "disabled", }, }); From 4ca1adb50e9a8cf631ae4d772e6e9ef965fd66d8 Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Sun, 31 May 2026 02:51:53 +0300 Subject: [PATCH 4/6] chore: lint --- packages/docs/src/analytics.ts | 4 +--- packages/fumadocs/src/docs-api.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/docs/src/analytics.ts b/packages/docs/src/analytics.ts index 61d44c08..45e54b03 100644 --- a/packages/docs/src/analytics.ts +++ b/packages/docs/src/analytics.ts @@ -119,9 +119,7 @@ export function createDocsAgentTraceContext(name = "agent.run"): DocsAgentTraceC export function getDocsRequestAnalyticsProperties(request: Request): Record { const userAgent = request.headers.get("user-agent")?.trim(); - return { - ...(userAgent ? { userAgent } : {}), - }; + return (userAgent ? { userAgent } : {}); } export function resolveDocsAnalyticsConfig( diff --git a/packages/fumadocs/src/docs-api.ts b/packages/fumadocs/src/docs-api.ts index 4efbac46..fee3ed22 100644 --- a/packages/fumadocs/src/docs-api.ts +++ b/packages/fumadocs/src/docs-api.ts @@ -2244,9 +2244,7 @@ function safeUrlOrigin(value: string): string { function getRequestAnalyticsProperties(request: Request): Record { const userAgent = request.headers.get("user-agent")?.trim(); - return { - ...(userAgent ? { userAgent } : {}), - }; + return (userAgent ? { userAgent } : {}); } async function handleAskAI( From be2e9acae5a28e11df788f6661171763e8de8f20 Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Sun, 31 May 2026 02:54:04 +0300 Subject: [PATCH 5/6] chore: format --- packages/docs/src/analytics.ts | 2 +- packages/fumadocs/src/docs-api.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs/src/analytics.ts b/packages/docs/src/analytics.ts index 45e54b03..3a333fe8 100644 --- a/packages/docs/src/analytics.ts +++ b/packages/docs/src/analytics.ts @@ -119,7 +119,7 @@ export function createDocsAgentTraceContext(name = "agent.run"): DocsAgentTraceC export function getDocsRequestAnalyticsProperties(request: Request): Record { const userAgent = request.headers.get("user-agent")?.trim(); - return (userAgent ? { userAgent } : {}); + return userAgent ? { userAgent } : {}; } export function resolveDocsAnalyticsConfig( diff --git a/packages/fumadocs/src/docs-api.ts b/packages/fumadocs/src/docs-api.ts index fee3ed22..9f72608c 100644 --- a/packages/fumadocs/src/docs-api.ts +++ b/packages/fumadocs/src/docs-api.ts @@ -2244,7 +2244,7 @@ function safeUrlOrigin(value: string): string { function getRequestAnalyticsProperties(request: Request): Record { const userAgent = request.headers.get("user-agent")?.trim(); - return (userAgent ? { userAgent } : {}); + return userAgent ? { userAgent } : {}; } async function handleAskAI( From 0240048eee35622ee2f6b71352335afc70aace1a Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Sun, 31 May 2026 02:57:57 +0300 Subject: [PATCH 6/6] fix: clarify docs cloud agent identity hints --- packages/docs/src/analytics.test.ts | 86 +++++++++++++++++++++++++++- packages/docs/src/cloud-analytics.ts | 5 +- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/docs/src/analytics.test.ts b/packages/docs/src/analytics.test.ts index 077638ec..427bc3d1 100644 --- a/packages/docs/src/analytics.test.ts +++ b/packages/docs/src/analytics.test.ts @@ -367,6 +367,52 @@ describe("analytics", () => { }, ); + await emitDocsAnalyticsEvent( + { + enabled: true, + console: false, + }, + { + type: "markdown_request", + source: "server", + path: "/docs/install.md", + properties: { + delivery: "md_route", + userAgent: "Mozilla/5.0", + }, + }, + ); + + await emitDocsAnalyticsEvent( + { + enabled: true, + console: false, + }, + { + type: "skill_request", + source: "server", + path: "/skill.md", + properties: { + userAgent: "CodexTestBot/1.0", + }, + }, + ); + + await emitDocsAnalyticsEvent( + { + enabled: true, + console: false, + }, + { + type: "llms_request", + source: "server", + path: "/llms.txt", + properties: { + trafficType: "agent", + }, + }, + ); + await emitDocsAnalyticsEvent( { enabled: true, @@ -394,6 +440,8 @@ describe("analytics", () => { path: "/docs/install", properties: { tool: "read_page", + agentName: " ", + botProvider: " ", }, }, ); @@ -414,16 +462,50 @@ describe("analytics", () => { }); expect(requestBodies[1]).toMatchObject({ event: { - type: "agent_read", + type: "markdown_request", source: "server", properties: { - delivery: "markdown_route", + delivery: "md_route", userAgent: "Mozilla/5.0", }, }, }); expect(requestBodies[1].event.properties).not.toHaveProperty("trafficType"); expect(requestBodies[2]).toMatchObject({ + event: { + type: "skill_request", + source: "server", + properties: { + userAgent: "CodexTestBot/1.0", + trafficType: "agent", + agentName: "Codex", + botProvider: "Codex", + }, + }, + }); + expect(requestBodies[3]).toMatchObject({ + event: { + type: "llms_request", + source: "server", + properties: { + trafficType: "agent", + agentName: "Docs reader", + botProvider: "Docs reader", + }, + }, + }); + expect(requestBodies[4]).toMatchObject({ + event: { + type: "agent_read", + source: "server", + properties: { + delivery: "markdown_route", + userAgent: "Mozilla/5.0", + }, + }, + }); + expect(requestBodies[4].event.properties).not.toHaveProperty("trafficType"); + expect(requestBodies[5]).toMatchObject({ event: { type: "mcp_tool", source: "mcp", diff --git a/packages/docs/src/cloud-analytics.ts b/packages/docs/src/cloud-analytics.ts index 4e4de3e7..bd670ec6 100644 --- a/packages/docs/src/cloud-analytics.ts +++ b/packages/docs/src/cloud-analytics.ts @@ -179,6 +179,7 @@ function withDocsCloudAnalyticsHints(event: DocsAnalyticsEvent): DocsAnalyticsEv const protocolAgent = isProtocolAgentEvent(event); const incomingTrafficType = asString(properties.trafficType)?.toLowerCase(); const explicitAgent = incomingTrafficType === "agent" || incomingTrafficType === "bot"; + // Agent-readable routes can still be opened by humans, so event type alone is not identity. const agentProvider = asString(properties.agentName) ?? asString(properties.agent) ?? @@ -198,8 +199,8 @@ function withDocsCloudAnalyticsHints(event: DocsAnalyticsEvent): DocsAnalyticsEv properties: { ...properties, trafficType: "agent", - ...(agentProvider && !properties.agentName ? { agentName: agentProvider } : {}), - ...(agentProvider && !properties.botProvider ? { botProvider: agentProvider } : {}), + ...(agentProvider && !asString(properties.agentName) ? { agentName: agentProvider } : {}), + ...(agentProvider && !asString(properties.botProvider) ? { botProvider: agentProvider } : {}), }, }; }