diff --git a/Cargo.lock b/Cargo.lock index d8211b94b2..12527ecdd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7036,7 +7036,7 @@ dependencies = [ [[package]] name = "nym-statistics-api" -version = "0.1.5" +version = "0.2.1" dependencies = [ "anyhow", "axum", diff --git a/nym-statistics-api/.sqlx/query-c0c71601e6fb9d8746fb2cd5df11ac8ad9163f4de90c528ac62da8c47d108fa5.json b/nym-statistics-api/.sqlx/query-c0c71601e6fb9d8746fb2cd5df11ac8ad9163f4de90c528ac62da8c47d108fa5.json new file mode 100644 index 0000000000..664f91b6c7 --- /dev/null +++ b/nym-statistics-api/.sqlx/query-c0c71601e6fb9d8746fb2cd5df11ac8ad9163f4de90c528ac62da8c47d108fa5.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO report_v1 (\n received_at,\n source_ip,\n device_id,\n from_mixnet,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n connection_time_ms,\n two_hop,\n country_code)\n VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Text", + "Text", + "Int4", + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "c0c71601e6fb9d8746fb2cd5df11ac8ad9163f4de90c528ac62da8c47d108fa5" +} diff --git a/nym-statistics-api/Cargo.toml b/nym-statistics-api/Cargo.toml index 5d070a1dcd..e9e51ac626 100644 --- a/nym-statistics-api/Cargo.toml +++ b/nym-statistics-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-statistics-api" -version = "0.1.5" +version = "0.2.1" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-statistics-api/migrations/002_unique_table.sql b/nym-statistics-api/migrations/002_unique_table.sql new file mode 100644 index 0000000000..e257eb97a2 --- /dev/null +++ b/nym-statistics-api/migrations/002_unique_table.sql @@ -0,0 +1,17 @@ + +CREATE TABLE report_v1 ( + received_at TIMESTAMP WITH TIME ZONE NOT NULL, + source_ip TEXT, + device_id TEXT NOT NULL, + from_mixnet BOOLEAN, + os_type TEXT, + os_version TEXT, + architecture TEXT, + app_version TEXT, + user_agent TEXT, + connection_time_ms INTEGER, + two_hop BOOLEAN, + country_code TEXT +); + +CREATE INDEX idx_report_v1_received_at ON report_v1 (received_at); \ No newline at end of file diff --git a/nym-statistics-api/src/http/api/stats.rs b/nym-statistics-api/src/http/api/stats.rs index 7e7a51ec14..3d079e0a30 100644 --- a/nym-statistics-api/src/http/api/stats.rs +++ b/nym-statistics-api/src/http/api/stats.rs @@ -9,7 +9,7 @@ use crate::{ error::{HttpError, HttpResult}, state::AppState, }, - storage::models::{ConnectionInfoDto, DailyActiveDeviceDto}, + storage::models::{ConnectionInfoDto, DailyActiveDeviceDto, StatsReportV1Dto}, }; pub(crate) fn routes() -> Router { @@ -49,7 +49,7 @@ async fn submit_stats_report( debug!("Received a report from outside of the network"); } - let active_device = DailyActiveDeviceDto::new(now, &report, user_agent, from_mixnet); + let active_device = DailyActiveDeviceDto::new(now, &report, user_agent.clone(), from_mixnet); let maybe_connection_info = ConnectionInfoDto::maybe_new( now, &report, @@ -58,9 +58,24 @@ async fn submit_stats_report( from_mixnet, ); + let stats_report = StatsReportV1Dto::new( + now, + &report, + user_agent, + from_mixnet, + insecure_ip_addr.0, + maybe_location, + ); + state .storage() - .store_vpn_client_report(active_device, maybe_connection_info) + .store_vpn_client_report(stats_report) + .await + .map_err(HttpError::internal_with_logging)?; + + state + .storage() + .store_legacy_vpn_client_report(active_device, maybe_connection_info) .await .map_err(HttpError::internal_with_logging)?; diff --git a/nym-statistics-api/src/storage/mod.rs b/nym-statistics-api/src/storage/mod.rs index 4ea9bad118..82b0624f51 100644 --- a/nym-statistics-api/src/storage/mod.rs +++ b/nym-statistics-api/src/storage/mod.rs @@ -7,6 +7,8 @@ use sqlx::{ }; use std::{path::PathBuf, str::FromStr}; +use crate::storage::models::StatsReportV1Dto; + pub(crate) mod models; pub(crate) type DbPool = sqlx::PgPool; @@ -60,7 +62,7 @@ impl StatisticsStorage { }) } - pub(crate) async fn store_vpn_client_report( + pub(crate) async fn store_legacy_vpn_client_report( &mut self, active_device: DailyActiveDeviceDto, connection_info: Option, @@ -119,4 +121,41 @@ impl StatisticsStorage { .await?; Ok(()) } + + pub(crate) async fn store_vpn_client_report( + &mut self, + report_v1: StatsReportV1Dto, + ) -> Result<()> { + sqlx::query!( + r#"INSERT INTO report_v1 ( + received_at, + source_ip, + device_id, + from_mixnet, + os_type, + os_version, + architecture, + app_version, + user_agent, + connection_time_ms, + two_hop, + country_code) + VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"#, + report_v1.received_at as time::OffsetDateTime, + report_v1.received_from, + report_v1.stats_id, + report_v1.from_mixnet, + report_v1.os_type, + report_v1.os_version, + report_v1.os_arch, + report_v1.app_version, + report_v1.user_agent, + report_v1.connection_time_ms, + report_v1.two_hop, + report_v1.country_code + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } } diff --git a/nym-statistics-api/src/storage/models.rs b/nym-statistics-api/src/storage/models.rs index 669bcf2a54..95112ae64a 100644 --- a/nym-statistics-api/src/storage/models.rs +++ b/nym-statistics-api/src/storage/models.rs @@ -67,3 +67,52 @@ impl ConnectionInfoDto { }) } } + +// New structure. The two above will be removed when it is confirmed to work +#[derive(Debug, Clone, sqlx::FromRow)] +pub(crate) struct StatsReportV1Dto { + pub(crate) received_at: OffsetDateTime, + pub(crate) received_from: String, + pub(crate) stats_id: StatsId, + pub(crate) from_mixnet: bool, + pub(crate) os_type: String, + pub(crate) os_version: Option, + pub(crate) os_arch: String, + pub(crate) app_version: String, + pub(crate) user_agent: String, + pub(crate) connection_time_ms: Option, + pub(crate) two_hop: Option, + pub(crate) country_code: Option, +} + +impl StatsReportV1Dto { + pub(crate) fn new( + received_at: OffsetDateTime, + stats_report: &VpnClientStatsReport, + user_agent: UserAgent, + from_mixnet: bool, + received_from: IpAddr, + maybe_country: Option, + ) -> Self { + let mut report = Self { + received_at, + received_from: received_from.to_string(), + stats_id: stats_report.stats_id.clone(), + from_mixnet, + os_type: stats_report.static_information.os_type.clone(), + os_version: stats_report.static_information.os_version.clone(), + os_arch: stats_report.static_information.os_arch.clone(), + app_version: stats_report.static_information.app_version.clone(), + user_agent: user_agent.to_string(), + connection_time_ms: None, + two_hop: None, + country_code: maybe_country.map(|c| c.alpha2.into()), + }; + if let Some(usage_report) = stats_report.basic_usage.as_ref() { + report.connection_time_ms = usage_report.connection_time_ms; + report.two_hop = Some(usage_report.two_hop); + } + + report + } +}