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