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.8.0

Published

KloudMate Browser RUM SDK — traces, session replay, and behavioral analytics

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 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 addEvent or setUser during 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 logout

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

With 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.

  • app must equal the applicationName you pass to init().
  • release should equal the version you pass to init(). 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 an Access-Control-Allow-Origin header on the script's response. Without both, the browser reports only Script 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-ignore or km-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.full attribute 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