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.
Install
Section titled “Install”pnpm add @openrum/browsernpm install @openrum/browseryarn add @openrum/browserAsynchronous loading (recommended) does not block HTML parsing and initializes after the SDK loads:
<script> (function () { var script = document.createElement("script"); script.src = "https://rum.example.com/sdk/browser/0.1.2/openrum.min.js"; script.async = true; script.onload = function () { OpenRUM.init({ dsn: "https://YOUR_PUBLIC_DSN@rum.example.com/ingest/v1/envelope", environment: "production", }); }; document.head.appendChild(script); })();</script>Synchronous loading starts collection earlier, but blocks HTML parsing while the bundle downloads:
<script src="https://rum.example.com/sdk/browser/0.1.2/openrum.min.js"></script><script> OpenRUM.init({ dsn: "https://YOUR_PUBLIC_DSN@rum.example.com/ingest/v1/envelope", environment: "production", });</script>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.
Initialize
Section titled “Initialize”-
Create a Project and copy its client DSN. See Create your first project.
-
Add the application’s origin to the Project’s allowed origins. The SDK posts with
mode: "cors"andcredentials: "omit", so an origin the Instance does not allow is rejected at the browser. -
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.
Verify
Section titled “Verify”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.
What is captured automatically
Section titled “What is captured automatically”| 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.
Structured logs
Section titled “Structured logs”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.
Forward selected console methods
Section titled “Forward selected console methods”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.
Support matrix
Section titled “Support matrix”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. |
Server rendering drops Events silently
Section titled “Server rendering drops Events silently”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.
Route patterns are not recorded yet
Section titled “Route patterns are not recorded yet”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 rejected DSN disables the page
Section titled “A rejected DSN disables the page”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.
Custom Events
Section titled “Custom Events”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.
Keys that are rejected outright
Section titled “Keys that are rejected outright”A key is dropped, rather than scrubbed, when it:
- collides with an Envelope field, such as
session_id,user_id,release,timestamp,typeorpage_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,tokenorcredit card - is
__proto__,constructororprototype - 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.
Every rejection is silent
Section titled “Every rejection is silent”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.
Custom Events also become breadcrumbs
Section titled “Custom Events also become breadcrumbs”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.
Identity
Section titled “Identity”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.
Sampling
Section titled “Sampling”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