Instrumenting Browser JavaScript Apps | Honeycomb

We use cookies or similar technologies to personalize your online experience & tailor marketing to you. Many of our product features require cookies to function properly.

Read our privacy policy I accept cookies from this site

Instrumenting Browser JavaScript Apps

If you send data about your web applications to Honeycomb, you probably already know to “start at the edge” and instrument each request as it reaches your servers.

OpenTelemetry in the Browser 

OpenTelemetry can be used in the browser to trace your frontend application. We recommend creating spans and events yourself. As of this writing, we do not feel that OpenTelemetry’s automatic instrumentation support for browsers is an effective choice for Honeycomb. You can utilize the full OpenTelemetry API to do manual instrumentation. The whitepaper has more detail on the tradeoffs of automatic instrumentation.

OpenTelemetry browser traces are sent via OTLP with HTTP/JSON. Sensitive data is never safe in the browser. We recommend to not directly expose your Honeycomb API keys in your browser application’s code. This means you will need to set up an OpenTelemetry Collector to accept any browser traces before you send them to Honeycomb. By using a collector, you can store sensitive credentials there and ensure that any data being sent to Honeycomb is processed safely (along with any additional data control enabled by the collector).

Instrumentation Packages 

To instrument your Web page, add the following packages:

npm install --save @opentelemetry/api
npm install --save @opentelemetry/sdk-trace-web
npm install --save @opentelemetry/exporter-trace-otlp-http
npm install --save @opentelemetry/context-zone
yarn add @opentelemetry/api
yarn add @opentelemetry/sdk-trace-web
yarn add @opentelemetry/exporter-trace-otlp-http
yarn add @opentelemetry/context-zone

Initialization 

The OpenTelemetry initialization needs to happen as early as possible in the webpage. Accomplish this by creating an initialization file, similar to the JavaScript example below.

// tracing.js
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { Resource }  from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const exporter = new OTLPTraceExporter({
  url: 'https://<your collector endpoint>:443/v1/traces'
});
const provider = new WebTracerProvider({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'browser',
  }),
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register({
  contextManager: new ZoneContextManager()
});

Then, load the initialization file at the top of your web page’s header or entry point file.

// index.js
import './tracing.js'

// ...rest of the app's entry point code

Note that the tracing.js example above calls the BatchSpanProcessor. Batching up span sending operations reduces client-side load and can help reduce battery and data usage from end user devices. Batching will also decrease contention with your application for network resources and ensure that your instrumentation creates minimal impact. The BatchSpanProcessor automatically batches events together.

You then need to add instrumentation for actions. Here, we show sample code that instruments the window load event and a button:

import { trace, context, } from '@opentelemetry/api';

const tracer = trace.getTracer();

const rootSpan = tracer.startActiveSpan('document_load', span => {
  //start span when navigating to page
  span.setAttribute('pageUrlwindow', window.location.href);
  window.onload = (event) => {
    // ... do loading things
    // ... attach timing information
    span.end(); //once page is loaded, end the span
  };

  button.clicked = (event) => {
    tracer.startActiveSpan('button_clicked', btnSpan => {
      // Add your attributes to describe the button clicked here
      btnSpan.setAttribute('some.attribute', 'some.value');

      btnSpan.end();
    });
  }
});

Connecting the Frontend and Backend Traces 

It is possible to connect your frontend request traces to your backend traces, which allows you to trace a request all the way from your browser through your distributed system. To connect your frontend traces to your backend traces, you need to include the trace context header in the request. This can be done with automatic instrumentation or manual instrumentation.

Automatically Propagate the Trace Context Header 

Use request automatic instrumentation to automatically send spans for every HTTP request. @opentelemetry/instrumentation-xml-http-request automatically instruments XHR requests and @opentelemetry/instrumentation-fetch automatically instruments fetch requests. If your browser application uses a request library to make requests, such as axios or superagent, these requests are also automatically instrumented by enabling the xml-http-request or fetch instrumentation, depending on what the library uses to make requests.

npm install --save @opentelemetry/instrumentation-xml-http-request
npm install --save @opentelemetry/instrumentation-fetch
yarn add @opentelemetry/instrumentation-xml-http-request
yarn add@opentelemetry/instrumentation-fetch

If utilizing the instrumentation-xml-http-request package or the instrumentation-fetch package, propagate the trace header automatically by setting this configuration property.

import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';

// ...general opentelemetry configuration

registerInstrumentations({
  instrumentations: [
    new XMLHttpRequestInstrumentation({
      propagateTraceHeaderCorsUrls: [
         /.+/g, //Regex to match your backend urls. This should be updated.
      ]
    }),
    new FetchInstrumentation({
      propagateTraceHeaderCorsUrls: [
         /.+/g, //Regex to match your backend urls. This should be updated.
      ]
    }),
  ],
});

Alternatively, if using the automatic instrumentation package for web, you can send this configuration property to the package:

registerInstrumentations({
 instrumentations: [
   getWebAutoInstrumentations({
     // load custom configuration for xml-http-request instrumentation
     '@opentelemetry/instrumentation-xml-http-request': {
       propagateTraceHeaderCorsUrls: [
           /.+/g, //Regex to match your backend urls. This should be updated.
         ],
     },
     '@opentelemetry/instrumentation-fetch': {
       propagateTraceHeaderCorsUrls: [
           /.+/g, //Regex to match your backend urls. This should be updated.
         ],
     },
   }),
 ],
});

Manually Propagate the Trace Context Header 

It is also possible to manually propagate the trace context header if automatic instrumentation is not an option:

// General request handler, instrumented with OTel
// Forwards traceparent header to connect spans created in the browser
// with spans created on the backend
const request = async (url, method = 'GET', headers, body) => {
  return trace
    .getTracer('request-tracer')
    .startActiveSpan(`Request: ${method} ${url}`, async (span) => {

      // construct W3C traceparent header
      const traceparent = `00-${span.spanContext().traceId}-${span.spanContext().spanId}-01`;

      try {
        const response = await fetch(url, {
          method,
          headers: {
            ...headers,
            // set traceparent header
            traceparent: traceparent,
          },
          body,
        });
        span.setAttributes({
          'http.method': method,
          'http.url': url,
          'response.status_code': response.status,
        });
        if (response.ok && response.status >= 200 && response.status < 400) {
          span.setStatus({ code: SpanStatusCode.OK });
          return response.text();
        } else {
          throw new Error(`Request Error ${response.status} ${response.statusText}`);
        }
      } catch (error) {
        span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
        throw new Error(error);
      } finally {
        span.end();
      }
    });
};

Other Automatic Instrumentation 

There are several automatic instrumentation libraries available for the browser. They tend to produce a lot of span events so we recommend adding them with some caution.

npm install --save @opentelemetry/instrumentation-document-load
npm install --save @opentelemetry/instrumentation-user-interaction
npm install --save @opentelemetry/instrumentation-long-task
yarn add @opentelemetry/instrumentation-document-load
yarn add @opentelemetry/instrumentation-user-interaction
yarn add @opentelemetry/instrumentation-long-task

Document Load 

@opentelemetry/instrumentation-document-load produces spans for document load information for the browser, including how long it takes the document to load and become interactive. It also creates child spans for each resource fetched by the browser. For example, CSS files, JS files, fonts and images.

import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';


// ...general opentelemetry configuration

registerInstrumentations({
  instrumentations: [
    new DocumentLoadInstrumentation(),
  ],
});

User Interaction 

@opentelemetry/instrumentation-user-interaction produces spans for user interactions with a browser web application.

import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';


// ...general opentelemetry configuration

registerInstrumentations({
  instrumentations: [
    new UserInteractionInstrumentation(),
  ],
});

By default, only click events are automatically instrumented. To automatically instrument other events, specify the events that should be captured for telemetry. Most browser events are supported.

import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';


// ...general opentelemetry configuration

registerInstrumentations({
  instrumentations: [
    new UserInteractionInstrumentation({
      eventNames: ['submit', 'click', 'keypress'],
    }),
  ],
});

To attach extra attributes to user interaction spans, provide a callback function:

import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';

// ...general opentelemetry configuration

registerInstrumentations({
  instrumentations: [
    new UserInteractionInstrumentation({
      eventNames: ['submit', 'click', 'keypress'],
      shouldPreventSpanCreation: (event, element, span) => {
        span.setAttribute('target.id', element.id)
        // etc..
      }
    }),
  ],
});

Long Tasks 

@opentelemetry/instrumentation-long-task provides automatic instrumentation for Long Task API.

import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';

// ...general opentelemetry configuration

registerInstrumentations({
  tracerProvider: provider,
  instrumentations: [
    new LongTaskInstrumentation({
      observerCallback: (span, longtaskEvent) => {
        span.setAttribute('location.pathname', window.location.pathname)
      }
    }),
  ],
});

More on Manual Instrumentation 

The OpenTelemetry documentation for JavaScript has a comprehensive set of topics on manual instrumentation.

OpenTelemetry Collector Configuration 

To receive browser telemetry, the Collector requires an enabled OTLP/HTTP receiver. The allowed_origins property is required to enable Cross-Origin Resource Sharing (CORS) from the browser to the collector. The Collector will need to be accessible by the browser. It is recommended to put an external load balancer in front of the collector, which will also need to be configured to accept requests from the browser origin.

In the example below, the configuration allows for the OpenTelemetry Collector to accept browser OpenTelemetry tracing, and is required to get data from the browser to Honeycomb.

receivers:
  otlp:
    protocols:
      http: # port 4318
        cors:
          allowed_origins:
            - "http://*.<yourdomain>.com"
            - "https://*.<yourdomain>.com"

processors:
  batch:

exporters:
  otlp:
    endpoint: "api.honeycomb.io:443"
    headers:
      "x-honeycomb-team": "YOUR_API_KEY"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

More configuration options can be found on the Collector Github Repository.

There may already be an OpenTelemetry Collector in use by other applications. If a Collector is already in use in your environment, it may be possible to make the necessary changes to the existing Collector configuration without needing to deploy something new. See more details about setting up the OpenTelemetry Collector, including deployment patterns to consider.

Troubleshooting 

CORS Errors 

Sometimes a CORS error may occur when setting up browser telemetry.

Confirm the receiver’s setup has the correct port defined. The default port for http is 4318. If this port or endpoint is overwritten in the collector configuration file, ensure it matches the endpoint set in the application sending telemetry.

Confirm the allowed_origins list in the receivers matches the origin of the browser telemetry. If there is a load balancer in front of the Collector, it should also be configured to accept requests from the browser origin.

One way to determine whether the issue is rooted in how the application is exporting as opposed to network connectivity is to issue a curl command to the server from the browser origin.

For example, if the application was running on http://localhost:3000, and the collector was listening on port 4318 at http://otel-collector.com/v1/traces:

curl -i http://otel-collector.com/v1/traces -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: X-Requested-With" -H "Content-Type: application/json" -X OPTIONS --verbose

The response from the server should include Access-Control-Allow-Credentials: true.

Did you find what you were looking for?