Skip to main content

systemprompt_analytics/services/
service.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use http::{HeaderMap, Uri};
6
7#[cfg(feature = "web")]
8use axum::extract::Request;
9use systemprompt_database::DbPool;
10use systemprompt_identifiers::{SessionId, SessionSource, UserId};
11use systemprompt_models::ContentRouting;
12
13use crate::repository::{CreateSessionParams, SessionRecord, SessionRepository};
14use crate::services::SessionAnalytics;
15use crate::GeoIpReader;
16
17#[derive(Debug)]
18pub struct CreateAnalyticsSessionInput<'a> {
19    pub session_id: &'a SessionId,
20    pub user_id: Option<&'a UserId>,
21    pub analytics: &'a SessionAnalytics,
22    pub session_source: SessionSource,
23    pub is_bot: bool,
24    pub expires_at: DateTime<Utc>,
25}
26
27#[derive(Clone)]
28pub struct AnalyticsService {
29    geoip_reader: Option<GeoIpReader>,
30    content_routing: Option<Arc<dyn ContentRouting>>,
31    session_repo: SessionRepository,
32}
33
34impl std::fmt::Debug for AnalyticsService {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("AnalyticsService")
37            .field("geoip_reader", &self.geoip_reader.is_some())
38            .field("content_routing", &self.content_routing.is_some())
39            .field("session_repo", &"SessionRepository")
40            .finish()
41    }
42}
43
44impl AnalyticsService {
45    pub fn new(
46        db_pool: DbPool,
47        geoip_reader: Option<GeoIpReader>,
48        content_routing: Option<Arc<dyn ContentRouting>>,
49    ) -> Self {
50        Self {
51            geoip_reader,
52            content_routing,
53            session_repo: SessionRepository::new(db_pool),
54        }
55    }
56
57    pub fn extract_analytics(&self, headers: &HeaderMap, uri: Option<&Uri>) -> SessionAnalytics {
58        SessionAnalytics::from_headers_and_uri(
59            headers,
60            uri,
61            self.geoip_reader.as_ref(),
62            self.content_routing.as_deref(),
63        )
64    }
65
66    #[cfg(feature = "web")]
67    pub fn extract_from_request(&self, request: &Request) -> SessionAnalytics {
68        SessionAnalytics::from_headers_and_uri(
69            request.headers(),
70            Some(request.uri()),
71            self.geoip_reader.as_ref(),
72            self.content_routing.as_deref(),
73        )
74    }
75
76    pub fn is_bot(analytics: &SessionAnalytics) -> bool {
77        analytics.should_skip_tracking()
78    }
79
80    pub fn compute_fingerprint(analytics: &SessionAnalytics) -> String {
81        analytics.fingerprint_hash.clone().unwrap_or_else(|| {
82            use xxhash_rust::xxh64::xxh64;
83
84            let data = format!(
85                "{}|{}",
86                analytics.user_agent.as_deref().unwrap_or(""),
87                analytics.preferred_locale.as_deref().unwrap_or("")
88            );
89
90            format!("fp_{:016x}", xxh64(data.as_bytes(), 0))
91        })
92    }
93
94    pub async fn create_analytics_session(
95        &self,
96        input: CreateAnalyticsSessionInput<'_>,
97    ) -> Result<()> {
98        let fingerprint = Self::compute_fingerprint(input.analytics);
99
100        let params = CreateSessionParams {
101            session_id: input.session_id,
102            user_id: input.user_id,
103            session_source: input.session_source,
104            fingerprint_hash: Some(&fingerprint),
105            ip_address: input.analytics.ip_address.as_deref(),
106            user_agent: input.analytics.user_agent.as_deref(),
107            device_type: input.analytics.device_type.as_deref(),
108            browser: input.analytics.browser.as_deref(),
109            os: input.analytics.os.as_deref(),
110            country: input.analytics.country.as_deref(),
111            region: input.analytics.region.as_deref(),
112            city: input.analytics.city.as_deref(),
113            preferred_locale: input.analytics.preferred_locale.as_deref(),
114            referrer_source: input.analytics.referrer_source.as_deref(),
115            referrer_url: input.analytics.referrer_url.as_deref(),
116            landing_page: input.analytics.landing_page.as_deref(),
117            entry_url: input.analytics.entry_url.as_deref(),
118            utm_source: input.analytics.utm_source.as_deref(),
119            utm_medium: input.analytics.utm_medium.as_deref(),
120            utm_campaign: input.analytics.utm_campaign.as_deref(),
121            is_bot: input.is_bot,
122            expires_at: input.expires_at,
123        };
124
125        self.session_repo.create_session(&params).await?;
126
127        Ok(())
128    }
129
130    pub async fn find_recent_session_by_fingerprint(
131        &self,
132        fingerprint: &str,
133        max_age_seconds: i64,
134    ) -> Result<Option<SessionRecord>> {
135        self.session_repo
136            .find_recent_by_fingerprint(fingerprint, max_age_seconds)
137            .await
138    }
139
140    pub const fn session_repo(&self) -> &SessionRepository {
141        &self.session_repo
142    }
143}