npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-web
import { 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/setUser during 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 logout

Custom 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'), never addEvent(\checkout_${userId}`). Distinct names are capped per workspace per day and the rest fold into a single other` bucket, so interpolated names collapse into one useless row. Put the varying part in an attribute instead.
  • value is reserved. A numeric attribute named exactly value is 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() returns null until then.
  • It clears exactly what setUser() set, including any extra keys you passed with it. applicationName / version / deploymentEnvironment and anything from setGlobalAttributes() 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