@apideck/agent-analytics 0.10.0 → 0.12.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/README.md +132 -0
- package/dist/adapters/posthog.cjs +2 -30
- package/dist/adapters/posthog.cjs.map +1 -1
- package/dist/adapters/posthog.d.cts +7 -1
- package/dist/adapters/posthog.d.ts +7 -1
- package/dist/adapters/posthog.js +2 -28
- package/dist/adapters/posthog.js.map +1 -1
- package/dist/adapters/webhook.cjs +1 -23
- package/dist/adapters/webhook.cjs.map +1 -1
- package/dist/adapters/webhook.d.cts +3 -1
- package/dist/adapters/webhook.d.ts +3 -1
- package/dist/adapters/webhook.js +1 -21
- package/dist/adapters/webhook.js.map +1 -1
- package/dist/index.cjs +2 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -10
- package/dist/index.d.ts +45 -10
- package/dist/index.js +2 -287
- package/dist/index.js.map +1 -1
- package/dist/markdown.cjs +2 -67
- package/dist/markdown.cjs.map +1 -1
- package/dist/markdown.js +2 -63
- package/dist/markdown.js.map +1 -1
- package/dist/{types-B7jSKtLz.d.cts → types-sQoQK-ox.d.cts} +48 -1
- package/dist/{types-B7jSKtLz.d.ts → types-sQoQK-ox.d.ts} +48 -1
- package/dist/verify.cjs +3 -0
- package/dist/verify.cjs.map +1 -0
- package/dist/verify.d.cts +50 -0
- package/dist/verify.d.ts +50 -0
- package/dist/verify.js +3 -0
- package/dist/verify.js.map +1 -0
- package/package.json +7 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { T as TrackVisitOptions, C as CaptureEvent, A as AnalyticsAdapter } from './types-
|
|
1
|
+
import { T as TrackVisitOptions, C as CaptureEvent, A as AnalyticsAdapter } from './types-sQoQK-ox.js';
|
|
2
|
+
export { B as BotVerificationLike } from './types-sQoQK-ox.js';
|
|
2
3
|
export { posthogAnalytics } from './adapters/posthog.js';
|
|
3
4
|
export { webhookAnalytics } from './adapters/webhook.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Capture an event describing the incoming request. Fire-and-forget: awaits
|
|
7
|
-
*
|
|
8
|
-
* the response path. Callers typically don't await
|
|
7
|
+
* Capture an event describing the incoming request. Fire-and-forget: awaits the
|
|
8
|
+
* adapter but routes errors to {@link TrackVisitOptions.onError} rather than
|
|
9
|
+
* letting them reach the response path. Callers typically don't await it.
|
|
9
10
|
*
|
|
10
11
|
* By default, captures every request so coding-agent traffic (axios, curl,
|
|
11
12
|
* Electron, …) shows up alongside branded crawlers. Set `onlyBots: true` to
|
|
@@ -131,12 +132,46 @@ declare function classifyAgent(userAgent: string | null | undefined): AgentClass
|
|
|
131
132
|
declare function classifyRequest(req: Request): AgentClassification;
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
135
|
+
* Keyed, non-reversible anonymous identifiers.
|
|
136
|
+
*
|
|
137
|
+
* The previous implementation was an unsalted 32-bit djb2 over `ip:userAgent`.
|
|
138
|
+
* Because the user agent is emitted in plaintext on the same event, an attacker
|
|
139
|
+
* held half the preimage and only had to search the IPv4 space — recovering a
|
|
140
|
+
* residential IP took 75 seconds single-threaded. That is pseudonymisation, not
|
|
141
|
+
* anonymisation, and it does not survive GDPR Recital 26.
|
|
142
|
+
*
|
|
143
|
+
* This uses HMAC-SHA-256 with a caller-supplied secret, truncated to 64 bits.
|
|
144
|
+
* Web Crypto is available on Vercel Edge, Cloudflare Workers, Deno and Node 18+.
|
|
145
|
+
*/
|
|
146
|
+
/** Thrown when a secret is missing or unusable. */
|
|
147
|
+
declare class HashSecretError extends Error {
|
|
148
|
+
constructor(message: string);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Hash `input` under `secret`, returning `anon_` followed by 16 hex characters
|
|
152
|
+
* (64 bits — collision-free well past any realistic distinct-visitor count).
|
|
153
|
+
*
|
|
154
|
+
* The secret must be stable across instances for identifiers to be comparable
|
|
155
|
+
* over time, and secret from anyone who can read your events: publishing it
|
|
156
|
+
* makes the identifier exactly as reversible as the old implementation was.
|
|
157
|
+
* Rotating it deliberately breaks continuity, which is correct behaviour for a
|
|
158
|
+
* privacy-preserving id.
|
|
159
|
+
*/
|
|
160
|
+
declare function hashId(input: string, secret: string): Promise<string>;
|
|
161
|
+
/**
|
|
162
|
+
* Generate a random secret. Used as the default when none is configured, so the
|
|
163
|
+
* privacy-preserving path is the one you get by doing nothing. Identifiers are
|
|
164
|
+
* then only stable within a single instance's lifetime — set a real secret when
|
|
165
|
+
* you need them comparable across instances and over time.
|
|
138
166
|
*/
|
|
139
|
-
declare function
|
|
167
|
+
declare function randomSecret(): string;
|
|
168
|
+
|
|
169
|
+
/** Thrown when the analytics backend rejects, errors, or times out a capture. */
|
|
170
|
+
declare class CaptureTransportError extends Error {
|
|
171
|
+
readonly status: number | undefined;
|
|
172
|
+
readonly body: string | undefined;
|
|
173
|
+
constructor(message: string, status?: number, body?: string);
|
|
174
|
+
}
|
|
140
175
|
|
|
141
176
|
/**
|
|
142
177
|
* Escape hatch for wiring a callback directly as an analytics adapter.
|
|
@@ -150,4 +185,4 @@ declare function hashId(input: string): string;
|
|
|
150
185
|
*/
|
|
151
186
|
declare function customAnalytics(capture: (event: CaptureEvent) => Promise<void> | void): AnalyticsAdapter;
|
|
152
187
|
|
|
153
|
-
export { AI_BOT_PATTERN, type AgentClassification, type AgentKind, AnalyticsAdapter, CaptureEvent, HTTP_CLIENT_PATTERN, type HeadlessDetection, TrackVisitOptions, classifyAgent, classifyRequest, customAnalytics, detectHeadless, firstUserAgentProduct, hashId, isAiBot, isHttpClient, parseBotName, trackVisit };
|
|
188
|
+
export { AI_BOT_PATTERN, type AgentClassification, type AgentKind, AnalyticsAdapter, CaptureEvent, CaptureTransportError, HTTP_CLIENT_PATTERN, HashSecretError, type HeadlessDetection, TrackVisitOptions, classifyAgent, classifyRequest, customAnalytics, detectHeadless, firstUserAgentProduct, hashId, isAiBot, isHttpClient, parseBotName, randomSecret, trackVisit };
|
package/dist/index.js
CHANGED
|
@@ -1,288 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var HTTP_CLIENT_PATTERN = /axios\/|curl\/|(?:^|[\s(])got(?:\/|[\s(])|\bcolly\b|Electron\/|node-fetch\/|python-requests\/|Go-http-client\/|okhttp\/|aiohttp\/|Deno\//i;
|
|
4
|
-
function isAiBot(userAgent) {
|
|
5
|
-
if (!userAgent) return false;
|
|
6
|
-
return AI_BOT_PATTERN.test(userAgent);
|
|
7
|
-
}
|
|
8
|
-
function isHttpClient(userAgent) {
|
|
9
|
-
if (!userAgent) return false;
|
|
10
|
-
return HTTP_CLIENT_PATTERN.test(userAgent);
|
|
11
|
-
}
|
|
12
|
-
function parseBotName(userAgent) {
|
|
13
|
-
if (!userAgent || typeof userAgent !== "string") return "Other";
|
|
14
|
-
const s = userAgent.toLowerCase();
|
|
15
|
-
if (s.includes("chatgpt-user") || s.includes("gptbot") || s.includes("oai-searchbot") || s.includes("openai"))
|
|
16
|
-
return "ChatGPT";
|
|
17
|
-
if (s.includes("claudebot") || s.includes("claude-user") || s.includes("claude-searchbot") || s.includes("claude-web") || s.includes("anthropic"))
|
|
18
|
-
return "Claude";
|
|
19
|
-
if (s.includes("perplexitybot") || s.includes("perplexity-user")) return "Perplexity";
|
|
20
|
-
if (s.includes("ccbot")) return "Common Crawl";
|
|
21
|
-
if (s.includes("google-extended") || s.includes("googlebot") || s.includes("google-cloudvertexbot") || s.includes("google-agent") || s.includes("googleagent-mariner") || s.includes("gemini-deep-research"))
|
|
22
|
-
return "Google";
|
|
23
|
-
if (s.includes("applebot")) return "Apple";
|
|
24
|
-
if (s.includes("bingbot")) return "Bing";
|
|
25
|
-
if (s.includes("bytespider")) return "Bytespider";
|
|
26
|
-
if (s.includes("amazonbot") || s.includes("amzn-searchbot") || s.includes("novaact")) return "Amazon";
|
|
27
|
-
if (s.includes("meta-externalagent") || s.includes("meta-externalfetcher") || s.includes("meta-webindexer") || s.includes("facebookbot"))
|
|
28
|
-
return "Meta";
|
|
29
|
-
if (s.includes("mistralai-user")) return "Mistral";
|
|
30
|
-
if (s.includes("duckassistbot")) return "DuckDuckGo";
|
|
31
|
-
if (s.includes("youbot")) return "You.com";
|
|
32
|
-
if (s.includes("diffbot")) return "Diffbot";
|
|
33
|
-
if (s.includes("ai2bot")) return "AI2";
|
|
34
|
-
if (s.includes("cohere")) return "Cohere";
|
|
35
|
-
if (s.includes("cursor")) return "Cursor";
|
|
36
|
-
if (s.includes("windsurf")) return "Windsurf";
|
|
37
|
-
if (s.includes("deepseek")) return "DeepSeek";
|
|
38
|
-
if (s.includes("pangubot")) return "Huawei";
|
|
39
|
-
if (s.includes("webzio") || s.includes("omgili")) return "Webz.io";
|
|
40
|
-
if (s.includes("timpibot")) return "Timpi";
|
|
41
|
-
if (s.includes("grok") || s.includes("xai-")) return "xAI";
|
|
42
|
-
if (s.includes("manus-user")) return "Manus";
|
|
43
|
-
if (s.includes("quillbot")) return "QuillBot";
|
|
44
|
-
if (s.includes("azureai-searchbot")) return "Microsoft";
|
|
45
|
-
if (s.includes("mycentralaiscraperbot")) return "MyCentralAI";
|
|
46
|
-
if (s.includes("petalbot")) return "PetalBot";
|
|
47
|
-
if (s.includes("ahrefsbot")) return "Ahrefs";
|
|
48
|
-
if (s.includes("semrushbot")) return "Semrush";
|
|
49
|
-
if (s.includes("mj12bot")) return "Majestic";
|
|
50
|
-
if (s.includes("dotbot")) return "Moz";
|
|
51
|
-
if (s.includes("rogerbot")) return "Moz";
|
|
52
|
-
if (s.includes("screaming frog")) return "Screaming Frog";
|
|
53
|
-
if (s.includes("sitebulb")) return "Sitebulb";
|
|
54
|
-
if (s.includes("linkfluence")) return "Linkfluence";
|
|
55
|
-
if (s.includes("dataforseo")) return "DataForSEO";
|
|
56
|
-
if (s.includes("serpstatbot")) return "Serpstat";
|
|
57
|
-
if (s.includes("uptimerobot")) return "UptimeRobot";
|
|
58
|
-
if (s.includes("pingdom")) return "Pingdom";
|
|
59
|
-
if (s.includes("statuscake")) return "StatusCake";
|
|
60
|
-
if (s.includes("newrelicpinger")) return "New Relic";
|
|
61
|
-
if (s.includes("datadogagent") || s.includes("datadog")) return "Datadog";
|
|
62
|
-
if (s.includes("slackbot")) return "Slack";
|
|
63
|
-
if (s.includes("twitterbot")) return "Twitter";
|
|
64
|
-
if (s.includes("linkedinbot")) return "LinkedIn";
|
|
65
|
-
if (s.includes("discordbot")) return "Discord";
|
|
66
|
-
if (s.includes("telegrambot")) return "Telegram";
|
|
67
|
-
if (s.includes("whatsapp")) return "WhatsApp";
|
|
68
|
-
if (s.includes("linkupbot")) return "Linkup";
|
|
69
|
-
if (s.includes("sogou")) return "Sogou";
|
|
70
|
-
if (s.includes("yandexbot")) return "Yandex";
|
|
71
|
-
if (s.includes("baiduspider")) return "Baidu";
|
|
72
|
-
if (s.includes("facebookexternalhit")) return "Facebook";
|
|
73
|
-
if (s.includes("com.apple.webkit")) return "Apple URL Preview";
|
|
74
|
-
if (s.includes("ohdear")) return "Oh Dear";
|
|
75
|
-
if (s.includes("scrapy")) return "Scrapy";
|
|
76
|
-
if (s.includes("headlesschrome")) return "Headless Chrome";
|
|
77
|
-
if (s.includes("phantomjs")) return "PhantomJS";
|
|
78
|
-
if (s.includes("wget")) return "wget";
|
|
79
|
-
if (s.includes("httpie")) return "HTTPie";
|
|
80
|
-
if (s.includes("guzzlehttp")) return "Guzzle";
|
|
81
|
-
if (s.includes("electron/")) return "Electron";
|
|
82
|
-
if (/curl\//.test(s)) return "curl";
|
|
83
|
-
if (/axios\//.test(s)) return "axios";
|
|
84
|
-
if (/(?:^|[\s(])got(?:\/|[\s(])/.test(s)) return "got";
|
|
85
|
-
if (/\bcolly\b/.test(s)) return "colly";
|
|
86
|
-
if (/node-fetch\//.test(s)) return "node-fetch";
|
|
87
|
-
if (/python-requests\//.test(s)) return "python-requests";
|
|
88
|
-
if (/go-http-client\//.test(s)) return "Go http client";
|
|
89
|
-
if (/okhttp\//.test(s)) return "OkHttp";
|
|
90
|
-
if (/aiohttp\//.test(s)) return "aiohttp";
|
|
91
|
-
if (/deno\//.test(s)) return "Deno";
|
|
92
|
-
if (s.includes("mozilla") || s.includes("chrome") || s.includes("safari") || s.includes("firefox"))
|
|
93
|
-
return "Browser";
|
|
94
|
-
return "Other";
|
|
95
|
-
}
|
|
96
|
-
function firstUserAgentProduct(userAgent) {
|
|
97
|
-
if (!userAgent || typeof userAgent !== "string") return "Other";
|
|
98
|
-
const compatibleMatch = userAgent.match(/compatible;\s*([^/;\s]+)(?:\/[^\s;]*)?/i);
|
|
99
|
-
if (compatibleMatch && compatibleMatch[1]) return compatibleMatch[1].trim();
|
|
100
|
-
const first = userAgent.trim().split("/")[0]?.trim().split(/\s+/)[0]?.trim();
|
|
101
|
-
return first || "Other";
|
|
102
|
-
}
|
|
103
|
-
function detectHeadless(req) {
|
|
104
|
-
const signals = [];
|
|
105
|
-
const ua = (req.headers.get("user-agent") || "").toLowerCase();
|
|
106
|
-
const isBrowserUA = ua.includes("mozilla") || ua.includes("chrome") || ua.includes("safari") || ua.includes("firefox");
|
|
107
|
-
if (!isBrowserUA) return { score: 0, signals: [], likely: false };
|
|
108
|
-
if (!req.headers.get("accept-language")) {
|
|
109
|
-
signals.push("missing-accept-language");
|
|
110
|
-
}
|
|
111
|
-
if (!req.headers.get("sec-fetch-mode")) {
|
|
112
|
-
signals.push("missing-sec-fetch-mode");
|
|
113
|
-
}
|
|
114
|
-
const secChUa = req.headers.get("sec-ch-ua");
|
|
115
|
-
if (!secChUa) {
|
|
116
|
-
signals.push("missing-sec-ch-ua");
|
|
117
|
-
} else if (secChUa.toLowerCase().includes("headlesschrome")) {
|
|
118
|
-
signals.push("headless-chrome-hint");
|
|
119
|
-
}
|
|
120
|
-
const accept = req.headers.get("accept") || "";
|
|
121
|
-
if (!accept || accept === "*/*") {
|
|
122
|
-
signals.push("missing-or-bare-accept");
|
|
123
|
-
}
|
|
124
|
-
if ((req.headers.get("connection") || "").toLowerCase() === "close") {
|
|
125
|
-
signals.push("connection-close");
|
|
126
|
-
}
|
|
127
|
-
const score = signals.length;
|
|
128
|
-
return { score, signals, likely: score >= 2 };
|
|
129
|
-
}
|
|
130
|
-
function classifyAgent(userAgent) {
|
|
131
|
-
const label = parseBotName(userAgent);
|
|
132
|
-
const aiBot = isAiBot(userAgent);
|
|
133
|
-
const httpClient = isHttpClient(userAgent);
|
|
134
|
-
let kind;
|
|
135
|
-
if (aiBot) kind = "declared-crawler";
|
|
136
|
-
else if (httpClient) kind = "coding-agent-hint";
|
|
137
|
-
else if (label === "Browser") kind = "browser";
|
|
138
|
-
else kind = "other";
|
|
139
|
-
return { kind, label, isAiBot: aiBot, codingAgentHint: httpClient };
|
|
140
|
-
}
|
|
141
|
-
function classifyRequest(req) {
|
|
142
|
-
const userAgent = req.headers.get("user-agent") || "";
|
|
143
|
-
const base = classifyAgent(userAgent);
|
|
144
|
-
const headless = detectHeadless(req);
|
|
145
|
-
let kind = base.kind;
|
|
146
|
-
if (kind === "browser" && headless.likely) {
|
|
147
|
-
kind = "headless-likely";
|
|
148
|
-
}
|
|
149
|
-
return { ...base, kind, headless };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// src/hash.ts
|
|
153
|
-
function hashId(input) {
|
|
154
|
-
let h = 5381;
|
|
155
|
-
for (let i = 0; i < input.length; i++) {
|
|
156
|
-
h = (h << 5) + h + input.charCodeAt(i) & 4294967295;
|
|
157
|
-
}
|
|
158
|
-
return "anon_" + (h >>> 0).toString(16);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/track.ts
|
|
162
|
-
async function trackVisit(req, opts) {
|
|
163
|
-
const userAgent = req.headers.get("user-agent") || "";
|
|
164
|
-
const onlyBots = opts.onlyBots ?? false;
|
|
165
|
-
const skipBrowsers = opts.skipBrowsers ?? false;
|
|
166
|
-
if (onlyBots && !isAiBot(userAgent)) return;
|
|
167
|
-
if (skipBrowsers && !isAiBot(userAgent) && !isHttpClient(userAgent)) {
|
|
168
|
-
if (!detectHeadless(req).likely) return;
|
|
169
|
-
}
|
|
170
|
-
let pathname = "/";
|
|
171
|
-
let originFromUrl = "";
|
|
172
|
-
try {
|
|
173
|
-
const url = new URL(req.url);
|
|
174
|
-
pathname = url.pathname;
|
|
175
|
-
originFromUrl = url.origin;
|
|
176
|
-
} catch {
|
|
177
|
-
pathname = req.url || "/";
|
|
178
|
-
}
|
|
179
|
-
const origin = opts.origin ?? originFromUrl;
|
|
180
|
-
const forwardedFor = req.headers.get("x-forwarded-for") || "";
|
|
181
|
-
const ip = forwardedFor.split(",")[0]?.trim() ?? "";
|
|
182
|
-
const referer = req.headers.get("referer");
|
|
183
|
-
const country = opts.captureCountry ? req.headers.get("x-vercel-ip-country") || req.headers.get("cf-ipcountry") || req.headers.get("x-country-code") || null : null;
|
|
184
|
-
const geo = opts.captureGeo ? extractGeo(req) : null;
|
|
185
|
-
const classification = classifyRequest(req);
|
|
186
|
-
const event = {
|
|
187
|
-
event: opts.eventName ?? "agent_visit",
|
|
188
|
-
distinctId: hashId(`${ip}:${userAgent}`),
|
|
189
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
190
|
-
properties: {
|
|
191
|
-
$process_person_profile: false,
|
|
192
|
-
$current_url: origin ? `${origin}${pathname}` : pathname,
|
|
193
|
-
path: pathname,
|
|
194
|
-
method: req.method,
|
|
195
|
-
...opts.captureCountry ? { country_code: country } : {},
|
|
196
|
-
...geo ?? {},
|
|
197
|
-
...opts.captureIp ? { client_ip: ip || null } : {},
|
|
198
|
-
user_agent: userAgent,
|
|
199
|
-
is_ai_bot: classification.isAiBot,
|
|
200
|
-
bot_name: classification.label,
|
|
201
|
-
ua_category: classification.kind,
|
|
202
|
-
coding_agent_hint: classification.codingAgentHint,
|
|
203
|
-
headless_score: classification.headless?.score ?? 0,
|
|
204
|
-
headless_likely: classification.headless?.likely ?? false,
|
|
205
|
-
referer,
|
|
206
|
-
source: opts.source ?? null,
|
|
207
|
-
...opts.properties
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
try {
|
|
211
|
-
await opts.analytics.capture(event);
|
|
212
|
-
} catch {
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function extractGeo(req) {
|
|
216
|
-
const decode = (v) => {
|
|
217
|
-
if (!v) return "";
|
|
218
|
-
try {
|
|
219
|
-
return decodeURIComponent(v);
|
|
220
|
-
} catch {
|
|
221
|
-
return v;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
const fields = [
|
|
225
|
-
["region", decode(req.headers.get("x-vercel-ip-country-region"))],
|
|
226
|
-
["city", decode(req.headers.get("x-vercel-ip-city"))],
|
|
227
|
-
["latitude", req.headers.get("x-vercel-ip-latitude") ?? ""],
|
|
228
|
-
["longitude", req.headers.get("x-vercel-ip-longitude") ?? ""],
|
|
229
|
-
["timezone", req.headers.get("x-vercel-ip-timezone") ?? ""]
|
|
230
|
-
];
|
|
231
|
-
const out = {};
|
|
232
|
-
for (const [k, v] of fields) if (v) out[k] = v;
|
|
233
|
-
return out;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// src/adapters/posthog.ts
|
|
237
|
-
function posthogAnalytics(config) {
|
|
238
|
-
const hostRaw = config.host ?? "https://us.i.posthog.com";
|
|
239
|
-
const base = (/^https?:\/\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\/$/, "");
|
|
240
|
-
const path = (config.path ?? "/i/v0/e/").replace(/^(?!\/)/, "/");
|
|
241
|
-
const endpoint = `${base}${path}`;
|
|
242
|
-
const fetchImpl = config.fetchImpl ?? fetch;
|
|
243
|
-
return {
|
|
244
|
-
async capture(event) {
|
|
245
|
-
const payload = {
|
|
246
|
-
api_key: config.apiKey,
|
|
247
|
-
event: event.event,
|
|
248
|
-
distinct_id: event.distinctId,
|
|
249
|
-
timestamp: event.timestamp,
|
|
250
|
-
properties: event.properties
|
|
251
|
-
};
|
|
252
|
-
await fetchImpl(endpoint, {
|
|
253
|
-
method: "POST",
|
|
254
|
-
headers: { "Content-Type": "application/json" },
|
|
255
|
-
body: JSON.stringify(payload),
|
|
256
|
-
keepalive: true
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/adapters/webhook.ts
|
|
263
|
-
function webhookAnalytics(config) {
|
|
264
|
-
const fetchImpl = config.fetchImpl ?? fetch;
|
|
265
|
-
const transform = config.transform ?? ((e) => e);
|
|
266
|
-
return {
|
|
267
|
-
async capture(event) {
|
|
268
|
-
await fetchImpl(config.url, {
|
|
269
|
-
method: "POST",
|
|
270
|
-
headers: {
|
|
271
|
-
"Content-Type": "application/json",
|
|
272
|
-
...config.headers ?? {}
|
|
273
|
-
},
|
|
274
|
-
body: JSON.stringify(transform(event)),
|
|
275
|
-
keepalive: true
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// src/adapters/custom.ts
|
|
282
|
-
function customAnalytics(capture) {
|
|
283
|
-
return { capture };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export { AI_BOT_PATTERN, HTTP_CLIENT_PATTERN, classifyAgent, classifyRequest, customAnalytics, detectHeadless, firstUserAgentProduct, hashId, isAiBot, isHttpClient, parseBotName, posthogAnalytics, trackVisit, webhookAnalytics };
|
|
287
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
var C=/ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i,T=/axios\/|curl\/|(?:^|[\s(])got(?:\/|[\s(])|\bcolly\b|Electron\/|node-fetch\/|python-requests\/|Go-http-client\/|okhttp\/|aiohttp\/|Deno\//i;function d(t){return t?C.test(t):false}function g(t){return t?T.test(t):false}function S(t){if(!t||typeof t!="string")return "Other";let e=t.toLowerCase();return e.includes("chatgpt-user")||e.includes("gptbot")||e.includes("oai-searchbot")||e.includes("openai")?"ChatGPT":e.includes("claudebot")||e.includes("claude-user")||e.includes("claude-searchbot")||e.includes("claude-web")||e.includes("anthropic")?"Claude":e.includes("perplexitybot")||e.includes("perplexity-user")?"Perplexity":e.includes("ccbot")?"Common Crawl":e.includes("google-extended")||e.includes("googlebot")||e.includes("google-cloudvertexbot")||e.includes("google-agent")||e.includes("googleagent-mariner")||e.includes("gemini-deep-research")?"Google":e.includes("applebot")?"Apple":e.includes("bingbot")?"Bing":e.includes("bytespider")?"Bytespider":e.includes("amazonbot")||e.includes("amzn-searchbot")||e.includes("novaact")?"Amazon":e.includes("meta-externalagent")||e.includes("meta-externalfetcher")||e.includes("meta-webindexer")||e.includes("facebookbot")?"Meta":e.includes("mistralai-user")?"Mistral":e.includes("duckassistbot")?"DuckDuckGo":e.includes("youbot")?"You.com":e.includes("diffbot")?"Diffbot":e.includes("ai2bot")?"AI2":e.includes("cohere")?"Cohere":e.includes("cursor")?"Cursor":e.includes("windsurf")?"Windsurf":e.includes("deepseek")?"DeepSeek":e.includes("pangubot")?"Huawei":e.includes("webzio")||e.includes("omgili")?"Webz.io":e.includes("timpibot")?"Timpi":e.includes("grok")||e.includes("xai-")?"xAI":e.includes("manus-user")?"Manus":e.includes("quillbot")?"QuillBot":e.includes("azureai-searchbot")?"Microsoft":e.includes("mycentralaiscraperbot")?"MyCentralAI":e.includes("petalbot")?"PetalBot":e.includes("ahrefsbot")?"Ahrefs":e.includes("semrushbot")?"Semrush":e.includes("mj12bot")?"Majestic":e.includes("dotbot")||e.includes("rogerbot")?"Moz":e.includes("screaming frog")?"Screaming Frog":e.includes("sitebulb")?"Sitebulb":e.includes("linkfluence")?"Linkfluence":e.includes("dataforseo")?"DataForSEO":e.includes("serpstatbot")?"Serpstat":e.includes("uptimerobot")?"UptimeRobot":e.includes("pingdom")?"Pingdom":e.includes("statuscake")?"StatusCake":e.includes("newrelicpinger")?"New Relic":e.includes("datadogagent")||e.includes("datadog")?"Datadog":e.includes("slackbot")?"Slack":e.includes("twitterbot")?"Twitter":e.includes("linkedinbot")?"LinkedIn":e.includes("discordbot")?"Discord":e.includes("telegrambot")?"Telegram":e.includes("whatsapp")?"WhatsApp":e.includes("linkupbot")?"Linkup":e.includes("sogou")?"Sogou":e.includes("yandexbot")?"Yandex":e.includes("baiduspider")?"Baidu":e.includes("facebookexternalhit")?"Facebook":e.includes("com.apple.webkit")?"Apple URL Preview":e.includes("ohdear")?"Oh Dear":e.includes("scrapy")?"Scrapy":e.includes("headlesschrome")?"Headless Chrome":e.includes("phantomjs")?"PhantomJS":e.includes("wget")?"wget":e.includes("httpie")?"HTTPie":e.includes("guzzlehttp")?"Guzzle":e.includes("electron/")?"Electron":/curl\//.test(e)?"curl":/axios\//.test(e)?"axios":/(?:^|[\s(])got(?:\/|[\s(])/.test(e)?"got":/\bcolly\b/.test(e)?"colly":/node-fetch\//.test(e)?"node-fetch":/python-requests\//.test(e)?"python-requests":/go-http-client\//.test(e)?"Go http client":/okhttp\//.test(e)?"OkHttp":/aiohttp\//.test(e)?"aiohttp":/deno\//.test(e)?"Deno":e.includes("mozilla")||e.includes("chrome")||e.includes("safari")||e.includes("firefox")?"Browser":"Other"}function M(t){if(!t||typeof t!="string")return "Other";let e=t.match(/compatible;\s*([^/;\s]+)(?:\/[^\s;]*)?/i);return e&&e[1]?e[1].trim():t.trim().split("/")[0]?.trim().split(/\s+/)[0]?.trim()||"Other"}function h(t){let e=[],n=(t.headers.get("user-agent")||"").toLowerCase();if(!(n.includes("mozilla")||n.includes("chrome")||n.includes("safari")||n.includes("firefox")))return {score:0,signals:[],likely:false};t.headers.get("accept-language")||e.push("missing-accept-language"),t.headers.get("sec-fetch-mode")||e.push("missing-sec-fetch-mode");let r=t.headers.get("sec-ch-ua");r?r.toLowerCase().includes("headlesschrome")&&e.push("headless-chrome-hint"):e.push("missing-sec-ch-ua");let i=t.headers.get("accept")||"";(!i||i==="*/*")&&e.push("missing-or-bare-accept"),(t.headers.get("connection")||"").toLowerCase()==="close"&&e.push("connection-close");let o=e.length;return {score:o,signals:e,likely:o>=2}}function v(t){let e=S(t),n=d(t),s=g(t),r;return n?r="declared-crawler":s?r="coding-agent-hint":e==="Browser"?r="browser":r="other",{kind:r,label:e,isAiBot:n,codingAgentHint:s}}function m(t){let e=t.headers.get("user-agent")||"",n=v(e),s=h(t),r=n.kind,i=n.label;return r==="browser"&&s.likely&&(r="headless-likely",i="Headless"),{...n,kind:r,label:i,headless:s}}var f=class extends Error{constructor(e){super(e),this.name="HashSecretError";}};function y(){let t=globalThis.crypto;if(!t?.subtle)throw new f("Web Crypto is unavailable. agent-analytics requires Node >= 20, or any runtime exposing globalThis.crypto.subtle (Vercel Edge, Cloudflare Workers, Deno, browsers).");return t.subtle}var B=new Map;function z(t){let e=B.get(t);return e||(e=y().importKey("raw",new TextEncoder().encode(t),{name:"HMAC",hash:"SHA-256"},false,["sign"]),B.set(t,e)),e}async function b(t,e){if(typeof e!="string"||e.length===0)throw new f("hashId requires a non-empty secret");let n=await y().sign("HMAC",await z(e),new TextEncoder().encode(t)),s=new Uint8Array(n,0,8),r="";for(let i of s)r+=i.toString(16).padStart(2,"0");return "anon_"+r}function A(){let t=new Uint8Array(32);y(),globalThis.crypto.getRandomValues(t);let e="";for(let n of t)e+=n.toString(16).padStart(2,"0");return e}var x,E=false;function N(t){if(t)return t;let e=typeof process<"u"?process.env?.AGENT_ANALYTICS_ID_SECRET:void 0;return e||(x||(x=A(),E||(E=true,console.warn("[agent-analytics] No idSecret or AGENT_ANALYTICS_ID_SECRET set. Using a per-instance random secret: distinctIds will not correlate across instances or deploys."))),x)}async function G(t,e){let n=t.headers.get("user-agent")||"",s=e.onlyBots??false,r=e.skipBrowsers??false;if(!(s&&!d(n))&&!(r&&!d(n)&&!g(n)&&!h(t).likely))try{let i="/",o="";try{let w=new URL(t.url);i=w.pathname,o=w.origin;}catch{i=t.url||"/";}let p=e.origin??o,k=(t.headers.get("x-forwarded-for")||"").split(",")[0]?.trim()??"",_=t.headers.get("referer"),I=e.captureCountry&&(t.headers.get("x-vercel-ip-country")||t.headers.get("cf-ipcountry")||t.headers.get("x-country-code"))||null,P=e.captureGeo?O(t):null,a=m(t),l=e.verify?e.verify(t):null,H=a.kind==="headless-likely"||a.kind==="browser",R=await b(`${k}:${n}`,N(e.idSecret)),D={event:e.eventName??"agent_visit",distinctId:R,timestamp:new Date().toISOString(),properties:{...e.properties,$process_person_profile:!1,$current_url:p?`${p}${i}`:i,path:i,method:t.method,...e.captureCountry?{country_code:I}:{},...P??{},...e.captureIp?{client_ip:k||null}:{},user_agent:n,is_ai_bot:a.isAiBot,bot_name:a.label,ua_category:a.kind,coding_agent_hint:a.codingAgentHint,...H?{headless_score:a.headless?.score??0,headless_likely:a.headless?.likely??!1}:{},...l?{bot_verified:l.verified,bot_verification:l.verdict,...l.reason?{bot_verification_reason:l.reason}:{}}:{},referer:_,source:e.source??null}};await e.analytics.capture(D);}catch(i){e.onError?.(i instanceof Error?i:new Error(String(i)));}}function O(t){let e=r=>{if(!r)return "";try{return decodeURIComponent(r)}catch{return r}},n=[["region",e(t.headers.get("x-vercel-ip-country-region"))],["city",e(t.headers.get("x-vercel-ip-city"))],["latitude",t.headers.get("x-vercel-ip-latitude")??""],["longitude",t.headers.get("x-vercel-ip-longitude")??""],["timezone",t.headers.get("x-vercel-ip-timezone")??""]],s={};for(let[r,i]of n)i&&(s[r]=i);return s}var c=class extends Error{status;body;constructor(e,n,s){super(e),this.name="CaptureTransportError",this.status=n,this.body=s;}};function U(t){let e=t.host??"https://us.i.posthog.com",n=(/^https?:\/\//.test(e)?e:`https://${e}`).replace(/\/$/,""),s=(t.path??"/i/v0/e/").replace(/^(?!\/)/,"/"),r=`${n}${s}`,i=t.fetchImpl??fetch;return {async capture(o){let p={api_key:t.apiKey,event:o.event,distinct_id:o.distinctId,timestamp:o.timestamp,properties:o.properties},u=await i(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!u.ok)throw new c(`PostHog capture failed: ${u.status} ${u.statusText}`,u.status,await u.text().catch(()=>{}))}}}function L(t){let e=t.fetchImpl??fetch,n=t.transform??(s=>s);return {async capture(s){let r=await e(t.url,{method:"POST",headers:{"Content-Type":"application/json",...t.headers??{}},body:JSON.stringify(n(s)),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!r.ok)throw new c(`Webhook capture failed: ${r.status} ${r.statusText}`,r.status,await r.text().catch(()=>{}))}}}function $(t){return {capture:t}}
|
|
2
|
+
export{C as AI_BOT_PATTERN,c as CaptureTransportError,T as HTTP_CLIENT_PATTERN,f as HashSecretError,v as classifyAgent,m as classifyRequest,$ as customAnalytics,h as detectHeadless,M as firstUserAgentProduct,b as hashId,d as isAiBot,g as isHttpClient,S as parseBotName,U as posthogAnalytics,A as randomSecret,G as trackVisit,L as webhookAnalytics};//# sourceMappingURL=index.js.map
|
|
288
3
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 **publicly declared** AI crawlers — the\n * branded bots that identify themselves by name (OpenAI's GPTBot, Anthropic's\n * ClaudeBot, Perplexity-User, Google-Extended, etc.). High-confidence: when\n * this matches, the request almost certainly comes from that vendor's crawler\n * fleet.\n *\n * Does NOT include **coding-agent traffic** (Claude Code, Cline, Cursor,\n * Windsurf, Aider, OpenCode, VS Code). Those tools use generic HTTP library\n * UAs (axios, curl, got, colly, Electron) or spoof full browser UAs — they\n * can't be distinguished from non-AI traffic by UA alone. See\n * {@link HTTP_CLIENT_PATTERN} for the loose heuristic layer.\n *\n * Sources consulted when updating: darkvisitors.com, vendor docs from OpenAI,\n * Anthropic, Google, Perplexity, Cohere, Apple, Bytedance.\n */\nexport const AI_BOT_PATTERN =\n /ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i\n\n/**\n * HTTP library / runtime signatures frequently used by coding agents. Matching\n * any of these is a **loose** signal — legitimate curl scripts, CI jobs, and\n * server-to-server traffic use the same libraries. Use this for the wider\n * net (`coding_agent_hint: true`) and pair with other signals (request\n * shape, JA4 fingerprint, path patterns) for higher confidence.\n *\n * Based on behavioural signatures observed by Addy Osmani:\n * Claude Code → axios/1.8.4\n * Cline, Junie → curl/8.4.0\n * Cursor → got (sindresorhus/got)\n * Windsurf → colly\n * VS Code → Electron / Chromium\n *\n * Aider and OpenCode use Playwright-driven full Mozilla/Safari UAs and are\n * indistinguishable from real browsers at the UA layer.\n */\nexport const HTTP_CLIENT_PATTERN =\n /axios\\/|curl\\/|(?:^|[\\s(])got(?:\\/|[\\s(])|\\bcolly\\b|Electron\\/|node-fetch\\/|python-requests\\/|Go-http-client\\/|okhttp\\/|aiohttp\\/|Deno\\//i\n\nexport function isAiBot(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return AI_BOT_PATTERN.test(userAgent)\n}\n\nexport function isHttpClient(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return HTTP_CLIENT_PATTERN.test(userAgent)\n}\n\n/**\n * Map a user-agent string to a coarse, human-readable label. Returns one of:\n *\n * - A branded-crawler name (`'Claude'`, `'ChatGPT'`, …) — pair with\n * {@link isAiBot} for `is_ai_bot: true` segmentation.\n * - An HTTP-library name (`'curl'`, `'axios'`, `'got'`, `'colly'`,\n * `'Electron'`, …) — hint of a coding agent or automation; not\n * conclusive. Pair with {@link isHttpClient}.\n * - `'Browser'` for typical desktop browsers (possibly spoofed by\n * Playwright-based agents like Aider/OpenCode — this label alone can't\n * tell you).\n * - `'Other'` for anything unrecognised or empty input.\n */\nexport function parseBotName(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const s = userAgent.toLowerCase()\n\n // Publicly declared AI crawlers (high confidence).\n if (s.includes('chatgpt-user') || s.includes('gptbot') || s.includes('oai-searchbot') || s.includes('openai'))\n return 'ChatGPT'\n if (\n s.includes('claudebot') ||\n s.includes('claude-user') ||\n s.includes('claude-searchbot') ||\n s.includes('claude-web') ||\n s.includes('anthropic')\n )\n return 'Claude'\n if (s.includes('perplexitybot') || s.includes('perplexity-user')) return 'Perplexity'\n if (s.includes('ccbot')) return 'Common Crawl'\n if (\n s.includes('google-extended') ||\n s.includes('googlebot') ||\n s.includes('google-cloudvertexbot') ||\n s.includes('google-agent') ||\n s.includes('googleagent-mariner') ||\n s.includes('gemini-deep-research')\n )\n return 'Google'\n if (s.includes('applebot')) return 'Apple'\n if (s.includes('bingbot')) return 'Bing'\n if (s.includes('bytespider')) return 'Bytespider'\n if (s.includes('amazonbot') || s.includes('amzn-searchbot') || s.includes('novaact')) return 'Amazon'\n if (\n s.includes('meta-externalagent') ||\n s.includes('meta-externalfetcher') ||\n s.includes('meta-webindexer') ||\n s.includes('facebookbot')\n )\n 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('deepseek')) return 'DeepSeek'\n if (s.includes('pangubot')) return 'Huawei'\n if (s.includes('webzio') || s.includes('omgili')) return 'Webz.io'\n if (s.includes('timpibot')) return 'Timpi'\n if (s.includes('grok') || s.includes('xai-')) return 'xAI'\n if (s.includes('manus-user')) return 'Manus'\n if (s.includes('quillbot')) return 'QuillBot'\n if (s.includes('azureai-searchbot')) return 'Microsoft'\n if (s.includes('mycentralaiscraperbot')) return 'MyCentralAI'\n if (s.includes('petalbot')) return 'PetalBot'\n\n // SEO crawlers and monitoring bots.\n if (s.includes('ahrefsbot')) return 'Ahrefs'\n if (s.includes('semrushbot')) return 'Semrush'\n if (s.includes('mj12bot')) return 'Majestic'\n if (s.includes('dotbot')) return 'Moz'\n if (s.includes('rogerbot')) return 'Moz'\n if (s.includes('screaming frog')) return 'Screaming Frog'\n if (s.includes('sitebulb')) return 'Sitebulb'\n if (s.includes('linkfluence')) return 'Linkfluence'\n if (s.includes('dataforseo')) return 'DataForSEO'\n if (s.includes('serpstatbot')) return 'Serpstat'\n\n // Monitoring and feed bots.\n if (s.includes('uptimerobot')) return 'UptimeRobot'\n if (s.includes('pingdom')) return 'Pingdom'\n if (s.includes('statuscake')) return 'StatusCake'\n if (s.includes('newrelicpinger')) return 'New Relic'\n if (s.includes('datadogagent') || s.includes('datadog')) return 'Datadog'\n if (s.includes('slackbot')) return 'Slack'\n if (s.includes('twitterbot')) return 'Twitter'\n if (s.includes('linkedinbot')) return 'LinkedIn'\n if (s.includes('discordbot')) return 'Discord'\n if (s.includes('telegrambot')) return 'Telegram'\n if (s.includes('whatsapp')) return 'WhatsApp'\n\n // AI search and indexing bots.\n if (s.includes('linkupbot')) return 'Linkup'\n if (s.includes('sogou')) return 'Sogou'\n if (s.includes('yandexbot')) return 'Yandex'\n if (s.includes('baiduspider')) return 'Baidu'\n\n // Link preview fetchers.\n if (s.includes('facebookexternalhit')) return 'Facebook'\n if (s.includes('com.apple.webkit')) return 'Apple URL Preview'\n\n // Uptime and monitoring.\n if (s.includes('ohdear')) return 'Oh Dear'\n\n // Generic scrapers.\n if (s.includes('scrapy')) return 'Scrapy'\n if (s.includes('headlesschrome')) return 'Headless Chrome'\n if (s.includes('phantomjs')) return 'PhantomJS'\n if (s.includes('wget')) return 'wget'\n if (s.includes('httpie')) return 'HTTPie'\n if (s.includes('guzzlehttp')) return 'Guzzle'\n\n // HTTP library / runtime signatures (loose — coding agent or automation).\n // Check Electron before Browser since Electron UAs contain Chrome/Safari.\n if (s.includes('electron/')) return 'Electron'\n if (/curl\\//.test(s)) return 'curl'\n if (/axios\\//.test(s)) return 'axios'\n if (/(?:^|[\\s(])got(?:\\/|[\\s(])/.test(s)) return 'got'\n if (/\\bcolly\\b/.test(s)) return 'colly'\n if (/node-fetch\\//.test(s)) return 'node-fetch'\n if (/python-requests\\//.test(s)) return 'python-requests'\n if (/go-http-client\\//.test(s)) return 'Go http client'\n if (/okhttp\\//.test(s)) return 'OkHttp'\n if (/aiohttp\\//.test(s)) return 'aiohttp'\n if (/deno\\//.test(s)) return 'Deno'\n\n // Real browsers (or UAs spoofed to look like them — see Aider/OpenCode note).\n if (s.includes('mozilla') || s.includes('chrome') || s.includes('safari') || s.includes('firefox'))\n return 'Browser'\n\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/**\n * Detect likely headless/automated browsers by checking for missing headers\n * that real browsers always send. Playwright, Puppeteer, and similar tools\n * spoof the UA but often omit standard browser headers.\n *\n * Signals checked (each scores 1 point):\n * - Missing `Accept-Language` — every real browser sends this\n * - Missing `Sec-Fetch-Mode` — sent by all modern browsers\n * - Missing `Sec-CH-UA` — Client Hints, Chromium 89+\n * - `Sec-CH-UA` contains \"HeadlessChrome\"\n * - Missing or bare Accept header — browsers send detailed accept lists\n * - `Connection: close` with browser UA — browsers use keep-alive\n *\n * Returns a score (0-6), the signals that fired, and a boolean `likely`\n * flag (score >= 2 with a browser-like UA).\n */\nexport function detectHeadless(req: Request): HeadlessDetection {\n const signals: string[] = []\n const ua = (req.headers.get('user-agent') || '').toLowerCase()\n const isBrowserUA =\n ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari') || ua.includes('firefox')\n\n if (!isBrowserUA) return { score: 0, signals: [], likely: false }\n\n if (!req.headers.get('accept-language')) {\n signals.push('missing-accept-language')\n }\n if (!req.headers.get('sec-fetch-mode')) {\n signals.push('missing-sec-fetch-mode')\n }\n const secChUa = req.headers.get('sec-ch-ua')\n if (!secChUa) {\n signals.push('missing-sec-ch-ua')\n } else if (secChUa.toLowerCase().includes('headlesschrome')) {\n signals.push('headless-chrome-hint')\n }\n const accept = req.headers.get('accept') || ''\n if (!accept || accept === '*/*') {\n signals.push('missing-or-bare-accept')\n }\n if ((req.headers.get('connection') || '').toLowerCase() === 'close') {\n signals.push('connection-close')\n }\n\n const score = signals.length\n return { score, signals, likely: score >= 2 }\n}\n\nexport interface HeadlessDetection {\n /** Number of suspicious signals found (0-6). */\n score: number\n /** Names of the specific signals that fired. */\n signals: string[]\n /** True when score >= 2 — strong headless indication. */\n likely: boolean\n}\n\nexport type AgentKind =\n | 'declared-crawler'\n | 'coding-agent-hint'\n | 'headless-likely'\n | 'browser'\n | 'other'\n\nexport interface AgentClassification {\n /**\n * Categorical tag for the request:\n *\n * - `'declared-crawler'` — {@link AI_BOT_PATTERN} matched. High confidence.\n * - `'coding-agent-hint'` — {@link HTTP_CLIENT_PATTERN} matched. Loose\n * signal; could be a coding agent, a curl script, or any automation.\n * - `'headless-likely'` — Browser-like UA but missing standard headers.\n * Strong signal of Playwright/Puppeteer automation (Aider, OpenCode, etc.).\n * - `'browser'` — Looks like a real browser with expected headers present.\n * - `'other'` — Unrecognised or empty.\n */\n kind: AgentKind\n /** Human-readable label, same string {@link parseBotName} returns. */\n label: string\n /** Strict: `true` only when the UA matches a branded AI crawler. */\n isAiBot: boolean\n /** Loose: `true` for known HTTP-library / automation UAs. */\n codingAgentHint: boolean\n /** Headless browser detection result. Only populated when `req` is passed. */\n headless?: HeadlessDetection\n}\n\n/**\n * UA-only classification. Use {@link classifyRequest} for full detection\n * including headless browser heuristics.\n */\nexport function classifyAgent(userAgent: string | null | undefined): AgentClassification {\n const label = parseBotName(userAgent)\n const aiBot = isAiBot(userAgent)\n const httpClient = isHttpClient(userAgent)\n\n let kind: AgentKind\n if (aiBot) kind = 'declared-crawler'\n else if (httpClient) kind = 'coding-agent-hint'\n else if (label === 'Browser') kind = 'browser'\n else kind = 'other'\n\n return { kind, label, isAiBot: aiBot, codingAgentHint: httpClient }\n}\n\n/**\n * Full request classification — combines UA parsing with header-based\n * headless detection. When a browser-like UA is missing standard headers,\n * the kind is promoted from `'browser'` to `'headless-likely'`.\n */\nexport function classifyRequest(req: Request): AgentClassification {\n const userAgent = req.headers.get('user-agent') || ''\n const base = classifyAgent(userAgent)\n const headless = detectHeadless(req)\n\n let kind = base.kind\n if (kind === 'browser' && headless.likely) {\n kind = 'headless-likely'\n }\n\n return { ...base, kind, headless }\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 { classifyRequest, detectHeadless, isAiBot, isHttpClient } from './bots.js'\nimport { hashId } from './hash.js'\nimport type { TrackVisitOptions } 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 * By default, captures every request so coding-agent traffic (axios, curl,\n * Electron, …) shows up alongside branded crawlers. Set `onlyBots: true` to\n * restrict capture to UAs matching {@link AI_BOT_PATTERN}.\n */\nexport async function trackVisit(\n req: Request,\n opts: TrackVisitOptions\n): Promise<void> {\n const userAgent = req.headers.get('user-agent') || ''\n\n const onlyBots = opts.onlyBots ?? false\n const skipBrowsers = opts.skipBrowsers ?? false\n if (onlyBots && !isAiBot(userAgent)) return\n if (skipBrowsers && !isAiBot(userAgent) && !isHttpClient(userAgent)) {\n // Not a declared bot or HTTP client — check headless heuristics.\n // Playwright-based agents (Aider, OpenCode) will pass if they're missing\n // standard browser headers. Real browsers get skipped.\n if (!detectHeadless(req).likely) return\n }\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 const country = opts.captureCountry\n ? req.headers.get('x-vercel-ip-country') ||\n req.headers.get('cf-ipcountry') ||\n req.headers.get('x-country-code') ||\n null\n : null\n const geo = opts.captureGeo ? extractGeo(req) : null\n const classification = classifyRequest(req)\n\n const event = {\n event: opts.eventName ?? 'agent_visit',\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 method: req.method,\n ...(opts.captureCountry ? { country_code: country } : {}),\n ...(geo ?? {}),\n ...(opts.captureIp ? { client_ip: ip || null } : {}),\n user_agent: userAgent,\n is_ai_bot: classification.isAiBot,\n bot_name: classification.label,\n ua_category: classification.kind,\n coding_agent_hint: classification.codingAgentHint,\n headless_score: classification.headless?.score ?? 0,\n headless_likely: classification.headless?.likely ?? false,\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\n// Vercel edge URL-encodes city/region (e.g. `San%20Francisco`); decode so\n// downstream consumers don't have to. Numeric fields (lat/lng) and timezone\n// pass through untouched. Headers without a value are dropped rather than\n// emitted as empty strings.\nfunction extractGeo(req: Request): Record<string, string> {\n const decode = (v: string | null) => {\n if (!v) return ''\n try {\n return decodeURIComponent(v)\n } catch {\n return v\n }\n }\n const fields: Array<[string, string]> = [\n ['region', decode(req.headers.get('x-vercel-ip-country-region'))],\n ['city', decode(req.headers.get('x-vercel-ip-city'))],\n ['latitude', req.headers.get('x-vercel-ip-latitude') ?? ''],\n ['longitude', req.headers.get('x-vercel-ip-longitude') ?? ''],\n ['timezone', req.headers.get('x-vercel-ip-timezone') ?? '']\n ]\n const out: Record<string, string> = {}\n for (const [k, v] of fields) if (v) out[k] = v\n return out\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/hash.ts","../src/track.ts","../src/errors.ts","../src/adapters/posthog.ts","../src/adapters/webhook.ts","../src/adapters/custom.ts"],"names":["AI_BOT_PATTERN","HTTP_CLIENT_PATTERN","isAiBot","userAgent","isHttpClient","parseBotName","s","firstUserAgentProduct","compatibleMatch","detectHeadless","req","signals","ua","secChUa","accept","score","classifyAgent","label","aiBot","httpClient","kind","classifyRequest","base","headless","HashSecretError","message","subtle","c","KEYS","keyFor","secret","k","hashId","input","sig","bytes","out","b","randomSecret","x","fallbackSecret","warnedNoSecret","resolveSecret","explicit","fromEnv","trackVisit","opts","onlyBots","skipBrowsers","pathname","originFromUrl","url","origin","ip","referer","country","geo","extractGeo","classification","verification","headlessMeaningful","distinctId","event","err","decode","v","fields","CaptureTransportError","status","body","posthogAnalytics","config","hostRaw","path","endpoint","fetchImpl","payload","res","webhookAnalytics","transform","e","customAnalytics","capture"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * User-agent substrings that identify **publicly declared** AI crawlers — the\n * branded bots that identify themselves by name (OpenAI's GPTBot, Anthropic's\n * ClaudeBot, Perplexity-User, Google-Extended, etc.). High-confidence: when\n * this matches, the request almost certainly comes from that vendor's crawler\n * fleet.\n *\n * Does NOT include **coding-agent traffic** (Claude Code, Cline, Cursor,\n * Windsurf, Aider, OpenCode, VS Code). Those tools use generic HTTP library\n * UAs (axios, curl, got, colly, Electron) or spoof full browser UAs — they\n * can't be distinguished from non-AI traffic by UA alone. See\n * {@link HTTP_CLIENT_PATTERN} for the loose heuristic layer.\n *\n * Sources consulted when updating: darkvisitors.com, vendor docs from OpenAI,\n * Anthropic, Google, Perplexity, Cohere, Apple, Bytedance.\n */\nexport const AI_BOT_PATTERN =\n /ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i\n\n/**\n * HTTP library / runtime signatures frequently used by coding agents. Matching\n * any of these is a **loose** signal — legitimate curl scripts, CI jobs, and\n * server-to-server traffic use the same libraries. Use this for the wider\n * net (`coding_agent_hint: true`) and pair with other signals (request\n * shape, JA4 fingerprint, path patterns) for higher confidence.\n *\n * Based on behavioural signatures observed by Addy Osmani:\n * Claude Code → axios/1.8.4\n * Cline, Junie → curl/8.4.0\n * Cursor → got (sindresorhus/got)\n * Windsurf → colly\n * VS Code → Electron / Chromium\n *\n * Aider and OpenCode use Playwright-driven full Mozilla/Safari UAs and are\n * indistinguishable from real browsers at the UA layer.\n */\nexport const HTTP_CLIENT_PATTERN =\n /axios\\/|curl\\/|(?:^|[\\s(])got(?:\\/|[\\s(])|\\bcolly\\b|Electron\\/|node-fetch\\/|python-requests\\/|Go-http-client\\/|okhttp\\/|aiohttp\\/|Deno\\//i\n\nexport function isAiBot(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return AI_BOT_PATTERN.test(userAgent)\n}\n\nexport function isHttpClient(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return HTTP_CLIENT_PATTERN.test(userAgent)\n}\n\n/**\n * Map a user-agent string to a coarse, human-readable label. Returns one of:\n *\n * - A branded-crawler name (`'Claude'`, `'ChatGPT'`, …) — pair with\n * {@link isAiBot} for `is_ai_bot: true` segmentation.\n * - An HTTP-library name (`'curl'`, `'axios'`, `'got'`, `'colly'`,\n * `'Electron'`, …) — hint of a coding agent or automation; not\n * conclusive. Pair with {@link isHttpClient}.\n * - `'Browser'` for typical desktop browsers (possibly spoofed by\n * Playwright-based agents like Aider/OpenCode — this label alone can't\n * tell you).\n * - `'Other'` for anything unrecognised or empty input.\n */\nexport function parseBotName(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const s = userAgent.toLowerCase()\n\n // Publicly declared AI crawlers (high confidence).\n if (s.includes('chatgpt-user') || s.includes('gptbot') || s.includes('oai-searchbot') || s.includes('openai'))\n return 'ChatGPT'\n if (\n s.includes('claudebot') ||\n s.includes('claude-user') ||\n s.includes('claude-searchbot') ||\n s.includes('claude-web') ||\n s.includes('anthropic')\n )\n return 'Claude'\n if (s.includes('perplexitybot') || s.includes('perplexity-user')) return 'Perplexity'\n if (s.includes('ccbot')) return 'Common Crawl'\n if (\n s.includes('google-extended') ||\n s.includes('googlebot') ||\n s.includes('google-cloudvertexbot') ||\n s.includes('google-agent') ||\n s.includes('googleagent-mariner') ||\n s.includes('gemini-deep-research')\n )\n return 'Google'\n if (s.includes('applebot')) return 'Apple'\n if (s.includes('bingbot')) return 'Bing'\n if (s.includes('bytespider')) return 'Bytespider'\n if (s.includes('amazonbot') || s.includes('amzn-searchbot') || s.includes('novaact')) return 'Amazon'\n if (\n s.includes('meta-externalagent') ||\n s.includes('meta-externalfetcher') ||\n s.includes('meta-webindexer') ||\n s.includes('facebookbot')\n )\n 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('deepseek')) return 'DeepSeek'\n if (s.includes('pangubot')) return 'Huawei'\n if (s.includes('webzio') || s.includes('omgili')) return 'Webz.io'\n if (s.includes('timpibot')) return 'Timpi'\n if (s.includes('grok') || s.includes('xai-')) return 'xAI'\n if (s.includes('manus-user')) return 'Manus'\n if (s.includes('quillbot')) return 'QuillBot'\n if (s.includes('azureai-searchbot')) return 'Microsoft'\n if (s.includes('mycentralaiscraperbot')) return 'MyCentralAI'\n if (s.includes('petalbot')) return 'PetalBot'\n\n // SEO crawlers and monitoring bots.\n if (s.includes('ahrefsbot')) return 'Ahrefs'\n if (s.includes('semrushbot')) return 'Semrush'\n if (s.includes('mj12bot')) return 'Majestic'\n if (s.includes('dotbot')) return 'Moz'\n if (s.includes('rogerbot')) return 'Moz'\n if (s.includes('screaming frog')) return 'Screaming Frog'\n if (s.includes('sitebulb')) return 'Sitebulb'\n if (s.includes('linkfluence')) return 'Linkfluence'\n if (s.includes('dataforseo')) return 'DataForSEO'\n if (s.includes('serpstatbot')) return 'Serpstat'\n\n // Monitoring and feed bots.\n if (s.includes('uptimerobot')) return 'UptimeRobot'\n if (s.includes('pingdom')) return 'Pingdom'\n if (s.includes('statuscake')) return 'StatusCake'\n if (s.includes('newrelicpinger')) return 'New Relic'\n if (s.includes('datadogagent') || s.includes('datadog')) return 'Datadog'\n if (s.includes('slackbot')) return 'Slack'\n if (s.includes('twitterbot')) return 'Twitter'\n if (s.includes('linkedinbot')) return 'LinkedIn'\n if (s.includes('discordbot')) return 'Discord'\n if (s.includes('telegrambot')) return 'Telegram'\n if (s.includes('whatsapp')) return 'WhatsApp'\n\n // AI search and indexing bots.\n if (s.includes('linkupbot')) return 'Linkup'\n if (s.includes('sogou')) return 'Sogou'\n if (s.includes('yandexbot')) return 'Yandex'\n if (s.includes('baiduspider')) return 'Baidu'\n\n // Link preview fetchers.\n if (s.includes('facebookexternalhit')) return 'Facebook'\n if (s.includes('com.apple.webkit')) return 'Apple URL Preview'\n\n // Uptime and monitoring.\n if (s.includes('ohdear')) return 'Oh Dear'\n\n // Generic scrapers.\n if (s.includes('scrapy')) return 'Scrapy'\n if (s.includes('headlesschrome')) return 'Headless Chrome'\n if (s.includes('phantomjs')) return 'PhantomJS'\n if (s.includes('wget')) return 'wget'\n if (s.includes('httpie')) return 'HTTPie'\n if (s.includes('guzzlehttp')) return 'Guzzle'\n\n // HTTP library / runtime signatures (loose — coding agent or automation).\n // Check Electron before Browser since Electron UAs contain Chrome/Safari.\n if (s.includes('electron/')) return 'Electron'\n if (/curl\\//.test(s)) return 'curl'\n if (/axios\\//.test(s)) return 'axios'\n if (/(?:^|[\\s(])got(?:\\/|[\\s(])/.test(s)) return 'got'\n if (/\\bcolly\\b/.test(s)) return 'colly'\n if (/node-fetch\\//.test(s)) return 'node-fetch'\n if (/python-requests\\//.test(s)) return 'python-requests'\n if (/go-http-client\\//.test(s)) return 'Go http client'\n if (/okhttp\\//.test(s)) return 'OkHttp'\n if (/aiohttp\\//.test(s)) return 'aiohttp'\n if (/deno\\//.test(s)) return 'Deno'\n\n // Real browsers (or UAs spoofed to look like them — see Aider/OpenCode note).\n if (s.includes('mozilla') || s.includes('chrome') || s.includes('safari') || s.includes('firefox'))\n return 'Browser'\n\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/**\n * Detect likely headless/automated browsers by checking for missing headers\n * that real browsers always send. Playwright, Puppeteer, and similar tools\n * spoof the UA but often omit standard browser headers.\n *\n * Signals checked (each scores 1 point):\n * - Missing `Accept-Language` — every real browser sends this\n * - Missing `Sec-Fetch-Mode` — sent by all modern browsers\n * - Missing `Sec-CH-UA` — Client Hints, Chromium 89+\n * - `Sec-CH-UA` contains \"HeadlessChrome\"\n * - Missing or bare Accept header — browsers send detailed accept lists\n * - `Connection: close` with browser UA — browsers use keep-alive\n *\n * Returns a score (0-6), the signals that fired, and a boolean `likely`\n * flag (score >= 2 with a browser-like UA).\n */\nexport function detectHeadless(req: Request): HeadlessDetection {\n const signals: string[] = []\n const ua = (req.headers.get('user-agent') || '').toLowerCase()\n const isBrowserUA =\n ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari') || ua.includes('firefox')\n\n if (!isBrowserUA) return { score: 0, signals: [], likely: false }\n\n if (!req.headers.get('accept-language')) {\n signals.push('missing-accept-language')\n }\n if (!req.headers.get('sec-fetch-mode')) {\n signals.push('missing-sec-fetch-mode')\n }\n const secChUa = req.headers.get('sec-ch-ua')\n if (!secChUa) {\n signals.push('missing-sec-ch-ua')\n } else if (secChUa.toLowerCase().includes('headlesschrome')) {\n signals.push('headless-chrome-hint')\n }\n const accept = req.headers.get('accept') || ''\n if (!accept || accept === '*/*') {\n signals.push('missing-or-bare-accept')\n }\n if ((req.headers.get('connection') || '').toLowerCase() === 'close') {\n signals.push('connection-close')\n }\n\n const score = signals.length\n return { score, signals, likely: score >= 2 }\n}\n\nexport interface HeadlessDetection {\n /** Number of suspicious signals found (0-6). */\n score: number\n /** Names of the specific signals that fired. */\n signals: string[]\n /** True when score >= 2 — strong headless indication. */\n likely: boolean\n}\n\nexport type AgentKind =\n | 'declared-crawler'\n | 'coding-agent-hint'\n | 'headless-likely'\n | 'browser'\n | 'other'\n\nexport interface AgentClassification {\n /**\n * Categorical tag for the request:\n *\n * - `'declared-crawler'` — {@link AI_BOT_PATTERN} matched. High confidence.\n * - `'coding-agent-hint'` — {@link HTTP_CLIENT_PATTERN} matched. Loose\n * signal; could be a coding agent, a curl script, or any automation.\n * - `'headless-likely'` — Browser-like UA but missing standard headers.\n * Strong signal of Playwright/Puppeteer automation (Aider, OpenCode, etc.).\n * - `'browser'` — Looks like a real browser with expected headers present.\n * - `'other'` — Unrecognised or empty.\n */\n kind: AgentKind\n /** Human-readable label, same string {@link parseBotName} returns. */\n label: string\n /** Strict: `true` only when the UA matches a branded AI crawler. */\n isAiBot: boolean\n /** Loose: `true` for known HTTP-library / automation UAs. */\n codingAgentHint: boolean\n /** Headless browser detection result. Only populated when `req` is passed. */\n headless?: HeadlessDetection\n}\n\n/**\n * UA-only classification. Use {@link classifyRequest} for full detection\n * including headless browser heuristics.\n */\nexport function classifyAgent(userAgent: string | null | undefined): AgentClassification {\n const label = parseBotName(userAgent)\n const aiBot = isAiBot(userAgent)\n const httpClient = isHttpClient(userAgent)\n\n let kind: AgentKind\n if (aiBot) kind = 'declared-crawler'\n else if (httpClient) kind = 'coding-agent-hint'\n else if (label === 'Browser') kind = 'browser'\n else kind = 'other'\n\n return { kind, label, isAiBot: aiBot, codingAgentHint: httpClient }\n}\n\n/**\n * Full request classification — combines UA parsing with header-based\n * headless detection. When a browser-like UA is missing standard headers,\n * the kind is promoted from `'browser'` to `'headless-likely'`.\n */\nexport function classifyRequest(req: Request): AgentClassification {\n const userAgent = req.headers.get('user-agent') || ''\n const base = classifyAgent(userAgent)\n const headless = detectHeadless(req)\n\n let kind = base.kind\n let label = base.label\n if (kind === 'browser' && headless.likely) {\n kind = 'headless-likely'\n // Relabel too. Leaving it as 'Browser' meant automation with a spoofed\n // browser UA — 79% of one production site's agent traffic — was\n // indistinguishable from a human in any `bot_name` breakdown, and was\n // silently excluded by the obvious `bot_name != 'Browser'` filter.\n label = 'Headless'\n }\n\n return { ...base, kind, label, headless }\n}\n","/**\n * Keyed, non-reversible anonymous identifiers.\n *\n * The previous implementation was an unsalted 32-bit djb2 over `ip:userAgent`.\n * Because the user agent is emitted in plaintext on the same event, an attacker\n * held half the preimage and only had to search the IPv4 space — recovering a\n * residential IP took 75 seconds single-threaded. That is pseudonymisation, not\n * anonymisation, and it does not survive GDPR Recital 26.\n *\n * This uses HMAC-SHA-256 with a caller-supplied secret, truncated to 64 bits.\n * Web Crypto is available on Vercel Edge, Cloudflare Workers, Deno and Node 18+.\n */\n\n/** Thrown when a secret is missing or unusable. */\nexport class HashSecretError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'HashSecretError'\n }\n}\n\n/**\n * Web Crypto, or a clear error explaining why it is missing.\n *\n * `globalThis.crypto` is present by default from Node 19; on Node 18 it sat\n * behind `--experimental-global-webcrypto`. We require Node >= 20 rather than\n * shipping a `node:crypto` fallback, because a static import of a Node builtin\n * breaks bundling for the edge runtimes this library primarily targets — and\n * Node 18 reached end of life in April 2025.\n */\nfunction subtle(): SubtleCrypto {\n const c = globalThis.crypto\n if (!c?.subtle) {\n throw new HashSecretError(\n 'Web Crypto is unavailable. agent-analytics requires Node >= 20, or any ' +\n 'runtime exposing globalThis.crypto.subtle (Vercel Edge, Cloudflare ' +\n 'Workers, Deno, browsers).'\n )\n }\n return c.subtle\n}\n\n// Importing a CryptoKey costs more than the signature itself, so keep one per\n// secret. Bounded by however many secrets a process configures — realistically\n// one.\nconst KEYS = new Map<string, Promise<CryptoKey>>()\n\nfunction keyFor(secret: string): Promise<CryptoKey> {\n let k = KEYS.get(secret)\n if (!k) {\n k = subtle().importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n KEYS.set(secret, k)\n }\n return k\n}\n\n/**\n * Hash `input` under `secret`, returning `anon_` followed by 16 hex characters\n * (64 bits — collision-free well past any realistic distinct-visitor count).\n *\n * The secret must be stable across instances for identifiers to be comparable\n * over time, and secret from anyone who can read your events: publishing it\n * makes the identifier exactly as reversible as the old implementation was.\n * Rotating it deliberately breaks continuity, which is correct behaviour for a\n * privacy-preserving id.\n */\nexport async function hashId(input: string, secret: string): Promise<string> {\n if (typeof secret !== 'string' || secret.length === 0) {\n throw new HashSecretError('hashId requires a non-empty secret')\n }\n const sig = await subtle().sign('HMAC', await keyFor(secret), new TextEncoder().encode(input))\n const bytes = new Uint8Array(sig, 0, 8)\n let out = ''\n for (const b of bytes) out += b.toString(16).padStart(2, '0')\n return 'anon_' + out\n}\n\n/**\n * Generate a random secret. Used as the default when none is configured, so the\n * privacy-preserving path is the one you get by doing nothing. Identifiers are\n * then only stable within a single instance's lifetime — set a real secret when\n * you need them comparable across instances and over time.\n */\nexport function randomSecret(): string {\n const b = new Uint8Array(32)\n subtle() // surface the same clear error if Web Crypto is missing\n globalThis.crypto.getRandomValues(b)\n let out = ''\n for (const x of b) out += x.toString(16).padStart(2, '0')\n return out\n}\n","import { classifyRequest, detectHeadless, isAiBot, isHttpClient } from './bots.js'\nimport { hashId, randomSecret } from './hash.js'\nimport type { TrackVisitOptions } from './types.js'\n\n/**\n * Fallback secret, generated once per instance. Keeps the default path\n * privacy-preserving rather than making callers opt in to safety, at the cost\n * of identifiers that only correlate within one instance's lifetime.\n */\nlet fallbackSecret: string | undefined\nlet warnedNoSecret = false\n\nfunction resolveSecret(explicit: string | undefined): string {\n if (explicit) return explicit\n const fromEnv =\n typeof process !== 'undefined' ? process.env?.AGENT_ANALYTICS_ID_SECRET : undefined\n if (fromEnv) return fromEnv\n if (!fallbackSecret) {\n fallbackSecret = randomSecret()\n if (!warnedNoSecret) {\n warnedNoSecret = true\n // Once per instance, not once per request.\n console.warn(\n '[agent-analytics] No idSecret or AGENT_ANALYTICS_ID_SECRET set. ' +\n 'Using a per-instance random secret: distinctIds will not correlate ' +\n 'across instances or deploys.'\n )\n }\n }\n return fallbackSecret\n}\n\n/**\n * Capture an event describing the incoming request. Fire-and-forget: awaits the\n * adapter but routes errors to {@link TrackVisitOptions.onError} rather than\n * letting them reach the response path. Callers typically don't await it.\n *\n * By default, captures every request so coding-agent traffic (axios, curl,\n * Electron, …) shows up alongside branded crawlers. Set `onlyBots: true` to\n * restrict capture to UAs matching {@link AI_BOT_PATTERN}.\n */\nexport async function trackVisit(req: Request, opts: TrackVisitOptions): Promise<void> {\n const userAgent = req.headers.get('user-agent') || ''\n\n const onlyBots = opts.onlyBots ?? false\n const skipBrowsers = opts.skipBrowsers ?? false\n if (onlyBots && !isAiBot(userAgent)) return\n if (skipBrowsers && !isAiBot(userAgent) && !isHttpClient(userAgent)) {\n // Not a declared bot or HTTP client — check headless heuristics.\n // Playwright-based agents (Aider, OpenCode) will pass if they're missing\n // standard browser headers. Real browsers get skipped.\n if (!detectHeadless(req).likely) return\n }\n\n try {\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 const country = opts.captureCountry\n ? req.headers.get('x-vercel-ip-country') ||\n req.headers.get('cf-ipcountry') ||\n req.headers.get('x-country-code') ||\n null\n : null\n const geo = opts.captureGeo ? extractGeo(req) : null\n const classification = classifyRequest(req)\n\n // Verification is injected rather than imported, so the published IP range\n // tables only reach bundles that actually use them. Import `verifyRequest`\n // from `@apideck/agent-analytics/verify` and pass it as `verify`.\n const verification = opts.verify ? opts.verify(req) : null\n\n // Headless scoring only discriminates for browser-shaped UAs. On a declared\n // crawler or an HTTP client it fires on nearly everything — measured true on\n // 99% of captured events — so it reads as signal when it is noise. Omitted\n // rather than emitted as a near-constant.\n const headlessMeaningful =\n classification.kind === 'headless-likely' || classification.kind === 'browser'\n\n const distinctId = await hashId(`${ip}:${userAgent}`, resolveSecret(opts.idSecret))\n\n const event = {\n event: opts.eventName ?? 'agent_visit',\n distinctId,\n timestamp: new Date().toISOString(),\n properties: {\n // Caller properties are spread FIRST so library-computed fields always\n // win. Spreading them last let a colliding key silently overwrite\n // bot_name or is_ai_bot — corrupting the very classification they were\n // meant to annotate.\n ...opts.properties,\n $process_person_profile: false,\n $current_url: origin ? `${origin}${pathname}` : pathname,\n path: pathname,\n method: req.method,\n ...(opts.captureCountry ? { country_code: country } : {}),\n ...(geo ?? {}),\n ...(opts.captureIp ? { client_ip: ip || null } : {}),\n user_agent: userAgent,\n is_ai_bot: classification.isAiBot,\n bot_name: classification.label,\n ua_category: classification.kind,\n coding_agent_hint: classification.codingAgentHint,\n ...(headlessMeaningful\n ? {\n headless_score: classification.headless?.score ?? 0,\n headless_likely: classification.headless?.likely ?? false\n }\n : {}),\n ...(verification\n ? {\n bot_verified: verification.verified,\n bot_verification: verification.verdict,\n ...(verification.reason ? { bot_verification_reason: verification.reason } : {})\n }\n : {}),\n referer,\n source: opts.source ?? null\n }\n }\n\n await opts.analytics.capture(event)\n } catch (err) {\n // Analytics must never affect the response — but silence is how a wrong API\n // key goes unnoticed for a week, so surface it when the caller asks.\n opts.onError?.(err instanceof Error ? err : new Error(String(err)))\n }\n}\n\n// Vercel edge URL-encodes city/region (e.g. `San%20Francisco`); decode so\n// downstream consumers don't have to. Numeric fields (lat/lng) and timezone\n// pass through untouched. Headers without a value are dropped rather than\n// emitted as empty strings.\nfunction extractGeo(req: Request): Record<string, string> {\n const decode = (v: string | null) => {\n if (!v) return ''\n try {\n return decodeURIComponent(v)\n } catch {\n return v\n }\n }\n const fields: Array<[string, string]> = [\n ['region', decode(req.headers.get('x-vercel-ip-country-region'))],\n ['city', decode(req.headers.get('x-vercel-ip-city'))],\n ['latitude', req.headers.get('x-vercel-ip-latitude') ?? ''],\n ['longitude', req.headers.get('x-vercel-ip-longitude') ?? ''],\n ['timezone', req.headers.get('x-vercel-ip-timezone') ?? '']\n ]\n const out: Record<string, string> = {}\n for (const [k, v] of fields) if (v) out[k] = v\n return out\n}\n","/** Thrown when the analytics backend rejects, errors, or times out a capture. */\nexport class CaptureTransportError extends Error {\n readonly status: number | undefined\n readonly body: string | undefined\n constructor(message: string, status?: number, body?: string) {\n super(message)\n this.name = 'CaptureTransportError'\n this.status = status\n this.body = body\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\nimport { CaptureTransportError } from '../errors.js'\n\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 * Abort the capture after this many milliseconds. Defaults to 3000. Without\n * a bound, a hung backend leaves a pending promise for the lifetime of an\n * edge invocation.\n */\n timeoutMs?: number\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 // A 401 from a mistyped key used to look identical to success. Surface\n // it: `trackVisit` routes it to `onError` and still never throws into\n // the response path.\n const res = await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true,\n signal: AbortSignal.timeout(config.timeoutMs ?? 3000)\n })\n if (!res.ok) {\n throw new CaptureTransportError(\n `PostHog capture failed: ${res.status} ${res.statusText}`,\n res.status,\n await res.text().catch(() => undefined)\n )\n }\n }\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\nimport { CaptureTransportError } from '../errors.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 /** Abort the capture after this many milliseconds. Defaults to 3000. */\n timeoutMs?: number\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 const res = 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 signal: AbortSignal.timeout(config.timeoutMs ?? 3000)\n })\n if (!res.ok) {\n throw new CaptureTransportError(\n `Webhook capture failed: ${res.status} ${res.statusText}`,\n res.status,\n await res.text().catch(() => undefined)\n )\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"]}
|