Send Browser Data with Honeycomb Web Instrumentation

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.

Tip
If you’re using micro frontend architecture, visit Observability and Micro Frontends to see our recommendations for implementing OpenTelemetry and the Honeycomb OpenTelemetry Web SDK.

Why Use the Honeycomb Web Instrumentation package? 

Honeycomb’s Web Instrumentation package gives you all of the capabilities provided by the generic OpenTelemetry Distribution for JavaScript and also allows you to:

  • more easily initialize OpenTelemetry for use with Honeycomb
  • more easily add multi-span attributes
  • more easily configure sampling
  • link directly to your traces in Honeycomb with local visualizations
  • keep any custom instrumentation if you switch to the generic OpenTelemetry Distribution later
  • add automatic instrumentation for Core Web Vitals and customize attributes on those spans

Before You Begin 

Before you can set up instrumentation for your application, you will need to do a few things.

Get Your Honeycomb API Key 

To send data to Honeycomb, you need to:

  1. Sign up for a free Honeycomb account. To sign up, decide whether you would like Honeycomb to store your data in a US-based or EU-based location, then create a Honeycomb account in the US or create a Honeycomb account in the EU. Signup is free!
  2. Create a Honeycomb Ingest API Key. To get started, you can create a key that you expect to swap out when you deploy to production. Name it something helpful, perhaps noting that it’s a Getting Started key. Make note of your API key; for security reasons, you will not be able to see the key again, and you will need it later!
Tip
For setup, make sure you select the “Can create datasets” checkbox so that your data will show up in Honeycomb. Later, when you replace this key with a permanent one, you can uncheck that box.

Install the Honeycomb Web Instrumentation Package 

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'
Tip
If your repo does not contain a 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.

Configuration Options for Browser Telemetry 

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.

Tip

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 Honeycomb Ingest API Key. 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.

You can also configure the Collector to symbolicate your JavaScript stack traces. This translates obfuscated or minified names from your stack traces into human-readable formats.

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.

Example Express Proxy 

To accept POSTs 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.

Advanced Configuration 

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 the 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 This sends 1/n of all spans, so a sample rate of 5 would send 20% of your spans. Sampling is performed by default on a per-trace level. Adding sampling will not break your traces. Either all spans in a trace will be sent, or no spans in the trace will be sent.
tracesEndpoint optional string ${endpoint}/v1/traces Populate this option to send traces to a route other than /v1/traces.
debug optional boolean false Enable additional logging.
skipOptionsValidation optional boolean false Do not require any fields. Use with OpenTelemetry Collector.
dataset optional string Populate this option only if your Honeycomb environment is a Honeycomb Classic environment.
spanProcessors optional SpanProcessor[] Sets an array of span processors to apply to all generated spans.
traceExporters optional SpanExporter[] Sets an array of span exporters
disableDefaultTraceExporter optional boolean false Disable default honeycomb trace exporter. You can provide additional exporters via traceExporters config option.
webVitalsInstrumentationConfig optional object { enabled: true } Refer to WebVitalsInstrumentationConfig.
globalErrorsInstrumentationConfig optional object { enabled: true } Refer to GlobalErrorsInstrumentationConfig.
logLevel optional DiagLogLevel DiagLogLevel.DEBUG Controls the verbosity of logs printed to the console.
contextManager optional ContextManager Sets a Context Manager for managing global span context. Refer to Context Management for more details.
Note
* 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.

WebVitalsInstrumentationConfig 

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.

    GlobalErrorsInstrumentationConfig 

    name required? type default value description
    enabled optional boolean true Whether or not to enable this automatic instrumentation.
    enabled optional boolean true Whether or not to enable this automatic instrumentation.
    applyCustomAttributesOnSpan optional function n/a A callback function for adding custom attributes to the span when an error is recorded. Will automatically be applied to all spans generated by the automatic instrumentation.

    Network Events and Telemetry Volume 

    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,
      })],
    

    Add Application-Specific Resource Attributes 

    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
    

    Connecting the Frontend and Backend Traces 

    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.

    Automatically Propagate the Trace Context Header 

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

    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.
      ]
    }
    

    Manually Propagate the Trace Context Header 

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

    // General request handler, instrumented with OTel
    // Forwards traceparent header to connect spans created in the browser
    // with spans created on the backend
    const request = async (url, method = 'GET', headers, body) => {
      return trace
        .getTracer('request-tracer')
        .startActiveSpan(`Request: ${method} ${url}`, async (span) => {
    
          // construct W3C traceparent header
          const traceparent = `00-${span.spanContext().traceId}-${span.spanContext().spanId}-01`;
    
          try {
            const response = await fetch(url, {
              method,
              headers: {
                ...headers,
                // set traceparent header
                traceparent: traceparent,
              },
              body,
            });
            span.setAttributes({
              'http.method': method,
              'http.url': url,
              'response.status_code': response.status,
            });
            if (response.ok && response.status >= 200 && response.status < 400) {
              span.setStatus({ code: SpanStatusCode.OK });
              return response.text();
            } else {
              throw new Error(`Request Error ${response.status} ${response.statusText}`);
            }
          } catch (error) {
            span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
            throw new Error(error);
          } finally {
            span.end();
          }
        });
    };
    

    Other Automatic Instrumentation 

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

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

    User Interaction 

    @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();
    

    Long Tasks 

    @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();
    

    Add Custom Instrumentation 

    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.

    Acquire Dependencies 

    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();
        });
      }
    });
    

    Add Attributes to Spans 

    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.

    Initialize a Tracer 

    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.

    Create New 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();
      });
    }
    

    Add Multi-Span Attributes 

    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();
    });
    

    Custom Span Processing 

    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:

    • Add attributes
    • Add span events
    • Add span links
    • Update the name of a span
    • Record an exception on a span
    • Get the span context to create child spans
    • Stop spans from being sent

    Example: Basic Custom Span Processor 

    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();
    

    Example: Adding User Information After SDK Initialization 

    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();
    

    Example: Dynamic page routes with React Router 

    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();
    

    Sampling 

    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.

    Distributed Trace Propagation 

    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.

    Visualizing Traces Locally 

    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:

    1. 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>
      
    2. Select the link to view the trace in detail within the Honeycomb UI.

    Note
    In production, disable local visualization. Local visualization creates additional overhead to create the link to a trace in Honeycomb and print it to the console.

    Debug Mode 

    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.

    OTLP Protobuf Definitions 

    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 1.0 for traces, metrics, and logs.

    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.

    Receiving 464 Errors 

    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.

    Troubleshooting 

    To explore common issues when sending data, visit Common Issues with Sending Data in Honeycomb.

    Dataset Not Appearing 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'
    

    Dataset in Honeycomb Has Unexpected Name 

    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.

    Next.js Application “Navigator Is Undefined” Error 

    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) {}
    

    Available Attributes 

    Every default attribute available in the Honeycomb Web Instrumentation package.

    Browser Resource Attributes 

    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

    Entry Page Resource Attributes 

    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

    Honeycomb Metadata Resource Attributes 

    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

    Browser span attributes 

    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

    Largest Contentful Paint (LCP) Attributes 

    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

    Cumulative Layout Shift (CLS) Attributes 

    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

    Interaction to Next Paint (INP) Attributes 

    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

    Interaction to Next Paint (INP) With Long Animation Frame Timing Attributes 

    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

    Interaction to Next Paint (INP) With Performance Script Timing Attributes 

    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

    First Contentful Paint (FCP) 

    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

    Time To First Byte (TTFB) Attributes 

    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