Version: 1.1.0This documentation is for Spitfire 1.1.0.
Connecting a React.js app (browser) with OTel
React in the browser: page load, fetch/XHR and clicks as traces, linked to your backend with traceparent. Values: Connecting services with OTel: overview.
Version table
| Build | Package | Version |
|---|---|---|
| Vite 5+, Next.js 15.3+, Create React App 5 | @opentelemetry/sdk-trace, core, resources, context-zone |
3.0.0 |
@opentelemetry/exporter-trace-otlp-http, instrumentation, instrumentation-fetch, instrumentation-xml-http-request |
0.300.0 | |
@opentelemetry/instrumentation-document-load |
0.69.0 | |
@opentelemetry/instrumentation-user-interaction + zone.js |
0.68.0 + 0.16.3 | |
@opentelemetry/api |
1.10.0 | |
| Build machine | Node.js | 22.15 or later |
| Browser | ES2022 (Chrome 94, Firefox 93, Safari 16.4 and later) | – |
Find your version: node --version
Traces only. OTel marks browser instrumentation as experimental.
Verified: 2026-11-04. Source: OTel JS browser, OTel JS 3.x migration guide, npm @opentelemetry/sdk-trace, npm @opentelemetry/instrumentation-document-load, npm @opentelemetry/instrumentation-user-interaction, Next.js instrumentation-client.
What you need
- Spitfire address
https://<spitfire> - Service name
<service-name>, version<service-version>, environment<environment> - The app folder (here
/srv/web, wherepackage.jsonis) and its address (herehttps://<app-origin>) - Your API address, as a regular expression (here
^https://api\.example\.com/)
Vite
Create the key: Observability → Browser and app key, Allowed origins
https://<app-origin>; copysfrum_…(Observability).Add the packages:
cd /srv/web && npm i @opentelemetry/api@1.10.0 @opentelemetry/sdk-trace@3.0.0 @opentelemetry/core@3.0.0 @opentelemetry/resources@3.0.0 @opentelemetry/context-zone@3.0.0 @opentelemetry/exporter-trace-otlp-http@0.300.0 @opentelemetry/instrumentation@0.300.0 @opentelemetry/instrumentation-fetch@0.300.0 @opentelemetry/instrumentation-xml-http-request@0.300.0 @opentelemetry/instrumentation-document-load@0.69.0 @opentelemetry/instrumentation-user-interaction@0.68.0 zone.js@0.16.3- Create
/srv/web/.env.productionand paste:
VITE_IS_OTEL_ENABLED=true
VITE_OTEL_SERVICE_NAME=<service-name>
VITE_OTEL_SERVICE_VERSION=<service-version>
VITE_OTEL_ENVIRONMENT=<environment>
VITE_OTEL_TRACES_ENDPOINT=https://<spitfire>/otlp/v1/traces
VITE_OTEL_KEY=sfrum_…
VITE_OTEL_TRACES_SAMPLER_ARG=0.1
VITE_OTEL_PROPAGATE_URLS=^https://api\.example\.com/- Create
/srv/web/src/otel.tsand paste:
// VITE_IS_OTEL_ENABLED=true: OTel on; any other value: off.
import { context, propagation, trace } from '@opentelemetry/api';
import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from '@opentelemetry/core';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler, TracerProvider } from '@opentelemetry/sdk-trace';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
const env = import.meta.env;
if (env.VITE_IS_OTEL_ENABLED === 'true') {
const provider = new TracerProvider({
resource: resourceFromAttributes({
'service.name': env.VITE_OTEL_SERVICE_NAME,
'service.version': env.VITE_OTEL_SERVICE_VERSION,
'deployment.environment': env.VITE_OTEL_ENVIRONMENT,
}),
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(Number(env.VITE_OTEL_TRACES_SAMPLER_ARG)) }),
spanProcessors: [new BatchSpanProcessor({
exporter: new OTLPTraceExporter({ url: env.VITE_OTEL_TRACES_ENDPOINT, headers: { Authorization: `Bearer ${env.VITE_OTEL_KEY}` } }),
})],
});
trace.setGlobalTracerProvider(provider);
context.setGlobalContextManager(new ZoneContextManager().enable());
propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }));
const propagateTraceHeaderCorsUrls = [new RegExp(env.VITE_OTEL_PROPAGATE_URLS)];
registerInstrumentations({
instrumentations: [
new DocumentLoadInstrumentation(),
new UserInteractionInstrumentation({ eventNames: ['click', 'submit'] }),
new FetchInstrumentation({ propagateTraceHeaderCorsUrls }),
new XMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls }),
],
});
}- Make it the first line of
/srv/web/src/main.tsx:
import './otel';- Build:
cd /srv/web && npm run buildNext.js
- Vite steps 1 and 2, then create
/srv/web/.env.productionfrom Vite step 3 withNEXT_PUBLIC_in place ofVITE_:
sed -i.bak 's/^VITE_/NEXT_PUBLIC_/' /srv/web/.env.production- Save the Vite step 4 code as
/srv/web/instrumentation-client.tsand change the prefix:
perl -0pi -e 's/const env = import\.meta\.env;\n//; s/\benv\.VITE_/process.env.VITE_/g; s/VITE_/NEXT_PUBLIC_/g' /srv/web/instrumentation-client.ts- Build:
cd /srv/web && npm run buildCreate React App
- Vite steps 1, 2 and 4, then change the prefix of
.env.productionandsrc/otel.ts:
sed -i.bak 's/^VITE_/REACT_APP_/' /srv/web/.env.production && perl -0pi -e 's/const env = import\.meta\.env;\n//; s/\benv\.VITE_/process.env.VITE_/g; s/VITE_/REACT_APP_/g' /srv/web/src/otel.ts- Make
import './otel';the first line of/srv/web/src/index.tsx, then build:
cd /srv/web && npm run buildYour backend
- Let the backend's CORS allow the trace headers from
https://<app-origin>:
Access-Control-Allow-Headers: traceparent, tracestate, baggageVerify
- The browser may send:
curl -s -o /dev/null -w '%{http_code}' -X OPTIONS https://<spitfire>/otlp/v1/traces -H 'Origin: https://<app-origin>' -H 'Access-Control-Request-Method: POST' | grep -q '^204