feat(web): add client-side telemetry for API calls, errors, and scan operations
This commit is contained in:
+13
-1
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user