@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,24 @@
|
|
|
1
|
+
import { C as CaptureEvent, A as AnalyticsAdapter } from '../types-DOy0kk0t.js';
|
|
2
|
+
|
|
3
|
+
interface WebhookAdapterConfig {
|
|
4
|
+
/** Destination URL that receives a POST for each event. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Extra headers merged onto the POST (useful for shared-secret auth). */
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Transform the event into the exact JSON body the destination expects.
|
|
10
|
+
* Defaults to sending the {@link CaptureEvent} as-is.
|
|
11
|
+
*/
|
|
12
|
+
transform?: (event: CaptureEvent) => unknown;
|
|
13
|
+
/** Override the `fetch` implementation. */
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Adapter that POSTs each event to an arbitrary webhook URL. Keeps the
|
|
18
|
+
* library analytics-backend-agnostic — use this when PostHog isn't your
|
|
19
|
+
* analytics of record, or when you want to multiplex events through your
|
|
20
|
+
* own ingestion layer.
|
|
21
|
+
*/
|
|
22
|
+
declare function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter;
|
|
23
|
+
|
|
24
|
+
export { type WebhookAdapterConfig, webhookAnalytics };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/adapters/webhook.ts
|
|
2
|
+
function webhookAnalytics(config) {
|
|
3
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
4
|
+
const transform = config.transform ?? ((e) => e);
|
|
5
|
+
return {
|
|
6
|
+
async capture(event) {
|
|
7
|
+
await fetchImpl(config.url, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
...config.headers ?? {}
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(transform(event)),
|
|
14
|
+
keepalive: true
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { webhookAnalytics };
|
|
21
|
+
//# sourceMappingURL=webhook.js.map
|
|
22
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/webhook.ts"],"names":[],"mappings":";AAsBO,SAAS,iBAAiB,MAAA,EAAgD;AAC/E,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,KAAA;AACtC,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,SAAA,KAAc,CAAC,CAAA,KAA6B,CAAA,CAAA;AAErE,EAAA,OAAO;AAAA,IACL,MAAM,QAAQ,KAAA,EAAoC;AAChD,MAAA,MAAM,SAAA,CAAU,OAAO,GAAA,EAAK;AAAA,QAC1B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB,kBAAA;AAAA,UAChB,GAAI,MAAA,CAAO,OAAA,IAAW;AAAC,SACzB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,QACrC,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"webhook.js","sourcesContent":["import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true\n })\n }\n }\n}\n"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bots.ts
|
|
4
|
+
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;
|
|
5
|
+
function isAiBot(userAgent) {
|
|
6
|
+
if (!userAgent) return false;
|
|
7
|
+
return AI_BOT_PATTERN.test(userAgent);
|
|
8
|
+
}
|
|
9
|
+
function parseBotName(userAgent) {
|
|
10
|
+
if (!userAgent || typeof userAgent !== "string") return "Other";
|
|
11
|
+
const s = userAgent.toLowerCase();
|
|
12
|
+
if (s.includes("chatgpt-user") || s.includes("gptbot") || s.includes("oai-searchbot") || s.includes("openai"))
|
|
13
|
+
return "ChatGPT";
|
|
14
|
+
if (s.includes("claudebot") || s.includes("claude-user") || s.includes("anthropic")) return "Claude";
|
|
15
|
+
if (s.includes("perplexitybot") || s.includes("perplexity-user")) return "Perplexity";
|
|
16
|
+
if (s.includes("ccbot")) return "Common Crawl";
|
|
17
|
+
if (s.includes("google-extended") || s.includes("googlebot")) return "Google";
|
|
18
|
+
if (s.includes("applebot-extended") || s.includes("applebot")) return "Apple";
|
|
19
|
+
if (s.includes("bingbot")) return "Bing";
|
|
20
|
+
if (s.includes("bytespider")) return "Bytespider";
|
|
21
|
+
if (s.includes("amazonbot")) return "Amazon";
|
|
22
|
+
if (s.includes("meta-externalagent") || s.includes("facebookbot")) return "Meta";
|
|
23
|
+
if (s.includes("mistralai-user")) return "Mistral";
|
|
24
|
+
if (s.includes("duckassistbot")) return "DuckDuckGo";
|
|
25
|
+
if (s.includes("youbot")) return "You.com";
|
|
26
|
+
if (s.includes("diffbot")) return "Diffbot";
|
|
27
|
+
if (s.includes("ai2bot")) return "AI2";
|
|
28
|
+
if (s.includes("cohere")) return "Cohere";
|
|
29
|
+
if (s.includes("cursor")) return "Cursor";
|
|
30
|
+
if (s.includes("windsurf")) return "Windsurf";
|
|
31
|
+
if (s.includes("petalbot")) return "PetalBot";
|
|
32
|
+
if (s.includes("mozilla") || s.includes("chrome") || s.includes("safari") || s.includes("firefox"))
|
|
33
|
+
return "Browser";
|
|
34
|
+
return "Other";
|
|
35
|
+
}
|
|
36
|
+
function firstUserAgentProduct(userAgent) {
|
|
37
|
+
if (!userAgent || typeof userAgent !== "string") return "Other";
|
|
38
|
+
const compatibleMatch = userAgent.match(/compatible;\s*([^/;\s]+)(?:\/[^\s;]*)?/i);
|
|
39
|
+
if (compatibleMatch && compatibleMatch[1]) return compatibleMatch[1].trim();
|
|
40
|
+
const first = userAgent.trim().split("/")[0]?.trim().split(/\s+/)[0]?.trim();
|
|
41
|
+
return first || "Other";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/hash.ts
|
|
45
|
+
function hashId(input) {
|
|
46
|
+
let h = 5381;
|
|
47
|
+
for (let i = 0; i < input.length; i++) {
|
|
48
|
+
h = (h << 5) + h + input.charCodeAt(i) & 4294967295;
|
|
49
|
+
}
|
|
50
|
+
return "anon_" + (h >>> 0).toString(16);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/track.ts
|
|
54
|
+
async function trackDocView(req, opts) {
|
|
55
|
+
const userAgent = req.headers.get("user-agent") || "";
|
|
56
|
+
const onlyBots = opts.onlyBots ?? true;
|
|
57
|
+
if (onlyBots && !isAiBot(userAgent)) return;
|
|
58
|
+
let pathname = "/";
|
|
59
|
+
let originFromUrl = "";
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(req.url);
|
|
62
|
+
pathname = url.pathname;
|
|
63
|
+
originFromUrl = url.origin;
|
|
64
|
+
} catch {
|
|
65
|
+
pathname = req.url || "/";
|
|
66
|
+
}
|
|
67
|
+
const origin = opts.origin ?? originFromUrl;
|
|
68
|
+
const forwardedFor = req.headers.get("x-forwarded-for") || "";
|
|
69
|
+
const ip = forwardedFor.split(",")[0]?.trim() ?? "";
|
|
70
|
+
const referer = req.headers.get("referer");
|
|
71
|
+
const event = {
|
|
72
|
+
event: opts.eventName ?? "doc_view",
|
|
73
|
+
distinctId: hashId(`${ip}:${userAgent}`),
|
|
74
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
75
|
+
properties: {
|
|
76
|
+
$process_person_profile: false,
|
|
77
|
+
$current_url: origin ? `${origin}${pathname}` : pathname,
|
|
78
|
+
path: pathname,
|
|
79
|
+
user_agent: userAgent,
|
|
80
|
+
is_ai_bot: AI_BOT_PATTERN.test(userAgent),
|
|
81
|
+
referer,
|
|
82
|
+
source: opts.source ?? null,
|
|
83
|
+
...opts.properties
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
await opts.analytics.capture(event);
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/adapters/posthog.ts
|
|
93
|
+
function posthogAnalytics(config) {
|
|
94
|
+
const hostRaw = config.host ?? "https://us.i.posthog.com";
|
|
95
|
+
const base = (/^https?:\/\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\/$/, "");
|
|
96
|
+
const path = (config.path ?? "/i/v0/e/").replace(/^(?!\/)/, "/");
|
|
97
|
+
const endpoint = `${base}${path}`;
|
|
98
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
99
|
+
return {
|
|
100
|
+
async capture(event) {
|
|
101
|
+
const payload = {
|
|
102
|
+
api_key: config.apiKey,
|
|
103
|
+
event: event.event,
|
|
104
|
+
distinct_id: event.distinctId,
|
|
105
|
+
timestamp: event.timestamp,
|
|
106
|
+
properties: event.properties
|
|
107
|
+
};
|
|
108
|
+
await fetchImpl(endpoint, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify(payload),
|
|
112
|
+
keepalive: true
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/adapters/webhook.ts
|
|
119
|
+
function webhookAnalytics(config) {
|
|
120
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
121
|
+
const transform = config.transform ?? ((e) => e);
|
|
122
|
+
return {
|
|
123
|
+
async capture(event) {
|
|
124
|
+
await fetchImpl(config.url, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
...config.headers ?? {}
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify(transform(event)),
|
|
131
|
+
keepalive: true
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/adapters/custom.ts
|
|
138
|
+
function customAnalytics(capture) {
|
|
139
|
+
return { capture };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.AI_BOT_PATTERN = AI_BOT_PATTERN;
|
|
143
|
+
exports.customAnalytics = customAnalytics;
|
|
144
|
+
exports.firstUserAgentProduct = firstUserAgentProduct;
|
|
145
|
+
exports.hashId = hashId;
|
|
146
|
+
exports.isAiBot = isAiBot;
|
|
147
|
+
exports.parseBotName = parseBotName;
|
|
148
|
+
exports.posthogAnalytics = posthogAnalytics;
|
|
149
|
+
exports.trackDocView = trackDocView;
|
|
150
|
+
exports.webhookAnalytics = webhookAnalytics;
|
|
151
|
+
//# sourceMappingURL=index.cjs.map
|
|
152
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/hash.ts","../src/track.ts","../src/adapters/posthog.ts","../src/adapters/webhook.ts","../src/adapters/custom.ts"],"names":[],"mappings":"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","file":"index.cjs","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","/**\n * djb2 hash returning an 8-char hex string prefixed with `anon_`. Used to\n * build stable anonymous distinct-ids from `ip:ua:...` tuples without\n * collecting identifying data. Not cryptographic — collisions are fine for\n * analytics segmentation.\n */\nexport function hashId(input: string): string {\n let h = 5381\n for (let i = 0; i < input.length; i++) {\n h = ((h << 5) + h + input.charCodeAt(i)) & 0xffffffff\n }\n return 'anon_' + (h >>> 0).toString(16)\n}\n","import { isAiBot, AI_BOT_PATTERN } from './bots.js'\nimport { hashId } from './hash.js'\nimport type { TrackDocViewOptions } from './types.js'\n\n/**\n * Capture an event describing the incoming request. Fire-and-forget: awaits\n * the adapter but swallows errors so a downed analytics backend never breaks\n * the response path. Callers typically don't await the returned promise.\n *\n * When `onlyBots` is true (the default), skips capture unless the UA matches\n * {@link AI_BOT_PATTERN}. Set `onlyBots: false` to track every visit.\n */\nexport async function trackDocView(\n req: Request,\n opts: TrackDocViewOptions\n): Promise<void> {\n const userAgent = req.headers.get('user-agent') || ''\n\n const onlyBots = opts.onlyBots ?? true\n if (onlyBots && !isAiBot(userAgent)) return\n\n let pathname = '/'\n let originFromUrl = ''\n try {\n const url = new URL(req.url)\n pathname = url.pathname\n originFromUrl = url.origin\n } catch {\n // Some runtimes hand us a relative URL; fall back to the raw string.\n pathname = req.url || '/'\n }\n const origin = opts.origin ?? originFromUrl\n\n const forwardedFor = req.headers.get('x-forwarded-for') || ''\n const ip = forwardedFor.split(',')[0]?.trim() ?? ''\n const referer = req.headers.get('referer')\n\n const event = {\n event: opts.eventName ?? 'doc_view',\n distinctId: hashId(`${ip}:${userAgent}`),\n timestamp: new Date().toISOString(),\n properties: {\n $process_person_profile: false,\n $current_url: origin ? `${origin}${pathname}` : pathname,\n path: pathname,\n user_agent: userAgent,\n is_ai_bot: AI_BOT_PATTERN.test(userAgent),\n referer,\n source: opts.source ?? null,\n ...opts.properties\n }\n }\n\n try {\n await opts.analytics.capture(event)\n } catch {\n // Intentional swallow — analytics failures must not affect the response.\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface PostHogAdapterConfig {\n /** PostHog project API key (the public one used by the JS SDK). */\n apiKey: string\n /**\n * PostHog host, with or without scheme. Defaults to `https://us.i.posthog.com`.\n * Use `https://eu.i.posthog.com` for EU cloud, or your own reverse-proxy\n * domain (e.g. `https://svc.example.com`).\n */\n host?: string\n /**\n * Path on the host that accepts single-event captures. Defaults to\n * `/i/v0/e/` which is PostHog's current endpoint for this.\n */\n path?: string\n /**\n * Override the `fetch` implementation (useful for tests or custom runtimes\n * that need a pinned fetch).\n */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that posts each event to the PostHog capture endpoint. Uses\n * `keepalive: true` so the request survives after a serverless response\n * returns — events aren't guaranteed (fire-and-forget), but that's the\n * trade we want to keep the hot path fast.\n */\nexport function posthogAnalytics(config: PostHogAdapterConfig): AnalyticsAdapter {\n const hostRaw = config.host ?? 'https://us.i.posthog.com'\n const base = (/^https?:\\/\\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\\/$/, '')\n const path = (config.path ?? '/i/v0/e/').replace(/^(?!\\/)/, '/')\n const endpoint = `${base}${path}`\n const fetchImpl = config.fetchImpl ?? fetch\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const payload = {\n api_key: config.apiKey,\n event: event.event,\n distinct_id: event.distinctId,\n timestamp: event.timestamp,\n properties: event.properties\n }\n await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true\n })\n }\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true\n })\n }\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\n/**\n * Escape hatch for wiring a callback directly as an analytics adapter.\n * Useful when you want to log events, pipe them through your own SDK, or\n * compose multiple adapters.\n *\n * @example\n * ```ts\n * const devAnalytics = customAnalytics((e) => console.log('[doc_view]', e))\n * ```\n */\nexport function customAnalytics(\n capture: (event: CaptureEvent) => Promise<void> | void\n): AnalyticsAdapter {\n return { capture }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { T as TrackDocViewOptions, C as CaptureEvent, A as AnalyticsAdapter } from './types-DOy0kk0t.cjs';
|
|
2
|
+
export { posthogAnalytics } from './adapters/posthog.cjs';
|
|
3
|
+
export { webhookAnalytics } from './adapters/webhook.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Capture an event describing the incoming request. Fire-and-forget: awaits
|
|
7
|
+
* the adapter but swallows errors so a downed analytics backend never breaks
|
|
8
|
+
* the response path. Callers typically don't await the returned promise.
|
|
9
|
+
*
|
|
10
|
+
* When `onlyBots` is true (the default), skips capture unless the UA matches
|
|
11
|
+
* {@link AI_BOT_PATTERN}. Set `onlyBots: false` to track every visit.
|
|
12
|
+
*/
|
|
13
|
+
declare function trackDocView(req: Request, opts: TrackDocViewOptions): Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User-agent substrings that identify known AI crawlers and coding agents.
|
|
17
|
+
* Maintained by hand; add new entries as they appear in the wild.
|
|
18
|
+
*
|
|
19
|
+
* Sources consulted when updating: darkvisitors.com, official docs from OpenAI,
|
|
20
|
+
* Anthropic, Google, Perplexity, Cohere, Apple, Bytedance, cursor, windsurf.
|
|
21
|
+
*/
|
|
22
|
+
declare const AI_BOT_PATTERN: RegExp;
|
|
23
|
+
declare function isAiBot(userAgent: string | null | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Map a user-agent string to a coarse, human-readable bot label. Returns
|
|
26
|
+
* `'Browser'` for typical desktop browsers and `'Other'` for anything we
|
|
27
|
+
* don't recognise — don't treat a non-`'Other'` result as "definitely a bot";
|
|
28
|
+
* pair with {@link isAiBot} when that distinction matters.
|
|
29
|
+
*/
|
|
30
|
+
declare function parseBotName(userAgent: string | null | undefined): string;
|
|
31
|
+
/**
|
|
32
|
+
* Return the first product token from a UA header, useful for segmenting by
|
|
33
|
+
* client without hard-coding every bot name. Falls back to `'Other'` for empty
|
|
34
|
+
* input.
|
|
35
|
+
*/
|
|
36
|
+
declare function firstUserAgentProduct(userAgent: string | null | undefined): string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* djb2 hash returning an 8-char hex string prefixed with `anon_`. Used to
|
|
40
|
+
* build stable anonymous distinct-ids from `ip:ua:...` tuples without
|
|
41
|
+
* collecting identifying data. Not cryptographic — collisions are fine for
|
|
42
|
+
* analytics segmentation.
|
|
43
|
+
*/
|
|
44
|
+
declare function hashId(input: string): string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Escape hatch for wiring a callback directly as an analytics adapter.
|
|
48
|
+
* Useful when you want to log events, pipe them through your own SDK, or
|
|
49
|
+
* compose multiple adapters.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const devAnalytics = customAnalytics((e) => console.log('[doc_view]', e))
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
declare function customAnalytics(capture: (event: CaptureEvent) => Promise<void> | void): AnalyticsAdapter;
|
|
57
|
+
|
|
58
|
+
export { AI_BOT_PATTERN, AnalyticsAdapter, CaptureEvent, TrackDocViewOptions, customAnalytics, firstUserAgentProduct, hashId, isAiBot, parseBotName, trackDocView };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { T as TrackDocViewOptions, C as CaptureEvent, A as AnalyticsAdapter } from './types-DOy0kk0t.js';
|
|
2
|
+
export { posthogAnalytics } from './adapters/posthog.js';
|
|
3
|
+
export { webhookAnalytics } from './adapters/webhook.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Capture an event describing the incoming request. Fire-and-forget: awaits
|
|
7
|
+
* the adapter but swallows errors so a downed analytics backend never breaks
|
|
8
|
+
* the response path. Callers typically don't await the returned promise.
|
|
9
|
+
*
|
|
10
|
+
* When `onlyBots` is true (the default), skips capture unless the UA matches
|
|
11
|
+
* {@link AI_BOT_PATTERN}. Set `onlyBots: false` to track every visit.
|
|
12
|
+
*/
|
|
13
|
+
declare function trackDocView(req: Request, opts: TrackDocViewOptions): Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User-agent substrings that identify known AI crawlers and coding agents.
|
|
17
|
+
* Maintained by hand; add new entries as they appear in the wild.
|
|
18
|
+
*
|
|
19
|
+
* Sources consulted when updating: darkvisitors.com, official docs from OpenAI,
|
|
20
|
+
* Anthropic, Google, Perplexity, Cohere, Apple, Bytedance, cursor, windsurf.
|
|
21
|
+
*/
|
|
22
|
+
declare const AI_BOT_PATTERN: RegExp;
|
|
23
|
+
declare function isAiBot(userAgent: string | null | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Map a user-agent string to a coarse, human-readable bot label. Returns
|
|
26
|
+
* `'Browser'` for typical desktop browsers and `'Other'` for anything we
|
|
27
|
+
* don't recognise — don't treat a non-`'Other'` result as "definitely a bot";
|
|
28
|
+
* pair with {@link isAiBot} when that distinction matters.
|
|
29
|
+
*/
|
|
30
|
+
declare function parseBotName(userAgent: string | null | undefined): string;
|
|
31
|
+
/**
|
|
32
|
+
* Return the first product token from a UA header, useful for segmenting by
|
|
33
|
+
* client without hard-coding every bot name. Falls back to `'Other'` for empty
|
|
34
|
+
* input.
|
|
35
|
+
*/
|
|
36
|
+
declare function firstUserAgentProduct(userAgent: string | null | undefined): string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* djb2 hash returning an 8-char hex string prefixed with `anon_`. Used to
|
|
40
|
+
* build stable anonymous distinct-ids from `ip:ua:...` tuples without
|
|
41
|
+
* collecting identifying data. Not cryptographic — collisions are fine for
|
|
42
|
+
* analytics segmentation.
|
|
43
|
+
*/
|
|
44
|
+
declare function hashId(input: string): string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Escape hatch for wiring a callback directly as an analytics adapter.
|
|
48
|
+
* Useful when you want to log events, pipe them through your own SDK, or
|
|
49
|
+
* compose multiple adapters.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const devAnalytics = customAnalytics((e) => console.log('[doc_view]', e))
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
declare function customAnalytics(capture: (event: CaptureEvent) => Promise<void> | void): AnalyticsAdapter;
|
|
57
|
+
|
|
58
|
+
export { AI_BOT_PATTERN, AnalyticsAdapter, CaptureEvent, TrackDocViewOptions, customAnalytics, firstUserAgentProduct, hashId, isAiBot, parseBotName, trackDocView };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
function parseBotName(userAgent) {
|
|
8
|
+
if (!userAgent || typeof userAgent !== "string") return "Other";
|
|
9
|
+
const s = userAgent.toLowerCase();
|
|
10
|
+
if (s.includes("chatgpt-user") || s.includes("gptbot") || s.includes("oai-searchbot") || s.includes("openai"))
|
|
11
|
+
return "ChatGPT";
|
|
12
|
+
if (s.includes("claudebot") || s.includes("claude-user") || s.includes("anthropic")) return "Claude";
|
|
13
|
+
if (s.includes("perplexitybot") || s.includes("perplexity-user")) return "Perplexity";
|
|
14
|
+
if (s.includes("ccbot")) return "Common Crawl";
|
|
15
|
+
if (s.includes("google-extended") || s.includes("googlebot")) return "Google";
|
|
16
|
+
if (s.includes("applebot-extended") || s.includes("applebot")) return "Apple";
|
|
17
|
+
if (s.includes("bingbot")) return "Bing";
|
|
18
|
+
if (s.includes("bytespider")) return "Bytespider";
|
|
19
|
+
if (s.includes("amazonbot")) return "Amazon";
|
|
20
|
+
if (s.includes("meta-externalagent") || s.includes("facebookbot")) return "Meta";
|
|
21
|
+
if (s.includes("mistralai-user")) return "Mistral";
|
|
22
|
+
if (s.includes("duckassistbot")) return "DuckDuckGo";
|
|
23
|
+
if (s.includes("youbot")) return "You.com";
|
|
24
|
+
if (s.includes("diffbot")) return "Diffbot";
|
|
25
|
+
if (s.includes("ai2bot")) return "AI2";
|
|
26
|
+
if (s.includes("cohere")) return "Cohere";
|
|
27
|
+
if (s.includes("cursor")) return "Cursor";
|
|
28
|
+
if (s.includes("windsurf")) return "Windsurf";
|
|
29
|
+
if (s.includes("petalbot")) return "PetalBot";
|
|
30
|
+
if (s.includes("mozilla") || s.includes("chrome") || s.includes("safari") || s.includes("firefox"))
|
|
31
|
+
return "Browser";
|
|
32
|
+
return "Other";
|
|
33
|
+
}
|
|
34
|
+
function firstUserAgentProduct(userAgent) {
|
|
35
|
+
if (!userAgent || typeof userAgent !== "string") return "Other";
|
|
36
|
+
const compatibleMatch = userAgent.match(/compatible;\s*([^/;\s]+)(?:\/[^\s;]*)?/i);
|
|
37
|
+
if (compatibleMatch && compatibleMatch[1]) return compatibleMatch[1].trim();
|
|
38
|
+
const first = userAgent.trim().split("/")[0]?.trim().split(/\s+/)[0]?.trim();
|
|
39
|
+
return first || "Other";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/hash.ts
|
|
43
|
+
function hashId(input) {
|
|
44
|
+
let h = 5381;
|
|
45
|
+
for (let i = 0; i < input.length; i++) {
|
|
46
|
+
h = (h << 5) + h + input.charCodeAt(i) & 4294967295;
|
|
47
|
+
}
|
|
48
|
+
return "anon_" + (h >>> 0).toString(16);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/track.ts
|
|
52
|
+
async function trackDocView(req, opts) {
|
|
53
|
+
const userAgent = req.headers.get("user-agent") || "";
|
|
54
|
+
const onlyBots = opts.onlyBots ?? true;
|
|
55
|
+
if (onlyBots && !isAiBot(userAgent)) return;
|
|
56
|
+
let pathname = "/";
|
|
57
|
+
let originFromUrl = "";
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
pathname = url.pathname;
|
|
61
|
+
originFromUrl = url.origin;
|
|
62
|
+
} catch {
|
|
63
|
+
pathname = req.url || "/";
|
|
64
|
+
}
|
|
65
|
+
const origin = opts.origin ?? originFromUrl;
|
|
66
|
+
const forwardedFor = req.headers.get("x-forwarded-for") || "";
|
|
67
|
+
const ip = forwardedFor.split(",")[0]?.trim() ?? "";
|
|
68
|
+
const referer = req.headers.get("referer");
|
|
69
|
+
const event = {
|
|
70
|
+
event: opts.eventName ?? "doc_view",
|
|
71
|
+
distinctId: hashId(`${ip}:${userAgent}`),
|
|
72
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
73
|
+
properties: {
|
|
74
|
+
$process_person_profile: false,
|
|
75
|
+
$current_url: origin ? `${origin}${pathname}` : pathname,
|
|
76
|
+
path: pathname,
|
|
77
|
+
user_agent: userAgent,
|
|
78
|
+
is_ai_bot: AI_BOT_PATTERN.test(userAgent),
|
|
79
|
+
referer,
|
|
80
|
+
source: opts.source ?? null,
|
|
81
|
+
...opts.properties
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
await opts.analytics.capture(event);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/adapters/posthog.ts
|
|
91
|
+
function posthogAnalytics(config) {
|
|
92
|
+
const hostRaw = config.host ?? "https://us.i.posthog.com";
|
|
93
|
+
const base = (/^https?:\/\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\/$/, "");
|
|
94
|
+
const path = (config.path ?? "/i/v0/e/").replace(/^(?!\/)/, "/");
|
|
95
|
+
const endpoint = `${base}${path}`;
|
|
96
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
97
|
+
return {
|
|
98
|
+
async capture(event) {
|
|
99
|
+
const payload = {
|
|
100
|
+
api_key: config.apiKey,
|
|
101
|
+
event: event.event,
|
|
102
|
+
distinct_id: event.distinctId,
|
|
103
|
+
timestamp: event.timestamp,
|
|
104
|
+
properties: event.properties
|
|
105
|
+
};
|
|
106
|
+
await fetchImpl(endpoint, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify(payload),
|
|
110
|
+
keepalive: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/adapters/webhook.ts
|
|
117
|
+
function webhookAnalytics(config) {
|
|
118
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
119
|
+
const transform = config.transform ?? ((e) => e);
|
|
120
|
+
return {
|
|
121
|
+
async capture(event) {
|
|
122
|
+
await fetchImpl(config.url, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
...config.headers ?? {}
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(transform(event)),
|
|
129
|
+
keepalive: true
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/adapters/custom.ts
|
|
136
|
+
function customAnalytics(capture) {
|
|
137
|
+
return { capture };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { AI_BOT_PATTERN, customAnalytics, firstUserAgentProduct, hashId, isAiBot, parseBotName, posthogAnalytics, trackDocView, webhookAnalytics };
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/hash.ts","../src/track.ts","../src/adapters/posthog.ts","../src/adapters/webhook.ts","../src/adapters/custom.ts"],"names":[],"mappings":"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","file":"index.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","/**\n * djb2 hash returning an 8-char hex string prefixed with `anon_`. Used to\n * build stable anonymous distinct-ids from `ip:ua:...` tuples without\n * collecting identifying data. Not cryptographic — collisions are fine for\n * analytics segmentation.\n */\nexport function hashId(input: string): string {\n let h = 5381\n for (let i = 0; i < input.length; i++) {\n h = ((h << 5) + h + input.charCodeAt(i)) & 0xffffffff\n }\n return 'anon_' + (h >>> 0).toString(16)\n}\n","import { isAiBot, AI_BOT_PATTERN } from './bots.js'\nimport { hashId } from './hash.js'\nimport type { TrackDocViewOptions } from './types.js'\n\n/**\n * Capture an event describing the incoming request. Fire-and-forget: awaits\n * the adapter but swallows errors so a downed analytics backend never breaks\n * the response path. Callers typically don't await the returned promise.\n *\n * When `onlyBots` is true (the default), skips capture unless the UA matches\n * {@link AI_BOT_PATTERN}. Set `onlyBots: false` to track every visit.\n */\nexport async function trackDocView(\n req: Request,\n opts: TrackDocViewOptions\n): Promise<void> {\n const userAgent = req.headers.get('user-agent') || ''\n\n const onlyBots = opts.onlyBots ?? true\n if (onlyBots && !isAiBot(userAgent)) return\n\n let pathname = '/'\n let originFromUrl = ''\n try {\n const url = new URL(req.url)\n pathname = url.pathname\n originFromUrl = url.origin\n } catch {\n // Some runtimes hand us a relative URL; fall back to the raw string.\n pathname = req.url || '/'\n }\n const origin = opts.origin ?? originFromUrl\n\n const forwardedFor = req.headers.get('x-forwarded-for') || ''\n const ip = forwardedFor.split(',')[0]?.trim() ?? ''\n const referer = req.headers.get('referer')\n\n const event = {\n event: opts.eventName ?? 'doc_view',\n distinctId: hashId(`${ip}:${userAgent}`),\n timestamp: new Date().toISOString(),\n properties: {\n $process_person_profile: false,\n $current_url: origin ? `${origin}${pathname}` : pathname,\n path: pathname,\n user_agent: userAgent,\n is_ai_bot: AI_BOT_PATTERN.test(userAgent),\n referer,\n source: opts.source ?? null,\n ...opts.properties\n }\n }\n\n try {\n await opts.analytics.capture(event)\n } catch {\n // Intentional swallow — analytics failures must not affect the response.\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface PostHogAdapterConfig {\n /** PostHog project API key (the public one used by the JS SDK). */\n apiKey: string\n /**\n * PostHog host, with or without scheme. Defaults to `https://us.i.posthog.com`.\n * Use `https://eu.i.posthog.com` for EU cloud, or your own reverse-proxy\n * domain (e.g. `https://svc.example.com`).\n */\n host?: string\n /**\n * Path on the host that accepts single-event captures. Defaults to\n * `/i/v0/e/` which is PostHog's current endpoint for this.\n */\n path?: string\n /**\n * Override the `fetch` implementation (useful for tests or custom runtimes\n * that need a pinned fetch).\n */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that posts each event to the PostHog capture endpoint. Uses\n * `keepalive: true` so the request survives after a serverless response\n * returns — events aren't guaranteed (fire-and-forget), but that's the\n * trade we want to keep the hot path fast.\n */\nexport function posthogAnalytics(config: PostHogAdapterConfig): AnalyticsAdapter {\n const hostRaw = config.host ?? 'https://us.i.posthog.com'\n const base = (/^https?:\\/\\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\\/$/, '')\n const path = (config.path ?? '/i/v0/e/').replace(/^(?!\\/)/, '/')\n const endpoint = `${base}${path}`\n const fetchImpl = config.fetchImpl ?? fetch\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const payload = {\n api_key: config.apiKey,\n event: event.event,\n distinct_id: event.distinctId,\n timestamp: event.timestamp,\n properties: event.properties\n }\n await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true\n })\n }\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true\n })\n }\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\n/**\n * Escape hatch for wiring a callback directly as an analytics adapter.\n * Useful when you want to log events, pipe them through your own SDK, or\n * compose multiple adapters.\n *\n * @example\n * ```ts\n * const devAnalytics = customAnalytics((e) => console.log('[doc_view]', e))\n * ```\n */\nexport function customAnalytics(\n capture: (event: CaptureEvent) => Promise<void> | void\n): AnalyticsAdapter {\n return { capture }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bots.ts
|
|
4
|
+
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;
|
|
5
|
+
function isAiBot(userAgent) {
|
|
6
|
+
if (!userAgent) return false;
|
|
7
|
+
return AI_BOT_PATTERN.test(userAgent);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/markdown.ts
|
|
11
|
+
function markdownServeDecision(req) {
|
|
12
|
+
let pathname = "/";
|
|
13
|
+
try {
|
|
14
|
+
pathname = new URL(req.url).pathname;
|
|
15
|
+
} catch {
|
|
16
|
+
pathname = req.url || "/";
|
|
17
|
+
}
|
|
18
|
+
const ua = req.headers.get("user-agent") || "";
|
|
19
|
+
if (isAiBot(ua)) {
|
|
20
|
+
return { reason: "ua-rewrite", strippedPath: pathname };
|
|
21
|
+
}
|
|
22
|
+
if (pathname.endsWith(".md")) {
|
|
23
|
+
return { reason: "md-suffix", strippedPath: pathname.replace(/\.md$/, "") };
|
|
24
|
+
}
|
|
25
|
+
const accept = req.headers.get("accept") || "";
|
|
26
|
+
if (accept.includes("text/markdown")) {
|
|
27
|
+
return { reason: "accept-header", strippedPath: pathname };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function markdownHeaders(input = {}) {
|
|
32
|
+
const headers = {
|
|
33
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
34
|
+
"Content-Signal": input.contentSignal ?? "search=yes, ai-input=yes, ai-train=no",
|
|
35
|
+
Vary: "accept"
|
|
36
|
+
};
|
|
37
|
+
if (typeof input.tokens === "number" && input.tokens > 0) {
|
|
38
|
+
headers["x-markdown-tokens"] = Math.max(1, Math.ceil(input.tokens)).toString();
|
|
39
|
+
}
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
function synthesizeMarkdownPointer(input) {
|
|
43
|
+
const site = input.siteName ?? (() => {
|
|
44
|
+
try {
|
|
45
|
+
return new URL(input.origin).hostname;
|
|
46
|
+
} catch {
|
|
47
|
+
return input.origin;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
const url = `${input.origin}${input.pathname}`;
|
|
51
|
+
const lines = [`# ${site}`, "", `This page (${url}) does not have a dedicated Markdown mirror yet.`, ""];
|
|
52
|
+
const links = [];
|
|
53
|
+
if (input.llmsTxtUrl) links.push(`- [${input.llmsTxtUrl}](${input.llmsTxtUrl}) \u2014 curated index of docs`);
|
|
54
|
+
if (input.llmsFullTxtUrl)
|
|
55
|
+
links.push(`- [${input.llmsFullTxtUrl}](${input.llmsFullTxtUrl}) \u2014 full enumerated index`);
|
|
56
|
+
if (input.markdownIndexUrl)
|
|
57
|
+
links.push(`- [${input.markdownIndexUrl}](${input.markdownIndexUrl}) \u2014 JSON index of all Markdown paths`);
|
|
58
|
+
if (links.length) {
|
|
59
|
+
lines.push("For machine-readable documentation, see:", "", ...links, "");
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.markdownHeaders = markdownHeaders;
|
|
65
|
+
exports.markdownServeDecision = markdownServeDecision;
|
|
66
|
+
exports.synthesizeMarkdownPointer = synthesizeMarkdownPointer;
|
|
67
|
+
//# sourceMappingURL=markdown.cjs.map
|
|
68
|
+
//# sourceMappingURL=markdown.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/markdown.ts"],"names":[],"mappings":"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","file":"markdown.cjs","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"]}
|