‹ Back to Blog

OpenTelemetry and Distributed Tracing in JavaScript

Engineering

OpenTelemetry and Distributed Tracing in JavaScript

In our Configuring OpenTelemetry in Ruby blog post, we showed how to configure OpenTelemetry in a Ruby on Rails backend. In this post, we’ll cover how to configure OpenTelemetry in the front-end JavaScript in order to measure performance of browser and mobile devices and how to configure distributed tracing to work across the frontend and back end telemetry collection.

Let’s dive in!

The OpenTelemetry JavaScript packages for Node.js and browsers

The packages for OpenTelemetry supporting both Node.js apps and for desktop or mobile browser support can be found at https://github.com/open-telemetry/opentelemetry-js

OpenTelemetry Components used in this example

Resource

A Resource captures information about the entity for which telemetry is recorded.

SemanticResourceAttributes

OpenTelemetry standardizes the naming of certain attributes attached to telemetry collected. These standardized names are called Semantic Conventions in OpenTelemetry.

Propagator, B3Propagator

In order to pass contextual data across any services collecting OpenTelemetry data, you use Propagators that understand how to serialize and deserialize the data between services.

The contextual data transported by the Propagator is called Baggage. There are different formats for Baggage and each service collecting OpenTemetry should use the same baggage format. In this example, the B3 format is used.

Web Trace Provider

The Web TraceProvider supports the automatic tracing of the browser.

Span Processor, Exporter

The Span Processor handles processing of the traces when the trace is finished, including how to export the telemetry data.

WebVitals, DocumentLoader

These are additional telemetry collection libraries that capture telemetry for Core Web Vitals metrics.

Package Installation

You’ll first want to install the packages via npm or the appropriate method for your application.

In this example, we’ll be using the following packages in our package.json:

@opentelemetry/api
@opentelemetry/context-zone
@opentelemetry/exporter-collector
@opentelemetry/instrumentation-document-load
@opentelemetry/propagator-b3
@opentelemetry/sdk-trace-base
@opentelemetry/sdk-trace-web

Create otel-loader.ts

Imports

// OpenTelemetry
import * as otelCore from "@opentelemetry/core"
import * as otelApi from "@opentelemetry/api"

// Otel Setup (Trace Provider, Processor, Exporter, ...)
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { CollectorTraceExporter } from '@opentelemetry/exporter-collector'
import { registerInstrumentations } from '@opentelemetry/instrumentation'
import { ZoneContextManager } from "@opentelemetry/context-zone"

// Plugins
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'
import { WebVitalsInstrumentation } from './instrumentation/web-vitals-instrumentation'
import { B3Propagator, B3InjectEncoding } from "@opentelemetry/propagator-b3"

Initialization

After importing, we’ll initialize and configure the package in an initOpenTelemetry() function

export function initOpenTelemetry() {
    // setup Propagator
    const propagator = new otelCore.CompositePropagator({
        propagators: [
            new B3Propagator(),
            new B3Propagator({ injectEncoding: B3InjectEncoding.MULTI_HEADER })
        ]
    })
    otelApi.propagation.setGlobalPropagator(propagator)

    // setup Collector
    // Replace YOUR_OPENTELEMETRY_COLLECTOR_ENDPOINT with the URL that accepts OTLP
    const collectorOptions = {
        url: "https://YOUR_OPENTELEMETRY_COLLECTOR_ENDPOINT/v1/traces",
        headers: {
            "content-type": "application/json"
        }
    }

    // setup Span Resource
    const resource = new Resource({
        [SemanticResourceAttributes.SERVICE_NAME]: "scout_apm",
        [SemanticResourceAttributes.SERVICE_NAMESPACE]: "scout",
        [SemanticResourceAttributes.SERVICE_INSTANCE_ID]: "web",
        [SemanticResourceAttributes.SERVICE_VERSION]: process.env.VERSION || "0.0.0",
    })

    // init provider and exporter
    const provider = new WebTracerProvider({ resource })
    const exporter = new CollectorTraceExporter( collectorOptions )

    // Add Span Processor to provider
    provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
        // The maximum queue size. After the size is reached spans are dropped.
        maxQueueSize: 100,
        // The maximum batch size of every export. It must be smaller or equal to maxQueueSize.
        maxExportBatchSize: 50,
        // The interval between two consecutive exports
        scheduledDelayMillis: 500,
        // How long the export can run before it is canceled
        exportTimeoutMillis: 30000,
    }))

    // Instrumentation
    registerInstrumentations({
        instrumentations: [
            new DocumentLoadInstrumentation(),
            new WebVitalsInstrumentation(),
        ],
        tracerProvider: provider,
    })

    provider.register({
        contextManager: new ZoneContextManager().enable() as otelApi.ContextManager,
        propagator
    })

    // Baggage Init
    const baggage =
        otelApi.propagation.getBaggage(otelApi.context.active()) ||
        otelApi.propagation.createBaggage()
    baggage.setEntry("sessionId", { value: "session-id-value" })
    otelApi.propagation.setBaggage(otelApi.context.active(), baggage)
}

Wrapping up

Now we can load our OpenTelemetry code wherever we want:

import { initOpenTelemetry } from 'otel-loader'initOpenTelemetry()

That’s all there is for collecting telemetry data with distributed tracing connecting browser traces with back-end application traces! Learn about Scout’s path toward full-stack observability at scoutapm.com/observability.