systemprompt_logging/repository/analytics/
ingestion.rs1use async_trait::async_trait;
7use systemprompt_identifiers::SessionId;
8use systemprompt_traits::RepositoryError;
9use systemprompt_traits::analytics_events::{AnalyticsEventRecord, AnalyticsEventStore};
10
11use super::AnalyticsRepository;
12
13#[async_trait]
14impl AnalyticsEventStore for AnalyticsRepository {
15 async fn persist_events(&self, events: &[AnalyticsEventRecord]) -> Result<(), RepositoryError> {
16 if events.is_empty() {
17 return Ok(());
18 }
19
20 let ids: Vec<_> = events.iter().map(|event| event.id.clone()).collect();
21 let user_ids: Vec<_> = events
22 .iter()
23 .map(|event| event.user_id.to_string())
24 .collect();
25 let session_ids: Vec<_> = events
26 .iter()
27 .map(|event| event.session_id.to_string())
28 .collect();
29 let event_types: Vec<_> = events
30 .iter()
31 .map(|event| event.event_type.clone())
32 .collect();
33 let event_categories: Vec<_> = events
34 .iter()
35 .map(|event| event.event_category.clone())
36 .collect();
37 let severities = vec!["info".to_owned(); events.len()];
38 let endpoints: Vec<_> = events.iter().map(|event| event.page_url.clone()).collect();
39 let event_datas: Vec<_> = events
40 .iter()
41 .map(|event| event.event_data.clone())
42 .collect();
43
44 sqlx::query!(
45 r#"
46 INSERT INTO analytics_events (id, user_id, session_id, event_type, event_category, severity, endpoint, event_data)
47 SELECT * FROM UNNEST($1::text[], $2::text[], $3::text[], $4::text[], $5::text[], $6::text[], $7::text[], $8::jsonb[])
48 "#,
49 &ids,
50 &user_ids,
51 &session_ids,
52 &event_types,
53 &event_categories,
54 &severities,
55 &endpoints,
56 &event_datas
57 )
58 .execute(&*self.write_pool)
59 .await
60 .map_err(RepositoryError::database)?;
61
62 Ok(())
63 }
64
65 async fn get_endpoint_sequence(
66 &self,
67 session_id: &SessionId,
68 ) -> Result<Vec<String>, RepositoryError> {
69 let endpoints = sqlx::query_scalar!(
70 r#"
71 SELECT endpoint
72 FROM analytics_events
73 WHERE session_id = $1
74 AND event_type = 'page_view'
75 ORDER BY timestamp ASC
76 "#,
77 session_id.as_str()
78 )
79 .fetch_all(&*self.write_pool)
80 .await
81 .map_err(RepositoryError::database)?;
82 Ok(endpoints.into_iter().flatten().collect())
83 }
84
85 async fn get_request_timestamps(
86 &self,
87 session_id: &SessionId,
88 ) -> Result<Vec<chrono::DateTime<chrono::Utc>>, RepositoryError> {
89 sqlx::query_scalar!(
90 r#"
91 SELECT timestamp as "timestamp!"
92 FROM analytics_events
93 WHERE session_id = $1
94 ORDER BY timestamp ASC
95 "#,
96 session_id.as_str()
97 )
98 .fetch_all(&*self.write_pool)
99 .await
100 .map_err(RepositoryError::database)
101 }
102
103 async fn has_analytics_events(&self, session_id: &SessionId) -> Result<bool, RepositoryError> {
104 sqlx::query_scalar!(
105 r#"
106 SELECT EXISTS(
107 SELECT 1 FROM analytics_events WHERE session_id = $1
108 ) as "exists!"
109 "#,
110 session_id.as_str()
111 )
112 .fetch_one(&*self.write_pool)
113 .await
114 .map_err(RepositoryError::database)
115 }
116}