diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index d20748609a..5cc8b58cfb 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -17,7 +17,7 @@ use axum::{ use axum_extra::response::ErasedJson; use serde::{Deserialize, Serialize}; use serde_json::json; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use time::OffsetDateTime; use tower::ServiceBuilder; use tracing::instrument; @@ -43,6 +43,7 @@ pub fn routes(rpc_server: &Arc, state: Arc) -> Router, + Extension(app): Extension>, +) -> Result { + let durations_in_minutes = params.durations_in_minutes.split(','); + let mut user_sets = Vec::new(); + for duration in durations_in_minutes { + let duration = duration + .parse() + .map_err(|_| anyhow!("invalid duration: {duration}"))?; + user_sets.push(ActiveUserSet { + active_time_in_minutes: duration, + user_count: app + .db + .get_active_user_count( + params.period.start..params.period.end, + Duration::from_secs(duration * 60), + ) + .await?, + }) + } + Ok(ErasedJson::pretty(user_sets)) +} + #[derive(Deserialize)] struct GetProjectMetadataParams { project_id: u64, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e7ef0d5797..967bd2ee99 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -69,6 +69,14 @@ pub trait Db: Send + Sync { active_projects: &[(UserId, ProjectId)], ) -> Result<()>; + /// Get the number of users who have been active in the given + /// time period for at least the given time duration. + async fn get_active_user_count( + &self, + time_period: Range, + min_duration: Duration, + ) -> Result; + /// Get the users that have been most active during the given time period, /// along with the amount of time they have been active in each project. async fn get_top_users_activity_summary( @@ -593,6 +601,40 @@ impl Db for PostgresDb { Ok(()) } + async fn get_active_user_count( + &self, + time_period: Range, + min_duration: Duration, + ) -> Result { + let query = " + WITH + project_durations AS ( + SELECT user_id, project_id, SUM(duration_millis) AS project_duration + FROM project_activity_periods + WHERE $1 < ended_at AND ended_at <= $2 + GROUP BY user_id, project_id + ), + user_durations AS ( + SELECT user_id, SUM(project_duration) as total_duration + FROM project_durations + GROUP BY user_id + ORDER BY total_duration DESC + LIMIT $3 + ) + SELECT count(user_durations.user_id) + FROM user_durations + WHERE user_durations.total_duration >= $3 + "; + + let count: i64 = sqlx::query_scalar(query) + .bind(time_period.start) + .bind(time_period.end) + .bind(min_duration.as_millis() as i64) + .fetch_one(&self.pool) + .await?; + Ok(count as usize) + } + async fn get_top_users_activity_summary( &self, time_period: Range, @@ -1544,7 +1586,7 @@ pub mod tests { } #[tokio::test(flavor = "multi_thread")] - async fn test_project_activity() { + async fn test_user_activity() { let test_db = TestDb::postgres().await; let db = test_db.db(); @@ -1641,6 +1683,32 @@ pub mod tests { }, ] ); + + assert_eq!( + db.get_active_user_count(t0..t6, Duration::from_secs(56)) + .await + .unwrap(), + 0 + ); + assert_eq!( + db.get_active_user_count(t0..t6, Duration::from_secs(54)) + .await + .unwrap(), + 1 + ); + assert_eq!( + db.get_active_user_count(t0..t6, Duration::from_secs(30)) + .await + .unwrap(), + 2 + ); + assert_eq!( + db.get_active_user_count(t0..t6, Duration::from_secs(10)) + .await + .unwrap(), + 3 + ); + assert_eq!( db.get_user_activity_timeline(t3..t6, user_1).await.unwrap(), &[ @@ -2477,6 +2545,14 @@ pub mod tests { unimplemented!() } + async fn get_active_user_count( + &self, + _time_period: Range, + _min_duration: Duration, + ) -> Result { + unimplemented!() + } + async fn get_top_users_activity_summary( &self, _time_period: Range,