[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:
Generated
+1
-1
@@ -7248,7 +7248,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nym-statistics-api"
|
name = "nym-statistics-api"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use time::Date;
|
|||||||
|
|
||||||
const BASIC_REPORT_KIND: &str = "vpn_client_stats_report";
|
const BASIC_REPORT_KIND: &str = "vpn_client_stats_report";
|
||||||
const SESSION_REPORT_KIND: &str = "vpn_client_session_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_1: &str = "v1";
|
||||||
const VERSION_2: &str = "v2";
|
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))]
|
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StaticInformationReport {
|
pub struct StaticInformationReport {
|
||||||
@@ -90,6 +112,7 @@ pub struct SessionReport {
|
|||||||
pub session_duration_min: i32,
|
pub session_duration_min: i32,
|
||||||
pub disconnection_time_ms: i32,
|
pub disconnection_time_ms: i32,
|
||||||
pub exit_id: String,
|
pub exit_id: String,
|
||||||
|
pub exit_cc: Option<String>,
|
||||||
pub follow_up_id: Option<String>,
|
pub follow_up_id: Option<String>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"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": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -24,10 +24,11 @@
|
|||||||
"Int4",
|
"Int4",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
|
"Text",
|
||||||
"Text"
|
"Text"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518"
|
"hash": "5a6025e1b0d55dbfae098fdaa564e5a59642cec59947fabcb6f514d24423553c"
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "nym-statistics-api"
|
name = "nym-statistics-api"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.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
|
||||||
|
);
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
use axum::{Json, Router, extract::State};
|
use axum::{Json, Router, extract::State};
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use axum_extra::{TypedHeader, headers::UserAgent};
|
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 tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -15,6 +17,7 @@ use crate::{
|
|||||||
pub(crate) fn routes() -> Router<AppState> {
|
pub(crate) fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/report", axum::routing::post(submit_stats_report))
|
.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))
|
.route("/session", axum::routing::post(submit_session_report))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +79,53 @@ async fn submit_stats_report(
|
|||||||
Ok(Json(()))
|
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(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
request_body = VpnClientStatsReportV2,
|
request_body = VpnClientStatsReportV2,
|
||||||
|
|||||||
@@ -152,9 +152,10 @@ impl StatisticsStorage {
|
|||||||
session_duration_min,
|
session_duration_min,
|
||||||
disconnection_time_ms,
|
disconnection_time_ms,
|
||||||
exit_id,
|
exit_id,
|
||||||
|
exit_cc,
|
||||||
follow_up_id,
|
follow_up_id,
|
||||||
error)
|
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_at as time::OffsetDateTime,
|
||||||
report_v2.received_from,
|
report_v2.received_from,
|
||||||
report_v2.from_mixnet,
|
report_v2.from_mixnet,
|
||||||
@@ -173,6 +174,7 @@ impl StatisticsStorage {
|
|||||||
report_v2.session_duration_min,
|
report_v2.session_duration_min,
|
||||||
report_v2.disconnection_time_ms,
|
report_v2.disconnection_time_ms,
|
||||||
report_v2.exit_id,
|
report_v2.exit_id,
|
||||||
|
report_v2.exit_cc,
|
||||||
report_v2.follow_up_id,
|
report_v2.follow_up_id,
|
||||||
report_v2.error
|
report_v2.error
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ use std::net::IpAddr;
|
|||||||
|
|
||||||
use axum_extra::headers::UserAgent;
|
use axum_extra::headers::UserAgent;
|
||||||
use celes::Country;
|
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};
|
use time::{Date, OffsetDateTime};
|
||||||
|
|
||||||
pub type StatsId = String;
|
pub type StatsId = String;
|
||||||
@@ -54,6 +56,24 @@ impl DailyActiveDeviceDto {
|
|||||||
from_mixnet,
|
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)]
|
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||||
@@ -129,6 +149,7 @@ pub(crate) struct StatsReportV2Dto {
|
|||||||
pub(crate) session_duration_min: i32,
|
pub(crate) session_duration_min: i32,
|
||||||
pub(crate) disconnection_time_ms: i32,
|
pub(crate) disconnection_time_ms: i32,
|
||||||
pub(crate) exit_id: String,
|
pub(crate) exit_id: String,
|
||||||
|
pub(crate) exit_cc: Option<String>,
|
||||||
pub(crate) follow_up_id: Option<String>,
|
pub(crate) follow_up_id: Option<String>,
|
||||||
pub(crate) error: Option<String>,
|
pub(crate) error: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -161,6 +182,7 @@ impl StatsReportV2Dto {
|
|||||||
session_duration_min: stats_report.session_report.session_duration_min,
|
session_duration_min: stats_report.session_report.session_duration_min,
|
||||||
disconnection_time_ms: stats_report.session_report.disconnection_time_ms,
|
disconnection_time_ms: stats_report.session_report.disconnection_time_ms,
|
||||||
exit_id: stats_report.session_report.exit_id.clone(),
|
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(),
|
follow_up_id: stats_report.session_report.follow_up_id.clone(),
|
||||||
error: stats_report.session_report.error.clone(),
|
error: stats_report.session_report.error.clone(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user