Web RUM SDK
Set up the Last9 Web RUM SDK to monitor real user performance, errors, and behavior in your web application.
Discover Applications tracks how your users experience your web application by collecting Core Web Vitals, performance data, JavaScript errors, and user interactions directly from their browsers. The RUM SDK provides lightweight, non-blocking instrumentation that captures real-world application behavior without impacting performance.
The stable CDN path currently serves Web RUM SDK 2.14.0.
Prerequisites
Before installing the RUM SDK, ensure you have:
- Client Token: A
Client — Web Browsertoken from Ingestion Tokens. This token authenticates your application’s data collection. - Applications Base URL: Your base URL from the Applications integration page. This endpoint receives your application’s monitoring data.
Supported Frameworks
The RUM SDK integrates with popular frontend stacks out of the box:
- React
- Angular
- Vue
- Next.js, with support for SSR
If you run into framework-specific issues or need guidance for another setup, reach out to the Last9 team and we’ll help you get started quickly.
Install and Set Up
-
Add the RUM SDK script to your
index.htmlfile in the<head>section:<script src="https://cdn.last9.io/rum-sdk/builds/stable/v2/l9.umd.js"></script> -
Initialize RUM in your application’s main component (typically
App.jsorindex.js):import { useEffect } from 'react';function App() {useEffect(() => {L9RUM.init({baseUrl: "https://your-base-url",headers: {clientToken: "your-client-token",},resourceAttributes: {serviceName: "your-app-name",deploymentEnvironment: "production",appVersion: "1.0.0",},});}, []);return (// Your app components);} -
Add user tracking (optional):
L9RUM.identify({ id, email, name, fullName, roles }) // on loginL9RUM.clearUser() // on logout
-
Add the SDK script and initialization to your HTML file’s
<head>section:<!DOCTYPE html><html><head><!-- Load the RUM SDK --><script src="https://cdn.last9.io/rum-sdk/builds/stable/v2/l9.umd.js"></script><script>// Initialize RUM SDKL9RUM.init({baseUrl: "https://your-base-url",headers: {clientToken: "your-client-token",},resourceAttributes: {serviceName: "your-app-name",deploymentEnvironment: "production",appVersion: "1.0.0",},});</script></head><body><!-- Your application content --></body></html> -
Add user tracking (optional):
L9RUM.identify({ id, email, name, fullName, roles }) // on loginL9RUM.clearUser() // on logout
Configuration
Required Configuration
These settings are mandatory for the RUM SDK to function:
L9RUM.init({ baseUrl: "https://your-base-url", // Your Applications base URL headers: { clientToken: "your-client-token", // Browser-safe RUM client token }, resourceAttributes: { serviceName: "your-app-name", // Application identifier deploymentEnvironment: "production", // Environment (production, staging, development) appVersion: "1.0.0", // Version identifier (semver, git hash, etc.) },});The SDK sends clientToken as the reserved X-LAST9-API-TOKEN header. clientToken always takes precedence: an X-LAST9-API-TOKEN entry in headers cannot replace it. Set the token only through clientToken.
Optional Configuration
Customize the RUM SDK behavior with additional settings:
L9RUM.init({ // Required settings...
// Optional settings sampleRate: 40, // Percentage of sessions to monitor (1-100, default: 40) debug: false, // Enable console logging for troubleshooting (default: false)
// Control error tracking errors: { console: true, // Console errors (default: true) global: true, // Unhandled exceptions (default: true) report: true, // Browser reporting API (default: true) network: true, // Failed requests (default: true) ignorePatterns: [/ResizeObserver/i], // Regex patterns to ignore beforeSend: (event) => { // Filter or enrich errors before sending if (event.attributes["exception.message"]?.includes("ExpectedNoise")) { return null; // Drop this error } delete event.attributes["exception.stacktrace"]; // Remove an attribute return event; }, },
// Network tracking & backend trace correlation network: { // Control browser-side network tracking enabled: true, // default: true; set false to disable all network spans ignorePatterns: { // Optionally ignore noisy endpoints // Matches against the full URL (includes origin + path + query) fullUrl: ["https://cdn.example.com", /^https:\/\/.*\\.example\\.com/], // Matches against just the path portion (e.g., /healthz, /metrics) pathname: ["/healthz", /^\\/internal\\/metrics/], // Matches against just the hostname (e.g., api.example.com) hostname: ["localhost", "api-internal.example.com"], },
// Backend trace correlation backendCorrelation: { enabled: true, corsAllowedOrigins: ["https://api.internal.example"], customHeaders: { "x-app-name": "web-client" }, injectToAllRequests: false,
// W3C Baggage propagation (propagate custom attributes to backend) baggage: { enabled: true, allowedKeys: ["user.id", "tenant.id"], // only these keys are propagated }, }, },
// Capture user interactions interactions: { enabled: true, // opt in explicitly trackClicks: true, trackScroll: true, // throttled; default 500ms trackKeyboard: false, // off by default for privacy trackForm: true, // focus/input on non-sensitive fields trackTouch: false, captureElementText: false, // trim to 100 chars when enabled selectorDepth: 3, // depth used for simple selectors throttleMs: 500, },
// Geo-IP enrichment (optional, privacy-sensitive) geoIpEnabled: false, // default: false; set true to resolve IP + coarse geo via GeoJS
// Use known geo values instead of GeoJS lookup geoAttributesOverride: { country: "India", country_code: "IN", city: "Bangalore", organization: "Airtel", },});Network Tracking Controls
The SDK records browser network activity (fetch, XHR, and static resources) as spans attached to the active View. Use the network configuration to tune what is collected:
network.enabled: Master toggle for browser-side network tracking.- Default:
true. - Set
network: { enabled: false }to completely disable network spans (fetch, XHR, and resource loads) while keeping the rest of the SDK active.
- Default:
network.ignorePatterns: Filter out noisy or internal endpoints to keep dashboards focused and reduce cardinality.fullUrl: Matches against the complete URL (e.g.,https://cdn.example.com/assets/app.js).pathname: Matches against just the path (e.g.,/healthz,/metrics).hostname: Matches against the hostname (e.g.,api.internal.example).- Each array accepts strings (substring match) or regular expressions.
By default, the SDK automatically ignores requests to localhost and 127.0.0.1 (including their IPv6 variants) so that local development noise does not pollute production-like data. You can still opt in to local traffic by sending data to a non-localhost hostname (for example, a tunneled URL or a staging endpoint).
Geo Attribute Overrides
If your application already knows the user’s location (for example, from a backend lookup), you can pass those values into the SDK instead of relying on GeoJS IP inference. Pass any subset of these keys — they map onto the standard enduser.geo.* attributes:
| Key | Maps to |
|---|---|
country | enduser.geo.country |
country_code | enduser.geo.country_code |
city | enduser.geo.city |
organization | enduser.geo.organization |
L9RUM.init({ // ...required settings geoAttributesOverride: { country: "India", country_code: "IN", city: "Bangalore", organization: "Airtel", },});Providing geoAttributesOverride disables the GeoJS lookup, even when geoIpEnabled: true. Only the supplied keys are populated; missing keys stay unset. An empty override object ({}) also disables the lookup. To use GeoJS enrichment, omit geoAttributesOverride and set geoIpEnabled: true.
Sample Rate Guidelines
Choose your sample rate based on traffic volume and environment:
| Traffic Level | Daily Users | Recommended Rate | Purpose |
|---|---|---|---|
| High Traffic | >10,000 | 1-10% | Manage data volume while maintaining statistical significance |
| Medium Traffic | 1,000-10,000 | 10-25% | Balance coverage with data volume |
| Low Traffic | <1,000 | 25-50% | Maximize insights with comprehensive data collection |
| Development/Staging | Any | 50-100% | Thorough testing and validation before production |
User Interaction Tracking
Instrument real user actions as spans, attached to the active View:
enabled: Master toggle (default:false)trackClicks,trackScroll,trackKeyboard,trackForm,trackTouch: Turn on specific interaction types (all default tofalse)throttleMs: Minimum gap between scroll spans (default:500ms)captureElementText: Include element text up toelementTextMaxLength(default:false, 100 chars)selectorDepth: Depth for building simple CSS selectors (default:3)
Privacy defaults: keyboard tracking is off unless you opt in, sensitive form fields (password/credit-card/email, etc.) are skipped automatically, and captured element text is trimmed to the configured length.
- Click, focus, keyboard, input, and touch spans all skip sensitive
inputandtextareafields before they extract field metadata. The SDK does not export the field name, label, selector, or other structural metadata of a sensitive field. - A text-entry field whose
name,id, oraria-labelcontainsemailis sensitive — for exampleuseremail,emailfield,emailaddress, orprimaryemail. Email checkboxes and radio buttons, such asemail_notifications_enabled, stay trackable.
Backend Trace Correlation
Connect frontend requests to backend services for end-to-end visibility. When enabled, the SDK adds W3C trace headers (traceparent/tracestate) to outbound requests, allowing you to see complete request flows from browser to backend.
network: { backendCorrelation: { enabled: true, corsAllowedOrigins: ["https://api.internal.example"], // Domains to include customHeaders: { "x-app-name": "web-client" }, // Additional headers injectToAllRequests: false, // Set true only if all APIs support it },}Start with same-origin APIs, then progressively add cross-origin domains after verifying they accept the headers. Enable debug: true to identify configuration issues.
When backend correlation is enabled, validate the flow end to end:
- Open browser DevTools and trigger a backend request.
- Confirm the request includes W3C trace-context headers such as
traceparentandtracestate. - If baggage propagation is enabled, confirm only the allowed baggage keys are sent.
- Open the backend trace in Last9 and confirm the browser span and backend service span are part of the same trace.
- Check that the backend service uses the same environment naming convention as the frontend application.
If another browser monitoring SDK is also installed, treat coexistence as a validation step. Watch for duplicate browser spans, missing trace headers, overwritten trace context, double fetch/XHR instrumentation, or CORS failures. Keep injectToAllRequests: false unless every destination is allowed to receive trace-context headers.
Disable Trace Header Propagation
If your backend does not accept browser-originated trace context, or the extra headers cause CORS preflight failures, set propagateTraceHeaders: false. The SDK stops injecting traceparent, tracestate, and B3 headers into outgoing fetch/XHR requests while keeping network span collection, baggage propagation, and custom headers intact.
L9RUM.init({ network: { backendCorrelation: { enabled: true, propagateTraceHeaders: false, // no traceparent / tracestate / B3 headers }, },});propagateTraceHeaders defaults to true. The propagationFormats, forceOverwrite, and isolateTracePerRequest options only take effect while it is true. To disable all backend correlation features — including baggage and custom headers — set enabled: false instead.
W3C Baggage Propagation
Baggage lets you forward custom key-value pairs from the browser to your backend services via the standard W3C Baggage HTTP header. This is useful when your backend needs context that originates in the browser, for example linking a session ID from another tool, or passing a tenant identifier for multi-tenant routing.
L9RUM.init({ // ...required settings network: { backendCorrelation: { enabled: true, corsAllowedOrigins: ["https://api.myapp.com"], baggage: { enabled: true, allowedKeys: ["user.id", "tenant.id"], }, }, },});
// Set the attributes you want propagatedL9RUM.spanAttributes({ "user.id": currentUser.id, "tenant.id": currentUser.tenantId,});The SDK sends these as a baggage header alongside the existing traceparent / tracestate headers:
baggage: user.id=user-456,tenant.id=tenant-xyzBaggage values travel as plaintext HTTP headers. Only include non-sensitive attributes in allowedKeys. Never add secrets, tokens, passwords, or PII such as emails or phone numbers.
Baggage Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable W3C Baggage propagation |
allowedKeys | string[] | [] | Whitelist of attribute keys to propagate. Keys not listed here are never sent, even if set via spanAttributes() |
trackedUrls | (string | RegExp)[] | — | Restrict baggage to specific URLs. If omitted, baggage follows the same rules as trace context (corsAllowedOrigins / injectToAllRequests) |
maxTotalBytes | number | 8192 | Maximum baggage header size in bytes (W3C recommendation). Entries are truncated if exceeded |
warnAtPercentage | number | 80 | Log a warning when baggage size exceeds this percentage of maxTotalBytes |
Link Sessions from Another Tool
A common use case is propagating a session ID from another tool:
L9RUM.init({ // ...required settings network: { backendCorrelation: { enabled: true, forceOverwrite: true, // required when running alongside another RUM SDK baggage: { enabled: true, allowedKeys: ["external.session_id"], }, }, },});
// Capture the session ID from the other toolL9RUM.spanAttributes({ "external.session_id": otherRum.getSessionId(),});Extract Baggage on the Backend
Your backend services can read the baggage header using any OpenTelemetry SDK. Below are examples for common languages.
Register a BaggageSpanProcessor so every span automatically picks up baggage entries as attributes:
const api = require("@opentelemetry/api");
// Copies all W3C Baggage entries onto every span as attributesclass BaggageSpanProcessor { onStart(span, parentContext) { const baggage = api.propagation.getBaggage( parentContext || api.context.active(), ); if (!baggage) return; for (const [key, entry] of baggage.getAllEntries()) { span.setAttribute(key, entry.value); } } onEnd() {} forceFlush() { return Promise.resolve(); } shutdown() { return Promise.resolve(); }}
// Add to your tracer provider setupconst { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");const provider = new NodeTracerProvider();provider.addSpanProcessor(new BaggageSpanProcessor());provider.register();Once registered, attributes like user.id and tenant.id appear on every backend span without per-route middleware.
import ( "net/http"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/trace"
)
func handler(w http.ResponseWriter, r \*http.Request) {ctx := r.Context()bag := baggage.FromContext(ctx)
userID := bag.Member("user.id").Value() tenantID := bag.Member("tenant.id").Value()
span := trace.SpanFromContext(ctx) if userID != "" { span.SetAttributes(attribute.String("user.id", userID)) } if tenantID != "" { span.SetAttributes(attribute.String("tenant.id", tenantID)) }
}from opentelemetry import baggage, tracefrom opentelemetry.propagate import extract
def handle_request(request): ctx = extract(request.headers) bag = baggage.get_all(ctx)
user_id = bag.get("user.id") tenant_id = bag.get("tenant.id")
span = trace.get_current_span() if user_id: span.set_attribute("user.id", user_id) if tenant_id: span.set_attribute("tenant.id", tenant_id)GraphQL Network Observability
The SDK automatically enriches GraphQL requests made over fetch and XHR — no GraphQL client dependency or configuration is required. Enrichment is driven entirely by the request body, so plain REST calls (including JSON POSTs to non-GraphQL endpoints) are never misclassified as GraphQL.
When a GraphQL operation is detected, the network span is renamed to a descriptive name and tagged with operation metadata:
| Span attribute | Example | Notes |
|---|---|---|
| Span name | GraphQL: "GetUser" query | Anonymous operations become GraphQL: query |
graphql.operation.name | GetUser | Operation name parsed from the request body |
graphql.operation.type | query | query, mutation, or subscription |
l9.span.category | network | Categorizes the span for dashboard filtering |
GraphQL servers commonly return errors with an HTTP 2xx status and an errors[] array in the response body. The SDK inspects the body of every 2xx response (200..299) and flags these as errors so they surface in Error Tracking:
| Span attribute | Value | Notes |
|---|---|---|
error.type | GraphQLError | Set when the response contains GraphQL errors |
graphql.error.count | number | Number of GraphQL errors in the response |
Errors are counted in:
errors[]@defer/@streampayloads (incremental[].errors,completed[].errors)- a multipart-subscription
payload - every element of a batch response
- every part of a
multipart/mixedresponse
Request bodies are scanned up to 256 KB and response bodies are peeked up to 64 KB; requests larger than these limits are still traced as ordinary network spans without GraphQL enrichment.
- Batches: a JSON array of operations makes one network span. A batch of more than one operation is named
GraphQL: batchand getsgraphql.batch.sizeandgraphql.batch.operation.names(unique names in request order, max 10). A batch of one operation is named like that operation and getsgraphql.batch.size=1. Different batches share theGraphQL: batchrow; open a trace to see its operation names. - Persisted queries: a request with
extensions.persistedQuery(for example Apollo APQ) getsgraphql.persisted_query=trueandgraphql.persisted_query.id(thesha256Hash). The hash is never part of the span name. - Multipart responses: a fetch
multipart/*response (@defer,@stream, subscriptions) goes to your app at once. The SDK reads the stream as your app reads it and ends the span when the stream ends, is cancelled, or fails. A body your app never reads keeps its span open.
GraphQL over GET
GET requests are not parsed as GraphQL by default. To parse the operation from the query string (query, operationName, extensions) of a fetch or XHR GET, list your GraphQL endpoints in network.graphql.urls. A string matches when the full request URL contains it; a RegExp matches when it tests true against the full request URL. variables is never read from the URL.
L9RUM.init({ // Required settings... network: { graphql: { urls: ["https://api.myapp.com/graphql", /\/gql(\?|$)/], }, },});GraphQL detail (opt-in)
To debug a GraphQL request, you can export selected variables and the GraphQL error detail. Both are off by default. Set them with the graphqlDetail init option. The field names are the same in the Android, iOS, React Native, and Flutter SDKs.
L9RUM.init({ // Required settings... graphqlDetail: { captureVariables: true, variableAllowList: ["tenantId", "input.page.limit", "items.0.id"], extraSensitivePatterns: ["email"], captureErrorDetails: true, },});| Option | Type | Default | Description |
|---|---|---|---|
captureVariables | boolean | false | Export the allow-listed variables |
variableAllowList | string[] | [] | Dot paths to leaf values (tenantId, input.page.limit, items.0.id) |
extraSensitivePatterns | string[] | [] | Extra key patterns to redact, added to the built-in list |
maxVariables | number | 10 | Max graphql.variables.* attributes per span |
maxVariableValueLength | number | 256 | Max characters per variable value |
captureErrorDetails | boolean | false | Export per-error detail |
maxErrors | number | 5 | Max errors (by index) that get detail attributes |
maxErrorFieldLength | number | 200 | Max characters of each error field |
| Span attribute | Source |
|---|---|
graphql.variables.<entry> | The scalar value at the allow-list entry |
graphql.error.<i>.message | errors[i].message |
graphql.error.<i>.code | errors[i].extensions.code |
graphql.error.<i>.path | errors[i].path, joined with . (for example user.0.name) |
graphql.error.<i>.locations | errors[i].locations as line:column, joined with , |
exception.message | The first captured graphql.error.<i>.message |
- Only string, number, and boolean values are exported. Object, array, and
nullvalues are never exported, even when their path is allow-listed. - A value is exported as
[REDACTED]when any segment of its path contains a sensitive pattern (case-insensitive):password,passwd,secret,token,apikey,api_key,api-key,authorization,credential,cookie, plusextraSensitivePatterns. - The query text, the full request body, the response
data, errorextensionsother thancode, and HTTP headers are never captured, even with opt-in.
Dynamic segments in network span names
From 2.8.0, network span names fold fully-numeric and UUID path segments to ? so per-endpoint metrics aggregate instead of exploding by id. The same rule is used for view naming; version-like segments such as v2 are preserved.
| Request URL | Span name |
|---|---|
GET /workspaces/1 | GET /workspaces/? |
GET /users/550e8400-e29b-41d4-a716-446655440000 | GET /users/? |
GET /api/v2/orders | GET /api/v2/orders |
This is a behavior change for the emitted span_name on fetch, XHR, and resource spans. The full raw URL remains on http.url. GraphQL span names are operation-based and are not folded.
Add Custom Context
Global Attributes
Add business context to all monitoring data. Call L9RUM.spanAttributes() whenever context changes:
L9RUM.spanAttributes({ "app.org_slug": "acme-corp", "feature.flag.beta_checkout": true,});
// Clear attributes (e.g., on logout)L9RUM.spanAttributes(null);Note: Each call replaces all previous attributes, so always provide the complete set. These attributes become available as filters in the dashboard.
Custom Events
Track user actions and business events beyond standard page views:
L9RUM.addEvent("checkout_completed", { plan: "pro", amount: 99,});Each call dual-emits:
- A span event on the active view span, so the event shows up on the view’s timeline in RUM.
- An OTLP log record carrying
event.type=custom,event.name, your attributes, andsession.id/user attributes. When a view is active the log is correlated to it viatrace.id/span.id/view.id, so you can pivot between the log and the RUM session.
Log emission is unconditional. If no view is active when you call addEvent, the span event is dropped (no view span is fabricated to attach it to) but the log is still sent as an orphan, omitting trace.id/span.id/view.id. A custom event never silently vanishes because it fired outside a view.
Custom Spans
From 2.14.0, time your own operations with L9RUM. You do not need @opentelemetry/api.
// A finished span: ends now, started 250 ms agoL9RUM.recordSpan("checkout.validate", { durationMs: 250, attributes: { items: 3 },});
// Or with explicit epoch-ms timesL9RUM.recordSpan("sync", { startTimeMs: t0, endTimeMs: t1, kind: "client" });
// A span you end yourselfconst handle = L9RUM.startSpan("upload", { attributes: { "file.type": "png" },});// ... laterL9RUM.endSpan(handle);- A custom span is a child of the active view span and carries
view.idandl9.span.category=custom, so it shows under the page view. With no view open, it is a root span. recordSpantakes eitherdurationMsorstartTimeMs+endTimeMs. Both forms, neither, a partial pair, a negative duration, orend < startemit nothing and returnfalse.kindis"internal"(default),"client","server","producer", or"consumer".- Attributes keep strings, booleans, finite numbers, and string arrays. Other values are dropped. At most 64 of your attributes are kept, chosen by sorted key. Session, user, and global attributes are added as for any other span.
- A span not ended within 5 minutes, or still open on page exit, is ended with ERROR status.
endSpanreturnsfalsefor an unknown or already-ended handle. - Calls before
init, in a session that is not sampled, or with an empty name log a warning and returnfalse/null. The methods never throw.
Manual Error Capture
Explicitly track handled exceptions to maintain visibility into recoverable failures:
try { await searchProducts(query);} catch (error) { L9RUM.captureError(error, { handled: true, message: "search_query_timeout", query, page: 3, });}Manual captures default to handled: true. All errors — automatic and manual, pass through the errors.beforeSend hook for filtering.
beforeSend can drop an error (return null), add or change attributes, or remove them. An attribute that you delete in the hook — for example delete event.attributes["exception.stacktrace"] — is removed from both the trace span event and the OTel log record. Use this to remove PII or tokens before the SDK exports an error.
The SDK normalizes non-Error throws and promise rejections, including strings, primitives, plain objects, and objects with throwing properties. exception.type is inferred from the payload or stack header, and emitted error logs prefer the active RUM view span for trace.id and span.id.
Data Collection
The RUM SDK automatically collects performance and context data without affecting your application’s performance.
Sessions
Every span carries session.id and session.start_time (epoch ms, integer), so the backend can read the real start time of a session from any span — also from a span outside the selected time range.
- The
Session Startspan carries thesession.idandsession.start_timeof the new session. - When a session ends because of an inactivity timeout or a max-duration rollover,
Session Endandsession.time_spentuse the last recorded activity of the session. Idle and background time is not counted. The end always stays between the session start and the current time, also after a system-clock change.exitends use the actual end time. - A Browser RUM instance inside a native WebView that adopts the native session does not set
session.start_time, because the native-context bridge does not send a start time. - A
Session Endmeans the session ended, not that a page closed. A reload, hard navigation, or redirect does not emit one. A session that expires after the last tab closes gets itsSession Endwhen the next page loads, backdated to its last activity. Only one tab emits it.
Core Web Vitals
- Largest Contentful Paint (LCP): Loading performance measurement for largest visible content element
- First Contentful Paint (FCP): Initial rendering time for first visible content
- Cumulative Layout Shift (CLS): Visual stability score measuring unexpected layout shifts
- Interaction to Next Paint (INP): Responsiveness metric for user interactions
- Time to First Byte (TTFB): Server response time from initial request
Each metric is recorded with attribution data so you can see what caused a regression, not just that it happened:
- LCP — the largest-contentful element (CSS selector + ID when available), the resource URL, and the timing breakdown (
time_to_first_byte,resource_load_delay,resource_load_duration,element_render_delay) - CLS — the element with the largest layout shift (
largest_shift_target), its time, and value - INP — the event type (click, keydown, etc.), the target element, and the interaction phase that contributed most (
input_delay,processing_duration,presentation_delay) - FCP — the timing breakdown leading up to first paint
- TTFB — DNS, connection, request, and waiting durations
Document Load Timing
A full Navigation Timing breakdown is recorded for each page load, with derived metrics for the phases that matter:
- DOM processing duration
- Page rendering duration
- Network phase breakdown (DNS, connection, request, response)
- Total load time, DOM-interactive time, and DOM-complete time
Long Tasks
Main-thread blocking is tracked automatically. Each long task (>50 ms) is recorded with its duration, start time, and attribution to the script or container that caused it. A per-view Total Blocking Time is derived so you can spot interaction lag at a glance.
User Context
- Browser Information: Name, version, and complete user agent string
- Device Details: Type (desktop, mobile, tablet) and screen dimensions
- Network Conditions: Effective connection type (4g, 3g, 2g, slow-2g), downlink bandwidth estimate, round-trip time, and Data Saver mode — recorded per view so you can separate backend slowness from network flakiness
- Geolocation: Values supplied through
geoAttributesOverride, or country, region, city, IP, etc. from a successful GeoJS lookup whengeoIpEnabled: trueand no override object is set - User Identity: Name, email, role, and ID when provided via
identify()
Page Information
- Navigation Data: Page URL, path, referrer, and query parameters
- Route Changes: Single-page application navigation and route transitions
- Timing Metrics: Load times, navigation timing, and resource loading performance
- Network Activity: API calls, resource loading, and network errors
Verification
Confirm the RUM SDK is working correctly:
- Check Browser Console: Look for RUM initialization messages. Enable
debug: truein your configuration to see detailed logging. - Verify Network Requests: Open browser DevTools > Network tab and look for requests to your Applications base URL. Successful requests indicate data is being sent.
- Inspect API Calls: Confirm trace headers appear on backend requests if correlation is enabled.
- View Applications Dashboard: Navigate to Discover > Applications and confirm data appears within 2-3 minutes of page loads.
Check the connection from code
From 2.13.0, call sendTestEvent() after init to confirm that telemetry reaches Last9. The SDK sends one l9.test_event span at once and resolves with the export result. The promise never rejects.
await L9RUM.init({ /* ... */});
const result = await L9RUM.sendTestEvent();if (result.ok) { console.info("Last9 RUM is connected", result.status);} else { console.warn("Last9 RUM cannot reach Last9", result.status, result.error);}To monitor every export, pass onExport (each export) or onError (failed or unverified exports) to init. These hooks do not depend on debug.
L9RUM.init({ // Required settings... onError: (result) => { // for example { ok: false, status: 400, spanCount: 12, error: "HTTP 400: token origin not allowed" } reportToYourMonitoring(result); },});| Field | Type | Description |
|---|---|---|
ok | boolean | true when Last9 accepted the export (HTTP 2xx) |
status | number | HTTP status. 0 when there was no HTTP response (network error, CORS block, timeout) or the export used sendBeacon during unload |
spanCount | number | Number of spans in the export |
error | string? | Failure reason, with the Last9 response text when there is one. A queued sendBeacon export returns delivery_not_verified. sendTestEvent also uses not_initialized, session_not_sampled, and timeout |
When the session is not sampled, sendTestEvent sends nothing and resolves with error: "session_not_sampled". Set sampleRate: 100 while you verify the setup. With debug: true, the SDK also logs each export result and the OpenTelemetry diagnostic logs to the console.
Next Steps
With Applications monitoring successfully installed, explore the monitoring capabilities:
- Performance Monitoring: Track Core Web Vitals, analyze page performance, and identify bottlenecks
- Error Tracking: Monitor JavaScript exceptions and failed requests with detailed analysis
- Session Analysis: Analyze user journeys and navigation patterns
For the version history of this SDK, see the RUM changelog.
Troubleshooting
- No data showing? Verify your
baseUrlandclientTokenare correct. Check the browser console for initialization errors. Runawait L9RUM.sendTestEvent()and readstatusanderror(see Check the connection from code). - Exports report
status: 0? The browser got no readable HTTP response: a network error, a timeout, or a CORS block. Check the browser console for a CORS message on requests to your base URL. During page hide or unload,status: 0withdelivery_not_verifiedis expected, becausesendBeaconhas no response. - Custom spans missing? Use
L9RUM.recordSpan/L9RUM.startSpan(2.14.0and later). Spans created with your own@opentelemetry/apicopy are silently dropped when its minor version is newer than the SDK’s bundled1.9.0. - Trace headers missing? Confirm
network.backendCorrelation.enabledistrueand target domains are incorsAllowedOrigins. - Baggage not arriving on backend? Verify
baggage.enabledistrue, the attribute keys are listed inallowedKeys, and the values are set viaL9RUM.spanAttributes()before the request fires. Open DevTools → Network and check that thebaggageheader appears on outgoing requests. - Errors not appearing? Check that error toggles in the
errorsconfiguration are enabled andsampleRateis appropriate for your traffic. - “You don’t have access to this resource” on the beacon endpoint? Your application’s origin is not in the ingestion token’s allowed origins list. Go to Control Plane → Ingestion Tokens, edit your Client token, and add your domain (e.g.,
https://app.yourdomain.com). See origin configuration for format details.
Please get in touch with us on Discord or Email if you have any questions.