Skip to main content

systemprompt_users/sessions/
providers.rs

1//! Authoritative session persistence contracts.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use crate::Result;
7use crate::repository::SessionRepository;
8use async_trait::async_trait;
9use chrono::Utc;
10use systemprompt_identifiers::{SessionId, UserId};
11use systemprompt_traits::session_store::{
12    CreateSessionParams, SessionAttribution, SessionIdentity, SessionNetwork,
13};
14use systemprompt_traits::{
15    ActiveSession, AnalyticsProviderError, AnalyticsResult, AnalyticsSession, CreateSessionInput,
16    SessionProvider, SessionUsageCounters,
17};
18#[async_trait]
19impl SessionProvider for SessionRepository {
20    async fn create_session(&self, input: CreateSessionInput<'_>) -> AnalyticsResult<()> {
21        self.create_analytics_session(input)
22            .await
23            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
24    }
25
26    async fn find_recent_session_by_fingerprint(
27        &self,
28        fingerprint: &str,
29        max_age_seconds: i64,
30    ) -> AnalyticsResult<Option<AnalyticsSession>> {
31        let result = self
32            .find_recent_anonymous_by_fingerprint(fingerprint, max_age_seconds)
33            .await
34            .map_err(|e| AnalyticsProviderError::Internal(e.into()))?;
35
36        Ok(result.map(|r| AnalyticsSession {
37            session_id: r.session_id,
38            user_id: r.user_id,
39            fingerprint: Some(fingerprint.to_owned()),
40            created_at: Utc::now(),
41        }))
42    }
43
44    async fn find_session_by_id(
45        &self,
46        session_id: &SessionId,
47    ) -> AnalyticsResult<Option<AnalyticsSession>> {
48        let result = self
49            .find_by_id(session_id)
50            .await
51            .map_err(|e| AnalyticsProviderError::Internal(e.into()))?;
52
53        Ok(result.map(|r| AnalyticsSession {
54            session_id: r.session_id,
55            user_id: r.user_id,
56            fingerprint: r.fingerprint_hash,
57            created_at: r.timing.started_at.unwrap_or_else(Utc::now),
58        }))
59    }
60
61    async fn find_active_session_by_id(
62        &self,
63        session_id: &SessionId,
64    ) -> AnalyticsResult<Option<ActiveSession>> {
65        let result = self
66            .find_active_by_id(session_id)
67            .await
68            .map_err(|e| AnalyticsProviderError::Internal(e.into()))?;
69
70        Ok(result.map(|r| ActiveSession { user_id: r.user_id }))
71    }
72
73    async fn revoke_session(&self, session_id: &SessionId) -> AnalyticsResult<()> {
74        self.revoke_session(session_id)
75            .await
76            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
77    }
78
79    async fn revoke_all_sessions_for_user(&self, user_id: &UserId) -> AnalyticsResult<u64> {
80        self.revoke_all_for_user(user_id)
81            .await
82            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
83    }
84
85    async fn migrate_user_sessions(
86        &self,
87        from_user_id: &UserId,
88        to_user_id: &UserId,
89    ) -> AnalyticsResult<u64> {
90        self.migrate_user_sessions(from_user_id, to_user_id)
91            .await
92            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
93    }
94
95    async fn mark_session_converted(&self, session_id: &SessionId) -> AnalyticsResult<()> {
96        self.mark_converted(session_id)
97            .await
98            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
99    }
100}
101
102#[async_trait]
103impl SessionUsageCounters for SessionRepository {
104    async fn increment_task_count(&self, session_id: &SessionId) -> AnalyticsResult<()> {
105        Self::increment_task_count(self, session_id)
106            .await
107            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
108    }
109
110    async fn increment_message_count(&self, session_id: &SessionId) -> AnalyticsResult<()> {
111        Self::increment_message_count(self, session_id)
112            .await
113            .map_err(|e| AnalyticsProviderError::Internal(e.into()))
114    }
115}
116
117impl SessionRepository {
118    pub async fn create_analytics_session(&self, input: CreateSessionInput<'_>) -> Result<()> {
119        let fingerprint = input.analytics.compute_fingerprint();
120
121        let params = CreateSessionParams {
122            identity: SessionIdentity {
123                session_id: input.session_id,
124                user_id: input.user_id,
125                session_source: input.session_source,
126                fingerprint_hash: Some(&fingerprint),
127            },
128            network: SessionNetwork {
129                ip_address: input.analytics.ip_address.as_deref(),
130                user_agent: input.analytics.user_agent.as_deref(),
131                device_type: input.analytics.device_type.as_deref(),
132                browser: input.analytics.browser.as_deref(),
133                os: input.analytics.os.as_deref(),
134                country: input.analytics.country.as_deref(),
135                region: input.analytics.region.as_deref(),
136                city: input.analytics.city.as_deref(),
137                preferred_locale: input.analytics.preferred_locale.as_deref(),
138            },
139            attribution: SessionAttribution {
140                referrer_source: input.analytics.referrer_source.as_deref(),
141                referrer_url: input.analytics.referrer_url.as_deref(),
142                landing_page: input.analytics.landing_page.as_deref(),
143                entry_url: input.analytics.entry_url.as_deref(),
144                utm_source: input.analytics.utm_source.as_deref(),
145                utm_medium: input.analytics.utm_medium.as_deref(),
146                utm_campaign: input.analytics.utm_campaign.as_deref(),
147                utm_content: input.analytics.utm_content.as_deref(),
148                utm_term: input.analytics.utm_term.as_deref(),
149            },
150            is_bot: input.is_bot,
151            is_ai_crawler: input.is_ai_crawler,
152            expires_at: input.expires_at,
153        };
154
155        self.create_session(&params).await?;
156
157        Ok(())
158    }
159}