systemprompt_analytics/services/extractor/
mod.rs1use http::{HeaderMap, Uri};
14use std::collections::HashMap;
15
16use axum::extract::Request;
17
18use super::ai_crawler_keywords::matches_ai_crawler;
19use super::bot_keywords::{matches_bot_ip_range, matches_bot_pattern};
20use super::detection;
21use super::user_agent::parse_user_agent;
22use crate::GeoIpReader;
23
24mod geoip;
25
26#[derive(Debug, Clone, Default)]
27pub struct SessionAnalytics {
28 pub ip_address: Option<String>,
29 pub user_agent: Option<String>,
30 pub device_type: Option<String>,
31 pub browser: Option<String>,
32 pub os: Option<String>,
33 pub fingerprint_hash: Option<String>,
34 pub preferred_locale: Option<String>,
35 pub country: Option<String>,
36 pub region: Option<String>,
37 pub city: Option<String>,
38 pub referrer_source: Option<String>,
39 pub referrer_url: Option<String>,
40 pub landing_page: Option<String>,
41 pub entry_url: Option<String>,
42 pub utm_source: Option<String>,
43 pub utm_medium: Option<String>,
44 pub utm_campaign: Option<String>,
45 pub utm_content: Option<String>,
46 pub utm_term: Option<String>,
47}
48
49impl SessionAnalytics {
50 pub fn from_headers(headers: &HeaderMap) -> Self {
51 Self::from_headers_with_geoip(headers, None)
52 }
53
54 pub fn from_headers_with_geoip(
55 headers: &HeaderMap,
56 geoip_reader: Option<&GeoIpReader>,
57 ) -> Self {
58 Self::from_headers_with_geoip_and_socket(headers, geoip_reader, None)
59 }
60
61 pub fn from_headers_with_geoip_and_socket(
62 headers: &HeaderMap,
63 geoip_reader: Option<&GeoIpReader>,
64 socket_addr: Option<std::net::SocketAddr>,
65 ) -> Self {
66 let user_agent = headers
67 .get("user-agent")
68 .and_then(|v| v.to_str().ok())
69 .map(str::to_owned);
70
71 let ip_address = headers
72 .get("x-forwarded-for")
73 .and_then(|v| v.to_str().ok())
74 .and_then(|s| s.split(',').next())
75 .map(|s| s.trim().to_owned())
76 .or_else(|| {
77 headers
78 .get("x-real-ip")
79 .and_then(|v| v.to_str().ok())
80 .map(str::to_owned)
81 })
82 .or_else(|| socket_addr.map(|addr| addr.ip().to_string()));
83
84 let fingerprint_hash = headers
85 .get("x-fingerprint")
86 .and_then(|v| v.to_str().ok())
87 .map(str::to_owned);
88
89 let preferred_locale = headers
90 .get("accept-language")
91 .and_then(|v| v.to_str().ok())
92 .and_then(|s| s.split(',').next())
93 .map(|s| s.trim().split(';').next().unwrap_or(s).to_owned());
94
95 let (device_type, browser, os) = user_agent
96 .as_ref()
97 .map_or((None, None, None), |ua| parse_user_agent(ua));
98
99 let (country, region, city) = ip_address
100 .as_ref()
101 .and_then(|ip_str| geoip::lookup_geoip(ip_str, geoip_reader))
102 .unwrap_or((None, None, None));
103
104 let referrer_url = headers
105 .get("referer")
106 .and_then(|v| v.to_str().ok())
107 .map(str::to_owned);
108
109 let referrer_source = referrer_url
110 .as_ref()
111 .and_then(|url| Self::parse_referrer_source(url));
112
113 Self {
114 ip_address,
115 user_agent,
116 device_type,
117 browser,
118 os,
119 fingerprint_hash,
120 preferred_locale,
121 country,
122 region,
123 city,
124 referrer_source,
125 referrer_url,
126 landing_page: None,
127 entry_url: None,
128 utm_source: None,
129 utm_medium: None,
130 utm_campaign: None,
131 utm_content: None,
132 utm_term: None,
133 }
134 }
135
136 pub fn from_headers_and_uri(
137 headers: &HeaderMap,
138 uri: Option<&Uri>,
139 geoip_reader: Option<&GeoIpReader>,
140 content_routing: Option<&dyn systemprompt_models::ContentRouting>,
141 ) -> Self {
142 let mut analytics = Self::from_headers_with_geoip(headers, geoip_reader);
143
144 if let Some(uri) = uri {
145 let query_params = Self::parse_query_params(uri);
146
147 analytics.utm_source = query_params.get("utm_source").cloned();
148 analytics.utm_medium = query_params.get("utm_medium").cloned();
149 analytics.utm_campaign = query_params.get("utm_campaign").cloned();
150 analytics.utm_content = query_params.get("utm_content").cloned();
151 analytics.utm_term = query_params.get("utm_term").cloned();
152
153 let is_html_page =
154 content_routing.is_some_and(|routing| routing.is_html_page(uri.path()));
155
156 if is_html_page {
157 analytics.entry_url = Some(uri.to_string());
158 analytics.landing_page = Some(uri.path().to_owned());
159 }
160 }
161
162 analytics
163 }
164
165 pub fn from_request(
166 request: &Request,
167 geoip_reader: Option<&GeoIpReader>,
168 content_routing: Option<&dyn systemprompt_models::ContentRouting>,
169 ) -> Self {
170 Self::from_headers_and_uri(
171 request.headers(),
172 Some(request.uri()),
173 geoip_reader,
174 content_routing,
175 )
176 }
177
178 fn parse_query_params(uri: &Uri) -> HashMap<String, String> {
179 uri.query().map_or_else(HashMap::new, |q| {
180 q.split('&')
181 .filter_map(|param| {
182 let mut parts = param.splitn(2, '=');
183 Some((parts.next()?.to_owned(), parts.next()?.to_owned()))
184 })
185 .collect()
186 })
187 }
188
189 fn parse_referrer_source(url: &str) -> Option<String> {
190 geoip::parse_referrer_source(url)
191 }
192
193 pub fn is_bot(&self) -> bool {
194 if self.is_ai_crawler() {
195 return false;
196 }
197 self.user_agent
198 .as_ref()
199 .is_none_or(|ua| ua.is_empty() || matches_bot_pattern(ua))
200 }
201
202 pub fn is_ai_crawler(&self) -> bool {
203 self.user_agent
204 .as_ref()
205 .is_some_and(|ua| matches_ai_crawler(ua))
206 }
207
208 pub fn is_bot_ip(&self) -> bool {
209 self.ip_address
210 .as_ref()
211 .is_some_and(|ip| matches_bot_ip_range(ip))
212 }
213
214 pub fn is_spam_referrer(&self) -> bool {
215 self.referrer_url
216 .as_ref()
217 .is_some_and(|url| detection::is_spam_referrer(url))
218 }
219
220 pub fn is_datacenter_ip(&self) -> bool {
221 self.ip_address
222 .as_ref()
223 .is_some_and(|ip| detection::is_datacenter_ip(ip))
224 }
225
226 pub fn is_high_risk_country(&self) -> bool {
227 self.country
228 .as_ref()
229 .is_some_and(|c| detection::is_high_risk_country(c))
230 }
231
232 pub fn should_skip_tracking(&self) -> bool {
233 if self.is_ai_crawler() {
234 return false;
235 }
236 self.is_bot()
237 || self.is_bot_ip()
238 || self.is_datacenter_ip()
239 || self.is_high_risk_country()
240 || self.is_spam_referrer()
241 }
242}