feat(web): add client-side telemetry with Web Vitals and dev metrics endpoint

This commit is contained in:
MythEclipse
2026-06-07 18:13:11 +07:00
parent eefb16ad1d
commit 5a47b0c658
8 changed files with 122 additions and 7 deletions
+11 -5
View File
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createBrowserRouter,
@@ -5,7 +6,6 @@ import {
Navigate,
} from "react-router-dom";
import { AuthInitializer } from "@/components/auth-initializer";
// import { AuthGuard } from "@/components/auth-guard";
import { DashboardPage } from "@/pages/dashboard-page";
import { ScanPage } from "@/pages/scan-page";
import { LibraryPage } from "@/pages/library-page";
@@ -14,16 +14,13 @@ import { DiseaseDetailPage } from "@/pages/disease-detail-page";
import { DiagnosisDetailPage } from "@/pages/diagnosis-detail-page";
import { ExpertReviewsPage } from "@/pages/expert-reviews-page";
import { DiagnosesPage } from "@/pages/diagnoses-page";
// import { LoginPage } from "@/pages/login-page";
// import { RegisterPage } from "@/pages/register-page";
import { MainLayout } from "@/components/layout/main-layout";
import { trackPageView } from "./lib/telemetry";
const queryClient = new QueryClient();
const router = createBrowserRouter([
{ path: "/", element: <Navigate to="/dashboard" replace /> },
// { path: "/login", element: <LoginPage /> },
// { path: "/register", element: <RegisterPage /> },
{
path: "/dashboard",
element: (
@@ -90,10 +87,19 @@ const router = createBrowserRouter([
},
]);
function PageViewTracker() {
const location = window.location;
useEffect(() => {
trackPageView(location.pathname + location.search);
}, [location.pathname, location.search]);
return null;
}
export function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthInitializer />
<PageViewTracker />
<RouterProvider router={router} />
</QueryClientProvider>
);
+52
View File
@@ -0,0 +1,52 @@
/**
* Clientside telemetry for the ZeaVis Edu web app.
*
* In development, metrics are collected inmemory and exposed at /metrics
* via a Vite plugin. In production they are sent as HTTP beacons to the
* Telemetry pipeline (see METRICS.md).
*/
// ── Web Vitals ──────────────────────────────────────────
export type MetricEntry = {
name: string;
value: number;
rating?: string;
};
const vitalsBuffer: MetricEntry[] = [];
export function reportWebVitals(metric: MetricEntry): void {
vitalsBuffer.push(metric);
// Keep last 20 entries in memory for the /metrics endpoint
if (vitalsBuffer.length > 20) vitalsBuffer.shift();
console.debug(`[telemetry] ${metric.name}: ${metric.value} (${metric.rating ?? 'n/a'})`);
}
// ── Pageview counter ───────────────────────────────────
let pageViewCount = 0;
export function trackPageView(path: string): void {
pageViewCount++;
console.debug(`[telemetry] pageview: ${path} (total: ${pageViewCount})`);
}
// ── Metrics serialisation (consumed by viteplugin) ────
export function collectMetrics(): string {
const lines: string[] = [];
// ── Default processlike metrics ──────────────────────
lines.push('# HELP zeavis_web_page_views_total Total page views');
lines.push('# TYPE zeavis_web_page_views_total counter');
lines.push(`zeavis_web_page_views_total ${pageViewCount}`);
lines.push('# HELP zeavis_web_vital_bucket Web Vitals observed this session');
lines.push('# TYPE zeavis_web_vital_bucket gauge');
for (const v of vitalsBuffer) {
lines.push(`zeavis_web_vital_bucket{name="${v.name}",rating="${v.rating ?? 'unknown'}"} ${v.value}`);
}
return lines.join('\n') + '\n';
}
+9
View File
@@ -2,6 +2,15 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./app";
import "./index.css";
import { reportWebVitals } from "./lib/telemetry";
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
// Report Web Vitals to our in-memory telemetry store
onCLS((m) => reportWebVitals({ name: "CLS", value: m.value, rating: m.rating }));
onFCP((m) => reportWebVitals({ name: "FCP", value: m.value, rating: m.rating }));
onINP((m) => reportWebVitals({ name: "INP", value: m.value, rating: m.rating }));
onLCP((m) => reportWebVitals({ name: "LCP", value: m.value, rating: m.rating }));
onTTFB((m) => reportWebVitals({ name: "TTFB", value: m.value, rating: m.rating }));
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>