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