Fix new analytics backend bucketing and revenue (#6052)

* Fix analytics backend QA items

* cargo prepare
This commit is contained in:
aecsocket
2026-05-10 11:57:24 +01:00
committed by GitHub
parent 45398c546c
commit a5417e0851
2 changed files with 28 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT\n WIDTH_BUCKET(\n EXTRACT(EPOCH FROM created)::bigint,\n EXTRACT(EPOCH FROM $1::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n EXTRACT(EPOCH FROM $2::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n $3::integer\n ) AS bucket,\n CASE WHEN $5 THEN mod_id ELSE 0 END AS mod_id,\n SUM(amount) amount_sum\n FROM payouts_values\n WHERE\n user_id = $4\n -- only project revenue is counted here\n -- for affiliate code revenue, see `affiliate_code_revenue``\n AND payouts_values.mod_id IS NOT NULL\n AND created BETWEEN $1 AND $2\n GROUP BY bucket, mod_id", "query": "SELECT\n WIDTH_BUCKET(\n EXTRACT(EPOCH FROM created)::bigint,\n EXTRACT(EPOCH FROM $1::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n EXTRACT(EPOCH FROM $2::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n $3::integer\n ) AS bucket,\n mod_id,\n SUM(amount) amount_sum\n FROM payouts_values\n WHERE\n -- only project revenue is counted here\n -- for affiliate code revenue, see `affiliate_code_revenue`\n payouts_values.mod_id IS NOT NULL\n AND payouts_values.mod_id = ANY($4)\n AND created BETWEEN $1 AND $2\n GROUP BY bucket, mod_id",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -24,15 +24,14 @@
"Timestamptz", "Timestamptz",
"Timestamptz", "Timestamptz",
"Int4", "Int4",
"Int8", "Int8Array"
"Bool"
] ]
}, },
"nullable": [ "nullable": [
null, null,
null, true,
null null
] ]
}, },
"hash": "b617ed1011341416c1c012c00e716a59873a8204e1b122c7c517a1c4437edfb4" "hash": "8d38218e5a0c9297be7c6c77acf40a2339b12ff15f1f9e53a27a1c599a33e43b"
} }

View File

@@ -169,6 +169,8 @@ pub enum ProjectDownloadsField {
Domain, Domain,
/// Modrinth site path which was visited, e.g. `/mod/foo`. /// Modrinth site path which was visited, e.g. `/mod/foo`.
SitePath, SitePath,
/// Whether these downloads were monetized or not.
Monetized,
/// What country these downloads came from. /// What country these downloads came from.
/// ///
/// To anonymize the data, the country may be reported as `XX`. /// To anonymize the data, the country may be reported as `XX`.
@@ -329,6 +331,9 @@ pub struct ProjectDownloads {
/// [`ProjectDownloadsField::VersionId`]. /// [`ProjectDownloadsField::VersionId`].
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
version_id: Option<VersionId>, version_id: Option<VersionId>,
/// [`ProjectDownloadsField::Monetized`].
#[serde(skip_serializing_if = "Option::is_none")]
monetized: Option<bool>,
/// [`ProjectDownloadsField::Country`]. /// [`ProjectDownloadsField::Country`].
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
country: Option<String>, country: Option<String>,
@@ -473,6 +478,7 @@ mod query {
pub domain: String, pub domain: String,
pub site_path: String, pub site_path: String,
pub version_id: DBVersionId, pub version_id: DBVersionId,
pub monetized: i8,
pub country: String, pub country: String,
pub reason: String, pub reason: String,
pub game_version: String, pub game_version: String,
@@ -485,6 +491,7 @@ mod query {
const USE_DOMAIN: &str = "{use_domain: Bool}"; const USE_DOMAIN: &str = "{use_domain: Bool}";
const USE_SITE_PATH: &str = "{use_site_path: Bool}"; const USE_SITE_PATH: &str = "{use_site_path: Bool}";
const USE_VERSION_ID: &str = "{use_version_id: Bool}"; const USE_VERSION_ID: &str = "{use_version_id: Bool}";
const USE_MONETIZED: &str = "{use_monetized: Bool}";
const USE_COUNTRY: &str = "{use_country: Bool}"; const USE_COUNTRY: &str = "{use_country: Bool}";
const USE_REASON: &str = "{use_reason: Bool}"; const USE_REASON: &str = "{use_reason: Bool}";
const USE_GAME_VERSION: &str = "{use_game_version: Bool}"; const USE_GAME_VERSION: &str = "{use_game_version: Bool}";
@@ -497,6 +504,7 @@ mod query {
if({USE_DOMAIN}, domain, '') AS domain, if({USE_DOMAIN}, domain, '') AS domain,
if({USE_SITE_PATH}, site_path, '') AS site_path, if({USE_SITE_PATH}, site_path, '') AS site_path,
if({USE_VERSION_ID}, version_id, 0) AS version_id, if({USE_VERSION_ID}, version_id, 0) AS version_id,
if({USE_MONETIZED}, CAST(user_id != 0 AS Int8), -1) AS monetized,
if({USE_COUNTRY}, country, '') AS country, if({USE_COUNTRY}, country, '') AS country,
if({USE_REASON}, reason, '') AS reason, if({USE_REASON}, reason, '') AS reason,
if({USE_GAME_VERSION}, game_version, '') AS game_version, if({USE_GAME_VERSION}, game_version, '') AS game_version,
@@ -509,7 +517,7 @@ mod query {
-- not the possibly-zero one, -- not the possibly-zero one,
-- by using `downloads.project_id` instead of `project_id` -- by using `downloads.project_id` instead of `project_id`
AND downloads.project_id IN {PROJECT_IDS} AND downloads.project_id IN {PROJECT_IDS}
GROUP BY bucket, project_id, domain, site_path, version_id, country, reason, game_version, loader" GROUP BY bucket, project_id, domain, site_path, version_id, monetized, country, reason, game_version, loader"
) )
}; };
@@ -731,6 +739,7 @@ pub async fn fetch_analytics(
("use_domain", uses(F::Domain)), ("use_domain", uses(F::Domain)),
("use_site_path", uses(F::SitePath)), ("use_site_path", uses(F::SitePath)),
("use_version_id", uses(F::VersionId)), ("use_version_id", uses(F::VersionId)),
("use_monetized", uses(F::Monetized)),
("use_country", uses(F::Country)), ("use_country", uses(F::Country)),
("use_reason", uses(F::Reason)), ("use_reason", uses(F::Reason)),
("use_game_version", uses(F::GameVersion)), ("use_game_version", uses(F::GameVersion)),
@@ -749,6 +758,11 @@ pub async fn fetch_analytics(
domain: none_if_empty(row.domain), domain: none_if_empty(row.domain),
site_path: none_if_empty(row.site_path), site_path: none_if_empty(row.site_path),
version_id: none_if_zero_version_id(row.version_id), version_id: none_if_zero_version_id(row.version_id),
monetized: match row.monetized {
0 => Some(false),
1 => Some(true),
_ => None,
},
country, country,
reason: none_if_empty(row.reason) reason: none_if_empty(row.reason)
.and_then(|s| s.parse().ok()), .and_then(|s| s.parse().ok()),
@@ -821,11 +835,14 @@ pub async fn fetch_analytics(
.await?; .await?;
} }
if let Some(metrics) = &req.return_metrics.project_revenue { if req.return_metrics.project_revenue.is_some() {
if !scopes.contains(Scopes::PAYOUTS_READ) { if !scopes.contains(Scopes::PAYOUTS_READ) {
return Err(AuthenticationError::InvalidCredentials.into()); return Err(AuthenticationError::InvalidCredentials.into());
} }
let project_id_values =
project_ids.iter().map(|id| id.0).collect::<Vec<_>>();
let mut rows = sqlx::query!( let mut rows = sqlx::query!(
"SELECT "SELECT
WIDTH_BUCKET( WIDTH_BUCKET(
@@ -834,21 +851,20 @@ pub async fn fetch_analytics(
EXTRACT(EPOCH FROM $2::timestamp with time zone AT TIME ZONE 'UTC')::bigint, EXTRACT(EPOCH FROM $2::timestamp with time zone AT TIME ZONE 'UTC')::bigint,
$3::integer $3::integer
) AS bucket, ) AS bucket,
CASE WHEN $5 THEN mod_id ELSE 0 END AS mod_id, mod_id,
SUM(amount) amount_sum SUM(amount) amount_sum
FROM payouts_values FROM payouts_values
WHERE WHERE
user_id = $4
-- only project revenue is counted here -- only project revenue is counted here
-- for affiliate code revenue, see `affiliate_code_revenue`` -- for affiliate code revenue, see `affiliate_code_revenue`
AND payouts_values.mod_id IS NOT NULL payouts_values.mod_id IS NOT NULL
AND payouts_values.mod_id = ANY($4)
AND created BETWEEN $1 AND $2 AND created BETWEEN $1 AND $2
GROUP BY bucket, mod_id", GROUP BY bucket, mod_id",
req.time_range.start, req.time_range.start,
req.time_range.end, req.time_range.end,
num_time_slices as i64, num_time_slices as i64,
DBUserId::from(user.id) as DBUserId, &project_id_values,
metrics.bucket_by.contains(&ProjectRevenueField::ProjectId),
) )
.fetch(&**pool); .fetch(&**pool);
while let Some(row) = rows.next().await.transpose()? { while let Some(row) = rows.next().await.transpose()? {