From 8e03290fda7b5fef6bda79e3d55a680b7ba57f25 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Sun, 5 Nov 2023 01:19:13 +0530 Subject: [PATCH] feat: expose percentile scores for all analyis records through API endpoint --- src/api/v1/mod.rs | 2 + src/api/v1/routes.rs | 3 + src/api/v1/stats.rs | 252 +++++++++++++++++++++++++++++++++++++++++++ src/tests/mod.rs | 6 +- 4 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 src/api/v1/stats.rs diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index b9b33644..531fe13c 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -14,6 +14,7 @@ pub mod meta; pub mod notifications; pub mod pow; mod routes; +pub mod stats; pub mod survey; pub use routes::ROUTES; @@ -26,6 +27,7 @@ pub fn services(cfg: &mut ServiceConfig) { mcaptcha::services(cfg); notifications::services(cfg); survey::services(cfg); + stats::services(cfg); } #[derive(Deserialize)] diff --git a/src/api/v1/routes.rs b/src/api/v1/routes.rs index 6f5d98c6..d54244d5 100644 --- a/src/api/v1/routes.rs +++ b/src/api/v1/routes.rs @@ -11,6 +11,7 @@ use super::mcaptcha::routes::Captcha; use super::meta::routes::Meta; use super::notifications::routes::Notifications; use super::pow::routes::PoW; +use super::stats::routes::Stats; use super::survey::routes::Survey; pub const ROUTES: Routes = Routes::new(); @@ -23,6 +24,7 @@ pub struct Routes { pub pow: PoW, pub survey: Survey, pub notifications: Notifications, + pub stats: Stats, } impl Routes { @@ -35,6 +37,7 @@ impl Routes { pow: PoW::new(), notifications: Notifications::new(), survey: Survey::new(), + stats: Stats::new(), } } } diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs new file mode 100644 index 00000000..80fe069f --- /dev/null +++ b/src/api/v1/stats.rs @@ -0,0 +1,252 @@ +// Copyright (C) 2021 Aravinth Manivannan +// SPDX-FileCopyrightText: 2023 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use actix_web::{web, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::errors::*; +use crate::AppData; + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +pub struct BuildDetails { + pub version: &'static str, + pub git_commit_hash: &'static str, +} + +pub mod routes { + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] + pub struct Stats { + pub percentile_benches: &'static str, + } + + impl Stats { + pub const fn new() -> Self { + Self { + percentile_benches: "/api/v1/stats/analytics/percentile", + } + } + } +} + +/// Get difficulty factor with max time limit for percentile of stats +#[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")] +async fn percentile_benches( + data: AppData, + payload: web::Json, +) -> ServiceResult { + let count = data.db.stats_get_num_logs_under_time(payload.time).await?; + + if count == 0 { + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: None, + })); + } + + if count < 2 { + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: None, + })); + } + + let location = ((count - 1) as f64 * (payload.percentile / 100.00)) + 1.00; + let fraction = location - location.floor(); + + if fraction > 0.00 { + if let (Some(base), Some(ceiling)) = ( + data.db + .stats_get_entry_at_location_for_time_limit_asc( + payload.time, + location.floor() as u32, + ) + .await?, + data.db + .stats_get_entry_at_location_for_time_limit_asc( + payload.time, + location.floor() as u32 + 1, + ) + .await?, + ) { + let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32; + + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: Some(res), + })); + } + } else { + if let Some(base) = data + .db + .stats_get_entry_at_location_for_time_limit_asc( + payload.time, + location.floor() as u32, + ) + .await? + { + let res = base as u32; + + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: Some(res), + })); + } + }; + Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: None, + })) +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct PercentileReq { + time: u32, + percentile: f64, +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct PercentileResp { + difficulty_factor: Option, +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(percentile_benches); +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test, App}; + + use super::*; + use crate::api::v1::services; + use crate::*; + + #[actix_rt::test] + async fn stats_bench_work_pg() { + let data = crate::tests::pg::get_data().await; + stats_bench_work(data).await; + } + + #[actix_rt::test] + async fn stats_bench_work_maria() { + let data = crate::tests::maria::get_data().await; + stats_bench_work(data).await; + } + + async fn stats_bench_work(data: ArcData) { + use crate::tests::*; + + const NAME: &str = "benchstatsuesr"; + const EMAIL: &str = "benchstatsuesr@testadminuser.com"; + const PASSWORD: &str = "longpassword2"; + + const DEVICE_USER_PROVIDED: &str = "foo"; + const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2"; + const THREADS: i32 = 4; + + let data = &data; + { + delete_user(&data, NAME).await; + } + + register_and_signin(data, NAME, EMAIL, PASSWORD).await; + // create captcha + let (_, _signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; + let app = get_app!(data).await; + + let page = 1; + let tmp_id = uuid::Uuid::new_v4(); + let download_rotue = V1_API_ROUTES + .survey + .get_download_route(&tmp_id.to_string(), page); + + let download_req = test::call_service( + &app, + test::TestRequest::get().uri(&download_rotue).to_request(), + ) + .await; + assert_eq!(download_req.status(), StatusCode::NOT_FOUND); + + data.db + .analytics_create_psuedo_id_if_not_exists(&key.key) + .await + .unwrap(); + + let psuedo_id = data + .db + .analytics_get_psuedo_id_from_capmaign_id(&key.key) + .await + .unwrap(); + + for i in 1..6 { + println!("[{i}] Saving analytics"); + let analytics = db_core::CreatePerformanceAnalytics { + time: i, + difficulty_factor: i, + worker_type: "wasm".into(), + }; + data.db.analysis_save(&key.key, &analytics).await.unwrap(); + } + + let msg = PercentileReq { + time: 1, + percentile: 99.00, + }; + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert!(resp.difficulty_factor.is_none()); + + let msg = PercentileReq { + time: 1, + percentile: 100.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert!(resp.difficulty_factor.is_none()); + + let msg = PercentileReq { + time: 2, + percentile: 100.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert_eq!(resp.difficulty_factor.unwrap(), 2); + + let msg = PercentileReq { + time: 5, + percentile: 90.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert_eq!(resp.difficulty_factor.unwrap(), 4); + delete_user(&data, NAME).await; + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 927ab0d1..5bc18a25 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -31,10 +31,10 @@ pub mod pg { use sqlx::migrate::MigrateDatabase; + use crate::api::v1::mcaptcha::get_random; use crate::data::Data; use crate::settings::*; use crate::survey::SecretsStore; - use crate::api::v1::mcaptcha::get_random; use crate::ArcData; use super::get_settings; @@ -65,19 +65,17 @@ pub mod maria { use sqlx::migrate::MigrateDatabase; + use crate::api::v1::mcaptcha::get_random; use crate::data::Data; use crate::settings::*; use crate::survey::SecretsStore; use crate::ArcData; - use crate::api::v1::mcaptcha::get_random; use super::get_settings; pub async fn get_data() -> ArcData { let url = env::var("MARIA_DATABASE_URL").unwrap(); - - let mut parsed = url::Url::parse(&url).unwrap(); parsed.set_path(&get_random(16)); let url = parsed.to_string();