Adetayo Akinsanya unkletayo.dev

Frontend Observability Architecture: Real User Monitoring (RUM), OpenTelemetry, and Error Boundary Tracking

Deconstructing RUM telemetry, W3C traceparent context propagation, breadcrumb buffers, and Error Boundaries

Part 17 in Series — Catch up on the previous article: Edge Rendering & Server Components: SSR Hydration, Static Regeneration (ISR), and React Server Components (Part 16) before diving into this post.

For three consecutive days following a major software release, a digital media enterprise lost an estimated $140,000 in subscription renewals.

The backend infrastructure engineering team saw green dashboards everywhere: server CPU utilization was 12%, database latency was sub-10ms, and API response status codes were 99.99% 200 OK. Server-side APM tools reported flawless health.

However, a subtle bug in a third-party payment script had broken client-side token serialization on Safari iOS browsers. When users clicked “Renew Subscription”, an uncaught JavaScript error threw inside an event listener. The request never left the user’s browser—meaning the server never saw a single failed log or 500 error code.

Without client-side observability, the team was operating completely blind.

Frontend Observability extends monitoring into the browser environment—combining Real User Monitoring (RUM), Distributed Tracing (OpenTelemetry), and Error Telemetry to provide complete visibility into client-side application health.


1. Real User Monitoring (RUM) Data Collection Architecture

RUM monitors actual user sessions in real-time, capturing performance metrics (Core Web Vitals), user interaction events, and Javascript exceptions directly from client devices.

[ Browser Client RUM Agent ] 
     |
     | 1. Collect PerformanceObserver (LCP, INP, CLS) + Errors
     | 2. Buffer in Ring Buffer Queue
     | 3. Send Payload via navigator.sendBeacon() on Unload
     v
[ Telemetry Ingestion Collector ] ---> [ ClickHouse / Datadog / Sentry ]

Non-Blocking Telemetry Ingestion via navigator.sendBeacon

Transmitting telemetry data during page navigation or browser tab closure using standard fetch() or XMLHttpRequest risks request cancellation as the browser unloads the document.

The navigator.sendBeacon() API queues data for asynchronous background transmission by the browser agent, guaranteeing delivery without delaying page unloads:

// Production Telemetry Queue Manager
export class TelemetryBuffer {
  private queue: Array<Record<string, unknown>> = [];
  private readonly endpoint = "https://telemetry.enterprise.com/v1/rum";
  private readonly maxBufferSize = 50;

  constructor() {
    // Flush telemetry on page visibility change or unload
    window.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "hidden") {
        this.flush();
      }
    });
  }

  public track(event: string, payload: Record<string, unknown>) {
    this.queue.push({
      event,
      payload,
      timestamp: Date.now(),
      url: window.location.href
    });

    if (this.queue.length >= this.maxBufferSize) {
      this.flush();
    }
  }

  public flush() {
    if (this.queue.length === 0) return;

    const blob = new Blob([JSON.stringify(this.queue)], {
      type: "application/json"
    });

    // Uses sendBeacon for guaranteed asynchronous transmission on unload
    const success = navigator.sendBeacon(this.endpoint, blob);
    if (success) {
      this.queue = [];
    }
  }
}

2. Distributed Tracing: W3C Trace Context Propagation

To trace a single user action (e.g., clicking “Place Order”) across the frontend application and downstream backend microservices, the client API layer must inject W3C Trace Context HTTP headers.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
              |  |                                |                |
        Version  Trace ID (16 bytes)              Parent Span ID   Trace Flags
// Fetch Interceptor for W3C Distributed Tracing
export async function tracedFetch(url: string, options: RequestInit = {}): Promise<Response> {
  const traceId = generateHexBytes(16);  // 128-bit Trace ID
  const spanId = generateHexBytes(8);    // 64-bit Span ID
  const traceparent = `00-${traceId}-${spanId}-01`;

  const headers = new Headers(options.headers || {});
  headers.set("traceparent", traceparent);

  return fetch(url, { ...options, headers });
}

function generateHexBytes(byteCount: number): string {
  const array = new Uint8Array(byteCount);
  window.crypto.getRandomValues(array);
  return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
}

3. Global Error Handling & React Error Boundaries

A robust error tracking system captures both unhandled global runtime exceptions and isolated component tree failures.

// Complete React Error Boundary with Telemetry Integration
import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
  children: ReactNode;
  fallback: ReactNode;
  onCatch?: (error: Error, info: ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class TelemetryErrorBoundary extends Component<Props, State> {
  public state: State = { hasError: false, error: null };

  public static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error("Uncaught React Component Error:", error, errorInfo);
    
    // Log to RUM Telemetry System
    this.props.onCatch?.(error, errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

Summary & Key Takeaways

  • RUM Telemetry: Real User Monitoring captures field performance metrics (Core Web Vitals) and runtime errors directly from client devices.
  • navigator.sendBeacon: Ensures asynchronous telemetry payloads are reliably transmitted without blocking document unload transitions.
  • W3C Distributed Tracing: Inject traceparent headers into outgoing HTTP requests to link frontend spans with backend microservice trace trees.
  • React Error Boundaries: Use Error Boundaries (componentDidCatch) to isolate UI sub-tree crashes and log diagnostic component stacks.

References & Further Reading

  1. W3C Recommendation. W3C Trace Context Specification. W3C.
  2. OpenTelemetry Project. OpenTelemetry JavaScript Web SDK Guidelines. CNCF.
  3. W3C Candidate Recommendation. W3C Beacon API Specification. W3C.

Up Next in Series →

Part 18: Frontend System Design Interview Framework: Component Topology, Data Flow, and Scalability

Continue to Part 18 →