#
mcaptcha/api/v1/
stats.rs

    1// Copyright (C) 2021  Aravinth Manivannan <realaravinth@batsense.net>
    2// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
    3//
    4// SPDX-License-Identifier: AGPL-3.0-or-later
    5use actix_web::{web, HttpResponse, Responder};
    6use derive_builder::Builder;
    7use serde::{Deserialize, Serialize};
    8
    9use crate::errors::*;
    10use crate::AppData;
    11
    12#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
    13pub struct BuildDetails {
    14    pub version: &'static str,
    15    pub git_commit_hash: &'static str,
    16}
    17
    18pub mod routes {
    19    use serde::{Deserialize, Serialize};
    20
    21    #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
    22    pub struct Stats {
    23        pub percentile_benches: &'static str,
    24    }
    25
    26    impl Stats {
    27        pub const fn new() -> Self {
    28            Self {
    29                percentile_benches: "/api/v1/stats/analytics/percentile",
    30            }
    31        }
    32    }
    33}
    34
    35pub async fn percentile_bench_runner(
    36    data: &AppData,
    37    req: &PercentileReq,
    38) -> ServiceResult<PercentileResp> {
    39    let count = data.db.stats_get_num_logs_under_time(req.time).await?;
    40
    41    if count == 0 {
    42        return Ok(PercentileResp {
    43            difficulty_factor: None,
    44        });
    45    }
    46
    47    if count < 2 {
    48        return Ok(PercentileResp {
    49            difficulty_factor: None,
    50        });
    51    }
    52
    53    let location = ((count - 1) as f64 * (req.percentile / 100.00)) + 1.00;
    54    let fraction = location - location.floor();
    55
    56    if fraction > 0.00 {
    57        if let (Some(base), Some(ceiling)) = (
    58            data.db
    59                .stats_get_entry_at_location_for_time_limit_asc(
    60                    req.time,
    61                    location.floor() as u32,
    62                )
    63                .await?,
    64            data.db
    65                .stats_get_entry_at_location_for_time_limit_asc(
    66                    req.time,
    67                    location.floor() as u32 + 1,
    68                )
    69                .await?,
    70        ) {
    71            let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32;
    72
    73            return Ok(PercentileResp {
    74                difficulty_factor: Some(res),
    75            });
    76        }
    77    } else {
    78        if let Some(base) = data
    79            .db
    80            .stats_get_entry_at_location_for_time_limit_asc(
    81                req.time,
    82                location.floor() as u32,
    83            )
    84            .await?
    85        {
    86            let res = base as u32;
    87
    88            return Ok(PercentileResp {
    89                difficulty_factor: Some(res),
    90            });
    91        }
    92    };
    93    Ok(PercentileResp {
    94        difficulty_factor: None,
    95    })
    96}
    97
    98/// Get difficulty factor with max time limit for percentile of stats
    99#[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")]
    100async fn percentile_benches(
    101    data: AppData,
    102    payload: web::Json<PercentileReq>,
    103) -> ServiceResult<impl Responder> {
    104    Ok(HttpResponse::Ok().json(percentile_bench_runner(&data, &payload).await?))
    105}
    106
    107#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
    108/// Health check return datatype
    109pub struct PercentileReq {
    110    pub time: u32,
    111    pub percentile: f64,
    112}
    113
    114#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
    115/// Health check return datatype
    116pub struct PercentileResp {
    117    pub difficulty_factor: Option<u32>,
    118}
    119
    120pub fn services(cfg: &mut web::ServiceConfig) {
    121    cfg.service(percentile_benches);
    122}
    123
    124#[cfg(test)]
    125mod tests {
    126    use actix_web::{http::StatusCode, test, App};
    127
    128    use super::*;
    129    use crate::api::v1::services;
    130    use crate::*;
    131
    132    #[actix_rt::test]
    133    async fn stats_bench_work_pg() {
    134        let data = crate::tests::pg::get_data().await;
    135        stats_bench_work(data).await;
    136    }
    137
    138    #[actix_rt::test]
    139    async fn stats_bench_work_maria() {
    140        let data = crate::tests::maria::get_data().await;
    141        stats_bench_work(data).await;
    142    }
    143
    144    async fn stats_bench_work(data: ArcData) {
    145        use crate::tests::*;
    146
    147        const NAME: &str = "benchstatsuesr";
    148        const EMAIL: &str = "benchstatsuesr@testadminuser.com";
    149        const PASSWORD: &str = "longpassword2";
    150
    151        const DEVICE_USER_PROVIDED: &str = "foo";
    152        const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
    153        const THREADS: i32 = 4;
    154
    155        let data = &data;
    156        {
    157            delete_user(&data, NAME).await;
    158        }
    159
    160        register_and_signin(data, NAME, EMAIL, PASSWORD).await;
    161        // create captcha
    162        let (_, _signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await;
    163        let app = get_app!(data).await;
    164
    165        let page = 1;
    166        let tmp_id = uuid::Uuid::new_v4();
    167        let download_rotue = V1_API_ROUTES
    168            .survey
    169            .get_download_route(&tmp_id.to_string(), page);
    170
    171        let download_req = test::call_service(
    172            &app,
    173            test::TestRequest::get().uri(&download_rotue).to_request(),
    174        )
    175        .await;
    176        assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
    177
    178        data.db
    179            .analytics_create_psuedo_id_if_not_exists(&key.key)
    180            .await
    181            .unwrap();
    182
    183        let psuedo_id = data
    184            .db
    185            .analytics_get_psuedo_id_from_capmaign_id(&key.key)
    186            .await
    187            .unwrap();
    188
    189        for i in 1..6 {
    190            println!("[{i}] Saving analytics");
    191            let analytics = db_core::CreatePerformanceAnalytics {
    192                time: i,
    193                difficulty_factor: i,
    194                worker_type: "wasm".into(),
    195            };
    196            data.db.analysis_save(&key.key, &analytics).await.unwrap();
    197        }
    198
    199        let msg = PercentileReq {
    200            time: 1,
    201            percentile: 99.00,
    202        };
    203        let resp = test::call_service(
    204            &app,
    205            post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
    206        )
    207        .await;
    208        assert_eq!(resp.status(), StatusCode::OK);
    209        let resp: PercentileResp = test::read_body_json(resp).await;
    210
    211        assert!(resp.difficulty_factor.is_none());
    212
    213        let msg = PercentileReq {
    214            time: 1,
    215            percentile: 100.00,
    216        };
    217
    218        let resp = test::call_service(
    219            &app,
    220            post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
    221        )
    222        .await;
    223        assert_eq!(resp.status(), StatusCode::OK);
    224        let resp: PercentileResp = test::read_body_json(resp).await;
    225
    226        assert!(resp.difficulty_factor.is_none());
    227
    228        let msg = PercentileReq {
    229            time: 2,
    230            percentile: 100.00,
    231        };
    232
    233        let resp = test::call_service(
    234            &app,
    235            post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
    236        )
    237        .await;
    238        assert_eq!(resp.status(), StatusCode::OK);
    239        let resp: PercentileResp = test::read_body_json(resp).await;
    240
    241        assert_eq!(resp.difficulty_factor.unwrap(), 2);
    242
    243        let msg = PercentileReq {
    244            time: 5,
    245            percentile: 90.00,
    246        };
    247
    248        let resp = test::call_service(
    249            &app,
    250            post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
    251        )
    252        .await;
    253        assert_eq!(resp.status(), StatusCode::OK);
    254        let resp: PercentileResp = test::read_body_json(resp).await;
    255
    256        assert_eq!(resp.difficulty_factor.unwrap(), 4);
    257        delete_user(&data, NAME).await;
    258    }
    259}