From 1363d2c502fee340ea4f4a89b6dc372e8c2d9fcc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Jul 2022 14:58:08 -0700 Subject: [PATCH 1/2] Add admin API for counting users with a given amount of activity --- crates/collab/src/api.rs | 28 ++++++++++++++- crates/collab/src/db.rs | 78 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index d20748609a..6ecb43f7fd 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 = [10, 60, 4 * 60, 8 * 60]; + + let mut user_sets = Vec::with_capacity(durations_in_minutes.len()); + for duration in durations_in_minutes { + user_sets.push(ActiveUserSet { + active_time_in_minutes: duration, + user_count: app + .db + .get_active_user_count(params.start..params.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, From 69146fb318641a212981de051451a06cba734273 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Jul 2022 16:30:13 -0700 Subject: [PATCH 2/2] Allow the web client to specify activity bucket durations --- crates/collab/src/api.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 6ecb43f7fd..5cc8b58cfb 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -299,6 +299,13 @@ async fn get_user_activity_timeline( Ok(ErasedJson::pretty(summary)) } +#[derive(Deserialize)] +struct ActiveUserCountParams { + #[serde(flatten)] + period: TimePeriodParams, + durations_in_minutes: String, +} + #[derive(Serialize)] struct ActiveUserSet { active_time_in_minutes: u64, @@ -306,18 +313,23 @@ struct ActiveUserSet { } async fn get_active_user_counts( - Query(params): Query, + Query(params): Query, Extension(app): Extension>, ) -> Result { - let durations_in_minutes = [10, 60, 4 * 60, 8 * 60]; - - let mut user_sets = Vec::with_capacity(durations_in_minutes.len()); + 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.start..params.end, Duration::from_secs(duration * 60)) + .get_active_user_count( + params.period.start..params.period.end, + Duration::from_secs(duration * 60), + ) .await?, }) }