Skip to content
Docs

Browser SDK

Applies to@openrum/browser 0.1.2

@openrum/browser sends Events to an OpenRUM Instance. One init() call turns on Page Views, interaction descriptions, JavaScript errors, unhandled rejections, fetch and XHR timing, and Core Web Vitals.

Terminal window
pnpm add @openrum/browser

Do not add async to the external script and then call OpenRUM.init() immediately in the next script: those scripts have an execution race. Initialize from onload when loading asynchronously. Very early events may happen before an asynchronous bundle is ready; use synchronous loading when covering those events matters more than avoiding parser blocking.

  1. Create a Project and copy its client DSN. See Create your first project.

  2. Add the application’s origin to the Project’s allowed origins. The SDK posts with mode: "cors" and credentials: "omit", so an origin the Instance does not allow is rejected at the browser.

  3. Call init() once, before the code you want to observe runs.

Where init() goes depends on the framework. Pick yours for the exact code:

Every option is listed in the generated SDK options reference.

Load a page, click something, and let a request fail. Then open Events, API, Errors and Sessions in the Console and confirm the four views show the same session_id. One shared Session is the signal that capture, ingest and aggregation all agree; four populated views with different Session IDs mean something is initializing more than once.

Signal Detail
Page Views Initial load plus every pushState, replaceState, popstate and restored pageshow.
Interactions Bounded descriptions of interactive elements. Disable with captureClicks: false.
Errors Uncaught errors and unhandled rejections, with up to 50 preceding breadcrumbs.
API Requests fetch and XHR timing, excluding OpenRUM’s own ingest and config endpoints.
Core Web Vitals Collected through web-vitals, which is bundled into the package.

Query strings, URL fragments and raw input values are stripped in the browser before an Event is queued. Headers and bodies are not read at all unless the Project opted that API in, and a body that is read is redacted first; see API monitoring.

Logs are explicit: init() alone does not collect application logs. Import the singleton logger, or use the client returned by init(), and call a level method where the event happens.

import { init, logger, setUser } from "@openrum/browser";
init({ dsn: import.meta.env.VITE_OPENRUM_DSN });
setUser("customer-123"); // Pseudonymous application ID, never an email.
logger.info("checkout started", {
"order.id": "order-123",
items: 3,
});
logger.error("payment failed", {
"error.code": "UPSTREAM_TIMEOUT",
});
// Clear the identity when the application user signs out.
setUser(undefined);

Available levels are trace, debug, info, warn, error, and fatal. Attributes may contain up to 20 primitive string, number, or boolean values. Error and fatal logs are diagnostic Logs; they do not create an Issue.

captureConsole is a separate opt-in. Only the listed methods are forwarded, and their normal browser output is preserved.

init({
dsn: import.meta.env.VITE_OPENRUM_DSN,
captureConsole: ["warn", "error"],
beforeSendLog: (log) => (log.message.includes("ResizeObserver loop") ? null : log),
});

Omit captureConsole to keep every console message local. Object arguments are represented as [Object], not serialized. The former enableLogs option is deprecated and ignored; existing configurations may leave it in place until a future breaking upgrade.

To verify the integration, send a temporary logger.info("openrum setup check"), open Logs in the Console, and search for message:"openrum setup check" in the same Project, Environment, and time range. For user investigation, search user.id:"customer-123". See Logs for all search fields, limits, Session/Trace links, and privacy boundaries.

Stated against @openrum/browser 0.1.2. The table describes current behavior, not inferred support based on a framework’s popularity.

Target State What that means
React, Vue, Solid, Svelte, plain JavaScript Works Any bundler that consumes ES modules.
React Router, Vue Router in history mode, TanStack Router Works Navigations go through history.pushState, which the SDK observes.
Next.js Works, with placement rules init() must not run during server rendering. See Next.js.
Astro Works, with placement rules Runs in a client island, not in a .astro frontmatter block. See Astro.
Hash-based routing Not captured The SDK does not listen for hashchange, so only the first Page View is recorded.
Route patterns instead of resolved paths Not recorded See below.
CDN <script> tag Works The Instance serves the versioned IIFE bundle at /sdk/browser/0.1.2/openrum.min.js.
React Native and other non-browser runtimes Not supported The SDK reads window and keeps its queue in localStorage.

Under Node there is no window, so init() builds a client with no transport. It returns normally, and every captureEvent() after it is counted as dropped rather than raising. The count is readable through getClient()?.diagnostics(), but nothing is logged and nothing is sent. Initialize only on the client.

The recorded Route is location.pathname. On a route like /orders/$orderId that means one distinct Route per order, which spreads a single page’s data across thousands of rows in Analysis.

getClient()?.startPage({ route: "/orders/:orderId" }) does set the Route, but the automatic Page View for that navigation has already been sent with the resolved path, and startPage begins a new Page — so the Page View and the Events after it land on different page_id values. The current SDK cannot record route patterns correctly for automatic Page Views.

A 401 or 403 from ingest is treated as permanent: the SDK clears its queue and stops sending for the rest of the page, without logging. Check the DSN and the Project’s allowed origins in the Console when Events do not arrive.

import { captureEvent } from "@openrum/browser";
captureEvent("checkout_started", {
attributes: { plan: "standard", channel: "organic" },
measurements: { cart_value: 249.9 },
});

A Custom Event takes a name and two optional bags of data. There are only two, and the difference between them is what you can do with the result in Analysis: attributes are the dimensions you group and filter by, measurements are the numbers you aggregate. plan is an attribute because you want a breakdown per plan; cart_value is a measurement because you want its sum and percentiles.

Parameter Type Rules
name string, required 1 to 80 characters. Cannot start with openrum..
attributes Record<string, string> Up to 20 entries. Keys 1 to 64 characters, values up to 512 characters. String values only.
measurements Record<string, number> Up to 20 entries. Keys 1 to 64 characters. Finite numbers only — NaN and Infinity are rejected.

The two bags have separate 20-entry budgets, and the same limits are enforced again by the Envelope schema at ingest, so nothing here is advisory.

Values are typed strictly, and the wrong type is dropped

Section titled “Values are typed strictly, and the wrong type is dropped”

This is the mistake worth knowing about: a number in attributes and a numeric string in measurements are both discarded, silently.

captureEvent("checkout_started", {
attributes: { items: 3 }, // dropped: not a string
measurements: { cart_value: "249.9" }, // dropped: not a number
});

Write { items: "3" } and { cart_value: 249.9 } instead. Passing anything that is not a plain object — an array, a string, null — discards that whole bag.

A key is dropped, rather than scrubbed, when it:

  • collides with an Envelope field, such as session_id, user_id, release, timestamp, type or page_id
  • looks like it carries a secret or an identifier that should not be here: anything matching authorization, cookie, csrf, cvv, email, password, passphrase, phone, secret, session, token or credit card
  • is __proto__, constructor or prototype
  • starts with openrum., which is reserved for the SDK’s own Events

Key matching ignores case, so Email and X-Auth-Token are rejected too.

Values that are scrubbed rather than dropped

Section titled “Values that are scrubbed rather than dropped”

Attribute values still go through the same redaction as everything else the SDK sends. Email addresses become [REDACTED_EMAIL], Bearer and Basic credentials become [REDACTED_TOKEN], and runs of 13 to 19 digits become [REDACTED_NUMBER]. A value that looks like an http or https URL has its query string and fragment stripped first.

None of this is a reason to send such data. Redaction is a backstop for the string that slips through, not a sanitiser you can rely on — see Privacy.

Nothing above throws or logs. A rejected name increments a dropped-Event counter and a rejected key or value increments a dropped-attribute counter, both readable during development:

import { getClient } from "@openrum/browser";
console.log(getClient()?.diagnostics());

Custom Events are sampled by eventSampleRate, so a missing Event may also simply have been sampled out.

Each Custom Event appends a breadcrumb carrying its name and attributes, so the last 50 things that happened before an Error include your own milestones and not just clicks and requests. You can add breadcrumbs directly, with the same key and value rules applied to data:

import { addBreadcrumb } from "@openrum/browser";
addBreadcrumb({
category: "checkout",
message: "coupon rejected",
level: "warning",
data: { reason: "expired" },
});

category is required and capped at 64 characters, message is required and capped at 512, level is one of debug, info, warning or error, and timestamp defaults to now and must be parseable as a date if given. A breadcrumb that breaks any of those is dropped and counted.

import { setTag, setUser } from "@openrum/browser";
setUser("account_opaque_id");
setTag("plan", "enterprise");

Prefer an internal opaque ID; values that look like email addresses are rejected and counted as dropped attributes. setUser(undefined) clears the identity. Sessions are activity windows and renew independently of identity. At most 20 tags are kept.

Errors, API Requests and everything else are sampled independently, and the decision is deterministic per Session — a sampled-in Session stays sampled in, so a Session is never half-recorded. Defaults and bounds are in SDK options.

An Instance can lower these rates remotely through /api/v1/sdk/config. The SDK refreshes no more often than every 10 seconds and no less often than every 5 minutes, caches the last valid response, and falls back to the local values when a response is invalid.

Release Alpha · commit local