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

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)

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

/health

Liveness probe

return 200 '{"status":"ok"}' with access_log off. Does not touch the backend — confirms the nginx process is up and the container is serving.

/ready

Readiness probe

proxy_pass ${BACKEND_URL}/actuator/health with 5s connect/send/read timeouts. A 200 means both nginx and the Spring backend are ready to take traffic. Use this for Kubernetes readiness gates.

/nginx_status

nginx stub metrics

stub_status on; Protected by an ACL: allow 127.0.0.1; allow 10.0.0.0/8; allow 172.16.0.0/12; allow ${METRICS_ALLOW_IP}; deny all; — set METRICS_ALLOW_IP at deploy time to whitelist a Prometheus scraper or jump host.

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 /ready and /configs-service/ proxy.

  • METRICS_ALLOW_IP — additional CIDR/IP granted access to /nginx_status.

See Configuration and Docker & Deploy 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);
}
// 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.sh echoes the generated runtime-config.js contents and which nginx template (SSL vs HTTP-only) was selected based on /etc/nginx/ssl/tls.crt presence.

  • curl http://<host>/health — expect {"status":"ok"}. curl http://<host>/ready — expect 200 only when the backend /actuator/health is healthy.

Network monitoring

Check in browser DevTools → Network:

  • CSRF token flow:

    • GET /csrf called 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();

React DevTools

Install extension:

Inspect component state:

  1. Open DevTools → Components tab

  2. Select component in tree

  3. View props, state, hooks

Profiler:

  1. Open DevTools → Profiler tab

  2. Click "Record"

  3. Perform action

  4. Click "Stop"

  5. Analyze render times and re-renders

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:

  1. Reproduce: Try to reproduce the issue locally

  2. Check logs: Review browser console and network tab

  3. Check monitoring: Look for patterns in Sentry/Datadog

  4. Identify scope: Is it all users or specific user/browser/environment?

  5. Isolate cause: Backend error? Network issue? Client bug?

  6. Fix and verify: Deploy fix and monitor error rates

  7. Post-mortem: Document root cause and prevention steps