jacquardSnapshot

← snapshot

1628 bytes
//! 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)
}