Skip to main content

systemprompt_analytics/services/
service.rs

1//! `AnalyticsService`: request/session recording facade over the repositories.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::sync::Arc;
7
8use crate::Result;
9use http::HeaderMap;
10
11use systemprompt_database::DbPool;
12use systemprompt_models::ContentRouting;
13use systemprompt_traits::{CreateSessionInput, ExtractSignals};
14
15use crate::GeoIpReader;
16use crate::repository::{CreateSessionParams, SessionRecord, SessionRepository};
17use crate::services::{SessionAnalytics, SessionAnalyticsBuilder};
18
19#[derive(Clone)]
20pub struct AnalyticsService {
21    geoip_reader: Option<GeoIpReader>,
22    content_routing: Option<Arc<dyn ContentRouting>>,
23    session_repo: SessionRepository,
24}
25
26impl std::fmt::Debug for AnalyticsService {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("AnalyticsService")
29            .field("geoip_reader", &self.geoip_reader.is_some())
30            .field("content_routing", &self.content_routing.is_some())
31            .field("session_repo", &"SessionRepository")
32            .finish()
33    }
34}
35
36impl AnalyticsService {
37    pub fn new(
38        db_pool: &DbPool,
39        geoip_reader: Option<GeoIpReader>,
40        content_routing: Option<Arc<dyn ContentRouting>>,
41    ) -> Result<Self> {
42        Ok(Self {
43            geoip_reader,
44            content_routing,
45            session_repo: SessionRepository::new(db_pool)?,
46        })
47    }
48
49    pub fn extract_analytics(
50        &self,
51        headers: &HeaderMap,
52        signals: ExtractSignals<'_>,
53    ) -> SessionAnalytics {
54        let mut builder = SessionAnalyticsBuilder::new(headers);
55        if let Some(uri) = signals.uri {
56            builder = builder.with_uri(uri);
57        }
58        if let Some(reader) = self.geoip_reader.as_ref() {
59            builder = builder.with_geoip(reader);
60        }
61        if let Some(content_routing) = self.content_routing.as_deref() {
62            builder = builder.with_content_routing(content_routing);
63        }
64        if let Some(caller_ip) = signals.caller_ip {
65            builder = builder.with_caller_ip(caller_ip);
66        }
67        builder.build()
68    }
69
70    pub async fn create_analytics_session(&self, input: CreateSessionInput<'_>) -> Result<()> {
71        let fingerprint = input.analytics.compute_fingerprint();
72
73        let params = CreateSessionParams {
74            session_id: input.session_id,
75            user_id: input.user_id,
76            session_source: input.session_source,
77            fingerprint_hash: Some(&fingerprint),
78            ip_address: input.analytics.ip_address.as_deref(),
79            user_agent: input.analytics.user_agent.as_deref(),
80            device_type: input.analytics.device_type.as_deref(),
81            browser: input.analytics.browser.as_deref(),
82            os: input.analytics.os.as_deref(),
83            country: input.analytics.country.as_deref(),
84            region: input.analytics.region.as_deref(),
85            city: input.analytics.city.as_deref(),
86            preferred_locale: input.analytics.preferred_locale.as_deref(),
87            referrer_source: input.analytics.referrer_source.as_deref(),
88            referrer_url: input.analytics.referrer_url.as_deref(),
89            landing_page: input.analytics.landing_page.as_deref(),
90            entry_url: input.analytics.entry_url.as_deref(),
91            utm_source: input.analytics.utm_source.as_deref(),
92            utm_medium: input.analytics.utm_medium.as_deref(),
93            utm_content: input.analytics.utm_content.as_deref(),
94            utm_term: input.analytics.utm_term.as_deref(),
95            utm_campaign: input.analytics.utm_campaign.as_deref(),
96            is_bot: input.is_bot,
97            is_ai_crawler: input.is_ai_crawler,
98            expires_at: input.expires_at,
99        };
100
101        self.session_repo.create_session(&params).await?;
102
103        Ok(())
104    }
105
106    pub async fn find_recent_session_by_fingerprint(
107        &self,
108        fingerprint: &str,
109        max_age_seconds: i64,
110    ) -> Result<Option<SessionRecord>> {
111        self.session_repo
112            .find_recent_by_fingerprint(fingerprint, max_age_seconds)
113            .await
114    }
115
116    pub const fn session_repo(&self) -> &SessionRepository {
117        &self.session_repo
118    }
119}