systemprompt_analytics/services/
service.rs1use 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(¶ms).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}