diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/.openspec.yaml b/openvidu-dashboard/openspec/changes/media-stats-availability/.openspec.yaml deleted file mode 100644 index a7587c5f0..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/.openspec.yaml +++ /dev/null @@ -1,2 +0,0 @@ -schema: spec-driven -created: 2026-11-02 diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/design.md b/openvidu-dashboard/openspec/changes/media-stats-availability/design.md deleted file mode 100644 index c5574bd4a..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/design.md +++ /dev/null @@ -1,123 +0,0 @@ -# Design - -## Context - -- Telemetry stats reach the writer from two sources: per-track media stats, built from the RTP buffers of the server's - media path, and the signal (`TR_SG…`) and data (`TR_DT…`) pseudo-tracks, built from WebSocket and data channel byte - counters. A media engine that does not move RTP through those buffers produces only the second kind. -- `projection/stats.go` adds every stat to the participant (bytes, `lastStatsAt`) and skips pseudo-tracks before the - room delta, so the room gets `lastStatsAt`, bytes and quality only from media stats. -- `nodes` documents are written by `store.Heartbeat` on every batch tick and expire 30 days after `lastSeenAt`. Node ids - are generated per process, so a restarted node writes a new document and the old one expires on its own. -- Rooms take `nodeId` from `ROOM_CREATED` (and from the first event of any kind while it is unknown); participant - sessions take it from their first event. Entities are kept for `retention.entities` (90 days by default). -- `analytics.Node{ID, Region}` is built in `openvidu/openvidu.go`; the projection options in `analytics.go`. A server - edition adjusts analytics defaults through `openviduconfig` package variables before the analytics start - (`DefaultStatsDetail`). - -## Goals / Non-Goals - -**Goals:** -- Every view that shows bytes or quality can tell "not reported" apart from "zero", for the whole entity retention. -- The CE code and specs stay engine-agnostic: the server states a capability, not an engine name. - -**Non-Goals:** -- Hiding views or features: only the values and charts that depend on media statistics change. -- Marking track or subscription documents: views reach them through their participant. - -## Decisions - -### D1. A capability flag set by the server - -`openviduconfig.MediaStats` (default `true`) says whether this server's media engine reports media statistics. A server -whose engine does not sets it to `false` before the analytics start, like `DefaultStatsDetail`. `openvidu.Start` passes -it as `analytics.Node.MediaStats`. There is no configuration key: the value is a fact of the build and the engine, not -an operator choice. - -Rejected: the engine name in the data (`rtcEngine: "…"`). It ties the CE contract to an edition's engines and must be -reinterpreted when an engine gains statistics; the capability flips by itself. - -### D2. Mark both the node and the entities - -The heartbeat `$set`s `mediaStats` (true or false) on every tick. Rooms and participant sessions created by a node with -`MediaStats: false` get `mediaStats: false`; others get no field. Views of rooms and sessions read the entity, so the -mark lives as long as the entity; analytics read the nodes, since rollups have no per-engine dimension. - -Rejected: nodes only, with the dashboard joining `nodeId`. Node documents expire 30 days after their last heartbeat and -every restart creates a new node id, so rooms between 30 and 90 days old would fall back to "0 B". -Rejected: entities only. A range or scope of the Analytics page has no entity to read the mark from. - -### D3. Only `false` is stored on entities - -Absent means "reports media statistics", which is also what every document written before the change says. No -backfill, no extra bytes on the common case, and the API omits the property unless it is `false`. - -### D4. The mark follows `nodeId` - -The mark is computed where `nodeId` is: `ROOM_CREATED` sets `nodeId` and sets `mediaStats: false` or removes it -(`$$REMOVE`); the touch of other events sets `mediaStats` only when the document has no `nodeId` yet, in the same stage, -so the first writer of the room decides until `ROOM_CREATED` arrives. Participants follow their own `nodeId` rule. The -legacy migration replays with `MediaStats: true`: the engine of legacy nodes is unknown. - -### D5. Pseudo-track stats advance the room's `lastStatsAt` - -A pseudo-track stat creates the room delta with `statsAt` and zero bytes and quality, exactly as a media stat of zero -bytes would. Every connected participant flushes signal stats every 30 s, so the room's `lastStatsAt` becomes a -liveness signal under any engine: the room stale flag, the stale hints of the overview and the reconciler's -empty-listing guard (`Untrusted live listing`) cover these rooms. With media stats nothing changes, since the room -already gets the same flush time from them. - -### D6. Analytics coverage from heartbeat spans, on the summary only - -The summary reads `nodes` documents whose span overlaps the range (`firstSeenAt <= to`, `lastSeenAt >= from`, plus -`_id` for a node scope) with a `mediaStats` projection and folds them into `none`, `partial` or nothing. The page -applies the summary's coverage to the Data up / down tile and the Bandwidth section, and exposes it to the cards of the -page's extension slots as `AnalyticsPageContext.mediaStats` (a signal of `'partial' | 'none' | undefined`), so the -series and breakdown responses do not change. - -Rejected: a coverage field on every series response (the page already holds the summary of the same range and scope). -Rejected: inferring coverage from rollup bytes at 0 (a quiet range reads the same). - -### D7. Values stay as stored - -The rooms list keeps `bytes` next to `mediaStats: false`, and the summary keeps `bytesUp` and `bytesDown`; only the UI -replaces them. API clients see the same shapes plus one optional property. - -### D8. A generator option for nodes without media statistics - -`--nodes-without-media-stats ` (default 0) marks the last `n` generated nodes: their node documents carry -`mediaStats: false`, their rooms and sessions are stamped and get no media `track_stats`, room bytes or quality, and their -rollup contributions lose bytes and quality. The other nodes get `mediaStats: true`. The keep-alive preserves the flag. - -## Data model - -- `nodes.mediaStats`: bool, written on every heartbeat. `rooms.mediaStats` and `participants.mediaStats`: bool, only - `false`, set by the projections. Contract schemas of the three collections gain the optional property; the dashboard - mirror and its typings follow. -- No index, retention or schema version change (the v2 schema has not shipped). No mapping from legacy events (D4). - -## API - -- `RoomListItem.mediaStats?: false` (`GET /api/v1/rooms`). -- `AnalyticsSummary.mediaStats?: "partial" | "none"` (`GET /api/v1/analytics/summary`). -- The coverage query runs on `nodes`, backed by the `lastSeenAt` TTL index; the collection holds one document per node - process of the last 30 days. - -## Performance budget - -One extra query per summary on a collection of tens of documents: under 5 ms, verified by an integration test with an -`explain()` plan (`IXSCAN` on `lastSeenAt`) next to the existing analytics plan tests. The summary p95 budget (150 ms at -scenario L) is unchanged. The pseudo-track room delta adds at most one room update per room and batch, and only for -rooms whose flush carries no media stat; the writer benchmark of the batch path checks it stays within its budget. - -## Risks / Trade-offs - -- [A room touched first by an egress or ingress event of another node] → it takes that node's mark until its - `ROOM_CREATED` is applied, which then sets the host's mark (D4). -- [Coverage misses nodes whose documents expired] → ranges older than 30 days may read as fully reported; the entity - marks keep rooms and sessions right. -- [A pseudo-track-only room delta on a room not written yet] → the same upsert as a media stat delta today. - -## Migration Plan - -Ships with the v2 writer and dashboard. Rollback: dashboards ignore the property and writers without it never write it. diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/proposal.md b/openvidu-dashboard/openspec/changes/media-stats-availability/proposal.md deleted file mode 100644 index 1b4e901dc..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/proposal.md +++ /dev/null @@ -1,60 +0,0 @@ -# Proposal - -## Why - -A server node whose media engine does not hand RTP statistics to the telemetry sends only the signal and data -pseudo-track stats: no `track_stats`, room and rollup bytes at 0, no quality samples and no final publish or subscribe -statistics. The dashboard cannot tell that apart from a room where nothing was sent, so it shows "↑ 0 B ↓ 0 B", -empty charts and "No stats received" as if they were measurements. The same gap leaves `rooms.lastStatsAt` unset, so -the stale cue of a room never fires and the reconciler's empty-listing guard does not see those rooms. - -## What Changes - -- The server tells the writer whether the node reports media statistics (true unless the server says otherwise). -- The writer publishes it on the node heartbeat (`nodes.mediaStats`) and stamps `mediaStats: false` on the rooms and - participant sessions created on a node that does not report them, so the mark outlives the node document. -- Signal and data pseudo-track stats also advance the room's `lastStatsAt`, as the stats delta requirement already - states for every flush. -- The rooms list returns `mediaStats: false` for those rooms, and the Rooms table shows a dash with a tooltip instead - of "↑ 0 B ↓ 0 B". -- The analytics summary returns `mediaStats: "partial" | "none"` when some or all nodes seen in the range do not - report media statistics. The Data up / down tile and the Bandwidth section say the data is not reported (`none`) or - covers only some servers (`partial`). - -Scope excludes the following: -- Producing media statistics on such a node (a server concern). -- Views outside the rooms list and the Analytics page: they read the same marks, through the seam the Analytics page - exposes to its extension cards (`AnalyticsPageContext.mediaStats`) and the entity documents. -- Backfilling the mark on documents written before the change. - -Edition: CE and PRO (writer and the CE views every edition shows). Repositories: openvidu-livekit -(`openvidu/analytics`, `openvidu/openviduconfig`, `openvidu/openvidu.go`) and the dashboard (typings, backend, frontend, -data generator, specs). - -CHANGELOG lines: none. The v2 data model, its writer and these views have not shipped in a release, and a CE server -always reports media statistics. - -## Capabilities - -### New Capabilities - -None. - -### Modified Capabilities - -- `server-analytics-writer`: the node heartbeat carries `mediaStats`, rooms and sessions of such a node are stamped, - and pseudo-track stats advance the room's `lastStatsAt`. -- `data-model`: `mediaStats` on `nodes`, `rooms` and `participants`. -- `rooms-api`: the room list item carries `mediaStats: false`. -- `rooms-ui`: the Data up / down cell of such a room reads as not reported. -- `analytics-api`: the summary carries the media statistics coverage of its range and scope. -- `analytics-ui`: the Data up / down tile and the Bandwidth section follow that coverage. - -## Impact - -- Writer: a `MediaStats` setting on `openviduconfig`, passed through `analytics.Node` to the heartbeat and to the - projection options; room and participant projections; the stats projection; the contract schemas of `nodes`, - `rooms` and `participants`. Unit and integration tests in `openvidu/analytics`. -- Dashboard: typings and their schema mirror, rooms mapper, analytics summary (one `nodes` query per summary), Rooms - table and Analytics page, the data generator (an option for nodes without media statistics), unit and e2e tests. -- No schema version bump: the v2 schema has not shipped. diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/analytics-api/spec.md b/openvidu-dashboard/openspec/changes/media-stats-availability/specs/analytics-api/spec.md deleted file mode 100644 index e7055332b..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/analytics-api/spec.md +++ /dev/null @@ -1,33 +0,0 @@ -## ADDED Requirements - -### Requirement: Media statistics coverage - -Edition: CE. The summary SHALL carry `mediaStats: "none"` when every `nodes` document of its scope whose heartbeat span -(`firstSeenAt` to `lastSeenAt`) overlaps the range has `mediaStats: false`, `mediaStats: "partial"` when only some do, -and no `mediaStats` property when none does or no node document overlaps the range. The `global` scope covers every -node, a `node:` scope that node only. - -#### Scenario: No server reports media statistics - -- **GIVEN** two nodes seen during the range, both with `mediaStats: false` -- **WHEN** the summary is requested -- **THEN** the body has `mediaStats: "none"` and still carries `bytesUp` and `bytesDown` - -#### Scenario: Some servers report media statistics - -- **GIVEN** a node with `mediaStats: false` and a node with `mediaStats: true` seen during the range -- **WHEN** the summary is requested with the `global` scope -- **THEN** the body has `mediaStats: "partial"` - -#### Scenario: Node scope - -- **GIVEN** the same two nodes and `scope=node:` of the node with `mediaStats: true` -- **WHEN** the summary is requested -- **THEN** the body has no `mediaStats` property - -#### Scenario: Node seen outside the range - -- **GIVEN** a node with `mediaStats: false` whose `lastSeenAt` is earlier than `from`, and a node with - `mediaStats: true` seen during the range -- **WHEN** the summary is requested -- **THEN** the body has no `mediaStats` property diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/analytics-ui/spec.md b/openvidu-dashboard/openspec/changes/media-stats-availability/specs/analytics-ui/spec.md deleted file mode 100644 index 94f0d636f..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/analytics-ui/spec.md +++ /dev/null @@ -1,27 +0,0 @@ -## ADDED Requirements - -### Requirement: Media statistics coverage notes - -Edition: CE. With `mediaStats: "none"` in the summary, the Data up / down tile SHALL read "—" with the hint "Not -reported by the servers of this range" and the Bandwidth section SHALL show the notice "The servers of this range do -not report media statistics" instead of its chart and tiles. With `"partial"`, the tile and the section SHALL keep their -values and add the note "Only from the servers that report media statistics". - -#### Scenario: No server reports media statistics - -- **GIVEN** a summary with `mediaStats: "none"` -- **WHEN** the page renders -- **THEN** the Data up / down tile reads "—" with its hint, and the Bandwidth section shows the notice and no chart - -#### Scenario: Some servers report media statistics - -- **GIVEN** a summary with `mediaStats: "partial"`, `bytesUp: 1200000000` and `bytesDown: 3400000000` -- **WHEN** the page renders -- **THEN** the tile reads "↑ 1.2 GB" over "↓ 3.4 GB" with the hint "Only from the servers that report media - statistics", and the Bandwidth chart renders with the same note under its heading - -#### Scenario: Every server reports media statistics - -- **GIVEN** a summary without `mediaStats` -- **WHEN** the page renders -- **THEN** the tile and the Bandwidth section render as before, without the note diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/data-model/spec.md b/openvidu-dashboard/openspec/changes/media-stats-availability/specs/data-model/spec.md deleted file mode 100644 index fb819f119..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/data-model/spec.md +++ /dev/null @@ -1,78 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Room document -Edition: CE and PRO. A `rooms` document SHALL describe one room session: identity and lifecycle, node, -configuration, participant/track/egress/ingress counters, traffic, quality, feature flags and retention -fields, with the field list of the shared contract (`dashboard-ce/typings/src/data-model`). -`participants.sessions`, `unique`, `active`, `peakActive` and `aborted` SHALL count STANDARD sessions only; -`participants.byKind` SHALL count the sessions of every kind. - -#### Scenario: Fields of a room document -- **WHEN** a room document is read -- **THEN** it has `name`, `nameLower`, `status` (`active` | `closed`), `createdAt`, `endedAt`, `durationMs`, `endReason`, `closedBy`, `nodeId`, `config` (max participants, empty and departure timeouts, metadata when allowed), `participants { sessions, unique, active, peakActive, aborted, byKind }`, `tracks`, `egress` (by type), `ingress`, `bytes { up, down }`, `quality { scoreSum, scoreCount, minScore, poor, lost }`, `features { egress, ingress, agent, sip }`, `lastEventAt`, `lastStatsAt`, `mediaStats` (only `false`, when its node does not report media statistics), `schemaVersion`, `expiresAt` - -#### Scenario: Room lifecycle -- **GIVEN** `ROOM_CREATED`, three `PARTICIPANT_JOINED`/`ACTIVE`, one `PARTICIPANT_LEFT`, `ROOM_ENDED (API_DELETE)` -- **WHEN** all are processed -- **THEN** the room has `status: "closed"`, `endReason: "API_DELETE"`, `participants.sessions: 3`, `participants.active: 0`, `participants.peakActive: 3`, `durationMs = endedAt - createdAt`, `closedBy: "event"` - -#### Scenario: Dependent participants do not count as room size -- **GIVEN** a room with two STANDARD participants and one EGRESS participant -- **WHEN** counters are read -- **THEN** `participants.byKind.EGRESS` is 1 and `participants.active`/`peakActive` count only the two STANDARD participants - -#### Scenario: Only standard participants count as participants -- **GIVEN** an active room with two active STANDARD participants of different identities, one EGRESS, one INGRESS and one AGENT participant, and one aborted SIP session -- **WHEN** counters are read -- **THEN** `participants.sessions` is 2, `unique` is 2, `active` is 2, `peakActive` is 2, `aborted` is 0 and `participants.byKind` is `{ STANDARD: 2, EGRESS: 1, INGRESS: 1, AGENT: 1, SIP: 1 }` - -### Requirement: Participant session document -Edition: CE and PRO. A `participants` document SHALL represent one participant session (one SID) with its -identity, room, kind, lifecycle status, connection facts, client facts, track and subscription counters, -traffic, quality and retention fields, with the field list of the shared contract. - -#### Scenario: Fields of a participant document -- **WHEN** a participant document is read -- **THEN** it has `identity`, `identityLower`, `name`, `roomSid`, `roomName`, `kind`, `kindDetails`, `status` (`joining` | `active` | `left` | `aborted`), `joinedAt`, `activeAt`, `leftAt`, `durationMs`, `disconnectReason`, `closedBy`, `connectTimeMs`, `connectionType`, `region`, `nodeId`, `resumes`, `client { sdk, version, os, osVersion, browser, browserVersion, deviceModel, network, address }`, `permission`, `isPublisher`, `metadata` and `attributes` (when allowed), `tracks { published, bySource, subscribed, subscribeFailures }`, `bytes`, `quality`, `lastEventAt`, `lastStatsAt`, `mediaStats` (only `false`, when its node does not report media statistics), `schemaVersion`, `expiresAt` - -#### Scenario: Connection aborted -- **GIVEN** `PARTICIPANT_JOINED` followed by `PARTICIPANT_CONNECTION_ABORTED` -- **WHEN** both are processed -- **THEN** the document has `status: "aborted"`, `activeAt` absent, `leftAt` set and `disconnectReason` from the event - -#### Scenario: Reconnect creates a new session -- **GIVEN** a participant that reconnects with the same identity and receives a new SID -- **WHEN** `PARTICIPANT_LEFT (DUPLICATE_IDENTITY)` and the new `PARTICIPANT_JOINED` are processed -- **THEN** two documents exist with the same `identity` and `roomSid`, the first `left` and the second `joining` or `active` - -#### Scenario: Resume keeps the session -- **GIVEN** an active participant that resumes its signal connection -- **WHEN** `PARTICIPANT_RESUMED` is processed -- **THEN** the same document stays `active` and `resumes` increases by one - -### Requirement: Nodes -Edition: CE and PRO. The `nodes` collection SHALL hold one document per server node with `firstSeenAt`, -`lastSeenAt`, `serverVersion`, `region`, `batchIntervalMs` (int32 milliseconds, at least 1 000: the node's -configured batch interval) and `mediaStats`, updated by every writer batch, and SHALL expire documents 30 days after -their last heartbeat so decommissioned nodes disappear. A document without `batchIntervalMs` SHALL be read as a 10 s -interval, and one without `mediaStats` as `true`. - -#### Scenario: Node heartbeat -- **GIVEN** a node writing batches every 10 s -- **WHEN** `nodes` is read -- **THEN** its document has `lastSeenAt` within the last batch interval and `batchIntervalMs: 10000` - -#### Scenario: Node document without the interval -- **GIVEN** a `nodes` document without `batchIntervalMs` -- **WHEN** the dashboard derives that node's freshness windows -- **THEN** it uses a 10 000 ms interval - -#### Scenario: Interval below the minimum -- **GIVEN** the test configuration -- **WHEN** a `nodes` document with `batchIntervalMs: 500` is written -- **THEN** the write is rejected by the validator - -#### Scenario: Node document without media statistics -- **GIVEN** a `nodes` document without `mediaStats` -- **WHEN** the dashboard reads it -- **THEN** it treats the node as reporting media statistics diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/rooms-api/spec.md b/openvidu-dashboard/openspec/changes/media-stats-availability/specs/rooms-api/spec.md deleted file mode 100644 index 53e79a8df..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/rooms-api/spec.md +++ /dev/null @@ -1,44 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Room list item - -Edition: CE. Each rooms list item SHALL be the room document's public projection: identity, lifecycle, node, -participant, egress and ingress counters, bandwidth, feature flags, the end cause behind the "estimated" cue, the last -stats flush, the media statistics mark and the `stale` flag of `api-conventions`, never internal id arrays, -`schemaVersion` or `expiresAt`. -Participant counters count STANDARD sessions only; `participants.unique` counts distinct STANDARD identities. - -#### Scenario: Fields of an item - -- **WHEN** an item is read -- **THEN** it has `sid`, `name`, `status` (`active` | `closed`), `createdAt`, `endedAt?`, `durationMs?`, `closedBy?` - (`event` | `reconciler`), `nodeId?`, `participants { sessions, unique, active, peakActive }`, - `egress { total, active }`, `ingress { sessions, active }`, `bytes { up, down }`, - `features { egress, ingress, agent, sip }`, `lastStatsAt?`, `stale?`, `mediaStats?` and no `participantSids`, - `identities` or similar array - -#### Scenario: Active room - -- **GIVEN** a room still open with three active STANDARD participants and one active EGRESS participant -- **WHEN** it is returned -- **THEN** `status` is `active`, `endedAt`, `durationMs` and `closedBy` are absent and `participants.active` is 3 - -#### Scenario: Unique identities - -- **GIVEN** a closed room where STANDARD identity `alice` had three sessions, `bob` one, and an AGENT session joined -- **WHEN** it is returned -- **THEN** `participants.sessions` is 4 and `participants.unique` is 2 - -#### Scenario: Stale active room - -- **GIVEN** the default writer batch interval of 10 s and an active room whose `lastStatsAt` is 3 minutes old at the - server's clock -- **WHEN** it is returned -- **THEN** it has `stale: true`, and an active room whose `lastStatsAt` is 20 s old has no `stale` property - -#### Scenario: Room of a node without media statistics - -- **GIVEN** a room document with `mediaStats: false` and `bytes { up: 0, down: 0 }` -- **WHEN** it is returned -- **THEN** it has `mediaStats: false` and `bytes { up: 0, down: 0 }`, and an item of a room without the field has no - `mediaStats` property diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/rooms-ui/spec.md b/openvidu-dashboard/openspec/changes/media-stats-availability/specs/rooms-ui/spec.md deleted file mode 100644 index 2729e9f0f..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/rooms-ui/spec.md +++ /dev/null @@ -1,46 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Rooms table - -Edition: CE. The Rooms table SHALL render, in order, the columns SID (detail link with copy button), Name (copy -button), Status (estimated and stale cues), Started, Ended, Duration (live), Unique identities (`participants.unique`), -Active, Data up / down, Features (badges E, I, A) and Node (hidden by default); the sortable headers are Started -(`createdAt`, the default), Ended (`endedAt`), Duration (`duration`), Unique identities (`uniqueIdentities`) and Active -(`active`). - -#### Scenario: Row of an active room - -- **GIVEN** an active room `demo-room` / `RM_ab12cd` with 6 unique identities, 7 sessions, 5 active, 12 MB up, 48 MB - down, an egress and an agent -- **WHEN** the row renders -- **THEN** it shows, in order, `RM_ab12cd` once, as a link to `/rooms/RM_ab12cd` with its copy button, `demo-room` with - its copy button, the active badge without a LIVE chip, the start time, a dash for Ended, a live duration, 6, 5, - "↑ 12.0 MB ↓ 48.0 MB" and the E and A badges, under headers that include "Unique identities" and "Data up / down" - -#### Scenario: Sort by Ended - -- **GIVEN** `status=all` and the default 24 h range -- **WHEN** the user activates the Ended header -- **THEN** the URL gets `sort=endedAt&order=desc` without `cursor`, the list holds the rooms started in the range, - and the active rooms, which have no end yet, come first - -#### Scenario: Sort by unique identities - -- **GIVEN** the default 24 h range -- **WHEN** the user activates the Unique identities header -- **THEN** the URL gets `sort=uniqueIdentities&order=desc` without `cursor` and the rooms with the most distinct - STANDARD identities come first - -#### Scenario: Reconciler-closed room - -- **GIVEN** a room with `closedBy: "reconciler"` -- **WHEN** the row renders -- **THEN** the status badge has the dotted outline and the Ended cell carries the tooltip "Closed by the reconciler: - the end time is estimated" - -#### Scenario: Room of a node without media statistics - -- **GIVEN** a room item with `mediaStats: false` -- **WHEN** the row renders -- **THEN** the Data up / down cell reads "—" with the tooltip "Not reported: this room's server does not report media - statistics" instead of "↑ 0 B ↓ 0 B" diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/server-analytics-writer/spec.md b/openvidu-dashboard/openspec/changes/media-stats-availability/specs/server-analytics-writer/spec.md deleted file mode 100644 index 1567b157e..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/specs/server-analytics-writer/spec.md +++ /dev/null @@ -1,59 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Signal and data pseudo-tracks -Edition: CE and PRO. Stats whose track id starts with `TR_SG` or `TR_DT` SHALL NOT produce `track_stats` -documents; their bytes SHALL be added to the participant's `bytes` for their direction and `lastStatsAt` -SHALL be updated on the participant and on its room, without adding bytes or quality samples to the room. - -#### Scenario: Signal-only participant -- **GIVEN** a participant whose flush contains only `TR_SG…` upstream and downstream stats -- **WHEN** it is stored -- **THEN** no `track_stats` document is written, the participant's `bytes.up` and `bytes.down` grow by the sample bytes, and the participant's and the room's `lastStatsAt` equal the flush time while the room's `bytes` and `quality` are unchanged - -### Requirement: Node heartbeat -Edition: CE and PRO. On every batch tick, including empty batches, the writer SHALL upsert the node's -`nodes` document with `lastSeenAt`, `serverVersion`, `region`, `batchIntervalMs` (the configured -`openvidu.analytics.interval` in milliseconds) and `mediaStats`, setting `firstSeenAt` on insert. - -#### Scenario: Idle node -- **GIVEN** a node with no traffic for ten minutes -- **WHEN** `nodes` is read -- **THEN** its document has `lastSeenAt` within the last batch interval and `firstSeenAt` unchanged - -#### Scenario: Configured interval published -- **GIVEN** `openvidu.analytics.interval: 30s` -- **WHEN** a batch tick runs -- **THEN** the node's document has `batchIntervalMs: 30000` - -#### Scenario: Interval changed on restart -- **GIVEN** a node document with `batchIntervalMs: 10000` -- **WHEN** the node restarts with `openvidu.analytics.interval: 60s` and runs a batch tick -- **THEN** its document has `batchIntervalMs: 60000` and `firstSeenAt` unchanged - -#### Scenario: Node reporting media statistics -- **GIVEN** a server that reports media statistics -- **WHEN** a batch tick runs -- **THEN** the node's document has `mediaStats: true` - -## ADDED Requirements - -### Requirement: Nodes without media statistics -Edition: CE and PRO. A server whose media engine does not report media statistics SHALL tell the writer so, and the -writer SHALL then publish `mediaStats: false` on its node document and stamp `mediaStats: false` on the rooms and -participant sessions it creates, following their `nodeId`; the rooms and sessions of a node that reports them carry no -`mediaStats` field. - -#### Scenario: Room created on such a node -- **GIVEN** a node whose server does not report media statistics -- **WHEN** it writes `ROOM_CREATED` and `PARTICIPANT_JOINED` of a new room -- **THEN** its `nodes` document has `mediaStats: false`, and the room and the participant session both have `nodeId` of that node and `mediaStats: false` - -#### Scenario: Room created on a node reporting media statistics -- **GIVEN** a node whose server reports media statistics -- **WHEN** it writes `ROOM_CREATED` and `PARTICIPANT_JOINED` -- **THEN** neither the room nor the participant session has a `mediaStats` field - -#### Scenario: Room touched first by another node -- **GIVEN** a room document created by an egress event written by a node reporting media statistics, without `mediaStats` -- **WHEN** a node that does not report them writes its `ROOM_CREATED` -- **THEN** the room has that node's `nodeId` and `mediaStats: false` diff --git a/openvidu-dashboard/openspec/changes/media-stats-availability/tasks.md b/openvidu-dashboard/openspec/changes/media-stats-availability/tasks.md deleted file mode 100644 index 10ce9b2ad..000000000 --- a/openvidu-dashboard/openspec/changes/media-stats-availability/tasks.md +++ /dev/null @@ -1,60 +0,0 @@ -# Tasks - -## 1. Contract and typings - -- [x] 1.1 Add the optional `mediaStats` bool to the `nodes`, `rooms` and `participants` schemas of - `dashboard-ce/typings/contract/`, the document types of `src/data-model/` and the fixtures, then regenerate - `schema/` and `fixtures/`. Verify with the typings contract tests and the typecheck. -- [x] 1.2 Add `mediaStats?: false` to `RoomListItem` and `mediaStats?: 'partial' | 'none'` to `AnalyticsSummary` in - `src/api/`. Verify with the typecheck of every package. - -## 2. Writer (openvidu-livekit) - -- [x] 2.1 Vendor the contract into `openvidu/analytics/schema/contract/` with `contract/vendor.sh`. Verify with the - writer's contract tests and `./dashboard.sh contract-check`. -- [x] 2.2 Add `openviduconfig.MediaStats` (default true) and `analytics.Node.MediaStats`, set from it in - `openvidu/openvidu.go`, and write `mediaStats` on every heartbeat. Verify with a store integration test of the - heartbeat for both values. -- [x] 2.3 Stamp `mediaStats: false` on rooms and participant sessions created by a node without media statistics, - following `nodeId` (`ROOM_CREATED` sets or removes it; other events only while `nodeId` is unknown), and keep the - legacy migration at `MediaStats: true`. Verify with projection unit tests and writer integration tests of the three - scenarios of "Nodes without media statistics". -- [x] 2.4 Make signal and data pseudo-track stats advance the room's `lastStatsAt` without bytes or quality. Verify with - a projection unit test of the signal-only flush, the writer integration tests and the batch benchmark. -- [x] 2.5 Lint the fork with the Dockerfile Go toolchain and run its unit and integration tests. Verify that both pass. - -## 3. Backend - -- [x] 3.1 Return `mediaStats: false` on rooms list items of stamped rooms. Verify with a mapper unit test and a rooms - route integration test. -- [x] 3.2 Compute the summary's `mediaStats` coverage from the `nodes` documents whose span overlaps the range, for the - `global` and `node:` scopes, and declare it in the OpenAPI document. Verify with unit tests of the fold, an - integration test of the four "Media statistics coverage" scenarios with an `explain()` assertion on the `nodes` - query, and the OpenAPI test. - -## 4. Frontend - -- [x] 4.1 Render "—" with the tooltip in the Data up / down cell of stamped rooms in the Rooms table. Verify with a - component test and an e2e test against the mocked API. -- [x] 4.2 Apply the summary coverage to the Data up / down tile and the Bandwidth section (notice for `none`, note for - `partial`), and expose it to the extension cards as `AnalyticsPageContext.mediaStats`. Verify with component tests, - a test of the context signal and an e2e test of the three "Media statistics coverage notes" scenarios. - -## 5. Data generator - -- [x] 5.1 Add `--nodes-without-media-stats ` (default 0) to the data generator and keep the flag in the keep-alive, - let a calling demo pass extra generator options (`GENERATOR_ARGS` of `demo-lib.sh`), and update the README. Verify with a generator test that the marked node's rooms and sessions are stamped and have no - media `track_stats`, room bytes or quality, and that the other nodes report `mediaStats: true`. - -## 6. Docs and CHANGELOG - -- [x] 6.1 No CHANGELOG line: unreleased, and a CE server always reports media statistics. Update the `AGENTS.md` notes - that list the node heartbeat fields or the generator options, if any. Verify by reading the change against the - CHANGELOG and the `AGENTS.md` files. - -## 7. Integration check - -- [x] 7.1 Run `./dashboard.sh check` and `./dashboard.sh contract-check ../openvidu-livekit`. Verify that both pass. -- [x] 7.2 Run the analytics writer suite of `openvidu-test-integration` against a local deployment with the new - server image. Verify that the node document has `mediaStats: true` and that rooms of signal-only participants get - `lastStatsAt`. diff --git a/openvidu-dashboard/openspec/specs/analytics-api/spec.md b/openvidu-dashboard/openspec/specs/analytics-api/spec.md index 7c8e4aa2b..6d1813cd7 100644 --- a/openvidu-dashboard/openspec/specs/analytics-api/spec.md +++ b/openvidu-dashboard/openspec/specs/analytics-api/spec.md @@ -357,3 +357,35 @@ range by input type. It is not gated by any feature. - **GIVEN** no ingress session started in the range - **WHEN** `dimensions=ingressInputType` is requested - **THEN** the breakdown has `total: 0` and `items: []` + +### Requirement: Media statistics coverage + +Edition: CE. The summary SHALL carry `mediaStats: "none"` when every `nodes` document of its scope whose heartbeat span +(`firstSeenAt` to `lastSeenAt`) overlaps the range has `mediaStats: false`, `mediaStats: "partial"` when only some do, +and no `mediaStats` property when none does or no node document overlaps the range. The `global` scope covers every +node, a `node:` scope that node only. + +#### Scenario: No server reports media statistics + +- **GIVEN** two nodes seen during the range, both with `mediaStats: false` +- **WHEN** the summary is requested +- **THEN** the body has `mediaStats: "none"` and still carries `bytesUp` and `bytesDown` + +#### Scenario: Some servers report media statistics + +- **GIVEN** a node with `mediaStats: false` and a node with `mediaStats: true` seen during the range +- **WHEN** the summary is requested with the `global` scope +- **THEN** the body has `mediaStats: "partial"` + +#### Scenario: Node scope + +- **GIVEN** the same two nodes and `scope=node:` of the node with `mediaStats: true` +- **WHEN** the summary is requested +- **THEN** the body has no `mediaStats` property + +#### Scenario: Node seen outside the range + +- **GIVEN** a node with `mediaStats: false` whose `lastSeenAt` is earlier than `from`, and a node with + `mediaStats: true` seen during the range +- **WHEN** the summary is requested +- **THEN** the body has no `mediaStats` property diff --git a/openvidu-dashboard/openspec/specs/analytics-ui/spec.md b/openvidu-dashboard/openspec/specs/analytics-ui/spec.md index 86a092716..5f525b600 100644 --- a/openvidu-dashboard/openspec/specs/analytics-ui/spec.md +++ b/openvidu-dashboard/openspec/specs/analytics-ui/spec.md @@ -361,3 +361,29 @@ and the time zone of its time axis; the unique identities chart names UTC and ke - **GIVEN** `granularity=1d` and `tz=Europe/Madrid` - **WHEN** the unique identities chart renders - **THEN** its note reads "per UTC day" and names UTC, not Europe/Madrid + +### Requirement: Media statistics coverage notes + +Edition: CE. With `mediaStats: "none"` in the summary, the Data up / down tile SHALL read "—" with the hint "Not +reported by the servers of this range" and the Bandwidth section SHALL show the notice "The servers of this range do +not report media statistics" instead of its chart and tiles. With `"partial"`, the tile and the section SHALL keep their +values and add the note "Only from the servers that report media statistics". + +#### Scenario: No server reports media statistics + +- **GIVEN** a summary with `mediaStats: "none"` +- **WHEN** the page renders +- **THEN** the Data up / down tile reads "—" with its hint, and the Bandwidth section shows the notice and no chart + +#### Scenario: Some servers report media statistics + +- **GIVEN** a summary with `mediaStats: "partial"`, `bytesUp: 1200000000` and `bytesDown: 3400000000` +- **WHEN** the page renders +- **THEN** the tile reads "↑ 1.2 GB" over "↓ 3.4 GB" with the hint "Only from the servers that report media + statistics", and the Bandwidth chart renders with the same note under its heading + +#### Scenario: Every server reports media statistics + +- **GIVEN** a summary without `mediaStats` +- **WHEN** the page renders +- **THEN** the tile and the Bandwidth section render as before, without the note diff --git a/openvidu-dashboard/openspec/specs/data-model/spec.md b/openvidu-dashboard/openspec/specs/data-model/spec.md index 4343e6324..e0280416a 100644 --- a/openvidu-dashboard/openspec/specs/data-model/spec.md +++ b/openvidu-dashboard/openspec/specs/data-model/spec.md @@ -82,7 +82,7 @@ fields, with the field list of the shared contract (`dashboard-ce/typings/src/da #### Scenario: Fields of a room document - **WHEN** a room document is read -- **THEN** it has `name`, `nameLower`, `status` (`active` | `closed`), `createdAt`, `endedAt`, `durationMs`, `endReason`, `closedBy`, `nodeId`, `config` (max participants, empty and departure timeouts, metadata when allowed), `participants { sessions, unique, active, peakActive, aborted, byKind }`, `tracks`, `egress` (by type), `ingress`, `bytes { up, down }`, `quality { scoreSum, scoreCount, minScore, poor, lost }`, `features { egress, ingress, agent, sip }`, `lastEventAt`, `lastStatsAt`, `schemaVersion`, `expiresAt` +- **THEN** it has `name`, `nameLower`, `status` (`active` | `closed`), `createdAt`, `endedAt`, `durationMs`, `endReason`, `closedBy`, `nodeId`, `config` (max participants, empty and departure timeouts, metadata when allowed), `participants { sessions, unique, active, peakActive, aborted, byKind }`, `tracks`, `egress` (by type), `ingress`, `bytes { up, down }`, `quality { scoreSum, scoreCount, minScore, poor, lost }`, `features { egress, ingress, agent, sip }`, `lastEventAt`, `lastStatsAt`, `mediaStats` (only `false`, when its node does not report media statistics), `schemaVersion`, `expiresAt` #### Scenario: Room lifecycle - **GIVEN** `ROOM_CREATED`, three `PARTICIPANT_JOINED`/`ACTIVE`, one `PARTICIPANT_LEFT`, `ROOM_ENDED (API_DELETE)` @@ -106,7 +106,7 @@ traffic, quality and retention fields, with the field list of the shared contrac #### Scenario: Fields of a participant document - **WHEN** a participant document is read -- **THEN** it has `identity`, `identityLower`, `name`, `roomSid`, `roomName`, `kind`, `kindDetails`, `status` (`joining` | `active` | `left` | `aborted`), `joinedAt`, `activeAt`, `leftAt`, `durationMs`, `disconnectReason`, `closedBy`, `connectTimeMs`, `connectionType`, `region`, `nodeId`, `resumes`, `client { sdk, version, os, osVersion, browser, browserVersion, deviceModel, network, address }`, `permission`, `isPublisher`, `metadata` and `attributes` (when allowed), `tracks { published, bySource, subscribed, subscribeFailures }`, `bytes`, `quality`, `lastEventAt`, `lastStatsAt`, `schemaVersion`, `expiresAt` +- **THEN** it has `identity`, `identityLower`, `name`, `roomSid`, `roomName`, `kind`, `kindDetails`, `status` (`joining` | `active` | `left` | `aborted`), `joinedAt`, `activeAt`, `leftAt`, `durationMs`, `disconnectReason`, `closedBy`, `connectTimeMs`, `connectionType`, `region`, `nodeId`, `resumes`, `client { sdk, version, os, osVersion, browser, browserVersion, deviceModel, network, address }`, `permission`, `isPublisher`, `metadata` and `attributes` (when allowed), `tracks { published, bySource, subscribed, subscribeFailures }`, `bytes`, `quality`, `lastEventAt`, `lastStatsAt`, `mediaStats` (only `false`, when its node does not report media statistics), `schemaVersion`, `expiresAt` #### Scenario: Connection aborted - **GIVEN** `PARTICIPANT_JOINED` followed by `PARTICIPANT_CONNECTION_ABORTED` @@ -287,9 +287,10 @@ participant counter SHALL count STANDARD sessions only, except `participants.byK ### Requirement: Nodes Edition: CE and PRO. The `nodes` collection SHALL hold one document per server node with `firstSeenAt`, -`lastSeenAt`, `serverVersion`, `region` and `batchIntervalMs` (int32 milliseconds, at least 1 000: the node's -configured batch interval), updated by every writer batch, and SHALL expire documents 30 days after their last -heartbeat so decommissioned nodes disappear. A document without `batchIntervalMs` SHALL be read as a 10 s interval. +`lastSeenAt`, `serverVersion`, `region`, `batchIntervalMs` (int32 milliseconds, at least 1 000: the node's +configured batch interval) and `mediaStats`, updated by every writer batch, and SHALL expire documents 30 days after +their last heartbeat so decommissioned nodes disappear. A document without `batchIntervalMs` SHALL be read as a 10 s +interval, and one without `mediaStats` as `true`. #### Scenario: Node heartbeat - **GIVEN** a node writing batches every 10 s @@ -306,6 +307,11 @@ heartbeat so decommissioned nodes disappear. A document without `batchIntervalMs - **WHEN** a `nodes` document with `batchIntervalMs: 500` is written - **THEN** the write is rejected by the validator +#### Scenario: Node document without media statistics +- **GIVEN** a `nodes` document without `mediaStats` +- **WHEN** the dashboard reads it +- **THEN** it treats the node as reporting media statistics + ### Requirement: Retention Edition: CE and PRO. Retention SHALL be configurable per class with defaults events 7 d, stats 7 d, tracks (tracks and subscriptions) 7 d, entities (rooms, participants, egress, ingress) 90 d, 5-minute rollups 7 d, diff --git a/openvidu-dashboard/openspec/specs/rooms-api/spec.md b/openvidu-dashboard/openspec/specs/rooms-api/spec.md index b03033e2b..2be4ee8e0 100644 --- a/openvidu-dashboard/openspec/specs/rooms-api/spec.md +++ b/openvidu-dashboard/openspec/specs/rooms-api/spec.md @@ -43,7 +43,8 @@ filters `status`, `q`, `features` and `nodeId`, the sort keys `createdAt` (defau Edition: CE. Each rooms list item SHALL be the room document's public projection: identity, lifecycle, node, participant, egress and ingress counters, bandwidth, feature flags, the end cause behind the "estimated" cue, the last -stats flush and the `stale` flag of `api-conventions`, never internal id arrays, `schemaVersion` or `expiresAt`. +stats flush, the media statistics mark and the `stale` flag of `api-conventions`, never internal id arrays, +`schemaVersion` or `expiresAt`. Participant counters count STANDARD sessions only; `participants.unique` counts distinct STANDARD identities. #### Scenario: Fields of an item @@ -52,8 +53,8 @@ Participant counters count STANDARD sessions only; `participants.unique` counts - **THEN** it has `sid`, `name`, `status` (`active` | `closed`), `createdAt`, `endedAt?`, `durationMs?`, `closedBy?` (`event` | `reconciler`), `nodeId?`, `participants { sessions, unique, active, peakActive }`, `egress { total, active }`, `ingress { sessions, active }`, `bytes { up, down }`, - `features { egress, ingress, agent, sip }`, `lastStatsAt?`, `stale?` and no `participantSids`, `identities` or - similar array + `features { egress, ingress, agent, sip }`, `lastStatsAt?`, `stale?`, `mediaStats?` and no `participantSids`, + `identities` or similar array #### Scenario: Active room @@ -74,6 +75,13 @@ Participant counters count STANDARD sessions only; `participants.unique` counts - **WHEN** it is returned - **THEN** it has `stale: true`, and an active room whose `lastStatsAt` is 20 s old has no `stale` property +#### Scenario: Room of a node without media statistics + +- **GIVEN** a room document with `mediaStats: false` and `bytes { up: 0, down: 0 }` +- **WHEN** it is returned +- **THEN** it has `mediaStats: false` and `bytes { up: 0, down: 0 }`, and an item of a room without the field has no + `mediaStats` property + ### Requirement: Status filter Edition: CE. The rooms list SHALL accept `status` as `active`, `closed` or `all` (default `all`) and reject any other diff --git a/openvidu-dashboard/openspec/specs/rooms-ui/spec.md b/openvidu-dashboard/openspec/specs/rooms-ui/spec.md index af3d02ee8..ae47bc4a2 100644 --- a/openvidu-dashboard/openspec/specs/rooms-ui/spec.md +++ b/openvidu-dashboard/openspec/specs/rooms-ui/spec.md @@ -122,6 +122,13 @@ Active, Data up / down, Features (badges E, I, A) and Node (hidden by default); - **THEN** the status badge has the dotted outline and the Ended cell carries the tooltip "Closed by the reconciler: the end time is estimated" +#### Scenario: Room of a node without media statistics + +- **GIVEN** a room item with `mediaStats: false` +- **WHEN** the row renders +- **THEN** the Data up / down cell reads "—" with the tooltip "Not reported: this room's server does not report media + statistics" instead of "↑ 0 B ↓ 0 B" + ### Requirement: Rooms pagination Edition: CE. The Rooms page SHALL use the cursor paginator with Prev/Next bound to `prevCursor`/`nextCursor` for every diff --git a/openvidu-dashboard/openspec/specs/server-analytics-writer/spec.md b/openvidu-dashboard/openspec/specs/server-analytics-writer/spec.md index 116c28293..6f0d5b2da 100644 --- a/openvidu-dashboard/openspec/specs/server-analytics-writer/spec.md +++ b/openvidu-dashboard/openspec/specs/server-analytics-writer/spec.md @@ -186,12 +186,12 @@ per track and direction. Both modes produce the same document shape. ### Requirement: Signal and data pseudo-tracks Edition: CE and PRO. Stats whose track id starts with `TR_SG` or `TR_DT` SHALL NOT produce `track_stats` documents; their bytes SHALL be added to the participant's `bytes` for their direction and `lastStatsAt` -SHALL be updated. +SHALL be updated on the participant and on its room, without adding bytes or quality samples to the room. #### Scenario: Signal-only participant - **GIVEN** a participant whose flush contains only `TR_SG…` upstream and downstream stats - **WHEN** it is stored -- **THEN** no `track_stats` document is written, the participant's `bytes.up` and `bytes.down` grow by the sample bytes and `lastStatsAt` equals the flush time +- **THEN** no `track_stats` document is written, the participant's `bytes.up` and `bytes.down` grow by the sample bytes, and the participant's and the room's `lastStatsAt` equal the flush time while the room's `bytes` and `quality` are unchanged ### Requirement: Stats deltas into entities and rollups Edition: CE and PRO. Each stats batch SHALL add its byte totals and quality samples (score sum, sample @@ -381,8 +381,8 @@ OpenVidu 3.9 or earlier (one holding a document without `ts` while `schema_meta` ### Requirement: Node heartbeat Edition: CE and PRO. On every batch tick, including empty batches, the writer SHALL upsert the node's -`nodes` document with `lastSeenAt`, `serverVersion`, `region` and `batchIntervalMs` (the configured -`openvidu.analytics.interval` in milliseconds), setting `firstSeenAt` on insert. +`nodes` document with `lastSeenAt`, `serverVersion`, `region`, `batchIntervalMs` (the configured +`openvidu.analytics.interval` in milliseconds) and `mediaStats`, setting `firstSeenAt` on insert. #### Scenario: Idle node - **GIVEN** a node with no traffic for ten minutes @@ -399,6 +399,11 @@ Edition: CE and PRO. On every batch tick, including empty batches, the writer SH - **WHEN** the node restarts with `openvidu.analytics.interval: 60s` and runs a batch tick - **THEN** its document has `batchIntervalMs: 60000` and `firstSeenAt` unchanged +#### Scenario: Node reporting media statistics +- **GIVEN** a server that reports media statistics +- **WHEN** a batch tick runs +- **THEN** the node's document has `mediaStats: true` + ### Requirement: Write path metrics Edition: CE and PRO. The writer SHALL expose Prometheus metrics on the server's metrics endpoint with the `openvidu_analytics_` prefix: queue depth per queue, batches by result, batch duration, dropped entries by @@ -541,3 +546,24 @@ failure SHALL still stop the server. - **GIVEN** `mongo_url` pointing to a MongoDB that does not answer - **WHEN** the server starts - **THEN** it refuses to start, as before + +### Requirement: Nodes without media statistics +Edition: CE and PRO. A server whose media engine does not report media statistics SHALL tell the writer so, and the +writer SHALL then publish `mediaStats: false` on its node document and stamp `mediaStats: false` on the rooms and +participant sessions it creates, following their `nodeId`; the rooms and sessions of a node that reports them carry no +`mediaStats` field. + +#### Scenario: Room created on such a node +- **GIVEN** a node whose server does not report media statistics +- **WHEN** it writes `ROOM_CREATED` and `PARTICIPANT_JOINED` of a new room +- **THEN** its `nodes` document has `mediaStats: false`, and the room and the participant session both have `nodeId` of that node and `mediaStats: false` + +#### Scenario: Room created on a node reporting media statistics +- **GIVEN** a node whose server reports media statistics +- **WHEN** it writes `ROOM_CREATED` and `PARTICIPANT_JOINED` +- **THEN** neither the room nor the participant session has a `mediaStats` field + +#### Scenario: Room touched first by another node +- **GIVEN** a room document created by an egress event written by a node reporting media statistics, without `mediaStats` +- **WHEN** a node that does not report them writes its `ROOM_CREATED` +- **THEN** the room has that node's `nodeId` and `mediaStats: false`