//! Frontend telemetry the browser cannot ship itself, honestly: the
//! ingest key never reaches client code, so the one thing a page can do is
//! ask this surface to relay a metric on its behalf.
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use jac_core::{Clock as _, SystemClock};
use jac_telemetry::Envelope;
use serde::Deserialize;
use crate::error::ApiError;
use crate::state::SharedState;
/// One Core Web Vital, as `next/web-vitals` reports it: `name` is
/// `CLS|FCP|INP|LCP|TTFB` (case-insensitive), `value` is milliseconds
/// except for `CLS`, which is unitless.
#[derive(Debug, Deserialize)]
pub(crate) struct WebVitalRequest {
pub name: String,
pub value: f64,
pub pathname: String,
}
/// `POST /api/telemetry/web-vitals` — re-emitted as the `web_vitals` event
/// shape the console's Performance page groups by `context.pathname`.
pub(crate) async fn web_vitals(
State(state): State<SharedState>,
Json(req): Json<WebVitalRequest>,
) -> Result<StatusCode, ApiError> {
let measurement = match req.name.to_ascii_uppercase().as_str() {
"LCP" => "lcp_ms",
"INP" => "inp_ms",
"CLS" => "cls",
"TTFB" => "ttfb_ms",
"FCP" => "fcp_ms",
other => {
return Err(ApiError::invalid(format!(
"unknown web vital `{other}` (LCP|INP|CLS|TTFB|FCP)"
)));
}
};
state.telemetry.emit(
Envelope::event("web_vitals", SystemClock.now().as_millis())
.pathname(req.pathname)
.measurement(measurement, req.value),
);
Ok(StatusCode::ACCEPTED)
}