Integrating Observability SDKs: Sentry, Datadog RUM, and OpenTelemetry
Production browser error monitoring requires more than a global window.onerror handler. Three SDK ecosystems dominate the space — Sentry’s @sentry/browser, Datadog RUM, and the OpenTelemetry Web SDK — and each demands precise initialization sequencing, sampling configuration, and PII scrubbing before a single payload reaches an ingestion endpoint. This page covers all three. It assumes you have already established a Core JavaScript Error Handling & Boundaries foundation and are familiar with Mastering window.onerror and Global Event Listeners and Handling Unhandled Promise Rejections in Modern JS.
After completing this guide you will be able to:
- Initialize Sentry
@sentry/browserwithbeforeSendfiltering and distributed tracing - Boot Datadog RUM with
datadogRum.initand connect it to your APM backend - Configure
WebTracerProviderfrom the OpenTelemetry Web SDK and export spans via OTLP/HTTP - Apply dynamic sampling strategies without dropping critical failure signals
- Scrub PII at the SDK boundary before any payload leaves the browser
Problem Framing & Symptom Identification
The core problem is that browser runtime errors are ephemeral — once the page unloads they are gone. Without an SDK initializing before application code runs, any exception thrown during module evaluation, hydration, or script parse time is silently lost. The second problem is payload quality: raw Error objects arriving at an ingestion endpoint without symbolication context, sampling metadata, or PII scrubbing create compliance risk and debugging noise in equal measure.
Symptoms of a misconfigured SDK integration include:
- Error events in Sentry with
<anonymous>file names and no source context — indicates missing source map upload steps in CI. - Datadog RUM sessions with redacted stack traces showing
[object Object]in themessagefield — indicates a serialization issue inbeforeSend. - OpenTelemetry spans with
status: UNSETinstead ofstatus: ERROR— indicates the span exception recording API is being called after the span has already ended. - Quota exhaustion in Sentry from 100% error sampling on a high-traffic page — indicates missing
tracesSampleRateandsampleRatetuning.
All three SDKs must be initialized synchronously before any import that might throw. In bundled applications this means the SDK init call belongs in the entry point file, before any application module imports execute.
Prerequisites & Environment Setup
Versions tested against this guide:
# Sentry browser SDK
npm install @sentry/browser@8
# Datadog RUM
npm install @datadog/browser-rum@5
# OpenTelemetry Web (core + auto-instrumentation + OTLP exporter)
npm install \
@opentelemetry/sdk-trace-web@1 \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected] \
@opentelemetry/[email protected]
You also need:
- A Sentry DSN from your project settings (Settings → Client Keys)
- A Datadog Application ID and Client Token (RUM → New Application)
- An OTLP collector endpoint accepting HTTP/JSON — either a local OpenTelemetry Collector or a managed vendor endpoint
Step-by-Step Implementation
Step 1 — Initialize Sentry with beforeSend and tracing
Place this block at the very top of your bundle entry point, before any other imports that might throw.
// src/instrumentation.js — imported FIRST in src/main.js
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN, // pull from env, never hardcode
release: import.meta.env.VITE_APP_RELEASE, // correlates errors to a build
environment: import.meta.env.MODE, // 'production' | 'staging'
tracesSampleRate: 0.1, // 10% of transactions carry traces
sampleRate: 1.0, // 100% of errors are captured
beforeSend(event) {
// Drop errors originating from browser extensions
const frames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
if (frames.some(f => f.filename?.includes('chrome-extension://'))) {
return null; // returning null drops the event entirely
}
return event;
},
});
release is the most important field for production debugging. Without it, Sentry cannot locate the source maps you upload to symbolicate stack frames. The tracesSampleRate is distinct from sampleRate: the former controls performance transaction sampling, the latter controls error event capture. They are independent knobs.
Step 2 — Attach beforeSend for dynamic PII scrubbing in Sentry
A static beforeSend checking extension origins is not sufficient for privacy compliance. Add a second layer that redacts sensitive values from the event payload before transmission.
// Extend the Sentry.init call's beforeSend
beforeSend(event) {
// Scrub email addresses from exception message strings
if (event.exception?.values) {
event.exception.values = event.exception.values.map(ex => ({
...ex,
value: ex.value?.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}/g, '[email]'),
}));
}
// Scrub Authorization headers from breadcrumb fetch calls
if (event.breadcrumbs?.values) {
event.breadcrumbs.values = event.breadcrumbs.values.map(bc => {
if (bc.data?.['Authorization']) {
bc.data['Authorization'] = '[Filtered]'; // replace, not delete, to preserve structure
}
return bc;
});
}
return event;
},
beforeSend receives the fully assembled event object. Mutations applied here affect what leaves the browser — this is the correct enforcement point for GDPR Article 25 (data protection by design).
Step 3 — Initialize Datadog RUM
Datadog RUM tracks user sessions, resources, and JavaScript errors. The init call must precede any user interaction or network request you want attributed to a session.
// src/instrumentation.js (continued, or a separate file imported first)
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: import.meta.env.VITE_DD_APP_ID,
clientToken: import.meta.env.VITE_DD_CLIENT_TOKEN,
site: 'datadoghq.com', // or 'datadoghq.eu' for EU region
service: 'my-frontend-app',
env: import.meta.env.MODE,
version: import.meta.env.VITE_APP_RELEASE,
sessionSampleRate: 100, // 100% of sessions are tracked
sessionReplaySampleRate: 20, // 20% of sessions include replay
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask-user-input', // mask all form inputs by default
beforeSend(event) {
// Drop noisy ResizeObserver loop errors common in some browser extensions
if (event.type === 'error' && event.error?.message?.includes('ResizeObserver')) {
return false; // Datadog uses `false`, not `null`, to drop events
}
return true;
},
});
The defaultPrivacyLevel: 'mask-user-input' setting automatically masks text in <input>, <textarea>, and <select> elements in session replays. This is the minimum acceptable default for any application handling user data. For HIPAA-scoped applications, switch to 'mask' to mask all text content.
Step 4 — Wire Datadog RUM to APM distributed tracing
To correlate frontend errors with backend trace spans, configure allowedTracingUrls. This injects W3C traceparent headers into matching fetch and XHR requests.
datadogRum.init({
// ...existing config...
allowedTracingUrls: [
{ match: import.meta.env.VITE_API_BASE_URL, propagatorTypes: ['tracecontext'] },
],
// traceSampleRate controls what percentage of RUM sessions emit APM traces
traceSampleRate: 10, // 10% — keep in sync with backend sampling rate
});
The propagatorTypes: ['tracecontext'] value uses the W3C Trace Context standard (traceparent / tracestate headers) rather than Datadog’s proprietary x-datadog-* headers. Prefer tracecontext for interoperability when your backend also uses OpenTelemetry.
Step 5 — Bootstrap the OpenTelemetry WebTracerProvider
The OpenTelemetry Web SDK is lower-level than Sentry or Datadog RUM — it gives you a standards-based instrumentation layer but requires more assembly. The WebTracerProvider is the core object; instrumentations hook into browser APIs (fetch, XHR) via a plugin model.
// src/otel.js
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const provider = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-frontend-app',
[SemanticResourceAttributes.SERVICE_VERSION]: import.meta.env.VITE_APP_RELEASE,
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: import.meta.env.MODE,
}),
});
// OTLP/HTTP exporter — points to your collector or vendor endpoint
const exporter = new OTLPTraceExporter({
url: import.meta.env.VITE_OTLP_ENDPOINT, // e.g. https://otel.example.com/v1/traces
});
// BatchSpanProcessor buffers spans and flushes them in batches
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
// Register the provider globally so all instrumentations find it
provider.register();
// Auto-instrument fetch and XHR to create child spans under active trace context
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [new RegExp(import.meta.env.VITE_API_BASE_URL)],
}),
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [new RegExp(import.meta.env.VITE_API_BASE_URL)],
}),
],
});
BatchSpanProcessor is mandatory in browser environments — SimpleSpanProcessor flushes on every span end and will saturate network connections on high-activity pages. The propagateTraceHeaderCorsUrls regex must match your API base URL precisely or header injection is silently skipped.
Step 6 — Record error events as span exceptions in OpenTelemetry
OpenTelemetry does not automatically capture window.onerror or unhandledrejection events — you must wire them manually and record exceptions against the active span or create a dedicated error span.
// src/otel-error-bridge.js
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('browser-error-bridge', '1.0.0');
function recordGlobalError(error, source) {
const span = tracer.startSpan('browser.error'); // creates a new root span
span.recordException(error); // attaches exception event to span
span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
span.setAttribute('error.source', source);
span.end(); // must end the span or it never exports
}
window.addEventListener('error', (event) => {
if (event.error) recordGlobalError(event.error, event.filename ?? 'unknown');
});
window.addEventListener('unhandledrejection', (event) => {
const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
recordGlobalError(err, 'unhandledrejection');
});
span.recordException follows the OpenTelemetry specification: it attaches a structured event named exception with exception.type, exception.message, and exception.stacktrace attributes. The span status must be set to ERROR separately — recordException alone does not change span status.
SDK Comparison & Selection Criteria
Understanding which SDK provides what capability prevents duplicate instrumentation and redundant network payloads. The three SDKs overlap in error capture but diverge sharply in session context, distributed tracing protocol, and vendor lock-in.
| Capability | Sentry @sentry/browser |
Datadog RUM | OpenTelemetry Web |
|---|---|---|---|
| Error capture (auto) | Yes — hooks window.onerror and unhandledrejection |
Yes — same global hooks | No — requires manual bridge |
| Session context | Breadcrumbs + user scope | Full session replay + user journey | No built-in concept |
| Distributed tracing | Sentry-proprietary + W3C headers | W3C or Datadog headers | W3C standard only |
| PII scrubbing API | beforeSend callback |
beforeSend callback |
Custom span processor |
| Source map symbolication | Server-side via Sentry platform | Server-side via Datadog | Collector-side — backend dependent |
| Vendor lock-in | High — Sentry platform required | High — Datadog platform required | Low — OTLP is a standard |
| Bundle size (minified+gzip) | ~28 KB | ~35 KB | ~18 KB (core only) |
For greenfield projects that prioritize long-term portability, OpenTelemetry Web is the lowest-lock-in choice. For teams that need error lifecycle management (assignment, resolution, regression alerts) and are comfortable with vendor cost, Sentry’s issue tracker has no meaningful equivalent in the OTel ecosystem. Running Sentry for errors plus OpenTelemetry for traces is a common pattern — Sentry’s @sentry/opentelemetry bridge package makes them interoperate without double-counting errors.
Datadog RUM is most compelling when the team already uses Datadog APM for backend services. Correlating a frontend datadogRum session with a backend APM trace via traceparent headers requires zero additional tooling when both sides are Datadog. The same correlation across Sentry frontend and Datadog APM requires custom traceparent propagation code.
Production Telemetry Integration
Symbolication of minified stack traces is a prerequisite for actionable error reports from any of the three SDKs. Sentry handles symbolication server-side using uploaded .map artifacts. Review Uploading Source Maps from GitHub Actions to Sentry to integrate map uploads into your CI pipeline before promoting a release.
For Datadog RUM, source map uploads use the datadog-ci CLI:
# Upload source maps to Datadog — run after your build step, before deploy
npx datadog-ci sourcemaps upload ./dist \
--service my-frontend-app \
--release-version "$RELEASE_VERSION" \
--minified-path-prefix "https://cdn.example.com/assets/"
For OpenTelemetry, stack frame symbolication is the responsibility of your OTLP backend. If you route spans to Grafana Tempo or Jaeger, those platforms do not perform browser symbolication natively — run a symbolication proxy or pre-symbolicate stacks using the source-map npm package before emitting spans.
All three SDKs support release correlation. Align the release / version field value with your CI build identifier (git SHA or semantic version tag) and use the same identifier for source map artifact uploads. Mismatches between the release field and the artifact key cause symbolication to silently fall back to raw minified frames.
Verification & Testing
Validate SDK initialization without triggering real errors in production:
// Run this in the browser console after page load to verify Sentry is active
console.log('Sentry hub client:', !!Sentry.getCurrentHub().getClient());
// Expected output: Sentry hub client: true
// Trigger a test error — appears in Sentry under the configured release
Sentry.captureException(new Error('SDK smoke test'));
For Datadog RUM, use the browser extension “Datadog RUM Debugger” or inspect network requests to browser-intake-datadoghq.com in DevTools → Network. Each page view generates a rum request within seconds of init.
For OpenTelemetry, enable the ConsoleSpanExporter alongside OTLPTraceExporter during development:
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
// Add during development only — remove before production build
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
This prints every span to the browser console as JSON, letting you verify attribute presence and span hierarchy before routing to a real collector.
Failure Modes & Edge Cases
| Scenario | Root Cause | Fix |
|---|---|---|
Sentry events show <unknown> for all file names |
Source maps not uploaded or release tag mismatch between SDK init and sentry-cli upload |
Align release value exactly with the --release flag passed to sentry-cli sourcemaps upload |
Datadog RUM beforeSend returning false drops the event but the network request still fires |
beforeSend is called after event serialization; Datadog still initiates the request before the hook resolves in some SDK versions |
Upgrade to @datadog/[email protected]+ which evaluates beforeSend pre-serialization |
| OpenTelemetry spans never appear in the collector | provider.register() not called before registerInstrumentations or BatchSpanProcessor never flushed on page unload |
Call provider.register() before any instrumentation; add window.addEventListener('visibilitychange', () => provider.forceFlush()) |
tracesSampleRate: 1.0 causes Sentry quota exhaustion |
100% performance transaction sampling on high-traffic pages floods the ingest quota | Lower tracesSampleRate to 0.01–0.1; use tracesSampler function for per-route control |
| Cross-origin script errors arrive with empty stacks in all three SDKs | Missing crossorigin="anonymous" attribute or missing Access-Control-Allow-Origin header on script responses |
Add crossorigin="anonymous" to all script tags and configure CORS headers on the CDN origin |
Sentry beforeSend returning null still charges quota |
In older SDK versions beforeSend ran after the event was counted against the quota |
Upgrade to @sentry/browser@8+ and use beforeSendTransaction for transaction filtering |
FAQ
What is the difference between sampleRate and tracesSampleRate in Sentry?
sampleRate (0.0–1.0) controls what fraction of error events are sent to Sentry. tracesSampleRate controls what fraction of page loads start a performance transaction and attach distributed trace headers to outgoing requests. They are independent. Setting tracesSampleRate: 0 disables distributed tracing while leaving error capture at 100%.
Can I run Sentry and OpenTelemetry simultaneously without double-counting errors?
Yes, with explicit coordination. Initialize Sentry with integrations: [] to disable its default fetch/XHR instrumentation and handle network span creation through OpenTelemetry only. Use Sentry’s beforeSend exclusively for error events and OTel for traces. Sentry also ships a first-party @sentry/opentelemetry package that bridges the two via the OTel SDK natively.
Does Datadog RUM replace the need for a separate error tracking tool like Sentry? Datadog RUM captures JavaScript errors as part of session context, but its error grouping and stack trace resolution capabilities are less mature than Sentry’s issue tracker. For teams already invested in Sentry for error workflows and Datadog for infrastructure APM, running both is common: Sentry handles error lifecycle and Datadog handles full session context and distributed traces.
How do I ensure SDK initialization runs before any module that might throw?
In Vite/webpack, create a dedicated instrumentation.js file and import it as the first line of your entry file (main.js / index.js). Do not use dynamic import() for the SDK — static imports are evaluated in declaration order, but dynamic imports are scheduled as microtasks and may resolve after synchronous module evaluation errors have already occurred.