Browser SDK
Frontend telemetry with session replay, Web Vitals, and error capture — powered by OpenTelemetry sdk-trace-web
Installation
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.
Zero-Config Setup (Recommended)
One import. No configuration object. The SDK reads your environment variables automatically.
Set these environment variables (Vite example shown, but NEXT_PUBLIC_ and REACT_APP_ prefixes also work):
| Variable | Required | Default | Description |
|---|---|---|---|
VITE_OBTRACE_API_KEY | Yes | - | Your Obtrace API key |
VITE_OBTRACE_SERVICE_NAME | No | "web-app" | Connected app name for the project, or an explicit alias configured for that app |
VITE_OBTRACE_APP_ID | No | - | Override for the canonical project app name when you need to pin it explicitly |
VITE_OBTRACE_ENV | No | NODE_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:
React Setup
For React apps that want typed helper functions:
The React wrapper also exports convenience functions that work without holding a reference to the SDK instance:
The obtrace() call also patches window.fetch globally.
Manual Setup
For full control over configuration:
Import ob wherever you need it. Initialize once at app startup, before any user interaction.
initBrowserSDK returns an object with these methods:
| Method | Purpose |
|---|---|
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 |
sessionId | The 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:
| What | How | Needs code? |
|---|---|---|
| Fetch requests | OTel fetch instrumentation with spans, status, and trace propagation | No |
| XMLHttpRequest | OTel XMLHttpRequest instrumentation for legacy AJAX calls | No |
| Document load | OTel document-load instrumentation captures page load performance | No |
| User interactions | OTel user-interaction instrumentation tracks clicks and other events | No |
| Web Vitals | FCP, LCP, CLS, INP, TTFB measured via PerformanceObserver | No |
| Console output | console.* calls intercepted and forwarded as logs | No |
| Errors | window.onerror and unhandled promise rejections captured | No |
| Session replay | DOM mutations, mouse movements, scrolls, and inputs recorded (obtrace-specific) | No |
| Navigation | Route changes via pushState/replaceState/popstate captured | No |
Web Vitals
Measured via PerformanceObserver and sent as metrics:
| Vital | Metric name | Unit |
|---|---|---|
| First Contentful Paint | web_vital_fcp_ms | ms |
| Largest Contentful Paint | web_vital_lcp_ms | ms |
| Cumulative Layout Shift | web_vital_cls | 1 |
| Interaction to Next Paint | web_vital_inp_ms | ms |
| Time to First Byte | web_vital_ttfb_ms | ms |
Error Capture
window.onerrorevents are logged aterrorlevel with filename, line, and column- Unhandled promise rejections are logged at
errorlevel 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-blockare excluded from recording - Text in elements with the CSS class
ob-maskis masked
Navigation Tracking
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, usessendBeaconfor 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.
Errors
captureException(error, context?) extracts the error name and message and logs it at error level. Session and trace context are attached automatically.
Metrics
metric(name, value, unit?, context?) records a numeric value. Use it for performance and business measurements.
Custom Replay Events
captureReplayEvent(type, payload) attaches a custom event to the session replay timeline. Use it to mark significant user actions.
Context Object
Every manual call accepts an optional context object:
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:
Every instrumented fetch call produces:
- A span named
browser.fetch POST(or whatever the method is) withhttp.method,http.url,http.status_code,http.duration_ms, andreplay_id - A log entry with the request outcome
- A
networkreplay event (visible in the session replay timeline) - A recipe step for request replay (used by Obtrace's replay analysis)
- Automatic W3C
traceparentheader injection for backend trace correlation
Failed requests also produce an error log and a network_error replay event.
Configuration Reference
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
apiKey | string | Yes | - | Your Obtrace API key |
serviceName | string | Yes | - | Connected app name in the project, or an explicit alias configured for that app |
tenantId | string | No | - | Your tenant ID (auto-resolved from API key if not set) |
projectId | string | No | - | Your project ID (auto-resolved from API key if not set) |
appId | string | No | - | Canonical project app name when you need to override the inferred value explicitly |
env | string | No | - | Deployment environment (production, staging, etc.) |
serviceVersion | string | No | - | Version string for this deployment |
debug | boolean | No | false | Log SDK errors and warnings to console |
flushIntervalMs | number | No | 2000 | How often the SDK flushes queued telemetry |
maxQueueSize | number | No | 1000 | Max items in the send queue (oldest dropped when full) |
requestTimeoutMs | number | No | 5000 | Timeout per OTLP request |
validateSemanticMetrics | boolean | No | false | Warn on non-canonical metric names (requires debug: true) |
replay.enabled | boolean | No | true | Enable session replay recording |
replay.flushIntervalMs | number | No | 5000 | How often replay chunks are sent |
replay.maxChunkBytes | number | No | 480000 | Max size per replay chunk before auto-flush |
replay.captureNetworkRecipes | boolean | No | true | Capture HTTP requests as replayable recipe steps |
replay.sessionStorageKey | string | No | "obtrace_session_id" | localStorage key for session ID persistence |
replay.blockClass | string | No | "ob-block" | CSS class to exclude elements from replay |
replay.maskTextClass | string | No | "ob-mask" | CSS class to mask text content in replay |
replay.maskAllInputs | boolean | No | true | Mask all input field values in replay |
replay.sampling.mousemove | boolean | number | No | true | Capture mouse movement (or sampling interval in ms) |
replay.sampling.scroll | number | No | 150 | Scroll sampling interval in ms |
replay.sampling.input | "all" | "last" | No | "last" | Capture all input changes or only the last value |
vitals.enabled | boolean | No | true | Collect Web Vitals metrics |
vitals.reportAllChanges | boolean | No | false | Report every vital measurement (not just the final value) |
propagation.enabled | boolean | No | true | Inject traceparent header on outbound fetch requests |
propagation.headerName | string | No | "traceparent" | Header name for trace propagation |
propagation.sessionHeaderName | string | No | "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
errorlog 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/logsPOST /otlp/v1/tracesPOST /otlp/v1/metricsPOST /ingest/replay/chunkPOST /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:
To mask text content while preserving layout:
Sensitive input fields (password, email, phone, token, credit card) are automatically redacted regardless of CSS classes.