RUM Configuration and API
8 minute read
Overview
Every option on this page is passed to EdgeDelta.init in a script-tag install or to the imported init function in a bundler install. Only token and service are required. The runtime API is the same in both installs: script-tag pages call methods on EdgeDelta, bundler apps import them by name, and both forms queue safely before init runs.
Events flow through one pipeline in the browser before anything is sent, and most options hook a stage of it:
flowchart LR
A["Capture
errors, views, requests,
breadcrumbs"] --> B["ignoreErrors"]
B --> C["Middleware
event handlers, beforeSend"]
C --> D["Transport
OTLP over HTTPS"]
D --> E["Ingestion pipeline"]
Configuration options
Pass options to init:
| Option | Default | Description |
|---|---|---|
token | Required | Ingestion token from the pipeline’s HTTP ingestion node. Public by design. |
service | Required | Names this application. Becomes service.name in queries and dashboards. |
version | None | Your release version. Becomes service.version. Set it: stacks are minified today and need it to symbolicate later. |
environment | None | Deployment environment, for example production or staging. Becomes deployment.environment.name. |
endpoint | https://in.edgedelta.com | Scheme and host only. The SDK appends /otel/v1/traces and /otel/v1/logs itself. Point it at a first-party proxy to survive ad blockers. |
sampleRate | 1 | Fraction of sessions to record, 0 to 1. Decided once per session, so a recorded session is recorded completely. |
captureErrors | true | Captures uncaught errors and unhandled promise rejections. |
routeChanges | true | Opens a new view on each single page application path change. Turn it off only if the app calls startView itself. |
navigationSpans | false | Emits the page load phases as child spans for a waterfall. Multiplies trace items per page view by up to 8x. |
requestSpans | false | Emits one client span per fetch and XHR request. The largest volume multiplier, so turn it on deliberately. Also the basis for backend trace joining. |
propagateTo | [] | Cross-origin hosts that receive a traceparent header on requests. The target server must allow the header via CORS first. Same-origin requests always propagate when requestSpans is on. false disables propagation entirely. |
breadcrumbs | 20 | Length of the event trail attached to every error. false turns it off. |
ignoreErrors | [] | Drops matching errors before anything else sees them: substrings, RegExp values, or predicates. |
beforeSend | None | (event) => event or null. Last chance to redact or drop an event before it leaves the browser. |
debug | false | Logs the whole pipeline to the console. Also switched on by an ed_rum_debug key in localStorage, so a deployed page can be diagnosed without a config change. |
useBeacon | false | Sends the final flush with sendBeacon, which cannot set headers and falls back to a query string token that lands in access logs. Leave it off unless advised. |
Note: Three options change data volume rather than behavior:
sampleRatescales everything,navigationSpansmultiplies page view trace items by up to 8x, andrequestSpansadds a span for every HTTP request the page makes. Turn the last two on deliberately and watch the pipeline’s throughput after each change.
Runtime API
The API is available on window.EdgeDelta after the loader runs, or as named imports from @edgedelta/browser-rum:
| Call | Use it for |
|---|---|
setUser({ id, email, name }) | Attach a user to everything sent afterwards. setUser({}) clears it on logout. |
setContext({ attributes }) | Merge custom attributes into everything sent afterwards. |
addError(err, options?) | Report an error you already caught. Uncaught errors need no call. |
addMessage(text, options?) | Send a log record with no exception attached. |
addBreadcrumb({ type, message, data }) | Add an entry to the trail attached to later errors. |
startView(route?) / endView(route?) | Drive or name views yourself. |
use(...middleware) | Rewrite or drop events and outgoing HTTP requests. |
flush() | Send buffered errors and messages now. |
Only pass fields you are willing to store; they land in queryable telemetry.
Users and context
setContext merges, so a page can enrich in stages, for example the user at login and the organization at an organization switch, without re-sending what it already set. A key passed as undefined is removed. setUser is a wrapper for the user.* attribute conventions:
EdgeDelta.setUser({ id: "u_123", email: "ada@example.com", name: "Ada" });
EdgeDelta.setContext({ attributes: { "org.id": orgId } });
EdgeDelta.setUser({}); // Clear the user on logout.
Errors, messages, and fingerprints
addError is for errors you already handle, such as a caught fetch failure. addMessage sends a plain log record when there is no exception to attach. When one broken endpoint produces a different message every time, pass a fingerprint to group them instead of letting each become its own pattern:
EdgeDelta.addError(err, { fingerprint: "HTTP " + status + " " + route });
EdgeDelta.addMessage("checkout abandoned at payment step");
Breadcrumbs
Errors and messages carry ed.rum.breadcrumbs, a JSON array of the last 20 things that happened, newest last. Clicks, fetch calls, and console.error calls are recorded automatically; add your own with addBreadcrumb. The trail is a ring buffer per document, so it costs nothing on page views that never fail.
Click descriptors are structural only, for example button#save.btn[data-test-id=save-button]. Text content, input values, and aria-label are never read, because this string leaves the browser attached to every error the page reports afterwards. XMLHttpRequest calls are not recorded as breadcrumbs, so a page whose traffic is XHR sees no http crumbs.
Views
A view is one trace: the document load, then one per route change. Each carries its own span, its own Cumulative Layout Shift, and its own Interaction to Next Paint, and errors reported while it is open name its trace. Views are joined to each other by session.id, not by trace ID.
With routeChanges on (the default), detection needs no framework code: only a change of path opens a view, so a router rewriting the query string on every filter change costs nothing. What detection cannot know is the route pattern and when the transition actually finished. Both default to something reasonable, the pathname and the first paint after the URL commits, and endView replaces both when the router knows better:
router.subscribe("onResolved", ({ toLocation }) => EdgeDelta.endView(toLocation.routeId));
That reports ed.rum.route as /orgs/$orgId/logs rather than /orgs/42/logs, so the spans group, and measures the transition to the point the data resolved. To drive views entirely by hand, set routeChanges: false and call startView("/orgs/$orgId/logs"), which ends the open view and opens a named one.
Middleware
use registers handlers that run in order. Call next to continue the chain with a value, possibly a changed one, or return without calling it to drop the event or abandon the request. beforeSend is an event handler under the hood:
EdgeDelta.use({
// Drop or redact an event on its way into the queue.
event: (event, next) => { if (isNoise(event)) return; next(event); },
// Wrap the outgoing HTTP request: add headers or route through a first-party proxy.
send: (request, next) => next({ ...request, url: toFirstParty(request.url) }),
});
Everything is synchronous on purpose: the last chance to touch a payload happens while the page is unloading, where there is nothing to await with. A send handler sees the final url, headers, and body, plus the events the body was built from and whether this is the page-end flush.
Ignoring known-noisy errors
ignoreErrors takes substrings, RegExp values, and predicates. They are matched against the log record’s body and, for errors, exception.type, never the stack: nearly every stack in a bundled app names the framework, so matching stacks turns one common word into a silent outage. Spans are never matched, so a pattern meant for an error message cannot cost you a page view.
Plain strings survive JSON, which means a script-tag install can pass a list through its config file instead of needing a beforeSend closure. ignoreErrors runs ahead of beforeSend, so the hook only sees what survived.
Flushing
flush() sends queued errors and messages. flush(true) also sends the open view’s span, which is otherwise held back for final vitals; that is what the SDK’s own page-hide handling calls internally. Reach for it directly on a page that never unloads, such as a test harness or a kiosk view.
React error reporting
React catches render errors itself, so no global listener sees them. The @edgedelta/browser-rum/react subpath reports them:
import { createRoot } from "react-dom/client";
import { ErrorBoundary, rumRootOptions } from "@edgedelta/browser-rum/react";
createRoot(node, rumRootOptions()).render(
<ErrorBoundary fallback={({ eventId, resetError }) => <Oops id={eventId} onRetry={resetError} />}>
<App />
</ErrorBoundary>
);
The three reporting paths are told apart by the ed.rum.mechanism attribute:
| Mechanism | Comes from | Severity |
|---|---|---|
react.boundary | The ErrorBoundary component | ERROR: a subtree died |
react.root | rumRootOptions(), for errors no boundary caught | FATAL: the page died |
react.router | reportRouteError, for a router’s own per-route catch boundary | ERROR |
The third path matters more than it looks. A router that renders its own catch boundary intercepts route render errors before any boundary above the outlet can see them, so without it an entire route can crash invisibly. Wire reportRouteError to whatever hook the router offers.
The fallback render function receives { error, componentStack, eventId, resetError }. The eventId is the ed.rum.error_id attribute on the reported event, so a user can quote it and someone can find that exact error. All three paths accept a classify callback that can override the severity or add attributes for specific errors, for example downgrading stale-chunk load failures to warnings.
Testing an app that uses RUM
The @edgedelta/browser-rum/testing subpath provides a recorder that stands in for the SDK, so tests can assert on what the app reported without real requests:
import { mock } from "bun:test"; // or vi.mock / jest.mock
import { createRumRecorder } from "@edgedelta/browser-rum/testing";
export const rum = createRumRecorder();
mock.module("@edgedelta/browser-rum", () => rum.module);
Install it once from your runner’s setup file and call rum.reset() in a beforeEach. The recorder exposes what the app did as plain lists: rum.errors, rum.messages, rum.breadcrumbs, rum.contexts, rum.users, rum.startedViews, rum.endedViews, rum.initialized, and rum.flushes. rum.module covers the whole public API, so a module importing something the recorder does not track still gets a function rather than undefined.
See Also
- Install Browser RUM - Script tag and npm installation
- RUM Data Reference - What each signal carries and every attribute
- Troubleshooting RUM - Diagnose missing or partial data