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 device_type: Option<String>,
33 pub browser: Option<String>,
34 pub os: Option<String>,
35 pub fingerprint_hash: Option<String>,
36 pub referer: Option<String>,
37 pub referrer_url: Option<String>,
38 pub referrer_source: Option<String>,
39 pub accept_language: Option<String>,
40 pub preferred_locale: Option<String>,
41 pub screen_width: Option<i32>,
42 pub screen_height: Option<i32>,
43 pub timezone: Option<String>,
44 pub page_url: Option<String>,
45 pub landing_page: Option<String>,
46 pub entry_url: Option<String>,
47 pub country: Option<String>,
48 pub region: Option<String>,
49 pub city: Option<String>,
50 pub utm_source: Option<String>,
51 pub utm_medium: Option<String>,
52 pub utm_campaign: Option<String>,
53 pub utm_content: Option<String>,
54 pub utm_term: Option<String>,
55}
56
57impl SessionAnalytics {
58 pub fn is_bot(&self) -> bool {
59 self.user_agent.as_ref().is_some_and(|ua| {
60 let ua_lower = ua.to_lowercase();
61 ua_lower.contains("bot")
62 || ua_lower.contains("crawler")
63 || ua_lower.contains("spider")
64 || ua_lower.contains("headless")
65 })
66 }
67
68 pub fn compute_fingerprint(&self) -> String {
69 use xxhash_rust::xxh64::xxh64;
70
71 if let Some(hash) = &self.fingerprint_hash {
72 return hash.clone();
73 }
74
75 let data = format!(
76 "{}|{}",
77 self.user_agent.as_deref().unwrap_or(""),
78 self.accept_language
79 .as_deref()
80 .or(self.preferred_locale.as_deref())
81 .unwrap_or("")
82 );
83
84 format!("fp_{:016x}", xxh64(data.as_bytes(), 0))
85 }
86}
87
88#[derive(Debug, Clone)]
89pub struct AnalyticsSession {
90 pub session_id: SessionId,
91 pub user_id: Option<UserId>,
92 pub fingerprint: Option<String>,
93 pub created_at: DateTime<Utc>,
94}
95
96#[derive(Debug)]
97pub struct CreateSessionInput<'a> {
98 pub session_id: &'a SessionId,
99 pub user_id: Option<&'a UserId>,
100 pub analytics: &'a SessionAnalytics,
101 pub session_source: SessionSource,
102 pub is_bot: bool,
103 pub expires_at: DateTime<Utc>,
104}
105
106#[async_trait]
107pub trait AnalyticsProvider: Send + Sync {
108 fn extract_analytics(&self, headers: &HeaderMap, uri: Option<&Uri>) -> SessionAnalytics;
109
110 async fn create_session(&self, input: CreateSessionInput<'_>) -> AnalyticsResult<()>;
111
112 async fn find_recent_session_by_fingerprint(
113 &self,
114 fingerprint: &str,
115 max_age_seconds: i64,
116 ) -> AnalyticsResult<Option<AnalyticsSession>>;
117
118 async fn find_session_by_id(
119 &self,
120 session_id: &SessionId,
121 ) -> AnalyticsResult<Option<AnalyticsSession>>;
122
123 async fn migrate_user_sessions(
124 &self,
125 from_user_id: &UserId,
126 to_user_id: &UserId,
127 ) -> AnalyticsResult<u64>;
128
129 async fn mark_session_converted(&self, session_id: &SessionId) -> AnalyticsResult<()>;
130}
131
132#[async_trait]
133pub trait FingerprintProvider: Send + Sync {
134 async fn count_active_sessions(&self, fingerprint: &str) -> AnalyticsResult<i64>;
135
136 async fn find_reusable_session(&self, fingerprint: &str) -> AnalyticsResult<Option<String>>;
137
138 async fn upsert_fingerprint(
139 &self,
140 fingerprint: &str,
141 ip_address: Option<&str>,
142 user_agent: Option<&str>,
143 screen_info: Option<&str>,
144 ) -> AnalyticsResult<()>;
145}
146
147pub type DynAnalyticsProvider = Arc<dyn AnalyticsProvider>;
148pub type DynFingerprintProvider = Arc<dyn FingerprintProvider>;