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