Browse resources

Send data

Browser script

Record page loads, fetches and errors on your website with one script tag.

One script tag records page loads, Web Vitals, every fetch() and XMLHttpRequest, route changes and uncaught errors on your website, and links them to your backend's spans. It has no dependencies and is only a few kilobytes.

Install

Add it to every page, ideally in <head>. Create the ingest key on the project's Send data page.

<script
  src="https://YOUR-INSTANCE/t.js"
  data-key="kn_YOUR_KEY"
  data-service="my-website"
  data-propagate="api.example.com,*.example.net"
  defer
></script>
AttributeRequiredWhat it does
data-keyYesThe project's ingest key.
data-serviceNoservice.name for these spans. Default: the page's hostname.
data-propagateNoComma-separated hosts that also get a traceparent header.*.example.com matches subdomains. Your own origin always does.
data-endpointNoWhere to send spans. Default: the origin the script was loaded from.
Allow the traceparent header in CORS
Cross-origin APIs listed in data-propagate must accept a traceparent request header (Access-Control-Allow-Headers: traceparent), or the browser blocks the request. Only list hosts you control.

What it records

Page loads

A page load /path span for the initial load, with child spans for the dns, connect, request, response, dom and load phases, and the attributes page.ttfb_ms, page.dom_content_loaded_ms, page.load_ms, url.path and document.referrer.

Web Vitals

A web vitals /path span in the page load's trace, sent when the tab is hidden or closed — Vitals only settle once the visit is over. It carries web_vital.lcp_ms (largest contentful paint) with the web_vital.lcp_element tag name, web_vital.cls (cumulative layout shift, the worst 5-second window) and web_vital.inp_ms (interaction to next paint, taken as the slowest interaction rather than the spec's percentile). TTFB is on the page load span as page.ttfb_ms. Each metric is only reported where the browser supports it, so Safari sends fewer of them, and the span covers the whole visit rather than each route.

fetch() and XMLHttpRequest calls

A client span named like GET api.example.com/v1/orders with http.request.method, url.full, server.address and http.response.status_code. Responses of 500 and above, and network failures, are errors. Query strings are left out of URLs because they often carry tokens. XMLHttpRequest is traced the same way, including the traceparent header; a request that fails, times out or is aborted has no status code and is an error.

Route changes

Single-page navigations — pushState, replaceState and back/forward — become a route change /path span with url.path, url.full, page.previous_path and page.navigation_type (push, replace or popstate). Only the path is compared, so a navigation that changes the query string or hash alone isn't recorded, and route changes aren't page loads: no navigation timing exists for them.

Errors

Uncaught errors and unhandled promise rejections become an error: message span with an exception event (type, message, stack trace), plus exception.source and url.path.

Resource attributes

Every span carries service.name, user_agent.original and a random session.id that lasts for the browser tab.

Custom spans

The script exposes window.knotel. Use optional chaining so your code keeps working if the script is blocked.

Wrap a function
await window.knotel?.trace("checkout", async (span) => {
  span.setAttribute("cart.items", 3);
  await submitOrder();
});
Start and end a span yourself
const span = window.knotel?.startSpan("upload", { "file.size": file.size });
try {
  await upload(file);
} catch (err) {
  span?.recordException(err);
  throw err;
} finally {
  span?.end();
}
MethodDescription
trace(name, fn, attributes?)Run fn in a span. Async functions end the span when they settle; throws are recorded as errors.
startSpan(name, attributes?)Start a span; call end() when done.
flush()Send queued spans now.
span.setAttribute(key, value)Add or change an attribute. setAttributes takes an object.
span.addEvent(name, attributes?)Record a point in time inside the span.
span.recordException(err)Attach the exception and mark the span as an error.

Custom spans start their own traces: the browser has no request context to nest them under, so fetches made inside them appear as separate traces.

Delivery

Spans are batched: they're sent 2 seconds after the first one is queued, or at once when 50 are waiting, and flushed with keepalive when the tab is hidden or closed. At most 1,000 spans wait in memory if your instance is unreachable, and sending never throws into your code. The script is served with a 5-minute cache and open CORS.