OpenTelemetry can be used in the browser to trace your frontend application. Although automatic instrumentation for the web exists, you will get the most value by supplementing automatically-generated instrumentation in your application with custom spans and events. To add custom spans and events, which is known as manual instrumentation, you can use the full OpenTelemetry API.
To learn about the tradeoffs between automatic instrumentation and manual instrumentation, read our whitepaper, “Getting Started With Honeycomb Client-Side Instrumentation for Browser Applications”.
These instructions will explain how to set up automatic and manual instrumentation for a browser application. In order to follow along, you will need:
OpenTelemetry browser traces are sent via OTLP with HTTP/JSON.
Here is an example that configures a web application to send OpenTelemetry data to Honeycomb.
Before adding automatic instrumentation to your application for browser telemetry, choose from the following options to decide how you will send data to Honeycomb. Consider the security aspects of each option.
Sensitive data is never safe in the browser. We recommend setting up some kind of proxy to accept browser traces before sending them to Honeycomb. This will make sure you are not exposing your Honeycomb API keys in the browser application’s code.
The most common and recommended solution is to use an OpenTelemetry Collector. 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).
Browser code that sends requests directly to Honeycomb requires an API key. This exposes your Honeycomb data to the public. To limit that exposure, we strongly recommend that you create a new API key with permissions that only allows Send Events. This limits the impact of the key and enables you to rotate it often.
Set up the endpoint and header in the OTLPTraceExporter
.
const exporter = new OTLPTraceExporter({
url: "https://api.honeycomb.io/v1/traces",
headers: {
"x-honeycomb-team": "your-api-key",
},
})
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.
An alternative to the OpenTelemetry Collector is to create your own custom proxy endpoint.
Initialize the OTLPTraceExporter
with the URL of the custom endpoint.
const exporter = new OTLPTraceExporter({
url: "https://<your-custom-endpoint>",
})
Set the environment variables like the API Key and Honeycomb endpoint in the server-side code to keep it hidden from public view.
For example, to setup a proxy using a basic Express server, include the following in a post to https://api.honeycomb.io/v1/traces
:
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
},
body: JSON.stringify(otlpJsonExportedFromFrontend),
};
The server will also need cors
setup to allow connections from the browser, and will need to be able to parse json.
To accept POST
s to http://localhost:3000/v1/traces
from the browser at http://localhost:5000
to then send on to Honeycomb, the entire code might look like this:
// frontend.js
const exporter = new OTLPTraceExporter({
url: "http://localhost:3000/v1/traces",
})
// backend.js
import { config } from 'dotenv';
import express from 'express';
import fetch from 'node-fetch';
import cors from 'cors';
const app = express();
const port = 3000;
app.use(
cors({
origin: ['http://localhost:5000', 'http://127.0.0.1:5000'],
methods: ['POST'],
credentials: true,
}),
);
// Allow parsing of json
app.use(express.json());
// our api relay route
app.post('/v1/traces', async (req, res) => {
try {
const otlpJsonExportedFromFrontend = await req.body;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
},
body: JSON.stringify(otlpJsonExportedFromFrontend),
};
// sending on to Honeycomb
const response = await fetch('https://api.honeycomb.io/v1/traces', options)
.then((response) => console.log(response))
.catch((err) => console.error(err));
return res.json({
success: true,
response,
});
} catch (err) {
return res.status(500).json({
success: false,
message: err.message,
});
}
});
app.listen(port, () => console.log(`Server listening on port ${port}!`));
If your framework has server-side api routes that separate server-side code from the client-side bundle, that may be a viable option to consider for this endpoint.
To instrument your Web page using OpenTelemetry, you need to install the core OpenTelemetry API, SDK, exporter, and span processor packages:
npm install --save \
@opentelemetry/api \
@opentelemetry/sdk-trace-web \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/context-zone \
@opentelemetry/instrumentation \
@opentelemetry/auto-instrumentations-web
yarn add \
@opentelemetry/api \
@opentelemetry/sdk-trace-web \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/context-zone \
@opentelemetry/instrumentation \
@opentelemetry/auto-instrumentations-web
These packages include:
api
: An API package used to add instrumentation for everything you care about in your application.sdk-trace-web
: An SDK package, which creates traces that conform to the OpenTelemetry specification.exporter-trace-otlp-http
: An exporter package, which is responsible for sending trace data out via HTTP requests.context-zone
: A helper package that processes spans from across your application to ensure they have all the relevant context for rich telemetry.instrumentation
: An instrumentation package that initializes automatic instrumentation.auto-instrumentations-web
: A meta package that includes various web automatic instrumentation including request and document load instrumentation.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';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
// The exporter is responsible for sending traces from the browser to your collector
const exporter = new OTLPTraceExporter({
url: 'https://<your collector endpoint>:443/v1/traces'
});
// The TracerProvider is the core library for creating traces
const provider = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'browser',
}),
});
// The processor sorts through data as it comes in, before it is sent to the exporter
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
// A context manager allows OTel to keep the context of function calls across async functions
// ensuring you don't have disconnected traces
provider.register({
contextManager: new ZoneContextManager()
});
//
registerInstrumentations({
instrumentations: [
getWebAutoInstrumentations(),
],
});
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.
Then, load the initialization file at the top of your web page’s header or entry point file. This file needs to be loaded as early as possible in the page to ensure that you can capture as much data about pageload and timing as possible.
// index.js
import './tracing.js'
// ...rest of the app's entry point code
You can 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 must include the trace context header in the request. You can do this using either automatic instrumentation or manual instrumentation.
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.
If using the automatic instrumentation meta 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.
],
},
}),
],
});
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();
}
});
};
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 \
@opentelemetry/instrumentation-user-interaction \
@opentelemetry/instrumentation-long-task
yarn add \
@opentelemetry/instrumentation-document-load \
@opentelemetry/instrumentation-user-interaction \
@opentelemetry/instrumentation-long-task
@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(),
],
});
@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';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
// ...general opentelemetry configuration
registerInstrumentations({
instrumentations: [
new UserInteractionInstrumentation({
eventNames: ['submit', 'click', 'keypress'],
shouldPreventSpanCreation: (event, element, span) => {
span.setAttribute('target.id', element.id)
// etc..
}
}),
],
});
@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)
}
}),
],
});
To customize your instrumentation, you need to add instrumentation for actions. Refer to the sample code below 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();
});
}
});
The OpenTelemetry documentation for JavaScript has a comprehensive set of topics on manual instrumentation.
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
.