diff --git a/CLAUDE.md b/CLAUDE.md
index af090430..3deca6bd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,6 +24,52 @@ The pattern the user flags most: **announcing a quantity + a vague nominalizatio
| "Both workflow types are supported." | flat passive announcement | "KloudMate supports both Standard and Express workflows." |
| "…with three tabs: Executions, Metrics, Configuration." | needless count | "…with tabs for Executions, Metrics, and Configuration." |
+### The reference sample
+
+[`src/content/docs/docs/rum/instrumentation-guide/custom-events.mdx`](src/content/docs/docs/rum/instrumentation-guide/custom-events.mdx) and the **Filter the data** section of [`rum-interface.mdx`](src/content/docs/docs/rum/rum-interface.mdx) were rewritten by the maintainer as the model for this repo. Read them before writing a new page and match what they do:
+
+- **Second person, explaining to a competent reader.** "Every attribute you send with an event can be used as both a group-by key and a filter." Not a terse spec line, not a lecture.
+- **"For example," carries the concrete case.** State the rule, then show it: "For example, `checkout_completed` is a good event name."
+- **Complete sentences with the consequence spelled out.** "If you send `4999` as a string, it will be stored as text, so a filter such as `items > 2` won't work as expected."
+- **Contractions throughout:** don't, can't, won't, it's, you've.
+- **Bold for the concept being contrasted,** not for decoration: "The event name should describe **what happened**, not the specific object or user it happened to."
+
+### Don't describe the plumbing
+
+The reader has the product open. Describing what a screen does is fine when it changes how they read the data ("the filter bar tells you what it is matching", "it is shown as struck through with an explanation"). Describing how it is built is not.
+
+Cut on sight:
+
+- **Control plumbing:** "holds the choice in the URL", "URL backed, so a breakdown is shareable", "on the view-switcher row", "sits beside the bar".
+- **Quoted on-screen message strings, and "the tab says so".** State the rule that produces the message. The reader will read the message when they hit it.
+- **Menu geography:** "the **by** menu lists them under **Payload**, next to **Attributes**".
+- **Implementation:** which attribute map a value lands in, what the query compiles to, why the design went that way.
+
+| Don't write | Write instead |
+|---|---|
+| "Each view has its own **by** control and holds the choice in the URL, so a breakdown can be shared." | "You can group by any attribute your application sends in the event payload, in both Analyze and Raw events." |
+| "When nothing in the range carries the key, the tab falls back and says so: `Nothing in this range carries category…`" | "If you group by a key that isn't present in the selected range, the grouping falls back to event name." |
+| "Journeys resolves it over whole sessions through a semi-join." | "In a funnel, `cart_value > 100` selects sessions that emitted at least one matching event." |
+
+### Sentence shape
+
+One idea per sentence, subject first, verb early. Any clause the reader has to unpack reads as machine writing, and so does a technical thing described in literary paraphrase.
+
+| Don't write | Why it reads as AI | Write instead |
+|---|---|---|
+| "Group by anything the events carried." | literary paraphrase where a technical noun exists | "Group by any attribute from the event payload." |
+| "Events that never carried the attribute group under **No category**, not a blank row." | front-loaded relative clause, then an "X, not Y" tail | "Events that were sent without the attribute are collected into a single group. When you group by `category`, that group appears as **No category**." |
+| "…so it marks an instrumentation gap rather than a category." | "X rather than Y", contrasting against something nobody proposed | "It tells you how many events were sent without the attribute at all, so it's really a measure of missing instrumentation." |
+| "Over time charts the 50 largest groups. Group by **User** and most of them are missing." | pronoun with no clear antecedent; "missing" implies a bug | "If you group by an attribute with thousands of distinct values, such as a user ID, most of them won't appear on the chart." |
+| "**Events** and **Journeys** answer it." · "**Errors** cannot answer one." | screens do not answer, know, want, or try; anthropomorphism reads as drama | "**Events** and **Journeys** support payload filters." · "**Errors**, **Pages**, and **Releases** don't support payload filters." |
+| "The group counts missing instrumentation. Exclude it to compare the real values. Every panel on the tab drops it." | telegraphic fragments; plain is not the same as clipped | "Excluding it removes those events from every panel on the tab, which lets you compare the real categories against each other." |
+
+Name things with the term the product and the reader both use. "Anything the events carried" is a phrase; "an attribute from the event payload" is the thing.
+
+Plain does not mean cryptic. Cutting a sentence to four words that the reader then has to decode is a worse failure than the padding it replaced. Write the full sentence, then delete only what carries no meaning.
+
+Detail has a bar too: state the behavior, not its operator-by-operator mechanics. "Selecting more than one value on a field matches any of them" is the rule; "two `=` picks become **In**, two `!=` picks become **Not in**, and a second `>` replaces the first" is a spec dump.
+
### Tone (AGENTS.md §2 has the full word lists; these are the repeat offenders)
- **Em dashes for asides** → period, comma, colon, or parentheses.
diff --git a/astro.config.mjs b/astro.config.mjs
index 76e011be..7a5bee86 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -75,7 +75,8 @@ export default defineConfig({
// KloudMate RUM. `init` runs from `onload` so the bundle is fully executed
// before it is called; the rolling /v2/ path picks up SDK patches without an
// edit here. Session recording is off, so this collects page performance and
- // errors only.
+ // errors only. The `km-rum-ready` event lets UserNav.astro attach the
+ // signed-in user's userId/userEmail even though this bundle loads async.
head: [
{
tag: 'script',
@@ -91,7 +92,7 @@ export default defineConfig({
version: '',
sampleRate: 1.0,
sessionRecorder: { enabled: false },
- })`,
+ }); document.dispatchEvent(new Event('km-rum-ready'))`,
},
},
],
diff --git a/src/components/Header.astro b/src/components/Header.astro
index 27ce870a..dbb4f336 100644
--- a/src/components/Header.astro
+++ b/src/components/Header.astro
@@ -11,6 +11,8 @@ import starlightConfig from 'virtual:starlight/user-config';
import options from 'virtual:starlight-theme-nova/user-config';
import MobileMenuToggle from 'starlight-theme-nova/components/MobileMenuToggle.astro';
+import UserNav from './UserNav.astro';
+
const route = Astro.locals.starlightRoute;
const currentPath = normalizePath(Astro.url.pathname);
const siteTitleHref = withBase(route.siteTitleHref);
@@ -200,6 +202,7 @@ function getI18nText(value, activeRoute) {
+
diff --git a/src/components/UserNav.astro b/src/components/UserNav.astro
new file mode 100644
index 00000000..0818fd95
--- /dev/null
+++ b/src/components/UserNav.astro
@@ -0,0 +1,132 @@
+---
+/**
+ * Account button for the header: "Log in" by default, "Go to Dashboard" when a
+ * KloudMate session exists.
+ *
+ * The docs share the platform's HTTP-only session cookie (scoped to
+ * `.kloudmate.com`), so `GET https://api.
/users/me` with credentials
+ * resolves the signed-in user without any auth flow here. The API's CORS
+ * allowlist already reflects any `*.` origin with credentials.
+ * The same result is attached to RUM spans as `userId` / `userEmail`, which is
+ * what the RUM sessions UI reads (anonymous sessions otherwise).
+ *
+ * Off-domain (localhost, previews) there is no shared cookie, so the script
+ * skips the network call and keeps the static logged-out default.
+ */
+---
+
+Log in
+
+
+
+
diff --git a/src/content/docs/docs/platform/settings/notification-channels.mdx b/src/content/docs/docs/platform/settings/notification-channels.mdx
index 22eb28ab..1ced6823 100644
--- a/src/content/docs/docs/platform/settings/notification-channels.mdx
+++ b/src/content/docs/docs/platform/settings/notification-channels.mdx
@@ -86,6 +86,18 @@ Optionally, you can download the KloudMate logo from the link provided on the sc

+#### Customize the Request Body and Headers
+
+By default, a webhook sends KloudMate's standard JSON payload, which some receivers reject. Google Chat, for example, accepts only a message object like `{"text": "..."}`, with its auth token in the webhook URL, so no header or secret is needed. The same `{"text": ...}` body also works for Slack incoming webhooks and Microsoft Teams. A webhook channel can rewrite its outgoing body and add headers to match.
+
+In the webhook channel editor, click **Customize request body and headers** to expand the options.
+
+**Request body** takes JSON with Liquid `{{ }}` placeholders inside string values, filled from the event. Leave it empty to send the default payload. The **Examples** menu fills it with a starter template, and [Notification Payloads](#notification-payloads) lists the fields each event type exposes.
+
+**Custom headers** are key/value pairs sent with every request. `Content-Type` and `X-KM-Signature` are reserved and set automatically.
+
+**Test** previews the rendered body against a sample event. The **Event type** list picks which event to sample.
+
#### Webhook Signature Validation
When KloudMate sends a webhook to your configured URL, it includes an `X-KM-Signature` HTTP header. This signature is an HMAC-SHA256 hash generated using the payload body and the **Secret** you provided during configuration.
@@ -152,18 +164,32 @@ Here are examples of how to perform this validation in common programming langua
#### Notification Payloads
-Every webhook is delivered as a single `POST` with a JSON body and `Content-Type: application/json`. The body you receive is exactly the bytes that were HMAC-signed (see above), so verify the signature against the raw body before parsing.
+Every webhook is a single `POST` with a JSON body and `Content-Type: application/json`. Without a custom request body, this is what KloudMate sends, and a template reads its fields from the same payload. Receivers that verify the signature should hash the raw body before parsing it.
-The payload shape depends on which KloudMate feature triggered the notification:
+Each notification has an event type. The **Event type** picker in the channel editor lists all of them, and a template can reference any field from the matching payload.
-- **Alert group** notifications carry a top-level `event` field (`opened`, `appended`, `resolved`, or `rca_completed`) that identifies the lifecycle stage.
-- The other payloads have no `event` field — identify them by their distinctive top-level keys: `slo_name` (SLO burn rate), `monitor_id` (synthetic monitor), `serviceName` (new issue), and `investigationId` (AI investigation).
+| Event type | Payload |
+| --- | --- |
+| `alarm_group.opened` | Alert group, `event` is `opened` |
+| `alarm_group.appended` | Alert group, `event` is `appended` |
+| `alarm_group.resolved` | Alert group, `event` is `resolved` |
+| `alarm_group.rca_completed` | Alert group, `event` is `rca_completed` |
+| `issue.created` | New issue |
+| `slo.burn_rate.firing` | SLO burn rate |
+| `slo.burn_rate.resolved` | SLO burn rate |
+| `synthetics.incident.created` | Synthetic monitor |
+| `synthetics.incident.resolved` | Synthetic monitor |
+| `investigation.completed` | AI investigation |
-The tabs below show one representative example of each payload, with notes on how the variants differ.
+The alarm group payload sets a top-level `event` field to the lifecycle stage. The other payloads carry no `event` field. A receiver tells them apart by their distinctive top-level keys: `slo_name` for SLO burn rate, `monitor_id` for a synthetic monitor, `serviceName` for a new issue, and `investigationId` for an AI investigation.
+
+The tabs below show one representative example of each payload, the fields most useful in a template, and notes on how the variants differ.
- Sent when an [alert group](../../../alerts/) opens, appends new alerts, resolves, or completes a root-cause analysis. The `event` field tells you which one. `workspace` identifies the workspace (tenant) the alert belongs to. `group` carries the group's id, title, state, severity, `labels`, `annotations`, and its link in KloudMate. `rules` lists each alarm rule in the group; each rule's `instances` array holds every matched instance with its own `labels`, `annotations`, and `state`, and `commonLabels`/`commonAnnotations` are the values shared by all of them. `totals` and each rule's `counts` are keyed by state: `Firing`, `Resolved`, `No Data`, `Error`, `Normal`. `group.mode` is `"group"` for a correlated group or `"standalone"` for a single alarm rule.
+ **Event types:** `alarm_group.opened`, `alarm_group.appended`, `alarm_group.resolved`, `alarm_group.rca_completed`. **Fields for templates:** `group.title`, `group.severity`, `group.state`, `group.url`, `group.opened_at`, `totals.Firing`, `rca.summary`, `rules[0].alarm_name`, `rules[0].instances[0].labels.`.
+
+ Sent when an [alert group](../../../alerts/) opens, appends new alerts, resolves, or completes a root-cause analysis. The `event` field names which one. `workspace` identifies the workspace (tenant) the alert belongs to. `group` carries the group's id, title, state, severity, `labels`, `annotations`, and its link in KloudMate. `rules` lists each alarm rule in the group; each rule's `instances` array holds every matched instance with its own `labels`, `annotations`, and `state`, and `commonLabels`/`commonAnnotations` are the values shared by all of them. `totals` and each rule's `counts` are keyed by state: `Firing`, `Resolved`, `No Data`, `Error`, `Normal`. `group.mode` is `"group"` for a correlated group or `"standalone"` for a single alarm rule.
```json
{
@@ -237,6 +263,8 @@ The tabs below show one representative example of each payload, with notes on ho
```
+ **Event type:** `issue.created`. **Fields for templates:** `subject`, `serviceName`, `severity`, `errorMessage`, `occurrence`, `lastOccurrenceAt`, `actionUrl`, and `actionLabel`.
+
Sent when a new [issue](../../../issues/) is created for a service.
```json
@@ -253,6 +281,8 @@ The tabs below show one representative example of each payload, with notes on ho
```
+ **Event types:** `slo.burn_rate.firing`, `slo.burn_rate.resolved`. **Fields for templates:** `subject`, `slo_name`, `severity_tier`, `threshold`, `short_window`, `long_window`, `observed_short_burn`, `observed_long_burn`, `status_line`, and `actionUrl`.
+
Sent when an [SLO burn-rate alert](../../../reliability/burn-rate-alerts/) starts firing or resolves. `threshold` and the observed burn rates are formatted with a trailing `×` (an infinite rate renders as `∞`); windows render as `5m`, `1h`, etc.
```json
@@ -275,6 +305,8 @@ The tabs below show one representative example of each payload, with notes on ho
On resolve, `subject` becomes `"✅ SLO burn-rate resolved: Checkout availability"` and `status_line` becomes a duration, e.g. `"Fired for 17 minutes"`. `severity_tier` is one of `critical`, `high`, `medium`, or `low`.
+ **Event types:** `synthetics.incident.created`, `synthetics.incident.resolved`. **Fields for templates:** `subject`, `name`, `target`, `cause`, `start_time`, `duration`, `monitor_id`, `actionUrl`, and `actionLabel`.
+
Sent when a [synthetic monitor](../../../synthetic/) goes down or recovers.
```json
@@ -294,6 +326,8 @@ The tabs below show one representative example of each payload, with notes on ho
On recovery, `subject` becomes `"🟢 Monitor is UP: API health"`, `duration` is populated (e.g. `"15 minutes"`), and an `end_time` field is added.
+ **Event type:** `investigation.completed`. **Fields for templates:** `subject`, `title`, `rootCauseAnalysisText`, `completedAt`, `actionUrl`, and `actionLabel`.
+
Sent when an [AI investigation](../../../kloudmate-assistant/investigations/) completes. `rootCauseAnalysisText` is plain text, truncated to 8000 characters.
```json
diff --git a/src/content/docs/docs/rum/correlate-rum-traces-with-opentelemetry-backends.mdx b/src/content/docs/docs/rum/correlate-rum-traces-with-opentelemetry-backends.mdx
index d66fa01f..08b77f0d 100644
--- a/src/content/docs/docs/rum/correlate-rum-traces-with-opentelemetry-backends.mdx
+++ b/src/content/docs/docs/rum/correlate-rum-traces-with-opentelemetry-backends.mdx
@@ -5,11 +5,11 @@ sidebar:
order: 5
---
-Connect a request from your web app to the backend trace behind it, so a slow call in a session opens onto the service that made it slow.
+Connect a request from your web app to the backend trace behind it, so you can follow a slow call in a session through to the service that made it slow.
-The SDK already adds [W3C context propagation headers](https://opentelemetry.io/docs/concepts/context-propagation/#propagation) to `fetch` and XHR requests made to the same origin, so those are stitched with no work.
+The SDK already adds [W3C context propagation headers](https://opentelemetry.io/docs/concepts/context-propagation/#propagation) to `fetch` and XHR requests made to the same origin, so those are stitched together with no setup.
-To reach a backend on a different origin, configure the SDK to send the headers, then allow those headers in your backend's CORS policy. Do the first without the second and the browser blocks every call to that origin.
+To reach a backend on a different origin, configure the SDK to send the headers, then allow those headers in your backend's CORS policy. If you do the first without the second, the browser blocks every call to that origin.
## Send the trace headers from the browser
@@ -29,7 +29,7 @@ KloudMateRum.init({
});
```
-This adds context propagation headers to those requests. The backend can then generate its spans using this context.
+This adds context propagation headers to those requests. The backend can then create its spans from that context.
:::note
You can find examples of context extraction on the backend here: [https://opentelemetry.io/docs/languages/js/propagation/#generic-example](https://opentelemetry.io/docs/languages/js/propagation/#generic-example)
@@ -41,7 +41,7 @@ This adds context propagation headers to those requests. The backend can then ge
Skipping this step breaks the requests themselves, not just the tracing.
:::
-`traceparent` isn't a [CORS-safelisted request header](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header), so adding it turns every cross-origin call into a preflighted one. Your backend has to name it in the `Access-Control-Allow-Headers` response header. If it doesn't, the browser blocks the request before it's ever sent:
+`traceparent` isn't a [CORS-safelisted request header](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header), so adding it turns every cross-origin call into a preflighted one. Your backend must name it in the `Access-Control-Allow-Headers` response header. If it doesn't, the browser blocks the request before it's sent:
```text
Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com'
@@ -69,17 +69,17 @@ Match what you're seeing in the browser to the fix:
| Requests succeed, but the browser session and the backend show separate traces | The SDK never attached `traceparent`, so the backend started its own trace | Add the origin to `propagateTraceHeaderCorsUrls` |
| Requests fail with `Request header field traceparent is not allowed by Access-Control-Allow-Headers` | The SDK attached `traceparent`, but the backend's preflight response rejects it | Add `traceparent` to `Access-Control-Allow-Headers` |
-To confirm the header is arriving, log `req.headers.traceparent` on the backend. A value means the trace is stitched: the ID it carries is the same one on the browser span.
+To confirm the header is arriving, log `req.headers.traceparent` on the backend. If it has a value, the trace is stitched, and the ID it carries is the same one on the browser span.
-Once it is stitched, **View full backend trace** on a request row in [What happened](../session-detail/#what-happened) opens the distributed trace behind that call.
+After that, **View full backend trace** on a request row in [What happened](../session-detail/#what-happened) opens the distributed trace behind that call.
-**Sample Integration:**
+## Example
-1. Once the RUM and backend are integrated you can view the corresponding backend trace of a frontend request
+With RUM and the backend both instrumented, a request row in a session opens the backend trace behind it:

-2\. You can identify the same in APM traces
+The same trace is available from APM:

diff --git a/src/content/docs/docs/rum/images/rum-events-analyze.png b/src/content/docs/docs/rum/images/rum-events-analyze.png
deleted file mode 100644
index 725b8270..00000000
Binary files a/src/content/docs/docs/rum/images/rum-events-analyze.png and /dev/null differ
diff --git a/src/content/docs/docs/rum/images/rum-events-distribution.png b/src/content/docs/docs/rum/images/rum-events-distribution.png
new file mode 100644
index 00000000..a7bddd7f
Binary files /dev/null and b/src/content/docs/docs/rum/images/rum-events-distribution.png differ
diff --git a/src/content/docs/docs/rum/images/rum-events-over-time.png b/src/content/docs/docs/rum/images/rum-events-over-time.png
new file mode 100644
index 00000000..2de79122
Binary files /dev/null and b/src/content/docs/docs/rum/images/rum-events-over-time.png differ
diff --git a/src/content/docs/docs/rum/images/rum-events-raw.png b/src/content/docs/docs/rum/images/rum-events-raw.png
new file mode 100644
index 00000000..de2ed216
Binary files /dev/null and b/src/content/docs/docs/rum/images/rum-events-raw.png differ
diff --git a/src/content/docs/docs/rum/index.mdx b/src/content/docs/docs/rum/index.mdx
index 9f8c7967..a35cbfa8 100644
--- a/src/content/docs/docs/rum/index.mdx
+++ b/src/content/docs/docs/rum/index.mdx
@@ -5,44 +5,42 @@ sidebar:
label: "Overview"
order: 1
---
-Real user monitoring (RUM) is a performance monitoring practice that is used to observe and measure the performance of a website or an application from the users' perspective. RUM captures data on how real users interact with the system in real-time, which includes metrics like page load times, transaction speeds, error rates, and user behavior patterns.
-This data provides insights into how users directly experience the system. Using this data, developers can identify and address performance bottlenecks, understand user behavior, and improve overall user satisfaction of their websites or applications.
+Real user monitoring (RUM) measures how your site or app performs for the people actually using it. A synthetic test measures a page on a machine you control. RUM reports what happened in a real browser, on a real device, over a real network: how long pages took to load, which requests failed, what visitors clicked, and where they gave up.
-## Real User Monitoring With KloudMate
+Those two numbers often differ sharply. A page that loads in 400 ms on your laptop can take six seconds on a mid-range phone in another country, and only your visitors ever see the second one.
-RUM must be integrated with an APM backend such as KloudMate for the RUM data to be used effectively. This integration is essential as the APM backend, KloudMate in this case, provides the necessary infrastructure and tools to aggregate, process, and analyze the RUM data.
+## Real user monitoring with KloudMate
-### How Does RUM Work?
+RUM data is worth little until something collects, stores, and queries it. An SDK in your application sends its measurements to KloudMate, and the [RUM interface](./rum-interface/) turns them into pages you can read, filter, and share. Because that data sits alongside your logs, metrics, and backend traces, you can go from a slow request in a browser session to the backend service that made it slow.
+### How does RUM work?
-Rum works by embedding small JavaScript snippets into web pages while they are in use. These snippets record and send performance metrics back to a monitoring platform where the data can be queried and visualized. The collected data is then analyzed using the monitoring platform's monitoring capabilities to understand various performance aspects.
-
-The following diagram illustrates the step-by-step process of how Real User Monitoring (RUM) works:
+You add a small SDK to your website or mobile app. From then on it watches page loads, route changes, network calls, clicks, and errors as they happen, and sends what it records to KloudMate. Your application code doesn't change beyond the one call that initializes the SDK.

-**1. User Interaction:** A website user interacts with the RUM-instrumented website or application, initiating various actions like accessing the website, navigating pages, clicking buttons, or submitting forms.
+**1. User Interaction:** Someone loads a page, moves between routes, clicks a button, or submits a form.
-**2. RUM Instrumentation:** The front end of the web page that is being accessed is instrumented with RUM scripts, which include embedded JavaScript snippets. As the user continues interacting with the page, the RUM SDK will collect data on various performance metrics, such as page load times, resource loading times, and user interactions.
+**2. RUM Instrumentation:** The SDK measures what each of those actions cost: page load and resource timing, web vitals, request duration, and any error thrown along the way.
-**3. Data Collection:** The collected data is sent back to KloudMate. KloudMate then aggregates the data and allows for real-time monitoring and analysis.
+**3. Data Collection:** The SDK sends those measurements to KloudMate, where they're stored and indexed as they arrive.
-**4. Analysis & Visualization:** KloudMate creates a default dashboard dedicated to visualizing the collected RUM data. This dashboard is pre-populated with all the critical information such as Avg. LCP, CLS, INP, etc., and can be accessed within KloudMate's RUM interface.
+**4. Analysis & Visualization:** The [RUM interface](./rum-interface/) opens on **Overview**, which reports LCP, INP, and CLS at p75 with page views and error rate, and links through to the sessions behind each number.
-### Key Concepts in RUM
+### Key concepts in RUM
| **Term** | **Description** |
-| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Trace | A trace is a detailed sequential record of operations that occur within an application and its services during the execution of a particular user request or transaction. |
-| Span | A span represents a single operation within a trace. Each span includes metadata such as the start and end time, operation name, and other relevant details. |
-| Session | A session is a period during which a user interacts with an application or a website. It starts when the user first accesses the web or application page and ends after a predetermined period of inactivity or when the user closes the application. |
-| User Journey | The paths that users take through a website, including the sequence of pages and interactions. |
-| Session ID | A session ID is a unique identifier assigned to each user session. The session ID helps in correlating various user activities and interactions in a session, such as page views, clicks, form submissions, and other events, into a single coherent session. |
-| Session Replay | A recording of what the page looked like during a session, played back frame by frame. Recording is off until it is turned on, and only a sampled share of sessions carry one. See [Record sessions for replay](./instrumentation-guide/web/#record-sessions-for-replay). |
-| Frustration signal | A click the browser SDK judged unproductive at the moment it happened: a **rage click** (repeated clicks on the same element), a **dead click** (a click nothing responded to), or an **error click** (a click followed by a JavaScript error). See [Frustration signals](./session-detail/#frustration-signals). |
-| Network Response Time | The time it takes for a network request made by the user's browser to receive a response from the server. |
-| Web Vitals | Metrics that quantify how a page felt to load and respond: **Largest Contentful Paint (LCP)**, how long the largest visible element took to render; **Cumulative Layout Shift (CLS)**, how much the layout moved unexpectedly; **Interaction to Next Paint (INP)**, the delay between an interaction and the next paint; plus **Time to First Byte (TTFB)** and **First Contentful Paint (FCP)** on an individual session. INP replaced First Input Delay as a Core Web Vital in March 2024, and the RUM views report it instead. |
+| --- | --- |
+| Trace | The record of one request as it moves through your application and the services behind it. |
+| Span | A single operation inside a trace, with its own start time, duration, name, and attributes. |
+| Session | One visit. It starts when someone first loads a page and ends after a stretch of inactivity or when they close the app. |
+| User Journey | The path someone took through the app: which pages they saw, in what order, and what they did on each. |
+| Session ID | The identifier that ties everything in one visit together, so page views, clicks, form submissions, and errors read as a single session rather than as unrelated events. |
+| Session Replay | A recording of what the page looked like during a session, played back frame by frame. Recording is off until it is turned on, and only a sampled share of sessions carry one. See [Record sessions for replay](./instrumentation-guide/web/#record-sessions-for-replay). |
+| Frustration signal | A click the browser SDK judged unproductive at the moment it happened: a **rage click** (repeated clicks on the same element), a **dead click** (a click nothing responded to), or an **error click** (a click followed by a JavaScript error). See [Frustration signals](./session-detail/#frustration-signals). |
+| Network Response Time | How long a request from the visitor's browser took to come back from the server. |
+| Web Vitals | Metrics that quantify how a page felt to load and respond: **Largest Contentful Paint (LCP)**, how long the largest visible element took to render; **Cumulative Layout Shift (CLS)**, how much the layout moved unexpectedly; **Interaction to Next Paint (INP)**, the delay between an interaction and the next paint; plus **Time to First Byte (TTFB)** and **First Contentful Paint (FCP)** on an individual session. INP replaced First Input Delay as a Core Web Vital in March 2024, and the RUM views report it instead. |
***
diff --git a/src/content/docs/docs/rum/instrumentation-guide/android.mdx b/src/content/docs/docs/rum/instrumentation-guide/android.mdx
index 6ccaa60d..f7f69499 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/android.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/android.mdx
@@ -29,13 +29,13 @@ KloudMateRum.init(
Get your key and the rest of the values from **Add application**. See [Add an application](../#add-an-application).
-`appId` defaults to the package name, so there is no need to pass it.
+`appId` defaults to the package name, so you don't need to pass it.
Screen views, app start, slow and frozen frames, device vitals, ANRs, crashes, and tap spans are captured from this call on.
## Name a Compose screen
-Screens come from Activity and Fragment lifecycle, which Compose navigation happens inside, so it is invisible to those callbacks. Wire it up explicitly:
+Screen names come from Activity and Fragment lifecycle callbacks. Compose navigation happens inside a single Activity, so those callbacks never fire for it. Report the screen yourself:
```kotlin
navController.addOnDestinationChangedListener { _, destination, _ ->
@@ -53,7 +53,7 @@ val client = OkHttpClient.Builder()
.build()
```
-Without it the app reports no HTTP spans at all, and nothing says why.
+Without the interceptor, the app reports no HTTP spans at all.
## Identify the user
@@ -63,7 +63,7 @@ Set the user after sign-in:
KloudMateRum.setUser(id = "u123", email = "alice@example.com")
```
-Call `endSession()` on sign-out. Nothing else detects it, and a mobile session runs up to four hours, never times out while the app is in the foreground, and survives the process being killed. Without it, a sign-out followed by a different sign-in keeps one session ID across both people.
+Call `endSession()` on sign-out. Nothing else detects a sign-out, and a mobile session runs for up to four hours, never times out while the app is in the foreground, and survives the process being killed. Without the call, a sign-out followed by a different sign-in keeps one session ID across both people.
```kotlin
fun onLogout() {
@@ -72,7 +72,7 @@ fun onLogout() {
}
```
-Everything `setUser()` set is cleared, values you set with `setGlobalAttributes()` are kept, and the next session starts at the next instrumented event rather than inside the call.
+Everything `setUser()` set is cleared, values set with `setGlobalAttributes()` are kept, and the next session starts at the next instrumented event.
***
diff --git a/src/content/docs/docs/rum/instrumentation-guide/custom-events.mdx b/src/content/docs/docs/rum/instrumentation-guide/custom-events.mdx
index 54bb3f42..720b7373 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/custom-events.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/custom-events.mdx
@@ -7,9 +7,9 @@ sidebar:
import { Tabs, TabItem } from '@astrojs/starlight/components';
-`addEvent()` records something the SDK cannot infer on its own: a signup, a checkout, a plan upgrade. Everything else RUM collects describes how the app behaved, and these describe what the visitor accomplished.
+`addEvent()` records something the SDK can't work out on its own: a signup, a checkout, a plan upgrade. The rest of RUM tells you how the app behaved. Custom events tell you what the visitor got done.
-A custom event carries the session, route, and user context of the moment it fired, which is what lets it work as a [funnel](../../rum-interface/#funnels) step and show up on the session's own timeline. Events are counted per session and per user, rolled up hourly, and kept for 400 days, so they answer questions about last quarter long after the raw sessions have aged out.
+Every event carries the session, route, and user it fired in, so it can be used as a [funnel](../../rum-interface/#funnels) step and appears on that session's timeline. Events are counted per session and per user, rolled up hourly, and kept for 400 days, so you can still compare this quarter against last quarter after the raw sessions have aged out.
@@ -51,21 +51,35 @@ A custom event carries the session, route, and user context of the moment it fir
-Custom events matter more on mobile than on web. A funnel built only from screen names cannot express "added to cart", and there are no URLs to fall back on.
+## Attributes become breakdowns and filters
+
+Every attribute you send with an event can be used as both a group-by key and a filter. For example, `currency` and `items` from the snippet above are immediately available in the [Events](../../rum-interface/#events) tab for [grouping](../../rum-interface/#group-by-a-payload-attribute) and [filtering](../../rum-interface/#filter-on-a-payload-attribute). You don't need to define them ahead of time.
+
+Make sure numeric values are sent as numbers. If you send `4999` as a string, it will be stored as text, so a filter such as `items > 2` won't work as expected.
+
+It's also a good idea to keep attribute names consistent, just like event names. If the attribute key itself changes between events, you end up creating a new key for every event, which makes it impossible to use that attribute effectively for grouping or filtering.
## Keep the event name constant
-Name the event for the thing that happened, never for the thing it happened to. `checkout_completed` is a name; `checkout_user_8f21c` is thousands of them.
+The event name should describe **what happened**, not the specific object or user it happened to.
+
+For example, `checkout_completed` is a good event name. `checkout_user_8f21c` is not: it creates a separate event name for every user.
-A workspace can hold only so many distinct event names per day. Past that, the rest are folded into one bucket that the [Events](../../rum-interface/#events) tab labels **Over the daily name limit**, so interpolated names do not give you fine-grained rows, they collapse into a single useless one. Put the varying part in the attributes.
+Each workspace has a daily limit on the number of distinct event names it can store. Once that limit is reached, additional names are grouped into a single bucket that appears as **Over the daily name limit** in the [Events](../../rum-interface/#events) tab. Dynamic event names therefore don't give you more detailed data; they just end up in the same bucket.
+
+If something about the event needs to vary, put that information in an attribute instead.
## `value` is reserved
-An attribute named exactly `value` is the one the platform sums and takes percentiles over. That is what makes revenue-style questions work without a schema per customer: pick **value** as the measure on the [Events](../../rum-interface/#events) tab and sum it, or read its p90.
+The `value` attribute has a special meaning. It's the attribute the platform uses for calculations such as sums and percentiles, which makes it useful for things like revenue or transaction values without requiring a separate schema for every customer.
+
+For example, you can select **Event value** as the measure in the [Events](../../rum-interface/#events) tab and calculate a sum or view its p90.
+
+You can also select an individual event to see a [distribution](../../rum-interface/#what-a-distribution-needs) of its values.
-Pass it as a number. Types survive the wire, and a stringified `4999` is not something the query layer can aggregate.
+As with other numeric attributes, `value` must be sent as a number. A string such as `4999` is stored as text and can't be aggregated by the query layer.
-Both rules apply on every platform, and events from all of them land in the same place.
+These rules apply across all supported platforms. Events from web, mobile, and other platforms are all stored and queried in the same way.
***
diff --git a/src/content/docs/docs/rum/instrumentation-guide/index.mdx b/src/content/docs/docs/rum/instrumentation-guide/index.mdx
index 19d60794..795ba101 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/index.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/index.mdx
@@ -6,7 +6,7 @@ sidebar:
order: 1
---
-KloudMate has a RUM SDK for the browser, for native Android and iOS, and for React Native. Every one of them sends to the same collector and lands in the same [RUM interface](../rum-interface/), so a workspace can hold a website and its mobile apps side by side.
+KloudMate has a RUM SDK for the browser, for native Android and iOS, and for React Native. They all send to the same collector and appear in the same [RUM interface](../rum-interface/), so one workspace can hold a website and its mobile apps side by side.
## Supported platforms
@@ -25,31 +25,29 @@ Open **RUM** and click **Add application**.

-The last step generates a snippet carrying your key and the current SDK version. Copy that one rather than the examples in these pages, which use placeholders.
-
-Two things are worth deciding before you click through.
+The last step generates a snippet with your key and the current SDK version already in it. Copy that snippet rather than the examples on these pages, which use placeholders.
### The name is permanent
-Use only letters, digits, and `.`, `_`, or `-`. Anything else gets rewritten on the way in, and the rewritten name then disagrees with the original, which surfaces as panels that go blank.
+Use only letters, digits, and `.`, `_`, or `-`. Any other character is rewritten during ingest, and the rewritten name no longer matches the one you configured, which shows up as panels with no data in them.
:::caution
-There is no rename. A RUM application comes into existence when data arrives under a name, so a name you regret cannot be corrected later. Mobile apps set `applicationName` in source and never see this validation, so the same rule applies there by hand.
+There is no rename. An application is created the moment data arrives under a name, so you can't correct a name later. Mobile apps set `applicationName` in source and never go through this validation, so check the name yourself before you ship.
:::
Set **Version** here too, or the [Releases](../rum-interface/#releases) tab has nothing to compare.
### Sampling
-**Telemetry sampling** is the share of sessions that send data at all. An unsampled session registers no listeners and makes no requests, so this is the lever on ingest volume. **Replay sampling** then decides which of those sessions are recorded.
+**Telemetry sampling** is the share of sessions that send data at all. A session that isn't sampled registers no listeners and makes no requests, so this is what controls your ingest volume. **Replay sampling** then decides which of those sessions are also recorded.
The two multiply: 25% telemetry and 10% replay records one session in forty.
## The ingest key
-RUM sends with a **Frontend** ingest key, which ships in client code where anyone can read it. That is why a frontend key carries a list of **allowed hosts**: data sent from any other origin is rejected. If an application is installed correctly and still reports nothing, check that list first.
+RUM sends with a **Frontend** ingest key, which ships in client code where anyone can read it. Because of that, a frontend key carries a list of **allowed hosts**, and data sent from any other origin is rejected. If an application is installed correctly and still reports nothing, check that list first.
-Keys live under **Settings → Ingest Keys** in the workspace, where you can also add one, edit its allowed hosts, or delete a key that has leaked. A key is shown once at creation and cannot be retrieved later, so rotating means creating a replacement, swapping it into your app, and then deleting the old one. See [API Keys](../../platform/settings/api-keys/).
+Keys live under **Settings → Ingest Keys**, where you can add one, edit its allowed hosts, or delete a key that has leaked. A key is shown once when it's created and can't be retrieved later, so rotating one means creating a replacement, swapping it into your app, and then deleting the old key. See [API Keys](../../platform/settings/api-keys/).
## Next
diff --git a/src/content/docs/docs/rum/instrumentation-guide/ios.mdx b/src/content/docs/docs/rum/instrumentation-guide/ios.mdx
index 97ecd081..344822f5 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/ios.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/ios.mdx
@@ -26,13 +26,13 @@ KloudMateRum.shared.doInit(config: config)
Get your key and the rest of the values from **Add application**. See [Add an application](../#add-an-application).
-`KloudMateRumConfig` takes either those three arguments or all of them, with nothing in between, so pass the first three and assign the rest. `appId` defaults to the bundle ID.
+`KloudMateRumConfig` accepts either those three arguments or the full set, with nothing in between, so pass the first three and assign the rest as properties. `appId` defaults to the bundle ID.
-Screen views, app start, crashes, app hangs, and tap spans are captured from this call on. A screen the SDK cannot see, such as a purely programmatic transition, needs `KloudMateRum.shared.setCurrentScreen(name: "Checkout")`.
+Screen views, app start, crashes, app hangs, and tap spans are captured from this call on. For a screen the SDK can't detect, such as a purely programmatic transition, name it yourself with `KloudMateRum.shared.setCurrentScreen(name: "Checkout")`.
## Capture HTTP requests
-`URLSession.shared` is instrumented automatically. A session your app builds from its own configuration is not: it consults its own `protocolClasses`, which never contains a globally registered `NSURLProtocol`. Opt each one in:
+`URLSession.shared` is instrumented automatically. A session your app builds from its own configuration is not, because it only consults the `protocolClasses` on that configuration. Opt each one in:
```swift
let configuration = URLSessionConfiguration.default
@@ -50,12 +50,16 @@ Set the user after sign-in:
KloudMateRum.shared.setUser(id: "u123", email: "alice@example.com", extra: [:])
```
-Call `KloudMateRum.shared.endSession()` on sign-out. Nothing else detects it, and a mobile session runs up to four hours, never times out while the app is in the foreground, and survives the process being killed.
+Call `KloudMateRum.shared.endSession()` on sign-out. Nothing else detects a sign-out, and a mobile session runs for up to four hours, never times out while the app is in the foreground, and survives the process being killed.
-## What reads differently from Android
+## How iOS differs from Android
:::note
-App-start time on iOS is measured from `KloudMateRum.init` rather than from process start, so it excludes everything before that and is not comparable to the Android figure. An app-hang stack is sampled just before the main thread blocks, because no supported API reads a blocked thread's stack, so it points near the cause rather than exactly at it. Screen tracking samples the view-controller hierarchy about once a second, so a screen that appears and disappears faster than that is missed.
+
+- App-start time is measured from `KloudMateRum.init`, not from process start, so it leaves out everything before that and isn't comparable to the Android figure.
+- An app-hang stack is sampled just before the main thread blocks, so it points near the cause rather than exactly at it.
+- Screen tracking samples the view-controller hierarchy about once a second, so a screen that appears and disappears faster than that is missed.
+
:::
***
diff --git a/src/content/docs/docs/rum/instrumentation-guide/react-native.mdx b/src/content/docs/docs/rum/instrumentation-guide/react-native.mdx
index d75661bb..b55c74d7 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/react-native.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/react-native.mdx
@@ -31,11 +31,11 @@ init({
Get your key and the rest of the values from **Add application**. See [Add an application](../#add-an-application).
-Screens, taps, app start, crashes, and uncaught JS errors are captured from this call on. `captureJsErrors` turns the JS error handler off if you install your own. Name a screen with `setCurrentScreen()`, send a custom event with [`addEvent()`](../custom-events/), and report a handled error with `reportError()`.
+Screens, taps, app start, crashes, and uncaught JS errors are captured from this call on. Set `captureJsErrors` to `false` if you install your own JS error handler. Name a screen with `setCurrentScreen()`, send a custom event with [`addEvent()`](../custom-events/), and report a handled error with `reportError()`.
## Expo
-The SDK ships native code, so it does not run in Expo Go. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/).
+The SDK ships native code, so it doesn't run in Expo Go. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/).
Add the config plugin to your app config:
@@ -54,11 +54,11 @@ npx expo prebuild
npx expo run:android # or: npx expo run:ios
```
-The plugin registers the native module on Android; iOS needs no extra setup. It works with the New Architecture, and everything else on this page applies unchanged.
+The plugin registers the native module on Android, and iOS needs no extra setup. It works with the New Architecture, and everything else on this page applies unchanged.
## Capture HTTP requests
-React Native sends `fetch` and `XMLHttpRequest` through OkHttp on Android, so one native line in `MainApplication.kt` covers both with no JavaScript involved:
+On Android, React Native sends both `fetch` and `XMLHttpRequest` through OkHttp, so one change in `MainApplication.kt` covers both:
```kotlin
OkHttpClientProvider.setOkHttpClientFactory {
@@ -69,12 +69,12 @@ OkHttpClientProvider.setOkHttpClientFactory {
```
:::caution
-React Native on iOS captures no HTTP requests. Requests go through the framework's own `NSURLSession` inside `RCTHTTPRequestHandler`, which offers no injection point, so the `instrument(configuration:)` call a [native iOS](../ios/#capture-http-requests) app makes has nowhere to go. Screens, taps, app start, crashes, and JS errors are captured normally, and the Network tab on those sessions is empty.
+React Native on iOS captures no HTTP requests. Requests go through the framework's own `NSURLSession` inside `RCTHTTPRequestHandler`, which has no injection point, so the `instrument(configuration:)` call a [native iOS](../ios/#capture-http-requests) app makes has nothing to attach to. Screens, taps, app start, crashes, and JS errors are captured normally, and the Network tab on those sessions is empty.
:::
## Identify the user
-Set the user after sign-in with `setUser()`, and call `endSession()` on sign-out. A mobile session runs up to four hours and survives the process being killed, so without it a sign-out followed by a different sign-in keeps one session ID across both people.
+Set the user after sign-in with `setUser()`, and call `endSession()` on sign-out. A mobile session runs for up to four hours and survives the process being killed, so without that call a sign-out followed by a different sign-in keeps one session ID across both people.
***
diff --git a/src/content/docs/docs/rum/instrumentation-guide/sdk-reference.mdx b/src/content/docs/docs/rum/instrumentation-guide/sdk-reference.mdx
index 1614c519..89e1239a 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/sdk-reference.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/sdk-reference.mdx
@@ -20,7 +20,7 @@ Initializes the browser RUM SDK. `options` accepts the following properties:
| `sampleRate` | number | Share of sessions that send telemetry at all, from 0 to 1. Defaults to 1. |
| `sessionRecorder` | object
\{ enabled: boolean, sampleRate: number, options: [RRWebRecordConfig](https://github.com/rrweb-io/rrweb/blob/master/guide.md#options) } | Configure session recording. Set `enabled` to `true` to record. Off by default. See [Record sessions for replay](../web/#record-sessions-for-replay). |
| `events` | object | Which automatic events to capture, including console output. See [Choose what the SDK captures](../web/#choose-what-the-sdk-captures). |
-| `ignoreUrls` | `(string \| RegExp)[]` | URLs to skip instrumenting entirely. No span, and no trace header. |
+| `ignoreUrls` | `(string \| RegExp)[]` | URLs to skip instrumenting entirely. No span is created and no trace header is added. |
| `propagateTraceHeaderCorsUrls` | `(string \| RegExp)[]` | Cross-origin URLs allowed to receive the `traceparent` header. See [Correlate RUM traces with OpenTelemetry backends](../../correlate-rum-traces-with-opentelemetry-backends/). |
| `excludeBots` | boolean \| function | Skip automated and crawler traffic so it never starts a session. Defaults to `true`. |
| `captureConsoleErrors` | boolean | Report `console.error(...)` as a real error, not only as a console line. Defaults to `true`. |
@@ -29,15 +29,19 @@ Initializes the browser RUM SDK. `options` accepts the following properties:
| Method | Description |
| --- | --- |
-| `setGlobalAttributes(attributes)` | Add key-value pairs to every subsequent span. See [Identify the user](../web/#identify-the-user). |
+| `setUser(user)` | Set the signed-in user. `id` and `email` map to the `userId` and `userEmail` attributes; any other field is added as an attribute on every span. See [Identify the user](../web/#identify-the-user). |
+| `endSession()` | End the session in every tab on sign-out, clearing everything `setUser()` set. |
+| `setGlobalAttributes(attributes)` | Add key-value pairs to every subsequent span. Set a key to `null` to remove it. |
| `getGlobalAttributes()` | Return the current global attributes. |
+| `getSessionId()` | Return the session ID, or `null` between `endSession()` and the start of the next session. |
| `addEvent(name, attributes)` | Record a custom event. See [Send custom events](../custom-events/). |
+| `recordException(error, attributes)` | Report a handled error that automatic capture never sees. Pass an `Error` to keep the stack trace, or a string. |
The mobile SDKs share one `KloudMateRumConfig` across Android, iOS, and React Native. TypeScript definitions for the full React Native option list ship with the package.
## Data attributes
-Some of the important attributes the RUM SDKs collect:
+The attributes you're most likely to group or filter on:
| Attribute | Description |
| --- | --- |
diff --git a/src/content/docs/docs/rum/instrumentation-guide/web.mdx b/src/content/docs/docs/rum/instrumentation-guide/web.mdx
index 233de0b9..c001cd22 100644
--- a/src/content/docs/docs/rum/instrumentation-guide/web.mdx
+++ b/src/content/docs/docs/rum/instrumentation-guide/web.mdx
@@ -5,7 +5,7 @@ sidebar:
order: 2
---
-Paste the generated script into the `` of your site. `init` runs from the tag's `onload`, so the bundle is fully executed before it is called:
+Paste the generated script into the `` of your site. `init` runs from the tag's `onload`, so the bundle has finished loading before it's called:
```html
```
-The `/v2/` path is a rolling one: it carries patches and features but never a major version, so fixes reach your users without an edit here.
+The `/v2/` path rolls forward with patches and new features, but never with a breaking major version, so fixes reach your users without you editing this tag.
-If you would rather bundle it, install the package and call `init()` as early as possible in your entry point. Instrumentation only captures what happens after it runs:
+To bundle it instead, install the package and call `init()` as early as you can in your entry point. Nothing that happens before that call is captured:
```bash
npm install @kloudmate/rum-web
@@ -43,21 +43,25 @@ Get your key and the rest of the values from **Add application**. See [Add an ap
## Identify the user
-Without an ID, every session in the list reads **Anonymous**. Set the current user's ID and email once they are known, usually right after sign-in:
+Without an ID, sessions are listed as **Anonymous**. Set the user once they're known, usually right after sign-in:
```javascript
-KloudMateRum.setGlobalAttributes({ userId: 'u_123', userEmail: 'john@example.com' })
+KloudMateRum.setUser({ id: 'u_123', email: 'john@example.com' })
```
-Clear them on sign-out, so the next session on that browser is not attributed to the person who just left:
+`id` and `email` become the `userId` and `userEmail` attributes that the session list displays and filters on. Any other field is added as an attribute on every span, so `plan: 'pro'` tags the whole session with the plan. Calling `setUser()` on every page load to restore a remembered sign-in is fine, and the current session keeps running.
+
+Call `endSession()` on sign-out. Without it, a sign-out followed by a different sign-in keeps one session across both people:
```javascript
-KloudMateRum.setGlobalAttributes({ userId: null, userEmail: null })
+KloudMateRum.endSession()
```
+Everything `setUser()` set is cleared, extra fields included. Attributes set with `setGlobalAttributes()` are kept, so use those for values that aren't tied to a person, such as an A/B variant. The next session starts at the next instrumented event.
+
## Record sessions for replay
-Session recording is off until it is turned on, and even then only a share of sessions are recorded. That share is `sessionRecorder.sampleRate`, a fraction between 0 and 1:
+Session recording is off by default, and even once it's on only a share of sessions are recorded. `sessionRecorder.sampleRate` sets that share, as a fraction between 0 and 1:
```javascript
KloudMateRum.init({
@@ -73,9 +77,9 @@ KloudMateRum.init({
});
```
-The rates compose. The top-level `sampleRate` decides whether a session sends anything at all, and `sessionRecorder.sampleRate` then decides whether a session that is already sending is also recorded. At `sampleRate: 0.25` and `sessionRecorder.sampleRate: 0.1`, one session in forty carries a replay.
+The two rates multiply. The top-level `sampleRate` decides whether a session sends anything at all, and `sessionRecorder.sampleRate` then decides whether a session that is already sending is also recorded. At `sampleRate: 0.25` and `sessionRecorder.sampleRate: 0.1`, one session in forty carries a replay.
-Sessions without a recording still show their timeline, network waterfall, console output, and errors. The Replay tab reads [No replay for this session](../../session-detail/#replay).
+Sessions without a recording still show their timeline, network waterfall, console output, and errors. Only the [Replay](../../session-detail/#replay) tab is empty.
Inputs are masked by default. Add `km-block` to an element's class list to keep it out of the recording entirely, or `km-ignore` to record the element but not what is typed into it.
@@ -100,15 +104,15 @@ KloudMateRum.init({
| `events.navigation` | `true` | Route changes in a single-page app, and navigation timing on the initial load. |
| `events.resourceTiming` | `false` | One event per sub-resource. High volume, so opt in deliberately. |
-Web vitals are captured regardless of this setting, as spans, since they drive the p75 charts on Pages. [Frustration signals](../../session-detail/#frustration-signals) are always detected too, because rage, dead, and error clicks land as attributes on the click span that already exists.
+Web vitals are captured as spans whatever you set here, because they feed the p75 charts on Pages. [Frustration signals](../../session-detail/#frustration-signals) are always detected too.
-`captureConsoleErrors` is separate and on by default: it reports `console.error(...)` as a real error rather than only as a console line. Set it to `false` if `console.error` is used for logging that should not be counted against the error rate.
+`captureConsoleErrors` is a separate option, on by default. It reports `console.error(...)` as a real error rather than only as a console line. Set it to `false` if you use `console.error` for logging that shouldn't count against your error rate.
## Keep the SDK current
-Upgrading matters most for replay on long sessions. Since `@kloudmate/rum-web` 2.2.4, the recorder writes a full DOM snapshot every 5,000 events instead of relying on the single snapshot taken at the start of the recording. Seeking then rebuilds from the nearest snapshot rather than replaying everything before the target, which is the difference between a jump into minute 20 of a session taking about a second and taking most of a minute.
+Upgrading matters most for replay on long sessions. From `@kloudmate/rum-web` 2.2.4 the recorder writes a full DOM snapshot every 5,000 events, so seeking rebuilds the page from the nearest snapshot instead of replaying everything before it. Jumping to minute 20 of a session takes about a second on a current SDK, against most of a minute on an older one.
-There is nothing to configure. Loading the current bundle is enough, and the rolling `/v2/` CDN path in the generated snippet picks up patches without any edit.
+There's nothing to configure. Loading the current bundle is enough, and the `/v2/` CDN path in the generated snippet picks up patches on its own.
***
diff --git a/src/content/docs/docs/rum/rum-interface.mdx b/src/content/docs/docs/rum/rum-interface.mdx
index 68a3d988..be5b38ea 100644
--- a/src/content/docs/docs/rum/rum-interface.mdx
+++ b/src/content/docs/docs/rum/rum-interface.mdx
@@ -6,43 +6,43 @@ sidebar:
order: 3
---
-The RUM interface is where you read what your instrumented apps sent. The tabs run in roughly the order the questions come up: whether anything is wrong, which pages and releases it is wrong on, and what a single visitor actually experienced.
+The RUM interface is where you read the data your instrumented apps send. Start on **Overview** to see whether anything is wrong, then work down to the pages, releases, and individual sessions behind it.
To send data in the first place, see the [Instrumentation Guide](../instrumentation-guide/).
## Pick an application first
-Opening **RUM** lands on the applications index, which lists every web, Android, and iOS application reporting to the workspace. Choose one to open its tabs. Each application is a `serviceName`, set as `applicationName` when the SDK initializes.
+Open **RUM** to see every web, Android, and iOS application reporting to your workspace, then select one to open its tabs. An application is identified by its `serviceName`, which you set as `applicationName` when the SDK initializes.

-Each row shows the numbers that matter for its platform: **LCP P75** and **Error Rate** on web, **Crash Rate** and **Cold Start P90** on mobile. **Add application** starts the setup flow in the [Instrumentation Guide](../instrumentation-guide/).
+Each row carries the headline numbers for its platform: **LCP P75** and **Error Rate** on web, **Crash Rate** and **Cold Start P90** on mobile. **Add application** starts the setup flow described in the [Instrumentation Guide](../instrumentation-guide/).
-The tab set is the same on every platform, but the contents follow the application. A browser app has Core Web Vitals and no crash rate; a native app has crashes, app hangs, and app-start timings, and **Pages** is titled **Screens**.
+Every platform gets the same tabs, but what's inside them depends on the application. A browser app has Core Web Vitals and no crash rate. A native app has crashes, app hangs, and app-start timings, and **Pages** is titled **Screens**.
-| Tab | Answers |
+| Tab | What it shows |
| --- | --- |
-| [Overview](#overview) | Is anything wrong right now |
-| [Sessions](#sessions) | What did one visitor experience |
+| [Overview](#overview) | Whether anything needs attention right now |
+| [Sessions](#sessions) | What one visitor experienced |
| [Pages](#pages) | Which pages are slow or failing |
-| [Performance](#performance) | What is the app waiting on |
-| [Errors](#errors) | What is breaking, and how often |
-| [Releases](#releases) | Did the last release make things worse |
-| [Journeys](#journeys) | Where do people go, and where do they give up |
-| [Events](#events) | What are the custom events doing |
+| [Performance](#performance) | What the app spends its time waiting on |
+| [Errors](#errors) | What's breaking, and how often |
+| [Releases](#releases) | Whether the last release made things worse |
+| [Journeys](#journeys) | Where people go, and where they give up |
+| [Events](#events) | What your custom events are doing |
## Overview
-Start on **Overview** to find out whether anything needs attention right now. It runs top to bottom from the findings worth acting on, through the headline numbers and what is happening over time, to where it is happening and to whom.
+Start on **Overview** to find out whether anything needs attention right now.

-- **Needs attention** leads with the findings worth acting on, each stating its evidence, such as `70 errors in this window, 3.7x the norm of 19 for the previous 7 days`, and the action that follows from it.
-- **Core Web Vitals** gives LCP, INP, and CLS at p75 with the boundary each is judged against, plus page views and error rate. Web applications only.
-- **Release health** gives the crash-free rate, on applications that can crash.
+- **Needs attention** lists what changed, with the evidence behind each finding: `70 errors in this window, 3.7x the norm of 19 for the previous 7 days`.
+- **Core Web Vitals** reports LCP, INP, and CLS at p75 with the boundary each is judged against, plus page views and error rate. Web applications only.
+- **Release health** reports the crash-free rate, on applications that can crash.
- **Volume and failures** plots sessions, page views, and errors over the range.
-- **Worst pages by impact**, **Recent errors**, and **Delivery** each link through to the tab that owns them.
-- **Audience** breaks sessions down by browser or device, version, **Screen resolution**, and country. Counts are distinct sessions, so one long session firing a thousand spans stays one visit.
+- **Worst pages by impact**, **Recent errors**, and **Delivery** each link through to the tab they come from.
+- **Audience** breaks sessions down by browser or device, version, **Screen resolution**, and country. Counts are distinct sessions, so a long session that fired a thousand spans still counts once.

@@ -64,82 +64,84 @@ INP replaced First Input Delay as a Core Web Vital in March 2024, and the RUM vi

-Two columns are worth knowing about. **Signals** badges the crashes, ANRs, and frustration clicks in a session, worst first and with counts, so a session worth opening says so from the list. See [Frustration signals](../session-detail/#frustration-signals). **Play** is disabled where there is no recording, which is the common case.
+**Signals** badges the crashes, ANRs, and frustration clicks in each session, worst first and with counts, so you can tell which sessions are worth opening without opening them. See [Frustration signals](../session-detail/#frustration-signals).
+
+**Play** is disabled on sessions with no recording, which is most of them. See [Record sessions for replay](../instrumentation-guide/web/#record-sessions-for-replay).
Click any row to open the session. See [Session Detail](../session-detail/) for what is inside.
## Pages
-**Pages** ranks the individual pages of a web app, or the screens of a mobile app, on the measures that decide whether they feel fast.
+**Pages** ranks the pages of a web app, or the screens of a mobile app, by how fast they load and how often they fail.

-Each row gives a page's views, errors, and its load time, LCP, INP, and CLS at p75, banded good, needs-improvement, or poor. Sort by a column to bring the worst to the top, then open a page for its own trend and its errors.
+Each row carries a page's views and errors, plus its load time, LCP, INP, and CLS at p75, banded good, needs-improvement, or poor. Sort by any column to bring the worst to the top, then open a page to see its own trend and the errors on it.
-**Group similar pages** collapses parameterized URLs, so `/orders/1041` and `/orders/1042` read as one `/product/{param}` row rather than as thousands. **Manage groups** turns a local tweak into a workspace rule. Screens are class names rather than paths, so this is web only.
+**Group similar pages** collapses parameterized URLs, so `/orders/1041` and `/orders/1042` become one `/orders/{param}` row instead of thousands. Use **Manage groups** to save a grouping rule for the whole workspace. Mobile screens are class names rather than paths, so grouping is web only.
-Page views that arrived without a page name collect in an **Unattributed** row below the table.
+Page views that arrived without a page name are collected into an **Unattributed** row below the table.
## Performance
-**Performance** covers what the application waits on, and every panel on it is a way into the sessions behind the number.
+**Performance** covers what the application spends its time waiting on. Every panel links through to the sessions behind the number.
-On web it opens with **Core Web Vitals**, the same measures Overview reports, at Google's 75th percentile. **TTFB P75** joins them here and names the sample count behind it.
+On web it opens with **Core Web Vitals**, the same measures Overview reports, at Google's 75th percentile. **TTFB P75** appears here too, with the number of samples behind it.

-The distribution below the cards splits one measure into buckets banded by Google's boundaries. A p75 on its own cannot tell a tail problem from an everyone problem, and this can; selecting a bar opens the sessions inside that bucket.
+The distribution below the cards splits one measure into buckets, banded by Google's boundaries. A p75 on its own can't tell you whether a handful of visitors had a terrible experience or everyone had a mediocre one. The distribution can, and selecting a bar opens the sessions inside that bucket.
-- **Slowest pages by impact** ranks pages by how much load time each adds in total, so a busy page that is slightly slow outranks a rare one that is much slower. That is a different question from [Pages](#pages), which ranks by value.
-- **Who is slow** splits a measure by browser, device, OS, or country against the application's own percentile.
-- **Long tasks** measures blocking time from the 50 ms threshold rather than summing raw durations, so a page full of 55 ms hitches does not outrank one that froze.
+- **Slowest pages by impact** ranks pages by the total load time each one contributes, so a busy page that is slightly slow outranks a rarely visited page that is much slower. [Pages](#pages) ranks by the measurement itself, which is a different question.
+- **Who is slow** breaks a measure down by browser, device, OS, or country, compared against the application's own percentile.
+- **Long tasks** counts only the time past the 50 ms threshold rather than the full duration of each task, so a page with many 55 ms tasks doesn't outrank a page that blocked for two seconds.
**Network** covers the application's own `fetch` and XHR calls.

-The endpoints table puts latency and failure rate on one row, grouped without the query string so one route is one row. Sites that send browser resource timing also get a **Resources** section. Panels an application sends nothing for are left out rather than drawn empty.
+The endpoints table puts latency and failure rate on the same row, grouped without the query string so that one route is one row. A **Resources** section appears as well if the application sends browser resource timing. Panels with no data behind them aren't drawn at all.
-On Android and iOS the tab reads **App start**, **Screen loads**, **Who is slow**, **Rendering**, and the same **Network** section. Cold start and **TTFD** are separate measures there: cold start is the launch, time to fully drawn runs until the first screen is usable, and it is far longer.
+On Android and iOS the tab has **App start**, **Screen loads**, **Who is slow**, **Rendering**, and the same **Network** section. Cold start and **TTFD** measure different things. Cold start covers the launch itself, while time to fully drawn runs until the first screen is usable, so it's the larger of the two.
:::note
-This tab was called **Resources** on web and **Performance** on mobile. The two merged, and `/rum/resources` now redirects here, so existing bookmarks keep working.
+**Performance** covers web and mobile alike, and the older `/rum/resources` URL redirects here, so existing bookmarks keep working.
:::
## Errors
-**Errors** lists every error, crash, and app hang the SDKs reported, with a facet rail for narrowing and an error-volume chart above the list.
+**Errors** lists every error, crash, and app hang your SDKs reported, with a facet rail for narrowing the list and a volume chart above it.

-**All errors**, **Crashes**, and **App Hangs** split the list by kind, with **ANRs** in place of App Hangs on Android. **Grouped by issue** collapses identical errors into one row with its counts and first and last seen; switch to **Raw events** when the question is about one specific failure rather than a pattern.
+**All errors**, **Crashes**, and **App Hangs** split the list by kind, with **ANRs** in place of App Hangs on Android. **Grouped by issue** collapses identical errors into a single row with its count and its first and last occurrence. Switch to **Raw events** when you're chasing one specific failure rather than a pattern.
-Opening an error shows its stack, its attributes, and the sessions it happened in, so the error and the session that produced it are one click apart.
+Opening an error shows its stack trace, its attributes, and the sessions it happened in.
## Releases
-**Releases** answers what the version that just shipped changed. It opens on the current release against the one it replaced, and **Compare** swaps in any other pair.
+**Releases** shows what changed when the current version shipped. It opens on that version against the one it replaced, and **Compare** swaps in any other pair.

-- **What this release changed** states each measure as before, after, and the change: `0.18 to 1.25` errors per session, `+595%`.
-- **Who is affected** gives the share of sessions that hit an error by browser, OS version, device, or country. It reports how likely a session on that browser is to fail, not how many people use it.
-- **Errors that got worse** ranks regressions by per-session rate rather than by count, since a release part way through its rollout has lower counts for everything. **New in \** catches the signatures that did not exist before.
+- **What this release changed** gives each measure as before, after, and the change: `0.18 to 1.25` errors per session, `+595%`.
+- **Who is affected** breaks the share of sessions that hit an error down by browser, OS version, device, or country. It measures how likely a session on that browser is to fail, which is not the same as how many people use it.
+- **Errors that got worse** ranks regressions by per-session rate rather than by raw count, because a release partway through its rollout has lower counts for everything. **New in \** picks out the error signatures that didn't exist before.
- **Adoption**, **Sessions by version**, and **Version history** cover the rollout itself.
-Every number on the tab links into the rows behind it, scoped to that version.
+Every number on the tab opens the rows behind it, scoped to that version.
:::caution
-Releases needs the SDK to send a `version`. Without it the tab reads **Version is not configured in the SDK** and has nothing to compare. See the setup snippet for your platform in the [Instrumentation Guide](../instrumentation-guide/).
+Releases needs the SDK to send a `version`. Without one there's nothing to compare. Set it in the init call for your platform, in the [Instrumentation Guide](../instrumentation-guide/).
:::
-Release data is counted hourly and kept for 180 days rather than read from raw spans, so a release stays comparable long after its sessions have aged out. That matters because you ask "is 4.2 worse than 4.1" weeks after 4.1 stopped shipping. A release too new to have enough sessions reads **Too few sessions to compare yet**.
+Release data is counted hourly and kept for 180 days. You can still compare 4.2 against 4.1 weeks later, after the raw sessions behind both have aged out. A release that hasn't collected enough sessions yet isn't compared at all.
-Releases replaced the Deployments tab, and `/rum/deployments` redirects here.
+The older `/rum/deployments` URL redirects here.
## Journeys
-**Journeys** covers where people go through the app and where they stop. Both views work on one application at a time, so set an **Application** filter in the filter bar first. A path across two sites is two unrelated graphs drawn on top of each other.
+**Journeys** shows the routes people take through the app and where they stop. Both views work on one application at a time, so set an **Application** filter first. Without it you get two unrelated graphs drawn on top of each other.
### Pathways
@@ -147,9 +149,9 @@ Pathways draws the routes visitors actually took, as a left-to-right graph. Each

-Click a page to start the graph from there, and set how many steps forward it follows and how many sessions a path needs before it is drawn. Pages are grouped the same way as the [Pages](#pages) tab.
+Click a page to redraw the graph starting from it. You can also set how many steps forward it follows, and how many sessions a path needs before it's drawn at all. Pages are grouped the same way as on the [Pages](#pages) tab.
-Clicking a node opens its **Sessions**, the share who **Left here**, and the share who **Hit an error**. A high **Left here** mid-path is where people are giving up.
+Clicking a node opens its **Sessions**, along with the share who **Left here** and the share who **Hit an error**. A high **Left here** in the middle of a path is where people are giving up.
### Funnels
@@ -157,36 +159,101 @@ Funnels measure conversion across up to five steps. A step is either a **route**

-**Conversion**, **Biggest drop**, and **Time to convert** sit above the chart, and each step gives its sessions and how many dropped off there.
+**Conversion**, **Biggest drop**, and **Time to convert** sit above the chart, and each step reports its session count and how many people dropped off there.
-**Complete within** sets how long a session has to get from the first step to the last; steps further apart than that do not count as a conversion. **Compare to previous period** reports the change in percentage points against the preceding window.
+**Complete within** sets how long a session has to get from the first step to the last. Steps further apart than that don't count as a conversion. **Compare to previous period** reports the change in percentage points against the preceding window.
Clicking a step opens the sessions that stalled there, with the errors they hit and a **Play** button on every row. Save a funnel to track its conversion over time.
:::caution
-Funnels read raw RUM data, which is kept for a limited time and varies by workspace. A range longer than that retention covers fewer sessions than the dates suggest, and the tab says so when the selected range is long enough for it to matter.
+Funnels read raw RUM data, which is kept for a limited time that varies by workspace. If you select a range longer than your retention, the funnel covers fewer sessions than the dates suggest.
:::
## Events
-**Events** covers everything sent through [`addEvent()`](../instrumentation-guide/custom-events/). It splits into separate views, because the same events answer different kinds of question:
+**Events** covers everything sent through [`addEvent()`](../instrumentation-guide/custom-events/). The same events are presented in separate views:
+
+- **Analyze** is the default. Pick a measure, an aggregation, and something to group by, then render it as a table, over time, a top list, or a distribution.
+- **Raw events** lists individual events with their payloads. Use it when you want to see exactly what one session sent.
+- **Totals** reads an hourly rollup that is kept for 400 days, so it's the only view that reaches back further than raw retention. The rollup doesn't carry payload attributes.
+
+
+
+Custom events are counted per session and per user, and can be used as [funnel](#funnels) steps. Event names past the workspace's daily limit are collected under **Over the daily name limit**, which usually means a value was interpolated into the name. See [Keep the event name constant](../instrumentation-guide/custom-events/#keep-the-event-name-constant).
+
+### Group by a payload attribute
+
+You can group by any attribute your application sends in the event payload, in both Analyze and Raw events. If you send an attribute as a number, it is grouped as a number.
+
+For example, to chart revenue by category over time, select **Event value** as the measure, **sum** as the aggregation, and group by `category`.
+
+Over time charts the 50 largest groups. If you group by an attribute with thousands of distinct values, such as a user ID, most of them won't appear on the chart.
+
+A payload key only exists in the time ranges where your application actually sent it. If you group by a key that isn't present in the selected range, the grouping falls back to event name.
-- **Analyze** is the default: pick a measure, an aggregation, and something to group by, then render it as a table, over time, a top list, or a distribution.
+### A group with no value
-
+Events that were sent without the attribute are collected into a single group. When you group by `category`, that group appears as **No category**, and it's often the largest group on the tab.
-- **Raw events** lists individual events with their payloads. Use it for "which category is added most" or "what did this session do".
-- **Totals** reads the hourly rollup, kept for 400 days. It is the only view that can answer "is signup up on last quarter", since the other two run on raw retention. The rollup carries no payload.
+
-Custom events are counted per session and per user, and can be used as [funnel](#funnels) steps. An event name over the workspace's daily limit is collected under **Over the daily name limit**, which usually means a name was interpolated. See [Keep the event name constant](../instrumentation-guide/custom-events/#keep-the-event-name-constant).
+This group doesn't tell you anything about your categories. It tells you how many events were sent without the attribute at all, so it's really a measure of missing instrumentation.
+
+**Exclude** removes those events from every panel on the tab, which lets you compare the real categories against each other.
+
+Numeric attributes behave differently. A missing number comes back as `0`, so you can't distinguish it from a genuine zero, and there's no separate group to exclude.
+
+### What a distribution needs
+
+**Distribution** works on **Event value** only, one event at a time. The other measures are counts, and a count has no spread. Only events that carry a [`value`](../instrumentation-guide/custom-events/#value-is-reserved) can be charted this way.
+
+
+
+:::caution
+Event value has no fixed scale, so the buckets are calculated from the values in view. Charts from two different ranges are not comparable.
+:::
## Filter the data
-The **Filter** bar narrows every tab by page, browser, version, country, and more.
+The **Filter** bar lets you narrow down the data across the different tabs using things like page, browser, version, country, and more.
+
+When you select multiple values for the same field, the filter matches **any** of those values. For example, selecting Chrome and Firefox includes data from either browser.
+
+The meaning of a filter depends on the tab you're using, and the filter bar tells you what it is matching:
+
+- **Sessions and Journeys:** **Matching whole sessions**
+- **Errors:** **Matching error events**
+- **Pages:** **Matching page views, counted hourly**
+
+This distinction matters. A page filter on Sessions finds sessions that visited `/checkout`, while the same filter on Errors finds errors that occurred on `/checkout`.
+
+Not every filter is applicable to every tab. For example, a route filter doesn't apply to Journeys because Journeys match entire sessions. If a session visited one page in a journey, the other pages in that journey are already part of the session.
+
+When a filter can't be applied to the current tab, it is shown as struck through with an explanation. It is also excluded from the query rather than being silently ignored.
+
+### Filter on a payload attribute
+
+You can filter events using any attribute included in the event payload. Numeric attributes support comparison operators such as `>`, `>=`, `<`, and `<=`. For example, `cart_size > 2` matches events where `cart_size` is greater than 2.
+
+:::note
+Events that were sent without the attribute are excluded from `>`, `>=`, `<`, `<=`, `!=`, **Not in**, and **Not contains**. A missing key isn't treated as zero, so `cart_size < 5` returns only the events that actually carry `cart_size`.
+
+Matching an attribute against an empty value does the opposite. It selects exactly the events that are missing the attribute, which is useful for finding gaps in your instrumentation.
+:::
+
+Payload attribute filters are available on some tabs only:
+
+- **Events** and **Journeys** support payload filters. In a funnel, for example, `cart_value > 100` selects sessions that emitted at least one matching event. Earlier funnel steps are still included for those sessions.
+- **Sessions** supports text attributes only. Numeric attributes such as `cart_size` cannot be used as session filters.
+- **Errors**, **Pages**, and **Releases** don't support payload filters because errors, page views, and releases don't contain event payload attributes.
+
+### Save a filter set as a segment
+
+If you use the same set of filters regularly, you can save them as a **segment** and apply them later from any tab. A segment belongs to a single application.
-What a filter matches differs by tab, and the bar says which: **Matching whole sessions** on Sessions and Journeys, **Matching error events** on Errors, **Matching page views, counted hourly** on Pages. That is the difference between "sessions that visited `/checkout`" and "errors that happened on `/checkout`".
+You can update a segment while it's applied, or save the current filters as a new segment. Only the person who created a segment can delete it.
-Not every filter applies everywhere. A route filter does nothing on Journeys, since journeys match whole sessions and a session that opened one page also opened the others on its path. A filter that cannot apply is struck through with the reason and withheld from the queries, rather than dropped silently.
+Applying a segment replaces the filters currently selected, while keeping the selected application unchanged.
:::note
A longer range slows the query down, and the raw RUM data behind Sessions, Journeys, and Raw events is kept for a limited time that varies by workspace.
diff --git a/src/content/docs/docs/rum/session-detail.mdx b/src/content/docs/docs/rum/session-detail.mdx
index fceea6a2..f025f894 100644
--- a/src/content/docs/docs/rum/session-detail.mdx
+++ b/src/content/docs/docs/rum/session-detail.mdx
@@ -6,22 +6,22 @@ sidebar:
order: 4
---
-Opening a session shows one timeline with the replay and the evidence beside it. Everything on the screen runs on the same clock, so clicking an error in the list moves the replay to the moment it happened, and playing the replay moves the list along with it.
+Opening a session gives you one timeline, with the replay on one side and the evidence on the other. Everything runs on the same clock, so clicking an error in the list moves the replay to the moment it happened, and playing the replay moves the list along with it.

## Open a session
-Click any row on the **Sessions** tab. The tab you land on is part of the URL, so a shared session link reopens where it was shared from. Sessions also open from outside RUM: a drop-off table in Journeys, an affected session on an issue, a browser span in Traces.
+Click any row on the **Sessions** tab. A session link you share reopens on the same tab you shared it from. Sessions also open from outside RUM: from a drop-off table in Journeys, from an affected session on an issue, or from a browser span in Traces.
## The session header
-Who this was, what they were on, and badges for the crashes, app hangs, errors, and rage clicks in the session. A session with no identity reads **Anonymous session**; see [Identify the user](../instrumentation-guide/web/#identify-the-user) to fill that in.
+The header names the visitor and the device and browser they were on, and badges the crashes, app hangs, errors, and rage clicks in the session. Sessions with no identity attached are labeled **Anonymous session**. See [Identify the user](../instrumentation-guide/web/#identify-the-user) to set one.
Under it, **Entry page load** gives the web vitals for the page the visitor arrived on: **TTFB**, **FCP**, **LCP**, **INP**, and **CLS**, colored good, needs-improvement, or poor. Mobile sessions get **Start type** and **Time to first frame** instead.
:::note
-These are this session's own measurements, not workspace percentiles. The p75 figures on **Overview** and **Pages** answer a different question, and the two will rarely match.
+These are this session's own measurements, not workspace percentiles. The p75 figures on **Overview** and **Pages** are calculated across every session, so the two will rarely match.
:::
## The timeline
@@ -30,55 +30,55 @@ The bar below the header runs from 0:00 to the end of the recorded activity, and
**Segments** are the views the visitor was on, in order. Clicking one scopes **What happened** to that view, and clicking it again clears the scope.
-**Markers** flag the moments worth finding: errors, crashes, app hangs, frustration clicks, and long tasks. Plain clicks and successful requests are not marked, since a mark for every span would fill the track.
+**Markers** flag the moments worth finding: errors, crashes, app hangs, frustration clicks, and long tasks. Ordinary clicks and successful requests aren't marked.
## What happened
-The list on the left walks the session in order. Each row carries the time it happened, what kind of event it was, and enough of the detail to recognize it. Clicking a row seeks the replay; playing the replay highlights the row it has reached.
+The list on the left walks the session in order. Each row gives the time, the kind of event, and enough detail to recognize it. Clicking a row seeks the replay, and playing the replay highlights the row it has reached.
-**Important**, the default, keeps what explains a session: failures, frustration clicks, long tasks over 200 ms, views, requests, and [custom events](../instrumentation-guide/custom-events/). It drops plain clicks, which are most of a real session and bury the handful of events that matter. **All** shows every span the SDK sent.
+**Important** is the default. It keeps failures, frustration clicks, long tasks over 200 ms, views, requests, and [custom events](../instrumentation-guide/custom-events/), and it leaves out ordinary clicks, which make up most of a real session. Switch to **All** to see every span the SDK sent.
-Rows carry an action where there is one to take: an error opens its issue, a request opens its backend trace, and any row expands its raw attributes. For the backend trace to reach past the browser span, see [Correlate RUM traces with OpenTelemetry backends](../correlate-rum-traces-with-opentelemetry-backends/).
+Rows carry an action where there is one to take. An error opens its issue, a request opens its backend trace, and any row expands to show its raw attributes. For the backend trace to reach past the browser span, see [Correlate RUM traces with OpenTelemetry backends](../correlate-rum-traces-with-opentelemetry-backends/).
## Frustration signals
-Rage, dead, and error clicks are events in their own right. They appear as badges on the sessions list, as markers on the timeline, and as rows in **What happened**, so a session opened from a badge can point at the click behind it.
+Rage, dead, and error clicks are events in their own right. They appear as badges on the sessions list, as markers on the timeline, and as rows in **What happened**, so you can open a session from a badge and go straight to the click behind it.

-The example above is the shape these usually take: a coupon that will not apply, the same event firing over and over, and a rage click on the button that kept refusing.
+The example above is typical: a coupon that won't apply, the same event firing over and over, and a rage click on the button that kept rejecting it.
| Signal | What it means |
| --- | --- |
-| **Rage click** | Three or more clicks on the same element inside one second. The row reports the real size of the burst, so ten angry clicks read as ten. |
+| **Rage click** | Three or more clicks on the same element within one second. The row reports the full size of the burst, so ten clicks are counted as ten. |
| **Dead click** | A click that produced no DOM change and no navigation within 500 ms. Nothing visibly happened. |
| **Error click** | A click followed by a JavaScript error within one second. |
-A frustration row is titled with the element that was clicked, so it names the button or link that misbehaved.
+A frustration row is titled with the element that was clicked, so it names the button or link involved.
-A single click gets one verdict. When more than one applies, rage outranks error, which outranks dead, so a burst on a button that also did nothing reads as rage.
+A click is only ever classified as one signal. When more than one applies, rage outranks error and error outranks dead, so a burst of clicks on a button that did nothing is reported as a rage click.
:::note
-The browser SDK decides these at the moment of the click, using what the page did next. They are not inferred from the data after the fact, so a session recorded before the SDK shipped this cannot have them backfilled. Detection is on by default and needs no configuration.
+The browser SDK classifies these at the moment of the click, from what the page did next. They aren't derived from stored data afterwards, so sessions recorded before your SDK supported them can't be backfilled. Detection is on by default and needs no configuration.
:::
## The evidence tabs
-The right-hand pane switches between six views of the same session: **Replay**, **Console**, **Network**, **Errors**, **Attributes**, and **Raw**. A tab carries a count where it has one, such as `Network (210)` or `Errors (14)`, so a pane worth opening says so before it is opened.
+The right-hand pane switches between views of the same session: **Replay**, **Console**, **Network**, **Errors**, **Attributes**, and **Raw**. Each tab carries its own count where it has one, such as `Network (210)` or `Errors (14)`.
### Replay
-The replay plays the session's recording at the position the timeline is holding, on web and on mobile.
+The replay plays the session's recording from wherever the timeline is positioned, on web and on mobile.
-Most sessions do not carry a recording. Those read **No replay for this session**, and their **Play** button in the sessions list is disabled with the same explanation. Raising `sessionRecorder.sampleRate` records a larger share. See [Record sessions for replay](../instrumentation-guide/web/#record-sessions-for-replay).
+Most sessions have no recording, and their **Play** button in the sessions list is disabled. Raise `sessionRecorder.sampleRate` to record a larger share. See [Record sessions for replay](../instrumentation-guide/web/#record-sessions-for-replay).
-Playback runs at 1x through 8x, and **Skip inactivity** jumps the quiet stretches, which is most of a long session.
+Playback runs at 1x through 8x, and **Skip inactivity** jumps the quiet stretches, which are most of a long session.
-Seeking a long session takes about a second: recordings load in parallel, and a large one loads only the segment around the playhead. How well that works depends on the SDK version. See [Keep the SDK current](../instrumentation-guide/web/#keep-the-sdk-current).
+Seeking into a long session takes about a second, and how well that works depends on your SDK version. See [Keep the SDK current](../instrumentation-guide/web/#keep-the-sdk-current).
### Console
-The browser console for this session, capturing `console.error` and `console.warn` by default. To capture more levels, or none, set the `events.console` option. See [Choose what the SDK captures](../instrumentation-guide/web/#choose-what-the-sdk-captures).
+The browser console for this session. `console.error` and `console.warn` are captured by default. Set the `events.console` option to capture more levels, or none. See [Choose what the SDK captures](../instrumentation-guide/web/#choose-what-the-sdk-captures).
### Network
@@ -86,11 +86,11 @@ Every `fetch` and `XMLHttpRequest` the session made, drawn as a waterfall, with

-The bar is what makes this a waterfall rather than a list in time order: it places each request against the whole session, so three calls stacked in the same second look like the slow page they are. Same-origin calls show the path alone and third-party calls show host and path, with the full URL on hover. Failed requests are red across the row.
+Each bar is placed against the whole session rather than sized on its own, so three calls stacked in the same second are easy to spot. Same-origin calls show the path alone and third-party calls show host and path, with the full URL on hover. Failed requests are red across the row.
### Errors
-The errors, crashes, and app hangs from this session, each with its type and message. A clean session reads **Nothing failed in this session**.
+The errors, crashes, and app hangs from this session, each with its type and message.

@@ -100,11 +100,11 @@ The environment the session ran in: service, version, browser, OS, device, locat

-**Screen** is the device's screen resolution and **Screen Density** the scale factor. Both are reported for web and mobile, and the same measurement drives the **Screen resolution** breakdown on [Overview](../rum-interface/#overview).
+**Screen** is the device's screen resolution and **Screen Density** is its scale factor. Both are reported on web and mobile, and the same measurement feeds the **Screen resolution** breakdown on [Overview](../rum-interface/#overview).
### Raw
-Every span the session produced, with its full JSON. The place to check an attribute the curated views do not show.
+Every span the session produced, with its full JSON. Use it to check an attribute the other tabs don't show.
***