openvidu/openvidu-dashboard/openspec/specs/egress-api/spec.md

248 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# egress-api Specification
## Purpose
Read access to egresses: the list and its summary numbers, every query backed by an index.
## Requirements
### Requirement: Egresses list endpoint
Edition: CE. `GET /api/v1/egresses` SHALL list egresses with the common query contract (`from`, `to`, `tz`, `limit`
1-100 default 25, opaque `cursor`), the filters `status`, `type`, `room` and `q`, the sort keys `startedAt` (default,
index-backed; `_id` tie-breaker) and the ranked `endedAt` and `duration`, `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 egresses started in the last 24 hours and 5 older ones
- **WHEN** the client requests `GET /api/v1/egresses` with a valid token
- **THEN** the response is `200` with the 25 most recently started egresses of the last 24 hours ordered by
`startedAt desc`, `nextCursor` set, `prevCursor: null` and `total: 40`
#### Scenario: Cursor walk
- **GIVEN** 60 egresses in range and `limit=25`
- **WHEN** the client follows `nextCursor` twice and then `prevCursor` once
- **THEN** the pages hold egresses 1-25, 26-50, 51-60 and 26-50 with no gaps or duplicates
#### Scenario: Unsupported sort key
- **WHEN** the client sends `?sort=durationMs`
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["sort"]`
### Requirement: Egress list item
Edition: CE. Each item of the egresses list SHALL be the public projection of the egress document: identity, room
reference, type and source, lifecycle, times, an outputs summary and an error flag; it SHALL never carry the request,
the output locations or URLs, the status history, `schemaVersion` or `expiresAt`.
#### Scenario: Fields of an item
- **WHEN** an item is read
- **THEN** it has `egressId`, `roomSid?`, `roomName?`, `type` (`room_composite` | `web` | `participant` |
`track_composite` | `track`), `sourceType` (`web` | `sdk`), `status` (`starting` | `active` | `ending` | `complete`
| `failed` | `aborted` | `limit_reached`), `closedBy?` (`event` | `reconciler`), `startedAt`, `endedAt?`,
`durationMs?`, `outputs { kinds, count }`, `hasError` and `errorCode?` (the protocol's error code), and no
`request`, `statusHistory`, error message or output location
#### Scenario: Outputs summary
- **GIVEN** an egress with two file outputs and one stream output
- **WHEN** it is returned
- **THEN** `outputs` is `{ kinds: ["file", "stream"], count: 3 }` with kinds in order of first appearance
#### Scenario: Active egress
- **GIVEN** an egress whose status is `starting`, `active` or `ending`
- **WHEN** it is returned
- **THEN** `endedAt`, `durationMs` and `closedBy` are absent and `hasError` is `false`
#### Scenario: Web egress without room
- **GIVEN** a `web` egress started with an empty room id
- **WHEN** it is returned
- **THEN** `roomSid` and `roomName` are absent and `type` is `web`
### Requirement: Status and type filters
Edition: CE. The egresses list SHALL accept `status` as `all` (default) or a comma-separated subset of the seven
egress statuses, and `type` as a comma-separated subset of the five egress types (absent means all), and SHALL reject
an unknown value in either with `400` `VALIDATION_ERROR`.
#### Scenario: In-flight egresses
- **WHEN** the client sends `?status=starting,active,ending`
- **THEN** every item has one of those three statuses and the time range still applies to `startedAt`
#### Scenario: Failed recordings of one type
- **WHEN** the client sends `?status=failed&type=room_composite,participant`
- **THEN** every item has `status: "failed"` and `type` equal to `room_composite` or `participant`
#### Scenario: Unknown status
- **WHEN** the client sends `?status=running`
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["status"]`
### Requirement: Room filter
Edition: CE. The egresses list SHALL accept `room` as an exact room SID matching `^RM_[A-Za-z0-9]+$`, SHALL combine it
with `status`, `type` and the range, and SHALL reject any other value with `400`.
#### Scenario: Egresses of one room
- **GIVEN** room `RM_abc` with one complete and one active egress in range and other rooms with egresses
- **WHEN** the client sends `?room=RM_abc`
- **THEN** exactly the two egresses of `RM_abc` are returned, ordered by `startedAt desc`
#### Scenario: Malformed room
- **WHEN** the client sends `?room=demo-room`
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["room"]`
### Requirement: Room-name prefix search and egress id lookup
Edition: CE. The egresses list SHALL treat `q` as a case-insensitive prefix over the room name when it has at least
two characters, matched on the egress's own lower-cased room name `roomNameLower` with the range on `startedAt` and
combined with `status`, `type` and `room`, SHALL reject a one-character `q` with `400`, and SHALL treat a value matching
`^EG_[A-Za-z0-9]+$` as an exact egress id lookup that ignores `from`, `to`, `status`, `type`, `room` and `sort` and
returns at most one item.
#### Scenario: Prefix search
- **GIVEN** egresses in rooms `Demo-Room`, `demo-2` and `my-demo` started in range
- **WHEN** the client sends `?q=DE`
- **THEN** `items` holds the egresses of `Demo-Room` and `demo-2` ordered by `startedAt desc`, and none of `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 id outside the range
- **GIVEN** a complete egress `EG_ab12cd` started 40 days ago
- **WHEN** the client sends `?q=EG_ab12cd&from=now-24h&status=active`
- **THEN** the response is `200` with that single egress in `items`, `total: 1` and both cursors `null`
#### Scenario: Prefix matching many rooms
- **GIVEN** 150 rooms whose name starts with `load-`, each with one egress started in range
- **WHEN** the client sends `?q=load-` and walks the cursor
- **THEN** the pages hold the 150 egresses ordered by `startedAt desc` with no gaps or duplicates, `total` is 150, and
no room cap applies
#### Scenario: Room of the egress no longer stored
- **GIVEN** an egress of room `demo-room` started in range whose room document has expired
- **WHEN** the client sends `?q=demo`
- **THEN** the egress is returned, since the match reads the egress document's `roomNameLower`
#### Scenario: Document without the lower-cased room name
- **GIVEN** an egress document written without `roomNameLower`
- **WHEN** the client sends a prefix of its room name
- **THEN** the egress is not returned, and `?q=<its id>` still returns it
### Requirement: Time range on start time
Edition: CE. The egresses list SHALL apply `from`/`to` to `startedAt` for every sort key, order and filter, so that an
egress that started before `from` is not listed, even while it is active, unless it is looked up by exact id.
#### Scenario: Long-running egress outside the range
- **GIVEN** an active egress started 3 days ago
- **WHEN** the client sends `?status=active&from=now-24h&to=now`
- **THEN** the egress is not in `items`, and `?q=<its id>` returns it
#### Scenario: Ranked sort keeps the start range
- **GIVEN** the same active egress
- **WHEN** the client sends `?sort=duration&from=now-24h&to=now`
- **THEN** the egress is not in `items`
### Requirement: Egresses summary endpoint
Edition: CE. `GET /api/v1/egresses/summary` SHALL accept `from`, `to` and `tz` and answer `200` with
`{ active, startedInRange, byType, byEndStatus, totalDurationMs, approximate }`, where `active` counts the current
`starting`, `active` and `ending` egresses and the range figures are exact within `DASHBOARD_LIST_TOTAL_MAX_RANGE`
and hour-aligned sums of the hourly rollups with `approximate: true` beyond it.
#### Scenario: Short range
- **GIVEN** 3 in-flight egresses now, and 120 egresses started in the last 24 hours of which 100 are
`room_composite`, 20 `track`, 110 `complete`, 7 `failed`, 3 still active, totalling 36 000 000 ms of ended duration
- **WHEN** the client requests `GET /api/v1/egresses/summary?from=now-24h&to=now`
- **THEN** the response is `{ active: 3, startedInRange: 120, byType: { room_composite: 100, track: 20 },
byEndStatus: { complete: 110, failed: 7 }, totalDurationMs: 36000000, approximate: false }`
#### Scenario: Long range
- **GIVEN** the default threshold of 7 days
- **WHEN** the client requests `GET /api/v1/egresses/summary?from=now-30d&to=now`
- **THEN** `startedInRange`, `byType`, `byEndStatus` and `totalDurationMs` equal the sums of `egress.started`,
`egress.byType`, `egress.endedByStatus` and `egress.durationMs` of the global hourly rollups whose bucket starts
inside the range, `active` is still the exact current count, and `approximate` is `true`
### Requirement: Index-backed egress queries
Edition: CE and PRO. 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 egresses started 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: Room-name prefix
- **WHEN** the list with `q` set to a room-name prefix and no `room` is explained
- **THEN** the plan uses `roomNameLower_startedAt` with the range on `startedAt`, and no query reads `rooms`
#### Scenario: Ranked sorts over 7 days
- **GIVEN** a database generated at scenario M for 7 days
- **WHEN** the list is explained with `sort=endedAt` and `sort=duration`, with and without `status`, `room` and `q`
- **THEN** each plan scans the range index of its filters (`startedAt_id`, `status_startedAt`, `roomSid_startedAt` or
`roomNameLower_startedAt`), `totalDocsExamined` is at most the egresses started in the range, and the p95 of a page
is within 200 ms
### Requirement: Ranked egress sorts
Edition: CE. The egresses list SHALL accept the ranked sort keys (`api-conventions`) `endedAt`, ranking `endedAt`
(`asOf` when absent), and `duration`, ranking `durationMs` (`asOf − startedAt` when absent), over the egresses started
in the range and matching the filters, when the range is within `DASHBOARD_LIST_TOTAL_MAX_RANGE`, and SHALL answer
`400` with code `SORT_RANGE_TOO_WIDE` beyond it.
#### Scenario: Longest egresses first
- **GIVEN** egresses started in the last 24 hours: one complete after 2 hours, one complete after 10 minutes and one
active that started 45 minutes before `asOf`
- **WHEN** the client sends `GET /api/v1/egresses?sort=duration`
- **THEN** `items` holds the 2-hour egress, the active egress and the 10-minute egress, in that order
#### Scenario: Latest to end first
- **GIVEN** egresses started in range: one ended 3 hours ago, one ended 5 minutes ago and one active
- **WHEN** the client sends `?sort=endedAt&order=desc&status=all`
- **THEN** `items` holds the active egress (ranked at `asOf`), the egress ended 5 minutes ago and the egress ended 3
hours ago, in that order
#### Scenario: Ranked sort beyond the limit
- **GIVEN** the default `DASHBOARD_LIST_TOTAL_MAX_RANGE` of 7 days
- **WHEN** the client sends `?sort=duration&from=now-30d&to=now`
- **THEN** the response is `400` with code `SORT_RANGE_TOO_WIDE`
#### Scenario: Ranked cursor walk
- **GIVEN** 60 egresses started in range, two of them active, and `sort=endedAt&limit=25`
- **WHEN** the client follows `nextCursor` twice
- **THEN** the pages hold egresses 1-25, 26-50 and 51-60 of the ranking at the first page's `asOf`, with no gaps or
duplicates