@kloudmate/rum-web
v2.8.0
Published
KloudMate Browser RUM SDK — traces, session replay, and behavioral analytics
Maintainers
Readme
@kloudmate/rum-web — KloudMate Browser RUM SDK
Real User Monitoring with traces, session replay, and behavioral analytics.
Install
npm install @kloudmate/rum-webimport { init, addEvent, setUser } from '@kloudmate/rum-web';
init({
endpoint: 'https://otel.kloudmate.com:4318',
rumAccessToken: 'pk_your_public_key',
applicationName: 'MyApp',
deploymentEnvironment: 'production',
version: '1.0.0',
});Call init() once, as early as you can. The SDK captures only what happens
after it runs. TypeScript types are bundled, and require() also works.
Quick start (CDN)
Put this in your <head>. The loader is async, so it never blocks rendering.
<script async crossorigin="anonymous"
src="https://cdn.kloudmate.com/rum/js/v2/km-rum.umd.min.js"
onload="KloudMateRum.init({ endpoint: 'https://otel.kloudmate.com:4318', rumAccessToken: 'pk_your_public_key', applicationName: 'MyApp' })"></script>That covers the basic setup. To also record session replay, and to tag the user after login:
<script async crossorigin="anonymous"
src="https://cdn.kloudmate.com/rum/js/v2/km-rum.umd.min.js"
onload="KloudMateRum.init({
endpoint: 'https://otel.kloudmate.com:4318',
rumAccessToken: 'pk_your_public_key',
applicationName: 'MyApp',
deploymentEnvironment: 'production',
sessionRecorder: { enabled: true, sampleRate: 0.25 }
})"></script>// after your user logs in
KloudMateRum.setUser({ id: 'u123', email: '[email protected]' });Use the OTLP ingest endpoint shown for your KloudMate workspace.
Do you call the SDK before it loads? If you call
addEventorsetUserduring the first render, before the async bundle arrives, add the small loader stub. It queues those early calls so none are lost. Most apps do not need it.
Which CDN path to use
| Path | Gets | Use when |
|---|---|---|
| /rum/js/v2/… | patches and features, never a major | recommended — you want fixes without editing your page |
| /rum/js/latest/… | everything, majors included | you accept a major upgrade landing unannounced |
| /rum/js/v2.1.0/… | nothing, ever | you pin a version and upgrade on your own schedule |
The exact-version path is immutable and cached for a year. The rolling paths have a five-minute TTL, so a release reaches your users within minutes.
Loader stub
You need this only if you call the SDK before the async bundle finishes loading. Paste it before the loader. It queues early calls, and the SDK replays them the moment it arrives, so nothing is lost:
<script>
window.KloudMateRum=window.KloudMateRum||{};KloudMateRum.q=KloudMateRum.q||[];
['init','setUser','setGlobalAttributes','addEvent','recordException','endSession']
.forEach(function(m){KloudMateRum[m]=KloudMateRum[m]||function(){KloudMateRum.q.push([m,[].slice.call(arguments)])}});
</script>
<script async crossorigin="anonymous" src="https://cdn.kloudmate.com/rum/js/v2/km-rum.umd.min.js"></script>
<script>KloudMateRum.init({ endpoint: 'https://otel.kloudmate.com:4318', rumAccessToken: 'pk_your_public_key', applicationName: 'MyApp' });</script>Sampling
There are two separate rates, and they combine:
| Setting | Controls | Default |
|---|---|---|
| sampleRate | whether a session sends anything — spans, errors, custom events, replay | 1 (all sessions) |
| sessionRecorder.sampleRate | whether a sampled session is also recorded for replay | 1 (all sampled sessions) |
KloudMateRum.init({
sampleRate: 0.25, // 25% of sessions send telemetry
sessionRecorder: { sampleRate: 0.1 }, // of those, 10% also get replay — 2.5% overall
});sampleRate controls your ingest volume. The SDK checks it in init(), before
it installs any instrumentation. A session outside the rate adds no listeners
and makes no requests at all. This is not "capture then discard".
Both decisions are fixed for the life of the session and stored separately, so you get whole sessions, not fragments of many. A fresh roll on each page load breaks this. A 25% rate then gives one-page slivers of most sessions, instead of full recordings of a quarter of them.
Replay is sampled separately because it is by far the heaviest signal. It is normal to keep 100% of spans, but record only a small fraction of sessions.
Bot exclusion
The SDK drops automated and crawler traffic by default. A bot never starts a session, so it produces no spans, no replay, and no requests. This also keeps bots out of your session counts and off your bill.
The SDK treats a visitor as a bot in two cases: when navigator.webdriver is
set (Selenium, Playwright, Puppeteer, Cypress), or when the user agent matches a
known crawler (Googlebot, Bingbot, AhrefsBot, Lighthouse, headless browsers). It
never matches a real browser.
KloudMateRum.init({
excludeBots: true, // default — drop known bots
// excludeBots: false, // record everything, bots included
// excludeBots: ({ userAgent, webdriver }) => webdriver, // your own rule
});This is data hygiene, not bot security. A scraper that spoofs its user agent and
hides navigator.webdriver gets through. Every span carries
navigator.userAgent, so drop those server-side. Migrating? On bot-heavy
sites, the default lowers session counts. To keep the old behavior, set
excludeBots: false.
Replay modes
sessionRecorder.mode decides when the SDK uploads a recording. This is
separate from the sampling above, which decides whether a session records at
all.
| Mode | Behavior | Use when |
|---|---|---|
| 'continuous' (default) | Streams replay for the whole session, as it happens. | You want every recorded session's full timeline. |
| 'error' | Keeps only the last bufferWindowMs in memory and uploads nothing until an error fires. A session that never errors uploads no replay at all. | You only care about sessions that broke, and want to store far less. |
KloudMateRum.init({
sessionRecorder: {
enabled: true,
mode: 'error',
bufferWindowMs: 30_000, // how much history to keep before the error (default 30s)
// flushOnSessionEnd: true, // also keep a tail clip of clean sessions (default false)
},
});In 'error' mode, a clean session sends no replay by default. This avoids the
most junk. To also flush the buffered window on page teardown without an error,
set flushOnSessionEnd: true. You then keep a tail clip of every session, for
some extra storage.
This gates replay only. The SDK does not error-buffer spans, errors, or custom events. Once a session is sampled in, they stream continuously, whether or not an error occurs.
What it auto-captures
| Signal | How |
|--------|-----|
| Page load & resource timing | PerformanceNavigationTiming |
| Web Vitals (LCP, CLS, INP, FCP, TTFB) | web-vitals |
| fetch / XHR spans with trace-context propagation | Monkey-patch |
| JS errors & unhandled promise rejections | window.onerror + unhandledrejection |
| Click, submit, select, keypress user interactions | DOM event listeners |
| Long tasks (> 50 ms) | PerformanceObserver |
| Rage clicks — 3+ clicks in 1 s on same element | Friction detector |
| Dead clicks — click with no DOM reaction in 500 ms | Mutation observer |
| Error clicks — click followed by a JS error within 1 s | Error correlator |
| Failed fetch/XHR — 4xx, 5xx and connection failures | Error span per request |
| Session replay (rrweb) | Optional, with sessionRecorder.enabled |
Everything above turns on as soon as you call init(). The one exception is
session replay — enable it with sessionRecorder. The three frustration signals
arrive as km.interaction.* attributes on the interaction span.
API
KloudMateRum.init(config) // initialise (call once)
KloudMateRum.setUser({ id, email }) // identify the user
KloudMateRum.setGlobalAttributes({}) // add attributes to all subsequent spans
KloudMateRum.getGlobalAttributes() // read current global attributes
KloudMateRum.getSessionId() // returns the current session ID string
KloudMateRum.addEvent(name, attrs) // send a custom, application-defined event
KloudMateRum.recordException(err, attrs) // manually report an error
KloudMateRum.endSession() // end the session on logoutCustom events
Send your own events for feature usage, business milestones, or anything the SDK does not auto-capture. They show up in the session timeline, next to spans and errors.
KloudMateRum.addEvent('checkout_completed', { plan: 'pro', value: 49.00 });Each call records a zero-duration span. The span carries the session, route, user, and release context of that moment. This is what lets you funnel and filter on the event, not just read it back as text.
attrs is optional. It must be a flat Record<string, string | number>.
Numbers stay numbers on the wire, so funnels can sum and average them — send
49.00, not '49.00'. For structured data, use your own key names (order.id,
order.total). Do not pass objects or arrays as values.
Two rules to know:
- Use a constant event name. Write
addEvent('checkout_completed'), neveraddEvent(\checkout_${userId}`). The workspace caps distinct names per day and folds the rest into oneother` bucket. So interpolated names collapse into one useless row. Put the varying part in an attribute instead. valueis reserved. The platform sums a numeric attribute named exactlyvalueand takes percentiles of it, so revenue-style questions work with no per-app setup. Pass it as a number.
Manual error reporting
The SDK captures uncaught exceptions and unhandled promise rejections for you.
Some errors you catch and handle yourself, so the automatic window.onerror /
unhandledrejection listeners never see them. Report those explicitly:
try {
await submitOrder(order);
} catch (err) {
KloudMateRum.recordException(err, { orderId: order.id });
showErrorToast();
}It accepts a real Error (preferred, because it carries a stack trace) or a
plain string. Every error, recorded or captured automatically, carries the
page's last minute as a breadcrumb trail (page changes, clicks, requests,
console warnings, earlier errors and your addEvent calls), so you can see
what the user did right before it. Some errors carry no stack of their own (a thrown string, a
non-Error rejection). For those, the SDK builds a stack that points back at
your reporting code, so they stay traceable instead of blank.
You can call addEvent and recordException before init(). They do nothing
and never throw, so a call that races page load never breaks your app.
console.error capture
The SDK also reports console.error(...) calls as exceptions. They show up in
the error views, not just the console breadcrumb stream, so an error your app
catches and logs still surfaces. The first Error argument gives its real
stack. Otherwise the arguments form the message and the SDK builds a stack. The
SDK's own [KloudMate RUM] lines, such as a failed upload, are never captured.
If you use console.error for non-fatal logging you do not want counted, opt out:
KloudMateRum.init({
// ...
captureConsoleErrors: false,
});Filtering noisy errors
The SDK captures every uncaught error on the page, so it also picks up errors that are not yours — from browser extensions, bots, or automation tools on the visitor's machine. Four filters drop that noise before upload, so it never becomes an error and never wakes replay:
KloudMateRum.init({
// ...
// Errors thrown by a browser extension (chrome-extension://, moz-extension://,
// safari-web-extension://, webkit-masked-url://). ON by default — set false to keep them.
ignoreExtensionErrors: true,
// Drop by error MESSAGE (substring or RegExp). Good for bot/automation junk and
// known-benign noise. For example the AutoIt "Object Not Found Matching Id…" error,
// which arrives as a URL-less cross-origin "Script error." so only this catches it.
ignoreErrors: [/Object Not Found Matching Id/, 'ResizeObserver loop limit exceeded'],
// Drop by SOURCE URL — the script the error came from. Mute a noisy third party.
denyUrls: [/ads\.example\.com/, 'chat-widget.cdn.com'],
// Keep ONLY errors from these sources — everything third-party is dropped.
// Errors with no discernible URL are kept (the filter can't judge them).
allowUrls: [/yourdomain\.com/],
});ignoreExtensionErrors matches on the error's own source — its filename or top
stack frame. So an error that only passes through an extension wrapper, but
starts in your code, is still captured. These filters apply only to automatic
capture. A deliberate recordException() is always sent.
Trace context propagation
fetch and XHR requests carry a W3C traceparent header. A backend that runs
OpenTelemetry then continues the same trace, and its server spans appear under
the browser span that caused them.
The SDK propagates same-origin requests automatically. It does not propagate cross-origin requests by default. Otherwise trace context leaks to every third-party API and CDN your page talks to. Allow the ones you own:
KloudMateRum.init({
// ...
propagateTraceHeaderCorsUrls: [/https:\/\/api\.example\.com/],
});traceparent is not a CORS-safelisted header, so every allowlisted cross-origin
call becomes a preflighted request. Your server must accept the header or the
request itself fails, not just its tracing:
app.use(cors({
origin: 'https://app.example.com',
allowedHeaders: ['content-type', 'traceparent', 'baggage'],
}));To let your backend attribute its own spans to the RUM session, turn on baggage.
It rides on the same requests and carries session.id. OpenTelemetry's baggage
propagator reads it for you:
KloudMateRum.init({
// ...
propagateBaggage: true, // or { tier: 'pro' } to add entries
});It is off by default for the same CORS reason. If a server does not list
baggage in Access-Control-Allow-Headers, those requests break.
The SDK does not send tracestate. It starts the trace rather than continuing
one, so there is no upstream vendor state to forward, and nothing in the platform
reads it.
Linking a request back to the click that caused it
A request fired from an event handler is recorded as part of that interaction's trace, so you see click → request → your backend spans as one waterfall.
To follow that link across an await, you need
Zone.js, which
patches the browser's async APIs. The SDK uses Zone.js when your page already
provides it — every Angular app does — and never loads it itself. Zone.js is
too invasive to force on every page, and a second copy throws
Zone already loaded.
Without it, you still get a span for every request. A request made after an
await starts its own trace instead of nesting under the interaction:
button.addEventListener('click', async () => {
fetch('/a'); // ← attributed to this click, always
await validateForm();
fetch('/b'); // ← attributed only if the page provides Zone.js
});To get the second case without Angular, load Zone.js yourself before init().
Ending a session on logout
KloudMateRum.endSession();Call it when a user logs out. Nothing does this for you. In particular,
setUser() does not, because apps also call setUser() on start-up to restore a
remembered login. A rotation there splits the first session of every returning
user. Only your app knows when a different person is using it.
- The next session starts on the next captured event, not inside the call. Users
often log out and then close the tab. An eager rotation records a session that
has an identity but no activity.
getSessionId()returnsnulluntil then. - It clears exactly what
setUser()set, including any extra keys you passed with it. The SDK leavesapplicationName/version/deploymentEnvironmentand anything fromsetGlobalAttributes()alone. - It ends the session in every tab on the origin. A logout is a whole-browser event, and a sibling tab left on the old identity recreates the problem one tab over.
- It closes any session replay in progress. A new recording begins with the new session, so no recording spans two identities.
Readable stack traces
Production bundles are minified, so a raw stack frame reads like
index-4f2a.js:1:84210. KloudMate maps each frame back to your original file,
line and column using the source maps you upload at build time.
Add @kloudmate/sourcemaps
to your build. It injects a debug ID into each script, uploads the maps, and
removes them from the build output so your source is never deployed.
npm i -D @kloudmate/sourcemapsWith Vite:
// vite.config.ts
import { kloudmateSourcemaps } from '@kloudmate/sourcemaps/vite';
export default defineConfig({
build: { sourcemap: 'hidden' },
plugins: [kloudmateSourcemaps({ app: 'my-app', release: process.env.APP_VERSION })],
});In CI, set KM_SOURCEMAPS_TOKEN to an Ingest Key - Backend, created under
Settings → Ingest Keys in the workspace your app reports to. The package README
covers webpack, Rspack, and the npx @kloudmate/sourcemaps CLI for Next.js,
Angular and esbuild.
appmust equal theapplicationNameyou pass toinit().releaseshould equal theversionyou pass toinit(). Debug IDs resolve without it, but a frame with no debug ID falls back to matching by release and file path.- Scripts on another origin need
crossorigin="anonymous"on the<script>tag and anAccess-Control-Allow-Originheader on the script's response. Without both, the browser reports onlyScript error.with no stack, so there is nothing to map.
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>Privacy
- Replay masks all inputs by default (
maskAllInputs: true). - Add
class="km-block"to any element to exclude it from replay. - Add
class="km-ignore"to stop replay recording input on an element. - Click breadcrumbs record a button's or link's label, but no text for a control
inside, or holding, an element with
km-block,km-ignoreorkm-mask. - Breadcrumb URLs carry no query string or fragment.
- A failed request's error names the request by its path (origin and path when
cross-origin), with no query string or fragment, so neither reaches the issue
title or splits the issue. Its
url.fullattribute keeps the full URL, as the request span's does. - Every batch carries the browser's language and time zone (
browser.language,km.timezone), and every error says whether the app handled it (km.error.handled).
License
MIT
