Browser JavaScript
Browser JavaScript OpenTelemetry SDK¶
This guide covers how to instrument browser-based JavaScript applications with OpenTelemetry to send traces to Sematext Tracing. This includes React, Vue.js, Angular, and vanilla JavaScript applications.
Overview¶
Browser tracing helps you:
- Monitor frontend performance and user interactions
- Track API calls and network requests to backend services
- Connect frontend traces with backend traces for end-to-end visibility
- Identify performance bottlenecks in user workflows
Instrumentation Approach¶
Browser JavaScript applications support both auto-instrumentation and manual instrumentation:
Auto-Instrumentation (Recommended):
- Automatically instruments fetch requests, XMLHttpRequest, and DOM interactions
- Uses
getWebAutoInstrumentations()
for zero-code tracing of common browser APIs - Perfect for tracking API calls, page navigation, and user interactions
- Minimal setup - just configuration and initialization
Manual Instrumentation:
- Create custom spans for specific business logic or user actions
- Add detailed attributes and events to spans
- Fine-grained control over what gets traced
- Best for complex user workflows and custom metrics
This guide covers both approaches, starting with auto-instrumentation for quick setup, then showing manual instrumentation for advanced use cases.
Prerequisites¶
- A Sematext Tracing App (create one here)
- Sematext Agent running with OpenTelemetry support
- One of the proxy approaches configured (see Sending Traces from Browser)
Sending Traces from Browser¶
Browser applications require a proxy to send traces to the Sematext Agent due to browser security restrictions (CORS policy). Here are the available approaches:
1. Web Server Proxy (Production Recommended)¶
Configure your web server to proxy OTLP requests to the Sematext Agent.
Nginx Configuration:
server {
listen 80;
server_name yourdomain.com;
# Serve your frontend application
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
# Proxy traces to Sematext Agent
location /v1/traces {
proxy_pass http://localhost:4338/v1/traces;
proxy_set_header Content-Type application/x-protobuf;
proxy_set_header Host $host;
# Enable CORS for browser requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type';
}
}
}
Apache Configuration:
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/html
# Serve frontend application
<Directory /var/www/html>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Proxy traces to Sematext Agent
ProxyPass /v1/traces http://localhost:4338/v1/traces
ProxyPassReverse /v1/traces http://localhost:4338/v1/traces
# Enable CORS
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type"
</VirtualHost>
2. Backend API Proxy (Most Flexible)¶
Create an API endpoint in your backend that forwards traces to the agent.
Node.js/Express Example:
// routes/traces.js
const express = require('express');
const fetch = require('node-fetch');
const router = express.Router();
router.post('/api/traces', async (req, res) => {
try {
// Optional: Add server-side filtering or validation
const traceData = req.body;
// Forward to Sematext Agent
const response = await fetch('http://localhost:4338/v1/traces', {
method: 'POST',
headers: {
'Content-Type': 'application/x-protobuf',
},
body: req.body
});
if (response.ok) {
res.status(200).json({ success: true });
} else {
console.error('Failed to forward traces:', response.statusText);
res.status(500).json({ error: 'Failed to send traces' });
}
} catch (error) {
console.error('Trace proxy error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;
Python/Flask Example:
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
@app.route('/api/traces', methods=['POST', 'OPTIONS'])
def proxy_traces():
if request.method == 'OPTIONS':
# Handle CORS preflight
response = jsonify({'status': 'ok'})
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
response.headers.add('Access-Control-Allow-Methods', 'POST')
return response
try:
# Forward traces to Sematext Agent
response = requests.post(
'http://localhost:4338/v1/traces',
data=request.data,
headers={'Content-Type': 'application/x-protobuf'}
)
if response.status_code == 200:
result = jsonify({'success': True})
result.headers.add('Access-Control-Allow-Origin', '*')
return result
else:
return jsonify({'error': 'Failed to send traces'}), 500
except Exception as e:
print(f"Trace proxy error: {e}")
return jsonify({'error': 'Internal server error'}), 500
3. Development Proxy Setup¶
For local development, configure your development server to proxy trace requests.
React (Create React App) - Package.json:
{
"name": "your-react-app",
"proxy": "http://localhost:4338",
"scripts": {
"start": "react-scripts start"
}
}
React (Vite) - vite.config.js:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/v1/traces': {
target: 'http://localhost:4338',
changeOrigin: true,
}
}
}
})
Vue.js (Vue CLI) - vue.config.js:
module.exports = {
devServer: {
proxy: {
'/v1/traces': {
target: 'http://localhost:4338',
changeOrigin: true,
}
}
}
}
Angular - proxy.conf.json:
Then update angular.json:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json"
}
}
Basic Browser Setup¶
1. Install Dependencies¶
npm install @opentelemetry/sdk-web
npm install @opentelemetry/auto-instrumentations-web
npm install @opentelemetry/exporter-trace-otlp-http
2. Basic Configuration¶
Create a tracing initialization file:
// tracing.js
import { WebTracerProvider } from '@opentelemetry/sdk-web';
import { Resource } from '@opentelemetry/resources';
import { BatchSpanProcessor } from '@opentelemetry/sdk-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
// Determine the endpoint based on your proxy setup
const getTraceEndpoint = () => {
if (process.env.NODE_ENV === 'development') {
// For development with proxy
return '/v1/traces';
} else {
// For production (adjust based on your setup)
return '/v1/traces'; // Web server proxy
// OR return '/api/traces'; // Backend API proxy
}
};
const resource = new Resource({
'service.name': 'your-frontend-app',
'service.version': process.env.REACT_APP_VERSION || '1.0.0',
'deployment.environment': process.env.NODE_ENV || 'development',
});
const provider = new WebTracerProvider({
resource: resource,
});
const exporter = new OTLPTraceExporter({
url: getTraceEndpoint(),
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
// Register auto-instrumentations
registerInstrumentations({
instrumentations: [getWebAutoInstrumentations({
// Configure instrumentations as needed
'@opentelemetry/instrumentation-fetch': {
propagateTraceHeaderCorsUrls: [
/.*/ // Propagate trace headers to all URLs
],
},
'@opentelemetry/instrumentation-xml-http-request': {
propagateTraceHeaderCorsUrls: [
/.*/
],
},
})],
});
React Applications¶
Setup in React¶
For React applications, add the tracing initialization at the very beginning of your app:
// index.js or main.jsx
import './tracing'; // Import tracing first - IMPORTANT!
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Manual Instrumentation in React¶
For custom spans in React components:
// components/UserDashboard.jsx
import React, { useEffect, useState } from 'react';
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('frontend-app');
function UserDashboard({ userId }) {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadUserData = async () => {
const span = tracer.startSpan('load-user-data');
span.setAttributes({
'user.id': userId,
'component': 'UserDashboard'
});
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
span.setAttributes({
'http.status_code': response.status,
'user.name': data.name
});
setUserData(data);
span.setStatus({ code: 1 }); // OK
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message }); // ERROR
} finally {
setLoading(false);
span.end();
}
};
loadUserData();
}, [userId]);
if (loading) return <div>Loading...</div>;
return (
<div>
{userData && <h1>Welcome, {userData.name}!</h1>}
</div>
);
}
export default UserDashboard;
React Hooks for Tracing¶
Create a custom hook for easier tracing:
// hooks/useTracing.js
import { useCallback } from 'react';
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('frontend-app');
export function useTracing() {
const traceOperation = useCallback(async (name, operation, attributes = {}) => {
const span = tracer.startSpan(name);
span.setAttributes(attributes);
try {
const result = await operation();
span.setStatus({ code: 1 }); // OK
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
}, []);
const traceUserAction = useCallback((actionName, attributes = {}) => {
const span = tracer.startSpan(`user-action.${actionName}`);
span.setAttributes({
'user.action': actionName,
...attributes
});
span.end();
}, []);
return { traceOperation, traceUserAction };
}
Usage in components:
// components/ProductList.jsx
import React, { useEffect, useState } from 'react';
import { useTracing } from '../hooks/useTracing';
function ProductList() {
const [products, setProducts] = useState([]);
const { traceOperation, traceUserAction } = useTracing();
useEffect(() => {
traceOperation(
'fetch-products',
async () => {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
return data;
},
{ 'component': 'ProductList' }
);
}, [traceOperation]);
const handleProductClick = (product) => {
traceUserAction('product-click', {
'product.id': product.id,
'product.category': product.category
});
// Handle click logic
};
return (
<div>
{products.map(product => (
<div key={product.id} onClick={() => handleProductClick(product)}>
{product.name}
</div>
))}
</div>
);
}
Vue.js Applications¶
Setup in Vue 3¶
// main.js
import './tracing'; // Import tracing first - IMPORTANT!
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
Vue Composition API with Tracing¶
// composables/useTracing.js
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('vue-frontend-app');
export function useTracing() {
const traceAsync = async (spanName, asyncFn, attributes = {}) => {
const span = tracer.startSpan(spanName);
span.setAttributes(attributes);
try {
const result = await asyncFn();
span.setStatus({ code: 1 });
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
};
const traceUserEvent = (eventName, attributes = {}) => {
const span = tracer.startSpan(`user-event.${eventName}`);
span.setAttributes({
'event.type': eventName,
...attributes
});
span.end();
};
return { traceAsync, traceUserEvent };
}
Usage in Vue components:
// components/ProductCatalog.vue
<template>
<div>
<div v-if="loading">Loading products...</div>
<div v-else>
<product-item
v-for="product in products"
:key="product.id"
:product="product"
@click="handleProductClick(product)"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useTracing } from '../composables/useTracing';
import ProductItem from './ProductItem.vue';
const products = ref([]);
const loading = ref(true);
const { traceAsync, traceUserEvent } = useTracing();
const handleProductClick = (product) => {
traceUserEvent('product-view', {
'product.id': product.id,
'product.name': product.name
});
};
onMounted(async () => {
await traceAsync(
'load-product-catalog',
async () => {
const response = await fetch('/api/products');
products.value = await response.json();
loading.value = false;
},
{ 'component': 'ProductCatalog' }
);
});
</script>
Angular Applications¶
Setup in Angular¶
// main.ts
import './tracing'; // Import tracing first - IMPORTANT!
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
Angular Service for Tracing¶
// services/tracing.service.ts
import { Injectable } from '@angular/core';
import { trace } from '@opentelemetry/api';
@Injectable({
providedIn: 'root'
})
export class TracingService {
private tracer = trace.getTracer('angular-frontend-app');
async traceOperation<T>(
spanName: string,
operation: () => Promise<T>,
attributes: Record<string, string | number> = {}
): Promise<T> {
const span = this.tracer.startSpan(spanName);
span.setAttributes(attributes);
try {
const result = await operation();
span.setStatus({ code: 1 }); // OK
return result;
} catch (error: any) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
}
traceUserInteraction(interaction: string, attributes: Record<string, any> = {}) {
const span = this.tracer.startSpan(`user-interaction.${interaction}`);
span.setAttributes({
'interaction.type': interaction,
...attributes
});
span.end();
}
}
Usage in Angular components:
// components/user-profile.component.ts
import { Component, OnInit } from '@angular/core';
import { TracingService } from '../services/tracing.service';
import { UserService } from '../services/user.service';
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<button (click)="updateProfile()">Update Profile</button>
</div>
<div *ngIf="loading">Loading user profile...</div>
`
})
export class UserProfileComponent implements OnInit {
user: any = null;
loading = true;
constructor(
private tracingService: TracingService,
private userService: UserService
) {}
async ngOnInit() {
await this.tracingService.traceOperation(
'load-user-profile',
async () => {
this.user = await this.userService.getCurrentUser();
this.loading = false;
},
{ 'component': 'UserProfileComponent' }
);
}
async updateProfile() {
this.tracingService.traceUserInteraction('update-profile-click', {
'user.id': this.user.id
});
await this.tracingService.traceOperation(
'update-user-profile',
async () => {
await this.userService.updateUser(this.user);
}
);
}
}
Vanilla JavaScript¶
For applications without frameworks:
// app.js
import './tracing';
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('vanilla-js-app');
class App {
async initialize() {
const span = tracer.startSpan('app-initialization');
try {
await this.loadConfiguration();
await this.loadUserData();
this.setupEventListeners();
span.setStatus({ code: 1 });
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
} finally {
span.end();
}
}
async loadUserData() {
const span = tracer.startSpan('load-user-data');
try {
const response = await fetch('/api/user');
const userData = await response.json();
span.setAttributes({
'user.id': userData.id,
'user.role': userData.role
});
this.displayUser(userData);
span.setStatus({ code: 1 });
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
}
setupEventListeners() {
document.addEventListener('click', (event) => {
if (event.target.matches('.tracked-button')) {
const span = tracer.startSpan('button-click');
span.setAttributes({
'button.id': event.target.id,
'button.text': event.target.textContent,
'click.x': event.clientX,
'click.y': event.clientY
});
this.handleButtonClick(event.target);
span.end();
}
});
// Track form submissions
document.addEventListener('submit', (event) => {
const span = tracer.startSpan('form-submission');
span.setAttributes({
'form.id': event.target.id,
'form.action': event.target.action
});
span.end();
});
}
async handleButtonClick(button) {
const span = tracer.startSpan('handle-button-action');
try {
// Simulate some action
const action = button.dataset.action;
await this.performAction(action);
span.setStatus({ code: 1 });
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
} finally {
span.end();
}
}
}
const app = new App();
app.initialize();
Browser-Specific Considerations¶
Performance Considerations¶
Browser tracing can impact performance. Use these optimizations:
// Optimized tracing configuration
import { BatchSpanProcessor } from '@opentelemetry/sdk-web';
import { TraceIdRatioBasedSampler, AlwaysOnSampler } from '@opentelemetry/sdk-web';
const isProduction = process.env.NODE_ENV === 'production';
const provider = new WebTracerProvider({
resource: resource,
sampler: isProduction
? new TraceIdRatioBasedSampler(0.1) // 10% sampling in production
: new AlwaysOnSampler(), // 100% in development
});
const exporter = new OTLPTraceExporter({
url: getTraceEndpoint(),
});
// Optimize batch processing
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
maxExportBatchSize: 512,
maxQueueSize: 2048,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
}));
Security Considerations¶
- Never expose sensitive data in span attributes
- Be careful with user inputs in span names
- Consider sampling rates for high-traffic applications
// Safe attribute setting
const safeSetAttributes = (span, attributes) => {
const safeAttributes = {};
for (const [key, value] of Object.entries(attributes)) {
// Filter out sensitive data
if (key.includes('password') || key.includes('token') || key.includes('secret')) {
safeAttributes[key] = '[REDACTED]';
} else if (typeof value === 'string' && value.length > 100) {
safeAttributes[key] = value.substring(0, 100) + '...';
} else {
safeAttributes[key] = value;
}
}
span.setAttributes(safeAttributes);
};
Error Handling¶
Implement proper error handling to avoid breaking your application:
// Safe tracing wrapper
const safeTrace = async (spanName, operation, attributes = {}) => {
let span;
try {
span = tracer.startSpan(spanName);
span.setAttributes(attributes);
const result = await operation();
span?.setStatus({ code: 1 });
return result;
} catch (error) {
// Don't let tracing errors break the application
try {
span?.recordException(error);
span?.setStatus({ code: 2, message: error.message });
} catch (tracingError) {
console.warn('Tracing error:', tracingError);
}
throw error;
} finally {
try {
span?.end();
} catch (tracingError) {
console.warn('Tracing cleanup error:', tracingError);
}
}
};
Development vs Production¶
Use different configurations for different environments:
// Environment-specific configuration
const getTracingConfig = () => {
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';
return {
serviceName: process.env.REACT_APP_SERVICE_NAME || 'frontend-app',
serviceVersion: process.env.REACT_APP_VERSION || 'dev',
environment: process.env.NODE_ENV || 'development',
endpoint: isDevelopment
? '/v1/traces' // Development proxy
: '/v1/traces', // Production proxy
samplingRate: isProduction ? 0.01 : 1.0,
debug: isDevelopment,
};
};
// Apply configuration
const config = getTracingConfig();
if (config.debug) {
import('@opentelemetry/api').then(({ diag, DiagConsoleLogger, DiagLogLevel }) => {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
});
}
Connecting Frontend to Backend Traces¶
To connect your frontend traces with backend traces, ensure trace context propagation works correctly:
// Auto-instrumentation handles this automatically for fetch and XMLHttpRequest
const fetchUserData = async (userId) => {
const span = tracer.startSpan('fetch-user-data');
span.setAttributes({ 'user.id': userId });
try {
// OpenTelemetry auto-instrumentation will automatically
// add trace context headers (traceparent, tracestate) to this request
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
span.setAttributes({
'http.status_code': response.status,
'http.url': response.url
});
return data;
} finally {
span.end();
}
};
// Manual header propagation (if needed)
import { propagation, context } from '@opentelemetry/api';
const fetchWithManualPropagation = async (url, options = {}) => {
const headers = {};
// Inject current trace context into headers
propagation.inject(context.active(), headers);
return fetch(url, {
...options,
headers: {
...options.headers,
...headers,
},
});
};
Troubleshooting¶
Common Issues¶
No traces appearing in Sematext:
- Verify your proxy configuration is working
- Check browser developer console for network errors
- Ensure the Sematext Agent is running and accessible
- Check sampling rate isn't too low
CORS errors:
- Verify your proxy is handling CORS headers correctly
- Check that OPTIONS requests are handled properly
High performance impact:
- Reduce sampling rate in production
- Optimize batch span processor settings
- Disable unused auto-instrumentations
Proxy not working:
- Check proxy configuration in your build tool
- Verify the agent endpoint is accessible
- Test the proxy endpoint directly
Debug Mode¶
Enable debug logging to troubleshoot issues:
// Enable debug logging (development only)
if (process.env.NODE_ENV === 'development') {
import('@opentelemetry/api').then(({ diag, DiagConsoleLogger, DiagLogLevel }) => {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
});
}
Testing Your Setup¶
Create a simple test to verify tracing is working:
// test-tracing.js
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('test-app');
const testTracing = () => {
const span = tracer.startSpan('test-span');
span.setAttributes({
'test.name': 'tracing-setup-test',
'test.timestamp': Date.now()
});
console.log('Test span created:', span);
setTimeout(() => {
span.end();
console.log('Test span ended');
}, 100);
};
// Run test after page load
window.addEventListener('load', testTracing);
Best Practices¶
Span Naming¶
- Use descriptive, consistent naming:
load-user-profile
,submit-order-form
- Include the action and context:
user-interaction.button-click
,api-call.fetch-products
Attribute Guidelines¶
- Add meaningful context: component name, user ID, feature flags
- Don't include sensitive information
- Use consistent attribute naming across your application
Performance Tips¶
- Use appropriate sampling rates for your traffic volume
- Batch span processing for better performance
- Only instrument critical user journeys in high-traffic apps
Error Handling¶
- Always wrap tracing code in try-catch blocks
- Don't let tracing errors break your application
- Log tracing errors for debugging
Next Steps¶
- View your traces in Traces Explorer
- Set up alerts for frontend performance
- Configure sampling strategies
- Connect with backend services