Honeycomb provides the Honeycomb Web Instrumentation package to help you instrument your applications and send browser data to Honeycomb as quickly and easily as possible. Under the hood, the Honeycomb Web Instrumentation package uses OpenTelemetry for JavaScript.
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 custom instrumentation, use the full OpenTelemetry API.
If you prefer to learn by example, we provide several examples that configure applications to send OpenTelemetry data to Honeycomb.
Honeycomb’s Web Instrumentation package gives you all of the capabilities provided by the generic OpenTelemetry Distribution for JavaScript and also allows you to:
Before you can set up instrumentation for your application, you will need to do a few things.
To send data to Honeycomb, you need to:
We make our Honeycomb Web Instrumentation package available as an NPM package, so you can include it in your web bundle.
Navigate to the root directory of your service’s repo, and then install the package:
npm install @honeycombio/opentelemetry-web @opentelemetry/auto-instrumentations-web
yarn add @honeycombio/opentelemetry-web @opentelemetry/auto-instrumentations-web
yarn.lock
file, install with NPM.Module | Description |
---|---|
auto-instrumentations-web |
OpenTelemetry’s meta package that includes various web automatic instrumentation including request and document load instrumentation. |
opentelemetry-web |
Honeycomb’s Web Instrumentation package that streamlines configuration and allows you to instrument as quickly and easily as possible. |
Alternatively, install individual instrumentation packages.
Finally, confirm that the install was successful by opening your package.json
file and checking that the Dependencies
list now contains @honeycomb/opentelemetry-web
.
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.
The endpoint for the HoneycombWebSDK
defaults to sending to the Honeycomb US endpoint https://api.honeycomb.io/v1/traces
, so no changes need to be made for the endpoint and the API Key can be set directly in the code.
To send data to the Honeycomb EU endpoint, uncomment endpoint
in the example below.
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.
// index.js or main.js
// other import statements...
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
const configDefaults = {
ignoreNetworkEvents: true,
// propagateTraceHeaderCorsUrls: [
// /.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
// ]
}
const sdk = new HoneycombWebSDK({
// endpoint: "https://api.eu1.honeycomb.io/v1/traces", // Send to EU instance of Honeycomb. Defaults to sending to US instance.
debug: true, // Set to false for production environment.
apiKey: '[YOUR API KEY HERE]', // Replace with your Honeycomb Ingest API Key.
serviceName: '[YOUR APPLICATION NAME HERE]', // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
})],
});
sdk.start();
// application instantiation code
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:
endpoint: 0.0.0.0:4318
cors:
allowed_origins:
- "http://*.<yourdomain>.com"
- "https://*.<yourdomain>.com"
processors:
batch:
exporters:
otlp:
endpoint: "api.honeycomb.io:443" # US instance
#endpoint: "api.eu1.honeycomb.io:443" # EU instance
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.
The endpoint for the HoneycombWebSDK
defaults to sending to the Honeycomb US endpoint https://api.honeycomb.io/v1/traces
.
To send data to the Honeycomb EU endpoint, uncomment endpoint
in the example below.
To send to a Collector, update the endpoint and omit your API Key, because that will be set in the Collector itself.
// index.js or main.js
// other import statements...
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
const configDefaults = {
ignoreNetworkEvents: true,
// propagateTraceHeaderCorsUrls: [
// /.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
// ]
}
const sdk = new HoneycombWebSDK({
debug: true, // turn off in production
serviceName: '[YOUR APPLICATION NAME HERE]', // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
endpoint: '[YOUR COLLECTOR URL]',
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
})],
});
sdk.start();
// application instantiation code
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.
Update the HoneycombWebSDK
with the URL of the custom endpoint and omit your API Key, because that will be set in your proxy.
// index.js or main.js
// other import statements...
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
const configDefaults = {
ignoreNetworkEvents: true,
// propagateTraceHeaderCorsUrls: [
// /.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
// ]
}
const sdk = new HoneycombWebSDK({
debug: true, // Set to false for production environment.
serviceName: '[YOUR APPLICATION NAME HERE]', // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
endpoint: '[YOUR PROXY ENDPOINT HERE]',
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
})],
});
sdk.start();
// application instantiation code
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 either https://api.honeycomb.io/v1/traces
(if you are using our US instance) or https://api.eu1.honeycomb.io/v1/traces
(if you are using our EU instance):
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 sdk = new HoneycombWebSDK({
endpoint: "http://localhost:3000/v1/traces",
serviceName: "your-service-name",
instrumentations: [getWebAutoInstrumentations()],
})
// 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) // US instance
//const response = await fetch('https://api.eu1.honeycomb.io/v1/traces', options) // EU instance
.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.
This is the complete list of configuration options for the Honeycomb Web Instrumentation package.
name | required? | type | default value | description |
---|---|---|---|---|
apiKey | required* | string | Honeycomb API Key for sending traces directly to Honeycomb. | |
serviceName | optional | string | unknown_service | The name of this browser application. Your telemetry will appear in a Honeycomb dataset with this name. |
localVisualizations | optional | boolean | false | For each trace created, print a link to the console so that you can find it in Honeycomb. Useful in development environments, but do not use in production. |
sampleRate | optional | number | 1 | If you want to send a random fraction of traces, then make this a whole number greater than 1. Only 1 in sampleRate traces will be sent, and the rest never be created. |
tracesEndpoint | optional | string | ${endpoint}/v1/traces |
Populate this to send traces to a route other than /v1/ traces. |
debug | optional | boolean | false | Enable additional logging. |
dataset | optional | string | Populate this option only if your Honeycomb environment is a Honeycomb Classic environment. | |
skipOptionsValidation | optional | boolean | false | Do not require any fields.* Use with OpenTelemetry Collector. |
spanProcessor | optional | function | Allows you to use an OpenTelemetry SimpleSpanProcessor to modify all spans. |
*
API key and service name configuration options are required if sending data to Honeycomb directly.
If using an OpenTelemetry Collector, configure your API key at the Collector level instead.In the Honeycomb Web Instrumentation, we pass in the ignoreNetworkEvents: true
configuration to all automatic instrumentation packages.
This configuration option reduces the number of events emitted from OpenTelemetry by about 90%.
This configuration also means you do not collect network timing for all network requests, such as domainLookup
, requestStart
, requestEnd
, and so on.
IgnoreNetworkEvents
defaults to false
.
If you wish to collect timing events, you can remove the ignoreNetworkEvents: true
call for one or all of the instrumentations that the configDefaults
object is passed into, similar to the below example:
// Turn on for fetch requests only.
const configDefaults = {
ignoreNetworkEvents: true, // <- no change
// propagateTraceHeaderCorsUrls: [
// /.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
// ]
}
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
// '@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
})],
/**************************************/
// Turn on for all requests.
const configDefaults = {
ignoreNetworkEvents: false, // <- Changed from true
// propagateTraceHeaderCorsUrls: [
// /.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
// ]
}
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
})],
To optimize your data collection, we recommend that you add custom attributes that are specific to your application to every span.
You can specify extra attributes through the resourceAttributes
configuration option.
This data will be available on every span your instrumentation emits, which makes it easier to correlate your data to important business information.
// index.js or main.js
// other import statements...
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
const configDefaults = {
ignoreNetworkEvents: true,
// propagateTraceHeaderCorsUrls: [
// /.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
// ]
}
const sdk = new HoneycombWebSDK({
// endpoint: "https://api.eu1.honeycomb.io/v1/traces", // Send to EU instance of Honeycomb. Defaults to sending to US instance.
debug: true, // Set to false for production environment.
apiKey: '[YOUR API KEY HERE]', // Replace with your Honeycomb Ingest API Key.
serviceName: '[YOUR APPLICATION NAME HERE]', // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
})],
resourceAttributes: { // Data in this object is applied to every trace emitted.
"user.id": user.id, // Specific to your app.
"user.role": user.role, // Specific to your app.
},
});
sdk.start();
// Application instantiation 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.
When using the Honeycomb Instrumentation snippet (as documented on this page), uncomment the propagateTraceHeaderCorsUrls
array and add regex to include all target domains.
This method allows you to propagate to your backend services without leaking trace IDs to third-party services.
const configDefaults = {
ignoreNetworkEvents: true,
propagateTraceHeaderCorsUrls: [
/.+/g, // Regex to match your backend URLs. Update to the domains you wish to include.
]
}
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-user-interaction \
@opentelemetry/instrumentation-long-task
yarn add \
@opentelemetry/instrumentation-user-interaction \
@opentelemetry/instrumentation-long-task
@opentelemetry/instrumentation-user-interaction
produces spans for user interactions with a browser web application.
Import the package and add the output to the instrumentations
array in the configuration:
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
const sdk = new HoneycombWebSDK({
apiKey: '[YOUR API KEY HERE]', // Replace with your Honeycomb Ingest API Key.
serviceName: '[YOUR APPLICATION NAME HERE]' // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
}),
new UserInteractionInstrumentation(),
],
});
sdk.start();
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';
const sdk = new HoneycombWebSDK({
apiKey: '[YOUR API KEY HERE]', // Replace with your Honeycomb Ingest API Key.
serviceName: '[YOUR APPLICATION NAME HERE]' // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
}),
new UserInteractionInstrumentation({
eventNames: ['submit', 'click', 'keypress'],
}),
],
});
sdk.start();
To attach extra attributes to user interaction spans, provide a callback function:
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
const sdk = new HoneycombWebSDK({
apiKey: '[YOUR API KEY HERE]', // Replace with your Honeycomb Ingest API Key.
serviceName: '[YOUR APPLICATION NAME HERE]' // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
}),
new UserInteractionInstrumentation({
eventNames: ['submit', 'click', 'keypress'],
shouldPreventSpanCreation: (event, element, span) => {
span.setAttribute('target.id', element.id)
// etc..
}
}),
],
});
sdk.start();
@opentelemetry/instrumentation-long-task
provides automatic instrumentation for Long Task API.
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
const sdk = new HoneycombWebSDK({
apiKey: '[YOUR API KEY HERE]', // Replace with your Honeycomb Ingest API Key.
serviceName: '[YOUR APPLICATION NAME HERE]' // Replace with your application name. Honeycomb uses this string to find your dataset when we receive your data. When no matching dataset exists, we create a new one with this name if your API Key has the appropriate permissions.
instrumentations: [getWebAutoInstrumentations({
// Loads custom configuration for xml-http-request instrumentation.
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
'@opentelemetry/instrumentation-document-load': configDefaults,
}),
new LongTaskInstrumentation({
observerCallback: (span, longtaskEvent) => {
span.setAttribute('location.pathname', window.location.pathname)
}
}),
],
});
sdk.start();
Automatic instrumentation is the easiest way to get started with instrumenting your code, but to get the most insight into your system, you should add custom, or manual, instrumentation where appropriate. To do this, use the OpenTelemetry API to access the currently executing span and add attributes to it, and/or to create new spans.
To learn more about custom, or manual, instrumentation, visit the comprehensive set of topics covered by Manual Instrumentation for JavaScript in OpenTelemetry’s documentation.
Adding custom instrumentation requires the the OpenTelemetry API package. If not installed yet, use this command for installation:
npm install --save @opentelemetry/api
yarn add @opentelemetry/api
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();
});
}
});
Adding attributes to a currently executing span in a trace can be useful. For example, you may have an application or service that handles users, and you want to associate the user with the span when querying your service in Honeycomb. To do this, get the current span from the context and set an attribute with the user ID:
import opentelemetry from '@opentelemetry/api';
function handleUser(user: User) {
let activeSpan = opentelemetry.trace.getActiveSpan();
activeSpan.setAttribute("user.id", user.getId());
}
const opentelemetry = require("@opentelemetry/api");
function handleUser(user) {
let activeSpan = opentelemetry.trace.getActiveSpan();
activeSpan.setAttribute("user.id", user.getId());
}
This will add a user.id
field to the current span so that you can use the field in WHERE
, GROUP BY
or ORDER
clauses in the Honeycomb query builder.
To start manually tracing, you must initialize a tracer.
import opentelemetry from '@opentelemetry/api';
const tracer = opentelemetry.trace.getTracer("tracer.name.here");
const opentelemetry = require("@opentelemetry/api");
const tracer = opentelemetry.trace.getTracer("tracer.name.here");
When you create a Tracer
, OpenTelemetry requires you to give it a name as a string.
This string is the only required parameter.
When traces are sent to Honeycomb, the name of the Tracer
is turned into the library.name
field, which can be used to show all spans created from a particular tracer.
In general, pick a name that matches the appropriate scope for your traces. If you have one tracer for each service, then use the service name. If you have multiple tracers that live in different “layers” of your application, then use the name that corresponds to that “layer”.
The library.name
field is also used with traces created from instrumentation libraries.
You can then use this tracer to create custom spans.
Automatic instrumentation can show the shape of requests to your system, but only you know the truly important parts. To get the full picture of what is happening, you must add custom instrumentation and create some custom spans. To do this, grab the tracer from the OpenTelemetry API:
import opentelemetry from '@opentelemetry/api';
const tracer = opentelemetry.trace.getTracer("my-service-tracer");
function runQuery() {
tracer.startActiveSpan("expensive-query", (span) => {
// ... do cool stuff
span.end();
});
}
const opentelemetry = require("@opentelemetry/api");
const tracer = opentelemetry.trace.getTracer("my-service-tracer");
function runQuery() {
tracer.startActiveSpan("expensive-query", (span) => {
// ... do cool stuff
span.end();
});
}
Sometimes you want to add the same attribute to many spans within the same trace. This attribute may include variables calculated during your program, or other useful values for correlation or debugging purposes.
To add this attribute, leverage the OpenTelemetry concept of baggage.
Baggage allows you to add a key
with a value
as an attribute to every subsequent child span within the current application context.
import {
Context,
context,
propagation,
} from '@opentelemetry/api';
tracer.startActiveSpan('main', (span) => {
span.setAttribute('app.username', name); // add to current span
// new context based on current, with key/values added to baggage
const ctx: Context = propagation.setBaggage(
context.active(),
propagation.createBaggage({ 'app.username': { value: name } })
);
// within the new context, do some work and baggage will be
// applied as attributes on child spans
context.with(ctx, () => {
tracer.startActiveSpan('childSpan', (childSpan) => {
doTheWork();
childSpan.end();
});
});
span.end();
});
tracer.startActiveSpan('main', (span) => {
span.setAttribute('app.username', name); // add to current span
// new context based on current, with key/values added to baggage
const ctx = propagation.setBaggage(
context.active(),
propagation.createBaggage({ 'app.username': { value: name } })
);
// within the new context, do some work and baggage will be
// applied as attributes on child spans
context.with(ctx, () => {
tracer.startActiveSpan('childSpan', (childSpan) => {
doTheWork();
childSpan.end();
});
});
span.end();
});
The OpenTelemetry SDK uses span processors as synchronous hooks for when a span starts and when a span ends. This lets you mutate spans after they have been created by automatic instrumentation or manually. Some examples of the actions you can take on spans in a span processor include:
Here is a basic example of a custom span processor:
class TestSpanProcessorOne implements SpanProcessor {
onStart(span: Span): void {
span.setAttributes({
'processor1.name': 'TestSpanProcessorOne',
});
}
onEnd(): void {}
forceFlush() {
return Promise.resolve();
}
shutdown() {
return Promise.resolve();
}
}
To use it, add the span processor to your configuration when you initialize the Honeycomb Web SDK:
const sdk = new HoneycombWebSDK({
debug: true,
apiKey: 'api-key-goes-here',
serviceName: 'hny-web-distro',
// ... other config
spanProcessors: [new TestSpanProcessorOne()],
});
sdk.start();
Here is an example span processor that adds custom information for users:
import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
import { Span } from '@opentelemetry/api';
export class UserInfoSpanProcessor implements SpanProcessor {
userInfo: { userId: string; customerId: string; role: string } | undefined;
constructor() {
const getUserInfo = (): Promise<{
userId: string;
customerId: string;
role: string;
}> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
userId: '1234',
customerId: '5678',
role: 'admin',
});
}, 2000);
});
};
getUserInfo().then(
(userInfo: { userId: string; customerId: string; role: string }) => {
this.userInfo = userInfo;
},
);
}
onStart(span: Span) {
if (this.userInfo) {
span.setAttributes({
'app.user.id': this.userInfo.userId,
'app.user.customer_id': this.userInfo.customerId,
'app.user.role': this.userInfo.role,
});
}
}
onEnd() {}
forceFlush() {
return Promise.resolve();
}
shutdown() {
return Promise.resolve();
}
}
To use it, add the span processor to your configuration when you initialize the Honeycomb Web SDK:
const sdk = new HoneycombWebSDK({
debug: true,
apiKey: 'api-key-goes-here',
serviceName: 'hny-web-distro',
// ... other configuration
spanProcessors: [new UserInfoSpanProcessor()],
});
sdk.start();
Here is an example span processor that adds attributes to spans based on the state of the React Router.
It sets the page.route
attribute to the generic dynamic route, and records the span as an error if there are errors in the router state.
import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
import { Span } from '@opentelemetry/api';
/**
* SpanProcessor that adds attributes to spans based on the state of the React Router
* Sets the page.route attribute to the generic dynamic route
* Records the span as an error if there are errors in the router state (e.g. 404)
*/
export class ReactRouterSpanProcessor implements SpanProcessor {
router;
route;
constructor({ router }: { router }) {
this.router = router;
this.route =
router.state.matches[router.state.matches.length - 1]?.route.path;
this.router.subscribe((state: any) => {
this.route = state.matches[state.matches.length - 1]?.route.path;
});
}
onStart(span: Span) {
const { errors } = this.router.state;
// If there are errors, set the span status to error and record the error message
if (errors !== null) {
span.setStatus({
code: 2,
message: errors[0].data,
});
}
// Set the page.route as the generic dynamic route, making things easier to query
// e.g. /name/:name/pet/:pet instead of name/123/pet/456
// url.path attribute will have the more specific computed route
span.setAttributes({ 'page.route': this.route });
}
onEnd() {}
forceFlush() {
return Promise.resolve();
}
shutdown() {
return Promise.resolve();
}
}
To use it, add the span processor to your configuration when you initialize the Honeycomb Web SDK:
const sdk = new HoneycombWebSDK({
debug: true,
apiKey: 'api-key-goes-here',
serviceName: 'hny-web-distro',
// ... other configuration
spanProcessors: [new ReactRouterSpanProcessor({ router: router })],
});
sdk.start();
Deterministic head sampling can be used with the Honeycomb Web Instrumentation package, with or without manual instrumentation.
The package will read these variables and expect an integer that represents the sample rate you would like to apply.
For example, a value of 5
means that one out of every five traces will be sent to Honeycomb.
To add sampling to the package, add sampleRate
to the HoneycombWebSDK
:
const sdk = new HoneycombWebSDK({
apiKey: "your-api-key",
serviceName: "your-service-name",
instrumentations: [getWebAutoInstrumentations()],
sampleRate: 5,
})
The value of your sample rate must be a positive integer.
If you have multiple services that communicate with each other, it is important that they have the same sampling configuration. Otherwise, each service might make a different sampling decision, resulting in incomplete or broken traces. You can sample using a standalone proxy as an alternative, like Honeycomb Refinery, or when you have more robust sampling needs.
When a service calls another service, you want to ensure that the relevant trace information is propagated from one service to the other. This allows Honeycomb to connect the two services in a trace.
Distributed tracing enables you to trace and visualize interactions between multiple instrumented services. For example, your users may interact with a front-end API service, which talks to two internal APIs to fulfill their request. In order to have traces connect spans for all these services, it is necessary to propagate trace context between these services, usually by using an HTTP header.
Both the sending and receiving service must use the same propagation format, and both services must be configured to send data to the same Honeycomb environment.
Note: Any Baggage attributes that you set in your application will be attached to outgoing network requests as a header. If your service communicates to a third party API, do NOT put sensitive information in the Baggage attributes.
Trace context propagation is done by sending and parsing headers that conform to the W3C Trace Context specification.
By default, the Honeycomb’s Web Instrumentation package uses the W3C trace context format.
If you opt to use a different trace context specification than W3C, ensure that both the sending and receiving service are using the same propagation format, and that both services are configured to send data to the same Honeycomb environment.
Honeycomb’s Web Instrumentation package can create a link to a trace visualization in the Honeycomb UI for local traces. Local visualizations enables a faster feedback cycle when adding, modifying, or verifying instrumentation.
To enable local visualizations:
Set the local visualizations option to true
in the HoneycombWebSDK
:
const sdk = new HoneycombWebSDK({
apiKey: "your-api-key",
serviceName: "your-service-name",
instrumentations: [getWebAutoInstrumentations()],
localVisualizations: true,
})
The output displays the name of the root span and a link to Honeycomb that shows its trace. For example:
Trace for root-span-name
Honeycomb link: <link to Honeycomb trace>
Select the link to view the trace in detail within the Honeycomb UI.
To enable debugging when running the Honeycomb Web Instrumentation package, set debug
to true
the HoneycombWebSDK
:
const sdk = new HoneycombWebSDK({
apiKey: "your-api-key",
serviceName: "your-service-name",
instrumentations: [getWebAutoInstrumentations()],
localVisualizations: true,
debug: true
})
When the debug setting is enabled, the Honeycomb Web Instrumentation package configures a DiagConsoleLogger that logs telemetry to the console with the log level of Debug.
The debug setting in the Honeycomb Web Instrumentation package will also output to the console the main options configuration, including but not limited to API Key and endpoint.
Keep in mind that printing to the console is not recommended for production and should only be used for debugging purposes.
Honeycomb supports receiving telemetry data via OpenTelemetry’s native protocol, OTLP, over gRPC, HTTP/protobuf, and HTTP/JSON. The minimum supported versions of OTLP protobuf definitions are 0.7.0 for traces and metrics.
If the protobuf version in use by the SDK does not match a supported version by Honeycomb, a different version of the SDK may need to be used. If the SDK’s protobuf version is older than the minimum supported version, and telemetry is not appearing as expected in Honeycomb, upgrade the SDK to a version with the supported protobuf definitions. If using an added dependency on a proto library, ensure the version of protobuf definitions matches the supported version of the SDK.
You may receive a 464
error response from the Honeycomb API when sending telemetry using gRPC and HTTP1.
The gRPC format depends on using HTTP2 and any request over HTTP1 will be rejected by the Honeycomb servers.
To explore common issues when sending data, visit Common Issues with Sending Data in Honeycomb.
The apiKey
variable is used to send your data to Honeycomb.
Make sure you have replaced the placeholder value for it with your Honeycomb Ingest API Key and that your API key permissions include “Can create datasets”.
If Honeycomb is successfully instantiating but your API key is not included, you should see output similar to the following in your browser console:
@opentelemetry/api: Registered a global for diag v1.7.0
@honeycombio/opentelemetry-web: Honeycomb Web SDK Debug Mode Enabled
@honeycombio/opentelemetry-web: API Key configured for traces: '<YOUR_API_KEY>'
@honeycombio/opentelemetry-web: Service Name configured for traces: '<YOUR_SERVICE_NAME>'
@honeycombio/opentelemetry-web: Endpoint configured for traces: 'https://api.honeycomb.io/v1/traces'
@honeycombio/opentelemetry-web: Sample Rate configured for traces: '1'
We use the serviceName
variable to name your dataset in Honeycomb.
Be sure you have replaced the placeholder value for it with a name that you will find useful.
If a “navigator is undefined” error appears when you attempt to start your local server while following Next.js instructions, the instrumentation is being run in a server-side rendering path.
To fix this, try adding the 'use client';
directive at the top of the file where you instantiate Honeycomb’s web instrumentation.
Adding the 'use client';
directive tells React to only execute the file in a client environment.
If you are still seeing an error after adding the 'use client';
directive, try wrapping the function in a try/catch block.
By wrapping the function, you can catch the error and avoid instantiation in server-side environments, ensuring that your application starts up even if the code is executed in a server-side environment.
try {
const sdk = new HoneycombWebSDK({
debug: true,
apiKey: '[YOUR API KEY HERE]' // Replace with your Honeycomb Ingest API Key
serviceName: '[YOUR APPLICATION NAME HERE]', // Replace with your application name. Honeycomb will name your dataset using this variable.
instrumentations: [getWebAutoInstrumentations()], // Adds automatic instrumentation
});
sdk.start();
} catch (e) {}
Every default attribute available in the Honeycomb Web Instrumentation package.
Attribute | Description | Example Value |
---|---|---|
user_agent.original |
User agent string of the browser. | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36 |
browser.mobile |
Indicates whether the browser is on a mobile device. | true , false |
browser.touch_screen_enabled |
Indicates whether the device has a touch screen. | true , false |
browser.language |
Language setting of the browser. | en-US |
browser.name |
Name of the browser. | Chrome |
browser.version |
Browser version. | 91.0.4472.114 |
device.type |
Device type. | desktop , tablet , mobile |
network.effectiveType |
Effective network type. | 5g , 4g , 3g |
screen.width |
Screen width in pixels. | 1440 |
screen.height |
Screen height in pixels. | 900 |
screen.size |
Computed size of the screen based on width. | large |
Attribute | Description | Example |
---|---|---|
entry_page.url |
Full URL of the entry page. | https://example.com |
entry_page.path |
Path of the URL. | /home |
entry_page.search |
Query string of the URL. | ?query=example |
entry_page.hash |
Fragment identifier of the URL. | #section1 |
entry_page.hostname |
Hostname of the URL. | example.com |
entry_page.referrer |
Referrer URL of the entry page. | https://referrer.com |
Attribute | Description | Example |
---|---|---|
honeycomb.distro.version |
Version of the Honeycomb OpenTelemetry distribution. | 1.0.0 |
honeycomb.distro.runtime_version |
Runtime environment of the Honeycomb OpenTelemetry distribution. | browser |
Attribute | Description | Example |
---|---|---|
browser.width |
Inner width of the browser window. | 1920 |
browser.height |
Inner height of the browser window. | 1080 |
page.url |
Full URL of the page. | https://example.com |
page.hash |
Hash portion of the page URL. | #section1 |
page.route |
Pathname of the page URL. | /home |
page.hostname |
Hostname of the page URL. | example.com |
page.search |
Query string portion of the page URL. | ?query=example |
url.path |
Pathname of the URL. | /home |
session.id |
Randomly generated session ID. Generated during SDK initialization and does not persist in storage. | 004f84cb95b6cd7bd00e5f38b37e2f7e |
Attribute | Description | Example |
---|---|---|
lcp.id |
Unique identifier for the metric. | v4-1724854181686-4396946267816 |
lcp.delta |
Change in value since the last measurement in milliseconds. | 200 |
lcp.value |
Current value of LCP, in milliseconds. Measures how long it took the largest element on the page to paint. | 200 |
lcp.rating |
Metric rating. | good , needs-improvement , poor |
lcp.navigation_type |
Type of navigation that occurred. | restore , navigate , reload , back-forward , prerender , back-forward-cache |
lcp.element |
HTML element associated with the metric. | #my-element>div.menu>h1 |
lcp.url |
URL of the LCP image resource. If the LCP element is a text node, this value will not be set. | https://my-image.com/image.png |
lcp.time_to_first_byte |
Time between when the user initiates loading the page and the browser receives the first byte of the response (TTFB), in milliseconds. | 200 |
lcp.resource_load_delay |
Delta between TTFB and when the browser starts loading the LCP resource, in milliseconds. If there is no resource, returns 0 . |
400 |
lcp.resource_load_duration |
Total time it takes to load the LCP resource, in millseconds. If no resource exists, returns 0 . |
200 |
lcp.element_render_delay |
Delta between when the LCP resource finishes loading and the LCP element is fully rendered, in milliseconds. | 200 |
lcp.resource_load_time |
Same as lcp.resource_load_duration . Will be deprecated in a future version. |
200 |
Attribute | Description | Example |
---|---|---|
cls.id |
Unique identifier for the metric. | v4-1724856546003-8941918093361 |
cls.delta |
Change in value since the last measurement. | 0.1 |
cls.value |
Current value of CLS. Tracks how much visible content shifts in the viewport. | 0.1 |
cls.rating |
Metric rating. | good , needs-improvement , poor |
cls.navigation_type |
Type of navigation that occurred. | restore , navigate , reload , back-forward , prerender , back-forward-cache |
cls.largest_shift_target |
Selector identifying the first element (in document order) that shifted when the single largest layout shift contributing to the page’s CLS score occurred. | #my-element>div.menu>h1 |
cls.element |
Same as cls.largest_shift_target . |
#my-element>div.menu>h1 |
cls.largest_shift_time |
Timestamp indicating when the single largest layout shift contributing to the page’s CLS score occurred. | DOMHighResTimeStamp |
cls.largest_shift_value |
Layout shift score of the single largest layout shift contributing to the page’s CLS score. | 0.1 |
cls.load_state |
Document loading state when the largest layout shift contribution to the page’s CLS score occurred. | loading , dom-content-loading , complete , dom-interactive |
cls.had_recent_input |
Indicates whether recent user input occurred before the largest layout shift. | true , false |
Attribute | Description | Example |
---|---|---|
inp.id |
Unique identifier for the metric. | v4-1724856546003-8941918093361 |
inp.delta |
Change in value since the last measurement. | 100 |
inp.value |
Current value of INP, in milliseconds. Measures the time between when a user interacts with a web page (clicks, taps, or keyboard inputs) and the next time the page visually updates in response to that interaction. | 100 |
inp.rating |
Metric rating. | good , needs-improvement , poor |
inp.navigation_type |
Type of navigation that occurred. | restore , navigate , reload , back-forward , prerender , back-forward-cache |
inp.input_delay |
Delay between user input and the start of input processing. | 120 |
inp.interaction_target |
Selector identifying the element the user first interacted with as part of the frame where the INP candidate interaction occurred. If this value is an empty string, usually this means the element was removed from the DOM after the interaction. | button#submit |
inp.interaction_time |
Timestamp indicating when the user first interacted during the frame where the INP candidate interaction occurred. If more than one interaction occurred within the frame, only the first time is reported. | DOMHighResTimeStamp |
inp.interaction_type |
Type of interaction, based on the event type of the event entry for the interaction (the first event entry containing an interactionId dispatched in a given animation frame). For pointerdown , pointerup , or click events, this will be pointer . For keydown or keyup events, this will be keyboard . |
pointer , keyboard |
inp.load_state |
Loading state of the document at the time when the interaction corresponding to INP occurred. If the interaction occurred while the document was loading and executing the script (usually in the dom-interactive phase), it can result in long delays. |
loading , dom-content-loading , complete , dom-interactive |
inp.next_paint_time |
Time of the next paint after the interaction. | DOMHighResTimeStamp |
inp.presentation_delay |
Time between when the browser finished processing all event listeners for the user interaction and the next frame was presented on the screen and visible to the user. This time includes work on the main thread (such as requestAnimationFrame() callbacks, ResizeObserver and IntersectionObserver callbacks, and style/layout calculation) as well as off-main-thread work (such as compositor, GPU, and raster work). |
50 |
inp.processing_duration |
Time between when the first event listener started running in response to the user interaction and when all event listener processing had finished. | 30 |
inp.duration |
Sum of input delay, processing duration, and presentation delay. | 150 |
inp.element |
Same as inp.interaction_target . Will be deprecated in a future version. |
button#submit |
inp.event_type |
Same as inp.interaction_type . Will be deprecated in a future version. |
pointer , keyboard |
Attribute | Description | Example |
---|---|---|
inp.duration |
Duration of the performance entry. | DOMHighResTimeStamp |
inp.entryType |
Type of the performance entry. | mark , measure |
inp.name |
Name of the performance entry. | name |
inp.renderStart |
Render start time of the performance entry. | DOMHighResTimeStamp |
inp.startTime |
Start time of the performance entry. | DOMHighResTimeStamp |
Attribute | Description | Example |
---|---|---|
inp.entry_type |
Type of the script performance entry. | script |
inp.start_time |
Time when the function was invoked, which indicates the start time of the script rather than the start time of the frame. (Each entry in the performance timeline has a start time.) | DOMHighResTimeStamp |
inp.execution_start |
Time after compilation (if this script was parsed/compiled). Otherwise, matches inp.start_time . |
DOMHighResTimeStamp |
inp.duration |
Duration between start time and the time that the subsequent microtask queue finished processing. | 1.55 |
inp.forced_style_and_layout_duration |
Total time spent in forced layout/style inside this function. | 100 |
inp.invoker |
Various pieces of information about the invoker of the script. For callbacks, Object.functionName of the invoker (example: Window.setTimeout ). For element event listeners, TAGNAME#id.onevent or TAGNAME[src=src].onevent . For script blocks, the script source URL. For promises, the invoker of the promise (example: Window.fetch.then ). For promise resolvers, all of the handlers of the promise, mixed together as one long script. |
IMG#id.onload , Window.requestAnimationFrame , Response.json.then |
inp.pause_duration |
Total time spent in pausing synchronous operations (alert, synchronous XHR). | 100 |
inp.source_url |
Source URL of the script. | https://example.com |
inp.source_function_name |
Name of the source function. | myFunction |
inp.source_char_position |
Character position in the source. | 256 |
inp.window_attribution |
Relationship between the (same-origin) window where this script was executed and this window. | self , descendant , ancestor , same-page , other |
Attribute | Description | Example |
---|---|---|
fcp.id |
Unique identifier for the metric. | v4-1724856546003-8941918093361 |
fcp.delta |
Change in value since the last measurement. | 100 |
fcp.value |
Current value of FCP, in milliseconds. Measures the time between when the page starts loading and any part of the page’s content is rendered on the screen. | 100 |
fcp.rating |
Metric rating. | good , needs-improvement , poor |
fcp.navigation_type |
Type of navigation that occurred. | restore , navigate , reload , back-forward , prerender , back-forward-cache |
fcp.time_to_first_byte |
Time between when the user initiates loading the page and the browser receives the first byte of the response (also known as TTFB). | 200 |
fcp.time_since_first_byte |
Delta between the TTFB and the FCP. | 500 |
fcp.load_state |
Current state of the page load process. | loading , dom-content-loading , complete , dom-interactive |
Attribute | Description | Example |
---|---|---|
ttfb.id |
Unique identifier for the metric. | v4-1724856546003-8941918093361 |
ttfb.delta |
Change in value since the last measurement. | 100 |
ttfb.value |
Current value of TTFB, in milliseconds. Measures the time between when the user initiates loading the page and the browser receives the first byte of the response. | 100 |
ttfb.rating |
Metric rating. | good , needs-improvement , poor |
ttfb.navigation_type |
Type of navigation that occurred. | restore , navigate , reload , back-forward , prerender , back-forward-cache |
ttfb.waiting_duration |
Time spent waiting for the server to respond to the request. | 150 |
ttfb.dns_duration |
Time taken to resolve the DNS for the request. | 50 |
ttfb.connection_duration |
Time taken to establish a connection to the server. | 100 |
ttfb.request_duration |
Time taken for the server to process the request and send the first byte of the response. | 200 |
ttfb.cache_duration |
Time taken to retrieve the response from the cache, if applicable. | 20 |
ttfb.waiting_time |
Deprecated: Time spent waiting for the server to respond to the request. | 150 |
ttfb.dns_time |
Deprecated: Time taken to resolve the DNS for the request. | 50 |
ttfb.connection_time |
Deprecated: Time taken to establish a connection to the server. | 100 |
ttfb.request_time |
Deprecated: Time taken for the server to process the request and send the first byte of the response. | 200 |
This will be deprecated in a future version of web-vitals. Use INP instead.
Attribute | Description | Example |
---|---|---|
fid.id |
Unique identifier for the metric. | v4-1724856546003-8941918093361 |
fid.delta |
Change in value since the last measurement. | 100 |
fid.value |
Current value of FID, in milliseconds. Measures the time between when the user initiates loading the page and the browser receives the first byte of the response. | 100 |
fid.rating |
Metric rating. | good , needs-improvement , poor |
fid.navigation_type |
Type of navigation that occurred. | restore , navigate , reload , back-forward , prerender , back-forward-cache |
fid.element |
Selector identifying the element interacted with by the user. The element will be the target of the event dispatched. |
button#submit |
fid.event_type |
type of the event dispatched from the user interaction. |
click |
fid.load_state |
State of the page load when the event occurred. | loading , dom-content-loading , complete , dom-interactive |