Implement frontend and backend observability alignment

Align frontend and backend error observability with correlation IDs and
structured telemetry for distributed tracing across systems.

Backend changes:
- Add CorrelationIdMiddleware to generate/extract correlation IDs
- Include correlation_id in all ErrorResponse objects
- Store correlation ID in structlog contextvars for automatic inclusion in logs
- Add correlation ID to response headers (X-Correlation-ID)

Frontend changes:
- API client automatically generates session-scoped UUID4 and includes
  X-Correlation-ID header in all requests
- Extract correlation ID from API error responses
- Update error handlers to use telemetry with correlation IDs
- Add telemetry logging to ErrorBoundary, PageErrorBoundary, SectionErrorBoundary
- Implement redaction utilities for privacy-safe logging of sensitive data

Documentation:
- Add observability guidelines to Web-Development.md
  * Correlation ID usage patterns
  * Privacy & security best practices
  * Telemetry event structure
  * Redaction utilities for sensitive data
- Add distributed tracing architecture section to Architecture.md
  * Correlation ID flow across frontend/backend
  * Example troubleshooting scenario
  * Implementation details for future enhancements

Testing:
- Add comprehensive tests for correlation middleware
- Update error boundary tests to verify telemetry integration
- Verify TypeScript and ESLint pass with no warnings

Fixes: Issue #40 - Frontend and backend observability are not aligned

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-30 18:32:19 +02:00
parent 9a43123b3a
commit 3d1a6f5538
16 changed files with 916 additions and 54 deletions

View File

@@ -4,9 +4,13 @@
* Catches render-time exceptions in child components and shows a fallback UI.
* This is the base component; use PageErrorBoundary or SectionErrorBoundary
* for page and section-level boundaries.
*
* All errors are logged using the telemetry service with structured context
* for distributed tracing and debugging.
*/
import React from "react";
import { Button, makeStyles, Text, tokens } from "@fluentui/react-components";
import { recordCritical } from "../utils/telemetry";
interface ErrorBoundaryState {
hasError: boolean;
@@ -102,6 +106,13 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
const { onError } = this.props;
// Log the error using telemetry for distributed tracing
recordCritical("component_render_error", error, {
component_stack: errorInfo.componentStack,
error_message: error.message,
});
if (onError) {
onError(error, errorInfo);
} else {

View File

@@ -9,6 +9,7 @@
*/
import React from "react";
import { ErrorBoundary } from "./ErrorBoundary";
import { recordCritical } from "../utils/telemetry";
interface PageErrorBoundaryProps {
children: React.ReactNode;
@@ -28,13 +29,22 @@ export function PageErrorBoundary({
pageName = "Page",
onError,
}: PageErrorBoundaryProps): React.JSX.Element {
// Enhanced error handler that includes page name in telemetry
const handleError = (error: Error, errorInfo: React.ErrorInfo): void => {
recordCritical("page_render_error", error, {
page_name: pageName,
component_stack: errorInfo.componentStack,
});
onError?.(error, errorInfo);
};
return (
<ErrorBoundary
title={`${pageName} Error`}
message={`The ${pageName.toLowerCase()} encountered an error and could not load. Please try navigating to another page or reloading.`}
showReloadButton={true}
isFullPage={false}
onError={onError}
onError={handleError}
>
{children}
</ErrorBoundary>

View File

@@ -13,6 +13,7 @@
*/
import React from "react";
import { ErrorBoundary } from "./ErrorBoundary";
import { recordWarning } from "../utils/telemetry";
interface SectionErrorBoundaryProps {
children: React.ReactNode;
@@ -32,13 +33,22 @@ export function SectionErrorBoundary({
sectionName = "Section",
onError,
}: SectionErrorBoundaryProps): React.JSX.Element {
// Enhanced error handler that includes section name in telemetry
const handleError = (error: Error, errorInfo: React.ErrorInfo): void => {
recordWarning("section_render_error", error.message, {
section_name: sectionName,
error_type: error.name,
});
onError?.(error, errorInfo);
};
return (
<ErrorBoundary
title={`${sectionName} Unavailable`}
message={`Could not load ${sectionName.toLowerCase()}. The rest of the page is still functional.`}
showReloadButton={true}
isFullPage={false}
onError={onError}
onError={handleError}
>
{children}
</ErrorBoundary>

View File

@@ -1,6 +1,10 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ErrorBoundary } from "../ErrorBoundary";
import * as telemetry from "../../utils/telemetry";
// Mock telemetry to verify it's called
vi.mock("../../utils/telemetry");
function ExplodingChild(): React.ReactElement {
throw new Error("boom");
@@ -16,7 +20,6 @@ describe("ErrorBoundary", () => {
expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
expect(screen.getByText(/boom/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reload/i })).toBeInTheDocument();
});