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>
| Attribute | Required | What it does |
|---|---|---|
data-key | Yes | The project's ingest key. |
data-service | No | service.name for these spans. Default: the page's hostname. |
data-propagate | No | Comma-separated hosts that also get a traceparent header.*.example.com matches subdomains. Your own origin always does. |
data-endpoint | No | Where to send spans. Default: the origin the script was loaded from. |
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.
await window.knotel?.trace("checkout", async (span) => {
span.setAttribute("cart.items", 3);
await submitOrder();
});const span = window.knotel?.startSpan("upload", { "file.size": file.size });
try {
await upload(file);
} catch (err) {
span?.recordException(err);
throw err;
} finally {
span?.end();
}| Method | Description |
|---|---|
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.