Skip to main content

systemprompt_analytics/services/
service.rs

1//! Request signal extraction and analytics service composition.
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 http::HeaderMap;
9
10use systemprompt_models::ContentRouting;
11use systemprompt_traits::ExtractSignals;
12
13use crate::GeoIpReader;
14use crate::repository::{AnalyticsRepositories, CostAnalyticsRepository, SessionRepository};
15use crate::services::{ProfileUsageService, SessionAnalytics, SessionAnalyticsBuilder};
16
17#[derive(Clone)]
18pub struct AnalyticsService {
19    geoip_reader: Option<GeoIpReader>,
20    content_routing: Option<Arc<dyn ContentRouting>>,
21    session_repo: SessionRepository,
22    cost_repo: CostAnalyticsRepository,
23}
24
25impl std::fmt::Debug for AnalyticsService {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("AnalyticsService")
28            .field("geoip_reader", &self.geoip_reader.is_some())
29            .field("content_routing", &self.content_routing.is_some())
30            .field("session_repo", &"SessionRepository")
31            .field("cost_repo", &"CostAnalyticsRepository")
32            .finish()
33    }
34}
35
36impl AnalyticsService {
37    pub fn new(
38        geoip_reader: Option<GeoIpReader>,
39        content_routing: Option<Arc<dyn ContentRouting>>,
40        repositories: &AnalyticsRepositories,
41    ) -> Self {
42        Self {
43            geoip_reader,
44            content_routing,
45            session_repo: repositories.sessions.clone(),
46            cost_repo: repositories.costs.clone(),
47        }
48    }
49
50    pub fn extract_analytics(
51        &self,
52        headers: &HeaderMap,
53        signals: ExtractSignals<'_>,
54    ) -> SessionAnalytics {
55        let mut builder = SessionAnalyticsBuilder::new(headers);
56        if let Some(uri) = signals.uri {
57            builder = builder.with_uri(uri);
58        }
59        if let Some(reader) = self.geoip_reader.as_ref() {
60            builder = builder.with_geoip(reader);
61        }
62        if let Some(content_routing) = self.content_routing.as_deref() {
63            builder = builder.with_content_routing(content_routing);
64        }
65        if let Some(caller_ip) = signals.caller_ip {
66            builder = builder.with_caller_ip(caller_ip);
67        }
68        builder.build()
69    }
70
71
72    pub const fn cost_repo(&self) -> &CostAnalyticsRepository {
73        &self.cost_repo
74    }
75
76    #[must_use]
77    pub fn profile_usage(&self) -> ProfileUsageService {
78        ProfileUsageService::new(self.cost_repo.clone())
79    }
80
81    pub const fn session_repo(&self) -> &SessionRepository {
82        &self.session_repo
83    }
84}