@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.js
CHANGED
|
@@ -1,164 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
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: /
|
|
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
|
-
|
|
773
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
/**
|
|
790
|
-
function
|
|
791
|
-
const
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1044
|
+
const SERVER_SDK_VERSION = "0.3.2";
|
|
993
1045
|
/**
|
|
994
|
-
* Words that may mean a bot
|
|
995
|
-
*
|
|
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://
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1270
|
-
this.
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.5.
|
|
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
|
+
}
|