@invinite/otel-web
v2.1.2
Published
Lightweight OpenTelemetry initialization for browser SPAs. Sets up tracing with a single `initialize()` call and provides a plugin system for framework-specific instrumentation.
Downloads
959
Readme
@invinite/otel-web
Lightweight OpenTelemetry initialization for browser SPAs. Sets up tracing with a single initialize() call and provides a plugin system for framework-specific instrumentation.
Why
Setting up OpenTelemetry in the browser requires wiring together a provider, exporter, resource, and span processors. This library reduces that to a single function call while keeping full control through a plugin system.
Install
npm install @invinite/otel-web @opentelemetry/api@opentelemetry/api is a peer dependency — you should have exactly one copy in your app.
Quick Start
import { initialize } from "@invinite/otel-web";
import { createDocumentLoadPlugin } from "@invinite/otel-web/plugins/document-load";
import { createFetchPlugin } from "@invinite/otel-web/plugins/fetch";
const cleanup = initialize({
collectorUrl: "https://otel-collector.example.com",
serviceName: "my-app",
serviceVersion: "1.2.0",
environment: "production",
plugins: [
createDocumentLoadPlugin(),
createFetchPlugin({
propagateToUrls: [/api\.example\.com/],
}),
],
});
// Call cleanup() on app teardownConfiguration
interface OtelWebConfig {
/** Base OTLP HTTP URL (e.g. "https://collector.example.com"). Signal paths are appended automatically. */
collectorUrl: string;
/** Service name reported in spans */
serviceName: string;
/** Service version, added as a resource attribute */
serviceVersion?: string;
/** Deployment environment name (e.g. "production", "staging"), added as a resource attribute */
environment?: string;
/** Trace sampling rate between 0 and 1. Defaults to 1 (sample everything). */
sampleRate?: number;
/** Optional headers sent with every export request */
headers?: Record<string, string>;
/** Plugins to activate */
plugins?: OtelWebPlugin[];
/** Called on every span start — return attributes to attach */
getSpanAttributes?: () => SpanAttributes;
/** Enable experimental logging support (requires @opentelemetry/api-logs) */
enableLogging?: boolean;
}The collectorUrl follows the OTLP exporter specification — signal paths (/v1/traces, /v1/logs) are appended automatically. A custom path prefix is preserved: https://collector.example.com/mycollector becomes https://collector.example.com/mycollector/v1/traces.
Dynamic Span Attributes
getSpanAttributes is called at span creation time, so it can return values that change over the session (e.g., user identity after login):
initialize({
collectorUrl: "https://otel-collector.example.com",
serviceName: "my-app",
getSpanAttributes: () => ({
"session.id": sessionId,
"user.id": auth.currentUser?.id ?? "",
}),
});Plugins
Fetch & XHR
Auto-instruments fetch() and XMLHttpRequest. Creates a span for every request with method, URL, and status code. OTLP collector URLs are automatically excluded from tracing.
import { createFetchPlugin } from "@invinite/otel-web/plugins/fetch";
createFetchPlugin({
// Skip tracing for specific URLs (collector URLs are auto-ignored)
ignoreUrls: [/\/health/],
// Propagate W3C trace context headers for distributed tracing
propagateToUrls: [/api\.example\.com/],
});Distributed Tracing with propagateToUrls
To connect frontend traces to your backend, use propagateToUrls to specify which URLs should receive the W3C traceparent header. The fetch plugin automatically injects the header using its own span context — no manual span wrapping needed.
createFetchPlugin({
// Inject traceparent into requests matching these patterns
propagateToUrls: [/\/api\//, /api\.example\.com/],
});Your backend needs to extract the traceparent header to continue the trace. Most OpenTelemetry server SDKs do this automatically. The trace_id in the header links the frontend and backend spans into a single distributed trace.
Document Load
Emits spans for page load performance metrics from the Performance API.
import { createDocumentLoadPlugin } from "@invinite/otel-web/plugins/document-load";
createDocumentLoadPlugin();Spans emitted:
| Span | Attributes |
| --- | --- |
| document.load | DNS, connect, TLS, TTFB, response, DOM interactive, DOM content loaded, load event, transfer sizes |
| first-paint | Time to first paint |
| first-contentful-paint | Time to first contentful paint |
| largest-contentful-paint | LCP time, element, size |
SSR Trace Context
When server-side rendering, you can connect browser spans to the server trace by injecting W3C trace context meta tags into the HTML:
<meta name="traceparent" content="00-<trace-id>-<span-id>-01" />
<!-- Optional: -->
<meta name="tracestate" content="vendor=value" />The document-load plugin automatically reads these meta tags and uses them as the parent context for all page load spans. No additional configuration is needed.
TanStack Router
Creates spans for route navigations.
import { createRouterPlugin } from "@invinite/otel-web/plugins/tanstack-router";
createRouterPlugin(router, {
// Skip tracing for specific routes
ignoreRoutes: [/\/health/],
});TanStack Query
Creates spans for query fetches and mutations with lifecycle tracking.
import { createQueryPlugin } from "@invinite/otel-web/plugins/tanstack-query";
createQueryPlugin(queryClient, {
// Skip tracing for specific queries (matches against query hash)
ignoreQueries: [/health/],
// Skip tracing for specific mutations (matches against mutation key)
ignoreMutations: [/analytics/],
});Note: The query plugin traces the query/mutation lifecycle but does not instrument the underlying HTTP requests. If you need distributed tracing (i.e.,
traceparentpropagation to your backend), use both the query plugin and the fetch plugin together.
Error Handler
Captures uncaught errors and unhandled promise rejections as error spans.
import { createErrorHandlerPlugin } from "@invinite/otel-web/plugins/error-handler";
createErrorHandlerPlugin({
// Skip tracing for specific errors (matches against error message)
ignoreErrors: [/ResizeObserver loop/],
});Uses addEventListener (not direct assignment) so it won't interfere with existing error handlers.
Note: React and frameworks like TanStack Router catch rendering errors internally via error boundaries, so they never reach
window.onerror. To report these errors, hook into your framework's error boundary. For example with TanStack Router:import { SpanStatusCode, trace } from "@opentelemetry/api"; import { ErrorComponent, createRootRoute } from "@tanstack/react-router"; import { useEffect } from "react"; const RootErrorComponent = ({ error }: { error: Error }) => { useEffect(() => { const tracer = trace.getTracer("my-app"); const span = tracer.startSpan("error-boundary"); span.setAttribute("error.type", error.name); span.setAttribute("error.message", error.message); span.setAttribute("error.stack", error.stack ?? ""); span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); span.end(); }, [error]); return <ErrorComponent error={error} />; }; export const Route = createRootRoute({ errorComponent: RootErrorComponent, });
Logging (Experimental)
Note: OpenTelemetry browser logging is experimental. The API may change in future releases.
To enable logging, set enableLogging: true and install the @opentelemetry/api-logs peer dependency:
npm install @opentelemetry/api-logsconst cleanup = initialize({
collectorUrl: "https://otel-collector.example.com",
serviceName: "my-app",
enableLogging: true,
});Logs are exported to <collectorUrl>/v1/logs. Once initialized, get a logger anywhere in your app:
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
const logger = logs.getLogger("my-app");
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
body: "User completed checkout",
attributes: { "cart.items": 3 },
});Custom Spans
After initialize() is called, the global @opentelemetry/api tracer is available anywhere in your app for manual instrumentation:
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-app");
// Track a user action
const span = tracer.startSpan("checkout.submit", {
attributes: { "cart.items": 3, "cart.total": 99.99 },
});
span.setStatus({ code: SpanStatusCode.OK });
span.end();
// Record an error
const errorSpan = tracer.startSpan("payment.process");
try {
await processPayment();
errorSpan.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
errorSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
errorSpan.recordException(error);
throw error;
} finally {
errorSpan.end();
}These spans are exported to the same endpoint and carry any attributes from getSpanAttributes.
Custom Plugins
import type { OtelWebPlugin } from "@invinite/otel-web";
const myPlugin: OtelWebPlugin = {
setup(tracer) {
// Subscribe to events, start spans
},
teardown() {
// Unsubscribe, clean up
},
};License
MIT
