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