Analytics backend V2 (#4408)

* start with analytics v2

* the big ass SQL query™

* downloads and views analytics working

* Implement analytics bucketing API

* allow filtering by monetization

* Use a new format for project metrics and bucketing

* revenue API works

* Add country data to analytics API

* Add checks for number of slices and time slice resolution

* work on docs

* wip: fix tests and add docs

* Fix tests

* Fix tests

* Uncomment crates

* feat: frontend CLAUDE.md (#4433)

* Slight tweaks to time slicing logic

* More tweaks

* Fix error messages

* Fix sqlx cache

---------

Co-authored-by: Calum H. <contact@cal.engineer>
This commit is contained in:
aecsocket
2025-10-07 23:01:10 +01:00
committed by GitHub
parent f32558cf97
commit 6919c8dea9
10 changed files with 1210 additions and 940 deletions

View File

@@ -7,13 +7,14 @@ use actix_web::{
};
use async_trait::async_trait;
use bytes::Bytes;
use chrono::{DateTime, Utc};
use labrinth::{
models::{organizations::Organization, projects::Project},
routes::v3::analytics_get::{
GetRequest, GetResponse, Metrics, ReturnMetrics, TimeRange,
},
search::SearchResults,
util::actix::AppendsMultipart,
};
use rust_decimal::Decimal;
use serde_json::json;
use crate::{
@@ -570,70 +571,42 @@ impl ApiV3 {
pub async fn get_analytics_revenue(
&self,
id_or_slugs: Vec<&str>,
ids_are_version_ids: bool,
start_date: Option<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
resolution_minutes: Option<u32>,
time_range: TimeRange,
pat: Option<&str>,
) -> ServiceResponse {
let pv_string = if ids_are_version_ids {
let version_string: String =
serde_json::to_string(&id_or_slugs).unwrap();
let version_string = urlencoding::encode(&version_string);
format!("version_ids={version_string}")
} else {
let projects_string: String =
serde_json::to_string(&id_or_slugs).unwrap();
let projects_string = urlencoding::encode(&projects_string);
format!("project_ids={projects_string}")
) -> GetResponse {
let req = GetRequest {
time_range,
return_metrics: ReturnMetrics {
project_revenue: Some(Metrics {
bucket_by: Vec::new(),
}),
..Default::default()
},
};
let mut extra_args = String::new();
if let Some(start_date) = start_date {
let start_date = start_date.to_rfc3339();
// let start_date = serde_json::to_string(&start_date).unwrap();
let start_date = urlencoding::encode(&start_date);
write!(&mut extra_args, "&start_date={start_date}").unwrap();
}
if let Some(end_date) = end_date {
let end_date = end_date.to_rfc3339();
// let end_date = serde_json::to_string(&end_date).unwrap();
let end_date = urlencoding::encode(&end_date);
write!(&mut extra_args, "&end_date={end_date}").unwrap();
}
if let Some(resolution_minutes) = resolution_minutes {
write!(&mut extra_args, "&resolution_minutes={resolution_minutes}")
.unwrap();
}
let req = test::TestRequest::get()
.uri(&format!("/v3/analytics/revenue?{pv_string}{extra_args}",))
let req = test::TestRequest::post()
.uri("/v3/analytics")
.set_json(req)
.append_pat(pat)
.to_request();
self.call(req).await
let resp = self.call(req).await;
assert_status!(&resp, StatusCode::OK);
test::read_body_json(resp).await
}
pub async fn get_analytics_revenue_deserialized(
pub async fn get_analytics_revenue_new(
&self,
id_or_slugs: Vec<&str>,
ids_are_version_ids: bool,
start_date: Option<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
resolution_minutes: Option<u32>,
request: GetRequest,
pat: Option<&str>,
) -> HashMap<String, HashMap<i64, Decimal>> {
let resp = self
.get_analytics_revenue(
id_or_slugs,
ids_are_version_ids,
start_date,
end_date,
resolution_minutes,
pat,
)
.await;
) -> GetResponse {
let req = test::TestRequest::post()
.uri("/v3/analytics")
.set_json(request)
.append_pat(pat)
.to_request();
let resp = self.call(req).await;
assert_status!(&resp, StatusCode::OK);
test::read_body_json(resp).await
}