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
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
],
});
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 />);
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
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
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();
}
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 },
});
}
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;
}
}
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'],
});
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
},
});
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)