Dashboard Get started

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

bash
la init --dsn la_live_YOUR_TOKEN_HERE

Run 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:

src/main.tsx
import './la-tracing';
import ReactDOM from 'react-dom/client';
// ...
Set your bundler's build target to 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 errorswindow.onerror and 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:

typescript
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

bash
# 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:

typescript
new FetchInstrumentation({
  propagateTraceHeaderCorsUrls: [
    /^https:\/\/api\.yourapp\.com/,
  ],
})
Your backend's CORS config must also explicitly allow the 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:

typescript
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)
bash
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-zone
src/la-tracing.ts
import { 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 ?? '' },
  });
});