systemprompt_analytics/services/
service.rs1use std::sync::Arc;
7
8use crate::Result;
9use chrono::{DateTime, Utc};
10use http::{HeaderMap, Uri};
11
12use axum::extract::Request;
13use systemprompt_database::DbPool;
14use systemprompt_identifiers::{SessionId, SessionSource, UserId};
15use systemprompt_models::ContentRouting;
16
17use crate::GeoIpReader;
18use crate::repository::{CreateSessionParams, SessionRecord, SessionRepository};
19use crate::services::SessionAnalytics;
20
21#[derive(Debug)]
22pub struct CreateAnalyticsSessionInput<'a> {
23 pub session_id: &'a SessionId,
24 pub user_id: Option<&'a UserId>,
25 pub analytics: &'a SessionAnalytics,
26 pub session_source: SessionSource,
27 pub is_bot: bool,
28 pub is_ai_crawler: bool,
29 pub expires_at: DateTime<Utc>,
30}
31
32#[derive(Clone)]
33pub struct AnalyticsService {
34 geoip_reader: Option<GeoIpReader>,
35 content_routing: Option<Arc<dyn ContentRouting>>,
36 session_repo: SessionRepository,
37}
38
39impl std::fmt::Debug for AnalyticsService {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("AnalyticsService")
42 .field("geoip_reader", &self.geoip_reader.is_some())
43 .field("content_routing", &self.content_routing.is_some())
44 .field("session_repo", &"SessionRepository")
45 .finish()
46 }
47}
48
49impl AnalyticsService {
50 pub fn new(
51 db_pool: &DbPool,
52 geoip_reader: Option<GeoIpReader>,
53 content_routing: Option<Arc<dyn ContentRouting>>,
54 ) -> Result<Self> {
55 Ok(Self {
56 geoip_reader,
57 content_routing,
58 session_repo: SessionRepository::new(db_pool)?,
59 })
60 }
61
62 pub fn extract_analytics(&self, headers: &HeaderMap, uri: Option<&Uri>) -> SessionAnalytics {
63 SessionAnalytics::from_headers_and_uri(
64 headers,
65 uri,
66 self.geoip_reader.as_ref(),
67 self.content_routing.as_deref(),
68 )
69 }
70
71 pub fn extract_from_request(&self, request: &Request) -> SessionAnalytics {
72 SessionAnalytics::from_headers_and_uri(
73 request.headers(),
74 Some(request.uri()),
75 self.geoip_reader.as_ref(),
76 self.content_routing.as_deref(),
77 )
78 }
79
80 pub fn is_bot(analytics: &SessionAnalytics) -> bool {
81 analytics.should_skip_tracking()
82 }
83
84 pub fn compute_fingerprint(analytics: &SessionAnalytics) -> String {
85 analytics.fingerprint_hash.clone().unwrap_or_else(|| {
86 use xxhash_rust::xxh64::xxh64;
87
88 let data = format!(
89 "{}|{}",
90 analytics.user_agent.as_deref().unwrap_or(""),
91 analytics.preferred_locale.as_deref().unwrap_or("")
92 );
93
94 format!("fp_{:016x}", xxh64(data.as_bytes(), 0))
95 })
96 }
97
98 pub async fn create_analytics_session(
99 &self,
100 input: CreateAnalyticsSessionInput<'_>,
101 ) -> Result<()> {
102 let fingerprint = Self::compute_fingerprint(input.analytics);
103
104 let params = CreateSessionParams {
105 session_id: input.session_id,
106 user_id: input.user_id,
107 session_source: input.session_source,
108 fingerprint_hash: Some(&fingerprint),
109 ip_address: input.analytics.ip_address.as_deref(),
110 user_agent: input.analytics.user_agent.as_deref(),
111 device_type: input.analytics.device_type.as_deref(),
112 browser: input.analytics.browser.as_deref(),
113 os: input.analytics.os.as_deref(),
114 country: input.analytics.country.as_deref(),
115 region: input.analytics.region.as_deref(),
116 city: input.analytics.city.as_deref(),
117 preferred_locale: input.analytics.preferred_locale.as_deref(),
118 referrer_source: input.analytics.referrer_source.as_deref(),
119 referrer_url: input.analytics.referrer_url.as_deref(),
120 landing_page: input.analytics.landing_page.as_deref(),
121 entry_url: input.analytics.entry_url.as_deref(),
122 utm_source: input.analytics.utm_source.as_deref(),
123 utm_medium: input.analytics.utm_medium.as_deref(),
124 utm_content: input.analytics.utm_content.as_deref(),
125 utm_term: input.analytics.utm_term.as_deref(),
126 utm_campaign: input.analytics.utm_campaign.as_deref(),
127 is_bot: input.is_bot,
128 is_ai_crawler: input.is_ai_crawler,
129 expires_at: input.expires_at,
130 };
131
132 self.session_repo.create_session(¶ms).await?;
133
134 Ok(())
135 }
136
137 pub async fn find_recent_session_by_fingerprint(
138 &self,
139 fingerprint: &str,
140 max_age_seconds: i64,
141 ) -> Result<Option<SessionRecord>> {
142 self.session_repo
143 .find_recent_by_fingerprint(fingerprint, max_age_seconds)
144 .await
145 }
146
147 pub const fn session_repo(&self) -> &SessionRepository {
148 &self.session_repo
149 }
150}