[Stats API] Add flat table to stats API (#6073)

* add flat table to stats API

* remove day column
This commit is contained in:
Simon Wicky
2025-09-30 14:30:05 +02:00
committed by GitHub
parent a0e37e78e2
commit a7ec178c9f
7 changed files with 151 additions and 6 deletions
Generated
+1 -1
View File
@@ -7036,7 +7036,7 @@ dependencies = [
[[package]]
name = "nym-statistics-api"
version = "0.1.5"
version = "0.2.1"
dependencies = [
"anyhow",
"axum",
@@ -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"
}
+1 -1
View File
@@ -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
@@ -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);
+18 -3
View File
@@ -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<AppState> {
@@ -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)?;
+40 -1
View File
@@ -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<ConnectionInfoDto>,
@@ -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(())
}
}
+49
View File
@@ -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<String>,
pub(crate) os_arch: String,
pub(crate) app_version: String,
pub(crate) user_agent: String,
pub(crate) connection_time_ms: Option<i32>,
pub(crate) two_hop: Option<bool>,
pub(crate) country_code: Option<String>,
}
impl StatsReportV1Dto {
pub(crate) fn new(
received_at: OffsetDateTime,
stats_report: &VpnClientStatsReport,
user_agent: UserAgent,
from_mixnet: bool,
received_from: IpAddr,
maybe_country: Option<Country>,
) -> 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
}
}