RUM and Real User Context
The idea in one sentence
Section titled “The idea in one sentence”RUM captures what a real visitor actually did in their browser or app as structured events, and when it propagates trace context into the backend calls that visitor triggers, it extends the correlation story one hop past the backend trace, out to the human who caused it.
What RUM actually captures
Section titled “What RUM actually captures”Real User Monitoring (RUM) runs as an SDK inside the browser or mobile app itself, not on your servers. As a real visitor interacts with the page or app, it emits a small set of structured event types:
- session — one visit, start to finish
- view — a page (or a screen, on mobile) the user landed on
- action — a click, a tap, a form submission
- error — a JS exception or a failed resource load, captured client-side
This is a different vantage point than synthetic monitoring (a scripted browser pretending to be a user on a schedule, covered elsewhere in this course). RUM is the actual browser of an actual visitor, in real time, so it reflects real network conditions, real devices, and the real paths users take through the product — not one script’s idealized path.
NPM vs. CDN: two ways to load the same SDK
Section titled “NPM vs. CDN: two ways to load the same SDK”import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({ applicationId: '<RUM_APP_ID>', clientToken: '<RUM_CLIENT_TOKEN>', site: '<DATADOG_SITE>', service: 'checkout-web', sessionSampleRate: 100, sessionReplaySampleRate: 0});Installed the real, current @datadog/[email protected] (npm view @datadog/browser-rum version) and checked this shape against its own shipped .d.ts (RumInitConfiguration in configuration.d.ts) — every key here is real. That same interface’s own JSDoc shows the two ways this SDK actually loads: an npm import calling datadogRum.init({...}), or a CDN <script> tag exposing a window.DD_RUM global calling DD_RUM.init({...}) with the identical option object. Same SDK, same init shape, two different ways to get the script into the page — an npm bundle step, or a <script> tag with no build step at all.
allowedTracingUrls: the allowlist that links RUM to APM
Section titled “allowedTracingUrls: the allowlist that links RUM to APM”On its own, a RUM action or view is frontend-only — it tells you a user clicked “Pay now,” not what happened on the server afterward. The correlation payoff is that when the RUM SDK is configured to trace a specific set of backend URLs, it attaches the same kind of trace context header a backend tracer looks for onto the outgoing XHR/fetch call, so the backend service’s span, once created, links back to that exact RUM view/action.
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({ applicationId: '<RUM_APP_ID>', clientToken: '<RUM_CLIENT_TOKEN>', site: '<DATADOG_SITE>', service: 'checkout-web', allowedTracingUrls: [ (url) => url.startsWith('https://api.example.com') ]});Session Replay: a related but separate feature
Section titled “Session Replay: a related but separate feature”Session Replay reconstructs a visual, pixel-level recreation of what the user actually saw and did, built from the same RUM session — it is not automatically included just because RUM is initialized. Checked the real .d.ts defaults instead of assuming:
sessionSampleRate @defaultValue 100 -- RUM itself: on by defaultsessionReplaySampleRate @defaultValue 0 -- Session Replay: OFF by defaultdefaultPrivacyLevel @defaultValue mask-user-inputsessionReplaySampleRate defaults to 0 — Session Replay is opt-in, not something that silently turns on the moment RUM does. defaultPrivacyLevel defaults to mask-user-input (real values, from the same shipped source: allow, mask, mask-user-input, mask-unless-allowlisted) — meaning out of the box, Session Replay masks user-typed input by default rather than recording it in the clear, a privacy-conscious default rather than an opt-out one. Enabling replay recording is one more init key:
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({ applicationId: '<RUM_APP_ID>', clientToken: '<RUM_CLIENT_TOKEN>', site: '<DATADOG_SITE>', service: 'checkout-web', sessionSampleRate: 100, sessionReplaySampleRate: 20, defaultPrivacyLevel: 'mask'});docs.datadoghq.com/real_user_monitoring/session_replay/browser/privacy_options is the canonical reference for what each privacy level actually masks (form inputs only, vs. all text content, vs. an explicit allowlist) — worth reading before turning replay on for any page handling real user data, since the strictest default (mask-user-input) still records the DOM structure and non-input text around it.
Sampling: two independent dials
Section titled “Sampling: two independent dials”sessionSampleRate controls what fraction of sessions RUM tracks at all (@defaultValue 100 — every session, by default). sessionReplaySampleRate controls what fraction of those already-sampled sessions additionally get a replay recording (@defaultValue 0). They compose: sessionSampleRate: 50, sessionReplaySampleRate: 20 means roughly 50% of sessions are tracked by RUM at all, and of that 50%, roughly 20% additionally get a replay — not 20% of all sessions.
User context: setUser
Section titled “User context: setUser”import { datadogRum } from '@datadog/browser-rum';
datadogRum.setUser({ id: 'user-48213', name: 'A. Vetavos',});The real .d.ts (rumPublicApi.d.ts) declares two overloads: setUser(newUser: User & { id: string }) is the current, recommended shape — id required — and a second, @deprecated overload accepting a User with no id at all, kept only for backward compatibility. Every RUM event emitted after this call carries the user context under the @usr attribute, which is what lets you filter RUM (and, via the same correlation this lesson covers, the backend traces those sessions triggered) down to “everything this one specific user experienced” — useful for reproducing a bug a specific customer reported, without needing them to describe exactly what they clicked.
One request, traced all the way to the person
Section titled “One request, traced all the way to the person”A backend trace alone answers “which spans, across which services, handled this request” — that is the whole story the APM module told. RUM extends the same trace_id idea one hop further, out past the edge of your infrastructure, so the same identifier that ties spans together across services also ties back to a RUM view/session — answering “which real user, on which page, clicked what, that caused this trace to exist in the first place.” This lesson stays intentionally light — RUM is a large enough topic for a course of its own — but the correlation idea is the same one this whole module has been building: shared identifiers instead of manual guesswork.
flowchart LR U[Real user in browser] --> Act["RUM action: click 'Pay now'"] Act --> View[RUM view/session] Act --> Req["Traced fetch/XHR call\n(trace context injected)"] Req --> Span[Backend service span] Span --> Trace[APM distributed trace] View -.linked to.-> Trace
Reading AI-generated code
Section titled “Reading AI-generated code”🐛 Spot the bug
// AI comment: "Wire up RUM so we can see what users are doing on checkout"import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({applicationId: '<RUM_APP_ID>',clientToken: '<RUM_CLIENT_TOKEN>',site: '<DATADOG_SITE>',sessionSampleRate: 100,});
datadogRum.setUser({ name: 'checkout-user' });Show the answer
This compiles, loads, and starts recording real sessions/views/actions immediately — nothing about it looks broken. Two real gaps, proven above against the real .d.ts and docs. First: no service and no allowedTracingUrls — the doc comment on service says allowedTracingUrls requires it explicitly, and neither key has a default that would enable RUM-to-APM linking on its own, so every backend trace this checkout flow triggers stays completely disconnected from the RUM sessions that caused them — silently, with RUM itself working perfectly the whole time. Second: setUser({ name: 'checkout-user' }) uses the @deprecated overload (no id) — it still runs, but every RUM event’s @usr context is missing the one field (id) that makes it possible to reliably pivot to “everything this specific user did,” since names collide and IDs don’t. The fix: add service: 'checkout-web' and an allowedTracingUrls list matching the real backend origins, and pass a real id to setUser.
Mental Model
Section titled “Mental Model”Think of RUM as a second camera pointed at the same event from a completely different angle than backend tracing. Backend tracing’s camera is inside your infrastructure, watching spans hand off to each other. RUM’s camera is inside the user’s browser, watching clicks and page loads. Neither camera alone answers the full question. allowedTracingUrls is the only thing that splices the two camera feeds into one continuous shot — without it, you have two separate, perfectly good recordings of the same incident that never get edited together, and nothing on either side ever complains about the other camera’s footage being disconnected.
Trade-offs
Section titled “Trade-offs”sessionSampleRate: 100vs. a lower value — sampling every session gives complete RUM coverage but costs proportionally more (RUM is priced per tracked session); a lower rate saves cost at the price of only seeing a statistical slice of real user behavior, which matters more for a low-traffic page than a high-traffic one.sessionReplaySampleRateon vs. off (default0) — Session Replay is the most visually useful debugging tool RUM offers (seeing exactly what the user saw), but it is priced and sampled separately from RUM itself for a reason: recording, storing, and rendering full session video is far more expensive per session than structured events alone, which is exactly why the SDK defaults it to off rather than mirroringsessionSampleRate’s default-on stance.defaultPrivacyLevel: 'mask-user-input'(the default) vs.'mask'or'allow'— the stricter default protects sensitive form input (passwords, card numbers) automatically at the cost of some replay fidelity; loosening it toallowrecords everything faithfully but shifts the entire responsibility for not capturing sensitive data onto whoever configured the page.
Common Misconceptions
Section titled “Common Misconceptions”- “Initializing RUM automatically records Session Replay.” No — proven above from the real
.d.ts:sessionReplaySampleRatedefaults to0. RUM’s structured events (session/view/action/error) and Session Replay’s video-like recording are sampled and priced independently. - “
allowedTracingUrlsalone is enough to link RUM and APM.” No — proven above: theserviceinit option’s own doc comment statesallowedTracingUrlsstill requires an explicitly configuredservice. Both are required; missing either one produces no error, just two disconnected sets of telemetry. - “
window.DD_RUMand the npmdatadogRumexport are different SDKs with different capabilities.” No — proven above from the shipped.d.ts’s own JSDoc examples: they are the same SDK, sameinit()shape, loaded two different ways (a CDN<script>tag vs. an npm import). - “
setUser({ name: ... })without anidis just as good as passing one.” No — proven above: that overload is explicitly marked@deprecatedin the real.d.tsprecisely because names are not reliable identifiers; a realidis what a filtered “everything this user did” view actually depends on.