[Stats API] Active device endpoint and exit country code (#6265)

* active_device endpoint and exit_cc in report

* bump stats API version

* stats API version in lockflie

* migration changes
This commit is contained in:
Simon Wicky
2025-12-04 11:00:51 +01:00
committed by GitHub
parent 46268edf9c
commit 50bc3babb7
8 changed files with 136 additions and 7 deletions
Generated
+1 -1
View File
@@ -7248,7 +7248,7 @@ dependencies = [
[[package]]
name = "nym-statistics-api"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"anyhow",
"axum",
@@ -6,6 +6,7 @@ use time::Date;
const BASIC_REPORT_KIND: &str = "vpn_client_stats_report";
const SESSION_REPORT_KIND: &str = "vpn_client_session_report";
const ACTIVE_DEVICE_REPORT_KIND: &str = "vpn_client_active_device";
const VERSION_1: &str = "v1";
const VERSION_2: &str = "v2";
@@ -64,6 +65,27 @@ impl VpnClientStatsReportV2 {
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveDeviceReport {
pub kind: String,
pub api_version: String,
pub stats_id: String,
pub static_information: StaticInformationReport,
}
impl ActiveDeviceReport {
pub fn new(stats_id: String, static_information: StaticInformationReport) -> Self {
ActiveDeviceReport {
kind: ACTIVE_DEVICE_REPORT_KIND.into(),
api_version: VERSION_2.into(),
stats_id,
static_information,
}
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticInformationReport {
@@ -90,6 +112,7 @@ pub struct SessionReport {
pub session_duration_min: i32,
pub disconnection_time_ms: i32,
pub exit_id: String,
pub exit_cc: Option<String>,
pub follow_up_id: Option<String>,
pub error: Option<String>,
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO report_v2(\n received_at,\n source_ip,\n from_mixnet,\n country_code,\n report_version,\n device_id,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n start_day_utc,\n connection_time_ms,\n tunnel_type,\n retry_attempt,\n session_duration_min,\n disconnection_time_ms,\n exit_id,\n follow_up_id,\n error)\n VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)",
"query": "INSERT INTO report_v2(\n received_at,\n source_ip,\n from_mixnet,\n country_code,\n report_version,\n device_id,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n start_day_utc,\n connection_time_ms,\n tunnel_type,\n retry_attempt,\n session_duration_min,\n disconnection_time_ms,\n exit_id,\n exit_cc,\n follow_up_id,\n error)\n VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)",
"describe": {
"columns": [],
"parameters": {
@@ -24,10 +24,11 @@
"Int4",
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518"
"hash": "5a6025e1b0d55dbfae098fdaa564e5a59642cec59947fabcb6f514d24423553c"
}
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-statistics-api"
version = "0.3.0"
version = "0.3.1"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -0,0 +1,31 @@
-- IMPORTANT : At the time of writing this, there are no instances of the Stats API with data in that table. Dropping it to modify is therefore fine
DROP TABLE report_v2;
CREATE TABLE report_v2 (
-- some info about the report, inferred from when/from where we got it
received_at TIMESTAMP WITH TIME ZONE NOT NULL,
source_ip TEXT,
from_mixnet BOOLEAN,
country_code TEXT,
report_version TEXT,
-- some infos about the device sending the report
device_id TEXT NOT NULL,
os_type TEXT,
os_version TEXT,
architecture TEXT,
app_version TEXT,
user_agent TEXT,
-- session info
start_day_utc DATE,
connection_time_ms INTEGER,
tunnel_type TEXT,
retry_attempt INTEGER,
session_duration_min INTEGER,
disconnection_time_ms INTEGER,
exit_id TEXT,
exit_cc TEXT, -- new column
follow_up_id TEXT,
error TEXT
);
+51 -1
View File
@@ -1,7 +1,9 @@
use axum::{Json, Router, extract::State};
use axum_client_ip::InsecureClientIp;
use axum_extra::{TypedHeader, headers::UserAgent};
use nym_statistics_common::report::vpn_client::{VpnClientStatsReport, VpnClientStatsReportV2};
use nym_statistics_common::report::vpn_client::{
ActiveDeviceReport, StaticInformationReport, VpnClientStatsReport, VpnClientStatsReportV2,
};
use tracing::debug;
use crate::{
@@ -15,6 +17,7 @@ use crate::{
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/report", axum::routing::post(submit_stats_report))
.route("/active_device", axum::routing::post(submit_active_device))
.route("/session", axum::routing::post(submit_session_report))
}
@@ -76,6 +79,53 @@ async fn submit_stats_report(
Ok(Json(()))
}
#[utoipa::path(
post,
request_body = StaticInformationReport,
tag = "Stats",
path = "/active_device",
context_path = "/v1/stats",
responses(
(status = 200)
)
)]
#[tracing::instrument(level = "info", skip_all)]
async fn submit_active_device(
State(mut state): State<AppState>,
TypedHeader(user_agent): TypedHeader<UserAgent>,
insecure_ip_addr: InsecureClientIp,
Json(report): Json<ActiveDeviceReport>,
) -> HttpResult<Json<()>> {
let now = time::OffsetDateTime::now_utc();
let gateway_record = state
.network_view()
.get_country_by_ip(&insecure_ip_addr.0)
.await;
let from_mixnet = gateway_record.is_some();
if from_mixnet {
debug!("Received an active device ping from the network");
} else {
debug!("Received an active device ping from outside of the network");
}
let active_device = DailyActiveDeviceDto::from_active_device_report(
now,
&report,
user_agent.clone(),
from_mixnet,
);
state
.storage()
.store_active_device(active_device)
.await
.map_err(HttpError::internal_with_logging)?;
Ok(Json(()))
}
#[utoipa::path(
post,
request_body = VpnClientStatsReportV2,
+3 -1
View File
@@ -152,9 +152,10 @@ impl StatisticsStorage {
session_duration_min,
disconnection_time_ms,
exit_id,
exit_cc,
follow_up_id,
error)
VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)"#,
VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)"#,
report_v2.received_at as time::OffsetDateTime,
report_v2.received_from,
report_v2.from_mixnet,
@@ -173,6 +174,7 @@ impl StatisticsStorage {
report_v2.session_duration_min,
report_v2.disconnection_time_ms,
report_v2.exit_id,
report_v2.exit_cc,
report_v2.follow_up_id,
report_v2.error
)
+23 -1
View File
@@ -2,7 +2,9 @@ use std::net::IpAddr;
use axum_extra::headers::UserAgent;
use celes::Country;
use nym_statistics_common::report::vpn_client::{VpnClientStatsReport, VpnClientStatsReportV2};
use nym_statistics_common::report::vpn_client::{
ActiveDeviceReport, VpnClientStatsReport, VpnClientStatsReportV2,
};
use time::{Date, OffsetDateTime};
pub type StatsId = String;
@@ -54,6 +56,24 @@ impl DailyActiveDeviceDto {
from_mixnet,
}
}
pub(crate) fn from_active_device_report(
received_at: OffsetDateTime,
report: &ActiveDeviceReport,
user_agent: UserAgent,
from_mixnet: bool,
) -> Self {
Self {
day: received_at.date(),
stats_id: report.stats_id.clone(),
os_type: report.static_information.os_type.clone(),
os_version: report.static_information.os_version.clone(),
os_arch: report.static_information.os_arch.clone(),
app_version: report.static_information.app_version.clone(),
user_agent: user_agent.to_string(),
from_mixnet,
}
}
}
#[derive(Debug, Clone, sqlx::FromRow)]
@@ -129,6 +149,7 @@ pub(crate) struct StatsReportV2Dto {
pub(crate) session_duration_min: i32,
pub(crate) disconnection_time_ms: i32,
pub(crate) exit_id: String,
pub(crate) exit_cc: Option<String>,
pub(crate) follow_up_id: Option<String>,
pub(crate) error: Option<String>,
}
@@ -161,6 +182,7 @@ impl StatsReportV2Dto {
session_duration_min: stats_report.session_report.session_duration_min,
disconnection_time_ms: stats_report.session_report.disconnection_time_ms,
exit_id: stats_report.session_report.exit_id.clone(),
exit_cc: stats_report.session_report.exit_cc.clone(),
follow_up_id: stats_report.session_report.follow_up_id.clone(),
error: stats_report.session_report.error.clone(),
}