Implement analytics marker events (#6090)

* Analytics events

* prepare

* change route prefix

* update route return

* Add mod launcher analytics

* more UA strings

* fix ci

* caching on analytics events

* Return parent modpack versions for playtime queries

* sqlx prepare

* fmt

* dummy fixtures
This commit is contained in:
aecsocket
2026-05-19 13:06:04 +00:00
committed by GitHub
parent 48bb44155d
commit 244c263e40
19 changed files with 928 additions and 35 deletions
@@ -0,0 +1,140 @@
use chrono::{DateTime, Utc};
use futures::{StreamExt, TryStreamExt};
use sqlx::types::Json;
use crate::{
database::{
models::{DBAnalyticsEventId, DatabaseError},
redis::RedisPool,
},
models::v3::analytics_event::AnalyticsEventMeta,
};
use serde::{Deserialize, Serialize};
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events";
const ANALYTICS_EVENTS_ALL_KEY: &str = "all";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DBAnalyticsEvent {
pub id: DBAnalyticsEventId,
pub meta: AnalyticsEventMeta,
pub starts: DateTime<Utc>,
pub ends: DateTime<Utc>,
}
impl DBAnalyticsEvent {
pub async fn insert(
&self,
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO analytics_events (id, meta, starts, ends)
VALUES ($1, $2, $3, $4)
",
self.id as DBAnalyticsEventId,
sqlx::types::Json(&self.meta) as Json<&AnalyticsEventMeta>,
self.starts,
self.ends,
)
.execute(exec)
.await?;
Ok(())
}
pub async fn update(
&self,
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
) -> Result<bool, DatabaseError> {
let result = sqlx::query!(
"
UPDATE analytics_events
SET meta = $2, starts = $3, ends = $4
WHERE id = $1
",
self.id as DBAnalyticsEventId,
sqlx::types::Json(&self.meta) as Json<&AnalyticsEventMeta>,
self.starts,
self.ends,
)
.execute(exec)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn remove(
id: DBAnalyticsEventId,
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
) -> Result<bool, DatabaseError> {
let result = sqlx::query!(
"
DELETE FROM analytics_events
WHERE id = $1
",
id as DBAnalyticsEventId,
)
.execute(exec)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn get_all(
exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Vec<DBAnalyticsEvent>, DatabaseError> {
let mut redis = redis.connect().await?;
if let Some(events) = redis
.get_deserialized_from_json(
ANALYTICS_EVENTS_NAMESPACE,
ANALYTICS_EVENTS_ALL_KEY,
)
.await?
{
return Ok(events);
}
let events = sqlx::query!(
r#"
SELECT id, meta AS "meta: Json<AnalyticsEventMeta>", starts, ends
FROM analytics_events
ORDER BY starts DESC
"#
)
.fetch(exec)
.map(|record| {
let record = record?;
Ok::<_, DatabaseError>(DBAnalyticsEvent {
id: DBAnalyticsEventId(record.id),
meta: record.meta.0,
starts: record.starts,
ends: record.ends,
})
})
.try_collect::<Vec<_>>()
.await?;
redis
.set_serialized_to_json(
ANALYTICS_EVENTS_NAMESPACE,
ANALYTICS_EVENTS_ALL_KEY,
&events,
None,
)
.await?;
Ok(events)
}
pub async fn clear_cache(redis: &RedisPool) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete(ANALYTICS_EVENTS_NAMESPACE, ANALYTICS_EVENTS_ALL_KEY)
.await?;
Ok(())
}
}
+10 -6
View File
@@ -1,12 +1,12 @@
use super::DatabaseError;
use crate::database::PgTransaction;
use crate::models::ids::{
AffiliateCodeId, ChargeId, CollectionId, FileId, ImageId, NotificationId,
OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId,
OAuthRedirectUriId, OrganizationId, PatId, PayoutId, ProductId,
ProductPriceId, ProjectId, ReportId, SessionId, SharedInstanceId,
SharedInstanceVersionId, TeamId, TeamMemberId, ThreadId, ThreadMessageId,
UserSubscriptionId, VersionId,
AffiliateCodeId, AnalyticsEventId, ChargeId, CollectionId, FileId, ImageId,
NotificationId, OAuthAccessTokenId, OAuthClientAuthorizationId,
OAuthClientId, OAuthRedirectUriId, OrganizationId, PatId, PayoutId,
ProductId, ProductPriceId, ProjectId, ReportId, SessionId,
SharedInstanceId, SharedInstanceVersionId, TeamId, TeamMemberId, ThreadId,
ThreadMessageId, UserSubscriptionId, VersionId,
};
use ariadne::ids::base62_impl::to_base62;
use ariadne::ids::{UserId, random_base62_rng, random_base62_rng_range};
@@ -269,6 +269,10 @@ db_id_interface!(
AffiliateCodeId,
generator: generate_affiliate_code_id @ "affiliate_codes",
);
db_id_interface!(
AnalyticsEventId,
generator: generate_analytics_event_id @ "analytics_events",
);
id_type!(CategoryId as i32);
id_type!(GameId as i32);
+2
View File
@@ -1,6 +1,7 @@
use thiserror::Error;
pub mod affiliate_code_item;
pub mod analytics_event_item;
pub mod categories;
pub mod charge_item;
pub mod collection_item;
@@ -44,6 +45,7 @@ pub mod users_subscriptions_credits;
pub mod version_item;
pub use affiliate_code_item::DBAffiliateCode;
pub use analytics_event_item::DBAnalyticsEvent;
pub use collection_item::DBCollection;
pub use ids::*;
pub use image_item::DBImage;