@agentstats/sdk 0.4.1 → 0.5.1

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/dist/server.js CHANGED
@@ -1,164 +1,5 @@
1
- //#region src/bots.ts
2
- /** Categories that count as "AI traffic". Search engines and other bots are shown separately. */
3
- const AI_CATEGORIES = [
4
- "training",
5
- "search",
6
- "assistant",
7
- "agent"
8
- ];
9
- const isAiCategory = (c) => AI_CATEGORIES.includes(c);
10
- /** Products group several bots from one vendor ("ChatGPT" = GPTBot + OAI-SearchBot + ChatGPT-User + ChatGPT agent). */
11
- const BOT_PRODUCTS = {
12
- chatgpt: {
13
- name: "ChatGPT",
14
- operator: "OpenAI"
15
- },
16
- claude: {
17
- name: "Claude",
18
- operator: "Anthropic"
19
- },
20
- perplexity: {
21
- name: "Perplexity",
22
- operator: "Perplexity"
23
- },
24
- gemini: {
25
- name: "Gemini",
26
- operator: "Google"
27
- },
28
- copilot: {
29
- name: "Copilot & Bing",
30
- operator: "Microsoft"
31
- },
32
- meta: {
33
- name: "Meta AI",
34
- operator: "Meta"
35
- },
36
- apple: {
37
- name: "Apple",
38
- operator: "Apple"
39
- },
40
- amazon: {
41
- name: "Amazon",
42
- operator: "Amazon"
43
- },
44
- mistral: {
45
- name: "Mistral",
46
- operator: "Mistral AI"
47
- },
48
- deepseek: {
49
- name: "DeepSeek",
50
- operator: "DeepSeek"
51
- },
52
- bytedance: {
53
- name: "ByteDance",
54
- operator: "ByteDance"
55
- },
56
- xai: {
57
- name: "Grok",
58
- operator: "xAI"
59
- },
60
- duckduckgo: {
61
- name: "DuckDuckGo",
62
- operator: "DuckDuckGo"
63
- },
64
- commoncrawl: {
65
- name: "Common Crawl",
66
- operator: "Common Crawl Foundation"
67
- },
68
- cohere: {
69
- name: "Cohere",
70
- operator: "Cohere"
71
- },
72
- you: {
73
- name: "You.com",
74
- operator: "You.com"
75
- },
76
- qwen: {
77
- name: "Qwen",
78
- operator: "Alibaba"
79
- },
80
- kimi: {
81
- name: "Kimi",
82
- operator: "Moonshot AI"
83
- },
84
- baidu: {
85
- name: "Baidu",
86
- operator: "Baidu"
87
- },
88
- yandex: {
89
- name: "Yandex",
90
- operator: "Yandex"
91
- },
92
- huawei: {
93
- name: "Huawei",
94
- operator: "Huawei"
95
- },
96
- ai2: {
97
- name: "Ai2",
98
- operator: "Allen Institute for AI"
99
- },
100
- brave: {
101
- name: "Brave",
102
- operator: "Brave"
103
- },
104
- kagi: {
105
- name: "Kagi",
106
- operator: "Kagi"
107
- },
108
- phind: {
109
- name: "Phind",
110
- operator: "Phind"
111
- },
112
- poe: {
113
- name: "Poe",
114
- operator: "Quora"
115
- },
116
- manus: {
117
- name: "Manus",
118
- operator: "Butterfly Effect"
119
- },
120
- coding: {
121
- name: "Coding agents",
122
- operator: "Various"
123
- },
124
- aidata: {
125
- name: "AI data providers",
126
- operator: "Various"
127
- },
128
- google: {
129
- name: "Google Search",
130
- operator: "Google"
131
- },
132
- automation: {
133
- name: "Automated browsers",
134
- operator: "Various"
135
- },
136
- seo: {
137
- name: "SEO tools",
138
- operator: "Various"
139
- },
140
- social: {
141
- name: "Link previews",
142
- operator: "Various"
143
- },
144
- monitor: {
145
- name: "Uptime monitors",
146
- operator: "Various"
147
- },
148
- tools: {
149
- name: "HTTP clients & scripts",
150
- operator: "Various"
151
- },
152
- other: {
153
- name: "Other bots",
154
- operator: "Unknown"
155
- }
156
- };
157
- /**
158
- * Order matters: the first matching pattern wins, so specific tokens come
159
- * before the vendor's generic ones and the catch-all patterns come last.
160
- */
161
- const BOT_PATTERNS = [
1
+ // src/bots.curated.ts
2
+ const CURATED = [
162
3
  {
163
4
  id: "chatgpt-agent",
164
5
  name: "ChatGPT agent",
@@ -227,7 +68,7 @@ const BOT_PATTERNS = [
227
68
  name: "Perplexity-User",
228
69
  product: "perplexity",
229
70
  category: "assistant",
230
- pattern: /Perplexity-User/i
71
+ pattern: /Perplexity-?User/i
231
72
  },
232
73
  {
233
74
  id: "perplexitybot",
@@ -635,6 +476,13 @@ const BOT_PATTERNS = [
635
476
  category: "assistant",
636
477
  pattern: /FirecrawlAgent|Firecrawl/i
637
478
  },
479
+ {
480
+ id: "exa-user",
481
+ name: "Exa (live fetch)",
482
+ product: "aidata",
483
+ category: "assistant",
484
+ pattern: /ExaUser/i
485
+ },
638
486
  {
639
487
  id: "exabot",
640
488
  name: "Exa",
@@ -733,12 +581,19 @@ const BOT_PATTERNS = [
733
581
  category: "other",
734
582
  pattern: /HeadlessChrome|PhantomJS|Puppeteer|Playwright|Selenium|Lightpanda|webdriver/i
735
583
  },
584
+ {
585
+ id: "ahrefs",
586
+ name: "Ahrefs",
587
+ product: "seo",
588
+ category: "other",
589
+ pattern: /AhrefsBot|AhrefsSiteAudit/i
590
+ },
736
591
  {
737
592
  id: "seo",
738
593
  name: "SEO crawler",
739
594
  product: "seo",
740
595
  category: "other",
741
- pattern: /AhrefsBot|AhrefsSiteAudit|SemrushBot|SiteAuditBot|MJ12bot|DotBot|rogerbot|Screaming Frog|DataForSeoBot|BLEXBot|serpstatbot|SEOkicks|barkrowler/i
596
+ pattern: /SemrushBot|SiteAuditBot|MJ12bot|DotBot|rogerbot|Screaming Frog|DataForSeoBot|BLEXBot|serpstatbot|SEOkicks|barkrowler/i
742
597
  },
743
598
  {
744
599
  id: "link-preview",
@@ -767,36 +622,234 @@ const BOT_PATTERNS = [
767
622
  product: "tools",
768
623
  category: "other",
769
624
  pattern: /^curl\/|\bWget\b|python-requests|python-urllib|Python\/\d|aiohttp|python-httpx|Go-http-client|okhttp|axios\/|node-fetch|undici|^node$|Java\/\d|Apache-HttpClient|libwww-perl|^Ruby|Faraday|Scrapy|^Deno\/|^Bun\/|GuzzleHttp|^PostmanRuntime|HTTPie/i
625
+ }
626
+ ];
627
+ /** Matched last, after the generated lists: anything that still calls itself a bot, and no User-Agent at all. */
628
+ const CATCH_ALL = [{
629
+ id: "other-bot",
630
+ name: "Other bot",
631
+ product: "other",
632
+ category: "other",
633
+ pattern: /bot\b|bot\/|robot|crawl|spider|scrape|scraping|fetcher|preview|archiver|indexer/i
634
+ }, {
635
+ id: "no-ua",
636
+ name: "No user agent",
637
+ product: "other",
638
+ category: "other",
639
+ pattern: null
640
+ }];
641
+ // src/bots.ts
642
+ /**
643
+ * Known bots, AI crawlers and AI agents, recognised by their User-Agent: the ones we've
644
+ * checked against each vendor's documentation (bots.curated.ts). `detectBot()` uses them in
645
+ * your app, and the middleware uses them to decide what to send.
646
+ *
647
+ * The AgentStats server makes the final call with a much bigger catalogue (these plus public
648
+ * bot lists), so the dashboard knows bots this SDK version doesn't, without an upgrade.
649
+ */
650
+ /** Categories that count as "AI traffic". Search engines and other bots are shown separately. */
651
+ const AI_CATEGORIES = [
652
+ "training",
653
+ "search",
654
+ "assistant",
655
+ "agent"
656
+ ];
657
+ const isAiCategory = (c) => AI_CATEGORIES.includes(c);
658
+ /** Products group several bots from one vendor ("ChatGPT" = GPTBot + OAI-SearchBot + ChatGPT-User + ChatGPT agent). */
659
+ const BOT_PRODUCTS = {
660
+ chatgpt: {
661
+ name: "ChatGPT",
662
+ operator: "OpenAI"
770
663
  },
771
- {
772
- id: "other-bot",
773
- name: "Other bot",
774
- product: "other",
775
- category: "other",
776
- pattern: /bot\b|bot\/|robot|crawl|spider|scrape|scraping|fetcher|preview|archiver|indexer/i
664
+ claude: {
665
+ name: "Claude",
666
+ operator: "Anthropic"
777
667
  },
778
- {
779
- id: "no-ua",
780
- name: "No user agent",
781
- product: "other",
782
- category: "other",
783
- pattern: null
668
+ perplexity: {
669
+ name: "Perplexity",
670
+ operator: "Perplexity"
671
+ },
672
+ gemini: {
673
+ name: "Gemini",
674
+ operator: "Google"
675
+ },
676
+ copilot: {
677
+ name: "Copilot & Bing",
678
+ operator: "Microsoft"
679
+ },
680
+ meta: {
681
+ name: "Meta AI",
682
+ operator: "Meta"
683
+ },
684
+ apple: {
685
+ name: "Apple",
686
+ operator: "Apple"
687
+ },
688
+ amazon: {
689
+ name: "Amazon",
690
+ operator: "Amazon"
691
+ },
692
+ mistral: {
693
+ name: "Mistral",
694
+ operator: "Mistral AI"
695
+ },
696
+ deepseek: {
697
+ name: "DeepSeek",
698
+ operator: "DeepSeek"
699
+ },
700
+ bytedance: {
701
+ name: "ByteDance",
702
+ operator: "ByteDance"
703
+ },
704
+ xai: {
705
+ name: "Grok",
706
+ operator: "xAI"
707
+ },
708
+ duckduckgo: {
709
+ name: "DuckDuckGo",
710
+ operator: "DuckDuckGo"
711
+ },
712
+ commoncrawl: {
713
+ name: "Common Crawl",
714
+ operator: "Common Crawl Foundation"
715
+ },
716
+ cohere: {
717
+ name: "Cohere",
718
+ operator: "Cohere"
719
+ },
720
+ you: {
721
+ name: "You.com",
722
+ operator: "You.com"
723
+ },
724
+ qwen: {
725
+ name: "Qwen",
726
+ operator: "Alibaba"
727
+ },
728
+ kimi: {
729
+ name: "Kimi",
730
+ operator: "Moonshot AI"
731
+ },
732
+ baidu: {
733
+ name: "Baidu",
734
+ operator: "Baidu"
735
+ },
736
+ yandex: {
737
+ name: "Yandex",
738
+ operator: "Yandex"
739
+ },
740
+ huawei: {
741
+ name: "Huawei",
742
+ operator: "Huawei"
743
+ },
744
+ ai2: {
745
+ name: "Ai2",
746
+ operator: "Allen Institute for AI"
747
+ },
748
+ brave: {
749
+ name: "Brave",
750
+ operator: "Brave"
751
+ },
752
+ kagi: {
753
+ name: "Kagi",
754
+ operator: "Kagi"
755
+ },
756
+ phind: {
757
+ name: "Phind",
758
+ operator: "Phind"
759
+ },
760
+ poe: {
761
+ name: "Poe",
762
+ operator: "Quora"
763
+ },
764
+ manus: {
765
+ name: "Manus",
766
+ operator: "Butterfly Effect"
767
+ },
768
+ ai: {
769
+ name: "Other AI bots",
770
+ operator: "Various"
771
+ },
772
+ coding: {
773
+ name: "Coding agents",
774
+ operator: "Various"
775
+ },
776
+ aidata: {
777
+ name: "AI data providers",
778
+ operator: "Various"
779
+ },
780
+ google: {
781
+ name: "Google Search",
782
+ operator: "Google"
783
+ },
784
+ searchengines: {
785
+ name: "Other search engines",
786
+ operator: "Various"
787
+ },
788
+ automation: {
789
+ name: "Automated browsers",
790
+ operator: "Various"
791
+ },
792
+ seo: {
793
+ name: "SEO tools",
794
+ operator: "Various"
795
+ },
796
+ social: {
797
+ name: "Link previews",
798
+ operator: "Various"
799
+ },
800
+ monitor: {
801
+ name: "Monitoring",
802
+ operator: "Various"
803
+ },
804
+ feeds: {
805
+ name: "Feed readers",
806
+ operator: "Various"
807
+ },
808
+ security: {
809
+ name: "Security scanners",
810
+ operator: "Various"
811
+ },
812
+ archive: {
813
+ name: "Archivers",
814
+ operator: "Various"
815
+ },
816
+ research: {
817
+ name: "Research crawlers",
818
+ operator: "Various"
819
+ },
820
+ ads: {
821
+ name: "Advertising bots",
822
+ operator: "Various"
823
+ },
824
+ tools: {
825
+ name: "HTTP clients & scripts",
826
+ operator: "Various"
827
+ },
828
+ other: {
829
+ name: "Other bots",
830
+ operator: "Unknown"
784
831
  }
785
- ];
832
+ };
833
+ /** Every bot this SDK recognises, in the order they're tried: the first match wins, catch-alls last. */
834
+ const BOT_PATTERNS = [...CURATED, ...CATCH_ALL];
786
835
  const BY_ID = new Map(BOT_PATTERNS.map((b) => [b.id, b]));
787
836
  /** Real browsers whose UA happens to contain a bot-ish word. */
788
837
  const HUMAN_LOOKALIKES = /cubot|\bbotswana\b/i;
789
- /** Identify a bot from its User-Agent. Returns null for (what looks like) a normal browser. */
790
- function classifyUserAgent(ua) {
791
- const s = (ua ?? "").trim();
792
- if (!s) return BY_ID.get("no-ua");
793
- for (const b of BOT_PATTERNS) {
838
+ /** The first bot in `list` whose pattern matches `ua` (trimmed, non-empty). Shared with the server's bigger catalogue. */
839
+ function firstMatch(list, s) {
840
+ for (const b of list) {
794
841
  if (!b.pattern || !b.pattern.test(s)) continue;
795
842
  if ((b.id === "other-bot" || b.id === "link-preview") && HUMAN_LOOKALIKES.test(s)) continue;
796
843
  return b;
797
844
  }
798
845
  return null;
799
846
  }
847
+ /** Identify a bot from its User-Agent. Returns null for (what looks like) a normal browser. */
848
+ function classifyUserAgent(ua) {
849
+ const s = (ua ?? "").trim();
850
+ if (!s) return BY_ID.get("no-ua");
851
+ return firstMatch(BOT_PATTERNS, s);
852
+ }
800
853
  /** Hostnames of AI assistants that send people to websites, and the `utm_source` values they add. */
801
854
  const AI_REFERRERS = [
802
855
  {
@@ -948,14 +1001,13 @@ function detectFromHeaders(header, url) {
948
1001
  name: bot.name,
949
1002
  category: bot.category,
950
1003
  product: product.name,
951
- operator: product.operator,
952
- browser: bot.category === "agent" || bot.id === "headless",
1004
+ operator: bot.operator ?? product.operator,
1005
+ browser: bot.category === "agent" || bot.product === "automation",
953
1006
  signed,
954
1007
  referral: null
955
1008
  };
956
1009
  }
957
- //#endregion
958
- //#region src/server.ts
1010
+ // src/server.ts
959
1011
  /**
960
1012
  * AgentStats server integration — see which AI crawlers and agents visit your
961
1013
  * site (GPTBot, ClaudeBot, PerplexityBot, ChatGPT agent, Google-Agent, …),
@@ -989,12 +1041,21 @@ function detectFromHeaders(header, url) {
989
1041
  *
990
1042
  * const lens = createAgentStats({ apiKey: process.env.AGENTSTATS_API_KEY }) // API-only server
991
1043
  */
992
- const SERVER_SDK_VERSION = "0.3.1";
1044
+ const SERVER_SDK_VERSION = "0.3.2";
993
1045
  /**
994
- * Words that may mean a bot the table below doesn't know yet. Deliberately broad: the server
995
- * makes the final call, and its table can be newer than this SDK's.
1046
+ * Words that may mean a bot this SDK's table doesn't know. Deliberately broad: the server makes
1047
+ * the final call, with a catalogue far bigger and newer than this SDK's.
996
1048
  */
997
1049
  const BOT_HINT = /bot|crawl|spider|slurp|scrap|fetch|agent|preview|headless|python|curl|wget|httpx|http-?client|go-http|java\/|okhttp|axios|node|undici|libwww|perl|ruby|phantom|puppeteer|playwright|selenium|lighthouse|monitor|uptime|gpt|openai|claude|anthropic|perplexity|gemini|google-|googleother|meta-external|facebookexternalhit|bytespider|ccbot|diffbot|cohere|mistral|deepseek|manus|firecrawl|jina|tavily|exabot|linkup|novaact|amzn|cursor|devin|opencode|kagi|duckassist|qwen|tongyi|kimi|yiyan|ernie|doubao|petal|pangu|timpi|omgili|webzio|imagesift|img2dataset|lightpanda|crawl4ai|apify|youbot|phind/i;
1050
+ /**
1051
+ * A person's browser: "Mozilla/5.0 (…) AppleWebKit/… or Gecko/…", with no contact URL, email or
1052
+ * "compatible;", which bots dressed as browsers add and browsers never do. Anything else is sent
1053
+ * for the server to classify with its much bigger catalogue (crawlers that name themselves without
1054
+ * a bot-ish word, feed readers, scripts); what it can't name it counts as a person and keeps nothing.
1055
+ */
1056
+ const BROWSER_SHAPED = /^Mozilla\/5\.0 \(.*(?:AppleWebKit\/|Gecko\/)/;
1057
+ const NOT_A_BROWSER = /compatible;|https?:\/\/|@/i;
1058
+ const looksLikeBrowser = (ua) => BROWSER_SHAPED.test(ua) && !NOT_A_BROWSER.test(ua);
998
1059
  const ASSET = /\.(?:js|mjs|css|map|png|jpe?g|gif|webp|avif|svg|ico|bmp|woff2?|ttf|otf|eot|mp4|webm|mov|mp3|wav|ogg|zip|gz|wasm|pdf)$/i;
999
1060
  const SIGNATURE_HEADERS = [
1000
1061
  "signature",
@@ -1030,6 +1091,7 @@ function normalizeRoutePattern(pattern) {
1030
1091
  function fitRoute(pattern, path) {
1031
1092
  if (typeof pattern !== "string" || !pattern.startsWith("/")) return null;
1032
1093
  const p = normalizeRoutePattern(pattern).replace(/\/$/, "") || "/";
1094
+ if (/^\/(?:\*\*?|:[^/]+\*)(?::[^/]*)?$/.test(p)) return null;
1033
1095
  if (p.includes("*")) return p;
1034
1096
  const segments = (s) => s.split("/").filter(Boolean).length;
1035
1097
  return segments(p) === segments(path) ? p : null;
@@ -1129,7 +1191,7 @@ var AgentStatsServer = class {
1129
1191
  if (opts.serverKey && !opts.serverKey.startsWith("al_srv_")) throw new Error("[agentstats] `serverKey` must be the secret server key (al_srv_…), not the public site key");
1130
1192
  if (opts.apiKey && !opts.apiKey.startsWith("al_api_")) throw new Error("[agentstats] `apiKey` must be an API's key (al_api_…, Project settings → APIs)");
1131
1193
  const endpoint = opts?.endpoint ?? defaultEndpoint();
1132
- if (!endpoint && (opts?.serverKey || opts?.apiKey) && !opts?.disabled) throw new Error("[agentstats] `endpoint` is required, e.g. https://your-host/api/ingest/hits");
1194
+ if (!endpoint && (opts?.serverKey || opts?.apiKey) && !opts?.disabled) throw new Error("[agentstats] `endpoint` is required, e.g. https://agentstats.io/api/ingest/hits");
1133
1195
  this.options = {
1134
1196
  countHumans: true,
1135
1197
  visitCookie: true,
@@ -1179,7 +1241,18 @@ var AgentStatsServer = class {
1179
1241
  * so the hit is sent after the response. Returns what `detectBot` says about the request.
1180
1242
  */
1181
1243
  track(request, init = {}) {
1182
- const d = this.fetchDecision(request, init.ip);
1244
+ let ip = init.ip;
1245
+ if (ip === void 0 && init.remoteAddress !== void 0) try {
1246
+ const headers = {};
1247
+ request.headers.forEach((v, k) => {
1248
+ headers[k] = v;
1249
+ });
1250
+ ip = this.nodeIp({
1251
+ headers,
1252
+ socket: { remoteAddress: init.remoteAddress ?? void 0 }
1253
+ });
1254
+ } catch {}
1255
+ const d = this.fetchDecision(request, ip);
1183
1256
  this.setRoute(d, init.route);
1184
1257
  this.fetchFinish(d, init.status ?? null, init.response, init.waitUntil);
1185
1258
  return d.detection;
@@ -1249,9 +1322,10 @@ var AgentStatsServer = class {
1249
1322
  };
1250
1323
  }
1251
1324
  /**
1252
- * Nuxt / Nitro plugin: `export default defineNitroPlugin(lens.nitro())` in `server/plugins/agentstats.ts`.
1325
+ * Nuxt / Nitro plugin: `export default defineNitroPlugin(lens.nitro())` in `server/plugins/agentstats.ts`
1326
+ * (Nitro v3: `export default definePlugin(lens.nitro())`).
1253
1327
  * The detection is on `event.context.bot` in server routes, and on `useRequestEvent()?.context.bot` in pages.
1254
- * Works on every Nitro preset (Node, Vercel, Netlify, Cloudflare…).
1328
+ * Works on every Nitro preset (Node, Vercel, Netlify, Cloudflare…), on Nitro v2 (h3 v1) and v3 (h3 v2).
1255
1329
  */
1256
1330
  nitro() {
1257
1331
  return (nitroApp) => {
@@ -1260,18 +1334,27 @@ var AgentStatsServer = class {
1260
1334
  event.context.bot = d.detection;
1261
1335
  if (d.kind === "ignore") return;
1262
1336
  event.context[DECISION] = d;
1263
- if (d.cookie && event.node?.res) appendNodeCookie(event.node.res, d.cookie);
1337
+ if (d.cookie) {
1338
+ if (isH3v2(event)) try {
1339
+ event.res.headers.append("set-cookie", d.cookie);
1340
+ } catch {}
1341
+ else if (event.node?.res) appendNodeCookie(event.node.res, d.cookie);
1342
+ }
1264
1343
  });
1265
- const finish = (event, status) => {
1344
+ const finish = (event, status, contentType) => {
1266
1345
  const d = event?.context[DECISION];
1267
1346
  if (!d) return;
1268
1347
  delete event.context[DECISION];
1269
- this.setRoute(d, event.context.matchedRoute?.path);
1270
- this.setContentType(d, event.node?.res?.getHeader?.("content-type"));
1348
+ const matched = event.context.matchedRoute;
1349
+ this.setRoute(d, matched?.path ?? matched?.route);
1350
+ this.setContentType(d, contentType);
1271
1351
  this.commit(d, status, typeof event.waitUntil === "function" ? event.waitUntil.bind(event) : void 0);
1272
1352
  };
1273
- nitroApp.hooks.hook("afterResponse", (event) => finish(event, event.node?.res?.statusCode ?? null));
1274
- nitroApp.hooks.hook("error", (error, context = {}) => finish(context.event, error?.statusCode ?? 500));
1353
+ nitroApp.hooks.hook("afterResponse", (event) => finish(event, event.node?.res?.statusCode ?? null, event.node?.res?.getHeader?.("content-type")));
1354
+ nitroApp.hooks.hook("response", (res, event) => finish(event, res?.status ?? event?.res?.status ?? null, res?.headers?.get?.("content-type")));
1355
+ nitroApp.hooks.hook("error", (error, context = {}) => {
1356
+ if (context.event && !isH3v2(context.event)) finish(context.event, error?.statusCode ?? error?.status ?? 500, context.event.node?.res?.getHeader?.("content-type"));
1357
+ });
1275
1358
  };
1276
1359
  }
1277
1360
  /**
@@ -1409,7 +1492,7 @@ var AgentStatsServer = class {
1409
1492
  const ua = header("user-agent") ?? null;
1410
1493
  if (!ip && !ua?.trim()) return ignore;
1411
1494
  const signed = detection.signed;
1412
- if (signed || detection.isBot || ua && BOT_HINT.test(ua)) {
1495
+ if (signed || detection.isBot || ua && (BOT_HINT.test(ua) || !looksLikeBrowser(ua))) {
1413
1496
  const record = {
1414
1497
  ts: Date.now(),
1415
1498
  method: m,
@@ -1574,15 +1657,18 @@ var AgentStatsServer = class {
1574
1657
  }
1575
1658
  h3Decision(event) {
1576
1659
  const req = event.node?.req;
1577
- const get = typeof event.headers?.get === "function" ? (name) => event.headers.get(name) : (name) => headerValue(req?.headers[name]);
1660
+ const web = isH3v2(event) ? event.req : void 0;
1661
+ const get = web ? (name) => web.headers.get(name) : typeof event.headers?.get === "function" ? (name) => event.headers.get(name) : (name) => headerValue(req?.headers[name]);
1578
1662
  try {
1579
- if (get("x-nitro-prerender") || (event.path ?? req?.url ?? "").startsWith("/__nuxt_error")) return {
1663
+ const path = event.url?.pathname ?? event.path ?? req?.url ?? "";
1664
+ if (get("x-nitro-prerender") || path.startsWith("/__nuxt_error")) return {
1580
1665
  kind: "ignore",
1581
1666
  detection: detectFromHeaders(get, null)
1582
1667
  };
1583
1668
  const native = !!req?.socket?.remoteAddress;
1584
- const url = native && req ? this.nodeUrl(req) : event.web?.request?.url ?? `${firstIp(get("x-forwarded-proto")) || "https"}://${get("host") ?? "localhost"}${event.path ?? req?.url ?? "/"}`;
1585
- return this.decide(event.method ?? req?.method ?? "GET", url, get, native && req ? this.nodeIp(req) : this.fetchIp(get));
1669
+ const url = native && req ? this.nodeUrl(req) : web?.url ?? event.web?.request?.url ?? `${firstIp(get("x-forwarded-proto")) || "https"}://${get("host") ?? "localhost"}${event.path ?? req?.url ?? "/"}`;
1670
+ const ip = native && req ? this.nodeIp(req) : typeof web?.ip === "string" && web.ip && !this.options.ipHeader && !this.options.trustProxy && !platformIpHeader() ? web.ip : this.fetchIp(get);
1671
+ return this.decide(web?.method ?? event.method ?? req?.method ?? "GET", url, get, ip);
1586
1672
  } catch (err) {
1587
1673
  this.log("track failed", err);
1588
1674
  return {
@@ -1621,6 +1707,10 @@ var AgentStatsServer = class {
1621
1707
  }
1622
1708
  };
1623
1709
  const DECISION = Symbol.for("agentstats.decision");
1710
+ /** An h3 v2 event (Nitro v3), whose request is a web `Request`. h3 v1 events have no `req`. */
1711
+ function isH3v2(event) {
1712
+ return typeof event.req?.headers?.get === "function";
1713
+ }
1624
1714
  function headerValue(v) {
1625
1715
  if (v === void 0) return null;
1626
1716
  return Array.isArray(v) ? v.join(", ") : v;
@@ -1634,5 +1724,4 @@ function appendNodeCookie(res, cookie) {
1634
1724
  function createAgentStats(options) {
1635
1725
  return new AgentStatsServer(options);
1636
1726
  }
1637
- //#endregion
1638
1727
  export { AI_CATEGORIES, AgentStatsServer, BOT_PATTERNS, BOT_PRODUCTS, SERVER_SDK_VERSION, classifyReferral, classifyUserAgent, createAgentStats, createAgentStats as default, detectBot, isAiCategory, isPrivateIp, normalizeRoutePattern, routeFromPath };
package/package.json CHANGED
@@ -1,11 +1,7 @@
1
1
  {
2
2
  "name": "@agentstats/sdk",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Analytics for WebMCP tool calls and AI bot traffic, plus server-side AI bot detection. Framework-agnostic, zero dependencies.",
5
- "homepage": "https://agentstats.io",
6
- "bugs": {
7
- "url": "https://agentstats.io/docs"
8
- },
9
5
  "type": "module",
10
6
  "main": "./dist/index.cjs",
11
7
  "module": "./dist/index.js",
@@ -21,6 +17,11 @@
21
17
  "types": "./dist/server.d.ts",
22
18
  "import": "./dist/server.js",
23
19
  "require": "./dist/server.cjs"
20
+ },
21
+ "./bots": {
22
+ "types": "./dist/bots.d.ts",
23
+ "import": "./dist/bots.js",
24
+ "require": "./dist/bots.cjs"
24
25
  }
25
26
  },
26
27
  "files": [
@@ -29,12 +30,6 @@
29
30
  "sideEffects": [
30
31
  "./dist/browser.iife.js"
31
32
  ],
32
- "scripts": {
33
- "build": "tsdown",
34
- "test": "vitest run",
35
- "typecheck": "tsc --noEmit",
36
- "prepublishOnly": "pnpm build && pnpm test"
37
- },
38
33
  "keywords": [
39
34
  "webmcp",
40
35
  "analytics",
@@ -42,6 +37,15 @@
42
37
  "ai-agents"
43
38
  ],
44
39
  "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/getagentstats/agentstats-js.git",
43
+ "directory": "packages/sdk"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/getagentstats/agentstats-js/issues"
47
+ },
48
+ "homepage": "https://agentstats.io",
45
49
  "author": "AgentStats (https://agentstats.io)",
46
50
  "devDependencies": {
47
51
  "happy-dom": "20.0.11",
@@ -51,5 +55,10 @@
51
55
  },
52
56
  "publishConfig": {
53
57
  "access": "public"
58
+ },
59
+ "scripts": {
60
+ "build": "tsdown",
61
+ "test": "vitest run",
62
+ "typecheck": "tsc --noEmit"
54
63
  }
55
- }
64
+ }