React Instrumentation
One command wires up real browser tracing and logging with the OpenTelemetry Web SDK — fetch/XHR calls, page loads, and uncaught errors ship straight from the browser, no backend required.
Setup
la init --dsn la_live_YOUR_TOKEN_HERERun this inside your React project. la init detects React from package.json, installs the OTel Web SDK packages, and writes src/la-tracing.ts (or the project root if you don't use a src/ layout).
Add one import as the very first line of your entry point, before anything else renders:
import './la-tracing';
import ReactDOM from 'react-dom/client';
// ...ES2015 or ES2016. ZoneContextManager (used to keep async context correct across await/promise boundaries) breaks on ES2017+ due to a zone.js limitation. For Vite, that means setting build.target and esbuild.target in vite.config.ts — Vite's default (ES2020+) is too new. TypeScript's tsconfig.json target alone isn't enough for bundler-mode projects, since Vite/esbuild transpile independently of it.What gets captured automatically
- Fetch & XHR calls — every request your app makes: method, URL, status, duration
- Page loads — navigation timing and every resource the page fetches (scripts, images, fonts)
- Uncaught errors —
window.onerrorand unhandled promise rejections ship as ERROR-severity logs automatically
console.log is not captured — there's no browser auto-instrumentation for it in OpenTelemetry. Use the exported logger to emit structured log records manually where you want them.Screenshot needed
Traces page showing a React app's browser spans: GET fetch calls, resourceFetch entries for page assets, service=my-react-app, durations in ms.
Emitting your own logs
la-tracing.ts exports a ready-to-use logger:
import { logger } from './la-tracing';
import { SeverityNumber } from '@opentelemetry/api-logs';
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: 'INFO',
body: 'checkout started',
attributes: { 'order.id': orderId },
});Tracing your own backend too
This only instruments what happens in the browser. To trace your API, database, and background jobs, run la init again inside your backend project — see the Node.js or other backend instrumentation guides. Traces from both sides link up automatically once you set propagateTraceHeaderCorsUrls (see below) to your backend's URL.
Verify it's working
# Check spans are arriving (replace my-react-app with your service name)
la query "SELECT service, severity, body, duration_ms FROM recent_spans WHERE service = 'my-react-app' ORDER BY timestamp DESC LIMIT 10"Connecting browser traces to your backend
By default, no trace headers are sent cross-origin — this is opt-in for privacy. To link a browser trace to the backend request it triggers, add your backend's URL to propagateTraceHeaderCorsUrls in la-tracing.ts:
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [
/^https:\/\/api\.yourapp\.com/,
],
})traceparent and tracestate headers — they aren't in the browser's CORS-safelisted header set by default.Reducing trace volume for high-traffic apps
100% of traces and logs are sent by default — the free-tier byte cap is the natural backstop. For high-traffic apps, sample down with TraceIdRatioBasedSampler:
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';
const tracerProvider = new WebTracerProvider({
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1) }), // 10%
// ...
});Manual setup (if you'd rather not use the CLI)
npm install @opentelemetry/api @opentelemetry/api-logs \
@opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base @opentelemetry/sdk-logs \
@opentelemetry/instrumentation @opentelemetry/instrumentation-fetch \
@opentelemetry/instrumentation-xml-http-request @opentelemetry/instrumentation-document-load \
@opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-logs-otlp-http \
@opentelemetry/resources @opentelemetry/semantic-conventions \
@opentelemetry/context-zoneimport { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const SERVICE_NAME = 'my-react-app';
const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: SERVICE_NAME });
const tracerProvider = new WebTracerProvider({
resource,
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'https://la-ingest-gateway.lightarch.workers.dev/v1/traces',
headers: { 'x-la-dsn': 'la_live_YOUR_TOKEN_HERE' },
})
),
],
});
tracerProvider.register({ contextManager: new ZoneContextManager() });
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({ propagateTraceHeaderCorsUrls: [] }),
new XMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls: [] }),
new DocumentLoadInstrumentation(),
],
});
const loggerProvider = new LoggerProvider({
resource,
processors: [
new BatchLogRecordProcessor({
exporter: new OTLPLogExporter({
url: 'https://la-ingest-gateway.lightarch.workers.dev/v1/logs',
headers: { 'x-la-dsn': 'la_live_YOUR_TOKEN_HERE' },
}),
}),
],
});
logs.setGlobalLoggerProvider(loggerProvider);
export const logger = logs.getLogger('my-react-app');
window.addEventListener('error', (event) => {
logger.emit({
severityNumber: SeverityNumber.ERROR,
severityText: 'ERROR',
body: event.message,
attributes: { 'exception.type': event.error?.name ?? 'Error', 'exception.stacktrace': event.error?.stack ?? '' },
});
});
window.addEventListener('unhandledrejection', (event) => {
logger.emit({
severityNumber: SeverityNumber.ERROR,
severityText: 'ERROR',
body: String(event.reason?.message ?? event.reason),
attributes: { 'exception.type': event.reason?.name ?? 'UnhandledRejection', 'exception.stacktrace': event.reason?.stack ?? '' },
});
});