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_models::ContentRouting;
12use systemprompt_traits::{CreateSessionInput, ExtractSignals};
13
14use crate::GeoIpReader;
15use crate::repository::{
16    AnalyticsRepositories, CostAnalyticsRepository, CreateSessionParams, SessionRecord,
17    SessionRepository,
18};
19use crate::services::{SessionAnalytics, SessionAnalyticsBuilder};
20
21#[derive(Clone)]
22pub struct AnalyticsService {
23    geoip_reader: Option<GeoIpReader>,
24    content_routing: Option<Arc<dyn ContentRouting>>,
25    session_repo: SessionRepository,
26    cost_repo: CostAnalyticsRepository,
27}
28
29impl std::fmt::Debug for AnalyticsService {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("AnalyticsService")
32            .field("geoip_reader", &self.geoip_reader.is_some())
33            .field("content_routing", &self.content_routing.is_some())
34            .field("session_repo", &"SessionRepository")
35            .field("cost_repo", &"CostAnalyticsRepository")
36            .finish()
37    }
38}
39
40impl AnalyticsService {
41    pub fn new(
42        geoip_reader: Option<GeoIpReader>,
43        content_routing: Option<Arc<dyn ContentRouting>>,
44        repositories: &AnalyticsRepositories,
45    ) -> Self {
46        Self {
47            geoip_reader,
48            content_routing,
49            session_repo: repositories.sessions.clone(),
50            cost_repo: repositories.costs.clone(),
51        }
52    }
53
54    pub fn extract_analytics(
55        &self,
56        headers: &HeaderMap,
57        signals: ExtractSignals<'_>,
58    ) -> SessionAnalytics {
59        let mut builder = SessionAnalyticsBuilder::new(headers);
60        if let Some(uri) = signals.uri {
61            builder = builder.with_uri(uri);
62        }
63        if let Some(reader) = self.geoip_reader.as_ref() {
64            builder = builder.with_geoip(reader);
65        }
66        if let Some(content_routing) = self.content_routing.as_deref() {
67            builder = builder.with_content_routing(content_routing);
68        }
69        if let Some(caller_ip) = signals.caller_ip {
70            builder = builder.with_caller_ip(caller_ip);
71        }
72        builder.build()
73    }
74
75    pub async fn create_analytics_session(&self, input: CreateSessionInput<'_>) -> Result<()> {
76        let fingerprint = input.analytics.compute_fingerprint();
77
78        let params = CreateSessionParams {
79            session_id: input.session_id,
80            user_id: input.user_id,
81            session_source: input.session_source,
82            fingerprint_hash: Some(&fingerprint),
83            ip_address: input.analytics.ip_address.as_deref(),
84            user_agent: input.analytics.user_agent.as_deref(),
85            device_type: input.analytics.device_type.as_deref(),
86            browser: input.analytics.browser.as_deref(),
87            os: input.analytics.os.as_deref(),
88            country: input.analytics.country.as_deref(),
89            region: input.analytics.region.as_deref(),
90            city: input.analytics.city.as_deref(),
91            preferred_locale: input.analytics.preferred_locale.as_deref(),
92            referrer_source: input.analytics.referrer_source.as_deref(),
93            referrer_url: input.analytics.referrer_url.as_deref(),
94            landing_page: input.analytics.landing_page.as_deref(),
95            entry_url: input.analytics.entry_url.as_deref(),
96            utm_source: input.analytics.utm_source.as_deref(),
97            utm_medium: input.analytics.utm_medium.as_deref(),
98            utm_content: input.analytics.utm_content.as_deref(),
99            utm_term: input.analytics.utm_term.as_deref(),
100            utm_campaign: input.analytics.utm_campaign.as_deref(),
101            is_bot: input.is_bot,
102            is_ai_crawler: input.is_ai_crawler,
103            expires_at: input.expires_at,
104        };
105
106        self.session_repo.create_session(&params).await?;
107
108        Ok(())
109    }
110
111    pub async fn find_recent_session_by_fingerprint(
112        &self,
113        fingerprint: &str,
114        max_age_seconds: i64,
115    ) -> Result<Option<SessionRecord>> {
116        self.session_repo
117            .find_recent_by_fingerprint(fingerprint, max_age_seconds)
118            .await
119    }
120
121    pub const fn cost_repo(&self) -> &CostAnalyticsRepository {
122        &self.cost_repo
123    }
124
125    pub const fn session_repo(&self) -> &SessionRepository {
126        &self.session_repo
127    }
128}