Skip to content

Commit 2f31164

Browse files
authored
Expose new analytics data in backend route (#5982)
* Expose more analytics data in backend * Adjust fetch analytics body * fix * fix
1 parent e13a89d commit 2f31164

2 files changed

Lines changed: 129 additions & 55 deletions

File tree

apps/labrinth/src/models/v3/analytics.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,16 @@ pub struct Download {
3333

3434
/// Why a project was downloaded.
3535
#[derive(
36-
Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
36+
Debug,
37+
Display,
38+
Clone,
39+
Copy,
40+
PartialEq,
41+
Eq,
42+
Hash,
43+
Serialize,
44+
Deserialize,
45+
utoipa::ToSchema,
3746
)]
3847
#[serde(rename_all = "snake_case")]
3948
#[display(rename_all = "snake_case")]
@@ -47,6 +56,15 @@ pub enum DownloadReason {
4756
Modpack,
4857
}
4958

59+
impl std::str::FromStr for DownloadReason {
60+
type Err = ();
61+
62+
fn from_str(s: &str) -> Result<Self, Self::Err> {
63+
serde_json::from_value(serde_json::Value::String(s.to_string()))
64+
.map_err(|_| ())
65+
}
66+
}
67+
5068
#[derive(Debug, Row, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
5169
pub struct PageView {
5270
pub recorded: i64,

apps/labrinth/src/routes/v3/analytics_get.rs

Lines changed: 110 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use crate::{
3333
ids::{AffiliateCodeId, ProjectId, VersionId},
3434
pats::Scopes,
3535
teams::ProjectPermissions,
36+
v3::analytics::DownloadReason,
3637
},
3738
queue::session::AuthQueue,
3839
routes::ApiError,
@@ -168,10 +169,16 @@ pub enum ProjectDownloadsField {
168169
Domain,
169170
/// Modrinth site path which was visited, e.g. `/mod/foo`.
170171
SitePath,
171-
/// What country these views came from.
172+
/// What country these downloads came from.
172173
///
173174
/// To anonymize the data, the country may be reported as `XX`.
174175
Country,
176+
/// Download reason.
177+
Reason,
178+
/// Game version used for this download.
179+
GameVersion,
180+
/// Mod loader used for this download.
181+
Loader,
175182
}
176183

177184
/// Fields for [`ReturnMetrics::project_playtime`].
@@ -188,6 +195,10 @@ pub enum ProjectPlaytimeField {
188195
Loader,
189196
/// Game version which this project was played on.
190197
GameVersion,
198+
/// What country this playtime came from.
199+
///
200+
/// To anonymize the data, the country may be reported as `XX`.
201+
Country,
191202
}
192203

193204
/// Fields for [`ReturnMetrics::project_revenue`].
@@ -240,12 +251,13 @@ pub const MAX_TIME_SLICES: usize = 1024;
240251
// response
241252

242253
/// Response for a [`GetRequest`].
243-
///
244-
/// This is a list of N [`TimeSlice`]s, where each slice represents an equal
245-
/// time interval of metrics collection. The number of slices is determined
246-
/// by [`GetRequest::time_range`].
247254
#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)]
248-
pub struct FetchResponse(pub Vec<TimeSlice>);
255+
pub struct FetchResponse {
256+
/// List of N [`TimeSlice`]s, where each slice represents an equal
257+
/// time interval of metrics collection. The number of slices is determined
258+
/// by [`GetRequest::time_range`].
259+
pub metrics: Vec<TimeSlice>,
260+
}
249261

250262
/// Single time interval of metrics collection.
251263
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
@@ -320,6 +332,15 @@ pub struct ProjectDownloads {
320332
/// [`ProjectDownloadsField::Country`].
321333
#[serde(skip_serializing_if = "Option::is_none")]
322334
country: Option<String>,
335+
/// [`ProjectDownloadsField::Reason`].
336+
#[serde(skip_serializing_if = "Option::is_none")]
337+
reason: Option<DownloadReason>,
338+
/// [`ProjectDownloadsField::GameVersion`].
339+
#[serde(skip_serializing_if = "Option::is_none")]
340+
game_version: Option<String>,
341+
/// [`ProjectDownloadsField::Loader`].
342+
#[serde(skip_serializing_if = "Option::is_none")]
343+
loader: Option<String>,
323344
/// Total number of downloads for this bucket.
324345
downloads: u64,
325346
}
@@ -336,6 +357,9 @@ pub struct ProjectPlaytime {
336357
/// [`ProjectPlaytimeField::GameVersion`].
337358
#[serde(skip_serializing_if = "Option::is_none")]
338359
game_version: Option<String>,
360+
/// [`ProjectPlaytimeField::Country`].
361+
#[serde(skip_serializing_if = "Option::is_none")]
362+
country: Option<String>,
339363
/// Total number of seconds of playtime for this bucket.
340364
seconds: u64,
341365
}
@@ -450,6 +474,9 @@ mod query {
450474
pub site_path: String,
451475
pub version_id: DBVersionId,
452476
pub country: String,
477+
pub reason: String,
478+
pub game_version: String,
479+
pub loader: String,
453480
pub downloads: u64,
454481
}
455482

@@ -459,6 +486,9 @@ mod query {
459486
const USE_SITE_PATH: &str = "{use_site_path: Bool}";
460487
const USE_VERSION_ID: &str = "{use_version_id: Bool}";
461488
const USE_COUNTRY: &str = "{use_country: Bool}";
489+
const USE_REASON: &str = "{use_reason: Bool}";
490+
const USE_GAME_VERSION: &str = "{use_game_version: Bool}";
491+
const USE_LOADER: &str = "{use_loader: Bool}";
462492

463493
formatcp!(
464494
"SELECT
@@ -468,6 +498,9 @@ mod query {
468498
if({USE_SITE_PATH}, site_path, '') AS site_path,
469499
if({USE_VERSION_ID}, version_id, 0) AS version_id,
470500
if({USE_COUNTRY}, country, '') AS country,
501+
if({USE_REASON}, reason, '') AS reason,
502+
if({USE_GAME_VERSION}, game_version, '') AS game_version,
503+
if({USE_LOADER}, loader, '') AS loader,
471504
COUNT(*) AS downloads
472505
FROM downloads
473506
WHERE
@@ -476,7 +509,7 @@ mod query {
476509
-- not the possibly-zero one,
477510
-- by using `downloads.project_id` instead of `project_id`
478511
AND downloads.project_id IN {PROJECT_IDS}
479-
GROUP BY bucket, project_id, domain, site_path, version_id, country"
512+
GROUP BY bucket, project_id, domain, site_path, version_id, country, reason, game_version, loader"
480513
)
481514
};
482515

@@ -487,6 +520,7 @@ mod query {
487520
pub version_id: DBVersionId,
488521
pub loader: String,
489522
pub game_version: String,
523+
pub country: String,
490524
pub seconds: u64,
491525
}
492526

@@ -495,6 +529,7 @@ mod query {
495529
const USE_VERSION_ID: &str = "{use_version_id: Bool}";
496530
const USE_LOADER: &str = "{use_loader: Bool}";
497531
const USE_GAME_VERSION: &str = "{use_game_version: Bool}";
532+
const USE_COUNTRY: &str = "{use_country: Bool}";
498533

499534
formatcp!(
500535
"SELECT
@@ -503,6 +538,7 @@ mod query {
503538
if({USE_VERSION_ID}, version_id, 0) AS version_id,
504539
if({USE_LOADER}, loader, '') AS loader,
505540
if({USE_GAME_VERSION}, game_version, '') AS game_version,
541+
if({USE_COUNTRY}, country, '') AS country,
506542
SUM(seconds) AS seconds
507543
FROM playtime
508544
WHERE
@@ -511,7 +547,7 @@ mod query {
511547
-- not the possibly-zero one,
512548
-- by using `playtime.project_id` instead of `project_id`
513549
AND playtime.project_id IN {PROJECT_IDS}
514-
GROUP BY bucket, project_id, version_id, loader, game_version"
550+
GROUP BY bucket, project_id, version_id, loader, game_version, country"
515551
)
516552
};
517553

@@ -696,6 +732,9 @@ pub async fn fetch_analytics(
696732
("use_site_path", uses(F::SitePath)),
697733
("use_version_id", uses(F::VersionId)),
698734
("use_country", uses(F::Country)),
735+
("use_reason", uses(F::Reason)),
736+
("use_game_version", uses(F::GameVersion)),
737+
("use_loader", uses(F::Loader)),
699738
],
700739
|row| row.bucket,
701740
|row| {
@@ -711,6 +750,10 @@ pub async fn fetch_analytics(
711750
site_path: none_if_empty(row.site_path),
712751
version_id: none_if_zero_version_id(row.version_id),
713752
country,
753+
reason: none_if_empty(row.reason)
754+
.and_then(|s| s.parse().ok()),
755+
game_version: none_if_empty(row.game_version),
756+
loader: none_if_empty(row.loader),
714757
downloads: row.downloads,
715758
}),
716759
})
@@ -731,15 +774,22 @@ pub async fn fetch_analytics(
731774
("use_version_id", uses(F::VersionId)),
732775
("use_loader", uses(F::Loader)),
733776
("use_game_version", uses(F::GameVersion)),
777+
("use_country", uses(F::Country)),
734778
],
735779
|row| row.bucket,
736780
|row| {
781+
let country = if uses(F::Country) {
782+
Some(condense_country(row.country, row.seconds))
783+
} else {
784+
None
785+
};
737786
AnalyticsData::Project(ProjectAnalytics {
738787
source_project: row.project_id.into(),
739788
metrics: ProjectMetrics::Playtime(ProjectPlaytime {
740789
version_id: none_if_zero_version_id(row.version_id),
741790
loader: none_if_empty(row.loader),
742791
game_version: none_if_empty(row.game_version),
792+
country,
743793
seconds: row.seconds,
744794
}),
745795
})
@@ -937,7 +987,9 @@ pub async fn fetch_analytics(
937987
}
938988
}
939989

940-
Ok(web::Json(FetchResponse(time_slices)))
990+
Ok(web::Json(FetchResponse {
991+
metrics: time_slices,
992+
}))
941993
}
942994

943995
fn none_if_empty(s: String) -> Option<String> {
@@ -1108,55 +1160,59 @@ mod tests {
11081160
let test_project_2 = ProjectId(456);
11091161
let test_project_3 = ProjectId(789);
11101162

1111-
let src = FetchResponse(vec![
1112-
TimeSlice(vec![
1113-
AnalyticsData::Project(ProjectAnalytics {
1114-
source_project: test_project_1,
1115-
metrics: ProjectMetrics::Views(ProjectViews {
1116-
domain: Some("youtube.com".into()),
1117-
views: 100,
1118-
..Default::default()
1163+
let src = FetchResponse {
1164+
metrics: vec![
1165+
TimeSlice(vec![
1166+
AnalyticsData::Project(ProjectAnalytics {
1167+
source_project: test_project_1,
1168+
metrics: ProjectMetrics::Views(ProjectViews {
1169+
domain: Some("youtube.com".into()),
1170+
views: 100,
1171+
..Default::default()
1172+
}),
11191173
}),
1120-
}),
1121-
AnalyticsData::Project(ProjectAnalytics {
1122-
source_project: test_project_2,
1123-
metrics: ProjectMetrics::Downloads(ProjectDownloads {
1124-
domain: Some("discord.com".into()),
1125-
downloads: 150,
1126-
..Default::default()
1174+
AnalyticsData::Project(ProjectAnalytics {
1175+
source_project: test_project_2,
1176+
metrics: ProjectMetrics::Downloads(ProjectDownloads {
1177+
domain: Some("discord.com".into()),
1178+
downloads: 150,
1179+
..Default::default()
1180+
}),
11271181
}),
1128-
}),
1129-
]),
1130-
TimeSlice(vec![AnalyticsData::Project(ProjectAnalytics {
1131-
source_project: test_project_3,
1132-
metrics: ProjectMetrics::Revenue(ProjectRevenue {
1133-
revenue: Decimal::new(20000, 2),
1134-
}),
1135-
})]),
1136-
]);
1137-
let target = json!([
1138-
[
1139-
{
1140-
"source_project": test_project_1.to_string(),
1141-
"metric_kind": "views",
1142-
"domain": "youtube.com",
1143-
"views": 100,
1144-
},
1145-
{
1146-
"source_project": test_project_2.to_string(),
1147-
"metric_kind": "downloads",
1148-
"domain": "discord.com",
1149-
"downloads": 150,
1150-
}
1182+
]),
1183+
TimeSlice(vec![AnalyticsData::Project(ProjectAnalytics {
1184+
source_project: test_project_3,
1185+
metrics: ProjectMetrics::Revenue(ProjectRevenue {
1186+
revenue: Decimal::new(20000, 2),
1187+
}),
1188+
})]),
11511189
],
1152-
[
1153-
{
1154-
"source_project": test_project_3.to_string(),
1155-
"metric_kind": "revenue",
1156-
"revenue": "200.00",
1157-
}
1190+
};
1191+
let target = json!({
1192+
"metrics": [
1193+
[
1194+
{
1195+
"source_project": test_project_1.to_string(),
1196+
"metric_kind": "views",
1197+
"domain": "youtube.com",
1198+
"views": 100,
1199+
},
1200+
{
1201+
"source_project": test_project_2.to_string(),
1202+
"metric_kind": "downloads",
1203+
"domain": "discord.com",
1204+
"downloads": 150,
1205+
}
1206+
],
1207+
[
1208+
{
1209+
"source_project": test_project_3.to_string(),
1210+
"metric_kind": "revenue",
1211+
"revenue": "200.00",
1212+
}
1213+
]
11581214
]
1159-
]);
1215+
});
11601216

11611217
assert_eq!(serde_json::to_value(src).unwrap(), target);
11621218
}

0 commit comments

Comments
 (0)