1use async_trait::async_trait;
13use chrono::{DateTime, Utc};
14use http::{HeaderMap, Uri};
15use std::net::IpAddr;
16use std::sync::Arc;
17use systemprompt_identifiers::{SessionId, SessionSource, UserId};
18
19pub type AnalyticsResult<T> = Result<T, AnalyticsProviderError>;
20
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum AnalyticsProviderError {
24 #[error("Session not found")]
25 SessionNotFound,
26
27 #[error("Fingerprint not found")]
28 FingerprintNotFound,
29
30 #[error("Internal error: {0}")]
31 Internal(String),
32}
33
34#[derive(Debug, Clone, Default)]
41pub struct SessionAnalytics {
42 pub ip_address: Option<String>,
43 pub user_agent: Option<String>,
44 pub device_type: Option<String>,
45 pub browser: Option<String>,
46 pub os: Option<String>,
47 pub fingerprint_hash: Option<String>,
48 pub preferred_locale: Option<String>,
49 pub country: Option<String>,
50 pub region: Option<String>,
51 pub city: Option<String>,
52 pub referrer_source: Option<String>,
53 pub referrer_url: Option<String>,
54 pub landing_page: Option<String>,
55 pub entry_url: Option<String>,
56 pub utm_source: Option<String>,
57 pub utm_medium: Option<String>,
58 pub utm_campaign: Option<String>,
59 pub utm_content: Option<String>,
60 pub utm_term: Option<String>,
61 pub is_bot: bool,
62 pub is_ai_crawler: bool,
63 pub skip_tracking: bool,
64}
65
66impl SessionAnalytics {
67 pub fn compute_fingerprint(&self) -> String {
68 use xxhash_rust::xxh64::xxh64;
69
70 if let Some(hash) = &self.fingerprint_hash {
71 return hash.clone();
72 }
73
74 let data = format!(
75 "{}|{}",
76 self.user_agent.as_deref().unwrap_or(""),
77 self.preferred_locale.as_deref().unwrap_or("")
78 );
79
80 format!("fp_{:016x}", xxh64(data.as_bytes(), 0))
81 }
82}
83
84#[derive(Debug, Clone)]
85pub struct AnalyticsSession {
86 pub session_id: SessionId,
87 pub user_id: Option<UserId>,
88 pub fingerprint: Option<String>,
89 pub created_at: DateTime<Utc>,
90}
91
92#[derive(Debug, Clone)]
93pub struct ActiveSession {
94 pub user_id: Option<UserId>,
95}
96
97#[derive(Debug)]
98pub struct CreateSessionInput<'a> {
99 pub session_id: &'a SessionId,
100 pub user_id: Option<&'a UserId>,
101 pub analytics: &'a SessionAnalytics,
102 pub session_source: SessionSource,
103 pub is_bot: bool,
104 pub is_ai_crawler: bool,
105 pub expires_at: DateTime<Utc>,
106}
107
108impl<'a> CreateSessionInput<'a> {
109 #[must_use]
110 pub const fn new(
111 session_id: &'a SessionId,
112 analytics: &'a SessionAnalytics,
113 session_source: SessionSource,
114 expires_at: DateTime<Utc>,
115 ) -> Self {
116 Self {
117 session_id,
118 user_id: None,
119 analytics,
120 session_source,
121 is_bot: false,
122 is_ai_crawler: false,
123 expires_at,
124 }
125 }
126
127 #[must_use]
128 pub const fn with_user_id(mut self, user_id: &'a UserId) -> Self {
129 self.user_id = Some(user_id);
130 self
131 }
132
133 #[must_use]
134 pub const fn with_classification(mut self, is_bot: bool, is_ai_crawler: bool) -> Self {
135 self.is_bot = is_bot;
136 self.is_ai_crawler = is_ai_crawler;
137 self
138 }
139}
140
141#[derive(Debug, Default, Clone, Copy)]
145pub struct ExtractSignals<'a> {
146 pub uri: Option<&'a Uri>,
147 pub caller_ip: Option<IpAddr>,
148}
149
150pub trait AnalyticsProvider: Send + Sync {
151 fn extract_analytics(
152 &self,
153 headers: &HeaderMap,
154 signals: ExtractSignals<'_>,
155 ) -> SessionAnalytics;
156}
157
158#[async_trait]
159pub trait SessionProvider: Send + Sync {
160 async fn create_session(&self, input: CreateSessionInput<'_>) -> AnalyticsResult<()>;
161
162 async fn find_recent_session_by_fingerprint(
163 &self,
164 fingerprint: &str,
165 max_age_seconds: i64,
166 ) -> AnalyticsResult<Option<AnalyticsSession>>;
167
168 async fn find_session_by_id(
169 &self,
170 session_id: &SessionId,
171 ) -> AnalyticsResult<Option<AnalyticsSession>>;
172
173 async fn find_active_session_by_id(
174 &self,
175 session_id: &SessionId,
176 ) -> AnalyticsResult<Option<ActiveSession>>;
177
178 async fn revoke_session(&self, session_id: &SessionId) -> AnalyticsResult<()>;
179
180 async fn revoke_all_sessions_for_user(&self, user_id: &UserId) -> AnalyticsResult<u64>;
181
182 async fn migrate_user_sessions(
183 &self,
184 from_user_id: &UserId,
185 to_user_id: &UserId,
186 ) -> AnalyticsResult<u64>;
187
188 async fn mark_session_converted(&self, session_id: &SessionId) -> AnalyticsResult<()>;
189}
190
191#[async_trait]
197pub trait SessionUsageCounters: Send + Sync {
198 async fn increment_task_count(&self, session_id: &SessionId) -> AnalyticsResult<()>;
199
200 async fn increment_message_count(&self, session_id: &SessionId) -> AnalyticsResult<()>;
201}
202
203#[async_trait]
204pub trait FingerprintProvider: Send + Sync {
205 async fn count_active_sessions(&self, fingerprint: &str) -> AnalyticsResult<i64>;
206
207 async fn find_reusable_session(&self, fingerprint: &str) -> AnalyticsResult<Option<SessionId>>;
208
209 async fn upsert_fingerprint(
210 &self,
211 fingerprint: &str,
212 ip_address: Option<&str>,
213 user_agent: Option<&str>,
214 screen_info: Option<&str>,
215 ) -> AnalyticsResult<()>;
216}
217
218pub type DynAnalyticsProvider = Arc<dyn AnalyticsProvider>;
219
220pub type DynFingerprintProvider = Arc<dyn FingerprintProvider>;
221
222pub type DynSessionUsageCounters = Arc<dyn SessionUsageCounters>;
223
224pub type DynSessionProvider = Arc<dyn SessionProvider>;