OpenTelemetry Trace Context Propagation [JavaScript]
This guide covers JavaScript-specific implementation of context propagation. For a comprehensive overview of context propagation concepts, W3C TraceContext, propagators, baggage, and troubleshooting, see the OpenTelemetry Context Propagation guide.
What is traceparent header?
The traceparent HTTP header contains information about the incoming request in a distributed tracing system, for example:
# {version}-{trace-id}-{parent-id}-{trace-flags}
traceparent: 00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01
You can find the traceparent header in HTTP responses using browser developer tools to extract trace IDs and locate specific traces in distributed tracing tools.
Using the header, you can extract a trace ID to find the trace in one of the distributed tracing tools. For example, from the header above, the trace ID is 80e1afed08e019fc1110464cfa66635c.
Traceparent header format
The traceparent header uses the version-trace_id-parent_id-trace_flags format where:
versionis always00trace_idis a hex-encoded trace ID (16 bytes)span_idis a hex-encoded span ID (8 bytes)trace_flagsis a hex-encoded 8-bit field containing tracing flags such as sampling
Automatic propagation
OpenTelemetry JavaScript handles traceparent headers automatically in most scenarios. When using the Node.js SDK or browser SDK, HTTP client libraries automatically inject traceparent headers into outgoing requests, and server libraries automatically extract them from incoming requests.
Node.js automatic propagation
const { NodeSDK } = require('@opentelemetry/sdk-node')
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node')
const sdk = new NodeSDK({
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
All HTTP requests made with http, https, fetch, and popular libraries like axios will automatically include traceparent headers without code changes.
Browser automatic propagation
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'
import { registerInstrumentations } from '@opentelemetry/instrumentation'
const provider = new WebTracerProvider()
provider.register()
registerInstrumentations({
instrumentations: [
new FetchInstrumentation(),
new XMLHttpRequestInstrumentation(),
],
})
Note: Browser instrumentation is experimental. All fetch and XMLHttpRequest calls will automatically include traceparent headers, but some features may be limited compared to Node.js.
Manual propagation
When automatic instrumentation is not available, you can manually handle traceparent headers using OpenTelemetry's Propagators API.
Extracting from incoming requests (Node.js)
const { propagation, trace, context } = require('@opentelemetry/api')
class TraceContextExtractor {
extractContext(request) {
// Extract context from incoming HTTP headers
const extractedContext = propagation.extract(
context.active(),
request.headers,
)
return extractedContext
}
// Usage in Express middleware
createMiddleware() {
return (req, res, next) => {
const extractedContext = this.extractContext(req)
// Make the extracted context active for downstream operations
context.with(extractedContext, () => {
next()
})
}
}
}
// Usage with Express
const express = require('express')
const app = express()
const extractor = new TraceContextExtractor()
app.use(extractor.createMiddleware())
app.get('/api/users', (req, res) => {
// This request now has the proper trace context from the traceparent header
const currentSpan = trace.getActiveSpan()
console.log('Current trace ID:', currentSpan?.spanContext().traceId)
res.json({ users: [] })
})
Note: Most HTTP frameworks are automatically instrumented by OpenTelemetry, so manual extraction is typically only needed for custom protocols or unsupported libraries.
Injecting into outgoing requests (Node.js)
const { propagation, context } = require('@opentelemetry/api')
const https = require('https')
class TraceContextInjector {
makeRequest(url, options = {}) {
const headers = { ...options.headers }
// Inject current trace context into headers
propagation.inject(context.active(), headers)
const requestOptions = {
...options,
headers: headers,
}
return new Promise((resolve, reject) => {
const req = https.request(url, requestOptions, (res) => {
let data = ''
res.on('data', (chunk) => (data += chunk))
res.on('end', () => resolve(data))
})
req.on('error', reject)
req.end()
})
}
// Usage with fetch
async makeFetchRequest(url, options = {}) {
const headers = new Headers(options.headers)
// Create a headers object that propagation.inject can work with
const headersObj = {}
propagation.inject(context.active(), headersObj)
// Add injected headers to the fetch headers
Object.entries(headersObj).forEach(([key, value]) => {
headers.set(key, value)
})
return fetch(url, {
...options,
headers: headers,
})
}
}
// Usage
const injector = new TraceContextInjector()
// With custom request
injector.makeRequest('https://api.example.com/data')
// With fetch
injector.makeFetchRequest('https://api.example.com/users')
Browser manual propagation
import { propagation, context } from '@opentelemetry/api'
class BrowserTraceInjector {
// Inject into fetch requests
async makeTracedFetch(url, options = {}) {
const headers = new Headers(options.headers)
// Create headers object for injection
const headersObj = {}
propagation.inject(context.active(), headersObj)
// Add trace headers
Object.entries(headersObj).forEach(([key, value]) => {
headers.set(key, value)
})
return fetch(url, {
...options,
headers: headers,
})
}
// Inject into XMLHttpRequest
makeTracedXHR(method, url) {
const xhr = new XMLHttpRequest()
// Inject headers
const headers = {}
propagation.inject(context.active(), headers)
xhr.open(method, url)
// Set trace headers
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value)
})
return xhr
}
}
// Usage
const injector = new BrowserTraceInjector()
// Traced fetch
injector.makeTracedFetch('/api/data')
// Traced XHR
const xhr = injector.makeTracedXHR('GET', '/api/users')
xhr.send()
Injecting into response headers
To include traceparent headers in HTTP responses (Node.js):
const { propagation, context, trace } = require('@opentelemetry/api')
class TraceparentResponseMiddleware {
createMiddleware() {
return (req, res, next) => {
// Store original res.end to intercept response
const originalEnd = res.end.bind(res)
res.end = function (...args) {
// Inject current trace context into response headers
const currentSpan = trace.getActiveSpan()
if (currentSpan && currentSpan.spanContext().isValid()) {
const headers = {}
propagation.inject(context.active(), headers)
// Add trace headers to response
Object.entries(headers).forEach(([key, value]) => {
res.setHeader(key, value)
})
}
// Call original end method
originalEnd.apply(this, args)
}
next()
}
}
}
// Usage with Express
const express = require('express')
const app = express()
const responseMiddleware = new TraceparentResponseMiddleware()
app.use(responseMiddleware.createMiddleware())
app.get('/api/status', (req, res) => {
res.json({ status: 'ok' })
// Response will automatically include traceparent header
})
Debugging propagation
Logging trace context
Log incoming traceparent headers and current span context for debugging:
const { trace, context } = require('@opentelemetry/api')
class TraceDebugMiddleware {
createMiddleware() {
return (req, res, next) => {
// Log incoming traceparent header
const incomingTraceparent = req.headers['traceparent']
console.log('Incoming traceparent:', incomingTraceparent)
// Log current span context
const currentSpan = trace.getActiveSpan()
if (currentSpan && currentSpan.spanContext().isValid()) {
const spanContext = currentSpan.spanContext()
console.log('Current trace context:', {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
traceFlags: spanContext.traceFlags,
isSampled: (spanContext.traceFlags & 1) === 1,
isRemote: spanContext.isRemote,
})
} else {
console.warn('No valid span context found')
}
next()
}
}
}
// Usage
const debugMiddleware = new TraceDebugMiddleware()
app.use(debugMiddleware.createMiddleware())
Validating format
Validate and parse traceparent headers to ensure they follow the W3C specification:
class TraceparentValidator {
static TRACEPARENT_REGEX = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
static isValidTraceparent(traceparent) {
if (!traceparent || typeof traceparent !== 'string') {
return false
}
return this.TRACEPARENT_REGEX.test(traceparent)
}
static parseTraceparent(traceparent) {
if (!this.isValidTraceparent(traceparent)) {
throw new Error(`Invalid traceparent format: ${traceparent}`)
}
const parts = traceparent.split('-')
return {
version: parts[0],
traceId: parts[1],
spanId: parts[2],
flags: parts[3],
isSampled: parts[3] === '01',
}
}
static formatTraceparent(traceId, spanId, flags = '01') {
return `00-${traceId}-${spanId}-${flags}`
}
}
// Usage
const traceparent = '00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01'
if (TraceparentValidator.isValidTraceparent(traceparent)) {
const parsed = TraceparentValidator.parseTraceparent(traceparent)
console.log('Parsed traceparent:', parsed)
// Output: { version: '00', traceId: '80e1afed08e019fc1110464cfa66635c', ... }
}
Getting trace information
Access current trace context information and format it as traceparent header:
const { trace } = require('@opentelemetry/api')
class TraceInfoService {
getTraceInfo() {
const currentSpan = trace.getActiveSpan()
if (!currentSpan || !currentSpan.spanContext().isValid()) {
return { error: 'No valid span context available' }
}
const spanContext = currentSpan.spanContext()
return {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
traceFlags: spanContext.traceFlags,
isSampled: (spanContext.traceFlags & 1) === 1,
isRemote: spanContext.isRemote,
traceState: spanContext.traceState?.serialize() || '',
// Format as traceparent header
traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${spanContext.traceFlags.toString(16).padStart(2, '0')}`,
}
}
// Express endpoint to return trace info
createTraceInfoEndpoint() {
return (req, res) => {
const traceInfo = this.getTraceInfo()
res.json(traceInfo)
}
}
}
// Usage
const traceInfoService = new TraceInfoService()
app.get('/trace-info', traceInfoService.createTraceInfoEndpoint())
// Or get trace info programmatically
const currentTraceInfo = traceInfoService.getTraceInfo()
console.log('Current trace:', currentTraceInfo)
Browser trace extraction
Extract trace information in browser applications:
import { trace } from '@opentelemetry/api'
class BrowserTraceInfo {
static getTraceInfo() {
const currentSpan = trace.getActiveSpan()
if (!currentSpan || !currentSpan.spanContext().isValid()) {
return { error: 'No valid span context available' }
}
const spanContext = currentSpan.spanContext()
return {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
isSampled: (spanContext.traceFlags & 1) === 1,
traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${spanContext.traceFlags.toString(16).padStart(2, '0')}`,
}
}
static logTraceToConsole() {
const traceInfo = this.getTraceInfo()
console.log('Current browser trace:', traceInfo)
if (traceInfo.traceparent) {
console.log(
`Find this trace in Uptrace: https://app.uptrace.dev/traces/${traceInfo.traceId}`,
)
}
}
// Simple trace display (avoid complex DOM manipulation in production)
static addTraceToDOM() {
const traceInfo = this.getTraceInfo()
if (traceInfo.traceparent && !document.getElementById('trace-info')) {
const traceElement = document.createElement('div')
traceElement.id = 'trace-info'
traceElement.style.cssText =
'position: fixed; top: 10px; right: 10px; background: #f0f0f0; padding: 5px; font-family: monospace; font-size: 12px; z-index: 9999;'
traceElement.textContent = `Trace: ${traceInfo.traceId.substring(0, 8)}...`
document.body.appendChild(traceElement)
}
}
}
// Usage in browser (development only)
BrowserTraceInfo.logTraceToConsole()
BrowserTraceInfo.addTraceToDOM()
Custom propagators
Note: Custom propagator implementation is an advanced topic and typically not needed for most applications. The standard W3C Trace Context propagator handles traceparent headers automatically.
For specific requirements, you can create custom propagation logic:
const { propagation, trace } = require('@opentelemetry/api')
class CustomHeaderPropagator {
inject(context, carrier, setter) {
const span = trace.getSpan(context)
if (!span || !span.spanContext().isValid()) {
return
}
const spanContext = span.spanContext()
// Inject custom headers alongside standard traceparent
setter(carrier, 'x-trace-id', spanContext.traceId)
setter(carrier, 'x-span-id', spanContext.spanId)
setter(
carrier,
'x-sampled',
(spanContext.traceFlags & 1) === 1 ? 'true' : 'false',
)
}
extract(context, carrier, getter) {
const traceId = getter(carrier, 'x-trace-id')
const spanId = getter(carrier, 'x-span-id')
if (traceId && spanId) {
// Create span context from custom headers
const spanContext = trace.createSpanContext({
traceId: traceId,
spanId: spanId,
traceFlags: getter(carrier, 'x-sampled') === 'true' ? 1 : 0,
})
return trace.setSpanContext(context, spanContext)
}
return context
}
fields() {
return ['x-trace-id', 'x-span-id', 'x-sampled']
}
}
// Usage (advanced scenarios only)
const customPropagator = new CustomHeaderPropagator()
// Note: This would replace the default propagator entirely