@@ -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
943995fn 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