feat(web): add client-side telemetry for API calls, errors, and scan operations

This commit is contained in:
MythEclipse
2026-06-08 01:23:51 +07:00
parent 89dcbe8fa2
commit 64dcfa70d8
3 changed files with 25 additions and 1 deletions
+13 -1
View File
@@ -16,7 +16,7 @@ import { ExpertReviewsPage } from "@/pages/expert-reviews-page";
import { DiagnosesPage } from "@/pages/diagnoses-page"; import { DiagnosesPage } from "@/pages/diagnoses-page";
import { TelemetryPage } from "@/pages/telemetry-page"; import { TelemetryPage } from "@/pages/telemetry-page";
import { MainLayout } from "@/components/layout/main-layout"; import { MainLayout } from "@/components/layout/main-layout";
import { trackPageView } from "./lib/telemetry"; import { trackPageView, trackError } from "./lib/telemetry";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -104,11 +104,23 @@ function PageViewTracker() {
return null; return null;
} }
function GlobalErrorTracker() {
useEffect(() => {
const handler = (event: ErrorEvent) => {
trackError(event.filename || "global");
};
window.addEventListener("error", handler);
return () => window.removeEventListener("error", handler);
}, []);
return null;
}
export function App() { export function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthInitializer /> <AuthInitializer />
<PageViewTracker /> <PageViewTracker />
<GlobalErrorTracker />
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryClientProvider> </QueryClientProvider>
); );
+6
View File
@@ -12,6 +12,7 @@ import type {
ReviewDiagnosisRequest, ReviewDiagnosisRequest,
DashboardSummary, DashboardSummary,
} from '@zeavis/shared'; } from '@zeavis/shared';
import { recordApiCall } from './telemetry';
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''; const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? '';
@@ -21,6 +22,7 @@ export interface ApiError extends Error {
} }
async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> { async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
const start = performance.now();
const url = `${apiBaseUrl}${endpoint}`; const url = `${apiBaseUrl}${endpoint}`;
const response = await fetch(url, { const response = await fetch(url, {
credentials: 'include', credentials: 'include',
@@ -28,6 +30,10 @@ async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T>
headers: options?.headers, headers: options?.headers,
}); });
const duration = performance.now() - start;
const method = options?.method ?? 'GET';
recordApiCall(method, endpoint, duration, response.status);
if (!response.ok) { if (!response.ok) {
let errorMessage = `HTTP ${response.status}`; let errorMessage = `HTTP ${response.status}`;
let source: 'uploader' | 'model-service' | 'unknown' | undefined; let source: 'uploader' | 'model-service' | 'unknown' | undefined;
+6
View File
@@ -18,6 +18,7 @@ import { Modal } from "@/components/ui/modal";
import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge"; import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
import type { DiagnosisRecord } from "@zeavis/shared"; import type { DiagnosisRecord } from "@zeavis/shared";
import { apiClient } from "@/lib/api-client"; import { apiClient } from "@/lib/api-client";
import { trackScan, trackDiagnosisResult } from "@/lib/telemetry";
export function ScanPage() { export function ScanPage() {
const [fileName, setFileName] = useState<string | null>(null); const [fileName, setFileName] = useState<string | null>(null);
@@ -31,6 +32,7 @@ export function ScanPage() {
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (file: File) => apiClient.createDiagnosis(file), mutationFn: (file: File) => apiClient.createDiagnosis(file),
onSuccess: (diagnosis) => { onSuccess: (diagnosis) => {
trackDiagnosisResult(diagnosis.status !== "failed");
queryClient.invalidateQueries({ queryKey: ["diagnoses"] }); queryClient.invalidateQueries({ queryKey: ["diagnoses"] });
setDiagnosisPreview(diagnosis); setDiagnosisPreview(diagnosis);
setPreviewOpen(true); setPreviewOpen(true);
@@ -38,6 +40,9 @@ export function ScanPage() {
setPreviewUrl(null); setPreviewUrl(null);
if (inputRef.current) inputRef.current.value = ""; if (inputRef.current) inputRef.current.value = "";
}, },
onError: () => {
trackDiagnosisResult(false);
},
}); });
const handleFile = (f?: File) => { const handleFile = (f?: File) => {
@@ -61,6 +66,7 @@ export function ScanPage() {
const handleUpload = () => { const handleUpload = () => {
const file = inputRef.current?.files?.[0]; const file = inputRef.current?.files?.[0];
if (!file) return; if (!file) return;
trackScan();
mutation.mutate(file); mutation.mutate(file);
}; };