Skip to content

Commit 87a125e

Browse files
committed
Add country data to analytics API
1 parent 37955c4 commit 87a125e

File tree

1 file changed

+68
-20
lines changed

1 file changed

+68
-20
lines changed

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

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ struct GetRequest {
4949
struct TimeRange {
5050
start: DateTime<Utc>,
5151
end: DateTime<Utc>,
52-
resolution_minutes: NonZeroU64,
52+
resolution: TimeRangeResolution,
53+
}
54+
55+
#[derive(Debug, Serialize, Deserialize)]
56+
#[serde(rename_all = "snake_case")]
57+
pub enum TimeRangeResolution {
58+
Minutes(NonZeroU64),
59+
Slices(NonZeroU64),
5360
}
5461

5562
#[derive(Debug, Serialize, Deserialize)]
@@ -73,6 +80,7 @@ enum ProjectViewsField {
7380
Domain,
7481
SitePath,
7582
Monetized,
83+
Country,
7684
}
7785

7886
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -82,6 +90,7 @@ enum ProjectDownloadsField {
8290
VersionId,
8391
Domain,
8492
SitePath,
93+
Country,
8594
}
8695

8796
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -125,6 +134,8 @@ enum ProjectMetrics {
125134
site_path: Option<String>,
126135
#[serde(skip_serializing_if = "Option::is_none")]
127136
monetized: Option<bool>,
137+
#[serde(skip_serializing_if = "Option::is_none")]
138+
country: Option<String>,
128139
views: u64,
129140
},
130141
Downloads {
@@ -134,6 +145,8 @@ enum ProjectMetrics {
134145
site_path: Option<String>,
135146
#[serde(skip_serializing_if = "Option::is_none")]
136147
version_id: Option<VersionId>,
148+
#[serde(skip_serializing_if = "Option::is_none")]
149+
country: Option<String>,
137150
downloads: u64,
138151
},
139152
Playtime {
@@ -174,6 +187,7 @@ mod query {
174187
pub domain: String,
175188
pub site_path: String,
176189
pub monetized: i8,
190+
pub country: String,
177191
pub views: u64,
178192
}
179193

@@ -182,6 +196,7 @@ mod query {
182196
const USE_DOMAIN: &str = "{use_domain: Bool}";
183197
const USE_SITE_PATH: &str = "{use_site_path: Bool}";
184198
const USE_MONETIZED: &str = "{use_monetized: Bool}";
199+
const USE_COUNTRY: &str = "{use_country: Bool}";
185200

186201
formatcp!(
187202
"SELECT
@@ -190,6 +205,7 @@ mod query {
190205
if({USE_DOMAIN}, domain, '') AS domain,
191206
if({USE_SITE_PATH}, site_path, '') AS site_path,
192207
if({USE_MONETIZED}, CAST(monetized AS Int8), -1) AS monetized,
208+
if({USE_COUNTRY}, country, '') AS country,
193209
COUNT(*) AS views
194210
FROM views
195211
WHERE
@@ -199,7 +215,7 @@ mod query {
199215
-- by using `views.project_id` instead of `project_id`
200216
AND views.project_id IN {PROJECT_IDS}
201217
GROUP BY
202-
bucket, project_id, domain, site_path, monetized"
218+
bucket, project_id, domain, site_path, monetized, country"
203219
)
204220
};
205221

@@ -210,6 +226,7 @@ mod query {
210226
pub domain: String,
211227
pub site_path: String,
212228
pub version_id: DBVersionId,
229+
pub country: String,
213230
pub downloads: u64,
214231
}
215232

@@ -218,6 +235,7 @@ mod query {
218235
const USE_DOMAIN: &str = "{use_domain: Bool}";
219236
const USE_SITE_PATH: &str = "{use_site_path: Bool}";
220237
const USE_VERSION_ID: &str = "{use_version_id: Bool}";
238+
const USE_COUNTRY: &str = "{use_country: Bool}";
221239

222240
formatcp!(
223241
"SELECT
@@ -226,6 +244,7 @@ mod query {
226244
if({USE_DOMAIN}, domain, '') AS domain,
227245
if({USE_SITE_PATH}, site_path, '') AS site_path,
228246
if({USE_VERSION_ID}, version_id, 0) AS version_id,
247+
if({USE_COUNTRY}, country, '') AS country,
229248
COUNT(*) AS downloads
230249
FROM downloads
231250
WHERE
@@ -235,7 +254,7 @@ mod query {
235254
-- by using `downloads.project_id` instead of `project_id`
236255
AND downloads.project_id IN {PROJECT_IDS}
237256
GROUP BY
238-
bucket, project_id, domain, site_path, version_id"
257+
bucket, project_id, domain, site_path, version_id, country"
239258
)
240259
};
241260

@@ -285,7 +304,7 @@ async fn get(
285304
clickhouse: web::Data<clickhouse::Client>,
286305
) -> Result<web::Json<GetResponse>, ApiError> {
287306
const MIN_RESOLUTION_MINUTES: u64 = 60;
288-
const MAX_TIME_SLICES: usize = 0x10000;
307+
const MAX_TIME_SLICES: usize = 1024;
289308

290309
let (scopes, user) = get_user_from_headers(
291310
&http_req,
@@ -296,23 +315,31 @@ async fn get(
296315
)
297316
.await?;
298317

299-
let resolution_minutes = req.time_range.resolution_minutes.get();
300-
if resolution_minutes < MIN_RESOLUTION_MINUTES {
301-
return Err(ApiError::InvalidInput(format!(
302-
"resolution must be at least {} minutes",
303-
MIN_RESOLUTION_MINUTES
304-
)));
305-
}
318+
let num_time_slices = match req.time_range.resolution {
319+
TimeRangeResolution::Slices(slices) => slices.get(),
320+
TimeRangeResolution::Minutes(resolution_minutes) => {
321+
if resolution_minutes.get() < MIN_RESOLUTION_MINUTES {
322+
return Err(ApiError::InvalidInput(format!(
323+
"resolution must be at least {} minutes",
324+
MIN_RESOLUTION_MINUTES
325+
)));
326+
}
306327

307-
let range_minutes = u64::try_from(
308-
(req.time_range.end - req.time_range.start).num_minutes(),
309-
)
310-
.map_err(|_| {
311-
ApiError::InvalidInput("time range end must be after start".into())
312-
})?;
328+
let range_minutes = u64::try_from(
329+
(req.time_range.end - req.time_range.start).num_minutes(),
330+
)
331+
.map_err(|_| {
332+
ApiError::InvalidInput(
333+
"time range end must be after start".into(),
334+
)
335+
})?;
313336

314-
let num_time_slices = usize::try_from(range_minutes / resolution_minutes)
337+
range_minutes / resolution_minutes
338+
}
339+
};
340+
let num_time_slices = usize::try_from(num_time_slices)
315341
.expect("u64 should fit within a usize");
342+
316343
if num_time_slices > MAX_TIME_SLICES {
317344
return Err(ApiError::InvalidInput(format!(
318345
"resolution is too fine or range is too large - maximum of {MAX_TIME_SLICES} time slices"
@@ -346,9 +373,15 @@ async fn get(
346373
("use_domain", uses(F::Domain)),
347374
("use_site_path", uses(F::SitePath)),
348375
("use_monetized", uses(F::Monetized)),
376+
("use_country", uses(F::Country)),
349377
],
350378
|row| row.bucket,
351379
|row| {
380+
let country = if uses(F::Country) {
381+
Some(condense_country(row.country, row.views))
382+
} else {
383+
None
384+
};
352385
ProjectAnalytics {
353386
source_project: row.project_id.into(),
354387
metrics: ProjectMetrics::Views {
@@ -359,6 +392,7 @@ async fn get(
359392
1 => Some(true),
360393
_ => None,
361394
},
395+
country,
362396
views: row.views,
363397
},
364398
}
@@ -380,15 +414,22 @@ async fn get(
380414
("use_domain", uses(F::Domain)),
381415
("use_site_path", uses(F::SitePath)),
382416
("use_version_id", uses(F::VersionId)),
417+
("use_country", uses(F::Country)),
383418
],
384419
|row| row.bucket,
385420
|row| {
421+
let country = if uses(F::Country) {
422+
Some(condense_country(row.country, row.downloads))
423+
} else {
424+
None
425+
};
386426
ProjectAnalytics {
387427
source_project: row.project_id.into(),
388428
metrics: ProjectMetrics::Downloads {
389429
domain: none_if_empty(row.domain),
390430
site_path: none_if_empty(row.site_path),
391431
version_id: none_if_zero_version_id(row.version_id),
432+
country,
392433
downloads: row.downloads,
393434
},
394435
}
@@ -466,8 +507,6 @@ async fn get(
466507
)
467508
})?;
468509

469-
tracing::info!("bkt = {bucket}");
470-
471510
if let Some(source_project) =
472511
row.mod_id.map(DBProjectId).map(ProjectId::from)
473512
&& let Some(revenue) = row.amount_sum
@@ -496,6 +535,15 @@ fn none_if_zero_version_id(v: DBVersionId) -> Option<VersionId> {
496535
if v.0 == 0 { None } else { Some(v.into()) }
497536
}
498537

538+
fn condense_country(country: String, count: u64) -> String {
539+
// Every country under '50' (view or downloads) should be condensed into 'XX'
540+
if count < 50 {
541+
"XX".to_string()
542+
} else {
543+
country
544+
}
545+
}
546+
499547
struct QueryClickhouseContext<'a> {
500548
clickhouse: &'a clickhouse::Client,
501549
req: &'a GetRequest,

0 commit comments

Comments
 (0)