@vunet/otel-rum
v0.1.2-6.21473182407
Published
Adds OpenTelemetry tracing auto-instrumentation in the browser. Collects spans on network events and sends them to Vunet Systems.
Maintainers
Readme
Table of Contents
- Overview
- Features
- Core Components
- Architecture
- Quick Start
- Installation
- Configuration
- API Reference
- Usage Examples
- Trace Context Propagation
- Development Setup
- Browser Support
- License
- Contributing
Overview
The Vunet RUM Browser SDK is a modular Real User Monitoring solution that captures telemetry data from web applications. Built on OpenTelemetry, it provides automatic instrumentation for page loads, user interactions, network requests, and session replay.
OpenTelemetry Based: The SDK uses industry-standard OpenTelemetry APIs and protocols, ensuring compatibility with a wide range of observability backends.
How It Works
- Script Loading: The loader script is added to your page via a
<script>tag - Decide API: The SDK calls the decide API to determine sampling percentages
- Module Loading: Based on sampling, Core RUM and/or Session Replay modules are loaded
- Initialization: Modules register themselves and are initialized with configuration
- Data Collection: Telemetry data is captured and batched
- Export: Data is sent to the OTLP collector endpoint
Features
| Feature | Description | |---------|-------------| | Modular Architecture | Load only what you need - Core RUM, Session Replay, or both | | Server-Side Sampling | Decide API controls what percentage of users are monitored | | W3C Trace Context | Propagates trace headers across distributed systems | | Automatic Instrumentation | Zero-config capture of common web interactions | | Session Persistence | Maintains session IDs across page navigations | | User Mapping | Associate telemetry with user identities | | Custom Spans | Create custom traces for business-specific flows | | Error Recording | Capture and report JavaScript errors automatically |
Automatic Instrumentations
- XMLHttpRequest and Fetch APIs auto-instrumentation
- User interactions (click, submit, drop, etc.)
- Document load with fetched resources
- History API and hash change support (SPA navigation)
- Web Vitals (TTFB, FCP, LCP)
- Session ID tracking
- Long tasks with automatic context attaching
- Uncaught exceptions, unhandled rejections, document errors, and console errors
- Automatic context carrying through timers, promises, native async-await, events, observers
Core Components
Core RUM
Captures traces, spans, and performance metrics using OpenTelemetry instrumentation:
- Page load performance (TTFB, FCP, LCP)
- XHR and Fetch requests
- User interactions (clicks, inputs)
- Long tasks (>50ms)
- JavaScript errors
- Route changes (SPA navigation)
Session Replay
Records user sessions as visual replays using rrweb:
- Full DOM snapshots at session start
- Incremental DOM mutations
- Mouse movements and clicks
- Scroll positions
- Input field changes (with masking for sensitive data)
- Window resize events
- Console logs (optional)
Module Registry
Coordinates module loading with a callback-based system:
- Promise-based module waiting
- Eliminates race conditions
- Timeout handling for failed loads
- Supports parallel module loading
Architecture
Project Structure
src/
├── index.ts # Loader - Entry point, loads modules
├── core-rum/
│ ├── index.ts # Core RUM module - OpenTelemetry setup
│ ├── SessionExporter.ts # OTLP trace exporter with persistence
│ ├── sumologic-span-processor/ # Span processing and enrichment
│ ├── sumologic-logs-exporter/ # Log/error exporter
│ ├── sumologic-context-manager/ # Context propagation
│ └── FormInstrumentation.ts # Form navigation tracking
├── session-replay/
│ ├── index.ts # Session Replay module
│ └── vunet-rrweb/
│ ├── rrweb.ts # rrweb recorder and exporter
│ ├── decideApi.ts # Decide API client
│ ├── FailCounter.ts # API failure tracking
│ └── types.ts # Type definitions
├── shared/
│ ├── index.ts # Shared exports
│ ├── moduleRegistry.ts # Module ready callback system
│ ├── globals.ts # Global type declarations (window.vunetRum)
│ ├── types.ts # Shared TypeScript types
│ ├── logger.ts # Internal logger
│ └── mapUser.ts # User mapping utilities
└── utils/
└── helpers.ts # Utility functionsModule Dependency Flow
flowchart TB
subgraph Browser["Browser Window"]
Script["script tag"]
end
subgraph Loader["Loader - index.ts"]
Init["initialize()"]
DecideAPI["callDecideApi()"]
LoadScript["loadScript()"]
end
subgraph Registry["Module Registry"]
WaitFor["waitForModule()"]
Register["registerModuleReady()"]
end
subgraph CoreRUM["Core RUM Module"]
OTel["OpenTelemetry Setup"]
Tracer["TracerProvider"]
Instruments["Instrumentations"]
Exporter["OTLPTraceExporter"]
end
subgraph SessionReplay["Session Replay Module"]
RRWeb["rrweb Recorder"]
SRExporter["SessionReplayExporter"]
end
subgraph Backend["Backend"]
DecideServer["Decide API Server"]
Collector["OTLP Collector"]
end
Script --> Init
Init --> DecideAPI
DecideAPI --> DecideServer
DecideServer --> DecideAPI
Init --> LoadScript
LoadScript --> WaitFor
CoreRUM --> Register
SessionReplay --> Register
Register --> WaitFor
WaitFor --> Init
Init --> OTel
Init --> RRWeb
OTel --> Tracer
Tracer --> Instruments
Tracer --> Exporter
Exporter --> Collector
RRWeb --> SRExporter
SRExporter --> CollectorData Flow
flowchart LR
subgraph Client["Browser"]
UI["User Interactions"]
Network["Network Requests"]
Errors["JS Errors"]
DOM["DOM Changes"]
end
subgraph SDK["Vunet RUM SDK"]
CoreRUM["Core RUM"]
SR["Session Replay"]
Buffer["Span Buffer"]
SRBuffer["Replay Buffer"]
end
subgraph Export["Export Layer"]
OTLP["OTLP Exporter"]
SRExp["SR Exporter"]
end
subgraph Backend["Backend"]
Collector["OTLP Collector"]
end
UI --> CoreRUM
Network --> CoreRUM
Errors --> CoreRUM
DOM --> SR
CoreRUM --> Buffer
SR --> SRBuffer
Buffer --> OTLP
SRBuffer --> SRExp
OTLP --> Collector
SRExp --> CollectorBuild Output
The SDK is built into three separate bundles:
| File | Description | Size (approx) |
|------|-------------|---------------|
| vunet-rum-loader.js | Main loader script - always loaded first | ~15 KB |
| vunet-rum-core.js | Core RUM module - OpenTelemetry instrumentations | ~150 KB |
| vunet-rum-session-replay.js | Session Replay module - rrweb recorder | ~100 KB |
Modular Loading: Only the loader is initially loaded. Core RUM and Session Replay are loaded on-demand based on the decide API response.
Quick Start
The easiest way to start collecting traces from your website is to put the code below inside the <head></head> tags:
<script
id="vunet-rum"
src="https://cdn.vunet.ai/rum/vunet-rum-loader.js"
data-collection-source-url="https://collector.example.com/v1/traces"
data-application-name="my-app"
data-service-name="my-frontend"
></script>That's it! With properly provided configuration, your website is ready and will send collected traces to the specified collector.
Installation
Script Tag (Recommended)
Auto-initialization with Data Attributes
<script
id="vunet-rum"
src="https://cdn.vunet.ai/rum/vunet-rum-loader.js"
data-collection-source-url="https://collector.example.com/v1/traces"
data-service-name="my-frontend"
data-application-name="my-app"
data-default-attributes='{"version":"1.2.3","env":"production"}'
data-rum-logging="true"
></script>Manual Initialization
<script src="https://cdn.vunet.ai/rum/vunet-rum-loader.js"></script>
<script>
window.vunetRum.initialize({
collectionSourceUrl: 'https://collector.example.com/v1/traces',
serviceName: 'my-frontend',
applicationName: 'my-app',
propagateTraceHeaderCorsUrls: [/api\.myservice\.com/],
collectErrors: true,
});
</script>Async Loading
You can load the script asynchronously, but some functionalities like user interactions or requests made before script run will be limited:
<script>
(function (w, s, d, r, e, n) {
(w[s] = w[s] || {
readyListeners: [],
onReady: function (e) {
w[s].readyListeners.push(e);
},
}),
((e = d.createElement('script')).async = 1),
(e.src = r),
(n = d.getElementsByTagName('script')[0]).parentNode.insertBefore(e, n);
})(window, 'vunetRum', document, 'https://cdn.vunet.ai/rum/vunet-rum-loader.js');
window.vunetRum.onReady(function () {
window.vunetRum.initialize({
collectionSourceUrl: 'https://collector.example.com/v1/traces',
serviceName: 'my-frontend',
propagateTraceHeaderCorsUrls: [/api\.myservice\.com/],
collectErrors: true,
});
});
</script>NPM Installation
npm install @vunet/otel-rumimport { initialize } from '@vunet/otel-rum';
initialize({
collectionSourceUrl: 'https://collector.example.com/v1/traces',
serviceName: 'my-frontend',
applicationName: 'my-app',
propagateTraceHeaderCorsUrls: [/api\.myservice\.com/],
});Configuration
Initialize Options
All configuration options passed to initialize():
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| collectionSourceUrl | string | Yes | - | OTLP collector endpoint URL for sending telemetry |
| decideApiEndpoint | string | No | /vuSmartMaps/api/rum/ | Decide API endpoint or static config (sr:100,sp:100) |
| authorizationToken | string | No | - | Authorization header value for collector requests |
| serviceName | string | No | unknown_service | Service name for telemetry attribution |
| applicationName | string | No | - | Application name identifier |
| deploymentEnvironment | string | No | - | Environment name (e.g., 'production', 'staging') |
| defaultAttributes | object | No | {} | Custom attributes added to all spans |
| bufferMaxSpans | number | No | 2048 | Maximum spans to buffer before forcing export |
| maxExportBatchSize | number | No | 50 | Maximum spans per export batch |
| bufferTimeout | number | No | 2000 | Timeout in ms before forcing export |
| ignoreUrls | (string\|RegExp)[] | No | [] | URL patterns to exclude from instrumentation |
| propagateTraceHeaderCorsUrls | (string\|RegExp)[] | No | [] | URLs where trace headers should be propagated |
| collectSessionId | boolean | No | true | Include session ID in spans |
| collectErrors | boolean | No | false | Enable automatic error collection |
| dropSingleUserInteractionTraces | boolean | No | false | Drop traces with only one user interaction span |
| dropShortTracesMs | number | No | 0 | Drop traces shorter than this duration (0 = disabled) |
| userInteractionElementNameLimit | number | No | 20 | Max length for user interaction element names |
| rumLogging | boolean | No | false | Enable SDK debug logging to console |
Decide API Response
The decide API returns sampling configuration:
interface ApiResponseData {
sampling_percentage: number; // 0-100, controls Core RUM sampling
session_replay_percentage: number; // 0-100, controls Session Replay sampling
eventFilter: EventFilter[]; // Optional event filtering rules
}Sampling Logic
Important: Session Replay requires Core RUM to be loaded first.
| sampling_percentage | session_replay_percentage | Result | |---------------------|---------------------------|--------| | 0 | Any | Nothing loads - SDK disabled | | 100 | 0 | Only Core RUM loads | | 100 | 50 | Core RUM + Session Replay (50% of sessions recorded) | | 50 | 100 | 50% of users get Core RUM + Session Replay |
Static Decide Configuration
For testing or when you don't have a decide API server:
// Static format: sr:SESSION_REPLAY%,sp:SAMPLING%
window.vunetRum.initialize({
collectionSourceUrl: 'https://collector.example.com/v1/traces',
decideApiEndpoint: 'sr:100,sp:100', // 100% for both
});Data Attributes
Configuration can be provided via script tag data attributes:
| Data Attribute | Maps To |
|----------------|---------|
| data-collection-source-url | collectionSourceUrl |
| data-authorization-token | authorizationToken |
| data-decide-api-endpoint | decideApiEndpoint |
| data-service-name | serviceName |
| data-application-name | applicationName |
| data-default-attributes | defaultAttributes (JSON string) |
| data-buffer-max-spans | bufferMaxSpans |
| data-buffer-timeout | bufferTimeout |
| data-ignore-urls | ignoreUrls (JSON array or comma-separated) |
| data-propagate-trace-header-cors-urls | propagateTraceHeaderCorsUrls |
| data-rum-logging | rumLogging ("true" or "") |
API Reference
All methods are available under the window.vunetRum object.
initialize(options)
Initializes the SDK with the provided configuration.
await window.vunetRum.initialize({
collectionSourceUrl: 'https://collector.example.com/v1/traces',
applicationName: 'my-app',
serviceName: 'frontend',
rumLogging: true,
});mapUser(id, userData)
Associates user identity with the current session. User data is attached to all subsequent spans and session replay data.
window.vunetRum.mapUser('user-123', {
email: '[email protected]',
name: 'John Doe',
plan: 'premium',
company: 'Acme Corp',
});setDefaultAttribute(key, value)
Adds a custom attribute to all spans.
window.vunetRum.setDefaultAttribute('feature.darkMode', true);
window.vunetRum.setDefaultAttribute('experiment.variant', 'B');
window.vunetRum.setDefaultAttribute('app.version', '2.1.0');recordError(message, attributes?)
Records a custom error with optional attributes.
window.vunetRum.recordError('Payment failed', {
errorCode: 'PAYMENT_DECLINED',
amount: 99.99,
paymentMethod: 'credit_card',
});getCurrentSessionId()
Returns the current session ID (UUID format).
const sessionId = window.vunetRum.getCurrentSessionId();
console.log('Session:', sessionId);
// Output: "abc123-def456-789..."onReady(callback)
Registers a callback to be invoked when the SDK is fully initialized.
window.vunetRum.onReady((getConfigOverrides) => {
console.log('SDK is ready!');
window.vunetRum.setDefaultAttribute('ready', true);
});disableInstrumentations()
Stops all automatic instrumentations.
window.vunetRum.disableInstrumentations();
// ... perform sensitive operation ...
window.vunetRum.registerInstrumentations();registerInstrumentations()
Re-enables all automatic instrumentations after they were disabled.
window.vunetRum.registerInstrumentations();stopProcessing()
Completely stops all SDK processing. This cannot be undone - page reload required to restart.
window.vunetRum.stopProcessing();tracer
OpenTelemetry Tracer instance for creating custom spans.
window.vunetRum.tracer.startActiveSpan('custom-operation', (span) => {
span.setAttribute('custom.attribute', 'value');
// Do work...
span.end();
});api
OpenTelemetry API instance for context and propagation.
const { context, trace } = window.vunetRum.api;
const activeSpan = trace.getActiveSpan();
const ctx = context.active();Usage Examples
User Identification
// After user logs in
function onUserLogin(user) {
window.vunetRum.mapUser(user.id, {
email: user.email,
name: user.name,
plan: user.subscription?.plan || 'free',
company: user.company?.name,
role: user.role,
});
}
// After user logs out
function onUserLogout() {
window.vunetRum.mapUser('anonymous', {});
}Custom Spans for Business Logic
async function checkout(cart) {
return window.vunetRum.tracer.startActiveSpan('checkout', async (span) => {
try {
span.setAttribute('cart.items', cart.items.length);
span.setAttribute('cart.total', cart.total);
// Nested span for payment
const result = await window.vunetRum.tracer.startActiveSpan('process-payment', async (paymentSpan) => {
paymentSpan.setAttribute('payment.method', cart.paymentMethod);
const res = await processPayment(cart);
paymentSpan.setAttribute('payment.transactionId', res.transactionId);
paymentSpan.end();
return res;
});
span.setAttribute('checkout.success', true);
span.setAttribute('order.id', result.orderId);
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2 }); // ERROR
throw error;
} finally {
span.end();
}
});
}Feature Flags & A/B Testing
function initializeFeatureFlags(flags) {
Object.entries(flags).forEach(([key, value]) => {
window.vunetRum.setDefaultAttribute(`feature.${key}`, value);
});
}
function trackExperiment(experimentName, variant) {
window.vunetRum.setDefaultAttribute(`experiment.${experimentName}`, variant);
}
// Example usage
trackExperiment('checkout_redesign', 'variant_b');React Integration
// src/rum.js
export function initializeRUM(config) {
return window.vunetRum.initialize({
collectionSourceUrl: config.collectorUrl,
applicationName: config.appName,
serviceName: 'react-frontend',
defaultAttributes: {
'app.version': config.version,
'react.version': React.version,
},
});
}
// src/App.jsx
import { useEffect } from 'react';
import { initializeRUM } from './rum';
function App() {
useEffect(() => {
initializeRUM({
collectorUrl: process.env.REACT_APP_COLLECTOR_URL,
appName: process.env.REACT_APP_NAME,
version: process.env.REACT_APP_VERSION,
});
}, []);
return <YourApp />;
}Session Correlation with Support
function getSupportContext() {
return {
sessionId: window.vunetRum.getCurrentSessionId(),
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
}
function reportBug(description) {
const context = getSupportContext();
return fetch('/api/support/ticket', {
method: 'POST',
body: JSON.stringify({ description, ...context }),
});
}Trace Context Propagation
By default, trace context propagation for cross-origin requests is not enabled due to browser CORS security restrictions.
Enabling CORS Trace Propagation
Set exact URLs or URL patterns in propagateTraceHeaderCorsUrls:
window.vunetRum.initialize({
collectionSourceUrl: 'https://collector.example.com/v1/traces',
propagateTraceHeaderCorsUrls: [
/^https:\/\/api\.myservice\.com\/.*/,
/^http:\/\/localhost:3000\/api\/.*/,
],
});You must configure your server to accept and return these CORS headers:
Access-Control-Allow-Headers: traceparent, tracestateRead W3C Trace Context for more details.
Baggage
Baggage is contextual information passed between spans - a key-value store that resides alongside span context:
const baggage =
window.vunetRum.api.propagation.getBaggage(
window.vunetRum.api.context.active(),
) || window.vunetRum.api.propagation.createBaggage();
baggage.setEntry('customerId', { value: 'customer-id-value' });
window.vunetRum.api.propagation.setBaggage(
window.vunetRum.api.context.active(),
baggage,
);Development Setup
Prerequisites
- Node.js: Version >= 18.0.0 (recommended: 20.19.4)
- npm: Version >= 9.0.0
Quick Start (Automated)
# Clone with submodules
git clone --recurse-submodules [email protected]:vunet-systems/vunet-rum-browser-sdk.git
cd vunet-rum-browser-sdk
# Run setup script
./setup.sh
# Build
npm run buildManual Setup
# Clone repository
git clone --recurse-submodules [email protected]:vunet-systems/vunet-rum-browser-sdk.git
cd vunet-rum-browser-sdk
# If cloned without submodules
git submodule update --init --recursive
# Install dependencies
npm install
# Build
npm run buildGit Submodules
This project uses Git submodules for:
src/opentelemetry-jssrc/opentelemetry-js-contrib
Running Tests
# Run all tests
npm test
# Unit tests only
npm run test:ut
# End-to-end tests only
npm run test:e2eDevelopment Workflow
# Linting
npm run eslint-fix
# Formatting
npm run prettier-format-all
# Type checking
npm run typesTroubleshooting
Submodule Issues:
git submodule deinit --all -f
git submodule update --init --recursiveNode Version Mismatch:
nvm use # Uses version from .nvmrcDependency Issues:
rm -rf node_modules package-lock.json
npm installFor detailed setup instructions, see HOW_TO_RUN.md.
Browser Support
The SDK supports all modern browsers:
| Browser | Minimum Version | |---------|-----------------| | Chrome | 60+ | | Firefox | 55+ | | Safari | 12+ | | Edge | 79+ |
Note: Internet Explorer is not supported. Some features may have limited functionality in older browser versions.
License
This project is released under the Apache 2.0 License.
Getting Started
Please refer to our How to Run documentation to set up and run the project on your local machine.
Contributing
Please refer to our Code of Conduct for contribution guidelines.
For detailed documentation, see the docs/ folder.
