Skip to main content

systemprompt_analytics/services/
providers.rs

1//! Bridges this crate's analytics services to the `systemprompt_traits`
2//! provider contracts.
3//!
4//! Implements [`AnalyticsProvider`] for `AnalyticsService` and
5//! [`FingerprintProvider`] for `FingerprintRepository`, translating between
6//! the crate-local types and the trait-level types and mapping every error
7//! into the providers' error enums. `#[async_trait]` is required because
8//! these provider traits are consumed as `dyn`.
9
10use async_trait::async_trait;
11use chrono::Utc;
12use http::{HeaderMap, Uri};
13use systemprompt_identifiers::{SessionId, UserId};
14use systemprompt_traits::{
15    ActiveSession, AnalyticsProvider, AnalyticsProviderError, AnalyticsResult, AnalyticsSession,
16    CreateSessionInput, FingerprintProvider, SessionAnalytics as TraitSessionAnalytics,
17};
18
19use super::SessionAnalytics;
20use super::service::AnalyticsService;
21use crate::repository::FingerprintRepository;
22
23#[async_trait]
24impl AnalyticsProvider for AnalyticsService {
25    fn extract_analytics(&self, headers: &HeaderMap, uri: Option<&Uri>) -> TraitSessionAnalytics {
26        let local = Self::extract_analytics(self, headers, uri);
27        TraitSessionAnalytics {
28            ip_address: local.ip_address,
29            user_agent: local.user_agent,
30            device_type: local.device_type,
31            browser: local.browser,
32            os: local.os,
33            fingerprint_hash: local.fingerprint_hash,
34            referer: local.referrer_url.clone(),
35            referrer_url: local.referrer_url,
36            referrer_source: local.referrer_source,
37            accept_language: local.preferred_locale.clone(),
38            preferred_locale: local.preferred_locale,
39            screen_width: None,
40            screen_height: None,
41            timezone: None,
42            page_url: local.entry_url.clone(),
43            landing_page: local.landing_page,
44            entry_url: local.entry_url,
45            country: local.country,
46            region: local.region,
47            city: local.city,
48            utm_source: local.utm_source,
49            utm_medium: local.utm_medium,
50            utm_campaign: local.utm_campaign,
51            utm_content: local.utm_content,
52            utm_term: local.utm_term,
53        }
54    }
55
56    async fn create_session(&self, input: CreateSessionInput<'_>) -> AnalyticsResult<()> {
57        let local_analytics = SessionAnalytics {
58            ip_address: input.analytics.ip_address.clone(),
59            user_agent: input.analytics.user_agent.clone(),
60            device_type: input.analytics.device_type.clone(),
61            browser: input.analytics.browser.clone(),
62            os: input.analytics.os.clone(),
63            fingerprint_hash: input.analytics.fingerprint_hash.clone(),
64            referrer_url: input
65                .analytics
66                .referrer_url
67                .clone()
68                .or_else(|| input.analytics.referer.clone()),
69            referrer_source: input.analytics.referrer_source.clone(),
70            preferred_locale: input
71                .analytics
72                .preferred_locale
73                .clone()
74                .or_else(|| input.analytics.accept_language.clone()),
75            landing_page: input.analytics.landing_page.clone(),
76            entry_url: input
77                .analytics
78                .entry_url
79                .clone()
80                .or_else(|| input.analytics.page_url.clone()),
81            country: input.analytics.country.clone(),
82            region: input.analytics.region.clone(),
83            city: input.analytics.city.clone(),
84            utm_source: input.analytics.utm_source.clone(),
85            utm_medium: input.analytics.utm_medium.clone(),
86            utm_campaign: input.analytics.utm_campaign.clone(),
87            utm_content: input.analytics.utm_content.clone(),
88            utm_term: input.analytics.utm_term.clone(),
89        };
90
91        let local_input = super::service::CreateAnalyticsSessionInput {
92            session_id: input.session_id,
93            user_id: input.user_id,
94            analytics: &local_analytics,
95            session_source: input.session_source,
96            is_bot: input.is_bot,
97            is_ai_crawler: input.is_ai_crawler,
98            expires_at: input.expires_at,
99        };
100
101        self.create_analytics_session(local_input)
102            .await
103            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
104    }
105
106    async fn find_recent_session_by_fingerprint(
107        &self,
108        fingerprint: &str,
109        max_age_seconds: i64,
110    ) -> AnalyticsResult<Option<AnalyticsSession>> {
111        let result = Self::find_recent_session_by_fingerprint(self, fingerprint, max_age_seconds)
112            .await
113            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))?;
114
115        Ok(result.map(|r| AnalyticsSession {
116            session_id: r.session_id,
117            user_id: r.user_id,
118            fingerprint: Some(fingerprint.to_owned()),
119            created_at: Utc::now(),
120        }))
121    }
122
123    async fn find_session_by_id(
124        &self,
125        session_id: &SessionId,
126    ) -> AnalyticsResult<Option<AnalyticsSession>> {
127        let result = self
128            .session_repo()
129            .find_by_id(session_id)
130            .await
131            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))?;
132
133        Ok(result.map(|r| AnalyticsSession {
134            session_id: r.session_id,
135            user_id: r.user_id,
136            fingerprint: r.fingerprint_hash,
137            created_at: r.started_at.unwrap_or_else(Utc::now),
138        }))
139    }
140
141    async fn find_active_session_by_id(
142        &self,
143        session_id: &SessionId,
144    ) -> AnalyticsResult<Option<ActiveSession>> {
145        let result = self
146            .session_repo()
147            .find_active_by_id(session_id)
148            .await
149            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))?;
150
151        Ok(result.map(|r| ActiveSession { user_id: r.user_id }))
152    }
153
154    async fn revoke_session(&self, session_id: &SessionId) -> AnalyticsResult<()> {
155        self.session_repo()
156            .revoke_session(session_id)
157            .await
158            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
159    }
160
161    async fn revoke_all_sessions_for_user(&self, user_id: &UserId) -> AnalyticsResult<u64> {
162        self.session_repo()
163            .revoke_all_for_user(user_id)
164            .await
165            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
166    }
167
168    async fn migrate_user_sessions(
169        &self,
170        from_user_id: &UserId,
171        to_user_id: &UserId,
172    ) -> AnalyticsResult<u64> {
173        self.session_repo()
174            .migrate_user_sessions(from_user_id, to_user_id)
175            .await
176            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
177    }
178
179    async fn mark_session_converted(&self, session_id: &SessionId) -> AnalyticsResult<()> {
180        self.session_repo()
181            .mark_converted(session_id)
182            .await
183            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
184    }
185}
186
187#[async_trait]
188impl FingerprintProvider for FingerprintRepository {
189    async fn count_active_sessions(&self, fingerprint: &str) -> AnalyticsResult<i64> {
190        self.count_active_sessions(fingerprint)
191            .await
192            .map(i64::from)
193            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
194    }
195
196    async fn find_reusable_session(&self, fingerprint: &str) -> AnalyticsResult<Option<String>> {
197        self.find_reusable_session(fingerprint)
198            .await
199            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
200    }
201
202    async fn upsert_fingerprint(
203        &self,
204        fingerprint: &str,
205        ip_address: Option<&str>,
206        user_agent: Option<&str>,
207        _screen_info: Option<&str>,
208    ) -> AnalyticsResult<()> {
209        self.upsert_fingerprint(fingerprint, ip_address, user_agent, None)
210            .await
211            .map(|_| ())
212            .map_err(|e| AnalyticsProviderError::Internal(e.to_string()))
213    }
214}