The Honeycomb OpenTelemetry Web SDK is Honeycomb’s distribution of OpenTelemetry for web applications. It includes instrumentation for things like Core Web Vitals as well as instrumentation provided by the standard OpenTelemetry distribution for JavaScript.
This page covers basic usage of the SDK, you can find more examples on GitHub.
Before you can use Honeycomb’s OpenTelemetry Web SDK, you need to install it.
Navigate to the root directory of your web application and install the package:
npm install @honeycombio/opentelemetry-web @opentelemetry/auto-instrumentations-web
yarn add @honeycombio/opentelemetry-web @opentelemetry/auto-instrumentations-web
| 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.
Initialize the SDK at the start of your web application. This ensures that events such as startup time and early asset loads are captured.
import { HoneycombWebSDK, WebVitalsInstrumentation } from '@honeycombio/opentelemetry-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
const sdk = new HoneycombWebSDK({
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
apiKey: "YOUR-API-KEY",
serviceName: "YOUR-SERVICE-NAME",
// add automatic instrumentation
instrumentations: [getWebAutoInstrumentations(), new WebVitalsInstrumentation()],
});
sdk.start();
The Honeycomb OpenTelemetry Web SDK can be configured using these configuration options. For example:
import { HoneycombWebSDK, WebVitalsInstrumentation } from '@honeycombio/opentelemetry-web';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
const sdk = new HoneycombWebSDK({
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
apiKey: "YOUR-API-KEY",
serviceName: "YOUR-SERVICE-NAME",
debug: true,
sampleRate: 40,
instrumentations: [
getWebAutoInstrumentations(),
new WebVitalsInstrumentation()
],
webVitalsInstrumentationConfig: {
vitalsToTrack: ['CLS', 'FCP', 'INP', 'LCP', 'TTFB'],
lcp: {
dataAttributes: ['hello', 'world'],
},
},
resourceAttributes: {
"user.kind": user.kind, // Specific to your app.
},
spanProcessors: [
new ExampleSpanProcessor();
],
});
sdk.start();
Resource attributes are available on every span your instrumentation emits. Adding custom, application-specific attributes makes it easier to correlate your data to important business information.
You can set resource attributes using the resourceAttributes configuration option.
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
const sdk = new HoneycombWebSDK({
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
apiKey: "YOUR-API-KEY",
serviceName: "YOUR-SERVICE-NAME",
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();
The SDK includes optional deterministic head sampling.
The sample rate is 1 by default, meaning every trace is exported.
The example below sets a sampleRate of 40, meaning 1 in 40 traces will be exported.
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
const sdk = new HoneycombWebSDK({
apiKey: "YOUR-API-KEY",
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
serviceName: "YOUR-SERVICE-NAME",
sampleRate: 40,
});
sdk.start();
In production, we recommend running an OpenTelemetry Collector. Your application sends telemetry to your Collector instead of directly to Honeycomb. Your Collector then forwards the telemetry data to Honeycomb, keeping your API key stored securely in the Collector’s configuration.
Configure your Collector’s URL by setting the endpoint option when initializing the Honeycomb Web SDK:
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
const sdk = new HoneycombWebSDK({
endpoint: "http(s)://YOUR-COLLECTOR-URL",
serviceName: "YOUR-SERVICE-NAME",
skipOptionsValidation: true // because we are not including apiKey
});
sdk.start();
To send telemetry data directly to Honeycomb, set the apiKey option with your Ingest API Key.
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
const sdk = new HoneycombWebSDK({
apiKey: "YOUR-API-KEY",
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
serviceName: "YOUR-SERVICE-NAME",
});
sdk.start();
Configure web vitals instrumentation by passing a WebVitalsInstrumentationConfig object with your options.
import { HoneycombWebSDK } from '@honeycombio/opentelemetry-web';
const sdk = new HoneycombWebSDK({
apiKey: "YOUR-API-KEY",
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
serviceName: "YOUR-SERVICE-NAME",
webVitalsInstrumentationConfig: {
vitalsToTrack: ['CLS', 'FCP', 'INP', 'LCP', 'TTFB'],
lcp: {
dataAttributes: ['hello', 'barBiz'],
},
},
});
sdk.start();
| name | required? | type | default value | description |
|---|---|---|---|---|
| enabled | optional | boolean | true |
Whether or not to enable this automatic instrumentation. |
| lcp | optional | VitalOpts | undefined |
Pass-through configuration options for web-vitals. Refer to Google Chrome’s ReportOpts. |
| lcp.applyCustomAttributes | optional | function | undefined |
A function for adding custom attributes to core web vitals spans. |
| lcp.dataAttributes | optional | string[] |
undefined |
An array of attribute names to filter reported as lcp.element.data.someAttr undefined will send all data-* attribute-value pairs. [] will send none ['myAttr'] will send the value of data-my-attr or '' if it’s not supplied. Note: An attribute that’s defined, but that has no specified value, such as <div data-my-attr />, will be sent as {lcp.element.data.myAttr: '' }, which is inline with the dataset API. |
| cls | optional | VitalOpts | undefined |
Pass-through configuration options for web-vitals. Refer to Google Chrome’s ReportOpts. |
| cls.applyCustomAttributes | optional | function | undefined |
A function for adding custom attributes to core web vitals spans. |
| inp | optional | VitalOptsWithTimings | undefined |
Pass-through configuration options for web-vitals. Refer to Google Chrome’s ReportOpts. |
| inp.applyCustomAttributes | optional | function | undefined |
A function for adding custom attributes to core web vitals spans. |
| inp.includeTimingsAsSpans | optional | boolean | false |
When set to true, this option will emit PerformanceLongAnimationFrameTiming and PerformanceScriptTiming as spans. |
| fcp | optional | VitalOpts | undefined |
Pass-through configuration options for web-vitals. Refer to Google Chrome’s ReportOpts. |
| fcp.applyCustomAttributes | optional | function | undefined |
A function for adding custom attributes to core web vitals spans. |
| ttf | optional | VitalOpts | undefined |
Pass-through configuration options for web-vitals. Refer to Google Chrome’s ReportOpts. |
| ttf.applyCustomAttributes | optional | function | undefined |
A function for adding custom attributes to core web vitals spans. |
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.
Enable local visualizations by setting the localVisualizations configuration option to true.
const { HoneycombWebSDK } = require("@honeycombio/opentelemetry-web");
const sdk = new HoneycombWebSDK({
apiKey: "YOUR-API-KEY",
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
serviceName: "YOUR-SERVICE-NAME",
localVisualizations: true,
debug: true,
});
The output displays the name of the root span and a link to Honeycomb that shows its trace. Select the link to view the trace in detail within the Honeycomb UI.
Trace for root-span-name
Honeycomb link: <link to Honeycomb trace>
Turn on debug logging by setting the debug configuration option to true.
const { HoneycombWebSDK } = require("@honeycombio/opentelemetry-web");
const sdk = new HoneycombWebSDK({
apiKey: "YOUR-API-KEY",
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
serviceName: "YOUR-SERVICE-NAME",
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.
The Honeycomb OpenTelemetry Web SDK includes auto-instrumentation for:
Automatic instrumentation is enabled by default.
You can enable or disable individual auto-instrumentation libraries in your configuration using the instrumentations option.
import { HoneycombWebSDK, WebVitalsInstrumentation } 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({
// Uncomment the line below to send to EU instance. Defaults to US.
// endpoint: "https://api.eu1.honeycomb.io:443",
apiKey: "YOUR-API-KEY",
serviceName: "YOUR-SERVICE-NAME",
instrumentations: [
getWebAutoInstrumentations({
// optionally apply defaults config to instrumentation
'@opentelemetry/instrumentation-xml-http-request': configDefaults,
'@opentelemetry/instrumentation-fetch': configDefaults,
// optionally disable document load instrumentation
'@opentelemetry/instrumentation-document-load': { enabled: false },
}),
new WebVitalsInstrumentation()
],
});
sdk.start();
Automatic instrumentation is a fast way to instrument your code, but you get more insight into your application by adding custom, otherwise known as manual, instrumentation.
Adding custom instrumentation requires the the OpenTelemetry API package.
npm install --save @opentelemetry/api
yarn add @opentelemetry/api
You can retrieve the currently active span in a trace and add attributes to it. This lets you add more context to traces and gives you more ways to group or filter traces in your queries.
import { trace } from '@opentelemetry/api';
function handleUser(user: User) {
let currentActiveSpan = trace.getActiveSpan();
currentActiveSpan.setAttribute('user.id', user.getId());
}
const { trace } = require("@opentelemetry/api");
function handleUser(user) {
let activeSpan = trace.getActiveSpan();
activeSpan.setAttribute("user.id", user.getId());
}
For manual tracing, you need to get a tracer:
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer("tracer.name.here");
const { trace } = require("@opentelemetry/api");
const tracer = 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.
Create custom spans to get a clear view of the critical parts in your application.
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('example-tracer', '0.1.0');
function trackWork() {
const span = tracer.startActiveSpan('do work');
console.log('performing work');
span.end();
}
const { trace } = require("@opentelemetry/api");
const tracer = trace.getTracer("my-service-tracer");
function trackWork() {
const span = tracer.startActiveSpan('do work');
console.log('performing work');
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 Honeycomb Web 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();
A Context Manager stores and propagates global span context through your system. OpenTelemetry provides a context manager for browser instrumentation based on the Zone.js library to track global context across asynchronous execution threads. This context manager can be added used in your instrumentation like so:
import { ZoneContextManager } from '@opentelemetry/context-zone';
const sdk = new HoneycombWebSDK({
// other config options omitted...
contextManager: new ZoneContextManager()
});
sdk.start();
Zone.js has known limitations with async/await code, and requires your code to be transpiled down to ES2015. It may also negatively impact your application’s performance.
For these reasons, the Honeycomb Web SDK does not enable the Zone.js context manager by default.
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();
}
});
};
Running into issues? Here are some common problems and ways to fix them.
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) {}
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.