@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/README.md +10 -0
- package/dist/bots.cjs +1022 -0
- package/dist/bots.d.cts +265 -0
- package/dist/bots.d.ts +265 -0
- package/dist/bots.js +1010 -0
- package/dist/browser.iife.js +1 -1
- package/dist/index.cjs +3 -6
- package/dist/index.d.cts +1 -5
- package/dist/index.d.ts +1 -5
- package/dist/index.js +3 -6
- package/dist/lite.iife.js +1 -1
- package/dist/server.cjs +293 -204
- package/dist/server.d.cts +76 -18
- package/dist/server.d.ts +76 -18
- package/dist/server.js +293 -204
- package/package.json +21 -12
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
|
-
|
|
6
|
-
|
|
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
|
|
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: /
|
|
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
|
-
|
|
777
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
/**
|
|
794
|
-
function
|
|
795
|
-
const
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1048
|
+
const SERVER_SDK_VERSION = "0.3.2";
|
|
997
1049
|
/**
|
|
998
|
-
* Words that may mean a bot
|
|
999
|
-
*
|
|
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://
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1274
|
-
this.
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|