good stuff
This commit is contained in:
parent
ea8389ef40
commit
f4de0eeb9f
39 changed files with 5165 additions and 348 deletions
77
server-rs/src/routes/telemetry.rs
Normal file
77
server-rs/src/routes/telemetry.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::Json;
|
||||
use axum::Extension;
|
||||
use metrics::{counter, gauge};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::auth::OptionalUser;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TelemetryPayload {
|
||||
session_seconds: u64,
|
||||
filter_count: u64,
|
||||
/// Sent once on first beacon: the entry page path
|
||||
#[serde(default)]
|
||||
entry_path: Option<String>,
|
||||
/// Sent once on first beacon: the document.referrer domain (or "direct")
|
||||
#[serde(default)]
|
||||
referrer: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn post_telemetry(
|
||||
Extension(user): Extension<OptionalUser>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<TelemetryPayload>,
|
||||
) -> StatusCode {
|
||||
let user_label = match &user.0 {
|
||||
Some(u) => u.email.clone(),
|
||||
None => "anonymous".to_string(),
|
||||
};
|
||||
|
||||
let ua = headers
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("unknown");
|
||||
let browser = parse_browser(ua);
|
||||
|
||||
gauge!("user_session_seconds", "user" => user_label.clone(), "browser" => browser.clone())
|
||||
.set(payload.session_seconds as f64);
|
||||
gauge!("user_active_filters", "user" => user_label, "browser" => browser)
|
||||
.set(payload.filter_count as f64);
|
||||
|
||||
// Entrypoint tracking (sent once per session)
|
||||
if let Some(path) = &payload.entry_path {
|
||||
let referrer = payload.referrer.as_deref().unwrap_or("direct");
|
||||
counter!("entrypoint_total", "path" => normalize_entry_path(path), "referrer" => referrer.to_string())
|
||||
.increment(1);
|
||||
}
|
||||
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
/// Normalize entry paths to prevent cardinality explosion.
|
||||
/// Keep known routes, parameterize dynamic segments.
|
||||
fn normalize_entry_path(path: &str) -> String {
|
||||
match path {
|
||||
"/" | "/dashboard" | "/pricing" | "/learn" | "/saved" | "/invites" | "/account" => {
|
||||
path.to_string()
|
||||
}
|
||||
p if p.starts_with("/invite/") => "/invite/:code".to_string(),
|
||||
p if p.starts_with("/s/") => "/s/:code".to_string(),
|
||||
_ => "/other".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_browser(ua: &str) -> String {
|
||||
if ua.contains("Firefox") {
|
||||
"Firefox".into()
|
||||
} else if ua.contains("Edg/") {
|
||||
"Edge".into()
|
||||
} else if ua.contains("Chrome") {
|
||||
"Chrome".into()
|
||||
} else if ua.contains("Safari") {
|
||||
"Safari".into()
|
||||
} else {
|
||||
"Other".into()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue