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