Skip to main content

systemprompt_traits/
analytics.rs

1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use http::{HeaderMap, Uri};
4use std::sync::Arc;
5use systemprompt_identifiers::{SessionId, SessionSource, UserId};
6
7pub type AnalyticsResult<T> = Result<T, AnalyticsProviderError>;
8
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum AnalyticsProviderError {
12    #[error("Session not found")]
13    SessionNotFound,
14
15    #[error("Fingerprint not found")]
16    FingerprintNotFound,
17
18    #[error("Internal error: {0}")]
19    Internal(String),
20}
21
22impl From<anyhow::Error> for AnalyticsProviderError {
23    fn from(err: anyhow::Error) -> Self {
24        Self::Internal(err.to_string())
25    }
26}
27
28#[derive(Debug, Clone, Default)]
29pub struct SessionAnalytics {
30    pub ip_address: Option<String>,
31    pub user_agent: Option<String>,
32    pub referer: Option<String>,
33    pub accept_language: Option<String>,
34    pub screen_width: Option<i32>,
35    pub screen_height: Option<i32>,
36    pub timezone: Option<String>,
37    pub page_url: Option<String>,
38}
39
40impl SessionAnalytics {
41    pub fn is_bot(&self) -> bool {
42        self.user_agent.as_ref().is_some_and(|ua| {
43            let ua_lower = ua.to_lowercase();
44            ua_lower.contains("bot")
45                || ua_lower.contains("crawler")
46                || ua_lower.contains("spider")
47                || ua_lower.contains("headless")
48        })
49    }
50
51    pub fn compute_fingerprint(&self) -> String {
52        use std::collections::hash_map::DefaultHasher;
53        use std::hash::{Hash, Hasher};
54
55        let mut hasher = DefaultHasher::new();
56        self.ip_address.hash(&mut hasher);
57        self.user_agent.hash(&mut hasher);
58        self.accept_language.hash(&mut hasher);
59        self.screen_width.hash(&mut hasher);
60        self.screen_height.hash(&mut hasher);
61        self.timezone.hash(&mut hasher);
62        format!("fp_{:x}", hasher.finish())
63    }
64}
65
66#[derive(Debug, Clone)]
67pub struct AnalyticsSession {
68    pub session_id: String,
69    pub user_id: Option<String>,
70    pub fingerprint: Option<String>,
71    pub created_at: DateTime<Utc>,
72}
73
74#[derive(Debug)]
75pub struct CreateSessionInput<'a> {
76    pub session_id: &'a SessionId,
77    pub user_id: Option<&'a UserId>,
78    pub analytics: &'a SessionAnalytics,
79    pub session_source: SessionSource,
80    pub is_bot: bool,
81    pub expires_at: DateTime<Utc>,
82}
83
84#[async_trait]
85pub trait AnalyticsProvider: Send + Sync {
86    fn extract_analytics(&self, headers: &HeaderMap, uri: Option<&Uri>) -> SessionAnalytics;
87
88    async fn create_session(&self, input: CreateSessionInput<'_>) -> AnalyticsResult<()>;
89
90    async fn find_recent_session_by_fingerprint(
91        &self,
92        fingerprint: &str,
93        max_age_seconds: i64,
94    ) -> AnalyticsResult<Option<AnalyticsSession>>;
95
96    async fn find_session_by_id(
97        &self,
98        session_id: &SessionId,
99    ) -> AnalyticsResult<Option<AnalyticsSession>>;
100
101    async fn migrate_user_sessions(
102        &self,
103        from_user_id: &UserId,
104        to_user_id: &UserId,
105    ) -> AnalyticsResult<u64>;
106}
107
108#[async_trait]
109pub trait FingerprintProvider: Send + Sync {
110    async fn count_active_sessions(&self, fingerprint: &str) -> AnalyticsResult<i64>;
111
112    async fn find_reusable_session(&self, fingerprint: &str) -> AnalyticsResult<Option<String>>;
113
114    async fn upsert_fingerprint(
115        &self,
116        fingerprint: &str,
117        ip_address: Option<&str>,
118        user_agent: Option<&str>,
119        screen_info: Option<&str>,
120    ) -> AnalyticsResult<()>;
121}
122
123pub type DynAnalyticsProvider = Arc<dyn AnalyticsProvider>;
124pub type DynFingerprintProvider = Arc<dyn FingerprintProvider>;