Skip to content
Last9
Book demo

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 Browser token 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

  1. Add the RUM SDK script to your index.html file in the <head> section:

    <script src="https://cdn.last9.io/rum-sdk/builds/stable/v2/l9.umd.js"></script>
  2. Initialize RUM in your application’s main component (typically App.js or index.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
    );
    }
  3. Add user tracking (optional):

    L9RUM.identify({ id, email, name, fullName, roles }) // on login
    L9RUM.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.
  • 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:

KeyMaps to
countryenduser.geo.country
country_codeenduser.geo.country_code
cityenduser.geo.city
organizationenduser.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 LevelDaily UsersRecommended RatePurpose
High Traffic>10,0001-10%Manage data volume while maintaining statistical significance
Medium Traffic1,000-10,00010-25%Balance coverage with data volume
Low Traffic<1,00025-50%Maximize insights with comprehensive data collection
Development/StagingAny50-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 to false)
  • throttleMs: Minimum gap between scroll spans (default: 500ms)
  • captureElementText: Include element text up to elementTextMaxLength (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 input and textarea fields 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, or aria-label contains email is sensitive — for example useremail, emailfield, emailaddress, or primaryemail. Email checkboxes and radio buttons, such as email_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:

  1. Open browser DevTools and trigger a backend request.
  2. Confirm the request includes W3C trace-context headers such as traceparent and tracestate.
  3. If baggage propagation is enabled, confirm only the allowed baggage keys are sent.
  4. Open the backend trace in Last9 and confirm the browser span and backend service span are part of the same trace.
  5. 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 propagated
L9RUM.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-xyz

Baggage 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

OptionTypeDefaultDescription
enabledbooleanfalseEnable W3C Baggage propagation
allowedKeysstring[][]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)
maxTotalBytesnumber8192Maximum baggage header size in bytes (W3C recommendation). Entries are truncated if exceeded
warnAtPercentagenumber80Log a warning when baggage size exceeds this percentage of maxTotalBytes

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 tool
L9RUM.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 attributes
class 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 setup
const { 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.

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 attributeExampleNotes
Span nameGraphQL: "GetUser" queryAnonymous operations become GraphQL: query
graphql.operation.nameGetUserOperation name parsed from the request body
graphql.operation.typequeryquery, mutation, or subscription
l9.span.categorynetworkCategorizes 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 attributeValueNotes
error.typeGraphQLErrorSet when the response contains GraphQL errors
graphql.error.countnumberNumber of GraphQL errors in the response

Errors are counted in:

  • errors[]
  • @defer / @stream payloads (incremental[].errors, completed[].errors)
  • a multipart-subscription payload
  • every element of a batch response
  • every part of a multipart/mixed response

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: batch and gets graphql.batch.size and graphql.batch.operation.names (unique names in request order, max 10). A batch of one operation is named like that operation and gets graphql.batch.size=1. Different batches share the GraphQL: batch row; open a trace to see its operation names.
  • Persisted queries: a request with extensions.persistedQuery (for example Apollo APQ) gets graphql.persisted_query=true and graphql.persisted_query.id (the sha256Hash). 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,
},
});
OptionTypeDefaultDescription
captureVariablesbooleanfalseExport the allow-listed variables
variableAllowListstring[][]Dot paths to leaf values (tenantId, input.page.limit, items.0.id)
extraSensitivePatternsstring[][]Extra key patterns to redact, added to the built-in list
maxVariablesnumber10Max graphql.variables.* attributes per span
maxVariableValueLengthnumber256Max characters per variable value
captureErrorDetailsbooleanfalseExport per-error detail
maxErrorsnumber5Max errors (by index) that get detail attributes
maxErrorFieldLengthnumber200Max characters of each error field
Span attributeSource
graphql.variables.<entry>The scalar value at the allow-list entry
graphql.error.<i>.messageerrors[i].message
graphql.error.<i>.codeerrors[i].extensions.code
graphql.error.<i>.patherrors[i].path, joined with . (for example user.0.name)
graphql.error.<i>.locationserrors[i].locations as line:column, joined with ,
exception.messageThe first captured graphql.error.<i>.message
  • Only string, number, and boolean values are exported. Object, array, and null values 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, plus extraSensitivePatterns.
  • The query text, the full request body, the response data, error extensions other than code, 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 URLSpan name
GET /workspaces/1GET /workspaces/?
GET /users/550e8400-e29b-41d4-a716-446655440000GET /users/?
GET /api/v2/ordersGET /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, and session.id/user attributes. When a view is active the log is correlated to it via trace.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 ago
L9RUM.recordSpan("checkout.validate", {
durationMs: 250,
attributes: { items: 3 },
});
// Or with explicit epoch-ms times
L9RUM.recordSpan("sync", { startTimeMs: t0, endTimeMs: t1, kind: "client" });
// A span you end yourself
const handle = L9RUM.startSpan("upload", {
attributes: { "file.type": "png" },
});
// ... later
L9RUM.endSpan(handle);
  • A custom span is a child of the active view span and carries view.id and l9.span.category=custom, so it shows under the page view. With no view open, it is a root span.
  • recordSpan takes either durationMs or startTimeMs + endTimeMs. Both forms, neither, a partial pair, a negative duration, or end < start emit nothing and return false.
  • kind is "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. endSpan returns false for 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 return false / 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 Start span carries the session.id and session.start_time of the new session.
  • When a session ends because of an inactivity timeout or a max-duration rollover, Session End and session.time_spent use 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. exit ends 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 End means 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 its Session End when 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 when geoIpEnabled: true and 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:

  1. Check Browser Console: Look for RUM initialization messages. Enable debug: true in your configuration to see detailed logging.
  2. Verify Network Requests: Open browser DevTools > Network tab and look for requests to your Applications base URL. Successful requests indicate data is being sent.
  3. Inspect API Calls: Confirm trace headers appear on backend requests if correlation is enabled.
  4. 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);
},
});
FieldTypeDescription
okbooleantrue when Last9 accepted the export (HTTP 2xx)
statusnumberHTTP status. 0 when there was no HTTP response (network error, CORS block, timeout) or the export used sendBeacon during unload
spanCountnumberNumber of spans in the export
errorstring?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:

For the version history of this SDK, see the RUM changelog.


Troubleshooting

  • No data showing? Verify your baseUrl and clientToken are correct. Check the browser console for initialization errors. Run await L9RUM.sendTestEvent() and read status and error (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: 0 with delivery_not_verified is expected, because sendBeacon has no response.
  • Custom spans missing? Use L9RUM.recordSpan / L9RUM.startSpan (2.14.0 and later). Spans created with your own @opentelemetry/api copy are silently dropped when its minor version is newer than the SDK’s bundled 1.9.0.
  • Trace headers missing? Confirm network.backendCorrelation.enabled is true and target domains are in corsAllowedOrigins.
  • Baggage not arriving on backend? Verify baggage.enabled is true, the attribute keys are listed in allowedKeys, and the values are set via L9RUM.spanAttributes() before the request fires. Open DevTools → Network and check that the baggage header appears on outgoing requests.
  • Errors not appearing? Check that error toggles in the errors configuration are enabled and sampleRate is 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.