@agentstats/sdk 0.5.0 → 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.cjs CHANGED
@@ -2,167 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- //#region src/bots.ts
6
- /** Categories that count as "AI traffic". Search engines and other bots are shown separately. */
7
- const AI_CATEGORIES = [
8
- "training",
9
- "search",
10
- "assistant",
11
- "agent"
12
- ];
13
- const isAiCategory = (c) => AI_CATEGORIES.includes(c);
14
- /** Products group several bots from one vendor ("ChatGPT" = GPTBot + OAI-SearchBot + ChatGPT-User + ChatGPT agent). */
15
- const BOT_PRODUCTS = {
16
- chatgpt: {
17
- name: "ChatGPT",
18
- operator: "OpenAI"
19
- },
20
- claude: {
21
- name: "Claude",
22
- operator: "Anthropic"
23
- },
24
- perplexity: {
25
- name: "Perplexity",
26
- operator: "Perplexity"
27
- },
28
- gemini: {
29
- name: "Gemini",
30
- operator: "Google"
31
- },
32
- copilot: {
33
- name: "Copilot & Bing",
34
- operator: "Microsoft"
35
- },
36
- meta: {
37
- name: "Meta AI",
38
- operator: "Meta"
39
- },
40
- apple: {
41
- name: "Apple",
42
- operator: "Apple"
43
- },
44
- amazon: {
45
- name: "Amazon",
46
- operator: "Amazon"
47
- },
48
- mistral: {
49
- name: "Mistral",
50
- operator: "Mistral AI"
51
- },
52
- deepseek: {
53
- name: "DeepSeek",
54
- operator: "DeepSeek"
55
- },
56
- bytedance: {
57
- name: "ByteDance",
58
- operator: "ByteDance"
59
- },
60
- xai: {
61
- name: "Grok",
62
- operator: "xAI"
63
- },
64
- duckduckgo: {
65
- name: "DuckDuckGo",
66
- operator: "DuckDuckGo"
67
- },
68
- commoncrawl: {
69
- name: "Common Crawl",
70
- operator: "Common Crawl Foundation"
71
- },
72
- cohere: {
73
- name: "Cohere",
74
- operator: "Cohere"
75
- },
76
- you: {
77
- name: "You.com",
78
- operator: "You.com"
79
- },
80
- qwen: {
81
- name: "Qwen",
82
- operator: "Alibaba"
83
- },
84
- kimi: {
85
- name: "Kimi",
86
- operator: "Moonshot AI"
87
- },
88
- baidu: {
89
- name: "Baidu",
90
- operator: "Baidu"
91
- },
92
- yandex: {
93
- name: "Yandex",
94
- operator: "Yandex"
95
- },
96
- huawei: {
97
- name: "Huawei",
98
- operator: "Huawei"
99
- },
100
- ai2: {
101
- name: "Ai2",
102
- operator: "Allen Institute for AI"
103
- },
104
- brave: {
105
- name: "Brave",
106
- operator: "Brave"
107
- },
108
- kagi: {
109
- name: "Kagi",
110
- operator: "Kagi"
111
- },
112
- phind: {
113
- name: "Phind",
114
- operator: "Phind"
115
- },
116
- poe: {
117
- name: "Poe",
118
- operator: "Quora"
119
- },
120
- manus: {
121
- name: "Manus",
122
- operator: "Butterfly Effect"
123
- },
124
- coding: {
125
- name: "Coding agents",
126
- operator: "Various"
127
- },
128
- aidata: {
129
- name: "AI data providers",
130
- operator: "Various"
131
- },
132
- google: {
133
- name: "Google Search",
134
- operator: "Google"
135
- },
136
- automation: {
137
- name: "Automated browsers",
138
- operator: "Various"
139
- },
140
- seo: {
141
- name: "SEO tools",
142
- operator: "Various"
143
- },
144
- social: {
145
- name: "Link previews",
146
- operator: "Various"
147
- },
148
- monitor: {
149
- name: "Uptime monitors",
150
- operator: "Various"
151
- },
152
- tools: {
153
- name: "HTTP clients & scripts",
154
- operator: "Various"
155
- },
156
- other: {
157
- name: "Other bots",
158
- operator: "Unknown"
159
- }
160
- };
161
- /**
162
- * Order matters: the first matching pattern wins, so specific tokens come
163
- * before the vendor's generic ones and the catch-all patterns come last.
164
- */
165
- const BOT_PATTERNS = [
5
+ // src/bots.curated.ts
6
+ const CURATED = [
166
7
  {
167
8
  id: "chatgpt-agent",
168
9
  name: "ChatGPT agent",
@@ -231,7 +72,7 @@ const BOT_PATTERNS = [
231
72
  name: "Perplexity-User",
232
73
  product: "perplexity",
233
74
  category: "assistant",
234
- pattern: /Perplexity-User/i
75
+ pattern: /Perplexity-?User/i
235
76
  },
236
77
  {
237
78
  id: "perplexitybot",
@@ -639,6 +480,13 @@ const BOT_PATTERNS = [
639
480
  category: "assistant",
640
481
  pattern: /FirecrawlAgent|Firecrawl/i
641
482
  },
483
+ {
484
+ id: "exa-user",
485
+ name: "Exa (live fetch)",
486
+ product: "aidata",
487
+ category: "assistant",
488
+ pattern: /ExaUser/i
489
+ },
642
490
  {
643
491
  id: "exabot",
644
492
  name: "Exa",
@@ -737,12 +585,19 @@ const BOT_PATTERNS = [
737
585
  category: "other",
738
586
  pattern: /HeadlessChrome|PhantomJS|Puppeteer|Playwright|Selenium|Lightpanda|webdriver/i
739
587
  },
588
+ {
589
+ id: "ahrefs",
590
+ name: "Ahrefs",
591
+ product: "seo",
592
+ category: "other",
593
+ pattern: /AhrefsBot|AhrefsSiteAudit/i
594
+ },
740
595
  {
741
596
  id: "seo",
742
597
  name: "SEO crawler",
743
598
  product: "seo",
744
599
  category: "other",
745
- pattern: /AhrefsBot|AhrefsSiteAudit|SemrushBot|SiteAuditBot|MJ12bot|DotBot|rogerbot|Screaming Frog|DataForSeoBot|BLEXBot|serpstatbot|SEOkicks|barkrowler/i
600
+ pattern: /SemrushBot|SiteAuditBot|MJ12bot|DotBot|rogerbot|Screaming Frog|DataForSeoBot|BLEXBot|serpstatbot|SEOkicks|barkrowler/i
746
601
  },
747
602
  {
748
603
  id: "link-preview",
@@ -771,36 +626,234 @@ const BOT_PATTERNS = [
771
626
  product: "tools",
772
627
  category: "other",
773
628
  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
629
+ }
630
+ ];
631
+ /** Matched last, after the generated lists: anything that still calls itself a bot, and no User-Agent at all. */
632
+ const CATCH_ALL = [{
633
+ id: "other-bot",
634
+ name: "Other bot",
635
+ product: "other",
636
+ category: "other",
637
+ pattern: /bot\b|bot\/|robot|crawl|spider|scrape|scraping|fetcher|preview|archiver|indexer/i
638
+ }, {
639
+ id: "no-ua",
640
+ name: "No user agent",
641
+ product: "other",
642
+ category: "other",
643
+ pattern: null
644
+ }];
645
+ // src/bots.ts
646
+ /**
647
+ * Known bots, AI crawlers and AI agents, recognised by their User-Agent: the ones we've
648
+ * checked against each vendor's documentation (bots.curated.ts). `detectBot()` uses them in
649
+ * your app, and the middleware uses them to decide what to send.
650
+ *
651
+ * The AgentStats server makes the final call with a much bigger catalogue (these plus public
652
+ * bot lists), so the dashboard knows bots this SDK version doesn't, without an upgrade.
653
+ */
654
+ /** Categories that count as "AI traffic". Search engines and other bots are shown separately. */
655
+ const AI_CATEGORIES = [
656
+ "training",
657
+ "search",
658
+ "assistant",
659
+ "agent"
660
+ ];
661
+ const isAiCategory = (c) => AI_CATEGORIES.includes(c);
662
+ /** Products group several bots from one vendor ("ChatGPT" = GPTBot + OAI-SearchBot + ChatGPT-User + ChatGPT agent). */
663
+ const BOT_PRODUCTS = {
664
+ chatgpt: {
665
+ name: "ChatGPT",
666
+ operator: "OpenAI"
774
667
  },
775
- {
776
- id: "other-bot",
777
- name: "Other bot",
778
- product: "other",
779
- category: "other",
780
- pattern: /bot\b|bot\/|robot|crawl|spider|scrape|scraping|fetcher|preview|archiver|indexer/i
668
+ claude: {
669
+ name: "Claude",
670
+ operator: "Anthropic"
781
671
  },
782
- {
783
- id: "no-ua",
784
- name: "No user agent",
785
- product: "other",
786
- category: "other",
787
- pattern: null
672
+ perplexity: {
673
+ name: "Perplexity",
674
+ operator: "Perplexity"
675
+ },
676
+ gemini: {
677
+ name: "Gemini",
678
+ operator: "Google"
679
+ },
680
+ copilot: {
681
+ name: "Copilot & Bing",
682
+ operator: "Microsoft"
683
+ },
684
+ meta: {
685
+ name: "Meta AI",
686
+ operator: "Meta"
687
+ },
688
+ apple: {
689
+ name: "Apple",
690
+ operator: "Apple"
691
+ },
692
+ amazon: {
693
+ name: "Amazon",
694
+ operator: "Amazon"
695
+ },
696
+ mistral: {
697
+ name: "Mistral",
698
+ operator: "Mistral AI"
699
+ },
700
+ deepseek: {
701
+ name: "DeepSeek",
702
+ operator: "DeepSeek"
703
+ },
704
+ bytedance: {
705
+ name: "ByteDance",
706
+ operator: "ByteDance"
707
+ },
708
+ xai: {
709
+ name: "Grok",
710
+ operator: "xAI"
711
+ },
712
+ duckduckgo: {
713
+ name: "DuckDuckGo",
714
+ operator: "DuckDuckGo"
715
+ },
716
+ commoncrawl: {
717
+ name: "Common Crawl",
718
+ operator: "Common Crawl Foundation"
719
+ },
720
+ cohere: {
721
+ name: "Cohere",
722
+ operator: "Cohere"
723
+ },
724
+ you: {
725
+ name: "You.com",
726
+ operator: "You.com"
727
+ },
728
+ qwen: {
729
+ name: "Qwen",
730
+ operator: "Alibaba"
731
+ },
732
+ kimi: {
733
+ name: "Kimi",
734
+ operator: "Moonshot AI"
735
+ },
736
+ baidu: {
737
+ name: "Baidu",
738
+ operator: "Baidu"
739
+ },
740
+ yandex: {
741
+ name: "Yandex",
742
+ operator: "Yandex"
743
+ },
744
+ huawei: {
745
+ name: "Huawei",
746
+ operator: "Huawei"
747
+ },
748
+ ai2: {
749
+ name: "Ai2",
750
+ operator: "Allen Institute for AI"
751
+ },
752
+ brave: {
753
+ name: "Brave",
754
+ operator: "Brave"
755
+ },
756
+ kagi: {
757
+ name: "Kagi",
758
+ operator: "Kagi"
759
+ },
760
+ phind: {
761
+ name: "Phind",
762
+ operator: "Phind"
763
+ },
764
+ poe: {
765
+ name: "Poe",
766
+ operator: "Quora"
767
+ },
768
+ manus: {
769
+ name: "Manus",
770
+ operator: "Butterfly Effect"
771
+ },
772
+ ai: {
773
+ name: "Other AI bots",
774
+ operator: "Various"
775
+ },
776
+ coding: {
777
+ name: "Coding agents",
778
+ operator: "Various"
779
+ },
780
+ aidata: {
781
+ name: "AI data providers",
782
+ operator: "Various"
783
+ },
784
+ google: {
785
+ name: "Google Search",
786
+ operator: "Google"
787
+ },
788
+ searchengines: {
789
+ name: "Other search engines",
790
+ operator: "Various"
791
+ },
792
+ automation: {
793
+ name: "Automated browsers",
794
+ operator: "Various"
795
+ },
796
+ seo: {
797
+ name: "SEO tools",
798
+ operator: "Various"
799
+ },
800
+ social: {
801
+ name: "Link previews",
802
+ operator: "Various"
803
+ },
804
+ monitor: {
805
+ name: "Monitoring",
806
+ operator: "Various"
807
+ },
808
+ feeds: {
809
+ name: "Feed readers",
810
+ operator: "Various"
811
+ },
812
+ security: {
813
+ name: "Security scanners",
814
+ operator: "Various"
815
+ },
816
+ archive: {
817
+ name: "Archivers",
818
+ operator: "Various"
819
+ },
820
+ research: {
821
+ name: "Research crawlers",
822
+ operator: "Various"
823
+ },
824
+ ads: {
825
+ name: "Advertising bots",
826
+ operator: "Various"
827
+ },
828
+ tools: {
829
+ name: "HTTP clients & scripts",
830
+ operator: "Various"
831
+ },
832
+ other: {
833
+ name: "Other bots",
834
+ operator: "Unknown"
788
835
  }
789
- ];
836
+ };
837
+ /** Every bot this SDK recognises, in the order they're tried: the first match wins, catch-alls last. */
838
+ const BOT_PATTERNS = [...CURATED, ...CATCH_ALL];
790
839
  const BY_ID = new Map(BOT_PATTERNS.map((b) => [b.id, b]));
791
840
  /** Real browsers whose UA happens to contain a bot-ish word. */
792
841
  const HUMAN_LOOKALIKES = /cubot|\bbotswana\b/i;
793
- /** Identify a bot from its User-Agent. Returns null for (what looks like) a normal browser. */
794
- function classifyUserAgent(ua) {
795
- const s = (ua ?? "").trim();
796
- if (!s) return BY_ID.get("no-ua");
797
- for (const b of BOT_PATTERNS) {
842
+ /** The first bot in `list` whose pattern matches `ua` (trimmed, non-empty). Shared with the server's bigger catalogue. */
843
+ function firstMatch(list, s) {
844
+ for (const b of list) {
798
845
  if (!b.pattern || !b.pattern.test(s)) continue;
799
846
  if ((b.id === "other-bot" || b.id === "link-preview") && HUMAN_LOOKALIKES.test(s)) continue;
800
847
  return b;
801
848
  }
802
849
  return null;
803
850
  }
851
+ /** Identify a bot from its User-Agent. Returns null for (what looks like) a normal browser. */
852
+ function classifyUserAgent(ua) {
853
+ const s = (ua ?? "").trim();
854
+ if (!s) return BY_ID.get("no-ua");
855
+ return firstMatch(BOT_PATTERNS, s);
856
+ }
804
857
  /** Hostnames of AI assistants that send people to websites, and the `utm_source` values they add. */
805
858
  const AI_REFERRERS = [
806
859
  {
@@ -952,14 +1005,13 @@ function detectFromHeaders(header, url) {
952
1005
  name: bot.name,
953
1006
  category: bot.category,
954
1007
  product: product.name,
955
- operator: product.operator,
956
- browser: bot.category === "agent" || bot.id === "headless",
1008
+ operator: bot.operator ?? product.operator,
1009
+ browser: bot.category === "agent" || bot.product === "automation",
957
1010
  signed,
958
1011
  referral: null
959
1012
  };
960
1013
  }
961
- //#endregion
962
- //#region src/server.ts
1014
+ // src/server.ts
963
1015
  /**
964
1016
  * AgentStats server integration — see which AI crawlers and agents visit your
965
1017
  * site (GPTBot, ClaudeBot, PerplexityBot, ChatGPT agent, Google-Agent, …),
@@ -993,12 +1045,21 @@ function detectFromHeaders(header, url) {
993
1045
  *
994
1046
  * const lens = createAgentStats({ apiKey: process.env.AGENTSTATS_API_KEY }) // API-only server
995
1047
  */
996
- const SERVER_SDK_VERSION = "0.3.1";
1048
+ const SERVER_SDK_VERSION = "0.3.2";
997
1049
  /**
998
- * Words that may mean a bot the table below doesn't know yet. Deliberately broad: the server
999
- * makes the final call, and its table can be newer than this SDK's.
1050
+ * Words that may mean a bot this SDK's table doesn't know. Deliberately broad: the server makes
1051
+ * the final call, with a catalogue far bigger and newer than this SDK's.
1000
1052
  */
1001
1053
  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;
1054
+ /**
1055
+ * A person's browser: "Mozilla/5.0 (…) AppleWebKit/… or Gecko/…", with no contact URL, email or
1056
+ * "compatible;", which bots dressed as browsers add and browsers never do. Anything else is sent
1057
+ * for the server to classify with its much bigger catalogue (crawlers that name themselves without
1058
+ * a bot-ish word, feed readers, scripts); what it can't name it counts as a person and keeps nothing.
1059
+ */
1060
+ const BROWSER_SHAPED = /^Mozilla\/5\.0 \(.*(?:AppleWebKit\/|Gecko\/)/;
1061
+ const NOT_A_BROWSER = /compatible;|https?:\/\/|@/i;
1062
+ const looksLikeBrowser = (ua) => BROWSER_SHAPED.test(ua) && !NOT_A_BROWSER.test(ua);
1002
1063
  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;
1003
1064
  const SIGNATURE_HEADERS = [
1004
1065
  "signature",
@@ -1034,6 +1095,7 @@ function normalizeRoutePattern(pattern) {
1034
1095
  function fitRoute(pattern, path) {
1035
1096
  if (typeof pattern !== "string" || !pattern.startsWith("/")) return null;
1036
1097
  const p = normalizeRoutePattern(pattern).replace(/\/$/, "") || "/";
1098
+ if (/^\/(?:\*\*?|:[^/]+\*)(?::[^/]*)?$/.test(p)) return null;
1037
1099
  if (p.includes("*")) return p;
1038
1100
  const segments = (s) => s.split("/").filter(Boolean).length;
1039
1101
  return segments(p) === segments(path) ? p : null;
@@ -1133,7 +1195,7 @@ var AgentStatsServer = class {
1133
1195
  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");
1134
1196
  if (opts.apiKey && !opts.apiKey.startsWith("al_api_")) throw new Error("[agentstats] `apiKey` must be an API's key (al_api_…, Project settings → APIs)");
1135
1197
  const endpoint = opts?.endpoint ?? defaultEndpoint();
1136
- if (!endpoint && (opts?.serverKey || opts?.apiKey) && !opts?.disabled) throw new Error("[agentstats] `endpoint` is required, e.g. https://your-host/api/ingest/hits");
1198
+ if (!endpoint && (opts?.serverKey || opts?.apiKey) && !opts?.disabled) throw new Error("[agentstats] `endpoint` is required, e.g. https://agentstats.io/api/ingest/hits");
1137
1199
  this.options = {
1138
1200
  countHumans: true,
1139
1201
  visitCookie: true,
@@ -1183,7 +1245,18 @@ var AgentStatsServer = class {
1183
1245
  * so the hit is sent after the response. Returns what `detectBot` says about the request.
1184
1246
  */
1185
1247
  track(request, init = {}) {
1186
- const d = this.fetchDecision(request, init.ip);
1248
+ let ip = init.ip;
1249
+ if (ip === void 0 && init.remoteAddress !== void 0) try {
1250
+ const headers = {};
1251
+ request.headers.forEach((v, k) => {
1252
+ headers[k] = v;
1253
+ });
1254
+ ip = this.nodeIp({
1255
+ headers,
1256
+ socket: { remoteAddress: init.remoteAddress ?? void 0 }
1257
+ });
1258
+ } catch {}
1259
+ const d = this.fetchDecision(request, ip);
1187
1260
  this.setRoute(d, init.route);
1188
1261
  this.fetchFinish(d, init.status ?? null, init.response, init.waitUntil);
1189
1262
  return d.detection;
@@ -1253,9 +1326,10 @@ var AgentStatsServer = class {
1253
1326
  };
1254
1327
  }
1255
1328
  /**
1256
- * Nuxt / Nitro plugin: `export default defineNitroPlugin(lens.nitro())` in `server/plugins/agentstats.ts`.
1329
+ * Nuxt / Nitro plugin: `export default defineNitroPlugin(lens.nitro())` in `server/plugins/agentstats.ts`
1330
+ * (Nitro v3: `export default definePlugin(lens.nitro())`).
1257
1331
  * The detection is on `event.context.bot` in server routes, and on `useRequestEvent()?.context.bot` in pages.
1258
- * Works on every Nitro preset (Node, Vercel, Netlify, Cloudflare…).
1332
+ * Works on every Nitro preset (Node, Vercel, Netlify, Cloudflare…), on Nitro v2 (h3 v1) and v3 (h3 v2).
1259
1333
  */
1260
1334
  nitro() {
1261
1335
  return (nitroApp) => {
@@ -1264,18 +1338,27 @@ var AgentStatsServer = class {
1264
1338
  event.context.bot = d.detection;
1265
1339
  if (d.kind === "ignore") return;
1266
1340
  event.context[DECISION] = d;
1267
- if (d.cookie && event.node?.res) appendNodeCookie(event.node.res, d.cookie);
1341
+ if (d.cookie) {
1342
+ if (isH3v2(event)) try {
1343
+ event.res.headers.append("set-cookie", d.cookie);
1344
+ } catch {}
1345
+ else if (event.node?.res) appendNodeCookie(event.node.res, d.cookie);
1346
+ }
1268
1347
  });
1269
- const finish = (event, status) => {
1348
+ const finish = (event, status, contentType) => {
1270
1349
  const d = event?.context[DECISION];
1271
1350
  if (!d) return;
1272
1351
  delete event.context[DECISION];
1273
- this.setRoute(d, event.context.matchedRoute?.path);
1274
- this.setContentType(d, event.node?.res?.getHeader?.("content-type"));
1352
+ const matched = event.context.matchedRoute;
1353
+ this.setRoute(d, matched?.path ?? matched?.route);
1354
+ this.setContentType(d, contentType);
1275
1355
  this.commit(d, status, typeof event.waitUntil === "function" ? event.waitUntil.bind(event) : void 0);
1276
1356
  };
1277
- nitroApp.hooks.hook("afterResponse", (event) => finish(event, event.node?.res?.statusCode ?? null));
1278
- nitroApp.hooks.hook("error", (error, context = {}) => finish(context.event, error?.statusCode ?? 500));
1357
+ nitroApp.hooks.hook("afterResponse", (event) => finish(event, event.node?.res?.statusCode ?? null, event.node?.res?.getHeader?.("content-type")));
1358
+ nitroApp.hooks.hook("response", (res, event) => finish(event, res?.status ?? event?.res?.status ?? null, res?.headers?.get?.("content-type")));
1359
+ nitroApp.hooks.hook("error", (error, context = {}) => {
1360
+ if (context.event && !isH3v2(context.event)) finish(context.event, error?.statusCode ?? error?.status ?? 500, context.event.node?.res?.getHeader?.("content-type"));
1361
+ });
1279
1362
  };
1280
1363
  }
1281
1364
  /**
@@ -1413,7 +1496,7 @@ var AgentStatsServer = class {
1413
1496
  const ua = header("user-agent") ?? null;
1414
1497
  if (!ip && !ua?.trim()) return ignore;
1415
1498
  const signed = detection.signed;
1416
- if (signed || detection.isBot || ua && BOT_HINT.test(ua)) {
1499
+ if (signed || detection.isBot || ua && (BOT_HINT.test(ua) || !looksLikeBrowser(ua))) {
1417
1500
  const record = {
1418
1501
  ts: Date.now(),
1419
1502
  method: m,
@@ -1578,15 +1661,18 @@ var AgentStatsServer = class {
1578
1661
  }
1579
1662
  h3Decision(event) {
1580
1663
  const req = event.node?.req;
1581
- const get = typeof event.headers?.get === "function" ? (name) => event.headers.get(name) : (name) => headerValue(req?.headers[name]);
1664
+ const web = isH3v2(event) ? event.req : void 0;
1665
+ const get = web ? (name) => web.headers.get(name) : typeof event.headers?.get === "function" ? (name) => event.headers.get(name) : (name) => headerValue(req?.headers[name]);
1582
1666
  try {
1583
- if (get("x-nitro-prerender") || (event.path ?? req?.url ?? "").startsWith("/__nuxt_error")) return {
1667
+ const path = event.url?.pathname ?? event.path ?? req?.url ?? "";
1668
+ if (get("x-nitro-prerender") || path.startsWith("/__nuxt_error")) return {
1584
1669
  kind: "ignore",
1585
1670
  detection: detectFromHeaders(get, null)
1586
1671
  };
1587
1672
  const native = !!req?.socket?.remoteAddress;
1588
- const url = native && req ? this.nodeUrl(req) : event.web?.request?.url ?? `${firstIp(get("x-forwarded-proto")) || "https"}://${get("host") ?? "localhost"}${event.path ?? req?.url ?? "/"}`;
1589
- return this.decide(event.method ?? req?.method ?? "GET", url, get, native && req ? this.nodeIp(req) : this.fetchIp(get));
1673
+ 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 ?? "/"}`;
1674
+ 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);
1675
+ return this.decide(web?.method ?? event.method ?? req?.method ?? "GET", url, get, ip);
1590
1676
  } catch (err) {
1591
1677
  this.log("track failed", err);
1592
1678
  return {
@@ -1625,6 +1711,10 @@ var AgentStatsServer = class {
1625
1711
  }
1626
1712
  };
1627
1713
  const DECISION = Symbol.for("agentstats.decision");
1714
+ /** An h3 v2 event (Nitro v3), whose request is a web `Request`. h3 v1 events have no `req`. */
1715
+ function isH3v2(event) {
1716
+ return typeof event.req?.headers?.get === "function";
1717
+ }
1628
1718
  function headerValue(v) {
1629
1719
  if (v === void 0) return null;
1630
1720
  return Array.isArray(v) ? v.join(", ") : v;
@@ -1638,7 +1728,6 @@ function appendNodeCookie(res, cookie) {
1638
1728
  function createAgentStats(options) {
1639
1729
  return new AgentStatsServer(options);
1640
1730
  }
1641
- //#endregion
1642
1731
  exports.AI_CATEGORIES = AI_CATEGORIES;
1643
1732
  exports.AgentStatsServer = AgentStatsServer;
1644
1733
  exports.BOT_PATTERNS = BOT_PATTERNS;