mirror of https://github.com/OpenVidu/openvidu.git
openvidu-dashboard: propose physical-media-nodes
A restart-stable media node label on rooms, sessions, nodes and node scopes, the recorded graceful stop, and no node UI in CE without per-node-breakdown.master
parent
1bc3d5287a
commit
90edceac29
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-11-04
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
- **Node documents:**
|
||||
- `analytics.Node{ID, Region, WithoutMediaStats}` is built in `openvidu/openvidu.go`.
|
||||
- `store.Heartbeat` upserts one `nodes` document per process on every batch tick, and the document expires 30 days
|
||||
after `lastSeenAt`.
|
||||
- Node ids are generated per process.
|
||||
- An edition adjusts the analytics through `openviduconfig` package variables before `openvidu.Start`
|
||||
(`DefaultStatsDetail`, `MediaStats`).
|
||||
- **Entity stamps:**
|
||||
- Rooms take `nodeId` from `ROOM_CREATED`, and from the first event of any kind while it is unknown.
|
||||
- Participant sessions take `nodeId` from their first event.
|
||||
- `mediaStats` follows the same rule (`projection/room.go`, `participant.go`).
|
||||
- **Rollups:** `projection/rollup.go` writes `global` and `node:<event node id>`. The event node id is always the
|
||||
writing process's (`pkg/telemetry/analyticsservice.go`).
|
||||
- **Reconciler:**
|
||||
- It adds the `participantsSum` of a room it closes to `node:<room.nodeId>`.
|
||||
- Its daily unique identities group sessions by `nodeId`.
|
||||
- It and the legacy migration project with `projectionOptions`, which carries no node fields.
|
||||
- **Shutdown:** `writer.drain` writes the queues after the rooms close, within the 5 s shutdown budget.
|
||||
- **Dashboard backend:**
|
||||
- The status service counts every `nodes` document (`foldWriter`).
|
||||
- The overview repository groups active rooms and sessions by `nodeId` for the edition extension
|
||||
(`OverviewInputs`).
|
||||
- The rooms list filters `nodeId` on `nodeId_status_createdAt_id`.
|
||||
- **Dashboard frontend:**
|
||||
- Three seams receive edition components: `OVERVIEW_NODES_CARD`, `ROOMS_NODE_FILTER` and `ANALYTICS_SCOPE_SELECT`.
|
||||
- CE renders them locked without `per-node-breakdown`.
|
||||
- The Node columns are CE columns reading `nodeId`.
|
||||
- **Index maintenance:** `schema.Initialize` creates and updates the contract's indexes and never drops an index that
|
||||
is absent from the contract.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- A restart-stable machine identity on every room and session, for the whole entity retention.
|
||||
- Per-node rollups that add up the restarts of one machine.
|
||||
- A stopped server is distinguishable from a silent one.
|
||||
- CE code and specs stay neutral: the label is an opaque string the server gives, and CE never shows per-node UI.
|
||||
|
||||
**Non-Goals:**
|
||||
- Deciding when a label exists or what it looks like (edition).
|
||||
- Labelling egress, ingress, tracks or subscriptions: views reach tracks and subscriptions through their participant.
|
||||
- Using `stoppedAt` in the reconciler's liveness check.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. An opaque label set by the server edition
|
||||
|
||||
`openviduconfig.MediaNode` (default empty) is the media node label. An edition sets it before the analytics start, like
|
||||
`MediaStats`. `openvidu.Start` passes it as `analytics.Node.MediaNode`, and `nodeProjectionOptions` passes it to the
|
||||
projector. There is no configuration key: the edition derives it from facts it already has.
|
||||
|
||||
Rejected:
|
||||
- The dashboard joining `nodeId` to `nodes`, for the reason `media-stats-availability` gave for `mediaStats`: node
|
||||
documents expire after 30 days and entities live 90 days by default.
|
||||
- A CE-defined label (hostname, IP). The hostname of a container is not the machine, and the right identity depends on
|
||||
how an edition deploys its nodes.
|
||||
|
||||
### D2. The stamp follows `nodeId`
|
||||
|
||||
The label is set with the same expressions as `mediaStats`. An entity this process writes first gets its label. The
|
||||
host's `ROOM_CREATED` (or the session's first event) sets `nodeId` and `mediaNode` together. So a room touched first by
|
||||
another process's egress event ends with the host's label.
|
||||
|
||||
### D3. Node scopes keyed by the label, none without one
|
||||
|
||||
`scopes()` returns `global` plus `node:<label>` when the projector has a label, and `global` alone otherwise. The
|
||||
reconciler's room-close increments use the room's `mediaNode`, and its daily unique identities group by `mediaNode`.
|
||||
The legacy migration has no label, so migrated data gets `global` only.
|
||||
|
||||
Rejected:
|
||||
- Keeping `node:<nodeId>` beside the label scope. It doubles rollup upserts on every server, CE included, for scopes
|
||||
nobody reads and that split one machine into its restarts.
|
||||
- Keeping `node:<nodeId>` only when there is no label. Same cost, and CE never reads it.
|
||||
|
||||
The effect on writes: an unlabelled server does half the rollup upserts it does today, and a labelled one the same.
|
||||
|
||||
### D4. `stoppedAt` after the final flush
|
||||
|
||||
After `drain` writes the queues, the writer upserts its node document with `$max lastSeenAt` and `$set stoppedAt`, both
|
||||
the same timestamp, within the remaining shutdown budget. A crash, a kill or an unreachable database leaves no
|
||||
`stoppedAt`, so the node reads as silent, which is what happened.
|
||||
|
||||
### D5. Status without node counts
|
||||
|
||||
`writer` becomes `{ lastSeenAt?, stoppedAt?, batchIntervalMs? }`. No CE view counts nodes any more. An edition that
|
||||
shows machines folds `nodes` itself, since the count of process documents is not a count of machines.
|
||||
|
||||
A document is alive when it has no `stoppedAt` and is within its fresh window. `stoppedAt` is reported only when no
|
||||
document is alive, which keeps a scale-in of one machine from reading as a stopped deployment.
|
||||
|
||||
### D6. Visibility follows `per-node-breakdown`
|
||||
|
||||
Without the feature, CE renders nothing per node: no card, select, scope selector or column, and no lock. With it, it
|
||||
renders the seams' components and the Node columns (from `mediaNode`). The edition decides when it reports the
|
||||
feature.
|
||||
|
||||
Rejected:
|
||||
- A separate `multiNode` fact in `/api/v1/info`: two switches for one decision.
|
||||
- Keeping the locks: nothing can be unlocked in a single-machine deployment.
|
||||
|
||||
### D7. `node` in the URL, `mediaNode` in the API
|
||||
|
||||
The value changes meaning, so the names change too. The API is unreleased, so there is no alias for `nodeId`. The Rooms
|
||||
page drops `node` without the feature, as it drops `nodeId` today.
|
||||
|
||||
### D8. A partial index for the filter
|
||||
|
||||
`mediaNode_status_createdAt_id` (`{ mediaNode: 1, status: 1, createdAt: -1, _id: -1 }`,
|
||||
`partialFilterExpression: { mediaNode: { $exists: true } }`) replaces `nodeId_status_createdAt_id`. An equality on
|
||||
`mediaNode` implies the partial filter, so the planner can use it. CE databases, with no label, keep it empty.
|
||||
|
||||
## Data model
|
||||
|
||||
| Collection | Field | Type | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `nodes` | `mediaNode` | string, 1-128 chars of `[A-Za-z0-9_.-]` | optional, present when the server has a label |
|
||||
| `nodes` | `stoppedAt` | date | optional, set by a graceful stop |
|
||||
| `rooms` | `mediaNode` | string, as above | optional, follows `nodeId` |
|
||||
| `participants` | `mediaNode` | string, as above | optional, follows `nodeId` |
|
||||
| `analytics_*` | `scope` | `global` or `node:<label>` | node scope only from labelled servers |
|
||||
|
||||
- **Characters:** the label's character set is the one `ANALYTICS_SCOPE_PATTERN` accepts, so every label is a valid
|
||||
scope.
|
||||
- **Indexes:** `rooms.mediaNode_status_createdAt_id` (partial) is added, and `rooms.nodeId_status_createdAt_id` leaves
|
||||
the contract. `Initialize` does not drop it, so a database written before the change keeps it until someone drops it
|
||||
by hand.
|
||||
- **Retention:** unchanged. `nodes` keeps its 30-day TTL and the stamped fields live with their entity.
|
||||
- **Schema version:** no bump, since the v2 schema has not shipped.
|
||||
- **Legacy migration:** legacy events carry no label, so migrated rooms and sessions get no `mediaNode`, and their
|
||||
rollups get only `global`.
|
||||
|
||||
## API
|
||||
|
||||
- **`GET /api/v1/status`:** `writer: { lastSeenAt?: date-time, stoppedAt?: date-time, batchIntervalMs?: integer }`.
|
||||
- It still reads `schema_meta` and `nodes` whole.
|
||||
- **`GET /api/v1/rooms`:**
|
||||
- Takes `mediaNode` (string, 1-128) instead of `nodeId`.
|
||||
- Items add `mediaNode?: string`.
|
||||
- It runs on `mediaNode_status_createdAt_id`, or on `nameLower_createdAt` with `mediaNode` as a residual filter when
|
||||
`q` is a prefix.
|
||||
- **`GET /api/v1/participants`, `GET /api/v1/agents`:** items add `mediaNode?: string`. Queries are unchanged.
|
||||
- **`GET /api/v1/analytics/summary`:** the coverage query of a `node:<label>` scope adds `mediaNode: <label>` to the
|
||||
`nodes` filter. It still runs on `lastSeenAt_ttl`, on a collection of tens of documents.
|
||||
|
||||
## Performance budget
|
||||
|
||||
- Rooms list with `mediaNode`:
|
||||
- the existing budget of `rooms-api` applies: p95 within 300 ms for ranked sorts over 7 days at scenario M;
|
||||
- verified with explain plans on a database generated with `--media-nodes`, with `IXSCAN` on the new index and
|
||||
`totalDocsExamined` within the documented bounds.
|
||||
- Status and summary:
|
||||
- no new query, and the `nodes` reads stay bounded by the process documents of 30 days;
|
||||
- verified by the existing command-monitoring and plan tests.
|
||||
- Writer: one extra field per heartbeat and stamp, half the rollup upserts on unlabelled servers, and one node upsert
|
||||
at shutdown. The existing benchmarks cover it.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [A label changes when an edition's machine identity changes, for example a new IP] → the dashboard sees a new media
|
||||
node. The edition documents its rule.
|
||||
- [Shutdown budget exhausted before `stoppedAt`] → the node reads as silent. The queues have priority, and the upsert
|
||||
is one small write.
|
||||
- [Pre-change databases keep `node:<nodeId>` scopes and the old index] → editions list only label scopes. The scopes
|
||||
expire with the rollup retention, and the index costs space on development databases only.
|
||||
- [Rooms between a restart and the new process's first event keep the old label] → that is the same machine, so the
|
||||
label is right by design.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
It ships with the v2 writer and dashboard of 3.10. On rollback, dashboards that do not know the fields ignore them and
|
||||
writers that do not know them never write them.
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
The node the analytics know is the server process: its id changes on every restart, and a `nodes` document lives 30
|
||||
days after its last batch. The dashboard therefore lists one "node" per restart, warns "1 of 4 nodes reporting" on a
|
||||
single machine that restarted three times, and can tell no one which machine hosts a room. A node that stopped on
|
||||
purpose (scale-in, maintenance) looks the same as one that crashed. Single-machine deployments, the only ones CE runs
|
||||
in, have nothing to show per node at all, yet CE renders node columns, filters and a locked per-node card.
|
||||
|
||||
## What Changes
|
||||
|
||||
- The server can give the writer a **media node label**: a name of the machine that stays the same across restarts.
|
||||
The CE server sets none; an edition that runs several media nodes sets it. With a label, the writer:
|
||||
- publishes it as `mediaNode` on the node heartbeat;
|
||||
- stamps `mediaNode` on the rooms and participant sessions the node hosts, following `nodeId` as `mediaStats` does,
|
||||
so the label lives as long as the entity;
|
||||
- keys the node scope of the rollups by it (`node:<label>`), and the reconciler's per-node daily unique identities
|
||||
too.
|
||||
- **BREAKING** (unreleased contract): the rollups get no node scope from a server without a label. Today every server
|
||||
writes `node:<nodeId>` beside `global`.
|
||||
- On a graceful stop, after the final flush, the writer records `stoppedAt` on its node document, so a stopped server
|
||||
reads as stopped instead of silent.
|
||||
- **BREAKING** (unreleased API): `GET /api/v1/status` reports the writer as `{ lastSeenAt?, stoppedAt?,
|
||||
batchIntervalMs? }`, without `nodes` and `aliveNodes`. `stoppedAt` is present when no node is alive and the newest
|
||||
one stopped.
|
||||
- The rooms list filters by `mediaNode` (exact match) instead of `nodeId`, backed by a `mediaNode` index that replaces
|
||||
the `nodeId` one. Room, participant and agent list items add `mediaNode`.
|
||||
- The Analytics media statistics coverage of a node scope reads the node documents of that media node.
|
||||
- CE dashboard: nothing about nodes is rendered without `per-node-breakdown`, not even locked:
|
||||
- Overview: no "N of M nodes reporting" line and no "By node" card, so Recent rooms takes the full width. The writer
|
||||
line reads "Server stopped at hh:mm:ss" with a neutral icon when the server stopped.
|
||||
- Rooms: no Node select. The URL parameter `node` replaces `nodeId` and CE ignores it.
|
||||
- Analytics: no scope selector.
|
||||
- The Node columns of the Rooms, Participants, Agents and participant sessions tables exist only with the feature,
|
||||
and show `mediaNode`.
|
||||
- The data generator gains `--media-nodes`: it labels its nodes, keys node scopes by the labels and adds the earlier,
|
||||
stopped process of one of them, so a restart shows up.
|
||||
|
||||
Scope excludes the following:
|
||||
- Which editions set a label, its format and the per-node views: the edition that unlocks `per-node-breakdown` owns
|
||||
them.
|
||||
- The node an egress or ingress process runs on (only rooms and participant sessions are labelled).
|
||||
- Reconciler liveness: it keeps judging the process (`nodeId` and the node registry), since a room lives in one
|
||||
process, whatever machine it ran on.
|
||||
- Backfilling labels on documents written before the change.
|
||||
|
||||
Edition: CE and PRO. The writer behaviour applies to every edition, and the views are CE views that every edition
|
||||
shows. Repositories:
|
||||
- openvidu-livekit: `openvidu/analytics`, `openvidu/openviduconfig`, `openvidu/openvidu.go`;
|
||||
- the dashboard: typings and contract, backend, frontend, data generator, demo, specs.
|
||||
|
||||
CHANGELOG lines:
|
||||
- `CHANGELOG.md`: the Overview line drops "and nodes reporting", and the graceful shutdown line adds that the node
|
||||
records when it stopped.
|
||||
- `CHANGELOG-openvidu.md` (openvidu-livekit): the data model line mentions that the heartbeat records a graceful stop.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
None.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `server-analytics-writer`:
|
||||
- adds the media node label (heartbeat, entity stamp, node scopes);
|
||||
- the graceful shutdown records `stoppedAt`;
|
||||
- stats deltas go to the labelled node scope.
|
||||
- `server-reconciler`: daily unique identities per media node label.
|
||||
- `data-model`:
|
||||
- `mediaNode` on `nodes`, `rooms` and `participants`, and `stoppedAt` on `nodes`;
|
||||
- node scopes keyed by the label;
|
||||
- the generator's `--media-nodes`.
|
||||
- `status-api`: the writer object without node counts, with `stoppedAt`.
|
||||
- `overview-ui`: no node line or card without `per-node-breakdown`, the full-width recent rooms, and the stopped
|
||||
writer line.
|
||||
- `ui-info-tips`: the Data pipeline tip no longer mentions nodes.
|
||||
- `rooms-api`:
|
||||
- the `mediaNode` filter and index replace `nodeId`;
|
||||
- the item carries `mediaNode`.
|
||||
- `rooms-ui`:
|
||||
- the `node` URL parameter;
|
||||
- no Node select or column without the feature.
|
||||
- `participants-api`: list items carry `mediaNode`.
|
||||
- `participants-ui`: the Node column only with the feature, showing `mediaNode`.
|
||||
- `agents-ui`: the Node column only with the feature, showing `mediaNode`.
|
||||
- `analytics-api`: the coverage of a node scope follows `mediaNode`.
|
||||
- `analytics-ui`: no scope selector without the feature.
|
||||
- `ui-shell`: `node` replaces `nodeId` among the URL parameters.
|
||||
|
||||
## Impact
|
||||
|
||||
- Writer:
|
||||
- an `openviduconfig.MediaNode` setting passed through `analytics.Node` to the heartbeat, the projection options
|
||||
and the rollup scopes;
|
||||
- the shutdown path writes `stoppedAt`;
|
||||
- the reconciler's daily unique identities group by `mediaNode`;
|
||||
- the contract schemas of `nodes`, `rooms` and `participants`, and the rooms index;
|
||||
- unit and integration tests in `openvidu/analytics`.
|
||||
- Dashboard:
|
||||
- typings and their schema mirror;
|
||||
- status service;
|
||||
- rooms query, mappers and index;
|
||||
- participant and agent mappers;
|
||||
- overview repository (groups by `mediaNode`, for editions);
|
||||
- analytics coverage;
|
||||
- Overview, Rooms, Participants, Agents and Analytics pages;
|
||||
- the lock catalogue;
|
||||
- the data generator and demo;
|
||||
- unit, integration, contract and e2e tests.
|
||||
- Editions that extend the per-node seams (Overview card, Rooms node select, Analytics scope select) read `mediaNode`
|
||||
and the `node` URL parameter.
|
||||
- No schema version bump: the v2 schema has not shipped.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Agents table
|
||||
Edition: CE. The table SHALL show, in order, SID (participant detail link with copy button), Identity (copy button),
|
||||
Name, Room (link to the room detail), Status (stale cue), Joined, Left, Duration (live), Published (source icons) and,
|
||||
with `per-node-breakdown` only, Node (`mediaNode`, hidden by default), with times formatted as on the other lists and the time-zone header hint; Joined (the
|
||||
default), Left and Duration are sortable; the cursor paginator and the shared skeleton, error and empty states apply.
|
||||
|
||||
#### Scenario: Agent row
|
||||
- **GIVEN** an active agent `agent-1` (session `PA_z9`, name "Support bot") in `demo-room` publishing a microphone
|
||||
- **WHEN** the row renders
|
||||
- **THEN** the cells read, in order, `PA_z9` once, linking to `/participants/PA_z9` with its copy button, `agent-1` with its copy button, "Support bot", `demo-room` linking to `/rooms/RM_abc`, "active", the join time, "—", a duration counting up live and the microphone source icon, and, with `features: []`, no Node column is offered
|
||||
|
||||
#### Scenario: Detail links in CE
|
||||
- **GIVEN** the CE dashboard with `features: []`
|
||||
- **WHEN** the user follows the SID link `PA_z9` or the room link `demo-room` of an agent row
|
||||
- **THEN** `/participants/PA_z9` or `/rooms/RM_abc` renders the full-page lock of its detail, as from the Participants and Rooms lists
|
||||
|
||||
#### Scenario: No agents
|
||||
- **GIVEN** `?status=active&q=zzz`
|
||||
- **WHEN** the list is empty
|
||||
- **THEN** the empty state reads "No agents match" with the hint "Change the filters or widen the time range.", "Clear filters" (drops `q`, `status` and `room`) and "Widen range" (opens the range picker)
|
||||
|
||||
#### Scenario: Dates as on the other lists
|
||||
- **GIVEN** today is 2026-10-18 in the time zone `Europe/Madrid`, an agent that joined today at 12:31:05 and one that joined yesterday at 09:00:00
|
||||
- **WHEN** the rows render with the default 24 h range
|
||||
- **THEN** the Joined cells read "12:31:05" and "2026-10-17 09:00:00", and the Joined and Left headers carry the time-zone hint `Europe/Madrid`
|
||||
|
||||
#### Scenario: Stale agent
|
||||
- **GIVEN** an active agent session that the list response marks `stale: true`
|
||||
- **WHEN** the row renders
|
||||
- **THEN** its status badge carries the stale cue, judged by the API and never by the browser clock
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
## MODIFIED 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 document, a `node:<label>` scope the documents with that `mediaNode`.
|
||||
|
||||
#### 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, labelled `mn-a` (`mediaStats: false`) and `mn-b` (`mediaStats: true`), and
|
||||
`scope=node:mn-b`
|
||||
- **WHEN** the summary is requested
|
||||
- **THEN** the body has no `mediaStats` property
|
||||
|
||||
#### Scenario: Restarts of one media node
|
||||
|
||||
- **GIVEN** two node documents with `mediaNode: "mn-a"` seen during the range, the earlier with `mediaStats: true` and
|
||||
the later with `mediaStats: false`, and `scope=node:mn-a`
|
||||
- **WHEN** the summary is requested
|
||||
- **THEN** the body has `mediaStats: "partial"`
|
||||
|
||||
#### 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
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Scope selector only with per-node breakdown
|
||||
|
||||
Edition: CE. Without `per-node-breakdown` the page header SHALL show no scope selector, lock or text about nodes and
|
||||
send no `/analytics/scopes` request; with it, it renders the scope selector the edition provides, whose node options
|
||||
are `node:<label>` scopes.
|
||||
|
||||
#### Scenario: Nothing per node in CE
|
||||
|
||||
- **GIVEN** `features` without `per-node-breakdown`
|
||||
- **WHEN** the header renders
|
||||
- **THEN** it has the range, time zone and granularity controls and no scope selector or lock, and no
|
||||
`/analytics/scopes` request is sent
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Per-node scope selector
|
||||
**Reason**: CE has a single machine to analyse; CE stops showing the selector locked.
|
||||
**Migration**: "Scope selector only with per-node breakdown" renders the selector only for an edition that provides it.
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Generated media nodes
|
||||
Edition: CE and PRO. With `--media-nodes` the data generator SHALL label its nodes `media-node-10-0-0-1` to `-3`,
|
||||
stamp the label on the rooms and sessions of each node, key the node rollup scopes (`--node-rollups`) by it, and add
|
||||
for the first label an earlier, stopped process: a node document whose `lastSeenAt` and `stoppedAt` precede the
|
||||
current one's `firstSeenAt`, host of some older rooms. Without it nothing carries a label and `--node-rollups` is
|
||||
rejected.
|
||||
|
||||
#### Scenario: Labelled generated database
|
||||
- **GIVEN** a database generated with `--media-nodes --node-rollups`
|
||||
- **WHEN** `nodes`, `rooms` and `analytics_hourly` are read
|
||||
- **THEN** there are four node documents for three labels, two of them with `mediaNode: "media-node-10-0-0-1"` and
|
||||
only the earlier one with `stoppedAt`, every room has the `mediaNode` of its node document, and every node scope of
|
||||
the rollups is `node:media-node-10-0-0-<n>`
|
||||
|
||||
#### Scenario: Keep-alive and the stopped process
|
||||
- **GIVEN** that database kept live by the generator's keep-alive
|
||||
- **WHEN** `nodes` is read ten minutes later
|
||||
- **THEN** the three current documents have a fresh `lastSeenAt` and the stopped one keeps its `lastSeenAt` and
|
||||
`stoppedAt`
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Room document
|
||||
Edition: CE and PRO. A `rooms` document SHALL describe one room session: identity and lifecycle, node and media 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`, `mediaNode` (only when the server that hosts it has a media node label), `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, node and media node, 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`, `mediaNode` (only when the server that hosts it has a media node label), `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: Rollups
|
||||
Edition: CE and PRO. `analytics_5min`, `analytics_hourly` and `analytics_daily` SHALL hold one document per
|
||||
scope (`global`, and `node:<label>` for the media node label of a labelled server) and bucket with pre-aggregated counters, so that long ranges never read
|
||||
entity or stats documents. Map keys SHALL be enum names or a fixed vocabulary, never user input. Every
|
||||
participant counter SHALL count STANDARD sessions only, except `participants.byKind` and the 5-minute
|
||||
`participants.connected` (participants with stats), which count every kind.
|
||||
|
||||
#### Scenario: Fields of the rollup documents
|
||||
- **WHEN** hourly or daily rollup documents are read
|
||||
- **THEN** they have counters for rooms (created, ended, by end reason, `durationMsSum`, `participantsSum`), participants (joined, active, aborted, left, by kind, SDK, OS, browser, connection type, disconnect reason, connect time, minutes), tracks, egress (started, by type, by end status, duration), ingress sessions (started, ended, started by input type, duration), bytes, quality (score sum/count, poor, lost), API calls and API errors (total and by method), and daily documents also `uniqueIdentities` (set by the server's reconciler once the UTC day has closed); 5-minute documents have bytes, quality, `participants.connected` (distinct participants with stats), the `PARTICIPANT_ACTIVE` count and the lifecycle counters (rooms created/ended, participants joined/left/aborted, egress started/ended)
|
||||
|
||||
#### Scenario: Five-minute bandwidth
|
||||
- **GIVEN** stats flushed between 10:00 and 10:05 UTC
|
||||
- **WHEN** `analytics_5min` is read for `global:…T10:00:00Z`
|
||||
- **THEN** `bytes.up` and `bytes.down` equal the sum of the flushed upstream and downstream bytes and `quality.scoreCount` equals the number of samples
|
||||
|
||||
#### Scenario: Hourly counters
|
||||
- **GIVEN** five rooms created and two ended between 10:00 and 11:00 UTC on a server with the media node label `mn-a`
|
||||
- **WHEN** the events are processed
|
||||
- **THEN** `analytics_hourly` has `global:2026-…T10:00:00Z` and `node:mn-a:2026-…T10:00:00Z` with `rooms.created: 5` and `rooms.ended: 2`
|
||||
|
||||
#### Scenario: Replayed batch does not double count
|
||||
- **GIVEN** a batch whose raw events were already inserted
|
||||
- **WHEN** the batch is processed again
|
||||
- **THEN** rollup counters are unchanged
|
||||
|
||||
#### Scenario: Services in rollups
|
||||
- **GIVEN** between 10:00 and 11:00 UTC three STANDARD, one EGRESS and one AGENT participant joined, and the AGENT left after 10 minutes
|
||||
- **WHEN** the hourly rollup is read
|
||||
- **THEN** `participants.joined` is 3, `participants.left` is 0, `participants.minutes` counts only the STANDARD sessions and `participants.byKind` is `{ STANDARD: 3, EGRESS: 1, AGENT: 1 }`
|
||||
|
||||
#### Scenario: Ingress sessions by input type
|
||||
- **GIVEN** two RTMP and one WHIP ingress sessions started between 10:00 and 11:00 UTC
|
||||
- **WHEN** the hourly rollup is read
|
||||
- **THEN** it has `ingress.sessionsStarted: 3` and `ingress.byInputType: { rtmp: 2, whip: 1 }`
|
||||
|
||||
### Requirement: Nodes
|
||||
Edition: CE and PRO. The `nodes` collection SHALL hold one document per server process (its node id: a restart
|
||||
writes a new one) with `firstSeenAt`, `lastSeenAt`, `serverVersion`, `region`, `batchIntervalMs` (int32 milliseconds,
|
||||
at least 1 000), `mediaStats`, the optional `mediaNode` and `stoppedAt`, and SHALL expire it 30 days after its last
|
||||
heartbeat. 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
|
||||
|
||||
#### Scenario: Restarted labelled server
|
||||
- **GIVEN** a server with the media node label `mn-a` that stopped gracefully and started again
|
||||
- **WHEN** `nodes` is read
|
||||
- **THEN** it holds two documents with `mediaNode: "mn-a"`: the earlier one with `stoppedAt`, the new one with a later
|
||||
`firstSeenAt` and no `stoppedAt`
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Per-node card only with per-node breakdown
|
||||
|
||||
Edition: CE. Without `per-node-breakdown` the Overview SHALL render no per-node card, lock or request of its own; with
|
||||
it, the "By node" card next to the recent rooms table holds the per-node content the edition provides.
|
||||
|
||||
#### Scenario: Nothing per node in CE
|
||||
- **GIVEN** `features` without `per-node-breakdown`
|
||||
- **WHEN** the page renders
|
||||
- **THEN** no "By node" card, lock or text about nodes is shown, and the page requests nothing besides `GET /api/v1/overview` and the shell's status poll
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Overview route and layout
|
||||
Edition: CE. `/overview` SHALL read only `tz` from the URL, show the page title, the live chip and the refresh
|
||||
control in its header, and render, in order: live tiles (balanced tile rows), the today card next to the server status
|
||||
card, and the recent rooms table, next to the per-node card when there is one and at full width otherwise.
|
||||
|
||||
#### Scenario: Default landing
|
||||
- **GIVEN** an authenticated user without a stored default zone, in a browser whose zone is Europe/Madrid
|
||||
- **WHEN** the application opens `/`
|
||||
- **THEN** it redirects to `/overview?tz=Europe/Madrid`, requests `GET /api/v1/overview?tz=Europe/Madrid` and shows the header with "LIVE", "updated hh:mm:ss" and the interval select
|
||||
|
||||
#### Scenario: Phone width
|
||||
- **WHEN** the viewport is narrower than 600 px
|
||||
- **THEN** tiles render two per row, the cards stack and the recent rooms table switches to cards
|
||||
|
||||
#### Scenario: Recent rooms at full width
|
||||
- **GIVEN** `features` without `per-node-breakdown` and a viewport 1440 px wide
|
||||
- **WHEN** the page renders
|
||||
- **THEN** the recent rooms card spans the width of the today and server status cards together
|
||||
|
||||
### Requirement: Server status card
|
||||
Edition: CE. A "Data pipeline" card SHALL show, from the last server status, the writer line, the migration progress
|
||||
bar only while it runs, and a "Storage and retention" link to `/settings?tab=about`; it shows a skeleton until the
|
||||
first status and updates with the status poll. The writer line reads "Server stopped at hh:mm:ss" with a neutral icon
|
||||
when `writer.stoppedAt` is set, "Last batch: never" with a warning icon when no batch was seen, and otherwise "Last
|
||||
batch at hh:mm:ss" with a check icon when fresh, as for the live chip, a warning icon if not.
|
||||
|
||||
#### Scenario: Healthy card
|
||||
- **GIVEN** a status with the writer batch 4 s before `generatedAt` at 12:31:05 and migration `completed`
|
||||
- **WHEN** the card renders
|
||||
- **THEN** it reads "Data pipeline" and "Last batch at 12:31:05" with the check icon, has no line about nodes, and shows no migration progress
|
||||
|
||||
#### Scenario: Server stopped
|
||||
- **GIVEN** a status with `writer.stoppedAt` and `writer.lastSeenAt` at 12:00:04, 10 minutes before `generatedAt`
|
||||
- **WHEN** the card renders
|
||||
- **THEN** the writer line reads "Server stopped at 12:00:04" with the neutral icon, not the warning icon
|
||||
|
||||
#### Scenario: Server silent without a stop
|
||||
- **GIVEN** a status whose writer batch is 10 minutes old and that has no `writer.stoppedAt`
|
||||
- **WHEN** the card renders
|
||||
- **THEN** the writer line reads "Last batch at hh:mm:ss" with the warning icon
|
||||
|
||||
#### Scenario: Migration running
|
||||
- **GIVEN** a status with `migration` `{ state: "running", progress: { processed: 400000, total: 1200000 } }`
|
||||
- **WHEN** the card renders
|
||||
- **THEN** a determinate progress bar at 33 % with "400,000 / 1,200,000" is shown and updates with every status response, every 10 s
|
||||
|
||||
#### Scenario: Auto-refresh off
|
||||
- **GIVEN** the auto-refresh set to Off
|
||||
- **WHEN** a new status reports a newer writer batch
|
||||
- **THEN** the writer line shows the new batch time while the tiles keep the last overview response
|
||||
|
||||
#### Scenario: Before the first status
|
||||
- **GIVEN** the page opened and no server status received yet
|
||||
- **WHEN** the card renders
|
||||
- **THEN** it shows a skeleton of its lines and no "Loading" text
|
||||
|
||||
#### Scenario: Slow writer batches
|
||||
- **GIVEN** a status with `writer.batchIntervalMs: 60000` and the writer batch 80 s before `generatedAt`
|
||||
- **WHEN** the card renders
|
||||
- **THEN** the writer line shows the check icon
|
||||
|
||||
### Requirement: Fresh deployment empty state
|
||||
Edition: CE. When the server status reports `schema.status` `uninitialized`, or `recentRooms` is empty with every live
|
||||
and today counter at `0`, the page SHALL replace the recent rooms table with the empty state "Waiting for the first
|
||||
events" and a hint that the server writes analytics once a room is created, keeping tiles at 0 and the status card
|
||||
visible.
|
||||
|
||||
#### Scenario: Empty database
|
||||
- **GIVEN** a fresh deployment with no room ever created
|
||||
- **WHEN** the page renders
|
||||
- **THEN** the empty state reads "Waiting for the first events", every tile reads 0, the status card shows "Last batch: never" with the warning icon, and no error banner is shown
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Per-node cards
|
||||
**Reason**: CE runs only in single-machine deployments, where a per-node card says nothing; CE stops showing it locked.
|
||||
**Migration**: "Per-node card only with per-node breakdown" renders the card only for an edition that provides it.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Participant list item shape
|
||||
|
||||
Edition: CE. Each item of the participants and agents lists SHALL carry `sid`, `identity`, `kind`, `status`, `roomSid`,
|
||||
`roomName`, `joinedAt`, `isPublisher`, `publishedSources`, the optional `name`, `leftAt`, `durationMs`,
|
||||
`connectionType`, `nodeId`, `mediaNode`, the `stale` flag that `api-conventions` defines and a `client` summary, with unknown values
|
||||
omitted as `api-conventions` requires.
|
||||
|
||||
#### Scenario: Complete row
|
||||
- **GIVEN** a left session of `alice` from the JS SDK 2.6.1 on Chrome 130 / macOS 14 over UDP
|
||||
- **WHEN** it is listed
|
||||
- **THEN** the item has `status: "left"`, `leftAt`, `durationMs`, `connectionType: "UDP"` and `client: { sdk: "JS", version: "2.6.1", browser: "Chrome", browserVersion: "130", os: "macOS", osVersion: "14" }`
|
||||
|
||||
#### Scenario: No client information
|
||||
- **GIVEN** a session whose `PARTICIPANT_JOINED` carried no `client_info`
|
||||
- **WHEN** it is listed
|
||||
- **THEN** the item has no `client` and no `connectionType` property
|
||||
|
||||
#### Scenario: Published sources
|
||||
- **GIVEN** a session whose `tracks.bySource` is `{ CAMERA: 2, MICROPHONE: 1 }`, and a session that never published
|
||||
- **WHEN** both are listed
|
||||
- **THEN** the first item has `publishedSources` with `CAMERA` and `MICROPHONE` and no other source, and the second has `publishedSources: []`
|
||||
|
||||
#### Scenario: Stale active session
|
||||
- **GIVEN** the default writer batch interval of 10 s and an active session whose `lastStatsAt` is 100 s old at the server's clock
|
||||
- **WHEN** it is listed
|
||||
- **THEN** the item has `stale: true`, and a left session with the same `lastStatsAt` has no `stale` property
|
||||
|
||||
#### Scenario: Session of a labelled node
|
||||
- **GIVEN** a session document with `nodeId: "ND_x"` and `mediaNode: "mn-a"`, and one without `mediaNode`
|
||||
- **WHEN** both are listed
|
||||
- **THEN** the first item has `nodeId: "ND_x"` and `mediaNode: "mn-a"`, and the second has no `mediaNode` property
|
||||
|
||||
#### Scenario: Sources and client summary
|
||||
- **WHEN** an item of the participants or agents list is returned
|
||||
- **THEN** `publishedSources` holds the track sources with at least one publication in `tracks.bySource` (empty when
|
||||
none) and `client` carries `sdk`, `version`, `os`, `browser`, `browserVersion` and `osVersion` when known
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Participants table
|
||||
Edition: CE. The table SHALL show, in order, the columns SID (link to the detail with a copy button), Identity (copy
|
||||
button), Name, Room (link to the room detail), Kind (kind badge), Status (status badge with the stale cue), Joined,
|
||||
Left, Duration (live), Published (source icons) and, with `per-node-breakdown` only, Node (`mediaNode`, hidden by
|
||||
default); the sortable headers are Joined
|
||||
(`joinedAt`, the default), Left (`leftAt`) and Duration (`duration`); links never trigger on row click.
|
||||
|
||||
#### Scenario: Row rendering
|
||||
- **GIVEN** an active STANDARD session `PA_x1y2` of `alice` in `demo-room` publishing a camera and a microphone
|
||||
- **WHEN** the row renders
|
||||
- **THEN** the cells read, in order, `PA_x1y2` once, linking to `/participants/PA_x1y2` with its copy button, `alice` with its copy button, the name, `demo-room` linking to `/rooms/RM_abc`, the Standard kind badge, "active", the join time, "—", a duration counting up live and the camera and microphone source icons
|
||||
|
||||
#### Scenario: Sort toggles order
|
||||
- **GIVEN** the default `joinedAt` descending
|
||||
- **WHEN** the user activates the Joined header
|
||||
- **THEN** the URL gets `sort=joinedAt&order=asc` without `cursor`
|
||||
|
||||
#### Scenario: Nothing published
|
||||
- **GIVEN** a session with empty `publishedSources`
|
||||
- **WHEN** the row renders
|
||||
- **THEN** the Published cell reads "—"
|
||||
|
||||
#### Scenario: Node column
|
||||
- **GIVEN** `features` with `per-node-breakdown`
|
||||
- **WHEN** the table renders with the default columns
|
||||
- **THEN** the Node column is hidden, the Columns menu offers it, and once shown it reads the session's `mediaNode`,
|
||||
or "—" without one
|
||||
|
||||
#### Scenario: No Node column in CE
|
||||
- **GIVEN** `features` without `per-node-breakdown`
|
||||
- **WHEN** the table renders
|
||||
- **THEN** it has no Node column and the Columns menu does not offer one
|
||||
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Rooms list endpoint
|
||||
|
||||
Edition: CE. `GET /api/v1/rooms` SHALL list the room sessions created in the range with the common query contract, the
|
||||
filters `status`, `q`, `features` and `mediaNode`, the sort keys `createdAt` (default, index-backed) and the ranked
|
||||
`endedAt`, `duration`, `uniqueIdentities` and `active`, `order` (`desc` default, `asc`), and the list envelope
|
||||
`{ items, nextCursor, prevCursor, total? }` with `total` under the `DASHBOARD_LIST_TOTAL_MAX_RANGE` rule.
|
||||
|
||||
#### Scenario: Default list
|
||||
|
||||
- **GIVEN** 40 rooms created in the last 24 hours and 5 older ones
|
||||
- **WHEN** the client requests `GET /api/v1/rooms` with a valid token
|
||||
- **THEN** the response is `200` with the 25 most recently created rooms of the last 24 hours, `nextCursor` set,
|
||||
`prevCursor: null` and `total: 40`
|
||||
|
||||
#### Scenario: Cursor walk
|
||||
|
||||
- **GIVEN** 60 rooms in range and `limit=25`
|
||||
- **WHEN** the client follows `nextCursor` twice and then `prevCursor` once
|
||||
- **THEN** the pages hold rooms 1-25, 26-50, 51-60 and 26-50 with no gaps or duplicates, each page ordered by
|
||||
`createdAt desc` with `_id desc` as tie-breaker
|
||||
|
||||
#### Scenario: Ranked cursor walk
|
||||
|
||||
- **GIVEN** 60 rooms created in range, two of them active, and `sort=duration&limit=25`
|
||||
- **WHEN** the client follows `nextCursor` twice
|
||||
- **THEN** the pages hold rooms 1-25, 26-50 and 51-60 of the duration ranking at the first page's `asOf`, with `total:
|
||||
60` on each page and no gaps or duplicates
|
||||
|
||||
#### Scenario: Common query contract
|
||||
|
||||
- **WHEN** the client requests `GET /api/v1/rooms`
|
||||
- **THEN** the common query contract it accepts is `from`, `to`, `tz`, `limit` (1-100, default 25) and an opaque
|
||||
`cursor`
|
||||
|
||||
### Requirement: Room list item
|
||||
|
||||
Edition: CE. Each rooms list item SHALL be the room document's public projection: identity, lifecycle, node, media 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?`, `mediaNode?`, `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 labelled node
|
||||
|
||||
- **GIVEN** a room document with `nodeId: "ND_x"` and `mediaNode: "mn-a"`
|
||||
- **WHEN** it is returned
|
||||
- **THEN** it has `nodeId: "ND_x"` and `mediaNode: "mn-a"`, and an item of a room without the label has no
|
||||
`mediaNode` 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: Name prefix search and SID lookup
|
||||
|
||||
Edition: CE. The rooms list SHALL treat `q` as a case-insensitive prefix over the room name when it has at least two
|
||||
characters, combined with `status`, `features` and `mediaNode`, SHALL reject a one-character `q` with `400`, and SHALL
|
||||
treat a value matching `^RM_[A-Za-z0-9]+$` as an exact SID lookup that ignores `from`, `to`, `status`, `features`,
|
||||
`mediaNode` and `sort` and returns at most one item.
|
||||
|
||||
#### Scenario: Prefix search
|
||||
|
||||
- **GIVEN** rooms named `Demo-Room`, `demo-2` and `my-demo` created in range
|
||||
- **WHEN** the client sends `?q=DE`
|
||||
- **THEN** `items` holds `Demo-Room` and `demo-2` ordered by `createdAt desc`, and not `my-demo`
|
||||
|
||||
#### Scenario: One character
|
||||
|
||||
- **WHEN** the client sends `?q=d`
|
||||
- **THEN** the response is `400` with code `VALIDATION_ERROR`, `details.issues[0].path` equal to `["q"]` and a message
|
||||
stating the two-character minimum
|
||||
|
||||
#### Scenario: Exact SID outside the range
|
||||
|
||||
- **GIVEN** a closed room `RM_ab12cd` created 40 days ago
|
||||
- **WHEN** the client sends `?q=RM_ab12cd&from=now-24h&status=active`
|
||||
- **THEN** the response is `200` with that single room in `items`, `total: 1` and both cursors `null`
|
||||
|
||||
#### Scenario: Prefix within one node
|
||||
|
||||
- **GIVEN** rooms named `demo-a` with `mediaNode: "mn-a"` and `demo-b` with `mediaNode: "mn-b"`, both created in range
|
||||
- **WHEN** the client sends `?q=demo&mediaNode=mn-a`
|
||||
- **THEN** `items` holds `demo-a` only
|
||||
|
||||
### Requirement: Feature and node filters
|
||||
|
||||
Edition: CE. The rooms list SHALL accept `features` as a comma-separated subset of `egress`, `ingress`, `agent`, `sip`
|
||||
and return rooms whose flag is set for any of the listed values, SHALL accept `mediaNode` as an exact match on the room's
|
||||
media node label, and SHALL reject an unknown feature value with `400`.
|
||||
|
||||
#### Scenario: Rooms with egress or ingress
|
||||
|
||||
- **GIVEN** a room with an egress, a room with an ingress session and a plain room
|
||||
- **WHEN** the client sends `?features=egress,ingress`
|
||||
- **THEN** the first two rooms are returned and the plain room is not
|
||||
|
||||
#### Scenario: Rooms of one node
|
||||
|
||||
- **WHEN** the client sends `?mediaNode=mn-a&status=active`
|
||||
- **THEN** every item has `mediaNode: "mn-a"` and `status: "active"`
|
||||
|
||||
#### Scenario: Label of a CE server
|
||||
|
||||
- **GIVEN** rooms without `mediaNode`
|
||||
- **WHEN** the client sends `?mediaNode=mn-a`
|
||||
- **THEN** the response is `200` with no items
|
||||
|
||||
#### Scenario: Unknown feature
|
||||
|
||||
- **WHEN** the client sends `?features=egress,webhook`
|
||||
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["features"]`
|
||||
|
||||
### Requirement: Index-backed room queries
|
||||
|
||||
Edition: CE. Every query of this capability SHALL run on an index of the shared contract
|
||||
(`dashboard-ce/typings/contract/indexes.ts`), or on `_id` for the hourly rollups the long-range summary reads: no
|
||||
winning plan contains a `COLLSCAN`, and `totalDocsExamined` is bounded
|
||||
by the page size plus the documented residual filters for the index-ordered pages and by the rooms created in the range
|
||||
for the ranked sorts.
|
||||
|
||||
#### Scenario: Explain plans on the generated database
|
||||
|
||||
- **GIVEN** a database generated by the data generator at scenario M
|
||||
- **WHEN** the list (every filter combination and sort) and summary queries are explained
|
||||
- **THEN** each winning plan uses an index of the shared contract and none contains `COLLSCAN`
|
||||
|
||||
#### Scenario: Ranked sorts over 7 days
|
||||
|
||||
- **GIVEN** a database generated at scenario M for 7 days
|
||||
- **WHEN** the list is explained with each ranked sort key, with and without `status`, `q`, `mediaNode` and `features`
|
||||
- **THEN** each plan scans the range index of its filters (`createdAt_id`, `status_createdAt_id`,
|
||||
`nameLower_createdAt`, `mediaNode_status_createdAt_id` or the feature indexes), `totalDocsExamined` is at most the rooms
|
||||
created in the range, and the p95 of a page is within 300 ms
|
||||
|
||||
#### Scenario: Name prefix within one node
|
||||
|
||||
- **WHEN** the list with `q` and `mediaNode` is explained
|
||||
- **THEN** the plan uses `nameLower_createdAt` with `mediaNode` as a residual filter
|
||||
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Rooms page URL state
|
||||
|
||||
Edition: CE. `/rooms` SHALL read its whole state from the query parameters `status`, `q`, `features`, `node`,
|
||||
`from`, `to`, `tz`, `sort`, `order`, `cursor` and `limit` as the shell's URL-state rules describe: defaults
|
||||
`status=all`, `sort=createdAt`, `order=desc`, the global range and the stored page size; the range always applies to
|
||||
the start time; invalid values fall back silently; any change of filter, search, range, sort or page size drops
|
||||
`cursor` and creates a history entry.
|
||||
|
||||
#### Scenario: Deep link reproduces the view
|
||||
|
||||
- **GIVEN** `/rooms?status=closed&q=demo&features=egress,agent&from=now-7d&to=now&tz=UTC&sort=endedAt&order=asc&limit=50`
|
||||
- **WHEN** it is opened in a new browser session
|
||||
- **THEN** the status segment shows Closed, the search box reads `demo`, the Egress and Agent feature chips are
|
||||
selected, the range picker shows the 7-day preset in UTC, the table lists rooms started in that range sorted by Ended
|
||||
ascending and shows 50 rows
|
||||
|
||||
#### Scenario: Filter change drops the cursor
|
||||
|
||||
- **GIVEN** the list on its third page
|
||||
- **WHEN** the user selects the Active status
|
||||
- **THEN** the URL has `status=active`, no `cursor`, and Back returns to the third page
|
||||
|
||||
### 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, with `per-node-breakdown` only, Node (the room's `mediaNode`,
|
||||
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: Node column
|
||||
- **GIVEN** `features` with `per-node-breakdown` and a room item with `mediaNode: "mn-a"`
|
||||
- **WHEN** the user shows the Node column from the Columns menu
|
||||
- **THEN** its cell reads `mn-a`, and reads "—" for a room without `mediaNode`
|
||||
|
||||
#### Scenario: No Node column in CE
|
||||
- **GIVEN** `features` without `per-node-breakdown`
|
||||
- **WHEN** the table renders
|
||||
- **THEN** it has no Node column and the Columns menu does not offer one
|
||||
|
||||
#### 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 page states
|
||||
|
||||
Edition: CE. The Rooms page SHALL show skeleton rows equal to the page size on first load only, the shared list empty
|
||||
state "No rooms match" with "Clear filters" and "Widen range" when no row matches, "Room <SID> was not found" with
|
||||
"Clear search" when an exact SID matches nothing, and the error banner with retry and request id while keeping the
|
||||
previous rows when a load fails.
|
||||
|
||||
#### Scenario: Empty filtered list
|
||||
|
||||
- **GIVEN** `?status=active&q=zzz`
|
||||
- **WHEN** the list answers no items
|
||||
- **THEN** the empty state reads "No rooms match" with the hint "Change the filters or widen the time range." and offers
|
||||
"Clear filters" (removes `status`, `q`, `features` and `node`) and "Widen range" (opens the range picker)
|
||||
|
||||
#### Scenario: Failed refresh
|
||||
|
||||
- **GIVEN** rows from the previous load
|
||||
- **WHEN** the next request fails with `500`
|
||||
- **THEN** the rows stay visible under the error banner showing the `X-Request-Id` and a Retry action
|
||||
|
||||
#### Scenario: Unknown SID
|
||||
|
||||
- **GIVEN** the search box reads `RM_missing`
|
||||
- **WHEN** the list answers no items
|
||||
- **THEN** the empty state reads "Room RM_missing was not found" with "Clear search", which removes `q`
|
||||
|
||||
|
||||
### Requirement: Rooms node filter
|
||||
|
||||
Edition: CE. Without the `per-node-breakdown` feature the Rooms page SHALL show no Node select, lock or text about
|
||||
nodes, SHALL ignore `node` and SHALL NOT request the scopes; with it, it renders the Node select the edition provides,
|
||||
which sets `node` to a media node label, and sends it to the list as `mediaNode`.
|
||||
|
||||
#### Scenario: Node filter in CE
|
||||
|
||||
- **GIVEN** `features` without `per-node-breakdown`
|
||||
- **WHEN** the Rooms page renders
|
||||
- **THEN** the filter bar has no Node select and no lock, and no request to `/api/v1/analytics/scopes` is sent
|
||||
|
||||
#### Scenario: Node in a CE URL
|
||||
|
||||
- **GIVEN** `features` without `per-node-breakdown` and `/rooms?node=mn-a`
|
||||
- **WHEN** the page loads
|
||||
- **THEN** the list request carries no `mediaNode`
|
||||
|
||||
#### Scenario: Node filter with the feature
|
||||
|
||||
- **GIVEN** `features` with `per-node-breakdown` and `/rooms?node=mn-a`
|
||||
- **WHEN** the page loads
|
||||
- **THEN** the list request carries `mediaNode=mn-a`
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Media node label
|
||||
Edition: CE and PRO. A server edition MAY give the writer, before the analytics start, a media node label: a name of
|
||||
the machine that stays the same across restarts. With a label the writer SHALL stamp it as `mediaNode` on the rooms and
|
||||
participant sessions it hosts, following their `nodeId` as `mediaStats` does, and key the node scope of its rollups by
|
||||
it; without one it SHALL write no `mediaNode` and no node scope. The CE server sets no label.
|
||||
|
||||
#### Scenario: Room created on a labelled node
|
||||
- **GIVEN** a server with the media node label `mn-a`
|
||||
- **WHEN** it writes `ROOM_CREATED` and `PARTICIPANT_JOINED` of a new room
|
||||
- **THEN** the room and the participant session have its `nodeId` and `mediaNode: "mn-a"`, and the hourly rollup of
|
||||
the bucket has the `global` and `node:mn-a` documents with `rooms.created` incremented
|
||||
|
||||
#### Scenario: Server without a label
|
||||
- **GIVEN** a CE server
|
||||
- **WHEN** it writes `ROOM_CREATED`
|
||||
- **THEN** the room has no `mediaNode` field and the rollups of the bucket get only the `global` document
|
||||
|
||||
#### Scenario: Restart keeps the label
|
||||
- **GIVEN** a server with the label `mn-a` that restarts and gets a new node id
|
||||
- **WHEN** it writes a new room
|
||||
- **THEN** the room has the new `nodeId` and `mediaNode: "mn-a"`, and its rollups go to `node:mn-a`
|
||||
|
||||
#### Scenario: Room touched first by another node
|
||||
- **GIVEN** a room document created by an egress event written by a server with the label `mn-b`
|
||||
- **WHEN** the server with the label `mn-a` that hosts the room writes its `ROOM_CREATED`
|
||||
- **THEN** the room has that server's `nodeId` and `mediaNode: "mn-a"`
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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), `mediaStats` and, when the server has one, its media node label as
|
||||
`mediaNode`, 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`
|
||||
|
||||
#### Scenario: Labelled node
|
||||
- **GIVEN** a server with the media node label `mn-a`
|
||||
- **WHEN** a batch tick runs
|
||||
- **THEN** the node's document has `mediaNode: "mn-a"`, and the document of a server without a label has no
|
||||
`mediaNode` field
|
||||
|
||||
### Requirement: Graceful shutdown
|
||||
Edition: CE and PRO. On a first stop signal the server drains: it takes no new participants and waits for the present
|
||||
ones to leave, whose leave events are written as usual; a second signal closes the rooms with `SERVER_SHUTDOWN`. Once
|
||||
the rooms are closed the server SHALL flush the telemetry stats workers and operation queue into the writer, write
|
||||
every queued event and stat and then set `stoppedAt` on its node document, waiting at most 5 s in all; entries not
|
||||
written in that window SHALL be counted as dropped.
|
||||
|
||||
#### Scenario: Planned restart
|
||||
- **GIVEN** a node with active rooms and participants
|
||||
- **WHEN** it receives `SIGTERM` and then a second `SIGTERM`
|
||||
- **THEN** the database holds the `ROOM_ENDED (ROOM_END_SERVER_SHUTDOWN)` and `PARTICIPANT_LEFT` events and the entities are `closed`/`left` with the final stats flushed
|
||||
|
||||
#### Scenario: Stop recorded
|
||||
- **GIVEN** a node with no rooms
|
||||
- **WHEN** it receives `SIGTERM` and exits
|
||||
- **THEN** its node document has `stoppedAt` no earlier than its last `lastSeenAt` and within 5 s of the signal
|
||||
|
||||
#### Scenario: Killed while draining
|
||||
- **GIVEN** a node with active participants that receives one `SIGTERM`
|
||||
- **WHEN** it is killed before they leave, as `docker-compose down` does after its grace period
|
||||
- **THEN** no end event of those sessions is written, its node document has no `stoppedAt`, and the reconciler closes
|
||||
them as for a crashed node, with `closedBy: "reconciler"`
|
||||
|
||||
#### Scenario: Database unavailable at shutdown
|
||||
- **GIVEN** an unreachable database
|
||||
- **WHEN** the node receives `SIGTERM`
|
||||
- **THEN** the process exits within 5 s of the signal and logs the number of dropped entries
|
||||
|
||||
### 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
|
||||
count, minimum score, poor and lost counts) to the participant, the room and the `global` documents, plus the node
|
||||
scope when the server has a media node label, of `analytics_5min`, `analytics_hourly` and `analytics_daily` of the
|
||||
flush bucket, SHALL add every participant with stats to the 5-minute `participants.connected` count, and SHALL set
|
||||
`lastStatsAt` on the participant and the room to the latest flush time.
|
||||
|
||||
#### Scenario: Bucket attribution
|
||||
- **GIVEN** a flush stamped 10:04:59 UTC with 1 000 upstream bytes on a server with the label `mn-a`
|
||||
- **WHEN** it is stored
|
||||
- **THEN** `analytics_5min` `global:…T10:00:00Z`, `analytics_hourly` `global:…T10:00:00Z` and `analytics_daily` `global:…T00:00:00Z` grow `bytes.up` by 1 000, and so do the `node:mn-a` documents
|
||||
|
||||
#### Scenario: Quality bands
|
||||
- **GIVEN** samples with scores 4.3, 1.5 and 1.0
|
||||
- **WHEN** they are stored
|
||||
- **THEN** the room and the rollups grow `quality.scoreSum` by 6.8, `scoreCount` by 3, `poor` by 2 and `lost` by 1, and `quality.minScore` is 1.0
|
||||
|
||||
#### Scenario: Active participants per five minutes
|
||||
- **GIVEN** stats of three distinct participants flushed twice within the same 5-minute bucket
|
||||
- **WHEN** they are stored
|
||||
- **THEN** the `analytics_5min` document reports 3 active participants
|
||||
|
||||
### Requirement: API call events
|
||||
Edition: CE and PRO. The server SHALL emit an `API_CALL` analytics event with its node id for every Twirp API
|
||||
response of the room, egress, ingress, SIP and agent dispatch services, so that the rollups count API calls and
|
||||
errors per method.
|
||||
|
||||
#### Scenario: API call counted
|
||||
- **GIVEN** analytics enabled on a server with the media node label `n1`
|
||||
- **WHEN** a client calls `RoomService.ListRooms` at 10:20 UTC
|
||||
- **THEN** the hourly and daily documents of `global` and `node:n1` increment `apiCalls.total` and `apiCalls.byMethod.RoomService_ListRooms` by 1 and no raw event is stored
|
||||
|
||||
### Requirement: Ingress sessions by input type in the rollups
|
||||
|
||||
Edition: CE and PRO. Each projected `INGRESS_STARTED` SHALL increment, besides `ingress.sessionsStarted`, the hourly
|
||||
and daily counter `ingress.byInputType.<type>` of its bucket in each of its scopes (`global`, and the node scope of a
|
||||
labelled server), where `<type>` is the lowercase input type of the ingress (`rtmp`, `whip` or `url`), with the same
|
||||
exactly-once rule as every rollup counter.
|
||||
|
||||
#### Scenario: RTMP session started
|
||||
|
||||
- **GIVEN** an `INGRESS_STARTED` of an ingress with `input_type: RTMP_INPUT` at 10:20 UTC on a server with the media
|
||||
node label `n1`
|
||||
- **WHEN** it is projected
|
||||
- **THEN** the hourly and daily documents of `global` and `node:n1` increment `ingress.sessionsStarted` and
|
||||
`ingress.byInputType.rtmp` by 1
|
||||
|
||||
#### Scenario: Replayed start
|
||||
|
||||
- **GIVEN** an `INGRESS_STARTED` already stored by a previous process
|
||||
- **WHEN** it is processed again
|
||||
- **THEN** `ingress.byInputType` is unchanged
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Daily unique identities
|
||||
Edition: CE and PRO servers. After its closures, within its time budget, each run SHALL set `uniqueIdentities` on
|
||||
the daily rollups (global, and per media node label: distinct identities of the STANDARD sessions that joined that UTC
|
||||
day, those of a node scope taken from the sessions with that `mediaNode`) of up to 7 days whose global document lacks
|
||||
it, from 5 minutes after the day closes and only for days inside the entities retention; a node scope without sessions
|
||||
that day gets 0.
|
||||
|
||||
#### Scenario: Day closed
|
||||
- **GIVEN** a global daily document of yesterday without `uniqueIdentities`, STANDARD sessions of `alice` twice and `bob` with `mediaNode: "mn-1"`, and `carol` with `mediaNode: "mn-2"`
|
||||
- **WHEN** a run executes after 00:05 UTC today
|
||||
- **THEN** the global document has `uniqueIdentities: 3`, `node:mn-1` 2 and `node:mn-2` 1
|
||||
|
||||
#### Scenario: Sessions without a label
|
||||
- **GIVEN** a global daily document of yesterday without `uniqueIdentities` and STANDARD sessions of `alice` and `bob` without `mediaNode`
|
||||
- **WHEN** a run executes after 00:05 UTC today
|
||||
- **THEN** the global document has `uniqueIdentities: 2` and no node scope document is written
|
||||
|
||||
#### Scenario: Day just closed
|
||||
- **GIVEN** a run at 00:03 UTC
|
||||
- **WHEN** it evaluates the previous day
|
||||
- **THEN** that day's `uniqueIdentities` is not set yet
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Status endpoint
|
||||
|
||||
Edition: CE. `GET /api/v1/status` SHALL require a bearer token, take no parameters and answer `200` with `generatedAt`
|
||||
(the MongoDB clock), `schema` and `migration` with the same shapes and sources as `GET /api/v1/info`, and
|
||||
`writer { lastSeenAt?, stoppedAt?, batchIntervalMs? }`. A node document is alive while it has no `stoppedAt` and its
|
||||
`lastSeenAt` is within `max(30 s, 1.5 × I + 5 s)`, I being its `batchIntervalMs` (10 s when absent). The response
|
||||
carries `Cache-Control: private, no-store`.
|
||||
|
||||
#### Scenario: Running migration
|
||||
- **GIVEN** two nodes last seen 4 s and 12 s ago, both with `batchIntervalMs: 10000`, writer schema 1 equal to the dashboard's and a migration with `processed` 400000 of `total` 1200000
|
||||
- **WHEN** the client requests `GET /api/v1/status` with a valid token
|
||||
- **THEN** the response is `200`, `writer` is `{ lastSeenAt, batchIntervalMs: 10000 }` with the 4 s old timestamp, `schema.status` is `"ok"` and `migration` is `{ state: "running", progress: { processed: 400000, total: 1200000, startedAt } }`
|
||||
|
||||
#### Scenario: Writer never seen
|
||||
- **GIVEN** empty `nodes` and `schema_meta` collections
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer` is `{}`, `schema.status` is `"uninitialized"` and `migration.state` is `"none"`
|
||||
|
||||
#### Scenario: Server stopped
|
||||
- **GIVEN** one node document with `stoppedAt` 12:00:04 and `lastSeenAt` 12:00:04, and no other node seen since
|
||||
- **WHEN** the client requests the status at 12:00:10
|
||||
- **THEN** `writer.lastSeenAt` and `writer.stoppedAt` are 12:00:04
|
||||
|
||||
#### Scenario: Restarted after a stop
|
||||
- **GIVEN** a node document with `stoppedAt` and a newer node document seen 3 s ago without it
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer` has no `stoppedAt` and `writer.lastSeenAt` is the newer document's
|
||||
|
||||
#### Scenario: One of several nodes stopped
|
||||
- **GIVEN** a node document stopped 2 s ago and another one seen 6 s ago, alive
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer` has no `stoppedAt`
|
||||
|
||||
#### Scenario: A node stopped reporting
|
||||
- **GIVEN** `node-a` seen 4 s ago and `node-b` seen 50 s ago, neither with `stoppedAt`
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer.lastSeenAt` is the 4 s old timestamp and `writer` has no `stoppedAt`
|
||||
|
||||
#### Scenario: Each node judged by its own interval
|
||||
- **GIVEN** `node-a` with `batchIntervalMs: 60000` seen 80 s ago, and `node-b` with `batchIntervalMs: 10000` stopped
|
||||
and last seen 2 minutes ago
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer` has no `stoppedAt` (`node-a` is alive within its 95 s window) and `writer.batchIntervalMs` is
|
||||
`60000`
|
||||
|
||||
#### Scenario: Largest interval of the last hour
|
||||
- **GIVEN** `node-a` with `batchIntervalMs: 10000` seen 5 s ago, `node-b` with `30000` seen 20 minutes ago and `node-c` with `60000` seen 2 hours ago
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer.batchIntervalMs` is `30000`
|
||||
|
||||
#### Scenario: Nodes without a reported interval
|
||||
- **GIVEN** one node seen 25 s ago whose document has no `batchIntervalMs`
|
||||
- **WHEN** the client requests the status
|
||||
- **THEN** `writer` has `lastSeenAt` and no `batchIntervalMs`
|
||||
|
||||
#### Scenario: No token
|
||||
- **WHEN** the client requests `GET /api/v1/status` without `Authorization`
|
||||
- **THEN** the response is `401` with code `UNAUTHORIZED`
|
||||
|
||||
#### Scenario: Writer fields
|
||||
- **WHEN** a client with a bearer token requests `GET /api/v1/status`
|
||||
- **THEN** `writer.lastSeenAt` is the newest `nodes.lastSeenAt`, `writer.stoppedAt` the `stoppedAt` of the document
|
||||
with that `lastSeenAt` when no node document is alive at `generatedAt` (absent otherwise), and
|
||||
`writer.batchIntervalMs` the largest `nodes.batchIntervalMs` among the nodes whose `lastSeenAt` is within one hour of
|
||||
`generatedAt`, absent when none of them reports one
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Overview info tips
|
||||
Edition: CE. The Overview SHALL carry info tips on the live tiles Participants, Publishers and Agents, the Today tiles
|
||||
Participants and Minutes, the Data pipeline card and the Unique identities column of Recent rooms, and on no other tile,
|
||||
card or column of its own.
|
||||
|
||||
#### Scenario: Live tiles
|
||||
- **WHEN** the user opens the info tips of the live tiles Participants, Publishers and Agents
|
||||
- **THEN** they read "Regular participants connected now. Egress, ingress and agents are not counted.", "Participants connected now that publish at least one track: camera, microphone or screen." and "Agent sessions connected now. Agents join rooms as participants of kind Agent but are not counted as participants."
|
||||
|
||||
#### Scenario: Today tiles
|
||||
- **WHEN** the user opens the info tips of the Today tiles Participants and Minutes
|
||||
- **THEN** they read "Participant sessions that started today. Only regular participants count." and "Minutes of the participant sessions that ended today."
|
||||
|
||||
#### Scenario: Data pipeline and recent rooms
|
||||
- **WHEN** the user opens the info tips of the Data pipeline card and of the Recent rooms "Unique identities" header
|
||||
- **THEN** the card's reads "The OpenVidu server writes analytics in batches. This card shows when the last batch arrived." and the header's reads the shared Unique identities text
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: URL query parameters as the single source of truth
|
||||
|
||||
Edition: CE. List and chart state SHALL live only in the query parameters `from`, `to`, `tz`, `granularity`, `scope`,
|
||||
`status`, `kind`, `publisher`, `features`, `node`, `room`, `type`, `inputType`, `q`, `sort`, `order`, `limit`,
|
||||
`cursor` and `page`, read by components as signals with no parallel state. Invalid values fall back to defaults
|
||||
silently. Every change is a new history entry merged into the current parameters; changing a filter, sort, range or
|
||||
`limit` removes `cursor` and `page`.
|
||||
|
||||
#### Scenario: Link reproduces a view
|
||||
- **GIVEN** `/rooms?from=now-7d&to=now&tz=UTC&status=active&sort=uniqueIdentities&order=desc&limit=50`
|
||||
- **WHEN** the URL is opened in a new browser
|
||||
- **THEN** the page shows the same range, filter, sort and page size
|
||||
|
||||
#### Scenario: Invalid value falls back
|
||||
- **GIVEN** `?limit=7&order=sideways`
|
||||
- **WHEN** the page parses the parameters
|
||||
- **THEN** it uses the stored page size and `desc`, without an error page
|
||||
|
||||
#### Scenario: Filter change drops the cursor
|
||||
- **GIVEN** a list on its third page
|
||||
- **WHEN** the status filter changes
|
||||
- **THEN** the new URL has the new status, no `cursor` and no `page`, and Back returns to the third page
|
||||
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# Tasks
|
||||
|
||||
## 1. Writer (openvidu-livekit)
|
||||
|
||||
- [x] 1.1 Add `openviduconfig.MediaNode` (string, default empty) and `analytics.Node.MediaNode`, set in
|
||||
`openvidu/openvidu.go`, and pass it through `nodeProjectionOptions` to the projector. Verify: `go test ./openvidu/...`
|
||||
with the Dockerfile toolchain, and lint.
|
||||
- [x] 1.2 Projection:
|
||||
- stamp `mediaNode` on rooms and participant sessions with the `mediaStats` expressions (first writer, and the
|
||||
host's `ROOM_CREATED`);
|
||||
- make `scopes()` return `node:<label>` only with a label, and the reconciler's room-close increments use the room's
|
||||
`mediaNode`.
|
||||
Verify: projection unit tests for a labelled room, an unlabelled room, a room first touched by another process's
|
||||
egress event, and the rollup increments of both kinds of server.
|
||||
- [x] 1.3 Heartbeat and shutdown:
|
||||
- the heartbeat writes `mediaNode` when set;
|
||||
- after `drain` the writer upserts the node document with `stoppedAt` and `$max lastSeenAt` within the shutdown
|
||||
budget, and not when the queues could not be written.
|
||||
Verify: writer integration tests (graceful stop leaves `stoppedAt`, an unreachable database exits within 5 s
|
||||
without it, a labelled node's document has `mediaNode`).
|
||||
- [x] 1.4 Reconciler daily unique identities group by `mediaNode` and write only label scopes. Verify: reconciler
|
||||
integration test with two labels and with unlabelled sessions (global only).
|
||||
- [x] 1.5 Contract in the writer: `nodes`, `rooms` and `participants` schemas with the new fields, and the rooms
|
||||
index replaced. Verify: `contract_integration_test.go` against the vendored contract from task 2.2.
|
||||
- [x] 1.6 `CHANGELOG-openvidu.md`: the data model line says the heartbeat records a graceful stop. Verify: reviewed
|
||||
in the diff.
|
||||
|
||||
## 2. Typings and contract (dashboard)
|
||||
|
||||
- [x] 2.1 Data model types: `NodeDocument.mediaNode?`, `stoppedAt?`, and `RoomDocument.mediaNode?`,
|
||||
`ParticipantDocument.mediaNode?`; JSON schemas with the label pattern; `indexes.ts` with
|
||||
`mediaNode_status_createdAt_id` (partial) instead of `nodeId_status_createdAt_id`; fixtures with a labelled node, a
|
||||
stopped earlier process and labelled entities. Verify: typings tests and `contract/generate.ts` output committed.
|
||||
- [x] 2.2 API types:
|
||||
- `StatusWriter` without `nodes` and `aliveNodes`, with `stoppedAt?`;
|
||||
- `RoomListItem.mediaNode?`, the participant and agent item `mediaNode?`, and the rooms query `mediaNode` replacing
|
||||
`nodeId`.
|
||||
Verify: `api-contract.test.ts`, and `./dashboard.sh check`.
|
||||
- [x] 2.3 Run `contract/writer-check.sh` against the openvidu-livekit branch of task 1. Verify: the check passes.
|
||||
|
||||
## 3. Data generator and demo
|
||||
|
||||
- [x] 3.1 `--media-nodes`:
|
||||
- labels the three nodes `media-node-10-0-0-1` to `-3` and stamps their rooms and sessions;
|
||||
- keys `--node-rollups` scopes by the label;
|
||||
- adds an earlier stopped process of the first label that hosted some older rooms;
|
||||
- rejects `--node-rollups` without it.
|
||||
The keep-alive refreshes only the current processes. Verify: generator unit tests for the four node documents, the
|
||||
stamps and the scopes, and the "Generated media nodes" scenarios against a generated database.
|
||||
- [x] 3.2 `demo-lib.sh` stops passing `--node-rollups` by default; the generator README documents `--media-nodes`
|
||||
and the new `--node-rollups` rule. Verify: `./dashboard.sh demo ce` starts and its database has no node scope.
|
||||
|
||||
## 4. Backend
|
||||
|
||||
- [x] 4.1 Status service: the writer fold with the alive rule (no `stoppedAt`, within the fresh window) and
|
||||
`stoppedAt` only when no document is alive. Verify: `status-api` scenarios as unit tests of the fold and an
|
||||
integration test of the route.
|
||||
- [x] 4.2 Rooms: `mediaNode` param, mapper, query on the new index, OpenAPI. Verify: `rooms-api` scenarios and the
|
||||
explain-plan tests with `mediaNode` (with and without `q`) on a database generated with `--media-nodes`.
|
||||
- [x] 4.3 Participants and agents: `mediaNode` in the projection, mapper and OpenAPI. Verify: `participants-api`
|
||||
scenario "Session of a labelled node".
|
||||
- [x] 4.4 Overview inputs for editions group active rooms and sessions by `mediaNode`. Verify: overview repository
|
||||
unit test, and `overview-api` tests unchanged.
|
||||
- [x] 4.5 Analytics coverage of a `node:<label>` scope reads the documents with that `mediaNode`. Verify:
|
||||
`analytics-api` coverage scenarios, including "Restarts of one media node", and the existing `explain()` test.
|
||||
|
||||
## 5. Frontend
|
||||
|
||||
- [x] 5.1 Overview:
|
||||
- remove the nodes line;
|
||||
- show the "Server stopped at hh:mm:ss" writer line with the neutral icon;
|
||||
- render the "By node" card only with `per-node-breakdown` and a provided component, without any lock;
|
||||
- Recent rooms at full width otherwise;
|
||||
- the Data pipeline info text.
|
||||
Verify: `overview-page.spec.ts` for the scenarios of `overview-ui` and `ui-info-tips`.
|
||||
- [x] 5.2 Rooms:
|
||||
- `node` URL parameter (dropped without the feature) sent as `mediaNode`;
|
||||
- no Node select or lock without the feature;
|
||||
- the Node column only with the feature, reading `mediaNode`;
|
||||
- "Clear filters" removes `node`.
|
||||
Verify: rooms page and filter specs for the `rooms-ui` scenarios.
|
||||
- [x] 5.3 Participants, agents and participant sessions tables: the Node column only with the feature, reading
|
||||
`mediaNode`. Verify: their table specs.
|
||||
- [x] 5.4 Analytics: no scope selector or lock without the feature. Verify: `analytics-page.spec.ts` for "Nothing per
|
||||
node in CE".
|
||||
- [x] 5.5 No CE view renders the `per-node-breakdown` lock (its copy stays in `FEATURE_COPY`, which covers every
|
||||
`FeatureKey`), and the URL state key is `node`. Verify: `./dashboard.sh check` and the specs of 5.1 to 5.4.
|
||||
|
||||
## 6. End to end, docs and CHANGELOG
|
||||
|
||||
- [x] 6.1 e2e: the CE Overview, Rooms and Analytics pages show nothing about nodes, and Recent rooms is full width.
|
||||
Verify: Playwright specs on the CE demo.
|
||||
- [x] 6.2 `CHANGELOG.md`: the Overview line without "and nodes reporting", and the graceful shutdown line with the
|
||||
recorded stop. Verify: reviewed in the diff.
|
||||
- [x] 6.3 `AGENTS.md` files: the data generator options and any seam whose contract changed (`OVERVIEW_NODES_CARD`,
|
||||
`ROOMS_NODE_FILTER`, `ANALYTICS_SCOPE_SELECT` render only with the feature, the `node` URL key). Verify: reviewed in
|
||||
the diff.
|
||||
- [x] 6.4 Full check: `./dashboard.sh check`, `./dashboard.sh test-integration` and the openvidu-livekit suites, one
|
||||
heavy load at a time. Verify: all green.
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- The PRO change in `dashboard-pro/openspec` builds on the seams and fields of this one; `ce.ref` moves once this
|
||||
change lands in `OpenVidu/openvidu`.
|
||||
- Archive after the owner's review, as usual.
|
||||
Loading…
Reference in New Issue