systemprompt_traits/
analytics.rs1use 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 xxhash_rust::xxh64::xxh64;
53
54 let data = format!(
55 "{}|{}",
56 self.user_agent.as_deref().unwrap_or(""),
57 self.accept_language.as_deref().unwrap_or("")
58 );
59
60 format!("fp_{:016x}", xxh64(data.as_bytes(), 0))
61 }
62}
63
64#[derive(Debug, Clone)]
65pub struct AnalyticsSession {
66 pub session_id: String,
67 pub user_id: Option<String>,
68 pub fingerprint: Option<String>,
69 pub created_at: DateTime<Utc>,
70}
71
72#[derive(Debug)]
73pub struct CreateSessionInput<'a> {
74 pub session_id: &'a SessionId,
75 pub user_id: Option<&'a UserId>,
76 pub analytics: &'a SessionAnalytics,
77 pub session_source: SessionSource,
78 pub is_bot: bool,
79 pub expires_at: DateTime<Utc>,
80}
81
82#[async_trait]
83pub trait AnalyticsProvider: Send + Sync {
84 fn extract_analytics(&self, headers: &HeaderMap, uri: Option<&Uri>) -> SessionAnalytics;
85
86 async fn create_session(&self, input: CreateSessionInput<'_>) -> AnalyticsResult<()>;
87
88 async fn find_recent_session_by_fingerprint(
89 &self,
90 fingerprint: &str,
91 max_age_seconds: i64,
92 ) -> AnalyticsResult<Option<AnalyticsSession>>;
93
94 async fn find_session_by_id(
95 &self,
96 session_id: &SessionId,
97 ) -> AnalyticsResult<Option<AnalyticsSession>>;
98
99 async fn migrate_user_sessions(
100 &self,
101 from_user_id: &UserId,
102 to_user_id: &UserId,
103 ) -> AnalyticsResult<u64>;
104}
105
106#[async_trait]
107pub trait FingerprintProvider: Send + Sync {
108 async fn count_active_sessions(&self, fingerprint: &str) -> AnalyticsResult<i64>;
109
110 async fn find_reusable_session(&self, fingerprint: &str) -> AnalyticsResult<Option<String>>;
111
112 async fn upsert_fingerprint(
113 &self,
114 fingerprint: &str,
115 ip_address: Option<&str>,
116 user_agent: Option<&str>,
117 screen_info: Option<&str>,
118 ) -> AnalyticsResult<()>;
119}
120
121pub type DynAnalyticsProvider = Arc<dyn AnalyticsProvider>;
122pub type DynFingerprintProvider = Arc<dyn FingerprintProvider>;