openvidu-dashboard: archive media-stats-availability, keep only the specs

master
juancarmore 2026-11-03 09:21:36 +01:00
parent 3058e6af0b
commit cf39d6a2ee
16 changed files with 117 additions and 544 deletions

View File

@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-11-02

View File

@ -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 <n>` (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.

View File

@ -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.

View File

@ -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:<id>` 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:<id>` 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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`

View File

@ -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:<id>` 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 <n>` (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`.

View File

@ -357,3 +357,35 @@ range by input type. It is not gated by any feature.
- **GIVEN** no ingress session started in the range - **GIVEN** no ingress session started in the range
- **WHEN** `dimensions=ingressInputType` is requested - **WHEN** `dimensions=ingressInputType` is requested
- **THEN** the breakdown has `total: 0` and `items: []` - **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:<id>` 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:<id>` 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

View File

@ -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` - **GIVEN** `granularity=1d` and `tz=Europe/Madrid`
- **WHEN** the unique identities chart renders - **WHEN** the unique identities chart renders
- **THEN** its note reads "per UTC day" and names UTC, not Europe/Madrid - **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

View File

@ -82,7 +82,7 @@ fields, with the field list of the shared contract (`dashboard-ce/typings/src/da
#### Scenario: Fields of a room document #### Scenario: Fields of a room document
- **WHEN** a room document is read - **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 #### Scenario: Room lifecycle
- **GIVEN** `ROOM_CREATED`, three `PARTICIPANT_JOINED`/`ACTIVE`, one `PARTICIPANT_LEFT`, `ROOM_ENDED (API_DELETE)` - **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 #### Scenario: Fields of a participant document
- **WHEN** a participant document is read - **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 #### Scenario: Connection aborted
- **GIVEN** `PARTICIPANT_JOINED` followed by `PARTICIPANT_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 ### Requirement: Nodes
Edition: CE and PRO. The `nodes` collection SHALL hold one document per server node with `firstSeenAt`, 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 `lastSeenAt`, `serverVersion`, `region`, `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 configured batch interval) and `mediaStats`, updated by every writer batch, and SHALL expire documents 30 days after
heartbeat so decommissioned nodes disappear. A document without `batchIntervalMs` SHALL be read as a 10 s interval. 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 #### Scenario: Node heartbeat
- **GIVEN** a node writing batches every 10 s - **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 - **WHEN** a `nodes` document with `batchIntervalMs: 500` is written
- **THEN** the write is rejected by the validator - **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 ### Requirement: Retention
Edition: CE and PRO. Retention SHALL be configurable per class with defaults events 7 d, stats 7 d, 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, tracks (tracks and subscriptions) 7 d, entities (rooms, participants, egress, ingress) 90 d, 5-minute rollups 7 d,

View File

@ -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, 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 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. Participant counters count STANDARD sessions only; `participants.unique` counts distinct STANDARD identities.
#### Scenario: Fields of an item #### 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?` - **THEN** it has `sid`, `name`, `status` (`active` | `closed`), `createdAt`, `endedAt?`, `durationMs?`, `closedBy?`
(`event` | `reconciler`), `nodeId?`, `participants { sessions, unique, active, peakActive }`, (`event` | `reconciler`), `nodeId?`, `participants { sessions, unique, active, peakActive }`,
`egress { total, active }`, `ingress { sessions, active }`, `bytes { up, down }`, `egress { total, active }`, `ingress { sessions, active }`, `bytes { up, down }`,
`features { egress, ingress, agent, sip }`, `lastStatsAt?`, `stale?` and no `participantSids`, `identities` or `features { egress, ingress, agent, sip }`, `lastStatsAt?`, `stale?`, `mediaStats?` and no `participantSids`,
similar array `identities` or similar array
#### Scenario: Active room #### Scenario: Active room
@ -74,6 +75,13 @@ Participant counters count STANDARD sessions only; `participants.unique` counts
- **WHEN** it is returned - **WHEN** it is returned
- **THEN** it has `stale: true`, and an active room whose `lastStatsAt` is 20 s old has no `stale` property - **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 ### Requirement: Status filter
Edition: CE. The rooms list SHALL accept `status` as `active`, `closed` or `all` (default `all`) and reject any other Edition: CE. The rooms list SHALL accept `status` as `active`, `closed` or `all` (default `all`) and reject any other

View File

@ -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: - **THEN** the status badge has the dotted outline and the Ended cell carries the tooltip "Closed by the reconciler:
the end time is estimated" 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 ### Requirement: Rooms pagination
Edition: CE. The Rooms page SHALL use the cursor paginator with Prev/Next bound to `prevCursor`/`nextCursor` for every Edition: CE. The Rooms page SHALL use the cursor paginator with Prev/Next bound to `prevCursor`/`nextCursor` for every

View File

@ -186,12 +186,12 @@ per track and direction. Both modes produce the same document shape.
### Requirement: Signal and data pseudo-tracks ### 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` 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` 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 #### Scenario: Signal-only participant
- **GIVEN** a participant whose flush contains only `TR_SG…` upstream and downstream stats - **GIVEN** a participant whose flush contains only `TR_SG…` upstream and downstream stats
- **WHEN** it is stored - **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 ### 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 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 ### Requirement: Node heartbeat
Edition: CE and PRO. On every batch tick, including empty batches, the writer SHALL upsert the node's 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 `nodes` document with `lastSeenAt`, `serverVersion`, `region`, `batchIntervalMs` (the configured
`openvidu.analytics.interval` in milliseconds), setting `firstSeenAt` on insert. `openvidu.analytics.interval` in milliseconds) and `mediaStats`, setting `firstSeenAt` on insert.
#### Scenario: Idle node #### Scenario: Idle node
- **GIVEN** a node with no traffic for ten minutes - **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 - **WHEN** the node restarts with `openvidu.analytics.interval: 60s` and runs a batch tick
- **THEN** its document has `batchIntervalMs: 60000` and `firstSeenAt` unchanged - **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 ### Requirement: Write path metrics
Edition: CE and PRO. The writer SHALL expose Prometheus metrics on the server's metrics endpoint with the 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 `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 - **GIVEN** `mongo_url` pointing to a MongoDB that does not answer
- **WHEN** the server starts - **WHEN** the server starts
- **THEN** it refuses to start, as before - **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`