Observability
This document outlines client-side monitoring, logging, debugging, and performance monitoring strategies for FT Configs UI.
Current status: No external observability SaaS integrations (Sentry/Datadog/etc.) are configured in the repository. Client telemetry is limited to structured browser-console logging; the runtime container (nginx:alpine) exposes liveness, readiness, and an nginx metrics endpoint described below.
Client-side error logging strategy
Current approach: Console logging
All errors are logged to browser console via:
-
console.error()— for caught exceptions -
console.warn()— for non-critical issues -
console.debug()— for development debugging
Implementation: useErrorHandler() hook in src/hooks/use-error-handler.ts
Error classification and structured logging
Errors are classified before logging:
// src/hooks/use-error-handler.ts
import {extractUnifiedError} from '@/lib/error-utils';
const handleError = (error: unknown, options?: {title?: string}) => {
const unified = extractUnifiedError(error, t);
console.error('[Error Handler]', {
type: unified.type, // 'server' | 'network' | 'validation' | 'frontend'
message: unified.message,
description: unified.description,
code: unified.code,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
});
toast({
title: options?.title || t('common.error'),
description: unified.message,
variant: 'destructive',
});
};
Logged fields:
-
type— error classification -
message— user-friendly error message -
description— additional context -
code— HTTP status or error code -
timestamp— when error occurred -
url— current page URL -
userAgent— browser info
Recommended: Integrate error monitoring service
If an external error-monitoring product is introduced later, prefer a framework-agnostic browser SDK (for example @sentry/react or @datadog/browser-rum) and initialize it early in src/main.tsx, before createRoot(…).render(<App />). Configuration should be read from Vite build-time env vars (import.meta.env.VITE_*) or from window.RUNTIME_CONFIG for values that must change per deployment without a rebuild.
Sketch (not currently in the repo):
// src/main.tsx
import * as Sentry from '@sentry/react';
if (import.meta.env.VITE_SENTRY_DSN) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
tracesSampleRate: 0.1,
beforeSend(event) {
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
return event;
},
});
}
Integration with the existing error pipeline happens inside useErrorHandler(): after extractUnifiedError() classifies the failure, call Sentry.captureException(error, { tags: { errorType, errorCode } }) before toast(…). Console logging stays for local development — gate it on import.meta.env.MODE === 'development'.
Error filtering and sampling
Don’t log:
-
Network errors during development (localhost CORS)
-
Aborted requests (user navigation)
-
401 errors (handled by token refresh)
-
User cancellations
Sample rate strategy:
-
Production: 100% errors, 10% transactions
-
Staging: 100% errors, 50% transactions
-
Development: console only (no external logging)
Performance monitoring approach
Current approach: Browser DevTools
Use browser Performance tab to analyze:
-
Time to First Byte (TTFB)
-
First Contentful Paint (FCP)
-
Largest Contentful Paint (LCP)
-
Cumulative Layout Shift (CLS)
-
Time to Interactive (TTI)
Web Vitals monitoring (recommended)
npm install web-vitals
// src/lib/observability/web-vitals.ts
import {getCLS, getFID, getFCP, getLCP, getTTFB} from 'web-vitals';
export const reportWebVitals = () => {
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
};
// src/main.tsx — call once, before render
import {reportWebVitals} from '@/lib/observability/web-vitals';
if (import.meta.env.MODE === 'production') {
reportWebVitals();
}
Performance budgets
Define performance thresholds:
Core Web Vitals targets:
-
LCP (Largest Contentful Paint): < 2.5s
-
FID (First Input Delay): < 100ms
-
CLS (Cumulative Layout Shift): < 0.1
Additional metrics:
-
TTFB (Time to First Byte): < 800ms
-
FCP (First Contentful Paint): < 1.8s
-
TTI (Time to Interactive): < 3.8s
Bundle size budgets:
-
JavaScript (initial): < 200 KB gzipped
-
JavaScript (total): < 500 KB gzipped
-
CSS: < 50 KB gzipped
-
Images: < 100 KB per image
Performance optimization techniques
Code splitting: Route-level code splitting is provided automatically by TanStack Router file-based routing — each src/routes/*/.tsx entry is compiled as a lazy chunk and fetched on navigation. For component-level splitting inside a route, use the standard React.lazy + <Suspense> pattern:
import {lazy, Suspense} from 'react';
import {Skeleton} from '@/components/ui/skeleton';
const HeavyEditor = lazy(() => import('@/components/acs/heavy-editor'));
export function View() {
return (
<Suspense fallback={<Skeleton className="h-64 w-full" />}>
<HeavyEditor />
</Suspense>
);
}
Asset optimization: Vite handles static assets (imported from src/ or served from public/) with hashed filenames and long-lived cache headers emitted by nginx (Cache-Control: public, immutable; expires 1y for .js|.css|.png|.jpg|.svg|.woff2). Use native browser primitives for images:
<img src="/logo.png" alt="Logo" width={200} height={50} loading="lazy" decoding="async" />
Debouncing expensive operations:
import {useDebounce} from '@/hooks/use-debounce';
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 250);
useEffect(() => {
if (debouncedSearch) {
fetchResults(debouncedSearch);
}
}, [debouncedSearch]);
Server-side endpoints (nginx container)
Because the production image is nginx:alpine, there is no Node.js process to surface application-level metrics. The nginx config in docker/ops/nginx.conf.template (and its HTTP-only sibling) exposes three endpoints used by orchestration and monitoring:
| Endpoint | Purpose | Behavior |
|---|---|---|
|
Liveness probe |
|
|
Readiness probe |
|
|
nginx stub metrics |
|
Scraping /nginx_status with Prometheus: pair it with nginx-prometheus-exporter (sidecar or separate service) pointed at http://<pod>/nginx_status. The stub format returns Active connections, accepts, handled, requests, Reading, Writing, Waiting — the exporter converts them to Prometheus metrics.
Runtime-config env vars relevant to observability:
-
BACKEND_URL— backend base URL used by/readyand/configs-service/proxy. -
METRICS_ALLOW_IP— additional CIDR/IP granted access to/nginx_status.
See Configuration and Installation & Deployment — Environment Configuration for the full list.
User analytics considerations
Current status: No analytics integration configured
Privacy-first analytics (recommended):
-
Use privacy-focused tools (Plausible, Fathom, Simple Analytics)
-
Avoid Google Analytics (GDPR concerns)
-
No tracking without user consent
-
Anonymize IP addresses
-
Don’t track PII (email, username)
What to track:
-
Page views (anonymized)
-
Feature usage (e.g., "Export clicked", "Import completed")
-
Error rates by page
-
Session duration
-
Browser/OS distribution (for compatibility)
What NOT to track:
-
Personally identifiable information (PII)
-
Configuration data
-
User inputs
-
API request/response payloads
-
Session tokens or credentials
Implementation example (Plausible):
// src/lib/observability/analytics.ts
export const trackEvent = (eventName: string, props?: Record<string, string>) => {
if (typeof window === 'undefined' || !window.plausible) {
return;
}
window.plausible(eventName, {props});
};
// Usage
trackEvent('Export', {module: 'acs-fcc-config'});
trackEvent('Import Success', {module: 'acs-fcc-config', warnings: 'false'});
trackEvent('Login', {role: user.role});
Debug mode
Current approach: Vite mode check
if (import.meta.env.MODE === 'development') {
console.debug('[Debug]', data);
}
Recommended: Feature flag debug mode
// Enable via query param: ?debug=true
const isDebugMode = () => {
if (typeof window === 'undefined') return false;
const params = new URLSearchParams(window.location.search);
return params.get('debug') === 'true';
};
// Enable via localStorage: localStorage.setItem('debug', 'true')
const isDebugModeLocalStorage = () => {
if (typeof window === 'undefined') return false;
return localStorage.getItem('debug') === 'true';
};
export const debug = (...args: unknown[]) => {
if (isDebugMode() || isDebugModeLocalStorage() || import.meta.env.MODE === 'development') {
console.debug('[Debug]', ...args);
}
};
Usage:
import {debug} from '@/lib/observability/debug';
debug('API response:', response);
debug('User state:', user);
debug('Form values:', formValues);
Debug panel (in-app)
// src/components/debug-panel.tsx
import {useState} from 'react';
import {useAuth} from '@/hooks/use-auth';
export function DebugPanel() {
const [open, setOpen] = useState(false);
const {user} = useAuth();
if (import.meta.env.MODE !== 'development') {
return null;
}
return (
<div className="fixed bottom-4 right-4 z-50">
<Button onClick={() => setOpen(!open)} size="sm">
🐛 Debug
</Button>
{open && (
<Card className="mt-2 p-4 max-w-md max-h-96 overflow-auto">
<h3 className="font-bold mb-2">Debug Info</h3>
<pre className="text-xs">
{JSON.stringify(
{
user,
apiUrl: window.__RUNTIME_CONFIG__?.API_URL,
mode: import.meta.env.MODE,
locale: document.cookie.match(/locale=([^;]+)/)?.[1],
},
null,
2
)}
</pre>
</Card>
)}
</div>
);
}
Practical troubleshooting signals
-
Browser devtools network tab: confirm base URL, cookies, CSRF calls, and 401 refresh behavior.
-
Container stdout:
docker/ops/entrypoint.shechoes the generatedruntime-config.jscontents and which nginx template (SSL vs HTTP-only) was selected based on/etc/nginx/ssl/tls.crtpresence. -
curl http://<host>/health— expect{"status":"ok"}.curl http://<host>/ready— expect 200 only when the backend/actuator/healthis healthy.
Network monitoring
Check in browser DevTools → Network:
-
CSRF token flow:
-
GET
/csrfcalled before first POST/PUT/DELETE -
Response headers contain
X-XSRF-TOKEN -
Request cookies contain
XSRF-TOKEN
-
-
Token refresh flow:
-
401 response triggers POST
/auth/refresh-token -
Successful refresh retries original request
-
Failed refresh redirects to
/login
-
-
API base URL:
-
All requests use correct base URL (check Request URL)
-
No CORS errors (check Console)
-
-
Request timing:
-
Slow requests (> 1s) highlighted in red
-
Identify bottlenecks (database queries, file processing)
-
Console patterns
Structured logging:
// Good: Structured with context
console.log('[Auth]', 'Login success', {username, role});
// Bad: Unstructured string
console.log('User admin logged in as ADMIN');
Log levels:
console.error('[Error]', error); // Errors (red)
console.warn('[Warning]', warning); // Warnings (yellow)
console.info('[Info]', info); // Info (blue)
console.debug('[Debug]', debug); // Debug (gray, hidden by default)
Group related logs:
console.group('[Import] Dry-run started');
console.log('File:', file.name);
console.log('Size:', file.size);
console.log('Type:', file.type);
console.groupEnd();
Environment snapshot
The UI calls /environment/snapshot and surfaces version/environment/cache layer info on the home page.
Use this data to confirm you are connected to the intended backend environment before applying imports.
Snapshot fields:
-
version— backend version -
environment— environment name (dev, staging, prod) -
cacheLayer— cache implementation details -
features— enabled feature flags
Implementation: Displayed on dashboard/home page
Observability checklist
Production readiness
-
Error monitoring configured (Sentry/Datadog)
-
Web Vitals tracking enabled
-
Performance budgets defined
-
Debug mode disabled in production builds
-
Sensitive data not logged
-
Error sampling configured (avoid log spam)
-
Analytics tracking (privacy-compliant)
-
Environment snapshot endpoint functional
Monitoring dashboards
Key metrics to track:
-
Error rate (errors per session)
-
Error types distribution (server/network/validation)
-
Page load times (P50, P95, P99)
-
API response times
-
User session duration
-
Feature adoption rates
Alerting thresholds:
-
Error rate > 5% → alert
-
LCP > 4s → warning
-
TTFB > 1.5s → warning
-
5xx errors → immediate alert
Incident investigation workflow
When an error is reported:
-
Reproduce: Try to reproduce the issue locally
-
Check logs: Review browser console and network tab
-
Check monitoring: Look for patterns in Sentry/Datadog
-
Identify scope: Is it all users or specific user/browser/environment?
-
Isolate cause: Backend error? Network issue? Client bug?
-
Fix and verify: Deploy fix and monitor error rates
-
Post-mortem: Document root cause and prevention steps