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}