systemprompt_analytics/repository/
events.rs1use std::sync::Arc;
13
14use crate::Result;
15use sqlx::PgPool;
16use systemprompt_database::DbPool;
17use systemprompt_identifiers::{ContentId, SessionId, UserId};
18
19use crate::models::{AnalyticsEventCreated, AnalyticsEventType, CreateAnalyticsEventInput};
20
21#[derive(Clone, Debug)]
22pub struct AnalyticsEventsRepository {
23 pool: Arc<PgPool>,
24 write_pool: Arc<PgPool>,
25}
26
27impl AnalyticsEventsRepository {
28 pub fn new(db: &DbPool) -> Result<Self> {
29 let pool = db.pool_arc()?;
30 let write_pool = db.write_pool_arc()?;
31 Ok(Self { pool, write_pool })
32 }
33
34 pub async fn create_event(
35 &self,
36 session_id: &SessionId,
37 user_id: &UserId,
38 input: &CreateAnalyticsEventInput,
39 ) -> Result<AnalyticsEventCreated> {
40 let id = format!("evt_{}", uuid::Uuid::new_v4());
41 let event_type = input.event_type.as_str();
42 let event_category = input.event_type.category();
43
44 let event_data = Self::build_event_data(input);
45
46 sqlx::query!(
47 r#"
48 INSERT INTO analytics_events (
49 id, user_id, session_id, event_type, event_category,
50 severity, endpoint, event_data
51 )
52 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
53 "#,
54 id,
55 user_id.as_str(),
56 session_id.as_str(),
57 event_type,
58 event_category,
59 "info",
60 input.page_url,
61 event_data
62 )
63 .execute(&*self.write_pool)
64 .await?;
65
66 Ok(AnalyticsEventCreated {
67 id,
68 event_type: event_type.to_owned(),
69 })
70 }
71
72 pub async fn create_events_batch(
73 &self,
74 session_id: &SessionId,
75 user_id: &UserId,
76 inputs: &[CreateAnalyticsEventInput],
77 ) -> Result<Vec<AnalyticsEventCreated>> {
78 if inputs.is_empty() {
79 return Ok(Vec::new());
80 }
81
82 let mut ids = Vec::with_capacity(inputs.len());
83 let mut user_ids = Vec::with_capacity(inputs.len());
84 let mut session_ids = Vec::with_capacity(inputs.len());
85 let mut event_types = Vec::with_capacity(inputs.len());
86 let mut event_categories = Vec::with_capacity(inputs.len());
87 let mut severities = Vec::with_capacity(inputs.len());
88 let mut endpoints: Vec<String> = Vec::with_capacity(inputs.len());
89 let mut event_datas = Vec::with_capacity(inputs.len());
90
91 for input in inputs {
92 let id = format!("evt_{}", uuid::Uuid::new_v4());
93 ids.push(id);
94 user_ids.push(user_id.as_str().to_owned());
95 session_ids.push(session_id.as_str().to_owned());
96 event_types.push(input.event_type.as_str().to_owned());
97 event_categories.push(input.event_type.category().to_owned());
98 severities.push("info".to_owned());
99 endpoints.push(input.page_url.clone());
100 event_datas.push(Self::build_event_data(input));
101 }
102
103 sqlx::query!(
104 r#"
105 INSERT INTO analytics_events (id, user_id, session_id, event_type, event_category, severity, endpoint, event_data)
106 SELECT * FROM UNNEST($1::text[], $2::text[], $3::text[], $4::text[], $5::text[], $6::text[], $7::text[], $8::jsonb[])
107 "#,
108 &ids,
109 &user_ids,
110 &session_ids,
111 &event_types,
112 &event_categories,
113 &severities,
114 &endpoints,
115 &event_datas
116 )
117 .execute(&*self.write_pool)
118 .await?;
119
120 Ok(ids
121 .into_iter()
122 .zip(event_types)
123 .map(|(id, event_type)| AnalyticsEventCreated { id, event_type })
124 .collect())
125 }
126
127 pub async fn count_events_by_type(
128 &self,
129 session_id: &SessionId,
130 event_type: &AnalyticsEventType,
131 ) -> Result<i64> {
132 let count = sqlx::query_scalar!(
133 r#"
134 SELECT COUNT(*) as "count!"
135 FROM analytics_events
136 WHERE session_id = $1 AND event_type = $2
137 "#,
138 session_id.as_str(),
139 event_type.as_str()
140 )
141 .fetch_one(&*self.pool)
142 .await?;
143
144 Ok(count)
145 }
146
147 pub async fn find_by_session(
148 &self,
149 session_id: &SessionId,
150 limit: i64,
151 ) -> Result<Vec<StoredAnalyticsEvent>> {
152 let events = sqlx::query_as!(
153 StoredAnalyticsEvent,
154 r#"
155 SELECT
156 id,
157 user_id as "user_id: UserId",
158 session_id as "session_id: SessionId",
159 event_type,
160 event_category,
161 endpoint as page_url,
162 event_data,
163 timestamp
164 FROM analytics_events
165 WHERE session_id = $1
166 ORDER BY timestamp DESC
167 LIMIT $2
168 "#,
169 session_id.as_str(),
170 limit
171 )
172 .fetch_all(&*self.pool)
173 .await?;
174
175 Ok(events)
176 }
177
178 pub async fn find_by_content(
179 &self,
180 content_id: &ContentId,
181 limit: i64,
182 ) -> Result<Vec<StoredAnalyticsEvent>> {
183 let events = sqlx::query_as!(
184 StoredAnalyticsEvent,
185 r#"
186 SELECT
187 id,
188 user_id as "user_id: UserId",
189 session_id as "session_id: SessionId",
190 event_type,
191 event_category,
192 endpoint as page_url,
193 event_data,
194 timestamp
195 FROM analytics_events
196 WHERE event_data->>'content_id' = $1
197 ORDER BY timestamp DESC
198 LIMIT $2
199 "#,
200 content_id.as_str(),
201 limit
202 )
203 .fetch_all(&*self.pool)
204 .await?;
205
206 Ok(events)
207 }
208
209 fn build_event_data(input: &CreateAnalyticsEventInput) -> serde_json::Value {
210 let mut data = input.data.clone().unwrap_or(serde_json::json!({}));
211
212 if let Some(obj) = data.as_object_mut() {
213 if let Some(content_id) = &input.content_id {
214 obj.insert(
215 "content_id".to_owned(),
216 serde_json::json!(content_id.as_str()),
217 );
218 }
219 if let Some(slug) = &input.slug {
220 obj.insert("slug".to_owned(), serde_json::json!(slug));
221 }
222 if let Some(referrer) = &input.referrer {
223 obj.insert("referrer".to_owned(), serde_json::json!(referrer));
224 }
225 }
226
227 data
228 }
229}
230
231#[derive(Debug, Clone, sqlx::FromRow)]
232pub struct StoredAnalyticsEvent {
233 pub id: String,
234 pub user_id: UserId,
235 pub session_id: Option<SessionId>,
236 pub event_type: String,
237 pub event_category: String,
238 pub page_url: Option<String>,
239 pub event_data: Option<serde_json::Value>,
240 pub timestamp: chrono::DateTime<chrono::Utc>,
241}