@kloudmate/rum-web
v2.3.0
Published
KloudMate Browser RUM SDK — traces, session replay, and behavioral analytics
Downloads
1,859
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 possible — instrumentation only captures what
happens after it runs. TypeScript types are bundled; require() works too.
Quick start (CDN)
Drop 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's it. 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.
Calling the SDK before it loads? If you fire
addEvent/setUserduring initial render — before the async bundle arrives — add the small loader stub so those early calls are queued, not lost. Most apps don't 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 deliberately and upgrade on your own schedule |
The exact-version path is immutable and cached for a year. The rolling paths carry a five-minute TTL, so a release reaches your users within minutes.
Loader stub
Only needed if you call the SDK before the async bundle finishes loading. Paste this 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 independent rates, and they compose:
| 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 is the lever on ingest volume. It is checked in init() before any
instrumentation is installed, so a session outside the rate registers no
listeners and makes no requests at all — it is not "capture then discard".
Both decisions are sticky for the life of the session and stored separately, so you get whole sessions rather than fragments of every session. Re-rolling per page load would turn a 25% rate into one-page slivers of most sessions instead of complete 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 while recording a small fraction of sessions.
Bot exclusion
Automated and crawler traffic is dropped by default — bots never start a session, so they produce no spans, no replay, and no requests at all (this also keeps them out of your session counts and off your bill).
A visitor is treated as a bot when navigator.webdriver is set (Selenium,
Playwright, Puppeteer, Cypress) or the user agent matches a known crawler
(Googlebot, Bingbot, AhrefsBot, Lighthouse, headless browsers, …). Real browsers
are never matched.
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: default-on lowers session counts on
bot-heavy sites versus recording everything — set excludeBots: false to keep
the old behavior.
Replay modes
sessionRecorder.mode decides when a recording is uploaded — orthogonal to
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 ships no replay by default — the strictest
junk-avoidance. Set flushOnSessionEnd: true to also flush the buffered window
on page teardown even without an error, trading some storage for always having a
tail clip of every session.
This gates replay only. Spans, errors, and custom events are not error-buffered — once a session is sampled in, they stream continuously regardless of whether an error occurs.
What it auto-captures
| Signal | How |
|--------|-----|
| Page load & resource timing | PerformanceNavigationTiming |
| Web Vitals (LCP, FID, CLS, INP) | 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 is on as soon as you call init(), except 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 — feature usage, business milestones, anything not already auto-captured — and they show up in the session timeline alongside spans and errors.
KloudMateRum.addEvent('checkout_completed', { plan: 'pro', value: 49.00 });Each call records a zero-duration span carrying the session, route, user and release context of the moment it happened — which is what lets you funnel on it and filter it, rather than just read it back as a line of text.
attrs is optional and 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'. Nest structured data into your own naming convention (order.id,
order.total, ...) rather than passing objects/arrays as values.
Two rules worth knowing:
- Use a constant event name.
addEvent('checkout_completed'), neveraddEvent(\checkout_${userId}`). Distinct names are capped per workspace per day and the rest fold into a singleother` bucket, so interpolated names collapse into one useless row. Put the varying part in an attribute instead. valueis reserved. A numeric attribute named exactlyvalueis summed and percentiled for you, so revenue-style questions work without any per-app setup. Pass it as a number.
Manual error reporting
The SDK automatically captures uncaught exceptions and unhandled promise
rejections. For an error you catch and handle yourself — one the SDK's automatic
window.onerror / unhandledrejection listeners never see — report it explicitly:
try {
await submitOrder(order);
} catch (err) {
KloudMateRum.recordException(err, { orderId: order.id });
showErrorToast();
}Accepts a real Error (preferred — carries a stack trace) or a plain string.
Every addEvent call in the preceding session is attached to the next
recordException as a breadcrumb trail, so you can see what the user did right
before the error. Errors reported without a stack of their own (a thrown string,
a non-Error rejection) get a synthesized stack pointing back at the reporting
code, so they stay traceable rather than showing up blank.
Both addEvent and recordException are safe to call before init() — they
no-op silently rather than throwing, so a call that races page load never
breaks your app.
console.error capture
Calls to console.error(...) are also reported as exceptions (they show up in
the error views, not just the console breadcrumb stream) — an error your app
catches and logs still surfaces. The first Error argument donates its real
stack; otherwise the arguments form the message and a stack is synthesized. Opt
out if you use console.error for non-fatal logging you don't want counted:
KloudMateRum.init({
// ...
captureConsoleErrors: false,
});Trace context propagation
fetch and XHR requests carry a W3C traceparent header, so a backend running
OpenTelemetry continues the same trace and its server spans appear under the
browser span that caused them.
Same-origin requests are propagated automatically. Cross-origin requests are not, by default — trace context would otherwise leak 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, which OpenTelemetry's
baggage propagator reads for you:
KloudMateRum.init({
// ...
propagateBaggage: true, // or { tier: 'pro' } to add entries
});It is off by default for the same CORS reason: enabling it against a server that
does not list baggage in Access-Control-Allow-Headers breaks those requests.
tracestate is not sent. The SDK 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.
Following that link across an await needs 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. It is
too invasive to impose on every page, and loading a second copy throws
Zone already loaded.
Without it you still get a span for every request; requests issued after an
await simply start their 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 implicitly — in particular
setUser() does not, because apps also call setUser() on start-up to restore
a remembered login, and rotating there would split the first session of every
returning user. Only your app knows a different person is now using it.
- The next session starts on the next captured event, not inside the call —
logging out and closing the tab is common, and rotating eagerly would record a
session with an identity and no activity.
getSessionId()returnsnulluntil then. - It clears exactly what
setUser()set, including any extra keys you passed with it.applicationName/version/deploymentEnvironmentand anything fromsetGlobalAttributes()are left alone. - It ends the session in every tab on the origin — logging out is a whole-browser event, and leaving a sibling tab on the old identity would recreate the problem one tab over.
- Any session replay in progress is closed and a new recording begins with the new session, so no recording spans two identities.
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 exclude an element from interaction tracking.
License
MIT
