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.

dave@scoutapp.com