Sessions and health
A session is a bounded period of one user using one app instance on one device. It is identified by session.id, a UUID v4 the SDK makes on the device. Every span and log record of the session carries it, so the server can put them on one line. A session has exactly one replay, keyed by the same id.
What every signal carries
Section titled “What every signal carries”| Attribute | Where | Meaning |
|---|---|---|
session.id |
every span and log record | the session |
session.previous_id |
the first signals of a session that replaced an expired one | links consecutive sessions |
user.id |
every span and log record | set by your app, or an anonymous id kept on the device |
ouo.user.anonymous |
every span and log record | true while user.id is the anonymous id |
device.id |
resource, and on session.start / session.end |
one browser profile (per origin) or one app install; survives rotation, login and logout |
service.name, service.version |
resource | the app, and its version: the “app version” for health percentages |
ouo.platform |
resource and every record | web or mobile |
The session attributes are on each span and record, not on the resource, because a session can rotate while the page or process lives. device.id is random. It is never derived from the IP address or a fingerprint.
The lifecycle
Section titled “The lifecycle”- no sessionSDK starts, or the app comes back after a rotationemits session.startactive
- activeno click, key, touch, scroll, navigation or requestidle
- idleany activity within 30 minsame session, no eventactive
- idle30 min without activityreason inactivityended
- activeolder than 4 hreason max_durationended
- activeanother identified user, or logoutreason user_changeended
- activethe app calls endSession()reason manualended
- activemobile app goes to the backgroundbackground
- backgroundback within 30 ssame sessionactive
- backgroundback after more than 30 sreason backgroundended
- activemobile fatal errorreason crash, on the next launchended
- activemobile process killed without a crashreason terminated, on the next launchended
- endednext activity, new session.idsession.previous_id = the old idactive
Activity is a click, key press, touch, scroll, a navigation or route change, an outgoing HTTP request, or the app coming to the foreground. Each one moves the session’s last-activity time, saved to storage at most once per second.
- Web. On init, the SDK reads the saved session from
localStorage(keyouo.session). If it was active in the last 30 minutes and is younger than 4 hours, it is resumed, with no new event: a page reload stays in the same session. Otherwise the SDK emitssession.endfor the old one, thensession.startfor a new one withsession.previous_idset. Tabs of one origin share the session throughlocalStorage. - Mobile. Every cold start begins a new session with
session.start. If the previous run left a session behind, the SDK first ends it withcrashwhen it left a crash marker, andterminatedotherwise.
ouo.session.end_reason |
When | Platforms |
|---|---|---|
inactivity |
no activity for 30 minutes | web, mobile |
max_duration |
the session is older than 4 hours | web, mobile |
user_change |
another identified user logged in, or the user logged out | web |
manual |
the app called endSession() |
web |
background |
the mobile app was in the background for more than 30 seconds | mobile |
crash |
a fatal error; emitted on the next launch from the crash marker | mobile |
terminated |
the process ended without a crash; emitted on the next launch | mobile |
Going from anonymous to identified (a login during the visit) keeps the session and only sets user.id: the anonymous id was never a person.
On the web, an uncaught error does not end the session. The user keeps using the page, and the session is counted as crashed (below).
The server does not need session.end
Section titled “The server does not need session.end”Pages close and phones lose their network, so session.end is a courtesy. rum-api treats a session as ended once its last signal is older than the inactivity timeout, and as active otherwise. An inferred end has ended_at at the last signal and the reason inactivity. A session without any session.end is the normal case, not an error.
All three timeouts are options: INACTIVITY_TIMEOUT (30 minutes), MAX_DURATION (4 hours) and the mobile BACKGROUND_TIMEOUT (30 seconds) are set in the SDK init options, and rum-api’s session.inactivity and session.max_duration must match them. See rum-api configuration.
Health
Section titled “Health”Each session has exactly one status. The worst thing that happened wins: crashed, then errored, then healthy.
| Status | Rule |
|---|---|
| crashed | at least one exception event with ouo.exception.fatal = true |
| errored | not crashed, and at least one exception with ouo.exception.fatal = false, or at least one http.request.failed |
| healthy | neither |
What counts as fatal:
- Web: an uncaught exception (
window.onerror) or an unhandled promise rejection. The page keeps running, but the session is crashed. This matches how Sentry counts browser sessions.console.errorand errors you report yourself are not fatal. - Mobile: an error that reaches the global handler with
isFatal = true, or a crash marker found on the next launch.
http.request.failed is emitted for network failures and for responses with a status from 500 to 599. A 4xx is not an error for session health.
The Grafana app’s Overview shows the share of each status for web and for mobile, split by service.version, over sessions that started in the selected time range. A bad release shows up as soon as its first sessions come in.
One session, end to end
Section titled “One session, end to end”A web session in which a checkout fails with a 500, and later an uncaught error crashes the session. Each row is one signal with the same session.id.
session.startnavigation/productsGET /api/productsweb_vitalLCP 1.8 s, goodPOST /api/checkout500http.request.failed500 POST /api/checkoutPOST /api/checkoutretry, 200exceptionfatal: uncaught TypeErrorrum-api folds these into one record, the same shape the Grafana app reads. Thirty minutes after the last signal, with no session.end, it marks the session ended at that last signal, with reason inactivity:
{ "session_id": "6f1c…", "user_id": "u-1", "device_id": "d-9a…", "platform": "web", "service_name": "web-store", "service_version": "1.4.2", "started_at": "2026-10-27T10:00:00Z", "last_signal_at": "2026-10-27T10:07:00Z", "ended_at": "2026-10-27T10:07:00Z", "end_reason": "inactivity", "state": "ended", "status": "crashed", "error_count": 0, "crash_count": 1, "failed_request_count": 1, "trace_count": 3, "event_count": 5, "has_replay": true}Run the stack sends a session with curl and shows it come out errored.