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