mirror of
https://github.com/vercel/flags.git
synced 2026-09-19 03:49:23 +08:00
eafa640057
* Add AI agent detection and automatic markdown rewrites When AI agents (Claude, ChatGPT, Cursor, etc.) request docs pages, the proxy now detects them and transparently rewrites to the markdown route — matching the geistdocs template default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix: Missing `detectionMethod` property in `TrackMdRequestParams` type causes TypeScript compilation error and build failure. This commit fixes the issue reported at apps/docs/lib/geistdocs/md-tracking.ts:24 **Bug explanation:** The `trackMdRequest` function in `apps/docs/lib/geistdocs/md-tracking.ts` destructures `detectionMethod` from its parameter (line 24) and includes it in the JSON body sent to the tracking endpoint (line 38). However, the `TrackMdRequestParams` type definition (lines 6-12) does not include `detectionMethod` as a property. In `apps/docs/proxy.ts` (line 82-87), `trackMdRequest` is called with `detectionMethod: agentResult.method` when an AI agent is detected, where `agentResult.method` is of type `DetectionMethod | null` from `@/lib/ai-agent-detection`. This causes an exact TypeScript compilation error confirmed in the build logs: ``` ./lib/geistdocs/md-tracking.ts:24:3 Type error: Property 'detectionMethod' does not exist on type 'TrackMdRequestParams'. ``` This error causes the entire `docs` build to fail (`next build` exits with code 1). **Fix explanation:** Added `detectionMethod?: DetectionMethod | null` as an optional property to the `TrackMdRequestParams` type, and added the corresponding `import type { DetectionMethod } from "@/lib/ai-agent-detection"` import at the top of the file. The property is optional (`?`) because most call sites (for `.md` URL tracking, `llms.txt` tracking, and header-negotiated tracking) don't pass a `detectionMethod` — only the agent-rewrite tracking path does. The `| null` union matches the `DetectionResult.method` type from `ai-agent-detection.ts`. Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: molebox <hello@richardhaines.dev> * format --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: Dominik Ferber <dominik.ferber@gmail.com>
169 lines
4.6 KiB
TypeScript
169 lines
4.6 KiB
TypeScript
/**
|
|
* AI Agent Detection Utility
|
|
*
|
|
* Multi-signal detection for AI agents/bots. Used to serve markdown
|
|
* responses when agents request docs pages.
|
|
*
|
|
* Three detection layers:
|
|
* 1. Known UA patterns (definitive) — curated from https://bots.fyi/?tags=ai_assistant
|
|
* 2. Signature-Agent header (definitive) — catches ChatGPT agent (RFC 9421)
|
|
* 3. Missing browser fingerprint heuristic — catches unknown bots
|
|
*
|
|
* Optimizes for recall over precision: serving markdown to a non-AI bot
|
|
* is low-harm; missing an AI agent means a worse experience.
|
|
*
|
|
* Last reviewed: 2026-03-20 against bots.fyi + official vendor docs
|
|
*/
|
|
|
|
// Layer 1: Known AI agent UA substrings (lowercase).
|
|
const AI_AGENT_UA_PATTERNS = [
|
|
// Anthropic — https://support.claude.com/en/articles/8896518
|
|
'claudebot',
|
|
'claude-searchbot',
|
|
'claude-user',
|
|
'anthropic-ai',
|
|
'claude-web',
|
|
|
|
// OpenAI — https://platform.openai.com/docs/bots
|
|
'chatgpt',
|
|
'gptbot',
|
|
'oai-searchbot',
|
|
'openai',
|
|
|
|
// Google AI
|
|
'gemini',
|
|
'bard',
|
|
'google-cloudvertexbot',
|
|
'google-extended',
|
|
|
|
// Meta
|
|
'meta-externalagent',
|
|
'meta-externalfetcher',
|
|
'meta-webindexer',
|
|
|
|
// Search/Research AI
|
|
'perplexity',
|
|
'youbot',
|
|
'you.com',
|
|
'deepseekbot',
|
|
|
|
// Coding assistants
|
|
'cursor',
|
|
'github-copilot',
|
|
'codeium',
|
|
'tabnine',
|
|
'sourcegraph',
|
|
|
|
// Other AI agents / data scrapers (low-harm to serve markdown)
|
|
'cohere-ai',
|
|
'bytespider',
|
|
'amazonbot',
|
|
'ai2bot',
|
|
'diffbot',
|
|
'omgili',
|
|
'omgilibot',
|
|
];
|
|
|
|
// Layer 2: Known AI service URLs in Signature-Agent header (RFC 9421).
|
|
const SIGNATURE_AGENT_DOMAINS = ['chatgpt.com'];
|
|
|
|
// Layer 3: Traditional bot exclusion list — bots that should NOT trigger
|
|
// the heuristic layer (they're search engine crawlers, social previews, or
|
|
// monitoring tools, not AI agents).
|
|
const TRADITIONAL_BOT_PATTERNS = [
|
|
'googlebot',
|
|
'bingbot',
|
|
'yandexbot',
|
|
'baiduspider',
|
|
'duckduckbot',
|
|
'slurp',
|
|
'msnbot',
|
|
'facebot',
|
|
'twitterbot',
|
|
'linkedinbot',
|
|
'whatsapp',
|
|
'telegrambot',
|
|
'pingdom',
|
|
'uptimerobot',
|
|
'newrelic',
|
|
'datadog',
|
|
'statuspage',
|
|
'site24x7',
|
|
'applebot',
|
|
];
|
|
|
|
// Broad regex for bot-like UA strings (used only in Layer 3 heuristic).
|
|
const BOT_LIKE_REGEX = /bot|agent|fetch|crawl|spider|search/i;
|
|
|
|
export type DetectionMethod = 'ua-match' | 'signature-agent' | 'heuristic';
|
|
|
|
export interface DetectionResult {
|
|
detected: boolean;
|
|
method: DetectionMethod | null;
|
|
}
|
|
|
|
/**
|
|
* Detects AI agents from HTTP request headers.
|
|
*
|
|
* Returns both whether the agent was detected and which signal triggered,
|
|
* so callers can log the detection method for accuracy tracking.
|
|
*/
|
|
export function isAIAgent(request: {
|
|
headers: { get(name: string): string | null };
|
|
}): DetectionResult {
|
|
const userAgent = request.headers.get('user-agent');
|
|
|
|
// Layer 1: Known UA pattern match
|
|
if (userAgent) {
|
|
const lowerUA = userAgent.toLowerCase();
|
|
if (AI_AGENT_UA_PATTERNS.some((pattern) => lowerUA.includes(pattern))) {
|
|
return { detected: true, method: 'ua-match' };
|
|
}
|
|
}
|
|
|
|
// Layer 2: Signature-Agent header (RFC 9421, used by ChatGPT agent)
|
|
const signatureAgent = request.headers.get('signature-agent');
|
|
if (signatureAgent) {
|
|
const lowerSig = signatureAgent.toLowerCase();
|
|
if (SIGNATURE_AGENT_DOMAINS.some((domain) => lowerSig.includes(domain))) {
|
|
return { detected: true, method: 'signature-agent' };
|
|
}
|
|
}
|
|
|
|
// Layer 3: Missing browser fingerprint heuristic
|
|
// Real browsers (Chrome 76+, Firefox 90+, Safari 16.4+) send sec-fetch-mode
|
|
// on navigation requests. Its absence signals a programmatic client.
|
|
const secFetchMode = request.headers.get('sec-fetch-mode');
|
|
if (!secFetchMode && userAgent && BOT_LIKE_REGEX.test(userAgent)) {
|
|
const lowerUA = userAgent.toLowerCase();
|
|
const isTraditionalBot = TRADITIONAL_BOT_PATTERNS.some((pattern) =>
|
|
lowerUA.includes(pattern),
|
|
);
|
|
if (!isTraditionalBot) {
|
|
return { detected: true, method: 'heuristic' };
|
|
}
|
|
}
|
|
|
|
return { detected: false, method: null };
|
|
}
|
|
|
|
/**
|
|
* Generates a markdown response for AI agents that hit non-existent URLs.
|
|
*/
|
|
export function generateAgentNotFoundResponse(requestedPath: string): string {
|
|
return `# Page Not Found
|
|
|
|
The URL \`${requestedPath}\` does not exist in the documentation.
|
|
|
|
## How to find the correct page
|
|
|
|
1. **Browse the sitemap**: [/sitemap.md](/sitemap.md) — A structured index of all pages with URLs, content types, and descriptions
|
|
2. **Browse the full index**: [/llms.txt](/llms.txt) — Complete documentation index
|
|
|
|
## Tips for requesting documentation
|
|
|
|
- For markdown responses, append \`.md\` to URLs (e.g., \`/docs/getting-started.md\`)
|
|
- Use \`Accept: text/markdown\` header for content negotiation
|
|
`;
|
|
}
|