DEV Community

ThankGod Chibugwum Obobo
ThankGod Chibugwum Obobo

Posted on • Originally published at actocodes.hashnode.dev

Frontend Observability with Grafana Faro: Real User Monitoring for Production Web Apps

Backend observability is a solved problem for most mature engineering teams. Structured logs, distributed traces, and metrics dashboards are standard practice. But the moment a user opens your web app, you lose visibility. JavaScript exceptions, slow renders, failed API calls, and broken user flows happen entirely in the browser, invisible to your backend monitoring stack.

Frontend observability closes this gap. And Grafana Faro is one of the most capable open-source tools for doing it. Purpose-built to collect Real User Monitoring (RUM) data, JavaScript errors, performance metrics, and custom events directly from the browser, and feed them into the same Grafana stack your backend already uses.

This guide covers what Grafana Faro is, how to set it up in a React application, what to instrument, and how to connect frontend signals to your existing Grafana Cloud dashboards for end-to-end observability.

What Is Frontend Observability?

Frontend observability is the practice of collecting, correlating, and analyzing signals from real users' browsers in production. It covers four primary signal types:

Signal What It Captures
Errors JavaScript exceptions, unhandled promise rejections, network errors
Performance Core Web Vitals, page load times, resource timing, long tasks
Traces User session flows, distributed traces linking frontend calls to backend spans
Logs Custom application events, user interactions, feature flag evaluations

Traditional Application Performance Monitoring (APM) tools cover backend services. Real User Monitoring (RUM) covers the actual experience of real users on real devices and networks, capturing variance that synthetic testing in CI simply cannot reproduce.

What Is Grafana Faro?

Grafana Faro is an open-source Web SDK and collector designed specifically for frontend observability. Released by Grafana Labs, it integrates natively with the broader Grafana observability stack:

  • Grafana Faro Web SDK - the browser-side agent that collects signals
  • Grafana Agent / Faro Collector - receives signals from the browser and forwards them to Grafana Cloud
  • Grafana Cloud - stores and visualizes the data in Loki (logs), Tempo (traces), and Prometheus (metrics)

The key advantage over standalone RUM tools like Sentry or Datadog RUM is unified observability, a single frontend error in Faro can be correlated with the corresponding backend trace in Tempo, giving you a complete picture of what the user experienced and why.

Step 1 - Installation and Basic Setup

Install the Faro Web SDK:

npm install @grafana/faro-web-sdk @grafana/faro-web-tracing
Enter fullscreen mode Exit fullscreen mode

Initialize Faro as early as possible in your application, before any other imports, to capture errors that occur during startup:

// src/instrumentation.ts  ← import this first in main.tsx/index.tsx
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';

export const faro = initializeFaro({
  url: process.env.VITE_FARO_COLLECTOR_URL!, // your Grafana Agent endpoint
  app: {
    name: 'my-web-app',
    version: process.env.VITE_APP_VERSION ?? '1.0.0',
    environment: process.env.NODE_ENV,
  },
  instrumentations: [
    ...getWebInstrumentations({
      captureConsole: true,          // capture console.error and console.warn
      captureConsoleDisabledLevels: ['log', 'debug'], // skip noisy levels
    }),
    new TracingInstrumentation(),    // distributed tracing for fetch/XHR
  ],
});
Enter fullscreen mode Exit fullscreen mode

Then in your entry point:

// src/main.tsx
import './instrumentation';         // must be first
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
Enter fullscreen mode Exit fullscreen mode

With this alone, Faro will automatically collect:

  • Unhandled JavaScript errors and promise rejections
  • Browser console errors and warnings
  • Core Web Vitals (LCP, CLS, INP)
  • Navigation and resource timing
  • Distributed trace headers on all fetch and XHR requests

Step 2 - Setting Up the Grafana Agent Collector

The Faro SDK sends signals to a collector endpoint, not directly to Grafana Cloud. The collector receives, batches, and forwards signals to the appropriate backends.

For local development, run the Grafana Agent via Docker:

# docker-compose.yml
services:
  grafana-agent:
    image: grafana/agent:latest
    ports:
      - "12347:12347"   # Faro receiver port
    volumes:
      - ./grafana-agent.yaml:/etc/agent/agent.yaml
    command: -config.file=/etc/agent/agent.yaml
Enter fullscreen mode Exit fullscreen mode

Configure the agent to receive Faro signals and forward to Grafana Cloud:

# grafana-agent.yaml
faro:
  server:
    listen_address: 0.0.0.0:12347
    cors_allowed_origins:
      - "http://localhost:5173"
      - "https://your-production-domain.com"

logs:
  configs:
    - name: faro-logs
      clients:
        - url: https://logs-prod-eu-west-0.grafana.net/loki/api/v1/push
          basic_auth:
            username: YOUR_GRAFANA_CLOUD_USER_ID
            password: YOUR_GRAFANA_CLOUD_API_KEY

traces:
  configs:
    - name: faro-traces
      receivers:
        otlp:
          protocols:
            grpc:
      remote_write:
        - endpoint: tempo-prod-eu-west-0.grafana.net:443
          basic_auth:
            username: YOUR_TEMPO_USER_ID
            password: YOUR_GRAFANA_CLOUD_API_KEY
Enter fullscreen mode Exit fullscreen mode

Store all credentials as environment variables or secrets, never hardcode them in configuration files committed to version control.

Step 3 - Identifying Users and Sessions

Raw error logs are difficult to act on without user context. Attach user identity and session metadata to every Faro signal:

// After user authentication succeeds
import { faro } from './instrumentation';

function onUserAuthenticated(user: { id: string; email: string; plan: string }) {
  faro.api.setUser({
    id: user.id,
    email: user.email,          // only if your privacy policy permits
    attributes: {
      plan: user.plan,
      region: user.region,
    },
  });
}

function onUserLoggedOut() {
  faro.api.resetUser();
}
Enter fullscreen mode Exit fullscreen mode

Faro automatically generates a session ID per browser session. Combined with the user ID, you can reconstruct the complete sequence of events that led to an error, page navigations, API calls, user interactions, all directly in Grafana.

Step 4 - Custom Events and Manual Instrumentation

Automatic instrumentation captures infrastructure-level signals. Custom events capture business-level signals, the actions that matter to your product:

import { faro } from './instrumentation';

// Track a significant user action
function onCheckoutCompleted(orderId: string, total: number) {
  faro.api.pushEvent('checkout_completed', {
    orderId,
    total: total.toString(),
    currency: 'USD',
  });
}

// Track feature flag evaluations
function onFeatureFlagEvaluated(flagKey: string, value: boolean) {
  faro.api.pushEvent('feature_flag_evaluated', {
    flagKey,
    value: value.toString(),
  });
}

// Push a custom log entry
function onPaymentRetry(attempt: number, reason: string) {
  faro.api.pushLog([`Payment retry attempt ${attempt}: ${reason}`], {
    level: LogLevel.WARN,
    context: { attempt: attempt.toString(), reason },
  });
}
Enter fullscreen mode Exit fullscreen mode

Custom events are queryable in Grafana Loki using LogQL, you can build dashboards showing checkout conversion rates, feature flag adoption, or payment retry frequency directly from frontend signals.

Step 5 - Error Boundary Integration

React's Error Boundaries catch rendering errors that Faro's automatic instrumentation misses. Integrate Faro into your error boundary to capture them with full component context:

// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { faro } from '../instrumentation';

interface Props {
  children: ReactNode;
  fallback: ReactNode;
}

interface State {
  hasError: boolean;
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false };

  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    faro.api.pushError(error, {
      type: 'ReactRenderError',
      context: {
        componentStack: info.componentStack ?? '',
      },
    });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrap critical sections of your UI dashboards, checkout flows, data-heavy views, with dedicated error boundaries so rendering failures are captured with component stack context, not just as generic JavaScript errors.

Step 6 - Distributed Tracing: Connecting Frontend to Backend

The most powerful feature of Grafana Faro is distributed tracing. When TracingInstrumentation is enabled, Faro automatically injects OpenTelemetry trace headers (traceparent) into every fetch and XMLHttpRequest call.

If your backend is also instrumented with OpenTelemetry, the frontend and backend spans are linked. A single user action that triggers an API call produces a trace that spans both the browser and the server.

To ensure your backend accepts and propagates these headers, configure CORS to allow traceparent and tracestate:

// NestJS main.ts
app.enableCors({
  origin: process.env.FRONTEND_URL,
  allowedHeaders: ['Content-Type', 'Authorization', 'traceparent', 'tracestate'],
});
Enter fullscreen mode Exit fullscreen mode

In Grafana Tempo, you can then view a single trace that shows the full lifecycle of a user request: the browser fetch initiating at 0ms, the API gateway receiving at 12ms, the database query completing at 45ms, and the response rendering in the browser at 67ms, in one unified view.

Step 7 - Performance Monitoring and Core Web Vitals

Faro automatically collects Core Web Vitals, the metrics Google uses to evaluate page experience:

Metric Measures Good Threshold
LCP (Largest Contentful Paint) Loading performance ≤ 2.5s
INP (Interaction to Next Paint) Interactivity ≤ 200ms
CLS (Cumulative Layout Shift) Visual stability ≤ 0.1

These metrics are collected from real users across real devices and network conditions, far more representative than Lighthouse scores in CI. Build Grafana dashboards that segment Web Vitals by:

  • Device type (mobile vs. desktop)
  • Geographic region
  • Page route (which pages are slowest)
  • App version (did the latest deployment improve or degrade performance?)

Performance regressions caught by real user data are the ones that actually affect your users, not synthetic benchmarks.

Privacy and Data Minimization

Frontend observability collects data from real users, which means privacy is non-negotiable:

Redact PII before it reaches Faro. Never push user email addresses, payment details, or health data as custom event attributes.

Respect consent. Initialize Faro only after the user has accepted analytics cookies if your jurisdiction requires it (GDPR, CCPA).

Configure sampling rates. For high-traffic applications, collecting 100% of traces is unnecessary and costly. Configure Faro to sample a representative percentage:

initializeFaro({
  // ...
  sessionTracking: {
    samplingRate: 0.1, // collect sessions from 10% of users
  },
});
Enter fullscreen mode Exit fullscreen mode

Use data retention policies. Configure Loki and Tempo retention to match your compliance requirements, frontend traces rarely need to be kept longer than 30–90 days.

Conclusion

Backend monitoring tells you when your services are struggling. Frontend observability with Grafana Faro tells you when your users are struggling and in production, those are often very different things. A backend that's perfectly healthy can still deliver a broken experience if a JavaScript error blocks checkout, a slow render degrades a dashboard, or a failed fetch leaves a user staring at a spinner.

By integrating Faro into your React application, connecting it to Grafana Cloud, and correlating frontend traces with backend spans, you achieve true end-to-end observability, a single pane of glass from the user's click to the database query and back.

Start with automatic instrumentation, add user context and custom events for your critical flows, and build dashboards around the metrics that reflect real user experience. Observability is only complete when it includes the user.

Using Vue, Angular, or a meta-framework like Next.js? Grafana Faro's Web SDK is framework-agnostic, the setup differs slightly but the instrumentation API is identical. Drop your stack in the comments.

Top comments (0)