React-aware, vendor-neutral observability for React 18+ applications.
Zero boilerplate. OTLP output. No vendor lock-in.
Existing observability tools operate below the React layer:
- Error tracking tools capture exceptions — but not render performance, component attribution, or concurrent mode timing
- RUM tools measure browser-level metrics — React-specific signals like unnecessary re-renders emerge at a layer above what standard RUM instruments
- Raw OpenTelemetry requires significant manual instrumentation to produce React-aware signals
- Development-only profilers produce no production observability data
react-telemetry-open fills this gap. It tracks signals that only exist inside React — unnecessary re-renders, component-attributed long tasks, rage clicks, ErrorBoundary catches — and exports them as standard OTLP. One package. Any backend. Zero configuration to start.
React signals — render count and duration per component, rage clicks, time to first interaction, ErrorBoundary crashes with componentStack
Web Vitals — FCP, LCP, FID, CLS, INP with good/needs-improvement/poor ratings per Google thresholds
Network — every fetch() and XHR call with duration, status, URL. Error rate over 60-second windows. Online/offline events
Browser — JS errors with stack traces, unhandled promise rejections, long tasks, JS heap memory
Custom — track any business event with useTrackEvent()
Context on every event — component name, route, anonymous session ID, device type, browser, OS, network quality, React version
npm install react-telemetry-open
npx react-telemetry-open initThe CLI reads your package.json, generates telemetry.config.json, and tells you exactly what to add to your app.
Add to your app entry point (src/main.tsx):
import { TelemetryProvider } from 'react-telemetry-open'
import appConfig from '../telemetry.config.json'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<TelemetryProvider appConfig={appConfig}>
<BrowserRouter>
<App />
</BrowserRouter>
</TelemetryProvider>
</React.StrictMode>
)Open DevTools console. Telemetry appears immediately — no backend required.
▼ [react-telemetry-open] react.render.duration
Type: metric
Route: /dashboard
Duration: 2.40ms
Value: 2.4 ms
Attributes: { component: "Dashboard", renderCount: 1 }
▶ App { name: "my-app", version: "1.0.0", environment: "development" }
▶ Session { id: "550e8400-...", duration: "4s", pageViews: 1 }
▶ Device { type: "desktop", viewport: "1440×900", cpuCores: 10 }
▶ Browser { name: "Chrome", version: "124", os: "macOS 14.4" }
▶ Network { online: true, type: "4g" }
import { useTraceRender } from 'react-telemetry-open'
function UserDashboard() {
useTraceRender('UserDashboard')
return <div>...</div>
}Emits react.render.duration with render time, render count, and priority on every render.
import { useTrackInteraction } from 'react-telemetry-open'
function CheckoutButton() {
const { onClick } = useTrackInteraction('checkout-submit')
return <button onClick={onClick}>Buy Now</button>
}Tracks clicks and inputs. Automatically detects rage clicks (3+ rapid clicks = user frustration).
import { useTrackEvent } from 'react-telemetry-open'
function OnboardingFlow() {
const track = useTrackEvent()
const handleComplete = () => {
track('onboarding:completed', { step: 4, plan: 'pro' })
}
}import { useRouteTrace } from 'react-telemetry-open'
function AppRouter() {
useRouteTrace()
return <Routes>...</Routes>
}Emits route.change with fromRoute, toRoute, and navigationDurationMs on every navigation.
appConfig — static identity. Import your telemetry.config.json here. Generated by npx react-telemetry-open init.
config — runtime overrides. For values known only at runtime — sampling rate, consent flags, feature toggles.
import appConfig from '../telemetry.config.json'
<TelemetryProvider
appConfig={appConfig}
config={{
sampling: { rate: isSlowDevice ? 0.1 : 1.0 },
signals: { memory: hasUserConsented },
}}
>Both props are optional. Without either, the package works with defaults and shows a setup warning in development.
Run init to generate both config files:
npx react-telemetry-open initEdit telemetry.config.prod.json:
{
"app": {
"environment": "production",
"buildId": "$VITE_BUILD_ID"
},
"exporter": {
"type": "otlp",
"url": "$VITE_OTEL_URL",
"apiKey": "$VITE_OTEL_KEY"
},
"sampling": {
"rate": 0.1
},
"debug": false
}Add to .env.production:
VITE_OTEL_URL=https://your-collector-endpoint/otlp
VITE_OTEL_KEY=your-api-key
VITE_BUILD_ID=your-git-sha
Switch config based on environment in src/main.tsx:
import appConfig from '../telemetry.config.json'
import prodConfig from '../telemetry.config.prod.json'
const config = import.meta.env.PROD ? prodConfig : appConfig
<TelemetryProvider appConfig={config}>react-telemetry-open sends standard OTLP/HTTP — works with any OTel-compatible backend.
| Backend | Direct from browser | Notes |
|---|---|---|
| Self-hosted OTel Collector | ✅ | Configure CORS on your collector |
| Grafana Alloy (self-hosted) | ✅ | Configure CORS in alloy.config |
| Honeycomb | ✅ | Works directly |
| Axiom | ✅ | Works directly |
| Grafana Cloud | Requires a proxy — CORS not supported | |
| Any OTLP/HTTP endpoint | ✅ | As long as CORS headers are present |
Grafana Cloud's OTLP gateway does not support browser CORS. A lightweight proxy is required:
Browser → your proxy (CORS enabled) → Grafana Cloud
The simplest proxy is a single serverless function (Vercel/Netlify) that forwards requests with your Grafana credentials server-side. This keeps your API key off the client.
For self-hosted OTel Collector or Grafana Alloy, direct browser connections work — configure cors.allowed_origins in your collector.
| Signal | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| Render timing | ✅ | ✅ | ✅ | ✅ |
| Web Vitals (full) | ✅ | ✅ | FCP only | FCP 15.4+ |
| Long tasks | ✅ | ✅ | ❌ | ❌ |
| Memory | ✅ | ✅ 124+ | ❌ | ❌ |
| Network | ✅ | ✅ | ✅ | ✅ |
| Errors | ✅ | ✅ | ✅ | ✅ |
| Resource timing | ✅ | ✅ | ✅ | ✅ |
- Anonymous sessions — random UUIDs, never linked to user identity
- No input values — only that an interaction occurred, never what was typed
- URL sanitisation — query params stripped; numeric IDs replaced with
:id - No raw user agent — parsed to structured fields only
- Do Not Track — browser DNT setting honoured; all collection skipped when enabled
API keys and collector URLs must never be hardcoded. Use $VAR_NAME syntax — resolved from environment variables at runtime:
{
"exporter": {
"url": "$VITE_OTEL_URL",
"apiKey": "$VITE_OTEL_KEY"
}
}Found a bug? Have a feature request?
- Report a bug → Open a Bug Report
- Request a feature → Open a Feature Request
- Contribute code → Read CONTRIBUTING.md then open a Pull Request
All contributions are welcome.
- Getting Started
- Configuration Reference
- Interpreting Your Data
- Signals Overview
- Signals Reference
- API Reference
- System Design
- Troubleshooting
Apache 2.0 — Copyright 2026 Abhishek Sinha ([email protected])