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

247 lines
12 KiB
Markdown
Raw Normal View History

# 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 and order, so that an egress
that started before `from` and is still active is not listed unless found by `room` within range or 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`): 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