Browser SDK

Frontend telemetry with session replay, Web Vitals, and error capture — powered by OpenTelemetry sdk-trace-web

Installation

npm install @obtrace/sdk-browser

Built on OpenTelemetry

The Browser SDK uses @opentelemetry/sdk-trace-web under the hood to instrument fetch, XMLHttpRequest, document loads, and user interactions. This gives you distributed tracing from the browser to your backend services with W3C trace context propagation.

Session replay, Web Vitals collection, and console capture remain obtrace-specific features layered on top of the OTel foundation.

One import. No configuration object. The SDK reads your environment variables automatically.

src/main.ts
import '@obtrace/sdk-browser/auto'

Set these environment variables (Vite example shown, but NEXT_PUBLIC_ and REACT_APP_ prefixes also work):

VariableRequiredDefaultDescription
VITE_OBTRACE_API_KEYYes-Your Obtrace API key
VITE_OBTRACE_SERVICE_NAMENo"web-app"Connected app name for the project, or an explicit alias configured for that app
VITE_OBTRACE_APP_IDNo-Override for the canonical project app name when you need to pin it explicitly
VITE_OBTRACE_ENVNoNODE_ENV or "production"Deployment environment

Obtrace treats the project app name as the canonical identity. Example: if the project app is core and it has alias web, browser traffic may send serviceName: "web", but ingest normalizes it to core. Names outside the configured app name and aliases are rejected.

The /auto entry point does everything: initializes the SDK, patches window.fetch globally so all HTTP calls are instrumented, captures errors, console output, Web Vitals, and session replay.

To access the SDK instance later:

import { getObtrace } from '@obtrace/sdk-browser/auto'
 
const ob = getObtrace()
ob?.log("info", "checkout.started")

React Setup

For React apps that want typed helper functions:

src/obtrace.ts
import { obtrace } from '@obtrace/sdk-browser/react'
 
export const ob = obtrace({
  apiKey: import.meta.env.VITE_OBTRACE_API_KEY,
  serviceName: "core",
})

The React wrapper also exports convenience functions that work without holding a reference to the SDK instance:

import { obtraceLog, obtraceMetric, obtraceError } from '@obtrace/sdk-browser/react'
 
obtraceLog("info", "page.loaded")
obtraceMetric("search.results", 42)
obtraceError(new Error("something broke"))

The obtrace() call also patches window.fetch globally.

Manual Setup

For full control over configuration:

src/obtrace.ts
import { initBrowserSDK } from "@obtrace/sdk-browser/browser";
 
export const ob = initBrowserSDK({
  apiKey: import.meta.env.VITE_OBTRACE_API_KEY,
  serviceName: "core",
});

Import ob wherever you need it. Initialize once at app startup, before any user interaction.

initBrowserSDK returns an object with these methods:

MethodPurpose
log(level, message, context?)Send a structured log
captureException(error, context?)Capture an error with context
captureError(error, context?)Alias for captureException
metric(name, value, unit?, context?)Record a numeric metric
instrumentFetch()Returns a fetch wrapper that auto-creates spans
captureReplayEvent(type, payload)Attach a custom event to the session replay
flushReplay()Force-flush the replay buffer
shutdown()Flush everything and stop the SDK
sessionIdThe current session ID (string, not a method)

What Gets Captured Automatically

After calling initBrowserSDK, the SDK enables OpenTelemetry browser instrumentations and installs obtrace-specific hooks with zero additional code from you:

WhatHowNeeds code?
Fetch requestsOTel fetch instrumentation with spans, status, and trace propagationNo
XMLHttpRequestOTel XMLHttpRequest instrumentation for legacy AJAX callsNo
Document loadOTel document-load instrumentation captures page load performanceNo
User interactionsOTel user-interaction instrumentation tracks clicks and other eventsNo
Web VitalsFCP, LCP, CLS, INP, TTFB measured via PerformanceObserverNo
Console outputconsole.* calls intercepted and forwarded as logsNo
Errorswindow.onerror and unhandled promise rejections capturedNo
Session replayDOM mutations, mouse movements, scrolls, and inputs recorded (obtrace-specific)No
NavigationRoute changes via pushState/replaceState/popstate capturedNo

Web Vitals

Measured via PerformanceObserver and sent as metrics:

VitalMetric nameUnit
First Contentful Paintweb_vital_fcp_msms
Largest Contentful Paintweb_vital_lcp_msms
Cumulative Layout Shiftweb_vital_cls1
Interaction to Next Paintweb_vital_inp_msms
Time to First Byteweb_vital_ttfb_msms

Error Capture

  • window.onerror events are logged at error level with filename, line, and column
  • Unhandled promise rejections are logged at error level with the rejection reason

Console Capture

All console.debug, console.info, console.warn, and console.error calls are intercepted and forwarded as logs at the matching level. The original console output still works normally.

Session Replay

Enabled by default. Uses rrweb to record DOM mutations, mouse movements, scrolls, and input events. Recordings are chunked and sent to /ingest/replay/chunk.

Privacy defaults:

  • All inputs are masked
  • Password, email, phone, token, and credit card fields are redacted
  • Elements with the CSS class ob-block are excluded from recording
  • Text in elements with the CSS class ob-mask is masked

Route changes via pushState, replaceState, popstate, and hashchange are captured as replay events with the current URL and page title.

Page Lifecycle

The SDK automatically flushes telemetry and replay data when:

  • The page becomes hidden (visibilitychange)
  • The page is about to unload (beforeunload, uses sendBeacon for reliability)

Adding Custom Telemetry

Logs

log(level, message, context?) sends a structured log. Levels: debug, info, warn, error, fatal. The current sessionId is automatically attached.

src/components/Checkout.tsx
import { ob } from "../obtrace";
 
function handleSubmit(cart: Cart) {
  ob.log("info", "checkout.started", {
    attrs: { item_count: cart.items.length, total: cart.total },
  });
}

Errors

captureException(error, context?) extracts the error name and message and logs it at error level. Session and trace context are attached automatically.

try {
  await submitOrder(cart);
} catch (err) {
  ob.captureException(err, {
    attrs: { step: "payment", cart_id: cart.id },
  });
}

Metrics

metric(name, value, unit?, context?) records a numeric value. Use it for performance and business measurements.

const start = performance.now();
await loadProductCatalog();
ob.metric("catalog.load_ms", performance.now() - start, "ms");
 
ob.metric("search.results_count", results.length);

Custom Replay Events

captureReplayEvent(type, payload) attaches a custom event to the session replay timeline. Use it to mark significant user actions.

ob.captureReplayEvent("user_action", {
  action: "added_to_cart",
  product_id: product.id,
  price: product.price,
});

Context Object

Every manual call accepts an optional context object:

interface SDKContext {
  traceId?: string;
  spanId?: string;
  traceState?: string;
  baggage?: string;
  sessionId?: string;
  routeTemplate?: string;
  endpoint?: string;
  method?: string;
  statusCode?: number;
  attrs?: Record<string, string | number | boolean>;
}

The sessionId is auto-injected by the browser SDK. The attrs field is for arbitrary key-value pairs.

HTTP Instrumentation

If you use the /auto or /react entry point, window.fetch is patched globally. Every fetch() call in your app is automatically instrumented with no extra code.

If you use the manual initBrowserSDK entry point, fetch is also patched by default (controlled by the instrumentGlobalFetch config option). You can also get an instrumented fetch wrapper explicitly:

src/api.ts
import { ob } from "./obtrace";
 
const fetcher = ob.instrumentFetch();

Every instrumented fetch call produces:

  • A span named browser.fetch POST (or whatever the method is) with http.method, http.url, http.status_code, http.duration_ms, and replay_id
  • A log entry with the request outcome
  • A network replay event (visible in the session replay timeline)
  • A recipe step for request replay (used by Obtrace's replay analysis)
  • Automatic W3C traceparent header injection for backend trace correlation

Failed requests also produce an error log and a network_error replay event.

Configuration Reference

OptionTypeRequiredDefaultDescription
apiKeystringYes-Your Obtrace API key
serviceNamestringYes-Connected app name in the project, or an explicit alias configured for that app
tenantIdstringNo-Your tenant ID (auto-resolved from API key if not set)
projectIdstringNo-Your project ID (auto-resolved from API key if not set)
appIdstringNo-Canonical project app name when you need to override the inferred value explicitly
envstringNo-Deployment environment (production, staging, etc.)
serviceVersionstringNo-Version string for this deployment
debugbooleanNofalseLog SDK errors and warnings to console
flushIntervalMsnumberNo2000How often the SDK flushes queued telemetry
maxQueueSizenumberNo1000Max items in the send queue (oldest dropped when full)
requestTimeoutMsnumberNo5000Timeout per OTLP request
validateSemanticMetricsbooleanNofalseWarn on non-canonical metric names (requires debug: true)
replay.enabledbooleanNotrueEnable session replay recording
replay.flushIntervalMsnumberNo5000How often replay chunks are sent
replay.maxChunkBytesnumberNo480000Max size per replay chunk before auto-flush
replay.captureNetworkRecipesbooleanNotrueCapture HTTP requests as replayable recipe steps
replay.sessionStorageKeystringNo"obtrace_session_id"localStorage key for session ID persistence
replay.blockClassstringNo"ob-block"CSS class to exclude elements from replay
replay.maskTextClassstringNo"ob-mask"CSS class to mask text content in replay
replay.maskAllInputsbooleanNotrueMask all input field values in replay
replay.sampling.mousemoveboolean | numberNotrueCapture mouse movement (or sampling interval in ms)
replay.sampling.scrollnumberNo150Scroll sampling interval in ms
replay.sampling.input"all" | "last"No"last"Capture all input changes or only the last value
vitals.enabledbooleanNotrueCollect Web Vitals metrics
vitals.reportAllChangesbooleanNofalseReport every vital measurement (not just the final value)
propagation.enabledbooleanNotrueInject traceparent header on outbound fetch requests
propagation.headerNamestringNo"traceparent"Header name for trace propagation
propagation.sessionHeaderNamestringNo"x-obtrace-session-id"Header name for session ID propagation

Validation Checklist

After deploying, verify these:

  • Open the Obtrace UI and confirm logs appear under the canonical project app for this frontend
  • Check that Web Vitals metrics (web_vital_lcp_ms, etc.) appear after a page load
  • Trigger a JavaScript error in a test environment -- an error log should appear with file and line info
  • Use the instrumented fetch to make an API call -- a span and network replay event should appear
  • Navigate between pages -- replay events with the new URL should appear
  • Open the session replay viewer and confirm the recording plays back correctly
  • Verify your ingest endpoint allows CORS from your frontend origin
  • Check that sensitive inputs (passwords, emails) are masked in the replay

CORS

Your Obtrace ingest endpoint must accept requests from your frontend origin. The SDK sends to these endpoints:

  • POST /otlp/v1/logs
  • POST /otlp/v1/traces
  • POST /otlp/v1/metrics
  • POST /ingest/replay/chunk
  • POST /ingest/replay/recipes

If you see failed requests in the browser console, check that your ingest-edge service has the correct CORS configuration for your domain.

Privacy Controls

To exclude a DOM element from session replay, add the ob-block class:

<div class="ob-block">
  This content will not appear in replays.
</div>

To mask text content while preserving layout:

<p class="ob-mask">
  This text will be replaced with asterisks in replays.
</p>

Sensitive input fields (password, email, phone, token, credit card) are automatically redacted regardless of CSS classes.