diff --git a/_sql/migrations/2025-09-24-055943_analytics_summaries/down.sql b/_sql/migrations/2025-09-24-055943_analytics_summaries/down.sql new file mode 100644 index 0000000000..198e43560d --- /dev/null +++ b/_sql/migrations/2025-09-24-055943_analytics_summaries/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +drop table analytics_totals; diff --git a/_sql/migrations/2025-09-24-055943_analytics_summaries/up.sql b/_sql/migrations/2025-09-24-055943_analytics_summaries/up.sql new file mode 100644 index 0000000000..88899eaf75 --- /dev/null +++ b/_sql/migrations/2025-09-24-055943_analytics_summaries/up.sql @@ -0,0 +1,37 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE analytics_active_users ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- If we host multiple sites with distinct categories, this will enable us to + -- segregate analytics. + app_namespace VARCHAR(32) NOT NULL, + + -- Who sent the event. + user_token VARCHAR(32) NOT NULL, + + -- IP address is updated on every write. + ip_address VARCHAR(40) NOT NULL, + + -- Incrementing count of events. + -- NB: This is kind of an "hours spent" measure, but will lose information if we ever + -- change the ping cadence (eg. 1 minute pings --> 5 minute pings). + ping_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + + -- ========== RECORD TIMESTAMPS ========== + + -- Written at create only. + first_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- Updated on every ping. + last_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- ========== INDICES ========== + PRIMARY KEY (id), + UNIQUE KEY (app_namespace, user_token), -- For now + INDEX idx_last_active_at (last_active_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_sql/migrations_squashed/analytics_totals.sql b/_sql/migrations_squashed/analytics_totals.sql new file mode 100644 index 0000000000..caaf8b05d0 --- /dev/null +++ b/_sql/migrations_squashed/analytics_totals.sql @@ -0,0 +1,29 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE analytics_totals ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- If we host multiple sites with distinct categories, this will enable us to + -- segregate analytics. + app_namespace VARCHAR(32) NOT NULL, + + -- Who sent the event. + user_token VARCHAR(32) NOT NULL, + + -- Incrementing count of events. + event_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + + -- ========== RECORD TIMESTAMPS ========== + + first_event_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_event_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- ========== INDICES ========== + PRIMARY KEY (id), + UNIQUE KEY (app_namespace, user_token), -- For now + INDEX idx_last_event_at (last_event_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/crates/schema/database/mysql_queries/src/queries/analytics_totals/mod.rs b/crates/schema/database/mysql_queries/src/queries/analytics_totals/mod.rs new file mode 100644 index 0000000000..3d621e6ec9 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/analytics_totals/mod.rs @@ -0,0 +1 @@ +mod upsert_analytics_totals; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/queries/analytics_totals/upsert_analytics_totals.rs b/crates/schema/database/mysql_queries/src/queries/analytics_totals/upsert_analytics_totals.rs new file mode 100644 index 0000000000..fa9296aff5 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/analytics_totals/upsert_analytics_totals.rs @@ -0,0 +1,92 @@ +use anyhow::anyhow; +use log::error; +use sqlx::{MySql, MySqlPool, QueryBuilder}; +use sqlx::mysql::MySqlArguments; +use sqlx::pool::PoolConnection; +use sqlx::query::Query; +use enums::common::payments_namespace::PaymentsNamespace; +use errors::AnyhowResult; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; +use tokens::tokens::browser_session_logs::BrowserSessionLogToken; +use tokens::tokens::users::UserToken; + +pub struct UpsertAnalyticsTotals { + pub user_token: UserToken, + pub ip_address: String, + // TODO: Swap PaymentsNamespace (artcraft,fakeyou) for an AnalyticsNamespace enum. + pub namespace: PaymentsNamespace, +} + +impl UpsertAnalyticsTotals { + + fn query(&self) -> Query { + sqlx::query!( + r#" +INSERT INTO analytics_totals +SET + app_namespace = ?, + user_token = ?, + ip_address = ?, + event_count = event_count + 1, + last_event_at = NOW() +ON DUPLICATE KEY UPDATE + ip_address = ?, + event_count = event_count + 1, + last_event_at = NOW() + "#, + &self.namespace.as_str(), + &self.user_token, + &self.ip_address, + self.namespace.as_str(), + &self.maybe_user_token, + self.action_was_taken, + self.should_ignore_retry, + ) + } +} + +pub async fn upsert_browser_session_log<'a>(args: UpsertAnalyticsTotalsArgs<'a>) -> AnyhowResult { + + let token = args.maybe_log_token + .map(|token| token.clone()) + .unwrap_or_else(|| BrowserSessionLogToken::generate()); + + let maybe_last_action = args.maybe_last_action + .map(|action| { + let mut action = action.trim().to_string(); + action.truncate(32); // NB: Field is 32 wide. + action + }); + + let query_result = sqlx::query!( + r#" +INSERT INTO browser_session_logs +SET + token = ?, + ip_address = ?, + maybe_user_token = ?, + maybe_anonymous_visitor_token = ?, + maybe_last_action = ? +ON DUPLICATE KEY UPDATE + maybe_last_action = ?, + update_count = update_count + 1, + maybe_last_updated_at = NOW() + "#, + token.as_str(), + args.ip_address, + args.maybe_user_token.map(|t| t.as_str()), + args.maybe_visitor_token.map(|t| t.as_str()), + maybe_last_action.as_deref(), + maybe_last_action.as_deref(), + ) + .execute(args.mysql_pool) + .await; + + match query_result { + Ok(_) => Ok(token), + Err(err) => { + error!("Error with query: {:?}", &err); + Err(anyhow!("query error: {:?}", &err)) + }, + } +} diff --git a/crates/schema/database/mysql_queries/src/queries/mod.rs b/crates/schema/database/mysql_queries/src/queries/mod.rs index 252bbdcd88..2fd536ccaf 100644 --- a/crates/schema/database/mysql_queries/src/queries/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/mod.rs @@ -1,3 +1,5 @@ + +pub mod analytics_totals; pub mod api_tokens; pub mod audit_logs; pub mod batch_generations; @@ -36,5 +38,5 @@ pub mod voice_clone_requests; pub mod voice_conversion; pub mod voice_designer; pub mod w2l; -pub mod wallets; -mod wallet_ledger_entries; +pub mod wallet_ledger_entries; +pub mod wallets; \ No newline at end of file