@apideck/agent-analytics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/dist/adapters/posthog.cjs +31 -0
- package/dist/adapters/posthog.cjs.map +1 -0
- package/dist/adapters/posthog.d.cts +31 -0
- package/dist/adapters/posthog.d.ts +31 -0
- package/dist/adapters/posthog.js +29 -0
- package/dist/adapters/posthog.js.map +1 -0
- package/dist/adapters/webhook.cjs +24 -0
- package/dist/adapters/webhook.cjs.map +1 -0
- package/dist/adapters/webhook.d.cts +24 -0
- package/dist/adapters/webhook.d.ts +24 -0
- package/dist/adapters/webhook.js +22 -0
- package/dist/adapters/webhook.js.map +1 -0
- package/dist/index.cjs +152 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.cjs +68 -0
- package/dist/markdown.cjs.map +1 -0
- package/dist/markdown.d.cts +63 -0
- package/dist/markdown.d.ts +63 -0
- package/dist/markdown.js +64 -0
- package/dist/markdown.js.map +1 -0
- package/dist/types-DOy0kk0t.d.cts +39 -0
- package/dist/types-DOy0kk0t.d.ts +39 -0
- package/package.json +80 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
type MarkdownServeReason = 'ua-rewrite' | 'md-suffix' | 'accept-header';
|
|
2
|
+
interface MarkdownDecision {
|
|
3
|
+
/** Why this request should be served Markdown. */
|
|
4
|
+
reason: MarkdownServeReason;
|
|
5
|
+
/**
|
|
6
|
+
* The request's original logical path, with any trailing `.md` stripped.
|
|
7
|
+
* Use this when mapping to a mirror file.
|
|
8
|
+
*/
|
|
9
|
+
strippedPath: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Decide whether the request should be served Markdown instead of HTML.
|
|
13
|
+
* Returns `null` when the request should go through your normal handler.
|
|
14
|
+
*
|
|
15
|
+
* Covers three triggers:
|
|
16
|
+
* - Known AI-bot UA on any URL (`ua-rewrite`)
|
|
17
|
+
* - Explicit `.md` suffix on the URL (`md-suffix`)
|
|
18
|
+
* - `Accept: text/markdown` header (`accept-header`)
|
|
19
|
+
*
|
|
20
|
+
* This helper intentionally does not perform the rewrite itself — routing is
|
|
21
|
+
* framework-specific (NextResponse.rewrite for Next.js, ctx.rewrite for
|
|
22
|
+
* Hono, etc.). Use the returned decision to build the appropriate response.
|
|
23
|
+
*/
|
|
24
|
+
declare function markdownServeDecision(req: Request): MarkdownDecision | null;
|
|
25
|
+
interface MarkdownHeadersInput {
|
|
26
|
+
/**
|
|
27
|
+
* If provided, rendered as `x-markdown-tokens` so agents can budget context
|
|
28
|
+
* before parsing the body. Typically `Math.ceil(body.length / 4)`.
|
|
29
|
+
*/
|
|
30
|
+
tokens?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Content-Signal directive (see contentsignals.org). Defaults to
|
|
33
|
+
* `'search=yes, ai-input=yes, ai-train=no'` — change if you want to permit
|
|
34
|
+
* training or restrict indexing.
|
|
35
|
+
*/
|
|
36
|
+
contentSignal?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the set of response headers to attach to a Markdown response. Safe
|
|
40
|
+
* defaults: UTF-8 text/markdown, Vary: accept, and a Content-Signal directive
|
|
41
|
+
* that permits search + agent input but denies training.
|
|
42
|
+
*/
|
|
43
|
+
declare function markdownHeaders(input?: MarkdownHeadersInput): Record<string, string>;
|
|
44
|
+
interface SynthesizePointerInput {
|
|
45
|
+
origin: string;
|
|
46
|
+
pathname: string;
|
|
47
|
+
/** URL of the site's curated index, usually `/llms.txt`. */
|
|
48
|
+
llmsTxtUrl?: string;
|
|
49
|
+
/** URL of the full enumerated index, usually `/llms-full.txt`. */
|
|
50
|
+
llmsFullTxtUrl?: string;
|
|
51
|
+
/** URL of the machine-readable path manifest, usually `/md/index.json`. */
|
|
52
|
+
markdownIndexUrl?: string;
|
|
53
|
+
/** Site name to title the pointer document. Defaults to the origin hostname. */
|
|
54
|
+
siteName?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate a minimal pointer Markdown document for URLs that don't have a
|
|
58
|
+
* pre-built mirror. Keeps the `Accept: text/markdown` contract intact
|
|
59
|
+
* site-wide — agents always get *something* parseable, not a 404.
|
|
60
|
+
*/
|
|
61
|
+
declare function synthesizeMarkdownPointer(input: SynthesizePointerInput): string;
|
|
62
|
+
|
|
63
|
+
export { type MarkdownDecision, type MarkdownHeadersInput, type MarkdownServeReason, type SynthesizePointerInput, markdownHeaders, markdownServeDecision, synthesizeMarkdownPointer };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
type MarkdownServeReason = 'ua-rewrite' | 'md-suffix' | 'accept-header';
|
|
2
|
+
interface MarkdownDecision {
|
|
3
|
+
/** Why this request should be served Markdown. */
|
|
4
|
+
reason: MarkdownServeReason;
|
|
5
|
+
/**
|
|
6
|
+
* The request's original logical path, with any trailing `.md` stripped.
|
|
7
|
+
* Use this when mapping to a mirror file.
|
|
8
|
+
*/
|
|
9
|
+
strippedPath: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Decide whether the request should be served Markdown instead of HTML.
|
|
13
|
+
* Returns `null` when the request should go through your normal handler.
|
|
14
|
+
*
|
|
15
|
+
* Covers three triggers:
|
|
16
|
+
* - Known AI-bot UA on any URL (`ua-rewrite`)
|
|
17
|
+
* - Explicit `.md` suffix on the URL (`md-suffix`)
|
|
18
|
+
* - `Accept: text/markdown` header (`accept-header`)
|
|
19
|
+
*
|
|
20
|
+
* This helper intentionally does not perform the rewrite itself — routing is
|
|
21
|
+
* framework-specific (NextResponse.rewrite for Next.js, ctx.rewrite for
|
|
22
|
+
* Hono, etc.). Use the returned decision to build the appropriate response.
|
|
23
|
+
*/
|
|
24
|
+
declare function markdownServeDecision(req: Request): MarkdownDecision | null;
|
|
25
|
+
interface MarkdownHeadersInput {
|
|
26
|
+
/**
|
|
27
|
+
* If provided, rendered as `x-markdown-tokens` so agents can budget context
|
|
28
|
+
* before parsing the body. Typically `Math.ceil(body.length / 4)`.
|
|
29
|
+
*/
|
|
30
|
+
tokens?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Content-Signal directive (see contentsignals.org). Defaults to
|
|
33
|
+
* `'search=yes, ai-input=yes, ai-train=no'` — change if you want to permit
|
|
34
|
+
* training or restrict indexing.
|
|
35
|
+
*/
|
|
36
|
+
contentSignal?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the set of response headers to attach to a Markdown response. Safe
|
|
40
|
+
* defaults: UTF-8 text/markdown, Vary: accept, and a Content-Signal directive
|
|
41
|
+
* that permits search + agent input but denies training.
|
|
42
|
+
*/
|
|
43
|
+
declare function markdownHeaders(input?: MarkdownHeadersInput): Record<string, string>;
|
|
44
|
+
interface SynthesizePointerInput {
|
|
45
|
+
origin: string;
|
|
46
|
+
pathname: string;
|
|
47
|
+
/** URL of the site's curated index, usually `/llms.txt`. */
|
|
48
|
+
llmsTxtUrl?: string;
|
|
49
|
+
/** URL of the full enumerated index, usually `/llms-full.txt`. */
|
|
50
|
+
llmsFullTxtUrl?: string;
|
|
51
|
+
/** URL of the machine-readable path manifest, usually `/md/index.json`. */
|
|
52
|
+
markdownIndexUrl?: string;
|
|
53
|
+
/** Site name to title the pointer document. Defaults to the origin hostname. */
|
|
54
|
+
siteName?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate a minimal pointer Markdown document for URLs that don't have a
|
|
58
|
+
* pre-built mirror. Keeps the `Accept: text/markdown` contract intact
|
|
59
|
+
* site-wide — agents always get *something* parseable, not a 404.
|
|
60
|
+
*/
|
|
61
|
+
declare function synthesizeMarkdownPointer(input: SynthesizePointerInput): string;
|
|
62
|
+
|
|
63
|
+
export { type MarkdownDecision, type MarkdownHeadersInput, type MarkdownServeReason, type SynthesizePointerInput, markdownHeaders, markdownServeDecision, synthesizeMarkdownPointer };
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/bots.ts
|
|
2
|
+
var AI_BOT_PATTERN = /ClaudeBot|Claude-User|Anthropic|ChatGPT-User|GPTBot|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|cohere-ai|Bytespider|CCBot|Amazonbot|Meta-ExternalAgent|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|Cursor|Windsurf/i;
|
|
3
|
+
function isAiBot(userAgent) {
|
|
4
|
+
if (!userAgent) return false;
|
|
5
|
+
return AI_BOT_PATTERN.test(userAgent);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/markdown.ts
|
|
9
|
+
function markdownServeDecision(req) {
|
|
10
|
+
let pathname = "/";
|
|
11
|
+
try {
|
|
12
|
+
pathname = new URL(req.url).pathname;
|
|
13
|
+
} catch {
|
|
14
|
+
pathname = req.url || "/";
|
|
15
|
+
}
|
|
16
|
+
const ua = req.headers.get("user-agent") || "";
|
|
17
|
+
if (isAiBot(ua)) {
|
|
18
|
+
return { reason: "ua-rewrite", strippedPath: pathname };
|
|
19
|
+
}
|
|
20
|
+
if (pathname.endsWith(".md")) {
|
|
21
|
+
return { reason: "md-suffix", strippedPath: pathname.replace(/\.md$/, "") };
|
|
22
|
+
}
|
|
23
|
+
const accept = req.headers.get("accept") || "";
|
|
24
|
+
if (accept.includes("text/markdown")) {
|
|
25
|
+
return { reason: "accept-header", strippedPath: pathname };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function markdownHeaders(input = {}) {
|
|
30
|
+
const headers = {
|
|
31
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
32
|
+
"Content-Signal": input.contentSignal ?? "search=yes, ai-input=yes, ai-train=no",
|
|
33
|
+
Vary: "accept"
|
|
34
|
+
};
|
|
35
|
+
if (typeof input.tokens === "number" && input.tokens > 0) {
|
|
36
|
+
headers["x-markdown-tokens"] = Math.max(1, Math.ceil(input.tokens)).toString();
|
|
37
|
+
}
|
|
38
|
+
return headers;
|
|
39
|
+
}
|
|
40
|
+
function synthesizeMarkdownPointer(input) {
|
|
41
|
+
const site = input.siteName ?? (() => {
|
|
42
|
+
try {
|
|
43
|
+
return new URL(input.origin).hostname;
|
|
44
|
+
} catch {
|
|
45
|
+
return input.origin;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
const url = `${input.origin}${input.pathname}`;
|
|
49
|
+
const lines = [`# ${site}`, "", `This page (${url}) does not have a dedicated Markdown mirror yet.`, ""];
|
|
50
|
+
const links = [];
|
|
51
|
+
if (input.llmsTxtUrl) links.push(`- [${input.llmsTxtUrl}](${input.llmsTxtUrl}) \u2014 curated index of docs`);
|
|
52
|
+
if (input.llmsFullTxtUrl)
|
|
53
|
+
links.push(`- [${input.llmsFullTxtUrl}](${input.llmsFullTxtUrl}) \u2014 full enumerated index`);
|
|
54
|
+
if (input.markdownIndexUrl)
|
|
55
|
+
links.push(`- [${input.markdownIndexUrl}](${input.markdownIndexUrl}) \u2014 JSON index of all Markdown paths`);
|
|
56
|
+
if (links.length) {
|
|
57
|
+
lines.push("For machine-readable documentation, see:", "", ...links, "");
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { markdownHeaders, markdownServeDecision, synthesizeMarkdownPointer };
|
|
63
|
+
//# sourceMappingURL=markdown.js.map
|
|
64
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/markdown.ts"],"names":[],"mappings":"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","file":"markdown.js","sourcesContent":["/**\n * User-agent substrings that identify known AI crawlers and coding agents.\n * Maintained by hand; add new entries as they appear in the wild.\n *\n * Sources consulted when updating: darkvisitors.com, official docs from OpenAI,\n * Anthropic, Google, Perplexity, Cohere, Apple, Bytedance, cursor, windsurf.\n */\nexport const AI_BOT_PATTERN =\n /ClaudeBot|Claude-User|Anthropic|ChatGPT-User|GPTBot|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Applebot-Extended|cohere-ai|Bytespider|CCBot|Amazonbot|Meta-ExternalAgent|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|Cursor|Windsurf/i\n\nexport function isAiBot(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return AI_BOT_PATTERN.test(userAgent)\n}\n\n/**\n * Map a user-agent string to a coarse, human-readable bot label. Returns\n * `'Browser'` for typical desktop browsers and `'Other'` for anything we\n * don't recognise — don't treat a non-`'Other'` result as \"definitely a bot\";\n * pair with {@link isAiBot} when that distinction matters.\n */\nexport function parseBotName(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const s = userAgent.toLowerCase()\n if (s.includes('chatgpt-user') || s.includes('gptbot') || s.includes('oai-searchbot') || s.includes('openai'))\n return 'ChatGPT'\n if (s.includes('claudebot') || s.includes('claude-user') || s.includes('anthropic')) return 'Claude'\n if (s.includes('perplexitybot') || s.includes('perplexity-user')) return 'Perplexity'\n if (s.includes('ccbot')) return 'Common Crawl'\n if (s.includes('google-extended') || s.includes('googlebot')) return 'Google'\n if (s.includes('applebot-extended') || s.includes('applebot')) return 'Apple'\n if (s.includes('bingbot')) return 'Bing'\n if (s.includes('bytespider')) return 'Bytespider'\n if (s.includes('amazonbot')) return 'Amazon'\n if (s.includes('meta-externalagent') || s.includes('facebookbot')) return 'Meta'\n if (s.includes('mistralai-user')) return 'Mistral'\n if (s.includes('duckassistbot')) return 'DuckDuckGo'\n if (s.includes('youbot')) return 'You.com'\n if (s.includes('diffbot')) return 'Diffbot'\n if (s.includes('ai2bot')) return 'AI2'\n if (s.includes('cohere')) return 'Cohere'\n if (s.includes('cursor')) return 'Cursor'\n if (s.includes('windsurf')) return 'Windsurf'\n if (s.includes('petalbot')) return 'PetalBot'\n if (s.includes('mozilla') || s.includes('chrome') || s.includes('safari') || s.includes('firefox'))\n return 'Browser'\n return 'Other'\n}\n\n/**\n * Return the first product token from a UA header, useful for segmenting by\n * client without hard-coding every bot name. Falls back to `'Other'` for empty\n * input.\n */\nexport function firstUserAgentProduct(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const compatibleMatch = userAgent.match(/compatible;\\s*([^/;\\s]+)(?:\\/[^\\s;]*)?/i)\n if (compatibleMatch && compatibleMatch[1]) return compatibleMatch[1].trim()\n const first = userAgent.trim().split('/')[0]?.trim().split(/\\s+/)[0]?.trim()\n return first || 'Other'\n}\n","import { isAiBot } from './bots.js'\n\nexport type MarkdownServeReason =\n | 'ua-rewrite'\n | 'md-suffix'\n | 'accept-header'\n\nexport interface MarkdownDecision {\n /** Why this request should be served Markdown. */\n reason: MarkdownServeReason\n /**\n * The request's original logical path, with any trailing `.md` stripped.\n * Use this when mapping to a mirror file.\n */\n strippedPath: string\n}\n\n/**\n * Decide whether the request should be served Markdown instead of HTML.\n * Returns `null` when the request should go through your normal handler.\n *\n * Covers three triggers:\n * - Known AI-bot UA on any URL (`ua-rewrite`)\n * - Explicit `.md` suffix on the URL (`md-suffix`)\n * - `Accept: text/markdown` header (`accept-header`)\n *\n * This helper intentionally does not perform the rewrite itself — routing is\n * framework-specific (NextResponse.rewrite for Next.js, ctx.rewrite for\n * Hono, etc.). Use the returned decision to build the appropriate response.\n */\nexport function markdownServeDecision(req: Request): MarkdownDecision | null {\n let pathname = '/'\n try {\n pathname = new URL(req.url).pathname\n } catch {\n pathname = req.url || '/'\n }\n\n const ua = req.headers.get('user-agent') || ''\n if (isAiBot(ua)) {\n return { reason: 'ua-rewrite', strippedPath: pathname }\n }\n\n if (pathname.endsWith('.md')) {\n return { reason: 'md-suffix', strippedPath: pathname.replace(/\\.md$/, '') }\n }\n\n const accept = req.headers.get('accept') || ''\n if (accept.includes('text/markdown')) {\n return { reason: 'accept-header', strippedPath: pathname }\n }\n\n return null\n}\n\nexport interface MarkdownHeadersInput {\n /**\n * If provided, rendered as `x-markdown-tokens` so agents can budget context\n * before parsing the body. Typically `Math.ceil(body.length / 4)`.\n */\n tokens?: number\n /**\n * Content-Signal directive (see contentsignals.org). Defaults to\n * `'search=yes, ai-input=yes, ai-train=no'` — change if you want to permit\n * training or restrict indexing.\n */\n contentSignal?: string\n}\n\n/**\n * Build the set of response headers to attach to a Markdown response. Safe\n * defaults: UTF-8 text/markdown, Vary: accept, and a Content-Signal directive\n * that permits search + agent input but denies training.\n */\nexport function markdownHeaders(input: MarkdownHeadersInput = {}): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'text/markdown; charset=utf-8',\n 'Content-Signal': input.contentSignal ?? 'search=yes, ai-input=yes, ai-train=no',\n Vary: 'accept'\n }\n if (typeof input.tokens === 'number' && input.tokens > 0) {\n headers['x-markdown-tokens'] = Math.max(1, Math.ceil(input.tokens)).toString()\n }\n return headers\n}\n\nexport interface SynthesizePointerInput {\n origin: string\n pathname: string\n /** URL of the site's curated index, usually `/llms.txt`. */\n llmsTxtUrl?: string\n /** URL of the full enumerated index, usually `/llms-full.txt`. */\n llmsFullTxtUrl?: string\n /** URL of the machine-readable path manifest, usually `/md/index.json`. */\n markdownIndexUrl?: string\n /** Site name to title the pointer document. Defaults to the origin hostname. */\n siteName?: string\n}\n\n/**\n * Generate a minimal pointer Markdown document for URLs that don't have a\n * pre-built mirror. Keeps the `Accept: text/markdown` contract intact\n * site-wide — agents always get *something* parseable, not a 404.\n */\nexport function synthesizeMarkdownPointer(input: SynthesizePointerInput): string {\n const site =\n input.siteName ??\n (() => {\n try {\n return new URL(input.origin).hostname\n } catch {\n return input.origin\n }\n })()\n const url = `${input.origin}${input.pathname}`\n const lines: string[] = [`# ${site}`, '', `This page (${url}) does not have a dedicated Markdown mirror yet.`, '']\n const links: string[] = []\n if (input.llmsTxtUrl) links.push(`- [${input.llmsTxtUrl}](${input.llmsTxtUrl}) — curated index of docs`)\n if (input.llmsFullTxtUrl)\n links.push(`- [${input.llmsFullTxtUrl}](${input.llmsFullTxtUrl}) — full enumerated index`)\n if (input.markdownIndexUrl)\n links.push(`- [${input.markdownIndexUrl}](${input.markdownIndexUrl}) — JSON index of all Markdown paths`)\n if (links.length) {\n lines.push('For machine-readable documentation, see:', '', ...links, '')\n }\n return lines.join('\\n')\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface CaptureEvent {
|
|
2
|
+
event: string;
|
|
3
|
+
distinctId: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
properties: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface AnalyticsAdapter {
|
|
8
|
+
capture(event: CaptureEvent): Promise<void> | void;
|
|
9
|
+
}
|
|
10
|
+
interface TrackDocViewOptions {
|
|
11
|
+
analytics: AnalyticsAdapter;
|
|
12
|
+
/**
|
|
13
|
+
* Label describing how the request arrived (e.g. `'page-view'`, `'md-suffix'`,
|
|
14
|
+
* `'ua-rewrite'`). Emitted as a `source` property on the captured event so
|
|
15
|
+
* you can segment by channel.
|
|
16
|
+
*/
|
|
17
|
+
source?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Event name. Defaults to `'doc_view'`.
|
|
20
|
+
*/
|
|
21
|
+
eventName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* When `true` (default), skip capture unless the request UA matches the
|
|
24
|
+
* built-in AI bot pattern. Set to `false` to capture every request.
|
|
25
|
+
*/
|
|
26
|
+
onlyBots?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extra properties merged into the captured event. Useful for tagging the
|
|
29
|
+
* site (`{ site: 'docs' }`) or any other dimension.
|
|
30
|
+
*/
|
|
31
|
+
properties?: Record<string, unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Override the origin used for `$current_url`. Defaults to the request URL's
|
|
34
|
+
* origin.
|
|
35
|
+
*/
|
|
36
|
+
origin?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type { AnalyticsAdapter as A, CaptureEvent as C, TrackDocViewOptions as T };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface CaptureEvent {
|
|
2
|
+
event: string;
|
|
3
|
+
distinctId: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
properties: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface AnalyticsAdapter {
|
|
8
|
+
capture(event: CaptureEvent): Promise<void> | void;
|
|
9
|
+
}
|
|
10
|
+
interface TrackDocViewOptions {
|
|
11
|
+
analytics: AnalyticsAdapter;
|
|
12
|
+
/**
|
|
13
|
+
* Label describing how the request arrived (e.g. `'page-view'`, `'md-suffix'`,
|
|
14
|
+
* `'ua-rewrite'`). Emitted as a `source` property on the captured event so
|
|
15
|
+
* you can segment by channel.
|
|
16
|
+
*/
|
|
17
|
+
source?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Event name. Defaults to `'doc_view'`.
|
|
20
|
+
*/
|
|
21
|
+
eventName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* When `true` (default), skip capture unless the request UA matches the
|
|
24
|
+
* built-in AI bot pattern. Set to `false` to capture every request.
|
|
25
|
+
*/
|
|
26
|
+
onlyBots?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extra properties merged into the captured event. Useful for tagging the
|
|
29
|
+
* site (`{ site: 'docs' }`) or any other dimension.
|
|
30
|
+
*/
|
|
31
|
+
properties?: Record<string, unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Override the origin used for `$current_url`. Defaults to the request URL's
|
|
34
|
+
* origin.
|
|
35
|
+
*/
|
|
36
|
+
origin?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type { AnalyticsAdapter as A, CaptureEvent as C, TrackDocViewOptions as T };
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apideck/agent-analytics",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Track AI agent and bot traffic to your Next.js / Vercel app — PostHog, webhooks, or any custom analytics backend. Detects Claude, ChatGPT, Perplexity, Google-Extended, and more.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agents",
|
|
8
|
+
"analytics",
|
|
9
|
+
"bot-detection",
|
|
10
|
+
"claude",
|
|
11
|
+
"chatgpt",
|
|
12
|
+
"perplexity",
|
|
13
|
+
"llm",
|
|
14
|
+
"posthog",
|
|
15
|
+
"nextjs",
|
|
16
|
+
"vercel",
|
|
17
|
+
"middleware"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/apideck-libraries/agent-analytics#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/apideck-libraries/agent-analytics/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/apideck-libraries/agent-analytics.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "Apideck",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs"
|
|
35
|
+
},
|
|
36
|
+
"./markdown": {
|
|
37
|
+
"types": "./dist/markdown.d.ts",
|
|
38
|
+
"import": "./dist/markdown.js",
|
|
39
|
+
"require": "./dist/markdown.cjs"
|
|
40
|
+
},
|
|
41
|
+
"./posthog": {
|
|
42
|
+
"types": "./dist/adapters/posthog.d.ts",
|
|
43
|
+
"import": "./dist/adapters/posthog.js",
|
|
44
|
+
"require": "./dist/adapters/posthog.cjs"
|
|
45
|
+
},
|
|
46
|
+
"./webhook": {
|
|
47
|
+
"types": "./dist/adapters/webhook.d.ts",
|
|
48
|
+
"import": "./dist/adapters/webhook.js",
|
|
49
|
+
"require": "./dist/adapters/webhook.cjs"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"main": "./dist/index.cjs",
|
|
53
|
+
"module": "./dist/index.js",
|
|
54
|
+
"types": "./dist/index.d.ts",
|
|
55
|
+
"files": [
|
|
56
|
+
"dist",
|
|
57
|
+
"README.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"dev": "tsup --watch",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"prepublishOnly": "npm run build"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^20.14.0",
|
|
70
|
+
"tsup": "^8.3.0",
|
|
71
|
+
"typescript": "^5.6.0",
|
|
72
|
+
"vitest": "^2.1.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=18"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
}
|
|
80
|
+
}
|